diff --git a/changelog.d/8089.fixed.md b/changelog.d/8089.fixed.md new file mode 100644 index 000000000..7828e3505 --- /dev/null +++ b/changelog.d/8089.fixed.md @@ -0,0 +1 @@ +Added other health insurance premiums as the non-Medicare premium category not covered by modeled Marketplace, CHIP, or Medicaid premiums. diff --git a/policyengine_us_data/calibration/target_config.yaml b/policyengine_us_data/calibration/target_config.yaml index 154dcf878..4b0702898 100644 --- a/policyengine_us_data/calibration/target_config.yaml +++ b/policyengine_us_data/calibration/target_config.yaml @@ -114,7 +114,7 @@ include: geo_level: national - variable: medicaid geo_level: national - - variable: medicare_part_b_premiums + - variable: medicare_part_b_premium geo_level: national - variable: other_medical_expenses geo_level: national diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index d12ba7eef..7c2ee0409 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -50,11 +50,6 @@ from policyengine_us_data.utils.asset_imputation import ( build_household_vehicle_receiver, ) -from policyengine_us_data.utils.policyengine import ( - supports_medicare_enrollment_input, - supports_modeled_medicare_part_b_inputs, -) - CURRENT_HEALTH_COVERAGE_REPORTED_VAR_MAP = { "reported_has_direct_purchase_health_coverage_at_interview": "NOW_DIR", @@ -193,6 +188,8 @@ def generate(self): add_takeup(self) logging.info("Imputing Marketplace plan benchmark ratio") add_marketplace_plan_benchmark_ratio(self) + logging.info("Deriving other health insurance premiums") + derive_other_health_insurance_premiums(self) logging.info("Downsampling") # Downsample @@ -519,6 +516,124 @@ def add_marketplace_plan_benchmark_ratio(self): self.save_dataset(data) +OTHER_HEALTH_INSURANCE_PREMIUM_TARGETS = { + "other_health_insurance_premiums": { + "reported_variable": "health_insurance_premiums_without_medicare_part_b", + "modeled_variables": ( + "chip_premium", + "marketplace_net_premium", + "medicaid_premium", + ), + }, +} + + +def derive_other_health_insurance_premiums(self): + """Create other premium inputs net of baseline computed premiums. + + The model adds computed premiums back explicitly, so it needs a separate + other-premium input for the parts of CPS-reported non-Medicare premiums + not explained by baseline computed Marketplace, CHIP, or Medicaid + premiums. The original CPS-reported premium inputs remain unchanged as raw + source fields. The data package requires a policyengine-us release with + these modeled premium variables, so missing variables fail fast instead of + silently producing an incomplete decomposition. + """ + from policyengine_us import Microsimulation + + data = self.load_dataset() + baseline = Microsimulation(dataset=self) + tbs = baseline.tax_benefit_system + period = self.time_period + changed = False + + for output_variable, config in OTHER_HEALTH_INSURANCE_PREMIUM_TARGETS.items(): + reported_variable = config["reported_variable"] + premium_variables = config["modeled_variables"] + + if reported_variable not in data: + continue + + computed_premium = np.zeros(len(data[reported_variable]), dtype=float) + for variable in premium_variables: + values = np.asarray( + baseline.calculate(variable, period=period).values, + dtype=float, + ) + computed_premium += _premium_values_to_person( + data=data, + source_entity=tbs.variables[variable].entity.key, + values=values, + ) + + data[output_variable] = compute_other_health_insurance_premiums( + reported_premium=data[reported_variable], + baseline_computed_premium=computed_premium, + ) + logging.info( + "Created %s from %s by subtracting baseline computed premiums: %s", + output_variable, + reported_variable, + ", ".join(premium_variables), + ) + changed = True + + if changed: + self.save_dataset(data) + + +def compute_other_health_insurance_premiums( + reported_premium: np.ndarray, + baseline_computed_premium: np.ndarray, +) -> np.ndarray: + """Return other premiums after subtracting baseline computed premiums.""" + return np.asarray(reported_premium, dtype=float) - np.asarray( + baseline_computed_premium, dtype=float + ) + + +def _premium_values_to_person( + data: dict, + source_entity: str, + values: np.ndarray, +) -> np.ndarray: + """Map computed premiums to person rows for person-level premium accounting.""" + person_ids = data["person_id"] + if source_entity == "person": + if len(values) != len(person_ids): + raise ValueError( + "Person-level computed premium length does not match person rows: " + f"got {len(values)}, expected {len(person_ids)}." + ) + return values + + entity_id_variable = f"{source_entity}_id" + person_entity_id_variable = f"person_{source_entity}_id" + if entity_id_variable not in data or person_entity_id_variable not in data: + raise ValueError( + f"Cannot allocate {source_entity}-level premiums to people: missing " + f"{entity_id_variable} or {person_entity_id_variable}." + ) + + entity_ids = data[entity_id_variable] + person_entity_ids = data[person_entity_id_variable] + if len(values) != len(entity_ids): + raise ValueError( + f"{source_entity}-level computed premium length does not match " + f"{source_entity} rows: got {len(values)}, expected {len(entity_ids)}." + ) + + entity_position = {entity_id: index for index, entity_id in enumerate(entity_ids)} + allocated = np.zeros(len(person_ids), dtype=float) + seen_entities = set() + for person_index, entity_id in enumerate(person_entity_ids): + if entity_id in seen_entities: + continue + allocated[person_index] = values[entity_position[entity_id]] + seen_entities.add(entity_id) + return allocated + + MARKETPLACE_PLAN_BENCHMARK_RATIO_MIN = 0.5 MARKETPLACE_PLAN_BENCHMARK_RATIO_MAX = 1.5 @@ -1009,12 +1124,7 @@ def add_personal_income_variables(cps: h5py.File, person: DataFrame, year: int): cps["health_insurance_premiums_without_medicare_part_b"] = person.PHIP_VAL cps["over_the_counter_health_expenses"] = person.POTC_VAL cps["other_medical_expenses"] = person.PMED_VAL - if supports_medicare_enrollment_input(): - cps["medicare_enrolled"] = person.MCARE == 1 - if supports_modeled_medicare_part_b_inputs(): - cps["medicare_part_b_premiums_reported"] = person.PEMCPREM - else: - cps["medicare_part_b_premiums"] = person.PEMCPREM + cps["medicare_enrolled"] = person.MCARE == 1 # Get QBI simulation parameters --- yamlfilename = ( diff --git a/policyengine_us_data/datasets/cps/extended_cps.py b/policyengine_us_data/datasets/cps/extended_cps.py index 53a6ceefe..061dc5d9d 100644 --- a/policyengine_us_data/datasets/cps/extended_cps.py +++ b/policyengine_us_data/datasets/cps/extended_cps.py @@ -19,9 +19,6 @@ impute_tax_unit_mortgage_balance_hints, ) from policyengine_us_data.utils.policyengine import has_policyengine_us_variables -from policyengine_us_data.utils.policyengine import ( - supports_modeled_medicare_part_b_inputs, -) from policyengine_us_data.utils.retirement_limits import ( get_retirement_limits, get_se_pension_limits, @@ -151,6 +148,7 @@ def _supports_structural_mortgage_inputs() -> bool: "spm_unit_pre_subsidy_childcare_expenses", # Medical expenses "health_insurance_premiums_without_medicare_part_b", + "other_health_insurance_premiums", "over_the_counter_health_expenses", "other_medical_expenses", "child_support_expense", @@ -166,9 +164,6 @@ def _supports_structural_mortgage_inputs() -> bool: "self_employment_income_last_year", ] -if not supports_modeled_medicare_part_b_inputs(): - CPS_ONLY_IMPUTED_VARIABLES.append("medicare_part_b_premiums") - # Set for O(1) lookup in the splice loop. _CPS_ONLY_SET = set(CPS_ONLY_IMPUTED_VARIABLES) diff --git a/policyengine_us_data/datasets/puf/puf.py b/policyengine_us_data/datasets/puf/puf.py index dc89c4a9a..18a2083ab 100644 --- a/policyengine_us_data/datasets/puf/puf.py +++ b/policyengine_us_data/datasets/puf/puf.py @@ -17,7 +17,9 @@ STRUCTURAL_MORTGAGE_VARIABLES, convert_mortgage_interest_to_structural_inputs, ) -from policyengine_us_data.utils.policyengine import has_policyengine_us_variables +from policyengine_us_data.utils.policyengine import ( + has_policyengine_us_variables, +) from policyengine_us_data.utils.uprating import ( create_policyengine_uprating_factors_table, ) @@ -984,7 +986,7 @@ class PUF_2024(PUF): "health_insurance_premiums_without_medicare_part_b": 0.453, "other_medical_expenses": 0.325, "over_the_counter_health_expenses": 0.085, - "medicare_part_b_premiums": 0.137, + "medicare_part_b_premium": 0.137, } if __name__ == "__main__": diff --git a/policyengine_us_data/db/etl_national_targets.py b/policyengine_us_data/db/etl_national_targets.py index a671daedb..9c882cf1b 100644 --- a/policyengine_us_data/db/etl_national_targets.py +++ b/policyengine_us_data/db/etl_national_targets.py @@ -155,7 +155,7 @@ def extract_national_targets(year: int = DEFAULT_YEAR): "year": 2024, }, { - "variable": "medicare_part_b_premiums", + "variable": "medicare_part_b_premium", "value": get_beneficiary_paid_medicare_part_b_premiums_target(2024), "source": get_beneficiary_paid_medicare_part_b_premiums_source(2024), "notes": get_beneficiary_paid_medicare_part_b_premiums_notes(2024), diff --git a/policyengine_us_data/storage/calibration_targets/pull_hardcoded_targets.py b/policyengine_us_data/storage/calibration_targets/pull_hardcoded_targets.py index 16e92ea01..cf37f9496 100644 --- a/policyengine_us_data/storage/calibration_targets/pull_hardcoded_targets.py +++ b/policyengine_us_data/storage/calibration_targets/pull_hardcoded_targets.py @@ -9,7 +9,7 @@ HARD_CODED_TOTALS = { "health_insurance_premiums_without_medicare_part_b": 385e9, "other_medical_expenses": 278e9, - "medicare_part_b_premiums": 112e9, + "medicare_part_b_premium": 112e9, "over_the_counter_health_expenses": 72e9, "spm_unit_spm_threshold": 3_945e9, "child_support_expense": 33e9, diff --git a/policyengine_us_data/utils/loss.py b/policyengine_us_data/utils/loss.py index ce71696cc..7150496ac 100644 --- a/policyengine_us_data/utils/loss.py +++ b/policyengine_us_data/utils/loss.py @@ -19,6 +19,9 @@ from policyengine_core.reforms import Reform from policyengine_us_data.utils.soi import pe_to_soi, get_soi + +MEDICARE_PART_B_PREMIUM_VARIABLE = "medicare_part_b_premium" + # National calibration targets consumed by build_loss_matrix(). # These values are specific to 2024 — they should NOT be applied to # other years without re-sourcing. They are duplicated in @@ -29,8 +32,8 @@ HARD_CODED_TOTALS = { "health_insurance_premiums_without_medicare_part_b": 385e9, "other_medical_expenses": 278e9, - "medicare_part_b_premiums": get_beneficiary_paid_medicare_part_b_premiums_target( - 2024 + MEDICARE_PART_B_PREMIUM_VARIABLE: ( + get_beneficiary_paid_medicare_part_b_premiums_target(2024) ), "over_the_counter_health_expenses": 72e9, "spm_unit_spm_threshold": 3_945e9, @@ -851,18 +854,21 @@ def build_loss_matrix(dataset: type, time_period): else: in_age_range = (age >= age_lower_bound) * (age < age_lower_bound + 10) label_suffix = f"age_{age_lower_bound}_to_{age_lower_bound + 9}" - for expense_type in [ - "health_insurance_premiums_without_medicare_part_b", - "over_the_counter_health_expenses", - "other_medical_expenses", - "medicare_part_b_premiums", + for expense_type, target_column in [ + ( + "health_insurance_premiums_without_medicare_part_b", + "health_insurance_premiums_without_medicare_part_b", + ), + ("over_the_counter_health_expenses", "over_the_counter_health_expenses"), + ("other_medical_expenses", "other_medical_expenses"), + (MEDICARE_PART_B_PREMIUM_VARIABLE, "medicare_part_b_premiums"), ]: label = f"nation/census/{expense_type}/{label_suffix}" value = sim.calculate(expense_type).values loss_matrix[label] = sim.map_result( in_age_range * value, "person", "household" ) - targets_array.append(row[expense_type]) + targets_array.append(row[target_column]) # AGI by SPM threshold totals diff --git a/policyengine_us_data/utils/policyengine.py b/policyengine_us_data/utils/policyengine.py index 869b95d9a..1d150ee97 100644 --- a/policyengine_us_data/utils/policyengine.py +++ b/policyengine_us_data/utils/policyengine.py @@ -134,13 +134,3 @@ def has_policyengine_us_variables(*variables: str) -> bool: return False return set(variables).issubset(available_variables) - - -def supports_medicare_enrollment_input() -> bool: - return has_policyengine_us_variables("medicare_enrolled") - - -def supports_modeled_medicare_part_b_inputs() -> bool: - return has_policyengine_us_variables( - "medicare_part_b_premiums_reported", - ) diff --git a/pyproject.toml b/pyproject.toml index 96f069460..4cad830c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "policyengine-us>=1.637.0", + "policyengine-us>=1.674.1", "policyengine-core>=3.23.6", "pandas>=2.3.1", "requests>=2.25.0", diff --git a/tests/unit/calibration/test_target_config.py b/tests/unit/calibration/test_target_config.py index c698e83db..8245c190e 100644 --- a/tests/unit/calibration/test_target_config.py +++ b/tests/unit/calibration/test_target_config.py @@ -206,6 +206,21 @@ def test_training_config_includes_national_ctc_agi_targets(self): "domain_variable": "adjusted_gross_income,non_refundable_ctc", } in include_rules + def test_training_config_includes_medicare_part_b_target(self): + config = load_target_config( + str( + Path(__file__).resolve().parents[3] + / "policyengine_us_data" + / "calibration" + / "target_config.yaml" + ) + ) + + assert { + "variable": "medicare_part_b_premium", + "geo_level": "national", + } in config["include"] + def test_training_config_includes_district_non_refundable_ctc_target(self): config = load_target_config( str( diff --git a/tests/unit/datasets/test_other_health_insurance_premiums.py b/tests/unit/datasets/test_other_health_insurance_premiums.py new file mode 100644 index 000000000..3d6b8ce59 --- /dev/null +++ b/tests/unit/datasets/test_other_health_insurance_premiums.py @@ -0,0 +1,119 @@ +from types import SimpleNamespace + +import numpy as np + +from policyengine_us_data.datasets.cps.cps import ( + _premium_values_to_person, + compute_other_health_insurance_premiums, + derive_other_health_insurance_premiums, +) + + +def test_other_health_insurance_premiums_subtracts_computed_premiums() -> None: + reported = np.array([500.0, 200.0, 50.0]) + computed = np.array([125.0, 250.0, 0.0]) + + result = compute_other_health_insurance_premiums( + reported_premium=reported, + baseline_computed_premium=computed, + ) + + np.testing.assert_allclose(result, [375.0, -50.0, 50.0]) + + +def test_other_health_insurance_premiums_preserves_reported_input() -> None: + reported = np.array([500.0, 200.0]) + computed = np.array([125.0, 250.0]) + + _ = compute_other_health_insurance_premiums( + reported_premium=reported, + baseline_computed_premium=computed, + ) + + np.testing.assert_allclose(reported, [500.0, 200.0]) + + +def test_tax_unit_premiums_allocate_to_first_person_only() -> None: + data = { + "person_id": np.array([1, 2, 3, 4]), + "tax_unit_id": np.array([10, 20]), + "person_tax_unit_id": np.array([10, 10, 20, 20]), + } + + result = _premium_values_to_person( + data=data, + source_entity="tax_unit", + values=np.array([300.0, 800.0]), + ) + + np.testing.assert_allclose(result, [300.0, 0.0, 800.0, 0.0]) + + +def test_person_premiums_pass_through_to_person_rows() -> None: + data = {"person_id": np.array([1, 2, 3])} + values = np.array([100.0, 200.0, 300.0]) + + result = _premium_values_to_person( + data=data, + source_entity="person", + values=values, + ) + + np.testing.assert_allclose(result, values) + + +def test_derive_other_health_insurance_premiums_emits_output( + monkeypatch, +) -> None: + class FakeDataset: + time_period = 2024 + + def __init__(self): + self.saved_data = None + self.data = { + "person_id": np.array([1, 2]), + "health_insurance_premiums_without_medicare_part_b": np.array( + [500.0, 200.0] + ), + } + + def load_dataset(self): + return self.data.copy() + + def save_dataset(self, data): + self.saved_data = data + + class FakeMicrosimulation: + tax_benefit_system = SimpleNamespace( + variables={ + "chip_premium": SimpleNamespace(entity=SimpleNamespace(key="person")), + "marketplace_net_premium": SimpleNamespace( + entity=SimpleNamespace(key="person") + ), + "medicaid_premium": SimpleNamespace( + entity=SimpleNamespace(key="person") + ), + } + ) + + def __init__(self, dataset): + pass + + def calculate(self, variable, period): + values = { + "chip_premium": np.array([50.0, 75.0]), + "marketplace_net_premium": np.array([25.0, 0.0]), + "medicaid_premium": np.array([0.0, 10.0]), + } + return SimpleNamespace(values=values[variable]) + + monkeypatch.setattr("policyengine_us.Microsimulation", FakeMicrosimulation) + + dataset = FakeDataset() + derive_other_health_insurance_premiums(dataset) + + assert dataset.saved_data is not None + np.testing.assert_allclose( + dataset.saved_data["other_health_insurance_premiums"], + [425.0, 115.0], + ) diff --git a/tests/unit/test_medical_expense_inputs.py b/tests/unit/test_medical_expense_inputs.py new file mode 100644 index 000000000..5581054d7 --- /dev/null +++ b/tests/unit/test_medical_expense_inputs.py @@ -0,0 +1,7 @@ +from policyengine_us_data.datasets.puf.puf import ( + MEDICAL_EXPENSE_CATEGORY_BREAKDOWNS, +) + + +def test_puf_medical_breakdown_still_sums_to_one(): + assert sum(MEDICAL_EXPENSE_CATEGORY_BREAKDOWNS.values()) == 1.0 diff --git a/tests/unit/test_medicare_part_b_inputs.py b/tests/unit/test_medicare_part_b_inputs.py deleted file mode 100644 index c69e88789..000000000 --- a/tests/unit/test_medicare_part_b_inputs.py +++ /dev/null @@ -1,27 +0,0 @@ -from policyengine_us_data.datasets.cps.extended_cps import ( - CPS_ONLY_IMPUTED_VARIABLES, - supports_modeled_medicare_part_b_inputs, -) -from policyengine_us_data.datasets.puf.puf import MEDICAL_EXPENSE_CATEGORY_BREAKDOWNS -from policyengine_us_data.utils import policyengine as policyengine_utils - - -def test_medicare_part_b_clone_imputation_matches_installed_model_support(): - assert ("medicare_part_b_premiums" in set(CPS_ONLY_IMPUTED_VARIABLES)) is ( - not supports_modeled_medicare_part_b_inputs() - ) - - -def test_puf_medical_breakdown_still_sums_to_one(): - assert sum(MEDICAL_EXPENSE_CATEGORY_BREAKDOWNS.values()) == 1.0 - - -def test_supports_medicare_enrollment_input_allows_partial_support(monkeypatch): - monkeypatch.setattr( - policyengine_utils, - "has_policyengine_us_variables", - lambda *variables: variables == ("medicare_enrolled",), - ) - - assert policyengine_utils.supports_medicare_enrollment_input() is True - assert policyengine_utils.supports_modeled_medicare_part_b_inputs() is False diff --git a/uv.lock b/uv.lock index f62f55f99..f5f7c340d 100644 --- a/uv.lock +++ b/uv.lock @@ -2095,7 +2095,7 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.23.6" +version = "3.25.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, @@ -2115,25 +2115,26 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/de/5bc5b02626703ea7d288c84c474ec51e823aa726d55ebabafe7c85e7285f/policyengine_core-3.23.6.tar.gz", hash = "sha256:81bb4057f5d6380f2d7f1af2fe4932bd3bd37fdfda7b841f7ee38b30aa5cc8e6", size = 163499, upload-time = "2026-01-25T14:04:43.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/a6/46a316ef534adbedffbdfb8b2b9cfc89be572e3d75fa79c61103c771000e/policyengine_core-3.25.3.tar.gz", hash = "sha256:bf6a22cc49eeeaba310531321cb932c41a2f10c6a5f4cc20fd7677641f60055d", size = 466467, upload-time = "2026-04-28T00:36:10.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/7a/b47b239fb0a85a36b36b47e7665db981800fcac3384aeec6dadf92a9e548/policyengine_core-3.23.6-py3-none-any.whl", hash = "sha256:f0834107335de6f2452d39e53db7a72a57088ed26d3703a4c4eaded55a4e7bce", size = 225309, upload-time = "2026-01-25T14:04:41.844Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/d3451e5c279bcea5da49c5cab3b3eec9e7fa35aafd62289394b769619b7e/policyengine_core-3.25.3-py3-none-any.whl", hash = "sha256:5b11ef29db4275121b58664a9c5ebd6478eeff5001e9f55b71e13716bbd9085f", size = 231186, upload-time = "2026-04-28T00:36:08.854Z" }, ] [[package]] name = "policyengine-us" -version = "1.637.0" +version = "1.674.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, { name = "pandas" }, { name = "policyengine-core" }, + { name = "spm-calculator" }, { name = "tables" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/3f/d72af00833f1e9dffed558ee6c5e3e74561ea582badb241fdc9b524af49d/policyengine_us-1.637.0.tar.gz", hash = "sha256:d1dbd2aba6dfd5fb1083f4deb2c75ae3bf10ba6933330cfcde137e6abb76714a", size = 8962293, upload-time = "2026-04-17T01:46:12.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/d0/cef3e52ecd087d6446f8761ddc91f8dd8ab56b2b320b95b54f43d63c0604/policyengine_us-1.674.1.tar.gz", hash = "sha256:e3141a11e3036713850fdc39d07793df336a74a3fb50c8989f70cecde1ecb556", size = 9341088, upload-time = "2026-04-29T20:53:36.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/19/358ef7d32f0dbf701b1a5d91b352ad939984eca4d81908b534009b020989/policyengine_us-1.637.0-py3-none-any.whl", hash = "sha256:292f8c3e8c2c4b29336ef2fe045f8d3f333fdd130303e7f1df3a11bf333be24f", size = 8963845, upload-time = "2026-04-17T01:46:09.269Z" }, + { url = "https://files.pythonhosted.org/packages/18/5e/d12fdb3dbaadc1dcc770d799c25d5265859967ce25fd59ae783af56f8604/policyengine_us-1.674.1-py3-none-any.whl", hash = "sha256:2d705e108255fae257dea54cafb324bed6d36fe597c9351987abf01a1c1eb097", size = 9663547, upload-time = "2026-04-29T20:53:32.207Z" }, ] [[package]] @@ -2202,7 +2203,7 @@ requires-dist = [ { name = "pandas", specifier = ">=2.3.1" }, { name = "pip-system-certs", specifier = ">=3.0" }, { name = "policyengine-core", specifier = ">=3.23.6" }, - { name = "policyengine-us", specifier = ">=1.637.0" }, + { name = "policyengine-us", specifier = ">=1.674.1" }, { name = "requests", specifier = ">=2.25.0" }, { name = "samplics", marker = "extra == 'calibration'" }, { name = "scipy", specifier = ">=1.15.3" },