Skip to main content

cdfi-benchmark 📊

CDFI and MDI peer benchmarking tool using FDIC call report data.

Pull call report financials for any FDIC-insured CDFI or MDI, compute key performance metrics, build a peer group of similar institutions, and generate a benchmarking report — using the free FDIC BankFind Suite API, no API key required.


Why cdfi-benchmark?

CDFI banks and MDIs benchmark their performance against peers manually — pulling call report data from FFIEC, computing ratios in Excel, and building comparison tables by hand. cdfi-benchmark automates the entire workflow in Python.


Installation

pip install cdfi-benchmark

Quickstart

from cdfibenchmark import (
    get_financials, build_peer_group,
    generate_report, summary_table,
)

# Pull call report data for City First Bank, N.A. (CERT 34352) — a real
# CDFI/MDI in Washington, DC. Look a CERT up with search_institutions()
# rather than copying one; a cert and a name are bound by the FDIC, not
# by this README.
institution = get_financials(cert=34352)

# Build peer group — the banks NEAREST the institution in assets, not the
# largest ones in its window. The group's real breadth is set by max_peers
# (50), NOT by the +/-50% asset window: for 98.1% of filers that window
# already holds more than 50 banks and does not bind at all. No API key
# needed. Peers are pinned to the institution's own report_date unless you
# pass another.
peers = build_peer_group(institution, same_state=True)

# How the group was chosen, in words, including that the window and the
# group size are this tool's own choices.
print(peers.selection_basis)
# Where the institution sits INSIDE its own peer group by assets. Near 50
# means the group brackets it; 0 or 100 means the comparison is size-skewed.
print("subject asset percentile:", peers.asset_percentile)

# Anything that makes the peer group less than ideal is on the group and
# is rendered on the report — a dropped same-state constraint, a group
# below min_peers, a mixed reporting period, a size-skewed group.
for caveat in peers.caveats:
    print("CAVEAT:", caveat)

# Generate benchmarking report
report = generate_report(institution, peers)
print(report)

# Get results as DataFrame — includes `basis` and `threshold_source`
df = summary_table(institution, peers)

Sample Data (No API Required)

Everything in this block is invented. The institution does not exist, its CERT is outside the FDIC's issued range, and build_sample_peer_group generates its peers by scaling these figures pseudo-randomly. No report built from it describes any real bank. Use it to see the output shape, never as data.

from cdfibenchmark import build_sample_peer_group
from cdfibenchmark.data.schema import InstitutionProfile

institution = InstitutionProfile(
    cert=99001,                                   # not an issued FDIC cert
    name="Riverstone Community Bank (SYNTHETIC)",
    city="Los Angeles",
    state="CA",
    report_date="20241231",                       # Q4 — a full-year period
    total_assets=655_000,
    total_deposits=520_000,
    net_loans=380_000,
    net_income=1_950,
    interest_income=28_000,
    interest_expense=8_000,
    non_interest_income=3_500,
    non_interest_expense=22_000,
    total_equity=48_000,
    tier1_ratio=12.2,
)

peers = build_sample_peer_group(institution)
report = generate_report(institution, peers)
print(report)

Because this profile carries no FDIC-published ratios, NIM/ROAA/ROAE fall back to the computed proxy — so the report shows their values with a Basis: line and grades NIM N/A. That is the intended behaviour, not a bug; see Period basis below.


Metrics Computed

Metric Source field Benchmark (Strong) Threshold provenance
NIM FDIC NIMY >= 3.5% HOUSE
Efficiency Ratio FDIC EEFFR <= 60% HOUSE
ROAA FDIC ROA >= 1.0% HOUSE
ROAE FDIC ROE >= 10% HOUSE
Tier 1 Leverage Ratio FDIC RBC1AAJ >= 8% 12 CFR 324.12 / 324.403
Loans-to-Deposits LNLSNET / DEP 50%–80% (band) HOUSE
NPL Ratio NCLNLS / LNLSGR <= 1.0% HOUSE
Reserve Coverage LNATRES / NCLNLS >= 100% HOUSE

Status is graded against thresholds, never against the peer group

status compares the institution's value to the fixed thresholds in the table above. It does not read the peer median or the percentiles, which are reported alongside it and answer a different question. A metric can grade STRONG while sitting below the peer median, and ADEQUATE while sitting entirely outside the peer range — both happen on real banks at 20260630. The rendered report carries the same sentence beside the table.

Threshold provenance

tier1_ratio is the only metric whose thresholds come from a published regulatory standard. Bank capital has one; earnings, efficiency, funding and reserve-coverage ratios do not — the FDIC publishes these series and reports them against a peer group in the UBPR, but publishes no required or "well capitalized"-equivalent cut point for any of them.

Every other threshold in this package is therefore a HOUSE rule of thumb: this tool's own, marked "source": "HOUSE" in BENCHMARKS, defined by HOUSE_-prefixed constants, and rendered on every report as "this tool's own threshold (HOUSE), not a regulatory or supervisory standard". Treat them as a starting point to be argued with, not as a standard to be met.

Loans-to-deposits is graded as a band, not a ladder. Above the band is funding strain; below it is under-deployment, which for a CDFI bank is its own failure. All three boundaries are house numbers. Calibration note, measured against the 50 banks nearest CERT 34352 in assets at 20260630 — selected from the 763 in its +/-50% asset window, retrieved 2026-09-05: this band grades 13 of 50 WEAK, 11 of them for exceeding 95%, and the peer median of 85.19% grades ADEQUATE. The WEAK tail is therefore almost entirely the funding-strain edge, not the under-deployment floor — which is the band doing what it was added to do. The boundaries are deliberately conservative; they have not been fitted to any population.


Period basis — read this before comparing a quarter

FDIC call-report income items (INTINC, EINTEXP, NETINC) are year-to-date. At a Q1 REPDTE they cover three months. Dividing them by a point-in-time balance and grading the result against an annual-basis threshold reads roughly 4x low — a healthy bank grades WEAK. Measured for CERT 34352 at 20260331, computed versus FDIC's own published series: NIM 4.30x, ROAA 4.12x, ROAE 4.01x.

This package does not annualize an estimate. It prefers FDIC's own published ratios — NIMY, ROA, ROE, EEFFR — which are already annualized and computed over the correct average denominators, which is the basis the thresholds are calibrated to. That is a measurement, not a projection.

When a published ratio is absent (a hand-built InstitutionProfile, or a field the API omitted) the computed proxy is used, its basis is rendered on the report, and it is not graded:

Metric Computed fallback Graded?
NIM net interest income / total assets, YTD Never — the 3.5% threshold is calibrated to NIMY, which is over average earning assets. A larger denominator biases it low at every period, including Q4.
ROAA / ROAE YTD net income / period-end balances Only at a Q4 REPDTE, where the flow covers the full year.
Efficiency Ratio (NONIX - EAMINTAN) / ((INTINC - EINTEXP) + NONII) Always — numerator and denominator are YTD flows over the same period, so the period cancels exactly. Annualizing it would introduce an error.
Tier 1, L/D, NPL, Reserve Coverage period-end balances only Always — no flow item, no period error.

Labels follow the basis. "Return on Average Assets (ROAA)" is used only when the value is FDIC's published ROA; the computed fallback renders as "Return on Assets, period-end (ROAA)", because that is what was actually divided by what.

A value that is reported but not graded shows its measurement and an explicit Not graded: line. The number is never hidden — only the grade is withheld.


The Tier 1 Leverage Ratio thresholds follow bank-capital regulation, not an arbitrary target: Strong >= 8% is the Community Bank Leverage Ratio (CBLR) qualifying level (12 CFR 324.12, lowered from 9% effective 2026-07-01) and Adequate >= 5% is the leverage-ratio minimum for "well capitalized" under Prompt Corrective Action (12 CFR 324.403).


Asset Size Buckets

These bands are this tool's own (HOUSE_ASSET_BUCKETS). They are not the FFIEC CRA small/intermediate/large-bank asset thresholds, not the FDIC community-bank definition, and not a UBPR peer-group band. "Large" in particular is a supervisory term that means specific, different things elsewhere; this bucket is not any of them.

  • micro — Under $50MM
  • small — $50MM to $250MM
  • medium — $250MM to $1B
  • large — $1B to $5B
  • mega — Over $5B

The report renders the bucket with its boundaries and this attribution beside it, so the word never travels alone. ASSET_BUCKETS remains as an alias.


Data Source

FDIC BankFind Suite API — free public API, no authentication required. Data covers all FDIC-insured institutions with quarterly call report data since 1934.

https://api.fdic.gov/banks

The historical host banks.data.fdic.gov/api now answers HTTP 301 and redirects here. Requests still succeed through the redirect, which is why the move went unnoticed; the package now calls the canonical host directly.


Error handling

The data layer fails loud. In an early-warning / anomaly-detection pipeline a silently-empty or fabricated result reads as "nothing anomalous" and masks the real problem, so the FDIC fetchers raise typed errors instead of swallowing failures:

  • FDICAPIError — a transport problem: the request never produced a usable response body. Network/timeout errors, non-2xx HTTP status, and JSON decode failures all raise this.
  • FDICResponseError — the response decoded but its structure is wrong, either at the envelope level (the top-level data key absent, null, or not a list) or at the field level inside a record: a record missing its CERT identity, a CERT that isn't int-coercible, or any core/optional financial field that is present but not numeric. A bad record is never coerced into a phantom cert=0 bank or a fabricated 0.0.
  • Legitimately empty is not an error. A successful request that returns zero rows ({"data": []}) returns the empty value for that fetcher — None, an empty DataFrame, or [] — and does not raise.

Missing-but-not-garbage fields inside an otherwise valid record are kept, not dropped: an absent core financial (e.g. ASSET) becomes NaN (unknown — it propagates to any metric computed from it rather than fabricating 0.0), and an absent optional ratio (e.g. RBCT1J) becomes None. A real present 0.0 is preserved as 0.0.

Both error types subclass CDFIBenchmarkError, so callers can catch the contract broadly or distinguish "the API is unreachable" (FDICAPIError) from "the API changed its shape" (FDICResponseError):

from cdfibenchmark import FDICAPIError, FDICResponseError

try:
    institution = get_financials(cert=34352)
except FDICAPIError:
    ...   # transport/HTTP/decode failure — retry or alert
except FDICResponseError:
    ...   # wrong-shape or present-but-garbage field — contract problem

Running Tests

PYTHONPATH=. pytest tests/ -v

Every gate in this suite was run RED before the fix it covers was written.


Who This Is For

  • CDFI banks and MDIs benchmarking against peers (FDIC-insured banks only — credit unions are NCUA-regulated and are not covered by this API or this tool)
  • MDI management teams preparing board reports
  • CDFI Fund analysts reviewing institution performance
  • Impact investors evaluating CDFI bank investments
  • Researchers studying community banking performance trends

License

MIT 2026 Jaypatel1511

Release files for cdfi-benchmark 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cdfi-benchmark 0.3.0
File Size Uploaded
cdfi_benchmark-0.3.0.tar.gz 113.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cdfi-benchmark 0.3.0
File Interpreter ABI Platform
cdfi_benchmark-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 155.9 kB

Release files / cdfi_benchmark-0.3.0.tar.gz

Download URL cdfi_benchmark-0.3.0.tar.gz
Size 113.6 kB
Tags Source
SHA-256 checksum
How to use checksums
3c9e450788140a607ac7bc39e3ac2f3745e47805bf4916a591ca18b72721a9a3
BLAKE2b-256 checksum
How to use checksums
cb8f028522e021c907ebec3b571ce9c7c2dc695fa2b3506cd42fb38b86f09ab3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release files / cdfi_benchmark-0.3.0-py3-none-any.whl

Download URL cdfi_benchmark-0.3.0-py3-none-any.whl
Size 42.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5dbd47810f33abf7efca1115a60c24acdce108023afd1673890804749d5e5404
BLAKE2b-256 checksum
How to use checksums
c0eaf6bc36eda0e1c392d791103e1f85679cc15ad404a00cdb40874801e61ddf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page