spm-calculator 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spm_calculator-0.1.0/LICENSE +21 -0
- spm_calculator-0.1.0/PKG-INFO +169 -0
- spm_calculator-0.1.0/README.md +135 -0
- spm_calculator-0.1.0/pyproject.toml +60 -0
- spm_calculator-0.1.0/setup.cfg +4 -0
- spm_calculator-0.1.0/spm_calculator/__init__.py +52 -0
- spm_calculator-0.1.0/spm_calculator/calculator.py +248 -0
- spm_calculator-0.1.0/spm_calculator/ce_threshold.py +342 -0
- spm_calculator-0.1.0/spm_calculator/equivalence_scale.py +88 -0
- spm_calculator-0.1.0/spm_calculator/fcsuti_cpi.py +210 -0
- spm_calculator-0.1.0/spm_calculator/forecast.py +276 -0
- spm_calculator-0.1.0/spm_calculator/geoadj.py +352 -0
- spm_calculator-0.1.0/spm_calculator.egg-info/PKG-INFO +169 -0
- spm_calculator-0.1.0/spm_calculator.egg-info/SOURCES.txt +20 -0
- spm_calculator-0.1.0/spm_calculator.egg-info/dependency_links.txt +1 -0
- spm_calculator-0.1.0/spm_calculator.egg-info/requires.txt +14 -0
- spm_calculator-0.1.0/spm_calculator.egg-info/top_level.txt +1 -0
- spm_calculator-0.1.0/tests/test_base_thresholds.py +152 -0
- spm_calculator-0.1.0/tests/test_calculator.py +346 -0
- spm_calculator-0.1.0/tests/test_ce_validation.py +163 -0
- spm_calculator-0.1.0/tests/test_equivalence_scale.py +123 -0
- spm_calculator-0.1.0/tests/test_geoadj.py +243 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 PolicyEngine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spm-calculator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Calculate SPM (Supplemental Poverty Measure) thresholds for any US geography and year
|
|
5
|
+
Author-email: PolicyEngine <hello@policyengine.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PolicyEngine/spm-calculator
|
|
8
|
+
Project-URL: Repository, https://github.com/PolicyEngine/spm-calculator
|
|
9
|
+
Project-URL: Issues, https://github.com/PolicyEngine/spm-calculator/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pandas>=2.0
|
|
22
|
+
Requires-Dist: numpy>=1.24
|
|
23
|
+
Requires-Dist: requests>=2.28
|
|
24
|
+
Requires-Dist: census>=0.8
|
|
25
|
+
Requires-Dist: us>=3.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
31
|
+
Provides-Extra: app
|
|
32
|
+
Requires-Dist: streamlit>=1.30; extra == "app"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# spm-calculator
|
|
36
|
+
|
|
37
|
+
Calculate [Supplemental Poverty Measure (SPM)](https://www.census.gov/topics/income-poverty/supplemental-poverty-measure.html) thresholds for any US geography and year.
|
|
38
|
+
|
|
39
|
+
[](https://policyengine.github.io/spm-calculator)
|
|
40
|
+
[](https://policyengine.github.io/spm-calculator)
|
|
41
|
+
|
|
42
|
+
## Interactive Calculator
|
|
43
|
+
|
|
44
|
+
**[Try the SPM Threshold Calculator](https://policyengine.github.io/spm-calculator)** - A static web app that walks you through calculating your SPM threshold based on your household characteristics.
|
|
45
|
+
|
|
46
|
+
The calculator runs entirely in your browser with no server required - all data is pre-computed and bundled.
|
|
47
|
+
|
|
48
|
+
### Run Locally
|
|
49
|
+
|
|
50
|
+
**Static React App (recommended):**
|
|
51
|
+
```bash
|
|
52
|
+
cd web
|
|
53
|
+
npm install
|
|
54
|
+
npm run dev
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Streamlit App (alternative):**
|
|
58
|
+
```bash
|
|
59
|
+
pip install spm-calculator[app]
|
|
60
|
+
streamlit run app/streamlit_app.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Overview
|
|
64
|
+
|
|
65
|
+
The SPM threshold is calculated as:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
threshold = base_threshold[tenure] × equivalence_scale × geoadj
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Where:
|
|
72
|
+
- **base_threshold** varies by housing tenure (renter, owner with mortgage, owner without mortgage), calculated from 5-year rolling Consumer Expenditure Survey data
|
|
73
|
+
- **equivalence_scale** adjusts for family composition using the SPM three-parameter scale
|
|
74
|
+
- **geoadj** adjusts for local housing costs based on ACS median rents
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install spm-calculator
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quick Start
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from spm_calculator import SPMCalculator
|
|
86
|
+
|
|
87
|
+
# Initialize calculator for a specific year
|
|
88
|
+
calc = SPMCalculator(year=2024)
|
|
89
|
+
|
|
90
|
+
# Get base thresholds by tenure (national, before geographic adjustment)
|
|
91
|
+
base = calc.get_base_thresholds()
|
|
92
|
+
# {'renter': 39430, 'owner_with_mortgage': 39068, 'owner_without_mortgage': 32586}
|
|
93
|
+
|
|
94
|
+
# Get GEOADJ for a specific geography
|
|
95
|
+
geoadj = calc.get_geoadj("congressional_district", "0612") # CA-12
|
|
96
|
+
# 1.24 (24% above national average due to high housing costs)
|
|
97
|
+
|
|
98
|
+
# Calculate threshold for a specific family in a specific location
|
|
99
|
+
threshold = calc.calculate_threshold(
|
|
100
|
+
num_adults=2,
|
|
101
|
+
num_children=2,
|
|
102
|
+
tenure="renter",
|
|
103
|
+
geography_type="congressional_district",
|
|
104
|
+
geography_id="0612"
|
|
105
|
+
)
|
|
106
|
+
# ~$48,893 (base $39,430 × equiv_scale 1.0 × geoadj 1.24)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Supported Geographies
|
|
110
|
+
|
|
111
|
+
- `nation` - National average
|
|
112
|
+
- `state` - 50 states + DC
|
|
113
|
+
- `county` - ~3,200 counties
|
|
114
|
+
- `metro_area` - Metropolitan statistical areas
|
|
115
|
+
- `congressional_district` - 435 congressional districts
|
|
116
|
+
- `puma` - Public Use Microdata Areas
|
|
117
|
+
- `tract` - Census tracts (limited availability)
|
|
118
|
+
|
|
119
|
+
## Data Sources
|
|
120
|
+
|
|
121
|
+
- **Base thresholds**: [BLS Consumer Expenditure Survey](https://www.bls.gov/cex/) - 5-year rolling FCSUti (Food, Clothing, Shelter, Utilities, telephone, internet)
|
|
122
|
+
- **Geographic adjustment**: [ACS 5-Year Estimates](https://www.census.gov/programs-surveys/acs) - Table B25031 (Median Gross Rent by Bedrooms)
|
|
123
|
+
- **Methodology**: [Census SPM Technical Documentation](https://www2.census.gov/programs-surveys/supplemental-poverty-measure/datasets/spm/spm_techdoc.pdf)
|
|
124
|
+
|
|
125
|
+
## Methodology
|
|
126
|
+
|
|
127
|
+
### Base Threshold Calculation
|
|
128
|
+
|
|
129
|
+
Following BLS methodology (updated September 2021):
|
|
130
|
+
1. Download 5 years of CE Survey PUMD (Public Use Microdata)
|
|
131
|
+
2. Filter to consumer units with children
|
|
132
|
+
3. Calculate FCSUti expenditures
|
|
133
|
+
4. Convert to reference family (2 adults, 2 children) using equivalence scale
|
|
134
|
+
5. Calculate 83% of median (47th-53rd percentile average) by tenure type
|
|
135
|
+
|
|
136
|
+
### Geographic Adjustment (GEOADJ)
|
|
137
|
+
|
|
138
|
+
Following Census methodology:
|
|
139
|
+
```
|
|
140
|
+
GEOADJ = (local_median_rent / national_median_rent) × 0.492 + 0.508
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Where 0.492 is the housing portion of the SPM threshold for renters.
|
|
144
|
+
|
|
145
|
+
### Equivalence Scale
|
|
146
|
+
|
|
147
|
+
The SPM uses a three-parameter equivalence scale:
|
|
148
|
+
- First adult: 1.0
|
|
149
|
+
- Additional adults: 0.5 each
|
|
150
|
+
- Children: 0.3 each
|
|
151
|
+
- Normalized to reference family (2A2C = 1.0)
|
|
152
|
+
|
|
153
|
+
## Validation
|
|
154
|
+
|
|
155
|
+
Base thresholds are validated against [BLS published values](https://www.bls.gov/pir/spm/spm_thresholds_2024.htm):
|
|
156
|
+
|
|
157
|
+
| Tenure | 2024 BLS | Calculator |
|
|
158
|
+
|--------|----------|------------|
|
|
159
|
+
| Renter | $39,430 | $39,430 |
|
|
160
|
+
| Owner w/ mortgage | $39,068 | $39,068 |
|
|
161
|
+
| Owner w/o mortgage | $32,586 | $32,586 |
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Contributions welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# spm-calculator
|
|
2
|
+
|
|
3
|
+
Calculate [Supplemental Poverty Measure (SPM)](https://www.census.gov/topics/income-poverty/supplemental-poverty-measure.html) thresholds for any US geography and year.
|
|
4
|
+
|
|
5
|
+
[](https://policyengine.github.io/spm-calculator)
|
|
6
|
+
[](https://policyengine.github.io/spm-calculator)
|
|
7
|
+
|
|
8
|
+
## Interactive Calculator
|
|
9
|
+
|
|
10
|
+
**[Try the SPM Threshold Calculator](https://policyengine.github.io/spm-calculator)** - A static web app that walks you through calculating your SPM threshold based on your household characteristics.
|
|
11
|
+
|
|
12
|
+
The calculator runs entirely in your browser with no server required - all data is pre-computed and bundled.
|
|
13
|
+
|
|
14
|
+
### Run Locally
|
|
15
|
+
|
|
16
|
+
**Static React App (recommended):**
|
|
17
|
+
```bash
|
|
18
|
+
cd web
|
|
19
|
+
npm install
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Streamlit App (alternative):**
|
|
24
|
+
```bash
|
|
25
|
+
pip install spm-calculator[app]
|
|
26
|
+
streamlit run app/streamlit_app.py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Overview
|
|
30
|
+
|
|
31
|
+
The SPM threshold is calculated as:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
threshold = base_threshold[tenure] × equivalence_scale × geoadj
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Where:
|
|
38
|
+
- **base_threshold** varies by housing tenure (renter, owner with mortgage, owner without mortgage), calculated from 5-year rolling Consumer Expenditure Survey data
|
|
39
|
+
- **equivalence_scale** adjusts for family composition using the SPM three-parameter scale
|
|
40
|
+
- **geoadj** adjusts for local housing costs based on ACS median rents
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install spm-calculator
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from spm_calculator import SPMCalculator
|
|
52
|
+
|
|
53
|
+
# Initialize calculator for a specific year
|
|
54
|
+
calc = SPMCalculator(year=2024)
|
|
55
|
+
|
|
56
|
+
# Get base thresholds by tenure (national, before geographic adjustment)
|
|
57
|
+
base = calc.get_base_thresholds()
|
|
58
|
+
# {'renter': 39430, 'owner_with_mortgage': 39068, 'owner_without_mortgage': 32586}
|
|
59
|
+
|
|
60
|
+
# Get GEOADJ for a specific geography
|
|
61
|
+
geoadj = calc.get_geoadj("congressional_district", "0612") # CA-12
|
|
62
|
+
# 1.24 (24% above national average due to high housing costs)
|
|
63
|
+
|
|
64
|
+
# Calculate threshold for a specific family in a specific location
|
|
65
|
+
threshold = calc.calculate_threshold(
|
|
66
|
+
num_adults=2,
|
|
67
|
+
num_children=2,
|
|
68
|
+
tenure="renter",
|
|
69
|
+
geography_type="congressional_district",
|
|
70
|
+
geography_id="0612"
|
|
71
|
+
)
|
|
72
|
+
# ~$48,893 (base $39,430 × equiv_scale 1.0 × geoadj 1.24)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Supported Geographies
|
|
76
|
+
|
|
77
|
+
- `nation` - National average
|
|
78
|
+
- `state` - 50 states + DC
|
|
79
|
+
- `county` - ~3,200 counties
|
|
80
|
+
- `metro_area` - Metropolitan statistical areas
|
|
81
|
+
- `congressional_district` - 435 congressional districts
|
|
82
|
+
- `puma` - Public Use Microdata Areas
|
|
83
|
+
- `tract` - Census tracts (limited availability)
|
|
84
|
+
|
|
85
|
+
## Data Sources
|
|
86
|
+
|
|
87
|
+
- **Base thresholds**: [BLS Consumer Expenditure Survey](https://www.bls.gov/cex/) - 5-year rolling FCSUti (Food, Clothing, Shelter, Utilities, telephone, internet)
|
|
88
|
+
- **Geographic adjustment**: [ACS 5-Year Estimates](https://www.census.gov/programs-surveys/acs) - Table B25031 (Median Gross Rent by Bedrooms)
|
|
89
|
+
- **Methodology**: [Census SPM Technical Documentation](https://www2.census.gov/programs-surveys/supplemental-poverty-measure/datasets/spm/spm_techdoc.pdf)
|
|
90
|
+
|
|
91
|
+
## Methodology
|
|
92
|
+
|
|
93
|
+
### Base Threshold Calculation
|
|
94
|
+
|
|
95
|
+
Following BLS methodology (updated September 2021):
|
|
96
|
+
1. Download 5 years of CE Survey PUMD (Public Use Microdata)
|
|
97
|
+
2. Filter to consumer units with children
|
|
98
|
+
3. Calculate FCSUti expenditures
|
|
99
|
+
4. Convert to reference family (2 adults, 2 children) using equivalence scale
|
|
100
|
+
5. Calculate 83% of median (47th-53rd percentile average) by tenure type
|
|
101
|
+
|
|
102
|
+
### Geographic Adjustment (GEOADJ)
|
|
103
|
+
|
|
104
|
+
Following Census methodology:
|
|
105
|
+
```
|
|
106
|
+
GEOADJ = (local_median_rent / national_median_rent) × 0.492 + 0.508
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Where 0.492 is the housing portion of the SPM threshold for renters.
|
|
110
|
+
|
|
111
|
+
### Equivalence Scale
|
|
112
|
+
|
|
113
|
+
The SPM uses a three-parameter equivalence scale:
|
|
114
|
+
- First adult: 1.0
|
|
115
|
+
- Additional adults: 0.5 each
|
|
116
|
+
- Children: 0.3 each
|
|
117
|
+
- Normalized to reference family (2A2C = 1.0)
|
|
118
|
+
|
|
119
|
+
## Validation
|
|
120
|
+
|
|
121
|
+
Base thresholds are validated against [BLS published values](https://www.bls.gov/pir/spm/spm_thresholds_2024.htm):
|
|
122
|
+
|
|
123
|
+
| Tenure | 2024 BLS | Calculator |
|
|
124
|
+
|--------|----------|------------|
|
|
125
|
+
| Renter | $39,430 | $39,430 |
|
|
126
|
+
| Owner w/ mortgage | $39,068 | $39,068 |
|
|
127
|
+
| Owner w/o mortgage | $32,586 | $32,586 |
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
Contributions welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=45", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spm-calculator"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Calculate SPM (Supplemental Poverty Measure) thresholds for any US geography and year"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "PolicyEngine", email = "hello@policyengine.org"}
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Scientific/Engineering",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.10"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"pandas>=2.0",
|
|
27
|
+
"numpy>=1.24",
|
|
28
|
+
"requests>=2.28",
|
|
29
|
+
"census>=0.8",
|
|
30
|
+
"us>=3.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"pytest-cov>=4.0",
|
|
37
|
+
"black>=23.0",
|
|
38
|
+
"ruff>=0.1",
|
|
39
|
+
]
|
|
40
|
+
app = [
|
|
41
|
+
"streamlit>=1.30",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/PolicyEngine/spm-calculator"
|
|
46
|
+
Repository = "https://github.com/PolicyEngine/spm-calculator"
|
|
47
|
+
Issues = "https://github.com/PolicyEngine/spm-calculator/issues"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools]
|
|
50
|
+
packages = ["spm_calculator"]
|
|
51
|
+
|
|
52
|
+
[tool.black]
|
|
53
|
+
line-length = 79
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 79
|
|
57
|
+
select = ["E", "F", "I"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPM Calculator - Calculate Supplemental Poverty Measure thresholds.
|
|
3
|
+
|
|
4
|
+
This package calculates SPM thresholds for any US geography and year using:
|
|
5
|
+
- Base thresholds from BLS Consumer Expenditure Survey (by tenure type)
|
|
6
|
+
- Geographic adjustments (GEOADJ) from ACS median rents
|
|
7
|
+
- SPM three-parameter equivalence scale for family composition
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from spm_calculator import SPMCalculator
|
|
11
|
+
>>> calc = SPMCalculator(year=2024)
|
|
12
|
+
>>> threshold = calc.calculate_threshold(
|
|
13
|
+
... num_adults=2,
|
|
14
|
+
... num_children=2,
|
|
15
|
+
... tenure="renter",
|
|
16
|
+
... geography_type="congressional_district",
|
|
17
|
+
... geography_id="0612"
|
|
18
|
+
... )
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .calculator import SPMCalculator
|
|
22
|
+
from .ce_threshold import calculate_base_thresholds, get_published_thresholds
|
|
23
|
+
from .geoadj import get_geoadj, create_geoadj_lookup
|
|
24
|
+
from .equivalence_scale import spm_equivalence_scale
|
|
25
|
+
from .fcsuti_cpi import get_fcsuti_cpi, get_fcsuti_inflation_factor
|
|
26
|
+
from .forecast import (
|
|
27
|
+
forecast_thresholds,
|
|
28
|
+
get_thresholds,
|
|
29
|
+
get_threshold_with_metadata,
|
|
30
|
+
get_available_years,
|
|
31
|
+
get_latest_published_year,
|
|
32
|
+
HISTORICAL_THRESHOLDS,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"SPMCalculator",
|
|
39
|
+
"calculate_base_thresholds",
|
|
40
|
+
"get_published_thresholds",
|
|
41
|
+
"get_geoadj",
|
|
42
|
+
"create_geoadj_lookup",
|
|
43
|
+
"spm_equivalence_scale",
|
|
44
|
+
"get_fcsuti_cpi",
|
|
45
|
+
"get_fcsuti_inflation_factor",
|
|
46
|
+
"forecast_thresholds",
|
|
47
|
+
"get_thresholds",
|
|
48
|
+
"get_threshold_with_metadata",
|
|
49
|
+
"get_available_years",
|
|
50
|
+
"get_latest_published_year",
|
|
51
|
+
"HISTORICAL_THRESHOLDS",
|
|
52
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main SPMCalculator class for calculating SPM thresholds.
|
|
3
|
+
|
|
4
|
+
The full SPM threshold calculation is:
|
|
5
|
+
threshold = base_threshold[tenure] × equivalence_scale × geoadj
|
|
6
|
+
|
|
7
|
+
Where:
|
|
8
|
+
- base_threshold varies by housing tenure (from CE Survey)
|
|
9
|
+
- equivalence_scale adjusts for family composition
|
|
10
|
+
- geoadj adjusts for local housing costs
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Union, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .ce_threshold import get_published_thresholds, calculate_base_thresholds
|
|
18
|
+
from .equivalence_scale import spm_equivalence_scale
|
|
19
|
+
from .geoadj import get_geoadj, create_geoadj_lookup, SUPPORTED_GEOGRAPHIES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
VALID_TENURE_TYPES = [
|
|
23
|
+
"renter",
|
|
24
|
+
"owner_with_mortgage",
|
|
25
|
+
"owner_without_mortgage",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SPMCalculator:
|
|
30
|
+
"""
|
|
31
|
+
Calculator for Supplemental Poverty Measure thresholds.
|
|
32
|
+
|
|
33
|
+
Calculates SPM thresholds for any US geography and year using:
|
|
34
|
+
- Base thresholds from BLS Consumer Expenditure Survey
|
|
35
|
+
- Geographic adjustments from ACS median rents
|
|
36
|
+
- SPM three-parameter equivalence scale
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> calc = SPMCalculator(year=2024)
|
|
40
|
+
>>> threshold = calc.calculate_threshold(
|
|
41
|
+
... num_adults=2,
|
|
42
|
+
... num_children=2,
|
|
43
|
+
... tenure="renter",
|
|
44
|
+
... geography_type="congressional_district",
|
|
45
|
+
... geography_id="0612"
|
|
46
|
+
... )
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
year: int,
|
|
52
|
+
use_published_thresholds: bool = True,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize SPMCalculator for a specific year.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
year: Target year for threshold calculation
|
|
59
|
+
use_published_thresholds: If True, use published BLS thresholds
|
|
60
|
+
when available. If False, calculate from CE Survey data.
|
|
61
|
+
"""
|
|
62
|
+
self.year = year
|
|
63
|
+
self.use_published_thresholds = use_published_thresholds
|
|
64
|
+
self._base_thresholds: Optional[dict[str, float]] = None
|
|
65
|
+
|
|
66
|
+
def get_base_thresholds(self) -> dict[str, float]:
|
|
67
|
+
"""
|
|
68
|
+
Get base SPM thresholds by tenure type.
|
|
69
|
+
|
|
70
|
+
Returns thresholds for the reference family (2 adults, 2 children)
|
|
71
|
+
before geographic adjustment.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dict with keys 'renter', 'owner_with_mortgage',
|
|
75
|
+
'owner_without_mortgage' and threshold values in dollars.
|
|
76
|
+
"""
|
|
77
|
+
if self._base_thresholds is not None:
|
|
78
|
+
return self._base_thresholds.copy()
|
|
79
|
+
|
|
80
|
+
if self.use_published_thresholds:
|
|
81
|
+
try:
|
|
82
|
+
self._base_thresholds = get_published_thresholds(self.year)
|
|
83
|
+
except ValueError:
|
|
84
|
+
# Published not available, forecast from latest available
|
|
85
|
+
# Use simple CPI-U projection from 2024 for now
|
|
86
|
+
# TODO: Implement proper CE-based forecasting
|
|
87
|
+
latest_thresholds = get_published_thresholds(2024)
|
|
88
|
+
years_ahead = self.year - 2024
|
|
89
|
+
# Assume ~3% annual inflation
|
|
90
|
+
inflation_factor = 1.03**years_ahead
|
|
91
|
+
self._base_thresholds = {
|
|
92
|
+
k: v * inflation_factor
|
|
93
|
+
for k, v in latest_thresholds.items()
|
|
94
|
+
}
|
|
95
|
+
else:
|
|
96
|
+
self._base_thresholds = calculate_base_thresholds(
|
|
97
|
+
target_year=self.year,
|
|
98
|
+
use_published_fallback=False,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return self._base_thresholds.copy()
|
|
102
|
+
|
|
103
|
+
def get_geoadj(
|
|
104
|
+
self,
|
|
105
|
+
geography_type: str,
|
|
106
|
+
geography_id: str,
|
|
107
|
+
) -> float:
|
|
108
|
+
"""
|
|
109
|
+
Get geographic adjustment factor for a specific location.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
geography_type: Type of geography (nation, state, county,
|
|
113
|
+
congressional_district, etc.)
|
|
114
|
+
geography_id: Geography identifier (FIPS code, etc.)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
GEOADJ value (typically 0.84 to 1.27)
|
|
118
|
+
"""
|
|
119
|
+
# Use year - 1 for ACS data (lag)
|
|
120
|
+
acs_year = min(self.year - 1, 2022) # TODO: Update as data available
|
|
121
|
+
return get_geoadj(geography_type, geography_id, acs_year)
|
|
122
|
+
|
|
123
|
+
def calculate_threshold(
|
|
124
|
+
self,
|
|
125
|
+
num_adults: int,
|
|
126
|
+
num_children: int,
|
|
127
|
+
tenure: str,
|
|
128
|
+
geography_type: str,
|
|
129
|
+
geography_id: str,
|
|
130
|
+
) -> float:
|
|
131
|
+
"""
|
|
132
|
+
Calculate SPM threshold for a specific SPM unit and location.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
num_adults: Number of adults (18+) in the SPM unit
|
|
136
|
+
num_children: Number of children (under 18) in the SPM unit
|
|
137
|
+
tenure: Housing tenure type ('renter', 'owner_with_mortgage',
|
|
138
|
+
'owner_without_mortgage')
|
|
139
|
+
geography_type: Type of geography (nation, state, county, etc.)
|
|
140
|
+
geography_id: Geography identifier
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
SPM threshold in dollars
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If tenure type invalid or geography not found
|
|
147
|
+
"""
|
|
148
|
+
# Validate inputs
|
|
149
|
+
if tenure not in VALID_TENURE_TYPES:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Invalid tenure type: {tenure}. "
|
|
152
|
+
f"Must be one of: {VALID_TENURE_TYPES}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if num_adults < 0 or num_children < 0:
|
|
156
|
+
raise ValueError("Number of persons cannot be negative")
|
|
157
|
+
|
|
158
|
+
# Zero persons = zero threshold
|
|
159
|
+
if num_adults == 0 and num_children == 0:
|
|
160
|
+
return 0.0
|
|
161
|
+
|
|
162
|
+
# Get components
|
|
163
|
+
base = self.get_base_thresholds()[tenure]
|
|
164
|
+
equiv_scale = spm_equivalence_scale(num_adults, num_children)
|
|
165
|
+
geoadj = self.get_geoadj(geography_type, geography_id)
|
|
166
|
+
|
|
167
|
+
return base * equiv_scale * geoadj
|
|
168
|
+
|
|
169
|
+
def calculate_thresholds(
|
|
170
|
+
self,
|
|
171
|
+
num_adults: Union[int, np.ndarray, Sequence[int]],
|
|
172
|
+
num_children: Union[int, np.ndarray, Sequence[int]],
|
|
173
|
+
tenure: Union[str, Sequence[str]],
|
|
174
|
+
geography_type: str,
|
|
175
|
+
geography_ids: Union[str, Sequence[str]],
|
|
176
|
+
) -> np.ndarray:
|
|
177
|
+
"""
|
|
178
|
+
Calculate SPM thresholds for multiple SPM units (vectorized).
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
num_adults: Number of adults for each unit
|
|
182
|
+
num_children: Number of children for each unit
|
|
183
|
+
tenure: Tenure type(s) - single value or per-unit
|
|
184
|
+
geography_type: Type of geography (same for all)
|
|
185
|
+
geography_ids: Geography ID(s) - single value or per-unit
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Array of SPM thresholds
|
|
189
|
+
"""
|
|
190
|
+
# Convert to arrays
|
|
191
|
+
num_adults = np.atleast_1d(num_adults)
|
|
192
|
+
num_children = np.atleast_1d(num_children)
|
|
193
|
+
n = len(num_adults)
|
|
194
|
+
|
|
195
|
+
# Handle tenure
|
|
196
|
+
if isinstance(tenure, str):
|
|
197
|
+
tenure = [tenure] * n
|
|
198
|
+
tenure = list(tenure)
|
|
199
|
+
|
|
200
|
+
# Handle geography_ids
|
|
201
|
+
if isinstance(geography_ids, str):
|
|
202
|
+
geography_ids = [geography_ids] * n
|
|
203
|
+
geography_ids = list(geography_ids)
|
|
204
|
+
|
|
205
|
+
# Validate lengths
|
|
206
|
+
if not (
|
|
207
|
+
len(num_adults)
|
|
208
|
+
== len(num_children)
|
|
209
|
+
== len(tenure)
|
|
210
|
+
== len(geography_ids)
|
|
211
|
+
):
|
|
212
|
+
raise ValueError("All input arrays must have same length")
|
|
213
|
+
|
|
214
|
+
# Validate tenure types
|
|
215
|
+
for t in tenure:
|
|
216
|
+
if t not in VALID_TENURE_TYPES:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Invalid tenure type: {t}. "
|
|
219
|
+
f"Must be one of: {VALID_TENURE_TYPES}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Get base thresholds
|
|
223
|
+
base_thresholds = self.get_base_thresholds()
|
|
224
|
+
|
|
225
|
+
# Calculate equivalence scales (vectorized)
|
|
226
|
+
equiv_scales = spm_equivalence_scale(num_adults, num_children)
|
|
227
|
+
|
|
228
|
+
# Get GEOADJ values
|
|
229
|
+
# Cache unique geographies
|
|
230
|
+
unique_geos = set(geography_ids)
|
|
231
|
+
geo_to_geoadj = {}
|
|
232
|
+
for geo_id in unique_geos:
|
|
233
|
+
geo_to_geoadj[geo_id] = self.get_geoadj(geography_type, geo_id)
|
|
234
|
+
|
|
235
|
+
geoadj_values = np.array([geo_to_geoadj[g] for g in geography_ids])
|
|
236
|
+
|
|
237
|
+
# Get base values by tenure
|
|
238
|
+
base_values = np.array([base_thresholds[t] for t in tenure])
|
|
239
|
+
|
|
240
|
+
# Calculate thresholds
|
|
241
|
+
thresholds = base_values * equiv_scales * geoadj_values
|
|
242
|
+
|
|
243
|
+
return thresholds
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def supported_geographies(self) -> list[str]:
|
|
247
|
+
"""List of supported geography types."""
|
|
248
|
+
return list(SUPPORTED_GEOGRAPHIES.keys())
|