# How social media forensics measures coordination

> The free checks that read a scraped crowd for coordination use no machine learning. Each one is a deterministic standard-library method, picked because it points the right way where the fancier statistic ranks backwards.

This is the how behind [Social media forensics: the hard part is not fooling yourself](/garden/social-forensics). Every free check is plain Python: string ratios, n-gram counts, sliding time windows, connected components. Nothing embeds, nothing trains, nothing bills. The bias was consistent, take the cheap method that points the right way over the fancy one that ranks backwards or needs data a three-post comment sweep never has.

![The free checks run first. Steps: 1) Collect to a file: Rows on disk, out of the agent's context; 2) themes and duplicates: Phrases by distinct author, text matches clustered; 3) bursts and cadence: Time windows gated on content, posting gaps; 4) accounts and overlap: Shared avatars, cross-post recurrence versus chance; 5) Bill only what is left: Embeddings for paraphrase and image templates](/garden-assets/experience/atom_notes/measuring-coordination/measuring-coordination-flow.svg?v=7b3cc1b114)

Steps 1 to 4 are plain standard library, string ratios and counts. Only the last step spends, embedding what the free checks could not settle.

## How each check works

**Themes are phrases, not topics.** `themes` does not cluster. It ranks every two and three word phrase by the number of distinct authors who used it, keeping only those carried by three or more. Frequency alone would rank one loud account's catchphrase like a shared narrative, so the count that matters is how many independent people said it.

**Clustering happens once, in duplicates.** The only clustering in the free tier finds near-identical comments two ways, whole-text similarity from difflib at 0.7 and a contiguous run of quoted words, then merges the pairs with union-find, plain connected components. Every cluster is labelled cross-author or same-author, because two accounts posting one line is a signal and one account posting it three times is a repeat-poster.

**Bursts flag a chorus, not a crowd.** The hard part is that a coordinated brigade and an ordinary viral moment look identical by every number you can count. Both are dozens of distinct accounts, both sit at the very top of the thread's density, both are statistically extreme. So counting cannot separate them, and no rarity or percentile gate can either, because the innocent case is every bit as extreme as the guilty one. The only thing that tells them apart is content. So `bursts` slides a short window (120 seconds by default) and, for each dense window, checks the closest text match between any two comments in it. If they are all saying different things it labels the window EXPLAINED BY TRAFFIC and moves on, however extreme the count. Many voices arriving together is just traffic; many voices arriving together and saying one line is the signal. A brief that is reworded rather than copied slips past this text check, which is what the billed semantic channel is for.

![A brigade and a viral post side by side. Both windows carry the identical alarming stats, 12 accounts in 90 seconds at the 99th percentile of density. On the left the accounts say different things (haha true, what is the source, not convinced, first) and the window is labelled EXPLAINED BY TRAFFIC. On the right all of them post the identical line (must watch, share this) and the window is labelled COHESIVE, FLAGGED. The numbers are identical, so only the words tell them apart.](/garden-assets/experience/atom_notes/measuring-coordination/bursts-chorus-vs-crowd.svg?v=43e8de236f)

The trap: the density, the account count, the rarity are identical in both. The innocent viral post is just as extreme as the brigade, so only whether they say the same thing can separate them.

**Cadence catches inhuman timing.** It reads timing two ways. Per account, it finds the shortest gap between that account's own comments, and anything under thirty seconds is faster than a person can type, so it points to automation. Per crowd, it sorts comments by hour of day and compares the quietest stretch to the busiest, because a real audience sleeps, so activity that never dips at night is running round the clock. It ignores the burstiness score the research usually reaches for, because on real comments that score points the wrong way: bots post at a steady machine-gun rhythm, which the score reads as calm, while ordinary people post in ragged bursts, which it reads as suspicious.

## Catches and passes

Each check catches one thing and is built to let its innocent look-alike pass.

| Check                 | 🚩 Catches                                           | ✅ Passes                                                        |
| --------------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
| `themes`              | A slogan many distinct accounts repeat               | A phrase one loud account repeats to itself                     |
| `duplicates`          | Two accounts posting one line verbatim               | One account repeating its own line                              |
| `bursts`              | Accounts arriving together and saying the same thing | Accounts arriving together and saying unrelated things          |
| `cadence`             | An account posting seconds apart                     | A person posting in bursts but never faster than a person types |
| `accounts`            | A group of accounts sharing one non-default avatar   | A crowd on the platform default avatar                          |
| `overlap`             | Accounts recurring across posts above chance         | An engaged audience overlapping at or below chance              |
| `semantic` (billed)   | One point reworded or translated across accounts     | Two people making the thread's obvious point                    |
| `thumbnails` (billed) | A reused image file or one shared template           | A press handout that reaches everyone, sitting in the control   |

A concrete pair from a demo run makes the split literal, both on `duplicates`:

* 🚩 **Catch.** Two accounts, `Carol M` and `Dave R`, post the same sentence word for word. Different authors, one line: a cross-author cluster, flagged.
* ✅ **Pass.** `promo_guy` posts that same sentence three times alone. One author repeating itself: hidden as a repeat-poster, not a network.

Machine learning shows up in only two checks, kept in separate scripts. `semantic` embeds text to catch a message reworded or translated across accounts, and `thumbnails` embeds images to catch a shared template. Both run only once the free checks have been read, because on a corpus where accounts paste the same sentence, difflib answers for nothing.

## The bottom line

None of the free checks are clever, and that is the design. Each is a deterministic method I could measure against a real corpus and trust to point the right way, worth more than a fancier statistic that ranks backwards or needs data a comment sweep never carries. The clever, billed methods only earn their place on what the boring ones leave behind.
