# Methodology — Five mayors, one newspaper, the first six months

This tool compares how much *The New York Times* wrote about each new New York
City mayor during the **first 185 days** of the term, and the tone of that
coverage. It is narrow so that the comparison is like for like.

## Why only The New York Times?

We considered every free data source for a five-mayor comparison. Only one
indexes coverage *uniformly* across all five terms:

| Source | What it covers | Spans all 5 terms (1994→2026)? |
|---|---|---|
| **NYT Article Search API** | The New York Times, indexed since 1851 | **Yes — uniform** |
| GDELT 2.0 | TV + global online news | No — 2015+ only |
| Internet Archive TV News Archive | TV captions | No — 2009+ only |
| ProQuest / Factiva / NewsBank | Post, Daily News, Newsday, WaPo, etc. | Licensed; no API; not reproducible |

A tool that claimed to compare TV and multi-paper coverage back to Giuliani
(1994) would be inventing data for the pre-2009 era, so this one doesn't. The Times
is one outlet — national, agenda-setting and consistently indexed — so it is a
**reliable single yardstick**; it does not stand in for "all coverage." Using
one outlet is what allows a like-for-like comparison across 32 years.

## The mayors and the clock

All five were inaugurated on **January 1**, so "the same point in the term" is
calendar day 1 through day 185:

| Mayor | Term start | Day-185 window ends |
|---|---|---|
| Rudolph Giuliani | 1994-01-01 | 1994-07-04 |
| Michael Bloomberg | 2002-01-01 | 2002-07-04 |
| Bill de Blasio | 2014-01-01 | 2014-07-04 |
| Eric Adams | 2022-01-01 | 2022-07-04 |
| Zohran Mamdani | 2026-01-01 | 2026-07-04 |

Mamdani's window ends on today's date (2026-07-04), so his six months are
complete for this comparison.

## How we count coverage

We searched the Article Search API for each mayor's name (`q` parameter),
bucketed by week across the 185-day window (`scripts/fetch_data.py`).

**Why a plain name search.** We tried two other methods first, and both failed
in the current API version:

1. **Person-facet filter** (`fq=persons:("Bloomberg, Michael R")`). The Times
   tags every article with disambiguated people, and we confirmed the exact tag
   for each mayor. But the API's `fq` filter returns **zero** results for
   `persons:` (and for `headline:`) in the current version — server-side facet
   filtering is effectively non-functional. Verified by direct test.
2. **Exact-name phrase** (`"Michael Bloomberg"`). This *undercounts* massively,
   because the Times wrote "Mr. Bloomberg" or "Michael R. Bloomberg" — the exact
   phrase "Michael Bloomberg" appeared in just **1** article in his first two
   weeks, versus 175 that mention "Bloomberg."

So the base count is **articles matching the surname** (`Giuliani`, `Bloomberg`,
`"de Blasio"`, `Adams`, `Mamdani`). An article counts once, regardless of how
many times the name appears. It works the same way across
all 32 years.

### Two metrics: "all mentions" and "featured"

The surname search has a known ambiguity for two names:
- **Bloomberg** also matches Bloomberg L.P./Bloomberg News (e.g. a "World
  Business Briefing").
- **Adams** also matches unrelated people named Adams.

The other three surnames (Giuliani, de Blasio, Mamdani) are effectively clean.
We also can't perfectly separate a substantive story about the mayor from an
article that mentions his name once, deep in the body — because the API matches
the whole article text but returns only headline + lead for us to inspect.

So we report **two labeled numbers** for every mayor:

- **All mentions** = every article where the surname appears anywhere. Complete
  and directly measured, but inflated for Bloomberg/Adams by wrong-entity matches
  and padded for everyone by trivial body mentions.
- **Featured** = articles where the mayor is front-and-center in the headline or
  lead. We draw a **sample** of each mayor's matched articles (up to 40 per month
  across the six months = 240 total) and have Claude Haiku read the headline +
  lead and judge whether the mayor is *featured* (about him; his administration,
  appointments, decisions, statements, conduct or the race) versus not evident /
  a different entity. **Featured total = all mentions × featured share.**

Because Claude sees only the headline and lead, "featured" excludes
deep-body mentions it cannot see. It is therefore a **strict lower bound** on
coverage, applied uniformly. The true amount of coverage of
each mayor sits between "featured" and "all mentions." The *same* strict test is applied to all five mayors. Every per-
mayor featured share is shown on the page and every label is stored in
`data/sentiment.json`.

**Known biases.** (1) NYT digitization/tagging practices changed over 32 years.
(2) The featured share is measured on a sample and applied to the whole window.
(3) Wire/brief items are included in "all mentions." (4) "Featured" undercounts
body-only mentions of a mayor.

## Sentiment (tone of coverage)

- In the same Claude Haiku pass, each **featured** article's **headline + lead
  paragraph** is labeled for tone *toward the mayor* — positive / negative /
  neutral — with a confidence score.
- We classify headline + lead only — the fields the API returns reliably for
  every era. This is a coarse signal, applied identically to all five mayors.
- **Tone of coverage ≠ the mayor's standing.** A bad event reported straight is
  neutral. Sentiment here measures how the Times *framed* the mayor at the top
  of the article, nothing more.
- Temperature 0 for stable re-runs. Every label ships in `data/sentiment.json`
  so any classification can be audited.

## Confidence

- **"All mentions" volume, within scope: high** — one outlet, transparent
  surname counts.
- **Cross-era comparison: medium** — 32 years of indexing drift; "featured" is a
  strict AI-judged lower bound applied uniformly.
- **Sentiment: medium-low** — headline-level, model-judged, coarse by design.

## Reproduce it

```
export NYT_API_KEY=...                    # free at developer.nytimes.com
python3 scripts/fetch_data.py             # -> data/volume.json + data/sample.json
ANTHROPIC_API_KEY=$(security find-generic-password -s ANTHROPIC_API_KEY -w) \
  python3 scripts/classify_sentiment.py   # -> data/sentiment.json
```

Data pulled: 2026-07-04. NYT free tier: ~5 requests/min, 500/day.
