Skip to content

Coverage

The country × feature × wave coverage/readiness model behind ll.coverage() and the make matrix CLI.

coverage_matrix

Country × Feature × Wave coverage / readiness model.

The importable core behind bench/matrix.py and the ll.coverage() reader. Composes existing machinery — it does not reinvent sanity checks or the declared matrix:

  • axes / declared coverage -> :mod:lsms_library.catalog + Country.waves + Wave.data_scheme (config only; no builds, no auth);
  • per-wave readiness -> :func:diagnostics.load_feature builds the country-level table once; we slice it on the t index level and run the existing :func:diagnostics.is_this_feature_sane per wave.

See .coder/charter-coverage-matrix.md and .coder/ledger/coverage-matrix.md.

Tier ladder (worst→best; each derived from existing machinery only):

===================== ==================================================== tier meaning ===================== ==================================================== n/a no per-wave readiness applies: a country-level-only feature, or a table with no t axis blocked we WANT it, we KNOW where it is, and we CANNOT get it — the acquisition channel is broken (Nepal: NSO site down; Albania 1996). A LIVE gap, not a settled one: visible, out of the work queue, and RE-PROBED on every refresh. See :func:load_blocked. not-asked adjudicated: the instrument genuinely never asked. Closed. Requires evidence (:func:load_verdicts). asked-not-distributed adjudicated: the instrument DID ask, but the shipped extract does not carry the variables. An acquisition problem, not a config one. Requires evidence. absent feature applies to the country but its source is not declared for this wave — and the gap is UN-ADJUDICATED (or adjudicated todo/unsure, which stay open). This is the live queue. declared source present for the wave; readiness not assessed (coverage-only run) dropped source declared for the wave but the wave is missing / empty in the built table (the silent-drop bug class) broken the country-level build raised, or the feature is empty builds wave slice is non-empty but SanityReport.ok is False sane wave slice is non-empty and SanityReport.ok is True — i.e. the automated checks passed. NOT a claim that any human has looked at a single number. blessed sane and a human read the numbers and believed them (git-tracked blessed.csv). Required for any cell feeding published analysis. ===================== ====================================================

Two rules the tiers exist to enforce, both learned the hard way:

  • An unevidenced negative is unfalsifiable, and therefore permanent whether or not it is true. So a closing verdict without evidence is refused outright.
  • A regrade is not a fix. When a change alters how cells are GRADED rather than what they CONTAIN, first inventory what the old grading was catching by accident (see GH #591, #592).

coverage

coverage(refresh: bool | str = False, *, countries=None, features=None, snapshot: Path | None = None) -> pd.DataFrame

Return the country × feature × wave status table.

Parameters:

Name Type Description Default
refresh bool | str
  • False — load the committed snapshot (the last make matrix readiness run). Raises FileNotFoundError with guidance if absent.
  • "coverage" (or True) — recompute the live coverage layer fresh from config (no builds, no auth). Always current; no readiness tiers.
  • "readiness" — recompute the full matrix in-process (heavy; builds every cell). Mostly for tests / one-offs.
False
countries list[str] | None

Restrict a refresh; ignored when reading the snapshot.

None
features list[str] | None

Restrict a refresh; ignored when reading the snapshot.

None
snapshot Path | None

Override the snapshot location (env LSMS_COVERAGE_SNAPSHOT also works).

None

Returns:

Type Description
pandas.DataFrame (columns: country, feature, wave, tier, coverage, n_rows, detail)

build_matrix

build_matrix(countries=None, features=None, *, readiness=True, blessed=None, verdicts=None, blocked=None, log=lambda _m: None) -> pd.DataFrame

Build the full (country × feature × wave) status table.

Parameters:

Name Type Description Default
countries list[str] | None

Restrict the sweep; None = all.

None
features list[str] | None

Restrict the sweep; None = all.

None
readiness bool

If False, emit only the cheap coverage layer (no builds, no auth).

True
blessed set | None

Blessed-cell set; defaults to :func:load_blessed.

None
log callable

Progress sink (one line per country).

lambda _m: None

grade_feature

grade_feature(country_name, feature, waves, co, env, *, avail_by_wave=None, blessed=frozenset(), verdicts=None, blocked=None, readiness=True) -> list[dict]

Per-wave cells for one (country, feature).

Builds the country-level table at most once (readiness=True) and grades each wave by slicing on t and reusing is_this_feature_sane.

avail_by_wave maps wave -> available-feature set; :func:build_matrix precomputes it once per country (it is identical across that country's features). When None we compute it here, keeping the function self-contained for tests / one-off calls.

grade_country_level

grade_country_level(country_name, feature, co, env, *, blessed=frozenset(), verdicts=None, blocked=None, readiness=True) -> dict

Grade a country-level-only feature (panel_ids / updated_ids): wave=None.

load_blessed

load_blessed(path: Path | None = None) -> set[tuple[str, str, str]]

Return the git-tracked set of blessed (country, feature, wave) cells.

CSV with header country,feature,wave (wave blank for country-level features). Missing file → empty set.

save_snapshot

save_snapshot(df: DataFrame, path: Path | None = None, *, merge: bool = True) -> Path

Write the status table to the git-tracked snapshot CSV.

merge=True (default) upserts df's cells into the existing snapshot on the (country, feature, wave) key, leaving every other cell untouched. merge=False replaces the file wholesale.

Why merge is the default

A scoped run (make matrix C="Uganda") grades only some cells. Writing that result wholesale silently destroys every cell it did not grade -- and docs/guide/coverage.md documents exactly that as the "spot refresh" procedure, immediately followed by git add .coder/coverage/latest.csv && git commit. Following the guide verbatim therefore committed a 67-cell snapshot over the authoritative 1849-cell one. (Observed 2026-07-12.)

A partial measurement must never be able to erase a complete one. A full sweep still rewrites everything it grades, so the authoritative run is unaffected; it simply no longer depends on not being scoped.

default_snapshot_path

default_snapshot_path() -> Path

Resolve the status snapshot CSV (env → CWD → repo root).