diff --git a/changelog.d/add-statefip-support.added.md b/changelog.d/add-statefip-support.added.md new file mode 100644 index 0000000..41f9fbc --- /dev/null +++ b/changelog.d/add-statefip-support.added.md @@ -0,0 +1 @@ +Add statefip (FIPS code) input support, matching TAXSIM-35 behavior. diff --git a/dashboard/src/components/CsvRunner.jsx b/dashboard/src/components/CsvRunner.jsx index 5fd7bdd..3180af0 100644 --- a/dashboard/src/components/CsvRunner.jsx +++ b/dashboard/src/components/CsvRunner.jsx @@ -40,7 +40,7 @@ const SAMPLE_DATASETS = [ const fmt = (n) => Number(n).toLocaleString(); const KNOWN_COLUMNS = new Set([ - 'taxsimid', 'year', 'state', 'mstat', 'page', 'sage', + 'taxsimid', 'year', 'state', 'statefip', 'mstat', 'page', 'sage', 'dependent_exemption', 'depx', 'pwages', 'swages', 'psemp', 'ssemp', 'dividends', 'intrec', 'stcg', 'ltcg', 'otherprop', 'nonprop', 'pensions', 'gssi', diff --git a/policyengine_taxsim/api.py b/policyengine_taxsim/api.py index 818cbc0..e4550b4 100644 --- a/policyengine_taxsim/api.py +++ b/policyengine_taxsim/api.py @@ -49,6 +49,7 @@ "taxsimid", "year", "state", + "statefip", "mstat", "page", "sage", diff --git a/policyengine_taxsim/core/utils.py b/policyengine_taxsim/core/utils.py index 0a29b69..b489a8c 100644 --- a/policyengine_taxsim/core/utils.py +++ b/policyengine_taxsim/core/utils.py @@ -284,3 +284,6 @@ def get_ordinal(n): 50: 55, # Wisconsin 51: 56, # Wyoming } + +# Reverse mapping: FIPS → SOI (for statefip input support) +FIPS_TO_SOI_MAP = {fips: soi for soi, fips in SOI_TO_FIPS_MAP.items()} diff --git a/policyengine_taxsim/runners/base_runner.py b/policyengine_taxsim/runners/base_runner.py index 33889fe..dc490d2 100644 --- a/policyengine_taxsim/runners/base_runner.py +++ b/policyengine_taxsim/runners/base_runner.py @@ -5,8 +5,10 @@ try: from ..core.io import write_output + from ..core.utils import FIPS_TO_SOI_MAP except ImportError: from policyengine_taxsim.core.io import write_output + from policyengine_taxsim.core.utils import FIPS_TO_SOI_MAP class BaseTaxRunner(ABC): @@ -39,6 +41,17 @@ def _validate_input(self): if "year" not in self.input_df.columns: raise ValueError("Input data must contain a 'year' column") + # Convert statefip (FIPS) to state (SOI) per row. + # Matches TAXSIM-35 behavior: for each record, if state==0 + # and statefip is set, convert FIPS→SOI. + if "statefip" in self.input_df.columns: + if "state" not in self.input_df.columns: + self.input_df["state"] = 0 + fips_vals = self.input_df["statefip"].astype(int) + soi_from_fips = fips_vals.map(FIPS_TO_SOI_MAP).fillna(0).astype(int) + use_fips = (self.input_df["state"] == 0) & (fips_vals > 0) + self.input_df.loc[use_fips, "state"] = soi_from_fips[use_fips] + # Auto-assign taxsimid if not present if "taxsimid" not in self.input_df.columns: # Assign sequential IDs starting from 1