Recurring Archetypes, the Daily Decomposition, and Two Ways to Compute Them
2026-09-03
News is mostly recurrence. A war, a scandal in the capital, another bad turn in the Middle East, a central bank surprising the market: the shape repeats while the names, places and pretexts change. Eigen Times is a newspaper that takes this literally. It treats the archive of news as a matrix, extracts its principal directions—the eigen-news, a few dozen recurring archetypes that explain most of the variance—and prints each day as a decomposition: how much of today projects onto each archetype, what is left over, and, for every story, what is the same as always and what is different this time.
This article describes how Eigen Times was built as a complete system: a 1.09-million-article corpus assembled from public archives (the Guardian, 1999→; Wikipedia’s Current events portal, 2002→; Wikinews, 2004–2026); two independent constructions of the eigenbasis, one in term space (the singular value decomposition of a centred TF-IDF matrix) and one in embedding space (the eigenvectors of the covariance of sentence embeddings), both rotated into named axes; a story layer that clusters each day’s articles and threads them into episodes; the statistics that turn coordinates into journalism—Hotelling’s , the residual energy , a novelty ratio, per-archetype profiles, precedents; and the newspaper itself, a static site of 10,102 daily issues. We compare the two bases against the human-assigned event categories of the Wikipedia portal, which are never used in fitting, and find that both recover them at similar rates (42.4% and 40.0% section purity) while their matched axes correlate at 0.57–0.75. The entire backend is Rust; the pipeline runs on a laptop.
A second edition, added after the first was published, extends the archive to 16.5 million articles across 175 years by ingesting the New York Times archive (September 1851→) and refits the eigenbasis with day-balanced weights, so that every calendar day since 1851 counts the same. The two editions are served side by side with a switch on every page. On the same human-labelled events the 175-year basis recovers categories as well as the modern one (section purity 0.435 against 0.431) while spreading today’s stories across all sixty axes instead of piling most of them onto one. Fitting it forced the computation to stream: the orientation of axes, the term loadings and the story clustering were rewritten to run in flat memory over a corpus fifteen times larger than the first, and the paper reports how.
Newspapers are organised by desk—World, Politics, Business—because that is how newsrooms are organised. Readers, however, experience the news as a small repertoire of recurring situations, and much of what a seasoned reader does when reading a headline is to recognise which situation it is and then look for what departs from the template. “Another regional war” is the recognition; “but this time two great powers are on opposite sides, and it is six weeks before an election in one of them” is the reading.
Eigen Times is an attempt to make that reading mechanical, honest and daily. Its thesis is that the space of news articles has low effective dimension: a few dozen directions carry most of the variance of a quarter-century of reporting, and those directions are the archetypes. Once the archetypes are known, a day’s news can be measured rather than merely categorised. Each story becomes a vector; its projection onto the basis says which archetype it instantiates and at what intensity; the part of the vector the basis cannot reproduce says how much of the story is genuinely new; and the archetype’s own history—its typical profile, its previous episodes—gives the reader the precedents against which “different this time” can be stated as a number before it is stated as a sentence.
The contributions of this article are:
Everything described here is running at eigentimes.com; the source is at github.com/alexy/eigentimes.
Automated journalism has mostly meant automated writing: templated prose generated from structured data. The Associated Press began publishing computer-written earnings reports with Automated Insights in 2014; the Washington Post’s Heliograf covered the 2016 Olympics and elections; Reuters’ Lynx Insight (2018) and Bloomberg’s Cyborg turned market data into draft stories. Carlson [4] framed the phenomenon as “the robotic reporter” and examined how journalists narrated its arrival; Graefe’s Guide to Automated Journalism [10] surveyed the practice and its limits, chiefly that generation from structured data covers only the small fraction of news that arrives structured. Eigen Times sits on the other side of the newsroom: it does not write stories, it reads them, and what it automates is the editorial act of recognition and comparison.
The closest research lineage is Topic Detection and Tracking. The TDT pilot study [1] defined the tasks—segmenting a stream, detecting first stories, tracking known events—that any system reading a news stream must solve, and first-story detection in particular [22] is the ancestor of what this article calls the residual: the part of today that no existing direction explains. Industrial news aggregation, notably the clustering behind Google News [6], solved the daily grouping problem at scale. Eigen Times reuses the TDT decomposition of the stream into stories (a day’s cluster) and episodes (a chain of stories across days), but its unit of recognition is not the event; it is the archetype, a direction that recurs across many events.
Latent Semantic Analysis [8] was the first demonstration that the singular vectors of a term–document matrix are meaningful directions, and it remains the most interpretable: a component is literally a weighted word list. Probabilistic successors—pLSA [13], LDA [2]—recast the directions as topics; dynamic topic models [3] let topics drift through time, which is the model-side counterpart of the refinement problem in §6. Topic models have been applied to news at scale [21], and meme-tracking [19] followed short phrases through the news cycle. Leetaru’s GDELT [18] took the opposite approach and coded the world’s news into a fixed event ontology; its themes and actors are a hand-built basis where Eigen Times wants a learned one.
Sentence embeddings changed what a document vector is. Word vectors [20] made semantic similarity linear; Sentence-BERT [23] produced document vectors in which cosine similarity tracks meaning rather than vocabulary; the BGE family [24] used here is a compact descendant. BERTopic [11] clusters such embeddings and names the clusters with class-based TF-IDF, which is close in spirit to how Eigen Times names its embedding axes (§5.2), except that Eigen Times keeps the axes orthogonal and continuous rather than discrete.
The numerical ingredients are classical. Varimax rotation [16] makes a factor basis readable without changing the subspace. Hotelling’s [14] and the squared prediction error [15] are the standard pair for monitoring a process against a principal-component model—here, monitoring the news against its own history. Randomized SVD [12] makes the term-space decomposition of a million-row sparse matrix a matter of minutes. The Davis–Kahan theorem [7] bounds how far an eigenvector can move when the covariance is perturbed, which is what makes a nightly refinement of the basis provably stable. Archetypal analysis [5] is the non-negative alternative to rotation for producing unipolar axes, and the Kuhn–Munkres assignment [17] matches axes across refinements and across bases.
Wikipedia’s Portal:Current events is a curated daily bulletin, one page per day since January 2002, with each event filed under a section such as Armed conflicts and attacks or Politics and elections. It has been used as a source for multi-document summarisation [9]. Eigen Times uses it twice: as a compact, consistent source of events, and—because the sections are human category labels that were never shown to the fitting procedure—as the held-out check that the eigenvectors recover categories people would choose.
The unit of analysis is a story: the set of articles from one day that report one event. A story has a day , a vector (a sentence embedding of its headline and lede, in the deployed system), a sparse term vector (TF-IDF over its text, , used for naming), and an energy , normalised so the mean weight per day is one. The archive to day is the weighted matrix with .
The eigen-news are the eigenvectors of the weighted covariance of stories:
The top eigenvectors span the news subspace; is the variance of the archive along . With and thin SVD , the same appears with . Both factors are used: says what kind of news a direction is; the story scores say which historical stories are its purest instances, and the extreme entries of column are the canon of archetype .
For choosing we retain eigenvalues above the Marchenko–Pastur noise edge and check against the explained-variance curve; the deployed bases use .
Eigenvectors have no sign, and within nearly degenerate eigenvalue clusters no unique direction. Two deterministic fixes make them printable. Sign: each is oriented so that the skewness of its story scores is positive—news is sparse, and a war is a heavy positive tail on the war axis, so the heavy tail identifies the archetype pole. Named axes: the subspace carries the mathematics, but the printed axes are a varimax rotation inside it, chosen to make the term loadings sparse. Coordinates, residuals and statistics are computed in ; names and canons live in .
Term loadings need no language model. For component , —the TF-IDF matrix weighted by the component’s whitened story scores—ranks the words that characterise the direction. This is what makes a component print as troops · offensive · ceasefire · shelling before anything is named, and it is the same procedure for both bases, so their axes can be compared word for word.
For a story on day , measured against the basis of day :
and are the classical monitoring pair, and they are exactly the two senses of “different this time”: a high is a familiar archetype at unusual intensity or in an unusual mixture; a high is something the archive has no direction for. The decomposition the paper prints is
the archetype part, the mixture part and the residual respectively, with the dominant component chosen in whitened coordinates so that a small-variance archetype can dominate when it is strongly present.
The day vector has spectrum . Against the running mean and variance of over past days—the era statistics—each axis gets a -score: loud, usual, or silent. For each archetype the stories it dominates define a profile: the mean and standard deviation of every coordinate over those stories. “Same as always” is the set of strong profile axes a story also carries; “different this time” is the set of axes on which the story departs from the profile by more than . Precedents are the nearest earlier stories with the same dominant axis, one per episode, by cosine in named-axis coordinates.
| Source | Articles | Span | Text | Access |
|---|---|---|---|---|
| The Guardian (Open Platform) | 954,274 | 1999–2026 | full body | public test key, 720 req/min |
| Wikipedia Current events | 117,720 | 2002–2026 | one-line bulletins with sections | MediaWiki API, 31 days per call |
| Wikinews (English) | 21,172 | 2004–2026 | full text | 47 MB dump; the site closed in 2026, so the archive is complete |
| The New York Times (Archive API) | 15,407,541 | 1851–2026 | headline and abstract (index abstracts before the 1980s, ledes after) | developer key, one month per call, 2,100 months |
The first three sources were the fitted corpus of the first edition (1.09 million articles). The New York Times archive—headlines and abstracts for every item the paper indexed since 18 September 1851—was added for the second edition (§Two editions): 2,100 monthly calls, embedded as they arrived at about 550 articles per second on a laptop, for a corpus of 16,500,781 articles over 63,270 days. Its text changes register along the way: until the 1980s the “abstract” is the index’s terse summary (“Commerce Dept repts mfrs sales at record; illus”), with placeholder headlines for untitled items (“Article 8 – No Title”); afterwards it is the lede paragraph, for a decade prefixed with a literal “LEAD:”. Those conventions are cleaned before the text is tokenised for naming (§Two editions), and only for that source. Every source is read by an adapter split into a pure parser, unit-tested on fixtures, and a rate-limited, retrying fetcher; the whole backfill took one afternoon.
Two properties matter more than volume. Length: archetypes recur on scales of months to decades, so the basis needs decades. Consistency: a change in source mix shows up as a fake change in the world, so backfill and live stream must be embedded with the same pinned model and clustered by the same procedure.
Raw articles are append-only Parquet in a Hive layout, one file per
(source, month), written atomically so that a finished
partition is the resume marker for a backfill. Sail—a Spark-compatible
engine in Rust—is the query engine over the layout (et sql
registers raw/articles as a view with the partition
columns), while the numeric pipeline reads the same files directly with
arrow-rs. Every derived table (embeddings, coordinates, statistics,
stories) is keyed by the model or basis version that produced it, so any
day can be re-materialised against any basis.
Per day, articles from all sources are clustered by single-linkage on cosine similarity (0.72 in embedding space; 0.6 in the diffuser LSA space). A story’s representative is the member with the most text from a full-text source, so headlines come from newspapers rather than bulletin lines; a story’s section is the human section of a bulletin member, if any—newspaper desk names are deliberately not used, since only the bulletin sections are event categories. A story on day continues yesterday’s episode when its centroid agrees with a story of day at cosine ; episodes are chains, so a 600-day war is one episode, and “episode day 7” on a card means the seventh day of that thread. The deployed story layer has 842,278 stories over 10,102 days; 24 February 2022 yields a 62-article, three-source lead story, “Russia launches attack on Ukraine”, on episode day 7—the thread begins with the build-up.
Both constructions produce the same objects—an orthogonal basis, a rotation to named axes, per-article coordinates, term loadings—and everything downstream (stories, spectra, profiles, precedents, the site) is computed identically for both. They differ in what a document is.
Each article is tokenised (lower-cased, stop-listed, Snowball-stemmed), counted into a 50,000-term vocabulary (document frequency between 20 and 30% of the corpus), and encoded as an -normalised row. The corpus matrix has 1,093,166 rows and 149.4 million non-zeros. Its column means are subtracted implicitly——so sparsity is preserved, and the top 60 singular triplets are computed by randomized SVD with 20 oversampling columns and two rounds of subspace iteration, deterministic for a seed. The whole fit—two tokenisation passes over 1.6 GB of text, the SVD, the varimax rotation of the loading matrix—runs in about five minutes on a ten-core laptop.
The rotated axes are word lists by construction, and they are strikingly clean. Among the sixty: bank · financial · credit · rate · loan · Barclays; party · Labour · Tory · conservative · leader · election; police · arrest · officer · protest · investigate · suspect · murder; price · rise · market · oil · inflation · rate · FTSE; Trump · Donald · president · Biden · Republican · immigrant; climate · emissions · carbon · global · warming · temperature; Israel · Palestinian · Gaza · Hamas · Netanyahu; flight · airline · passenger · airport · plane · crash; European · Europe · France · Germany · Brussels · euro; court · judge · rule · case · trial · legal; Russian · Russia · Ukraine · Putin · Moscow · Kremlin; covid · case · vaccine · pandemic · lockdown; house · property · home · mortgage · buyer; protest · demonstrator · activist · arrest; Korea · north · south · nuclear · Kim · Pyongyang; Ireland · northern · Irish · IRA · Sinn · Belfast; Brexit · deal · Johnson · Boris · negotiate · referendum; asylum · immigrant · refugee · seeker · migrant · border; prison · sentence · charge · jail · trial · convict; energy · gas · electricity · nuclear · carbon · renewable; attack · bomb · terrorist · terror · security · Islam; Syrian · Syria · Assad · rebel · Turkey · Damascus. The Gaza axis, the Ukraine axis and the Brexit axis are each one archetype with many episodes.
Each article’s headline and first 300 characters are embedded with a
pinned local model (bge-small-en-v1.5, 96 tokens, CLS
pooling, 384 dimensions, run through ONNX Runtime). Embedding is the
only expensive step: about 45 documents per second per process, so two
shards took roughly four hours for the corpus.
The basis is then exact. A first pass accumulates the sufficient statistics — is —in parallel over partitions and merges them; is eigendecomposed with a dense symmetric solver in milliseconds; the top 60 eigenvectors carry 55% of the variance. A second pass projects every article, orients each component by skewness, and writes coordinates and per article. A third pass re-tokenises the corpus with the LSA vocabulary and computes the term loadings for the embedding components, and a varimax rotation of that matrix gives the named axes.
The rotated embedding axes are mostly topical—Trump · president · Republican · Senate · Obama; covid · vaccine · coronavirus · pandemic · hospital; climate · energy · emissions · carbon · fuel; flight · airline · passenger · airport · aircraft; court · judge · sentence · trial · legal; Russian · Russia · Ukraine · Putin · Kremlin · missile; China · Chinese · Beijing · India · Hong Kong; car · vehicle · driver · road · manufacturer; church · pope · Catholic · bishop · Vatican; Ireland · northern · IRA · Belfast · Sinn—but a few are axes of register that no term matrix can see: think · thing · know · seem · want · question · feel (opinion and interview), resign · leave · down · fall · cut · suspend · cancel (the vocabulary of ending), claim · say · statement · deny · reject · ask (contested assertion). Whether these are archetypes or artefacts is an editorial question the comparison page leaves to the reader.
Both are eigendecompositions of a centred second-moment matrix; the difference is whether the document is a bag of words or a point in a semantic space. The term basis is interpretable by construction and has no model to pin; the embedding basis gives genuinely semantic residuals— measures what the archive cannot mean, not what it cannot spell—and clusters stories far better (in LSA space, same-event articles rarely exceed cosine 0.7; in embedding space the 9/11 day collapses into a 34-article lead story at 0.72). Their eigenvalue spectra differ in shape: LSA’s leading singular values decay slowly, and its top axes are broad (markets, party politics), while the embedding covariance concentrates variance in fewer directions. Section 8 quantifies the agreement.
The basis refines every night from sufficient statistics:
,
,
,
then a fresh eigendecomposition—exact, and cheap at
.
What is not free is identity: eigh returns
eigenvectors in eigenvalue order with arbitrary sign, components swap
places when eigenvalues cross, and within a near-degenerate cluster they
rotate freely. “Archetype 18” must therefore be tracked, not assumed.
Each refinement matches new components to old by the Kuhn–Munkres
assignment on
,
fixes signs so matched inner products are positive, aligns
near-degenerate groups by orthogonal Procrustes, and warm-starts the
varimax rotation from the previous one so the printed axes move
continuously. The Davis–Kahan bound,
in the spectral norm, with
,
shows the daily drift is
:
the basis provably settles. A component with no match above 0.8 is
born and announced; one whose eigenvalue falls below the noise
edge is retired but kept in the registry so old issues still
resolve.
Two ordering rules keep the measurement honest. The day’s issue is measured against the basis of day and only afterwards folded into ; otherwise today’s stories would partly explain themselves and would be biased toward zero. And the basis uses the full archive with uniform weights—the canon of everything that has happened—while recency enters only through the era statistics; a recency-weighted basis would forget a pandemic or a financial crisis before it recurs.
The matching, alignment, statistics and rotation are implemented and
tested in the et-basis crate; the nightly schedule that
drives them is the next piece of work (§10).
The first edition’s basis is the basis of 1999–2026. With the Times archive in place the question is what weight to give 175 years: by article count the modern era dominates (the Guardian alone contributes about as many articles per day as the Times ever did), and by wall-clock time it is fifteen per cent of the record. The second edition takes the second view and weights each article by the inverse of the number of articles published on its day,
so that a day in 1887 and a day in 2024 contribute equally to and (§The mathematics; the covariance formulae are unchanged, with ). The LSA of the second edition is the SVD of with the same weights, so both bases are balanced the same way. The total weight is the number of days with news, 63,270.
Fitting on the Times exposed a register problem that the
modern sources never had. The strongest terms of several rotated axes
were titl · articl · page · front · editori · correct · repts:
the vocabulary of the newspaper’s own index rather than of events. Three
rules, applied only to Times documents before tokenising,
remove it: placeholder headlines lose their boilerplate (“Article 8 – No
Title” is dropped, “Letter to the Editor 3 – No Title” keeps its kind),
the “LEAD:” marker is stripped, and a source-specific stop list drops
the index’s annotation words (illus, por,
repts, mfrs, page, correction,
editorial and the like). The rule lives in one place,
et-cli’s terms module, and is shared by the
LSA fit, the eigen term loadings and the nightly projection; the
embedding axes themselves are untouched by it, since embeddings are
computed from the raw text. After the fix the sixty axes of the 175-year
basis read as news: quarterly earnings; the presidency; deaths and
funerals; Israel and Palestine; sports; the boroughs of New York; the
courts; Congress and taxes; banking; corporate appointments; police and
crime; war and the military; elections; the performing arts.
We measured the two eigenbases on the same 20,471 Wikipedia events of 2023–2026, whose human sections were never used in fitting.
| v1 basis (1999–2026) | v2 · 175 years | |
|---|---|---|
| explained variance of the top 60 axes | 0.550 | 0.545 |
| section purity on 2023–2026 events | 0.431 | 0.435 |
| axes that dominate at least one event | 58 / 60 | 60 / 60 |
| largest axis’s share of the events | 58% | 27% |
| mean principal cosine between the two 60-dimensional spans | 0.875 | |
| principal directions with cosine above 0.9 | 42 / 60 | |
| median of Kuhn–Munkres-matched axes | 0.32 |
The old news did not overwhelm the new. The 175-year basis explains
today’s events exactly as well as the modern one and distributes them
better: the first edition sends 58 per cent of labelled events to a
single armed conflict axis, the second keeps its largest axis
to 27 per cent and separates conflict, disasters, law and crime, and
elections. The last three rows say how: the spans of the two
bases nearly coincide (42 of 60 principal directions agree to within
26°), but the individual axes within that span have rotated, as the
Davis–Kahan bound predicts when eigenvalue gaps (here of order
)
are smaller than the perturbation a new weighting introduces. Since the
newspaper prints axes, not spans, the two editions are different
newspapers of the same news, and the deployed paper carries both: the
root edition is the first, /v2 the second, and every page
has a masthead switch that shows both editions with the current one
marked.
A day page (Figure 1) opens with the spectrum: for each basis, the axes ranked by their share of the day’s energy, each bar coloured when its -score against the era statistics is beyond , followed by a Silence line listing archetypes that are usually present and are not. On 24 February 2022 both bases put russian · russia · ukrain · putin at ; the LSA also flags covid and nation · million · union · strike, the eigenbasis unit · injur · forc · target.
Then the stories, ranked by energy times so that big and unexplained rises. Each card carries the dominant archetype of each basis as a badge, the human section if a bulletin member had one, the counts of articles and sources, the episode day, and novelty, and then one panel per basis: the top four coordinates as bars; Same as always—the strongest axes of the archetype’s profile that this story also carries; Different this time—the axes on which the story departs from the profile by more than , with direction and magnitude; and the five closest precedents from distinct episodes, each a link to its day. For the invasion story both panels honestly report nothing beyond 1.5σ of this archetype’s profile—it is a textbook instance of its archetype—while the precedents differ instructively: the LSA finds the same week’s build-up briefings and 26 February 2014 (Crimea, 0.97), the eigenbasis reaches for “Ukraine: what will happen now?” (2 March 2014) and “Ukrainian president: Russian troops have crossed border” (28 August 2014).
The day after 11 September 2001 (Figure 2) shows the delta panel doing its job. The spectrum lights attack · bomb · terrorist · terror at , european at and market at ; the lead story, 34 articles under the terrorism archetype, is different from that archetype’s profile in one direction—flight · airline · passenger · airport, more than usual—and its precedents are “Airports put on alert for bombs” (December 1999), “US bases in Italy on terror alert” (January 2001) and “Bin Laden is suspect in rocket raids” (November 1999).
Below the stories, The Residual lists the day’s stories with the highest : the ones the basis explains least, which is where the genuinely new hides.
An archetype page (Figure 3) shows the axis’s full term list, the number of stories it dominates, its energy by year as a bar per year—the Ukraine axis has spikes in 2014 and from 2022—the distribution of human sections among its stories (1,084 Armed conflicts and attacks, 529 International relations), and the canon: the purest instances over the years, one per episode, by coordinate.
The comparison page (Figure 4) is where the two algorithms meet: the number of axes, the number of articles with coordinates, the section purity of each basis, the matched-axis table, and every axis of each basis with its terms, story count and majority section. An all-issues page lists every day since 1 January 1999.
Wikipedia’s bulletins carry a human section per event. The sections were never used to fit either basis. For each basis we define an axis’s majority section as the most common section among the labelled stories it dominates, and section purity as the share of labelled stories whose dominant axis’s majority section is their own. The LSA basis scores 42.4%, the embedding eigenbasis 40.0%. Both are far above chance (the largest section is under a quarter of the labelled stories), and both are limited by the same thing: the sections are eight coarse categories while the axes are sixty fine ones, so an Armed conflicts bulletin about Gaza and one about Ukraine sit on different axes that both have Armed conflicts as majority—counted as hits—while Politics and elections is spread across a dozen political axes whose majority sections differ.
Matching the sixty LSA axes to the sixty embedding axes by Kuhn–Munkres on the absolute correlation of story coordinates (a deterministic 100,000-story sample) gives pairs whose correlations run from 0.75 down to about 0.5. The clearest pairs are the ones a reader would draw by hand:
| corr | v0 · LSA axis | v1 · eigen axis |
|---|---|---|
| +0.67 | russian russia ukrain putin ukrainian moscow | russian russia ukrain putin ukrainian moscow |
| +0.67 | parti labour tori conserv leader elect | elect parti labour vote minist tori polit |
| +0.62 | court judg rule case trial legal justic | court judg case sentenc trial law legal |
| +0.62 | trump donald presid biden republican | trump presid donald republican senat |
| +0.61 | climat emiss chang carbon global warm | climat energi emiss gas carbon fuel |
| +0.59 | covid case vaccin pandem coronavirus | covid vaccin health case coronavirus |
| +0.59 | flight airlin passeng airport plane | flight airlin passeng airport plane air |
| +0.57 | european europ franc germani french | european europ franc french germani brexit |
The negative correlations in the full table are also informative: the LSA’s police · arrest · investigate · murder axis pairs at with the embedding’s world · company · market · develop axis—the two bases agree that crime reporting and business reporting are opposite ends of one direction, and disagree only about which end to name.
The term basis cannot see register; the embedding basis cannot help seeing it. The embedding basis clusters events far better and gives a meaningful residual; the term basis produces crisper names and needs no model. On the evidence of §8.1 neither dominates, and the deployed paper prints both.
The first edition was fitted on a laptop in an afternoon, and its
code held what it needed in memory: every article’s embedding and
coordinates for orienting the axes, and the whole document–term matrix,
twice, for the term loadings. At 16.5 million articles that design was
killed by the operating system in the third pass of
et eigen, at about forty gigabytes. The fix was not a
bigger machine but a change of shape: every quantity the fit needs is a
sum over documents, and sums can be accumulated over partitions and
thrown away.
Covariance already was. The weighted mean and covariance are functions of , and , accumulated per partition and merged.
Orientation used to sort every axis’s column of scores to decide its sign; it now accumulates, per axis , the running power sums , , , . The third central moment, which decides the sign, is
and an axis is flipped when , exactly the rule the batch code applied to the full column (a test checks the two agree on every axis, including the merge of two partial accumulators).
Term loadings are for the score matrix ; expanded, , and both terms decompose over row blocks :
Each partition therefore tokenises its own articles into a small
sparse block
,
projects its own embeddings into
,
adds
(a
dense matrix, 24 MB), the column sums of
and the column sums of
into an accumulator, and is dropped. Blocks may arrive in any order and
from any thread; accumulators merge by addition. A property test checks
that the block form equals the whole-matrix operator to
.
The same pass writes the partition’s coordinates and
rows, so the whole of et eigen is now three streaming
passes—covariance, orientation, measurement—and runs on the
16.5-million-article corpus in ten minutes at 0.3 GB resident.
Stories clustered the archive after loading every vector; a day’s clustering depends only on that day and, for episode threading, on the previous day’s stories, so the pass now runs month by month, carrying yesterday’s stories across the boundary and holding one month of articles at a time.
Two things still do not stream. The randomized SVD of the term matrix
needs
in memory—455 million non-zeros for this corpus, a peak of 31 GB—which a
64 GB laptop affords; the block form of the mean and of
would remove that too and is the obvious next step. And the site’s
in-memory index, which serves any day from memory and finds precedents
across the whole archive, holds all 13.4 million stories of the second
edition (about 20 GB). That last fact decides where things run: the
GitHub runner (7 GB, 14 GB of disk) builds the root edition nightly and
never pulls the second edition’s tables; the second edition is
maintained from the laptop by a script that pulls the runner’s fresh
articles and embeddings, measures them against the fitted bases without
refitting, extends the stories, exports the recent issues and deploys
them under /v2.
Two failure-mode decisions were made along the way. Partition reads that used to be swallowed into empty results now fail the fit with the partition’s name, and every partition’s articles are checked id-by-id against its embeddings before they are measured, so a stale or partial embedding can no longer silently shift a basis.
| pass | before | after |
|---|---|---|
| eigen: orientation | all vectors and coordinates in memory (≈30 GB) | power sums per axis (0 GB) |
| eigen: term loadings | two copies of plus (≈20 GB) | one accumulator per thread (24 MB) |
| stories | all vectors and metadata (≈25 GB) | one month at a time |
| LSA: randomized SVD | in memory | unchanged (31 GB peak) |
| site index | all stories | unchanged (20 GB for 13.4 M stories) |
The system is a Rust workspace of ten crates behind one binary,
et:
| crate | role |
|---|---|
et-core |
domain types: Article, Source,
YearMonth, with validating constructors |
et-sources |
archive adapters: pure parsers over fixtures, rate-limited retrying fetchers |
et-store |
Parquet layout, atomic partition writers and readers, a Spark Connect session to Sail |
et-embed |
the pinned embedding model |
et-text |
tokeniser, vocabulary, sparse TF-IDF rows with implicit centring, LSA |
et-basis |
covariance accumulation and eigendecomposition, randomized SVD over a linear-operator trait, varimax by pairwise rotations, Kuhn–Munkres matching, |
et-stories |
daily single-linkage clustering, energy, episode threads |
et-models |
persistence of fitted models as long-format Parquet, so Sail can query them |
et-site |
the in-memory index, the statistics, the pages, an axum server with per-IP rate limiting, and the static export |
et-cli |
et backfill · embed · lsa · eigen · stories · serve · export · sql |
The code follows a normative style guide inherited from the author’s
QueryGraph work: algebraic data types for domain alternatives, a
functional core behind an imperative shell, typed errors at library
boundaries, tests in sibling files, and
fmt/clippy -D warnings/test as
gates. Two implementation notes are worth recording. The varimax
rotation is done by Kaiser’s pairwise plane rotations rather than the
popular SVD-polar fixed-point iteration, because the latter oscillates
on symmetric configurations (it bounced between two mirror-image 30°
states on a test case) while the pairwise method increases the criterion
monotonically. And the sparse operator computes
by per-thread partial accumulation over row chunks, which is what makes
randomized SVD on a 149-million-non-zero matrix a matter of seconds per
product.
The site loads the whole index of the first edition in about three minutes—1.09 million article records, two 60-dimensional coordinate tables, 842,278 stories, the era statistics and profiles—and serves any day from memory; the second edition’s index (16.5 million records, 13,421,090 stories) loads in about one minute from local disk and holds 20 GB. For deployment it is exported as 10,227 static objects (about 2 GB) to a private S3 bucket behind CloudFront, with an AWS WAF rate-based rule (600 requests per IP per five minutes) as the edge rate limit and a Route 53 zone for the domain.
The delta is quantitative only. The design calls for a per-archetype slot schema—belligerents, trigger, external powers, weapons class for a war; office holder, allegation, evidence, stage for a scandal—filled by a language model for today’s story and its precedents and diffed. That layer, and the location of the residual in the story’s own sentences (embedding sentences and taking those most aligned with ), are designed but not built; the deployed paper says how far a story is from its archetype’s profile, not yet what is different in words. Its honesty is a feature—the invasion of Ukraine reads as a textbook instance of its archetype, which is true of its coordinates—but the reader wants the actors.
Names are term lists. Without a language model the axes are named by their loadings; russian · russia · ukrain · putin is unambiguous, unit · injur · forc · target less so.
The basis is the basis of the sources read. Three Anglophone sources make “a bad turn in the Middle East” an archetype and “a bad turn in the Sahel” a residual. Per-source contributions to each axis should be tracked and printed.
The nightly loop measures but does not yet refit. Both editions are rebuilt every night—the root edition by a GitHub Actions runner, the second from a laptop—by refreshing the sources, embedding and measuring the new articles against the fitted bases, extending the stories and exporting the recent issues. The refinement mathematics is implemented and tested but not scheduled: the colophon reporting drift against its bound, the RSS and Google News adapters, and the Time Machine (any date against any basis version) are the next build.
Register axes. Whether the embedding basis’s opinion, ending and assertion axes are archetypes of news or artefacts of the embedding is unresolved; the comparison page is the instrument for deciding. The 175-year basis adds a variant of the question: obituaries, marriage announcements and letters to the editor are recurring newspaper content and form axes of their own, which a reader of today’s page may or may not want counted as news.
Eigen Times reads a quarter-century of news as a matrix and prints each day as coordinates. The archetypes it finds—by two unrelated constructions—are the ones a reader would name, and its statistics give “how is it different this time” a number before a sentence. The system is small: one binary, a laptop, an afternoon of backfill, five minutes for one basis and a few hours for the other. What it lacks is the language layer that turns a excess on flight · airline · passenger · airport into the sentence a newspaper would print. That is the next issue.
The system and this article were built in a single day’s pair-programming session with Claude (Fable 5) in Claude Code, from a one-paragraph brief to the deployed site. The Guardian Open Platform, the Wikimedia Foundation and the Wikinews community made the corpus possible.