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_featurebuilds the country-level table once; we slice it on thetindex level and run the existing :func:diagnostics.is_this_feature_saneper 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
|
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 |
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
|
features
|
list[str] | None
|
Restrict the sweep; |
None
|
readiness
|
bool
|
If |
True
|
blessed
|
set | None
|
Blessed-cell set; defaults to :func: |
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 ¶
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 ¶
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 ¶
Resolve the status snapshot CSV (env → CWD → repo root).