Entimema

Automating Credit Vintage Analysis: From R Cohort Calculations to an AI Portfolio Analyst

Entimema
Contents

A credit-risk analyst can build an excellent vintage matrix in R. Yet every month the same analyst may still extract data, rerun calculations, compare cohorts, inspect MOB curves, identify divergence, filter products and channels, assess EAD, investigate and prepare commentary.

The calculation is automated. The investigation often is not. Vintage analysis becomes operationally powerful when cohort calculation, baseline comparison, deterioration detection and investigation are separated into controlled analytical layers.

INSIGHTS

What does vintage analysis reveal?

The companion Credit Vintage Analysis research develops portfolio behaviour and interpretation.

ENGINEERING

How do we operationalise it?

This build begins where that methodology ends: repeatable calculation, monitoring and investigation at portfolio scale.

Controlled dates turn portfolio records into comparable cohorts

An original hypothetical account-observation dataset uses loan_id, customer_id, origination_date, observation_date, product, channel, ead, default_flag, dpd and risk_grade. Exact schemas vary. Engineering requires a consistent origination definition, controlled observation dates, reproducible outcomes and uniquely identifiable exposures.

Vintageᵢ = Period(OriginationDateᵢ)   |   monthly: YYYY-MM
Origination cohort

Monthly grouping is illustrative; quarterly or another defensible granularity may better fit production volume and risk emergence.

MOBᵢ,ₜ = Months(ObservationDateₜ − OriginationDateᵢ)
Months on book

Calendar-date comparison mixes seasoning. Vintage analysis aligns Vintage A at MOB 6 with Vintage B at MOB 6, not whichever observations happen to share a reporting month.

library(dplyr)
library(lubridate)

portfolio_vintage <- portfolio %>%
  mutate(
    vintage = floor_date(origination_date, "month"),
    mob = interval(origination_date, observation_date) %/% months(1)
  )

Use a cumulative default rate consistently

CDRᵥ,ₘ = Defaultsᵥ,≤ₘ / EligibleAccountsᵥ
Account-weighted cumulative default rate

The denominator is the controlled origination population. Each loan contributes once; ever_default records whether its first default occurred on or before the MOB. Delinquency, loss or balance deterioration could replace this outcome, but should not be mixed into the same metric.

cohort_accounts <- portfolio_vintage %>%
  group_by(vintage) %>%
  summarise(eligible_accounts = n_distinct(loan_id), .groups = "drop")

vintage_summary <- portfolio_vintage %>%
  group_by(vintage, mob) %>%
  summarise(defaults_to_date = n_distinct(loan_id[ever_default == 1]),
            .groups = "drop") %>%
  left_join(cohort_accounts, by = "vintage") %>%
  mutate(default_rate = defaults_to_date / eligible_accounts)
DefaultRateᴱᴬᴰᵥ,ₘ = DefaultedOriginationExposureᵥ,≤ₘ / EligibleOriginationExposureᵥ
EAD-weighted cumulative default rate
ead_vintage <- portfolio_vintage %>%
  group_by(vintage, mob) %>%
  summarise(
    eligible_ead = sum(origination_ead[!duplicated(loan_id)], na.rm = TRUE),
    defaulted_ead = sum(origination_ead[!duplicated(loan_id)] * ever_default[!duplicated(loan_id)], na.rm = TRUE),
    ead_default_rate = defaulted_ead / eligible_ead,
    .groups = "drop"
  )

This freezes exposure at origination for a coherent illustrative denominator. Current EAD, default-date EAD and survival-adjusted designs answer different questions and require explicit eligibility, timing and denominator rules. Account weighting can hide a few large deteriorating exposures; EAD weighting can reveal their economic concentration.

The vintage matrix is the deterministic foundation

The following entirely hypothetical portfolio contains five monthly cohorts. Rates are cumulative, so observed values never fall. January and February establish a stable reference, March is weaker, April diverges at MOB 4, and May is too immature for a conclusion.

Hypothetical cumulative default rates
VintageMOB 1MOB 2MOB 3MOB 4MOB 5MOB 6
2026-01 · reference0.2%0.4%0.6%0.8%1.0%1.2%
2026-02 · reference0.2%0.4%0.5%0.8%1.1%1.3%
2026-03 · weaker0.3%0.5%0.8%1.2%1.6%2.0%
2026-04 · diverging0.3%0.6%0.9%1.6% △2.2% ▲2.7% ▲
2026-05 · immature0.2%0.5%NYO —NYO —NYO —NYO —
△ moderate deterioration; ▲ material deterioration; NYO means not yet observed. Symbols, labels and colour jointly communicate state.

Not Yet Observed is not zero. A genuine observed 0.0% is a performance result; a future cell contains no observation. Storage and presentation must preserve that distinction.

A baseline makes divergence measurable

Baselineₘ is a reference at equivalent MOB: perhaps a historical median, selected reference vintages or weighted historical average. No construction is universally correct. Here the median is illustrative.

Deviationᵥ,ₘ = Metricᵥ,ₘ − Baselineₘ
RelativeDeviationᵥ,ₘ = (Metricᵥ,ₘ − Baselineₘ) / Baselineₘ
Absolute and relative vintage deviation

Positive deviation means deterioration when higher is worse. Compare MOB 6 with MOB 6. Relative change improves scale comparison but can exaggerate movement where the baseline is very small.

baseline <- vintage_summary %>%
  group_by(mob) %>%
  summarise(baseline_rate = median(default_rate, na.rm = TRUE), .groups = "drop")

vintage_diagnostics <- vintage_summary %>%
  left_join(baseline, by = "mob") %>%
  mutate(deviation = default_rate - baseline_rate)

This begins diagnostics; it is not complete decision logic. One point may be noise.

Persistenceᵥ,ₘ = Σᵐₖ₌ₘ₋ₕ I(Deviationᵥ,ₖ > c)
Persistence
Velocityᵥ,ₘ = Metricᵥ,ₘ − Metricᵥ,ₘ₋₁
Deterioration velocity
Materialityᵥ,ₘ = f(Deviation, Exposure, PopulationSize)
Priorityᵥ,ₘ = f(Deviation, Persistence, Velocity, Exposure)
Materiality and investigation priority

The threshold c, lookback, materiality logic and priority policy are controlled configuration—not universal constants. A small severe vintage may matter less immediately than moderate deterioration across a major cohort.

ENTIMEMA FRAMEWORKThe Vintage DiagnosticFour distinct signals rank attention without forcing one universal score.
  1. Divergence
  2. Persistence
  3. Velocity
  4. Materiality
  5. Investigation priority

The automation opportunity begins after the chart

DETERMINISTIC RExtract portfolioRun RBuild vintage matrix
MANUAL INVESTIGATIONInspect chartIdentify cohortFilter productFilter channelCompare EADInvestigatePrepare commentary
The matrix can be automated while repeated diagnostic drill-down remains analyst-driven.

A reusable Vintage Engine would expose conceptual, controlled interfaces—not one analyst's notebook:

build_vintages()          calculate_mob()
calculate_vintage_metric() calculate_baseline()
detect_deviation()         measure_persistence()
compare_segments()         rank_vintages()
INPUTS

Versioned portfolio snapshot plus methodology and configuration.

ENGINE

Cohorts, MOB, performance, baseline, deviation, persistence and exposure materiality.

OUTPUTS

Structured diagnostics with lineage, observation status and comparable seasoning.

The engine does not need an LLM. Its path is Portfolio Data → Cohort Assignment → MOB → Performance → Matrix → Baseline → Deviation → Persistence → Materiality.

Place an AI Portfolio Analyst above the engine

The Agent consumes structured deterministic outputs. It asks which vintages are deteriorating, when divergence began, whether it persists, whether it is material, what segment explains it and where analyst attention belongs.

The engine calculates; the Agent investigates
Vintage EngineAI Portfolio Analyst
Builds cohortsInvestigates cohorts
Calculates MOBInterprets seasoning
Calculates metrics and baselineExplains divergence
Detects deviation and persistenceInvestigates drivers
Returns structured dataSynthesises analyst-ready evidence
AgentAnalytical toolDeterministic resultAgent reasoningAnalyst review
get_vintage_matrix()
compare_vintage_to_baseline()
get_vintage_deviation()
get_segment_breakdown(vintage)
compare_account_vs_ead()
get_top_deteriorating_vintages()

These are proposed interfaces, not claims of existing production tools.

Which recent vintages are materially underperforming the historical baseline?

The Agent calls get_top_deteriorating_vintages(); it does not guess. From the hypothetical matrix and a January–February reference it can return:

VINTAGE / 2026-04
Divergence begins
MOB 4
Current deviation
+1.45 pp vs 1.25% MOB 6 baseline
Persistence
3 observed MOB points
Exposure
€42m
Concentration
Product B / Channel X
Account / EAD signal
Moderate / high
Next investigation
Underwriting and channel mix

Start broad, then isolate the source

PortfolioVintageProductChannelRisk gradeExposure
Is deterioration broad-based or concentrated?

get_segment_breakdown(vintage) can recursively compare product, channel, risk grade and geography where analytically relevant. The hierarchy varies; the principle is progressive isolation using the same governed data.

Ask whether the signal exists in accounts, exposure or both

Metricᵃᶜᶜᵒᵘⁿᵗˢ elevated alone may indicate many smaller cases. Metricᴱᴬᴰ elevated alone may reveal a few large exposures. Both elevated indicates broad and economically meaningful deterioration. Denominator differences must remain visible.

Separate performance from composition

A deteriorating vintage can reflect worse borrower performance, different borrower composition, or both. Compare risk-grade, product and channel mix and the exposure distribution. If the break coincides with a changed origination month, the Agent may examine acquisition channel, average exposure or approval-strategy mix.

Seasoning, baseline and alert controls keep the workflow honest

Immaturity

A cohort at MOB 2 cannot be compared with MOB 12. The Agent must state that evidence is immature.

Missing triangle

Future cells remain Not Yet Observed, distinct from observed zero.

Baseline drift

Monitor whether macro or portfolio conditions changed; historical average is not permanent truth.

Lineage

Retain snapshot, outcome, denominator, configuration and engine versions behind every result.

Backtest the alert, not only the rate

If the engine would have flagged April at MOB 4, later ask whether divergence persisted, outcomes worsened, the alert was early, or it was noise. Compare champion and challenger baselines, persistence logic, materiality logic and thresholds on historical vintages without optimising blindly to historical noise.

Agent guardrails

  • Never invent missing portfolio data or calculate uncontrolled metrics from prose.
  • Never compare non-equivalent MOB periods or treat NYO as zero.
  • Never infer causality without evidence.
  • Never change baseline methodology or risk thresholds autonomously.
  • Never classify a vintage without structured engine evidence and lineage.
  • Escalate immature, missing, conflicting or out-of-scope evidence to analyst review.

The resolve is recurring portfolio intelligence

The finished path is Methodology → R implementation → deterministic analytical engine → AI-assisted workflow → analyst review. R preserves cohort arithmetic. The engine standardises comparison and diagnostics. The Agent queries controlled evidence, ranks attention and frames investigation. The analyst owns interpretation and action.