83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
|
|
"""Tests for district label normalization at the case-loader boundary.
|
|||
|
|
|
|||
|
|
The processed/cases_by_district_daily.parquet carries both bare ("武昌") and
|
|||
|
|
区-suffixed ("武昌区") spellings of each district (26 labels = 13 districts × 2
|
|||
|
|
spellings), which double-counts in any roll-up. data.case_loader normalizes
|
|||
|
|
these to the canonical 13 区-suffixed names and re-aggregates. These tests pin
|
|||
|
|
that behavior.
|
|||
|
|
"""
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import pandas as pd
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
# Ensure the backend package root is importable at collection time (mirrors the
|
|||
|
|
# sys.path handling other modules rely on once the app is imported).
|
|||
|
|
BACKEND_ROOT = Path(__file__).parent.parent
|
|||
|
|
if str(BACKEND_ROOT) not in sys.path:
|
|||
|
|
sys.path.insert(0, str(BACKEND_ROOT))
|
|||
|
|
|
|||
|
|
from data.case_loader import ( # noqa: E402
|
|||
|
|
CANONICAL_DISTRICTS,
|
|||
|
|
normalize_district,
|
|||
|
|
load_cases_by_district_daily,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
|||
|
|
RAW_PARQUET = PROJECT_ROOT / "processed" / "cases_by_district_daily.parquet"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_normalize_district_known_bare_forms():
|
|||
|
|
"""Every known bare form maps to its canonical 区-suffixed name."""
|
|||
|
|
cases = {
|
|||
|
|
"武昌": "武昌区", "汉阳": "汉阳区", "江岸": "江岸区", "硚口": "硚口区",
|
|||
|
|
"青山": "青山区", "洪山": "洪山区", "东西湖": "东西湖区", "汉南": "汉南区",
|
|||
|
|
"蔡甸": "蔡甸区", "江夏": "江夏区", "黄陂": "黄陂区", "新洲": "新洲区",
|
|||
|
|
"江汉": "江汉区",
|
|||
|
|
}
|
|||
|
|
for bare, canonical in cases.items():
|
|||
|
|
assert normalize_district(bare) == canonical
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_normalize_district_already_suffixed_passes_through():
|
|||
|
|
for d in CANONICAL_DISTRICTS:
|
|||
|
|
assert normalize_district(d) == d
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_canonical_set_is_exactly_thirteen():
|
|||
|
|
assert len(CANONICAL_DISTRICTS) == 13
|
|||
|
|
assert len(set(CANONICAL_DISTRICTS)) == 13
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.skipif(not RAW_PARQUET.exists(), reason="case parquet not present")
|
|||
|
|
def test_loader_collapses_to_thirteen_canonical_districts():
|
|||
|
|
df = load_cases_by_district_daily()
|
|||
|
|
districts = set(df["district"].unique())
|
|||
|
|
|
|||
|
|
# (a) exactly 13 unique districts, all canonical
|
|||
|
|
assert len(districts) == 13, f"expected 13 districts, got {len(districts)}: {sorted(districts)}"
|
|||
|
|
assert districts == set(CANONICAL_DISTRICTS)
|
|||
|
|
|
|||
|
|
# (b) no bare / unsuffixed duplicates remain
|
|||
|
|
for name in districts:
|
|||
|
|
assert name.endswith(("区", "县", "市")), f"unsuffixed district leaked: {name}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.skipif(not RAW_PARQUET.exists(), reason="case parquet not present")
|
|||
|
|
def test_loader_preserves_totals_no_rows_dropped_or_double_counted():
|
|||
|
|
"""Sum integrity: normalized total == raw parquet total."""
|
|||
|
|
raw = pd.read_parquet(RAW_PARQUET)
|
|||
|
|
normalized = load_cases_by_district_daily()
|
|||
|
|
|
|||
|
|
assert int(normalized["total_cases"].sum()) == int(raw["total_cases"].sum())
|
|||
|
|
assert int(normalized["outpatient_count"].sum()) == int(raw["outpatient_count"].sum())
|
|||
|
|
assert int(normalized["inpatient_count"].sum()) == int(raw["inpatient_count"].sum())
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.skipif(not RAW_PARQUET.exists(), reason="case parquet not present")
|
|||
|
|
def test_raw_parquet_actually_has_dirty_labels():
|
|||
|
|
"""Sanity: the raw file really has the 26-label problem we are fixing."""
|
|||
|
|
raw = pd.read_parquet(RAW_PARQUET)
|
|||
|
|
assert raw["district"].nunique() > 13
|