ffiec-data-connect (Python)
Peer analysis
Build a peer comparison from UBPR — pull one bank's ratios and the peer aggregate, compare and visualize.
UBPR computes standardized ratios for every Call Report filer and assigns every bank to a numbered peer group. Peer comparison is the canonical UBPR workflow. This page walks one through end-to-end.
Scenario
We want to see how one community bank — Cambridge Trust Co., RSSD 37 — stacks up on the four classic profitability and asset-quality ratios for the most recent period.
Step 1 — pull UBPR for the bank
from ffiec_data_connect import (
OAuth2Credentials,
collect_ubpr_reporting_periods,
collect_ubpr_facsimile_data,
)
creds = OAuth2Credentials(...)
periods = collect_ubpr_reporting_periods(creds, output_type="list")
period = periods[-1]
rows = collect_ubpr_facsimile_data(
creds,
rssd_id="37",
reporting_period=period,
output_type="pandas",
force_null_types="pandas",
)
rows is the same MDRM-per-line shape as Call Report data, but the MDRM codes are
UBPR codes. A few examples:
| UBPR code | Meaning |
|---|---|
UBPR4340 | Net Income |
UBPRE001 | Return on Average Assets |
UBPRE003 | Return on Average Equity |
UBPRE013 | Net Interest Margin |
UBPRE088 | Efficiency Ratio |
Exact codes change slightly between vintages; always check against the UBPR User Guide for the quarter you’re pulling.
Step 2 — pull the peer aggregate
Every UBPR facsimile row carries its peer-group aggregates alongside the bank’s own value. In the REST response they are separate rows with a modified MDRM, typically the bank row’s code with a peer-group suffix.
A cleaner approach for multi-bank peer work is to pull the panel of filers and compute the aggregates yourself from the Call Report data. For a one-bank sanity check, the UBPR facsimile’s embedded peer values are sufficient.
Step 3 — compare
import pandas as pd
def value_for(df: pd.DataFrame, mdrm: str) -> float | None:
hit = df.loc[df["mdrm"] == mdrm]
if hit.empty:
return None
row = hit.iloc[0]
if row["data_type"] == "float":
return row["float_data"]
if row["data_type"] == "int":
return row["int_data"]
return None
ratios = {
"ROA": "UBPRE001",
"ROE": "UBPRE003",
"NIM": "UBPRE013",
"Efficiency Ratio": "UBPRE088",
}
summary = pd.DataFrame(
{"Cambridge Trust": {label: value_for(rows, code) for label, code in ratios.items()}}
)
print(summary)
Step 4 — put it in context
A ratio alone is noise. Get the peer percentile from the UBPR facsimile, or compute your own peer aggregate from the panel:
from ffiec_data_connect import collect_filers_on_reporting_period
panel = collect_filers_on_reporting_period(creds, reporting_period=period, series="call")
# ... filter by asset-size band, state, charter type to define your peer group ...
# ... then pull UBPR for each peer RSSD and aggregate percentiles ...
This is a multi-call operation; make it ~200–300 peers to stay well inside the rate budget, or use the UBPR bulk file if your peer set is larger.
Visualizing
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 4))
summary.plot.bar(ax=ax)
ax.set_ylabel("Percent (or ratio)")
ax.set_title(f"Cambridge Trust — UBPR key ratios, {period}")
ax.set_xticklabels(ax.get_xticklabels(), rotation=0)
plt.tight_layout()
Caveats
- UBPR rates use average balances, not period-end. Homemade ratios from raw Call Report data won’t match exactly.
- UBPR peer-group definitions change over time. Don’t carry a peer-group number across many years without checking its current definition.
- A single-quarter snapshot can mislead — always pair with a short trend.
See also
- UBPR — full section on ratio structure.
- Bulk download — pulling the full peer set efficiently.