The first edition of Eigen Times was fitted on 1.09 million articles, and its code could afford to hold what it needed: every article’s embedding and coordinates while the axes were oriented, and the whole document–term matrix while the axes were named. The second edition ingested the New York Times archive back to 1851 and multiplied the corpus by fifteen, to 16.5 million articles. The first attempt to fit it was killed by the operating system at about forty gigabytes. What follows is the arithmetic that let the same computation run in a fraction of a gigabyte—not a bigger machine, but a change of shape. The principle is one sentence: every quantity the fit needs is a sum over documents, and a sum can be taken in blocks.
Before the sums, the weights. By article count the modern era dominates the archive—the Guardian alone publishes about as many articles a day as the Times index ever recorded—while by the calendar it is fifteen per cent of 175 years. Let index articles and a calendar day. Write for article ’s day and for the number of eligible articles on that day. The second edition assigns article the inverse-count weight . A sum constrained by includes only articles from the chosen day:
Every populated day therefore contributes one unit to the fit: a day in 1887 with 40 index entries and a day in 2024 with 900 articles count the same. The arithmetic is and . An article from the smaller day has greater individual weight, while both days have equal total influence.
The notebooks make this distinction visible using a synthetic coordinate that is zero for each of the 40 first-day articles and one for each of the 900 second-day articles. Its article-weighted mean is . Its day-balanced mean is . Neither is an arithmetic error: they answer different questions about what counts as one unit of influence. Total weight is the number of represented days, 63,270. The covariance formulas retain their shapes; the diagonal entries of change. For term-space fitting, let be the TF‑IDF mean under these same article weights. Let be the diagonal matrix of the nonnegative square roots of the article weights. With of shape and of length , the balanced LSA fit decomposes . Its mean is distinct from the unweighted naming mean below.
Why multiply centred rows by square-root weights? The covariance calculation subsequently multiplies a row by its transpose. The two factors each contribute , whose product is . Using the full weight on both sides instead would produce and answer a different weighting question. Weighting, centering, and normalization each have a separate role; none can be omitted merely because the matrix shapes still match.
Split the article rows into disjoint blocks indexed by . In Eigen Times, a block is one source’s articles for one month; there are about three thousand. The notation means article row belongs to block . Let be any fixed rule converting a row into a scalar, vector, or matrix contribution of the same shape for every row. Then
Read the inner sum first: calculate the contributions from articles in one block and add them. The outer sum adds the resulting block totals. Every article must appear in exactly one block; missing or duplicated rows would change the answer. Once a block’s contribution has been added, its article rows can be discarded from memory. Here the function is unrelated to the eigengap scalar in §10.
For covariance the contributions are already known: an article adds its weight to , its weighted coordinate column to , and its weighted outer-product matrix to . Each block produces a scalar, a column, and a matrix. Add like-shaped block summaries, then compute the mean and covariance . Do not average block means equally unless their total weights are equal; a block with more weight must contribute proportionally more.
A thread is a worker executing part of the computation; an accumulator stores its partial total. Workers can merge partial sums because addition commutes in exact arithmetic. Floating-point arithmetic can introduce small order-dependent rounding differences. Memory is bounded by the largest block plus these accumulators. The covariance’s are sufficient summaries for reconstructing that mean and covariance; the phrase does not claim they retain all information about the articles. Two other stages needed the same treatment.
Fix one raw axis and let be article ’s score on it. Let count the articles in this orientation pass and let be their unweighted mean score; an overbar denotes a mean. A central moment averages powers of deviations from a mean. The second central moment uses squares and measures spread; the third uses cubes and retains a sign. Cubes of negative deviations are negative, so a long negative tail can outweigh shorter positive deviations. Define the third central moment by
where . The orientation rule flips the axis when is negative. Flipping all scores reverses their centred cubes’ signs while preserving squared lengths. If the third moment is zero, this particular rule supplies no preference between the two signs. The old pass retained 16.5 million scores for each axis. Instead define three power sums, sums of first, second, and third powers:
These scalar are distinct from the naming-score matrix . For this one axis, abbreviate as . To see why these summaries suffice, first expand one squared deviation:
Averaging the first term gives . In the second, is constant over articles and the average score is itself , so the average is . The last term averages to . Adding them gives the second central moment, denoted :
The cube has four terms:
Average each term in order. They become , , , and . Combining the last two gives
The notebook reuses the synthetic orientation scores . They give , , , and , hence . Substitution gives
The negative third moment triggers a sign flip. The two notebooks verify these values both from the original centred scores and from independently accumulated blocks.
Four numbers per axis— and three sums—replace the column. Each partition adds its documents’ powers to an accumulator; accumulators merge by adding those four numbers. These polynomial identities are exact, but subtracting nearly equal floating-point values can lose precision when the mean is large compared with the spread. The reported tests compare streaming and batch orientation signs, including merged partial accumulators; they do not make that numerical risk disappear for arbitrary data.
Recall that has article rows and TF‑IDF columns, is their unweighted mean, and has the matching rows of whitened raw scores. Its row order must agree with . A cross-product multiplies one table’s transposed columns against another table’s columns. Here it measures which terms co-occur with large axis scores. It is a numerical association, not evidence that a term causes a story or uniquely owns an axis.
Let index vocabulary terms in this subsection and index raw score axes. Write for entry of the mean column . Article contributes its centred term value, , multiplied by its score . Add over articles to obtain entry of the term-loading matrix :
The mean term value is constant over articles. Distributing multiplication over subtraction therefore yields the same entry as
In matrix form, with the column of ones, these are the raw term–score product and its centering correction:
Check the correction’s shape: is a row of score sums; multiplying the term-mean column by it gives a outer product. A fitted score mean can be zero under one weighting convention but not another. Keeping the centering term explicit avoids assuming a cancellation the naming pass does not guarantee.
Let count articles in block , and let and contain its matching rows, with shapes and . Write for the column of ones, while without a subscript has length . Then
Read these formulas as three accumulation tasks. First multiply matching term and score rows to update the cross-product. Second add term columns to update their sums. Third add score columns to update their sums. Also retain the total row count so the final term sums can be divided by . The final mean and centering correction are computed after the block summaries have been combined, using the same unweighted naming convention as the full matrix.
Each block therefore contributes three small numerical arrays: a matrix (, twenty-four megabytes), the column sums of its term rows ( numbers) and the column sums of its scores ( numbers). The block itself—the sparse rows of one month’s articles and their scores—is built, folded in, and dropped (Figure 18). The full , which has 455 million non-zero entries for the 175-year corpus and was being held twice by the batch code, never exists in the naming pass. At the end the three accumulated pieces are combined by the formula above; a property test checks that the result equals the whole-matrix operator applied to the concatenated blocks, to .
The same pass over a block also projects its embeddings to coordinates and computes , and for its documents, writing them to the partition’s own table. The whole eigen fit is thus three passes over the archive—covariance, orientation, measurement—each of which reads one partition at a time. On 16.5 million articles it runs in ten minutes and stays at 0.3 GB of memory.
Clustering into stories (chapter The clustering thresholds) is done within a day, and threading into episodes links a day’s stories to the previous day’s. For the clustering and episode-linking calculation, the dependency of day is on that day’s articles and the previous day’s story state. The carried state includes centroids and episode identities, which summarize the thread inherited from earlier days. It is not necessary to reload every earlier article. Other tasks, such as finding long-range precedents, have broader dependencies and are not covered by this particular memory reduction. The batch code loaded every article’s vector and title before starting; the streaming version walks the months in order, loads one month’s articles, clusters its days in sequence, carries the last day’s stories across the month boundary, writes the month’s stories and drops the month. Memory is one month of articles instead of 175 years of them. The month boundary is only a storage boundary: preserve the previous day’s state when crossing it, or an ongoing episode would be split accidentally. The notebook processes the same six synthetic days once as a full batch and once as two blocks, checking that the resulting episode identities agree.
A matrix’s shape tells us how many numerical entries it contains. A dense accumulator has entries. With eight-byte floating-point values, the loading accumulator needs bytes, or 24 decimal megabytes. Each simultaneous worker needs its own accumulator unless the implementation arranges shared updates. A decimal megabyte is one million bytes; a gibibyte uses bytes, so unit labels matter when comparing reported memory.
This size calculation concerns numerical entries only. Sparse indices, object metadata, temporary arrays, and stored text consume additional memory. A measured process total cannot generally be obtained by multiplying a single matrix shape.
Two stages remain large. Randomized LSA uses a working matrix , where is the chosen working width, normally larger than the final retained rank. Products such as and are the range-finding operations described in §5, with the required centering and weighting corrections. The first product can be emitted by row block; the second is a sum of block cross-products. This avoids requiring all of at once, although working score matrices also need a storage plan. In the reported edition the 455-million-nonzero matrix remains in memory: 31 GB on a 64 GB laptop. The block form is a next step, not a claimed implementation. The other large object is the site index, which retains story centroids and profiles for precedents: about 20 GB for 13.4 million stories. These retained objects determine the second edition’s build-machine requirement.
| pass | before | after |
|---|---|---|
| eigen: orientation | every vector and coordinate (≈30 GB) | four power sums per axis |
| eigen: term loadings | twice, plus (≈20 GB) | one accumulator per thread (24 MB) |
| stories | every vector and title (≈25 GB) | one month at a time |
| LSA: randomized SVD | in memory (31 GB) | unchanged |
| site index | every story (20 GB) | unchanged |