Skip to main content

nmtc-mapper 🗺️

Automated NMTC eligibility checker for addresses and census tracts.

Pass a DataFrame of addresses and get back a tri-state nmtc_eligible column (True / False / None), distress level, poverty rate, AMI ratio, and more — using official CDFI Fund and Census Bureau data. No manual lookups required.

nmtc_eligible is Optional[bool]: True (verified eligible), False (verified ineligible — the CDFI Fund file explicitly says NO), or None (indeterminate — the address could not be geocoded, or the tract is absent from the ~85k-tract universe). None is not a falsy "ineligible": treating it as False fabricates a verified-ineligible answer. The additive eligibility_status column names the four outcomes explicitly — verified-eligible / verified-ineligible / not-found / geocode-failed.


Why nmtc-mapper?

The CDFI Fund provides a manual web tool (CIMS) for checking NMTC eligibility one address at a time. nmtc-mapper automates this — pass 10,000 addresses and get results in seconds, using the same official data source.


Installation

pip install nmtc-mapper

Quickstart

from nmtcmapper import NMTCMapper

mapper = NMTCMapper()

# Single address (geocodes automatically)
result = mapper.check_address("1234 S Michigan Ave, Chicago, IL 60605")
result.summary()
print(result.nmtc_eligible)      # True / False / None (None = indeterminate)
print(result.eligibility_status) # "verified-eligible" | "verified-ineligible"
                                 #  | "not-found" | "geocode-failed"
print(result.distress_level)     # "deep" / "severe" / "lic" / "ineligible" / "unknown"
print(result.poverty_rate)       # 0.38  (None if the tract is indeterminate)

# Known census tract (no geocoding needed)
result = mapper.check_tract("17031840100")
print(result.nmtc_eligible)    # True

# Batch — enrich a DataFrame of addresses
import pandas as pd
df = pd.read_csv("projects.csv")   # must have 'address' column
df = mapper.enrich(df, address_col="address")
print(df["nmtc_eligible"].value_counts())
print(df["distress_level"].value_counts())

# If you already have census tract IDs
df = mapper.enrich(df, tract_col="tract_id")

# Summary stats
mapper.eligible_count(df)

Failure behavior & offline / demo mode

NMTCMapper() downloads the official CDFI Fund eligibility and Opportunity Zone files (cached under ~/.nmtcmapper/cache). As of 0.3.4 it fails loud: if a download or parse fails, it raises a typed error instead of silently substituting demo data. (Before 0.3.4 any failure silently fell back to a 12-tract synthetic sample, which could report a real, eligible tract as "ineligible" — see the CHANGELOG.)

from nmtcmapper import NMTCMapper, NMTCMapperError

try:
    mapper = NMTCMapper()
except NMTCMapperError as e:
    # Blocked network, moved URL, corrupt file, etc. — never a fabricated answer.
    print(f"Could not load real NMTC data: {e}")
    raise

The exception hierarchy (NMTCMapperErrorEligibilityDataError / OZDataError → specific *DownloadError / *ParseError leaves) is exported from the top level, so you can catch broadly or precisely.

Explicit demo / offline data — for examples, tests, or an air-gapped demo, opt in to the synthetic sample dataset. This performs no network calls and stamps the mapper so you can tell demo answers from real ones:

from nmtcmapper import NMTCMapper, load_sample_table

mapper = NMTCMapper.from_sample()   # 12 sample tracts + 6 OZ tracts, offline
print(mapper.data_source)           # "sample"   (real data → "cdfi_fund")

df = load_sample_table()            # the raw 12-tract sample frame

⚠️ Sample data is 12 synthetic-vintage tracts for demos and tests. It is never valid for a real NMTC eligibility answer.


Eligibility Rules (2016-2020 ACS — mandatory since Sept 1, 2024)

A census tract qualifies as a Low-Income Community (LIC) if it meets ANY of:

  • Poverty rate >= 20%
  • Median Family Income <= 80% of metro/state AMI
  • Median Family Income <= 85% of state AMI (high migration rural counties)

Distress levels:

  • deep — the tract carries the CDFI Fund's deep-distress designation
  • severe — the tract carries the CDFI Fund's severe-distress designation
  • lic — NMTC eligible (meets LIC criteria) but not flagged severe/deep
  • ineligible — Does not qualify
  • unknown — indeterminate: geocode no-match, or the tract is absent from the eligibility universe (paired with nmtc_eligible = None; never "ineligible")

How distress is determined. For the official CDFI Fund file (the live .xlsb download), severe_distress and deep_distress are read directly from the Fund's own pre-computed columns — the package does not recompute them from ACS variables. The CDFI Fund's published criteria for those designations are, for reference, poverty >= 30% / MFI <= 60% AMI / unemployment >= 1.5x national (severe) and poverty >= 40% / MFI <= 50% AMI / unemployment >= 2x national (deep). A threshold-based fallback (_compute_eligibility) exists only for the generic CSV path and the built-in synthetic sample; it is not used for the official file.


Data Sources


Output Columns

After running .enrich(), your DataFrame will have:

  • nmtc_eligible (Optional[bool]: True / False / None — None = indeterminate)
  • eligibility_status (str: verified-eligible / verified-ineligible / not-found / geocode-failed)
  • distress_level (str: deep / severe / lic / ineligible / unknown)
  • poverty_rate (Optional[float])
  • ami_ratio (Optional[float])
  • unemployment_rate (Optional[float])
  • is_non_metro (bool)
  • severe_distress (bool)
  • deep_distress (bool)

Known Issues

is_opportunity_zone is unreliable — a False may be a vintage miss. The Opportunity Zone list is the CDFI Fund's Dec 2018 designated-QOZ file, and OZs were designated on 2010 census tracts (legally fixed to them). The geocoder returns 2020 tracts, and 1,408 of the 8,764 OZ designations (~16%) have no matching 2020 GEOID (they split/merged/renumbered after 2010). So an address in one of those designations reports Opportunity Zone: No even though it is in a designated OZ. A Yes is trustworthy; a No is not — it may mean "not an OZ" or "OZ with no 2020 GEOID", and the package cannot yet tell them apart. This is pre-existing (not introduced or worsened by 0.4.1's geocoder change). A tri-state fix (Optional[bool]) is slated for 0.5.0 — see the CHANGELOG.

is_nmtc_native_area is always False — it means "not determined," not "not a native area." No column in the live CDFI Fund .xlsb file feeds this field, so it is False for all 85,395 tracts. Native areas (Federal Indian Reservations, Off-Reservation Trust Lands, Hawaiian Home Lands, Alaska Native Village Statistical Areas) are a real NMTC Areas of Higher Distress criterion, but the CDFI Fund publishes it separately from the LIC eligibility file this package loads. Pre-existing since 0.1.0; 0.4.1 does not change it. Resolution deferred to 0.5.0 — see the CHANGELOG.


Running Tests

PYTHONPATH=. pytest tests/ -v

99 tests across all modules (including fail-loud, explicit-sample-mode, tri-state eligibility, and async-batch coverage).


Who This Is For

  • CDEs screening project locations for NMTC eligibility
  • CDFI analysts qualifying borrower locations at scale
  • Researchers analyzing geographic distribution of LIC tracts
  • Anyone replacing manual CIMS lookups with automated Python

License

MIT 2026 Jay Patel

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nmtc_mapper-0.4.1.tar.gz (45.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nmtc_mapper-0.4.1-py3-none-any.whl (29.8 kB view details)

Uploaded Python 3

File details

Details for the file nmtc_mapper-0.4.1.tar.gz.

File metadata

  • Download URL: nmtc_mapper-0.4.1.tar.gz
  • Upload date:
  • Size: 45.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for nmtc_mapper-0.4.1.tar.gz
Algorithm Hash digest
SHA256 d039ef7fb7081e215a8d01c60989e05f46614836365417903c9bfdd12ba72a73
MD5 587e4aa61f0fd4c4f3eb05b8182b5727
BLAKE2b-256 03762ac99ec42629759448297e8224af8e7da6851bdfe3fe80ccdbe1026b3cb4

See more details on using hashes here.

Provenance

The following attestation bundles were made for nmtc_mapper-0.4.1.tar.gz:

Publisher: release.yml on Jaypatel1511/nmtc-mapper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nmtc_mapper-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: nmtc_mapper-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 29.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for nmtc_mapper-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 75465376cc557e962cf9be1158660d250ed94f2b5dbea8f520484f3ed4ce9ff3
MD5 1e662bc4d33874b7cbe7b6b47ea7e76a
BLAKE2b-256 355e7f9f2c567779caf7676f1670b9710013c6c165f4f6fccf58118ad945ccd7

See more details on using hashes here.

Provenance

The following attestation bundles were made for nmtc_mapper-0.4.1-py3-none-any.whl:

Publisher: release.yml on Jaypatel1511/nmtc-mapper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.0

2 files

0.4.3

2 files

0.4.2

2 files

This release

0.4.1 This release

2 files

0.4.0

2 files

0.3.4

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page