nmtc-mapper 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.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: nmtc-mapper
3
+ Version: 0.1.0
4
+ Summary: Automated NMTC eligibility checker β€” geocode addresses and check Low-Income Community status using CDFI Fund and Census data
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/Jaypatel1511/nmtc-mapper
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pandas>=1.4.0
10
+ Requires-Dist: numpy>=1.21.0
11
+ Requires-Dist: requests>=2.27.0
12
+ Requires-Dist: openpyxl>=3.0.0
13
+
14
+ # nmtc-mapper πŸ—ΊοΈ
15
+
16
+ **Automated NMTC eligibility checker for addresses and census tracts.**
17
+
18
+ Pass a DataFrame of addresses and get back a boolean column for NMTC eligibility,
19
+ distress level, poverty rate, AMI ratio, and more β€” using official CDFI Fund and
20
+ Census Bureau data. No manual lookups required.
21
+
22
+ ---
23
+
24
+ ## Why nmtc-mapper?
25
+
26
+ The CDFI Fund provides a manual web tool (CIMS) for checking NMTC eligibility
27
+ one address at a time. nmtc-mapper automates this β€” pass 10,000 addresses and
28
+ get results in seconds, using the same official data source.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ pip install nmtc-mapper
35
+
36
+ ---
37
+
38
+ ## Quickstart
39
+
40
+ from nmtcmapper import NMTCMapper
41
+
42
+ mapper = NMTCMapper()
43
+
44
+ # Single address (geocodes automatically)
45
+ result = mapper.check_address("1234 S Michigan Ave, Chicago, IL 60605")
46
+ result.summary()
47
+ print(result.nmtc_eligible) # True
48
+ print(result.distress_level) # "severe"
49
+ print(result.poverty_rate) # 0.38
50
+
51
+ # Known census tract (no geocoding needed)
52
+ result = mapper.check_tract("17031840100")
53
+ print(result.nmtc_eligible) # True
54
+
55
+ # Batch β€” enrich a DataFrame of addresses
56
+ import pandas as pd
57
+ df = pd.read_csv("projects.csv") # must have 'address' column
58
+ df = mapper.enrich(df, address_col="address")
59
+ print(df["nmtc_eligible"].value_counts())
60
+ print(df["distress_level"].value_counts())
61
+
62
+ # If you already have census tract IDs
63
+ df = mapper.enrich(df, tract_col="tract_id")
64
+
65
+ # Summary stats
66
+ mapper.eligible_count(df)
67
+
68
+ ---
69
+
70
+ ## Eligibility Rules (2016-2020 ACS β€” mandatory since Sept 1, 2024)
71
+
72
+ A census tract qualifies as a Low-Income Community (LIC) if it meets ANY of:
73
+
74
+ - Poverty rate >= 20%
75
+ - Median Family Income <= 80% of metro/state AMI
76
+ - Median Family Income <= 85% of state AMI (high migration rural counties)
77
+
78
+ Distress levels:
79
+
80
+ - deep β€” Poverty >= 40% OR AMI <= 50% OR unemployment >= 2x national rate
81
+ - severe β€” Poverty >= 30% OR AMI <= 60% OR unemployment >= 1.5x national rate
82
+ - lic β€” NMTC eligible (meets LIC criteria)
83
+ - ineligible β€” Does not qualify
84
+
85
+ ---
86
+
87
+ ## Data Sources
88
+
89
+ - CDFI Fund 2016-2020 ACS Low-Income Community Eligibility File
90
+ https://www.cdfifund.gov/research-data
91
+ - US Census Bureau Geocoding API (free, no API key required)
92
+ https://geocoding.geo.census.gov
93
+
94
+ ---
95
+
96
+ ## Output Columns
97
+
98
+ After running .enrich(), your DataFrame will have:
99
+
100
+ - nmtc_eligible (bool)
101
+ - distress_level (str: deep / severe / lic / ineligible)
102
+ - poverty_rate (float)
103
+ - ami_ratio (float)
104
+ - unemployment_rate (float)
105
+ - is_non_metro (bool)
106
+ - severe_distress (bool)
107
+ - deep_distress (bool)
108
+
109
+ ---
110
+
111
+ ## Running Tests
112
+
113
+ PYTHONPATH=. pytest tests/ -v
114
+
115
+ 24 tests across all modules.
116
+
117
+ ---
118
+
119
+ ## Who This Is For
120
+
121
+ - CDEs screening project locations for NMTC eligibility
122
+ - CDFI analysts qualifying borrower locations at scale
123
+ - Researchers analyzing geographic distribution of LIC tracts
124
+ - Anyone replacing manual CIMS lookups with automated Python
125
+
126
+ ---
127
+
128
+ ## License
129
+
130
+ MIT 2026 Jaypatel1511
@@ -0,0 +1,117 @@
1
+ # nmtc-mapper πŸ—ΊοΈ
2
+
3
+ **Automated NMTC eligibility checker for addresses and census tracts.**
4
+
5
+ Pass a DataFrame of addresses and get back a boolean column for NMTC eligibility,
6
+ distress level, poverty rate, AMI ratio, and more β€” using official CDFI Fund and
7
+ Census Bureau data. No manual lookups required.
8
+
9
+ ---
10
+
11
+ ## Why nmtc-mapper?
12
+
13
+ The CDFI Fund provides a manual web tool (CIMS) for checking NMTC eligibility
14
+ one address at a time. nmtc-mapper automates this β€” pass 10,000 addresses and
15
+ get results in seconds, using the same official data source.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ pip install nmtc-mapper
22
+
23
+ ---
24
+
25
+ ## Quickstart
26
+
27
+ from nmtcmapper import NMTCMapper
28
+
29
+ mapper = NMTCMapper()
30
+
31
+ # Single address (geocodes automatically)
32
+ result = mapper.check_address("1234 S Michigan Ave, Chicago, IL 60605")
33
+ result.summary()
34
+ print(result.nmtc_eligible) # True
35
+ print(result.distress_level) # "severe"
36
+ print(result.poverty_rate) # 0.38
37
+
38
+ # Known census tract (no geocoding needed)
39
+ result = mapper.check_tract("17031840100")
40
+ print(result.nmtc_eligible) # True
41
+
42
+ # Batch β€” enrich a DataFrame of addresses
43
+ import pandas as pd
44
+ df = pd.read_csv("projects.csv") # must have 'address' column
45
+ df = mapper.enrich(df, address_col="address")
46
+ print(df["nmtc_eligible"].value_counts())
47
+ print(df["distress_level"].value_counts())
48
+
49
+ # If you already have census tract IDs
50
+ df = mapper.enrich(df, tract_col="tract_id")
51
+
52
+ # Summary stats
53
+ mapper.eligible_count(df)
54
+
55
+ ---
56
+
57
+ ## Eligibility Rules (2016-2020 ACS β€” mandatory since Sept 1, 2024)
58
+
59
+ A census tract qualifies as a Low-Income Community (LIC) if it meets ANY of:
60
+
61
+ - Poverty rate >= 20%
62
+ - Median Family Income <= 80% of metro/state AMI
63
+ - Median Family Income <= 85% of state AMI (high migration rural counties)
64
+
65
+ Distress levels:
66
+
67
+ - deep β€” Poverty >= 40% OR AMI <= 50% OR unemployment >= 2x national rate
68
+ - severe β€” Poverty >= 30% OR AMI <= 60% OR unemployment >= 1.5x national rate
69
+ - lic β€” NMTC eligible (meets LIC criteria)
70
+ - ineligible β€” Does not qualify
71
+
72
+ ---
73
+
74
+ ## Data Sources
75
+
76
+ - CDFI Fund 2016-2020 ACS Low-Income Community Eligibility File
77
+ https://www.cdfifund.gov/research-data
78
+ - US Census Bureau Geocoding API (free, no API key required)
79
+ https://geocoding.geo.census.gov
80
+
81
+ ---
82
+
83
+ ## Output Columns
84
+
85
+ After running .enrich(), your DataFrame will have:
86
+
87
+ - nmtc_eligible (bool)
88
+ - distress_level (str: deep / severe / lic / ineligible)
89
+ - poverty_rate (float)
90
+ - ami_ratio (float)
91
+ - unemployment_rate (float)
92
+ - is_non_metro (bool)
93
+ - severe_distress (bool)
94
+ - deep_distress (bool)
95
+
96
+ ---
97
+
98
+ ## Running Tests
99
+
100
+ PYTHONPATH=. pytest tests/ -v
101
+
102
+ 24 tests across all modules.
103
+
104
+ ---
105
+
106
+ ## Who This Is For
107
+
108
+ - CDEs screening project locations for NMTC eligibility
109
+ - CDFI analysts qualifying borrower locations at scale
110
+ - Researchers analyzing geographic distribution of LIC tracts
111
+ - Anyone replacing manual CIMS lookups with automated Python
112
+
113
+ ---
114
+
115
+ ## License
116
+
117
+ MIT 2026 Jaypatel1511
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: nmtc-mapper
3
+ Version: 0.1.0
4
+ Summary: Automated NMTC eligibility checker β€” geocode addresses and check Low-Income Community status using CDFI Fund and Census data
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/Jaypatel1511/nmtc-mapper
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pandas>=1.4.0
10
+ Requires-Dist: numpy>=1.21.0
11
+ Requires-Dist: requests>=2.27.0
12
+ Requires-Dist: openpyxl>=3.0.0
13
+
14
+ # nmtc-mapper πŸ—ΊοΈ
15
+
16
+ **Automated NMTC eligibility checker for addresses and census tracts.**
17
+
18
+ Pass a DataFrame of addresses and get back a boolean column for NMTC eligibility,
19
+ distress level, poverty rate, AMI ratio, and more β€” using official CDFI Fund and
20
+ Census Bureau data. No manual lookups required.
21
+
22
+ ---
23
+
24
+ ## Why nmtc-mapper?
25
+
26
+ The CDFI Fund provides a manual web tool (CIMS) for checking NMTC eligibility
27
+ one address at a time. nmtc-mapper automates this β€” pass 10,000 addresses and
28
+ get results in seconds, using the same official data source.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ pip install nmtc-mapper
35
+
36
+ ---
37
+
38
+ ## Quickstart
39
+
40
+ from nmtcmapper import NMTCMapper
41
+
42
+ mapper = NMTCMapper()
43
+
44
+ # Single address (geocodes automatically)
45
+ result = mapper.check_address("1234 S Michigan Ave, Chicago, IL 60605")
46
+ result.summary()
47
+ print(result.nmtc_eligible) # True
48
+ print(result.distress_level) # "severe"
49
+ print(result.poverty_rate) # 0.38
50
+
51
+ # Known census tract (no geocoding needed)
52
+ result = mapper.check_tract("17031840100")
53
+ print(result.nmtc_eligible) # True
54
+
55
+ # Batch β€” enrich a DataFrame of addresses
56
+ import pandas as pd
57
+ df = pd.read_csv("projects.csv") # must have 'address' column
58
+ df = mapper.enrich(df, address_col="address")
59
+ print(df["nmtc_eligible"].value_counts())
60
+ print(df["distress_level"].value_counts())
61
+
62
+ # If you already have census tract IDs
63
+ df = mapper.enrich(df, tract_col="tract_id")
64
+
65
+ # Summary stats
66
+ mapper.eligible_count(df)
67
+
68
+ ---
69
+
70
+ ## Eligibility Rules (2016-2020 ACS β€” mandatory since Sept 1, 2024)
71
+
72
+ A census tract qualifies as a Low-Income Community (LIC) if it meets ANY of:
73
+
74
+ - Poverty rate >= 20%
75
+ - Median Family Income <= 80% of metro/state AMI
76
+ - Median Family Income <= 85% of state AMI (high migration rural counties)
77
+
78
+ Distress levels:
79
+
80
+ - deep β€” Poverty >= 40% OR AMI <= 50% OR unemployment >= 2x national rate
81
+ - severe β€” Poverty >= 30% OR AMI <= 60% OR unemployment >= 1.5x national rate
82
+ - lic β€” NMTC eligible (meets LIC criteria)
83
+ - ineligible β€” Does not qualify
84
+
85
+ ---
86
+
87
+ ## Data Sources
88
+
89
+ - CDFI Fund 2016-2020 ACS Low-Income Community Eligibility File
90
+ https://www.cdfifund.gov/research-data
91
+ - US Census Bureau Geocoding API (free, no API key required)
92
+ https://geocoding.geo.census.gov
93
+
94
+ ---
95
+
96
+ ## Output Columns
97
+
98
+ After running .enrich(), your DataFrame will have:
99
+
100
+ - nmtc_eligible (bool)
101
+ - distress_level (str: deep / severe / lic / ineligible)
102
+ - poverty_rate (float)
103
+ - ami_ratio (float)
104
+ - unemployment_rate (float)
105
+ - is_non_metro (bool)
106
+ - severe_distress (bool)
107
+ - deep_distress (bool)
108
+
109
+ ---
110
+
111
+ ## Running Tests
112
+
113
+ PYTHONPATH=. pytest tests/ -v
114
+
115
+ 24 tests across all modules.
116
+
117
+ ---
118
+
119
+ ## Who This Is For
120
+
121
+ - CDEs screening project locations for NMTC eligibility
122
+ - CDFI analysts qualifying borrower locations at scale
123
+ - Researchers analyzing geographic distribution of LIC tracts
124
+ - Anyone replacing manual CIMS lookups with automated Python
125
+
126
+ ---
127
+
128
+ ## License
129
+
130
+ MIT 2026 Jaypatel1511
@@ -0,0 +1,22 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ nmtc_mapper.egg-info/PKG-INFO
5
+ nmtc_mapper.egg-info/SOURCES.txt
6
+ nmtc_mapper.egg-info/dependency_links.txt
7
+ nmtc_mapper.egg-info/requires.txt
8
+ nmtc_mapper.egg-info/top_level.txt
9
+ nmtcmapper/__init__.py
10
+ nmtcmapper/mapper.py
11
+ nmtcmapper/data/__init__.py
12
+ nmtcmapper/data/loader.py
13
+ nmtcmapper/data/schema.py
14
+ nmtcmapper/eligibility/__init__.py
15
+ nmtcmapper/eligibility/checker.py
16
+ nmtcmapper/geocoder/__init__.py
17
+ nmtcmapper/geocoder/census.py
18
+ tests/__init__.py
19
+ tests/conftest.py
20
+ tests/test_checker.py
21
+ tests/test_loader.py
22
+ tests/test_mapper.py
@@ -0,0 +1,4 @@
1
+ pandas>=1.4.0
2
+ numpy>=1.21.0
3
+ requests>=2.27.0
4
+ openpyxl>=3.0.0
@@ -0,0 +1,2 @@
1
+ nmtcmapper
2
+ tests
@@ -0,0 +1,10 @@
1
+ from nmtcmapper.mapper import NMTCMapper
2
+ from nmtcmapper.eligibility.checker import EligibilityResult
3
+ from nmtcmapper.data.loader import load_eligibility_table
4
+ from nmtcmapper.geocoder.census import geocode_address
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "NMTCMapper", "EligibilityResult",
9
+ "load_eligibility_table", "geocode_address",
10
+ ]
File without changes
@@ -0,0 +1,156 @@
1
+ """
2
+ Download and cache the CDFI Fund NMTC eligibility file.
3
+ Builds a lookup table of all eligible census tracts.
4
+ """
5
+ import os
6
+ import requests
7
+ import pandas as pd
8
+ from pathlib import Path
9
+
10
+ from nmtcmapper.data.schema import (
11
+ CACHE_DIR, CDFI_FUND_LIC_URL_2020,
12
+ ELIGIBILITY_FILE_COLUMNS,
13
+ LIC_POVERTY_RATE_THRESHOLD,
14
+ LIC_AMI_RATIO_METRO_THRESHOLD,
15
+ LIC_AMI_RATIO_RURAL_THRESHOLD,
16
+ SEVERE_POVERTY_THRESHOLD, SEVERE_AMI_THRESHOLD,
17
+ SEVERE_UNEMPLOYMENT_MULTIPLIER, NATIONAL_UNEMPLOYMENT_RATE,
18
+ DEEP_POVERTY_THRESHOLD, DEEP_AMI_THRESHOLD,
19
+ DEEP_UNEMPLOYMENT_MULTIPLIER,
20
+ )
21
+
22
+
23
+ def get_cache_dir() -> Path:
24
+ path = Path(CACHE_DIR)
25
+ path.mkdir(parents=True, exist_ok=True)
26
+ return path
27
+
28
+
29
+ def _cache_path(filename: str) -> Path:
30
+ return get_cache_dir() / filename
31
+
32
+
33
+ def download_eligibility_file(force: bool = False) -> Path:
34
+ filename = "NMTC_LIC_Eligibility_2016_2020.xlsx"
35
+ path = _cache_path(filename)
36
+ if path.exists() and not force:
37
+ print(f"Using cached eligibility file: {path}")
38
+ return path
39
+ print("Downloading NMTC eligibility file from CDFI Fund...")
40
+ try:
41
+ response = requests.get(CDFI_FUND_LIC_URL_2020, stream=True, timeout=120)
42
+ response.raise_for_status()
43
+ with open(path, "wb") as f:
44
+ for chunk in response.iter_content(chunk_size=8192):
45
+ f.write(chunk)
46
+ print(f"Saved to {path}")
47
+ return path
48
+ except Exception as e:
49
+ print(f"Download failed: {e}")
50
+ return None
51
+
52
+
53
+ def load_eligibility_table(force: bool = False) -> pd.DataFrame:
54
+ path = download_eligibility_file(force=force)
55
+ if path is None or not path.exists():
56
+ print("Using built-in sample eligibility data.")
57
+ return _build_sample_table()
58
+ print(f"Loading eligibility table from {path}...")
59
+ try:
60
+ df = pd.read_excel(path, dtype=str)
61
+ return _process_eligibility_table(df)
62
+ except Exception as e:
63
+ print(f"Error loading file: {e}. Using sample data.")
64
+ return _build_sample_table()
65
+
66
+
67
+ def _process_eligibility_table(df: pd.DataFrame) -> pd.DataFrame:
68
+ df.columns = df.columns.str.strip().str.upper()
69
+ col_map = {k: v for k, v in ELIGIBILITY_FILE_COLUMNS.items() if k in df.columns}
70
+ df = df.rename(columns=col_map)
71
+ if "tract_id" not in df.columns:
72
+ if all(c in df.columns for c in ["state", "county", "tract"]):
73
+ df["tract_id"] = (
74
+ df["state"].str.zfill(2) +
75
+ df["county"].str.zfill(3) +
76
+ df["tract"].str.zfill(6)
77
+ )
78
+ for col in ["poverty_rate", "ami_ratio", "unemployment_rate"]:
79
+ if col in df.columns:
80
+ df[col] = pd.to_numeric(df[col], errors="coerce")
81
+ for col in ["is_non_metro", "is_high_migration_rural"]:
82
+ if col in df.columns:
83
+ df[col] = df[col].isin({"Y", "YES", "1", "True", "TRUE", "X"})
84
+ df = _compute_eligibility(df)
85
+ if "tract_id" in df.columns:
86
+ df = df.set_index("tract_id")
87
+ print(f"Eligibility table loaded: {len(df):,} census tracts")
88
+ return df
89
+
90
+
91
+ def _compute_eligibility(df: pd.DataFrame) -> pd.DataFrame:
92
+ pr = df.get("poverty_rate", pd.Series(dtype=float))
93
+ ami = df.get("ami_ratio", pd.Series(dtype=float))
94
+ unemp = df.get("unemployment_rate", pd.Series(dtype=float))
95
+ non_metro = df.get("is_non_metro", pd.Series(False, index=df.index))
96
+
97
+ poverty_lic = pr >= LIC_POVERTY_RATE_THRESHOLD
98
+ ami_lic = (
99
+ (non_metro & (ami <= LIC_AMI_RATIO_RURAL_THRESHOLD)) |
100
+ (~non_metro & (ami <= LIC_AMI_RATIO_METRO_THRESHOLD))
101
+ )
102
+ df["nmtc_eligible"] = poverty_lic | ami_lic
103
+
104
+ sev_poverty = pr >= SEVERE_POVERTY_THRESHOLD
105
+ sev_ami = ami <= SEVERE_AMI_THRESHOLD
106
+ sev_unemp = unemp >= (NATIONAL_UNEMPLOYMENT_RATE * SEVERE_UNEMPLOYMENT_MULTIPLIER)
107
+ df["severe_distress"] = sev_poverty | sev_ami | sev_unemp
108
+
109
+ deep_poverty = pr >= DEEP_POVERTY_THRESHOLD
110
+ deep_ami = ami <= DEEP_AMI_THRESHOLD
111
+ deep_unemp = unemp >= (NATIONAL_UNEMPLOYMENT_RATE * DEEP_UNEMPLOYMENT_MULTIPLIER)
112
+ df["deep_distress"] = deep_poverty | deep_ami | deep_unemp
113
+
114
+ def distress_label(row):
115
+ if row.get("deep_distress"):
116
+ return "deep"
117
+ elif row.get("severe_distress"):
118
+ return "severe"
119
+ elif row.get("nmtc_eligible"):
120
+ return "lic"
121
+ return "ineligible"
122
+
123
+ df["distress_level"] = df.apply(distress_label, axis=1)
124
+ return df
125
+
126
+
127
+ def _build_sample_table() -> pd.DataFrame:
128
+ sample_tracts = [
129
+ ("17031840100", 0.38, 0.55, 0.12, False, False),
130
+ ("17031839100", 0.42, 0.48, 0.15, False, False),
131
+ ("17031010100", 0.18, 0.92, 0.04, False, False),
132
+ ("36061015900", 0.35, 0.60, 0.11, False, False),
133
+ ("36061019100", 0.28, 0.72, 0.09, False, False),
134
+ ("36047052200", 0.14, 0.88, 0.05, False, False),
135
+ ("26163518300", 0.45, 0.45, 0.18, False, False),
136
+ ("26163520100", 0.32, 0.62, 0.13, False, False),
137
+ ("13121010400", 0.29, 0.68, 0.10, False, False),
138
+ ("48113010900", 0.22, 0.78, 0.07, False, False),
139
+ ("17019000100", 0.15, 0.95, 0.03, True, True),
140
+ ("26001010100", 0.18, 0.88, 0.06, True, False),
141
+ ]
142
+ rows = []
143
+ for tid, pr, ami, unemp, non_metro, high_migration in sample_tracts:
144
+ rows.append({
145
+ "tract_id": tid,
146
+ "state": tid[:2],
147
+ "poverty_rate": pr,
148
+ "ami_ratio": ami,
149
+ "unemployment_rate": unemp,
150
+ "is_non_metro": non_metro,
151
+ "is_high_migration_rural": high_migration,
152
+ })
153
+ df = pd.DataFrame(rows)
154
+ df = _compute_eligibility(df)
155
+ df = df.set_index("tract_id")
156
+ return df
@@ -0,0 +1,68 @@
1
+ """
2
+ Column mappings, eligibility thresholds, and constants for NMTC eligibility.
3
+ Based on 2016-2020 ACS data β€” mandatory for QLICIs closed on or after Sept 1, 2024.
4
+ Source: https://www.cdfifund.gov/research-data
5
+ """
6
+
7
+ # ── Eligibility Thresholds ────────────────────────────────────────────────────
8
+
9
+ # Low-Income Community (LIC) criteria β€” Section 45D
10
+ LIC_POVERTY_RATE_THRESHOLD = 0.20 # >= 20% poverty rate
11
+ LIC_AMI_RATIO_METRO_THRESHOLD = 0.80 # <= 80% of metro/state AMI
12
+ LIC_AMI_RATIO_RURAL_THRESHOLD = 0.85 # <= 85% of state AMI (high migration rural)
13
+
14
+ # Severe Distress thresholds
15
+ SEVERE_POVERTY_THRESHOLD = 0.30 # >= 30% poverty rate
16
+ SEVERE_AMI_THRESHOLD = 0.60 # <= 60% of AMI
17
+ SEVERE_UNEMPLOYMENT_MULTIPLIER = 1.5 # >= 1.5x national unemployment rate
18
+
19
+ # Deep Distress thresholds
20
+ DEEP_POVERTY_THRESHOLD = 0.40 # >= 40% poverty rate
21
+ DEEP_AMI_THRESHOLD = 0.50 # <= 50% of AMI
22
+ DEEP_UNEMPLOYMENT_MULTIPLIER = 2.0 # >= 2x national unemployment rate
23
+
24
+ # National unemployment rate benchmark (2016-2020 ACS)
25
+ NATIONAL_UNEMPLOYMENT_RATE = 0.057 # 5.7%
26
+
27
+ # ── CDFI Fund Eligibility File Column Mappings ────────────────────────────────
28
+ # Source: 2016-2020 ACS Low-Income Community Eligibility file from cdfifund.gov
29
+
30
+ ELIGIBILITY_FILE_COLUMNS = {
31
+ "GEOID": "tract_id",
32
+ "STATE": "state",
33
+ "COUNTY": "county",
34
+ "TRACT": "tract",
35
+ "POVERTY_RATE": "poverty_rate",
36
+ "MFI_RATIO": "ami_ratio",
37
+ "UNEMPLOYMENT_RATE": "unemployment_rate",
38
+ "NON_METRO": "is_non_metro",
39
+ "HIGH_MIGRATION_RURAL": "is_high_migration_rural",
40
+ "LIC_ELIGIBLE": "lic_eligible_raw",
41
+ "SEVERE_DISTRESS": "severe_distress_raw",
42
+ }
43
+
44
+ # ── Download URLs ─────────────────────────────────────────────────────────────
45
+ CDFI_FUND_LIC_URL_2020 = (
46
+ "https://www.cdfifund.gov/sites/cdfi/files/2024-08/"
47
+ "NMTC_LIC_Eligibility_2016_2020_ACS.xlsx"
48
+ )
49
+
50
+ # ── Cache ─────────────────────────────────────────────────────────────────────
51
+ import os
52
+ CACHE_DIR = os.path.join(os.path.expanduser("~"), ".nmtcmapper", "cache")
53
+
54
+ # ── Census Geocoder API ───────────────────────────────────────────────────────
55
+ CENSUS_GEOCODER_URL = (
56
+ "https://geocoding.geo.census.gov/geocoder/geographies/address"
57
+ )
58
+ CENSUS_GEOCODER_BATCH_URL = (
59
+ "https://geocoding.geo.census.gov/geocoder/geographies/addressbatch"
60
+ )
61
+
62
+ # ── Distress Levels ───────────────────────────────────────────────────────────
63
+ DISTRESS_LEVELS = {
64
+ "deep": "Deep Distress β€” highest need, strongest NMTC application score",
65
+ "severe": "Severe Distress β€” qualifies for 85% investment commitment",
66
+ "lic": "Low-Income Community β€” NMTC eligible",
67
+ "ineligible": "Not NMTC eligible",
68
+ }
File without changes