From 59e52f7b2ba78ffc119f220e51c21de5a5b6012f Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Tue, 12 May 2026 14:23:31 -0400 Subject: [PATCH 1/7] Add local marketplace impact slice --- .gitignore | 6 +- aca_calc/data/enrollment_context_sample.json | 91 +++++++ aca_calc/data/marketplace_platforms_2026.json | 59 +++++ aca_calc/enrollment_context.py | 200 ++++++++++++++ pyproject.toml | 3 + src/App.jsx | 14 +- src/components/HouseholdExplorer.jsx | 2 +- src/data/households/all_households.json | 74 ++++++ src/data/households/cliff_demo.json | 39 +++ src/enrollmentContext.js | 102 ++++++++ src/pages/LocalImpact.css | 247 ++++++++++++++++++ src/pages/LocalImpact.jsx | 226 ++++++++++++++++ tests/test_enrollment_context.py | 45 ++++ 13 files changed, 1104 insertions(+), 4 deletions(-) create mode 100644 aca_calc/data/enrollment_context_sample.json create mode 100644 aca_calc/data/marketplace_platforms_2026.json create mode 100644 aca_calc/enrollment_context.py create mode 100644 src/data/households/all_households.json create mode 100644 src/data/households/cliff_demo.json create mode 100644 src/enrollmentContext.js create mode 100644 src/pages/LocalImpact.css create mode 100644 src/pages/LocalImpact.jsx create mode 100644 tests/test_enrollment_context.py diff --git a/.gitignore b/.gitignore index d111320..4efb19c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,8 +47,12 @@ Thumbs.db # Data/cache data/ .cache/ +!aca_calc/data/ +!aca_calc/data/*.json +!src/data/ +!src/data/** # Node.js / React node_modules/ dist/ -.vite/ \ No newline at end of file +.vite/ diff --git a/aca_calc/data/enrollment_context_sample.json b/aca_calc/data/enrollment_context_sample.json new file mode 100644 index 0000000..1ee5abd --- /dev/null +++ b/aca_calc/data/enrollment_context_sample.json @@ -0,0 +1,91 @@ +{ + "year": 2026, + "source": "CMS 2026 Marketplace Open Enrollment Period County-Level and ZIP-Level Public Use Files", + "source_url": "https://www.cms.gov/data-research/statistics-trends-reports/marketplace-products/2026-marketplace-open-enrollment-period-public-use-files", + "records": [ + { + "state": "TX", + "county": "Travis County", + "county_fips": "48453", + "marketplace_plan_selections": 184355, + "new_consumers": 37178, + "returning_consumers": 147177, + "consumers_with_aptc_or_csr": 162479, + "aptc_consumers": 162349, + "average_premium": 625, + "average_premium_after_aptc": 130, + "average_aptc": 562, + "consumers_premium_after_aptc_lte_10": 83892, + "zip_examples": [ + { + "zip": "78701", + "marketplace_plan_selections": 2614, + "aptc_consumers": 2097, + "average_aptc": 616 + }, + { + "zip": "78702", + "marketplace_plan_selections": 9023, + "aptc_consumers": 8324, + "average_aptc": 480 + } + ] + }, + { + "state": "FL", + "county": "Hillsborough County", + "county_fips": "12057", + "marketplace_plan_selections": 260235, + "new_consumers": 39390, + "returning_consumers": 220845, + "consumers_with_aptc_or_csr": 245436, + "aptc_consumers": 245341, + "average_premium": 757, + "average_premium_after_aptc": 108, + "average_aptc": 688, + "consumers_premium_after_aptc_lte_10": 84635, + "zip_examples": [ + { + "zip": "33602", + "marketplace_plan_selections": 3045, + "aptc_consumers": 2643, + "average_aptc": 659 + }, + { + "zip": "33603", + "marketplace_plan_selections": 4181, + "aptc_consumers": 3928, + "average_aptc": 689 + } + ] + }, + { + "state": "OR", + "county": "Multnomah County", + "county_fips": "41051", + "marketplace_plan_selections": 28221, + "new_consumers": 4343, + "returning_consumers": 23878, + "consumers_with_aptc_or_csr": 15576, + "aptc_consumers": 15466, + "average_premium": 655, + "average_premium_after_aptc": 420, + "average_aptc": 429, + "consumers_premium_after_aptc_lte_10": 294, + "zip_examples": [ + { + "zip": "97034", + "marketplace_plan_selections": 917, + "aptc_consumers": 414, + "average_aptc": 498 + }, + { + "zip": "97035", + "marketplace_plan_selections": 1104, + "aptc_consumers": 579, + "average_aptc": 473 + } + ] + } + ] +} diff --git a/aca_calc/data/marketplace_platforms_2026.json b/aca_calc/data/marketplace_platforms_2026.json new file mode 100644 index 0000000..4e13492 --- /dev/null +++ b/aca_calc/data/marketplace_platforms_2026.json @@ -0,0 +1,59 @@ +{ + "year": 2026, + "healthcare_gov_states": [ + "AK", + "AL", + "AR", + "AZ", + "DE", + "FL", + "HI", + "IA", + "IN", + "KS", + "LA", + "MI", + "MO", + "MS", + "MT", + "NC", + "ND", + "NE", + "NH", + "OH", + "OK", + "OR", + "SC", + "SD", + "TN", + "TX", + "UT", + "WI", + "WV", + "WY" + ], + "state_based_marketplace_states": [ + "CA", + "CO", + "CT", + "DC", + "GA", + "ID", + "IL", + "KY", + "MA", + "MD", + "ME", + "MN", + "NJ", + "NM", + "NV", + "NY", + "PA", + "RI", + "VA", + "VT", + "WA" + ], + "fine_grained_puf_note": "CMS 2026 county- and ZIP-level Marketplace Open Enrollment PUFs cover states that use the HealthCare.gov platform. State-based marketplace states remain selectable, but this first slice shows a state-level fallback because CMS county/ZIP PUF rows are not available for those states." +} diff --git a/aca_calc/enrollment_context.py b/aca_calc/enrollment_context.py new file mode 100644 index 0000000..0ed7d49 --- /dev/null +++ b/aca_calc/enrollment_context.py @@ -0,0 +1,200 @@ +"""CMS Marketplace enrollment context helpers.""" + +from __future__ import annotations + +import json +import re +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any + + +DATA_DIR = Path(__file__).resolve().parent / "data" +DEFAULT_ENROLLMENT_PATH = DATA_DIR / "enrollment_context_sample.json" +DEFAULT_PLATFORM_PATH = DATA_DIR / "marketplace_platforms_2026.json" + + +@dataclass(frozen=True) +class EnrollmentContext: + """Local Marketplace enrollment context for one state/county selection.""" + + year: int + state: str + county: str | None + status: str + marketplace_platform: str + fine_grained_cms_available: bool + county_context_available: bool + message: str + source: str | None = None + source_url: str | None = None + county_fips: str | None = None + marketplace_plan_selections: int | None = None + new_consumers: int | None = None + returning_consumers: int | None = None + consumers_with_aptc_or_csr: int | None = None + aptc_consumers: int | None = None + average_premium: float | None = None + average_premium_after_aptc: float | None = None + average_aptc: float | None = None + consumers_premium_after_aptc_lte_10: int | None = None + + def as_dict(self) -> dict[str, Any]: + """Return a JSON-serializable representation.""" + return asdict(self) + + +def load_marketplace_platforms( + path: str | Path = DEFAULT_PLATFORM_PATH, +) -> dict[str, Any]: + """Load the 2026 platform configuration.""" + with Path(path).open() as f: + return json.load(f) + + +def load_enrollment_records( + path: str | Path = DEFAULT_ENROLLMENT_PATH, +) -> dict[str, Any]: + """Load processed enrollment records. + + The default is a tiny checked-in sample fixture. A future ingestion job can + point this function at processed CMS County/ZIP PUF output with the same + field names. + """ + with Path(path).open() as f: + return json.load(f) + + +def _normalize_state(state: str | None) -> str: + return (state or "").strip().upper() + + +def _normalize_county(county: str | None) -> str: + value = (county or "").strip().casefold() + value = re.sub(r"[^a-z0-9]+", " ", value) + value = re.sub(r"\s+", " ", value).strip() + if value.endswith(" county"): + value = value.removesuffix(" county").strip() + return value + + +def _platform_for_state(state: str, platforms: dict[str, Any]) -> str: + if state in platforms["healthcare_gov_states"]: + return "HealthCare.gov" + if state in platforms["state_based_marketplace_states"]: + return "State-based marketplace" + return "Unknown" + + +def _record_index(records: list[dict[str, Any]]) -> dict[tuple[str, str], dict]: + return { + (record["state"].upper(), _normalize_county(record["county"])): record + for record in records + } + + +def get_enrollment_context( + state: str, + county: str | None = None, + *, + enrollment_path: str | Path = DEFAULT_ENROLLMENT_PATH, + platform_path: str | Path = DEFAULT_PLATFORM_PATH, +) -> EnrollmentContext: + """Return CMS Marketplace enrollment context for a state/county. + + HealthCare.gov-platform states can have county/ZIP PUF detail. State-based + marketplace states return a clear fallback status because CMS does not + publish those county/ZIP PUF rows for them. + """ + platforms = load_marketplace_platforms(platform_path) + enrollment_data = load_enrollment_records(enrollment_path) + state_code = _normalize_state(state) + platform = _platform_for_state(state_code, platforms) + year = enrollment_data.get("year", platforms.get("year", 2026)) + source = enrollment_data.get("source") + source_url = enrollment_data.get("source_url") + + if platform == "Unknown": + return EnrollmentContext( + year=year, + state=state_code, + county=county, + status="unknown_state", + marketplace_platform=platform, + fine_grained_cms_available=False, + county_context_available=False, + message=( + f"{state_code or 'This state'} is not recognized in the " + "2026 Marketplace platform configuration." + ), + source=source, + source_url=source_url, + ) + + if platform == "State-based marketplace": + return EnrollmentContext( + year=year, + state=state_code, + county=county, + status="state_based_marketplace_fallback", + marketplace_platform=platform, + fine_grained_cms_available=False, + county_context_available=False, + message=( + f"{state_code} runs a state-based marketplace. CMS county/ZIP " + "Marketplace PUF detail is not available here, so this view " + "falls back to state-level context only." + ), + source=source, + source_url=source_url, + ) + + index = _record_index(enrollment_data.get("records", [])) + record = index.get((state_code, _normalize_county(county))) + + if record is None: + location = f"{county}, {state_code}" if county else state_code + return EnrollmentContext( + year=year, + state=state_code, + county=county, + status="not_in_sample_fixture", + marketplace_platform=platform, + fine_grained_cms_available=True, + county_context_available=False, + message=( + f"CMS county/ZIP PUF detail is available for {state_code}, " + f"but {location} is not included in this tiny checked-in " + "sample fixture yet." + ), + source=source, + source_url=source_url, + ) + + return EnrollmentContext( + year=year, + state=state_code, + county=record["county"], + status="county_context_available", + marketplace_platform=platform, + fine_grained_cms_available=True, + county_context_available=True, + message=( + f"Fine-grained CMS county enrollment context is available for " + f"{record['county']}, {state_code} in the sample fixture." + ), + source=source, + source_url=source_url, + county_fips=record.get("county_fips"), + marketplace_plan_selections=record.get("marketplace_plan_selections"), + new_consumers=record.get("new_consumers"), + returning_consumers=record.get("returning_consumers"), + consumers_with_aptc_or_csr=record.get("consumers_with_aptc_or_csr"), + aptc_consumers=record.get("aptc_consumers"), + average_premium=record.get("average_premium"), + average_premium_after_aptc=record.get("average_premium_after_aptc"), + average_aptc=record.get("average_aptc"), + consumers_premium_after_aptc_lte_10=record.get( + "consumers_premium_after_aptc_lte_10" + ), + ) diff --git a/pyproject.toml b/pyproject.toml index 3ba0cb0..ab215b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,9 @@ build-backend = "setuptools.build_meta" [tool.setuptools] py-modules = [] +[tool.setuptools.package-data] +aca_calc = ["data/*.json"] + [tool.setuptools.packages.find] exclude = ["archive*", "notebooks*", "tests*"] diff --git a/src/App.jsx b/src/App.jsx index 179e28a..e859100 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,10 +5,11 @@ import CliffComparisonTable from "./components/CliffComparisonTable"; import ContributionScheduleTable from "./components/ContributionScheduleTable"; import ContributionScheduleChart from "./components/ContributionScheduleChart"; import HouseholdExplorer from "./components/HouseholdExplorer"; +import LocalImpact from "./pages/LocalImpact"; import "./App.css"; // Import precomputed household data -import cliffDemoData from "../data/households/cliff_demo.json"; +import cliffDemoData from "./data/households/cliff_demo.json"; // Scroll sections content - background first, then household example, then health programs, then reforms const SECTIONS = [ @@ -165,7 +166,7 @@ Click below to explore how four different households are affected—with details function App() { const [activeSection, setActiveSection] = useState(0); - const [currentPage, setCurrentPage] = useState("main"); // "main", "households", or "calculator" + const [currentPage, setCurrentPage] = useState("main"); const chartRef = useRef(null); // Get current section @@ -249,6 +250,8 @@ function App() { ); + const renderLocalImpactPage = () => ; + return (
@@ -270,12 +273,19 @@ function App() { > Explore Households +
{currentPage === "main" && renderMainPage()} {currentPage === "households" && renderHouseholdsPage()} + {currentPage === "local" && renderLocalImpactPage()}