Call.Report
GitHub
GitHub

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 codeMeaning
UBPR4340Net Income
UBPRE001Return on Average Assets
UBPRE003Return on Average Equity
UBPRE013Net Interest Margin
UBPRE088Efficiency 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.

navigate · open · Esc close