louisabraham/load-bearing

The load-bearing vocabulary of Claude: cluster analysis of GitHub pull requests

97

stars

96

commits

Python

primary language

Sep 10, 2026

updated

louisabraham.github.io/load-bearing/

README

The load-bearing vocabulary of Claude

GitHub pull request descriptions, grouped by the words they are written with rather than by anything they were told to look for: ten ways of writing, and every description belongs to one of them. One of the ten was 0.7% of the corpus at the start of 2025 and is 39% of it by the middle of 2026.

louisabraham.github.io/load-bearing

filewhat it is
fetch_day.pyten requests a day to GitHub's search API, one data/days/YYYY-MM-DD.jsonl. Standard library only.
analyze.pyreads the days, groups them into whole weeks, fits the model, writes analysis.js. Needs numpy, scipy and numba.
index.htmlreads analysis.js. One board, one screen: the figures, the stack, a word's own history, the thousand words. No build step. Open it.
tests/test_page.pywhat the page must keep doing, driven in a real browser. Every test is a bug it once had.
.github/workflows/daily.ymldoes all of the above daily, commits the corpus here, publishes the page to gh-pages.
pip install numpy scipy numba
export GITHUB_TOKEN=$(gh auth token)

python fetch_day.py                  # yesterday, ten requests
python fetch_day.py --backfill 30    # and the last 30 days, if missing
python analyze.py                    # ~50 s on twelve cores
python analyze.py --selftest         # the invariants, on synthetic data
open index.html

pre-commit install                   # optional: ruff and the html formatter, on commit
uv pip install pytest-playwright && pytest tests -q

Current state: 603 collected days, 595 of them in 85 whole weeks (2025-01-06 to 2026-08-17), 461,121 descriptions, 51,079,244 word appearances, 19,798 words above the floor.


1. Why GH Archive cannot be used

The natural source is the public archive of GitHub's event stream, and it stopped working. Since mid-2025 the feed carries almost only PushEvent — a complete hour of 2024-08-12 holds 13,555 IssueCommentEvent against 86 for the same hour of 2026-08-10 — and pushes carry no text, GitHub having removed the commit array in October 2025. The cause is upstream in the Events API: #310 has been open since July 2025 with no maintainer reply, and the same gaps appear in OSSInsight, which reads the API directly. No mirror repairs it, because they all read the same feed.

This was found the hard way. An earlier version of this project, built on the archive, reported load-bearing in 17 documents. That was wrong by a factor of 158: the comments had disappeared from the feed, not from GitHub.

2. How the data is collected

What works is GitHub's search API, for one reason: created: accepts timestamps and not only dates, so a window can be minutes wide and every response carries the full body text.

Ten five-minute windows a day, one drawn from each 2.4 hours of it. The ten starts are drawn to the second and one per block, which is not fussiness: they used to be multiples of five minutes, which is exactly the granularity a cron schedule fires on, so every window opened at an instant when scheduled automation opens pull requests. Blocks also keep the ten from clumping and make it impossible for two to overlap. The draw is seeded on the date, so the whole corpus is reproducible from its dates alone, and each day is one immutable file of about 1.4 MB, committed and never rewritten. The repository's history is the history of the sample.

Two filters go into the query itself and together take a page from 43 usable descriptions to 97: four Apps excluded by name — pull, dependabot, renovate, github-actions, which are 90% of App-authored bodies — and empty bodies excluded, 45% of all pull requests. There is no emptiness qualifier in the search API; requiring any one of ten function words in the body does it exactly.

One honest limit. A day of 2026 holds some 460,000 pull requests matching the query, a five-minute window about 1,250, and a page is a hundred — so a window is truncated to its earliest hundred and this samples rather than enumerates. Nothing here could enumerate a day: the search API returns at most 1,000 results per query however many matched. Uniform placement means this is not a bias in time; the effective width just narrows as GitHub gets busier. A day can also come in short, and 116 of the 603 do — mostly early 2025, when a window did not always fill its page, but two days hold 900 because one of their ten windows returns nothing at all and returns nothing again when asked twice. Those are holes in GitHub's own index, and the day is written short rather than patched.

The corpus and the site live on different branches. A published Pages site may be no larger than 1 GB and the corpus grows 1.4 MB a day, so the daily run commits the day here and pushes only index.html, analysis.js and .nojekyll — a quarter of a megabyte — to gh-pages. The corpus keeps its history because its history is the point; the site does not need one.

3. How the data is cleaned

A word is a run of letters, digits, slashes, hyphens and underscores containing at least one letter, so load-bearing, snake_case, --all-targets and src/main survive whole. No stemming, no n-grams, no stopword list. Links collapse to their domain and HTML tags are taken whole, because splitting on punctuation first put bugbot](https and href among a component's most characteristic words. The em dash is the one deliberate exception to requiring a letter, and it earns it: 0.2 appearances per 10,000 words in early 2024 against 123 in mid-2026. Median description: 65 words.

What gets thrown away. Accounts that are not people, by the shape of the login — anything ending [bot] or -bot, plus copilot — which is 3,784 accounts and 13.2% of collected rows. Identical word sets within a week, because one ordinary human account posted 147 copies of one sentence in a fortnight. And no author may contribute more than three descriptions to a week, which catches mass-produced text from accounts that look human and applies to humans on the same terms, which is why it is a cap and not an exclusion.

One floor on a word, and it counts people

A word is in the vocabulary when 50 distinct accounts have written it. That is the only floor. There were three — 45 appearances, 25 descriptions, 20 accounts — and two of them were doing nothing this one does not do better, because counting appearances cannot tell a shared word from one document written two hundred times:

wordappearancesdescriptionsaccounts
store-path2422422dropped
mq56953336dropped
load-bearing1,011905848kept
seam1,8491,2471,135kept

A word 848 people reached for is a word; a word in 242 descriptions from 2 accounts is one document written 242 times. The number is set on a property of the method, not on the answer: it is the least restrictive floor at which two independent fits agree on half of their top twenty words. Agreement rises with the floor all the way up, so there is no optimum to find — only a rate of return, and a rule that picks a point on it for a stated reason. It costs coverage: 19,798 words of the 2.1 million in the corpus, where the old three floors kept 26,113.

Whole weeks only. Seven days of ten windows is 7,000 descriptions collected and about 5,300 after the filters, so weeks are the same size by construction and need no cap. Part-weeks at either end are dropped outright, which matters daily — collection runs each morning, so the newest week is almost always half-collected, and it is the week everything leans on.

4. What the model is

Each of k ways of writing is a fixed distribution over the vocabulary, and every description is assigned to exactly one of them: the one it is closest to, under the divergence that belongs to word counts.

z_d \;=\; \arg\min_c \; n_d \, \mathrm{KL}(p_d \,\|\, W_c), \qquad W_c \;\propto \sum_{d\,:\,z_d = c} x_d

Each centre is the middle of what it was given — that cluster's KL-centroid. This is k-means with KL in place of squared distance, and the $n_d$ weight is the only trace of counting left in it: a long description pulls its centre harder than a short one. Nothing is ever evaluated as a divergence, because $x_d \cdot \log W_c = -n_d(\mathrm{KL}(p_d | W_c) + H(p_d))$ and $H(p_d)$ does not vary with $c$, so the nearest centre is the largest $x_d \cdot \log W_c$ and the assignment step is one sparse product against the corpus.

There is no t anywhere in that. One set of centres covers the whole window, so the fit has no per-week parameter — nothing that could describe a trend and no freedom to place one. Every curve the page draws is attribution instead: each description placed by its words alone, the weeks counted up afterwards. If a way of writing rises, the rise is in what people wrote, because there is nowhere else for it to be.

5. How the model is trained

Greedy k-means++ under KL, then Lloyd's algorithm to an exact fixed point — stop when no description changes hands, so there is no tolerance to choose and no pass count to guess. Eight fits from eight seeds, and the cheapest is published. The restarts are not there to find a better answer: cost correlates +0.03 with the share the page reports. They are there so the daily job publishes something.

What the page claims is that the component arrived, so two thresholds check it rather than select it: under 2% of the first eight weeks, at or above 20% of the last eight. Picking the biggest component says nothing about whether it arrived. If a batch fails the check, it runs again from fresh seeds; if four batches fail, nothing is published and the job stops. That retry would condition the fit on its own check, which is why the evidence is the rate at which unconditioned fits arrive: 31 of 32 single fits of this corpus, and in 1 of the 32 the leading component came out mixed with another. Where exactly it ends is one fit's answer, and SEED is listed below for that reason.

The selftest runs before every publish and stops the job if it fails: the centres are distributions, the weekly counts are whole numbers that reconstruct each week's total, and a planted way of writing is recovered from synthetic data — 0.000 to 0.350 at the week it was planted, though the model has no way to represent time.

6. How the results are displayed

The component shown is the largest across the last four weeks — a month rather than a week, so the subject of the page does not turn on which of two close components led across one of them. "Still growing" is read off the data rather than typed into the markup: a least-squares line over the last 12 weeks, currently +1.2 points a week.

The words are ranked by counting, not by the fit. The assignment is hard, so every appearance belongs to exactly one component and the counts partition it:

\mathrm{ratio}(v) = \frac{x^{\,\text{in}}_v \big/ N^{\,\text{in}}}
                        {\left(x^{\,\text{out}}_v + \tfrac{1}{2}\,\texttt{MIN\_AUTHORS}\right) \big/ N^{\,\text{out}}}

Each side is divided by its own size, which makes this a ratio of two frequencies rather than of two counts. The pseudo-count in the denominator is the difference between a ranking and a lottery. Without it the top of the list was decided by counts of two to seven in 42 million: a word written three times outside beat one written 158 times, on a difference no larger than its own noise, and both are too rare for anyone to have noticed. Half of MIN_AUTHORS is the fewest appearances a word in this vocabulary can have, halved — the honest prior for written outside less often than can be measured. A word never written outside then scores in proportion to what it was written inside, so the top is ordered by frequency among a component's exclusive words rather than by the accident of a tiny divisor.

The comparison is against everything that is not this component, not against the whole corpus, which would compare the component against itself. load-bearing is written 929 times inside and 82 outside — 39×, the top of the list — and size on the page follows the logarithm of the ratio, from there down to 5× at the thousandth word.

Choosing a word replaces the chart with that word's own history as a rate: appearances per million words written that week. The corpus is not the same size from one week to the next — 380,404 words in the thinnest, 1,406,687 in the fattest — so a curve of raw counts would draw the corpus growing wherever it draws the word arriving.

7. The arbitrary choices

Everything above is either measured or a judgement call. These are the numbers that could have been different, and one of them was chosen by looking at the answer.

constantvaluehow it was chosen
K10chosen on the outcome — see below
SEED0consequential — the seed moves the headline; §5
WINDOW_S300 sconsequential — see below
MIN_AUTHORS50measured — the least restrictive floor at which two fits agree on half their top twenty; §3
MAX_PER_AUTHOR3arbitrary
N_INIT8insurance — a single fit publishes 31 times in 32, so this is margin
LEAD_WINDOW4 weeksjudgement — "a month", to stop one week deciding the subject
LEAD_START, LEAD_END2%, 20%round numbers, wide margins, and they only check
WORDS_LEAD1000arbitrary round number
BOT_SUFFIX, BOT_LOGIN[bot], -bot, copilotjudgement — what a login says is not a person

K = 10 was chosen on the outcome, but from a window rather than a preference. Below ten the component is a mixture: at k = 8 the leading component's own top twenty carries WebKit, nixos and CSS vocabulary in 7 of 32 fits, and the arrival check fails in 10 of them. Above fourteen the component splits until the pieces fall under the 20% the check asks for — 6 of 8 fits arrive at k = 16, 1 of 8 at k = 24. Ten and twelve are both inside the window; ten keeps the title word at the top of the list and reports a fuller share, twelve makes the arrival unanimous. Cost cannot settle it: training cost falls with every added centre and held-out cost is still falling at k = 64, so the corpus would happily support far more clusters than a reader can look at.

WINDOW_S = 300 is not the sample size it looks like. Five minutes was chosen so a window would fit in one page of a hundred results. It does not, in 2025 or 2026, so the sampler truncates rather than enumerates and the width is doing almost nothing — it is a floor that guarantees a full page in the thinnest era of the corpus. WINDOWS = 10 is what sets the sample size, and it is set against a limit: ten pages is 1.4 MB a day, which is about the most the repository can take.

Contributors

louisabraham

72 commits

claude

13 commits

louisabraham/load-bearing

The load-bearing vocabulary of Claude: cluster analysis of GitHub pull requests

97

stars

96

commits

Python

primary language

Sep 10, 2026

updated

louisabraham.github.io/load-bearing/

README

The load-bearing vocabulary of Claude

GitHub pull request descriptions, grouped by the words they are written with rather than by anything they were told to look for: ten ways of writing, and every description belongs to one of them. One of the ten was 0.7% of the corpus at the start of 2025 and is 39% of it by the middle of 2026.

louisabraham.github.io/load-bearing

filewhat it is
fetch_day.pyten requests a day to GitHub's search API, one data/days/YYYY-MM-DD.jsonl. Standard library only.
analyze.pyreads the days, groups them into whole weeks, fits the model, writes analysis.js. Needs numpy, scipy and numba.
index.htmlreads analysis.js. One board, one screen: the figures, the stack, a word's own history, the thousand words. No build step. Open it.
tests/test_page.pywhat the page must keep doing, driven in a real browser. Every test is a bug it once had.
.github/workflows/daily.ymldoes all of the above daily, commits the corpus here, publishes the page to gh-pages.
pip install numpy scipy numba
export GITHUB_TOKEN=$(gh auth token)

python fetch_day.py                  # yesterday, ten requests
python fetch_day.py --backfill 30    # and the last 30 days, if missing
python analyze.py                    # ~50 s on twelve cores
python analyze.py --selftest         # the invariants, on synthetic data
open index.html

pre-commit install                   # optional: ruff and the html formatter, on commit
uv pip install pytest-playwright && pytest tests -q

Current state: 603 collected days, 595 of them in 85 whole weeks (2025-01-06 to 2026-08-17), 461,121 descriptions, 51,079,244 word appearances, 19,798 words above the floor.


1. Why GH Archive cannot be used

The natural source is the public archive of GitHub's event stream, and it stopped working. Since mid-2025 the feed carries almost only PushEvent — a complete hour of 2024-08-12 holds 13,555 IssueCommentEvent against 86 for the same hour of 2026-08-10 — and pushes carry no text, GitHub having removed the commit array in October 2025. The cause is upstream in the Events API: #310 has been open since July 2025 with no maintainer reply, and the same gaps appear in OSSInsight, which reads the API directly. No mirror repairs it, because they all read the same feed.

This was found the hard way. An earlier version of this project, built on the archive, reported load-bearing in 17 documents. That was wrong by a factor of 158: the comments had disappeared from the feed, not from GitHub.

2. How the data is collected

What works is GitHub's search API, for one reason: created: accepts timestamps and not only dates, so a window can be minutes wide and every response carries the full body text.

Ten five-minute windows a day, one drawn from each 2.4 hours of it. The ten starts are drawn to the second and one per block, which is not fussiness: they used to be multiples of five minutes, which is exactly the granularity a cron schedule fires on, so every window opened at an instant when scheduled automation opens pull requests. Blocks also keep the ten from clumping and make it impossible for two to overlap. The draw is seeded on the date, so the whole corpus is reproducible from its dates alone, and each day is one immutable file of about 1.4 MB, committed and never rewritten. The repository's history is the history of the sample.

Two filters go into the query itself and together take a page from 43 usable descriptions to 97: four Apps excluded by name — pull, dependabot, renovate, github-actions, which are 90% of App-authored bodies — and empty bodies excluded, 45% of all pull requests. There is no emptiness qualifier in the search API; requiring any one of ten function words in the body does it exactly.

One honest limit. A day of 2026 holds some 460,000 pull requests matching the query, a five-minute window about 1,250, and a page is a hundred — so a window is truncated to its earliest hundred and this samples rather than enumerates. Nothing here could enumerate a day: the search API returns at most 1,000 results per query however many matched. Uniform placement means this is not a bias in time; the effective width just narrows as GitHub gets busier. A day can also come in short, and 116 of the 603 do — mostly early 2025, when a window did not always fill its page, but two days hold 900 because one of their ten windows returns nothing at all and returns nothing again when asked twice. Those are holes in GitHub's own index, and the day is written short rather than patched.

The corpus and the site live on different branches. A published Pages site may be no larger than 1 GB and the corpus grows 1.4 MB a day, so the daily run commits the day here and pushes only index.html, analysis.js and .nojekyll — a quarter of a megabyte — to gh-pages. The corpus keeps its history because its history is the point; the site does not need one.

3. How the data is cleaned

A word is a run of letters, digits, slashes, hyphens and underscores containing at least one letter, so load-bearing, snake_case, --all-targets and src/main survive whole. No stemming, no n-grams, no stopword list. Links collapse to their domain and HTML tags are taken whole, because splitting on punctuation first put bugbot](https and href among a component's most characteristic words. The em dash is the one deliberate exception to requiring a letter, and it earns it: 0.2 appearances per 10,000 words in early 2024 against 123 in mid-2026. Median description: 65 words.

What gets thrown away. Accounts that are not people, by the shape of the login — anything ending [bot] or -bot, plus copilot — which is 3,784 accounts and 13.2% of collected rows. Identical word sets within a week, because one ordinary human account posted 147 copies of one sentence in a fortnight. And no author may contribute more than three descriptions to a week, which catches mass-produced text from accounts that look human and applies to humans on the same terms, which is why it is a cap and not an exclusion.

One floor on a word, and it counts people

A word is in the vocabulary when 50 distinct accounts have written it. That is the only floor. There were three — 45 appearances, 25 descriptions, 20 accounts — and two of them were doing nothing this one does not do better, because counting appearances cannot tell a shared word from one document written two hundred times:

wordappearancesdescriptionsaccounts
store-path2422422dropped
mq56953336dropped
load-bearing1,011905848kept
seam1,8491,2471,135kept

A word 848 people reached for is a word; a word in 242 descriptions from 2 accounts is one document written 242 times. The number is set on a property of the method, not on the answer: it is the least restrictive floor at which two independent fits agree on half of their top twenty words. Agreement rises with the floor all the way up, so there is no optimum to find — only a rate of return, and a rule that picks a point on it for a stated reason. It costs coverage: 19,798 words of the 2.1 million in the corpus, where the old three floors kept 26,113.

Whole weeks only. Seven days of ten windows is 7,000 descriptions collected and about 5,300 after the filters, so weeks are the same size by construction and need no cap. Part-weeks at either end are dropped outright, which matters daily — collection runs each morning, so the newest week is almost always half-collected, and it is the week everything leans on.

4. What the model is

Each of k ways of writing is a fixed distribution over the vocabulary, and every description is assigned to exactly one of them: the one it is closest to, under the divergence that belongs to word counts.

z_d \;=\; \arg\min_c \; n_d \, \mathrm{KL}(p_d \,\|\, W_c), \qquad W_c \;\propto \sum_{d\,:\,z_d = c} x_d

Each centre is the middle of what it was given — that cluster's KL-centroid. This is k-means with KL in place of squared distance, and the $n_d$ weight is the only trace of counting left in it: a long description pulls its centre harder than a short one. Nothing is ever evaluated as a divergence, because $x_d \cdot \log W_c = -n_d(\mathrm{KL}(p_d | W_c) + H(p_d))$ and $H(p_d)$ does not vary with $c$, so the nearest centre is the largest $x_d \cdot \log W_c$ and the assignment step is one sparse product against the corpus.

There is no t anywhere in that. One set of centres covers the whole window, so the fit has no per-week parameter — nothing that could describe a trend and no freedom to place one. Every curve the page draws is attribution instead: each description placed by its words alone, the weeks counted up afterwards. If a way of writing rises, the rise is in what people wrote, because there is nowhere else for it to be.

5. How the model is trained

Greedy k-means++ under KL, then Lloyd's algorithm to an exact fixed point — stop when no description changes hands, so there is no tolerance to choose and no pass count to guess. Eight fits from eight seeds, and the cheapest is published. The restarts are not there to find a better answer: cost correlates +0.03 with the share the page reports. They are there so the daily job publishes something.

What the page claims is that the component arrived, so two thresholds check it rather than select it: under 2% of the first eight weeks, at or above 20% of the last eight. Picking the biggest component says nothing about whether it arrived. If a batch fails the check, it runs again from fresh seeds; if four batches fail, nothing is published and the job stops. That retry would condition the fit on its own check, which is why the evidence is the rate at which unconditioned fits arrive: 31 of 32 single fits of this corpus, and in 1 of the 32 the leading component came out mixed with another. Where exactly it ends is one fit's answer, and SEED is listed below for that reason.

The selftest runs before every publish and stops the job if it fails: the centres are distributions, the weekly counts are whole numbers that reconstruct each week's total, and a planted way of writing is recovered from synthetic data — 0.000 to 0.350 at the week it was planted, though the model has no way to represent time.

6. How the results are displayed

The component shown is the largest across the last four weeks — a month rather than a week, so the subject of the page does not turn on which of two close components led across one of them. "Still growing" is read off the data rather than typed into the markup: a least-squares line over the last 12 weeks, currently +1.2 points a week.

The words are ranked by counting, not by the fit. The assignment is hard, so every appearance belongs to exactly one component and the counts partition it:

\mathrm{ratio}(v) = \frac{x^{\,\text{in}}_v \big/ N^{\,\text{in}}}
                        {\left(x^{\,\text{out}}_v + \tfrac{1}{2}\,\texttt{MIN\_AUTHORS}\right) \big/ N^{\,\text{out}}}

Each side is divided by its own size, which makes this a ratio of two frequencies rather than of two counts. The pseudo-count in the denominator is the difference between a ranking and a lottery. Without it the top of the list was decided by counts of two to seven in 42 million: a word written three times outside beat one written 158 times, on a difference no larger than its own noise, and both are too rare for anyone to have noticed. Half of MIN_AUTHORS is the fewest appearances a word in this vocabulary can have, halved — the honest prior for written outside less often than can be measured. A word never written outside then scores in proportion to what it was written inside, so the top is ordered by frequency among a component's exclusive words rather than by the accident of a tiny divisor.

The comparison is against everything that is not this component, not against the whole corpus, which would compare the component against itself. load-bearing is written 929 times inside and 82 outside — 39×, the top of the list — and size on the page follows the logarithm of the ratio, from there down to 5× at the thousandth word.

Choosing a word replaces the chart with that word's own history as a rate: appearances per million words written that week. The corpus is not the same size from one week to the next — 380,404 words in the thinnest, 1,406,687 in the fattest — so a curve of raw counts would draw the corpus growing wherever it draws the word arriving.

7. The arbitrary choices

Everything above is either measured or a judgement call. These are the numbers that could have been different, and one of them was chosen by looking at the answer.

constantvaluehow it was chosen
K10chosen on the outcome — see below
SEED0consequential — the seed moves the headline; §5
WINDOW_S300 sconsequential — see below
MIN_AUTHORS50measured — the least restrictive floor at which two fits agree on half their top twenty; §3
MAX_PER_AUTHOR3arbitrary
N_INIT8insurance — a single fit publishes 31 times in 32, so this is margin
LEAD_WINDOW4 weeksjudgement — "a month", to stop one week deciding the subject
LEAD_START, LEAD_END2%, 20%round numbers, wide margins, and they only check
WORDS_LEAD1000arbitrary round number
BOT_SUFFIX, BOT_LOGIN[bot], -bot, copilotjudgement — what a login says is not a person

K = 10 was chosen on the outcome, but from a window rather than a preference. Below ten the component is a mixture: at k = 8 the leading component's own top twenty carries WebKit, nixos and CSS vocabulary in 7 of 32 fits, and the arrival check fails in 10 of them. Above fourteen the component splits until the pieces fall under the 20% the check asks for — 6 of 8 fits arrive at k = 16, 1 of 8 at k = 24. Ten and twelve are both inside the window; ten keeps the title word at the top of the list and reports a fuller share, twelve makes the arrival unanimous. Cost cannot settle it: training cost falls with every added centre and held-out cost is still falling at k = 64, so the corpus would happily support far more clusters than a reader can look at.

WINDOW_S = 300 is not the sample size it looks like. Five minutes was chosen so a window would fit in one page of a hundred results. It does not, in 2025 or 2026, so the sampler truncates rather than enumerates and the width is doing almost nothing — it is a floor that guarantees a full page in the thinnest era of the corpus. WINDOWS = 10 is what sets the sample size, and it is set against a limit: ten pages is 1.4 MB a day, which is about the most the repository can take.

See what people are saying

Contributors

louisabraham

72 commits

claude

13 commits

Languages

Python

56.9%

HTML

43.1%