scr_bands() cuts a score into percentile or tail bands frozen on a
reference sample and reports, per band and sample, the event rate with
a Jeffreys interval, lift, capture, KS, WOE, IV, PSI and a one-sided
Fisher exact test of rank order, plus the AUC, Gini and KS with a
bootstrap interval. Accepts a scorecard, a data.frame (with weights,
a value column and sample labels) or pre-aggregated counts.scr_tiers() fits 2 to 9 tiers (labeled from "low" to "high", with
words for 2 to 7 tiers) by an exact dynamic program under share, event
and distinctness constraints, by event-rate anchors or by equal shares;
round_to gives policy-friendly cuts, and n_boot measures the
stability of the cuts. An infeasible tier count falls back and is
recorded in a ledger.scr_rag() lights discrimination, calibration, stability and, for a
scorecard, the variables red, amber or green, per period or segment
with by; the thresholds are an editable table from scr_rag_plan().scr_apply(), scr_sql() and scr_export() assign the bands or tiers
in R and in SQL and write the study to a workbook. Tier labels carry
their order in production ("01.very high" for the tier with the
highest event rate, down to "05.very low"), so they sort from the
event-richest tier; the tiers table has the same value in tier_label,
and numbered = FALSE gives the plain labels.scr_claims() tests statements such as "rate >= 60% for scores of 625
or more" on a band, a tier or a score range, with an exact one-sided
binomial test, a one-sided Jeffreys bound and a Holm adjustment, and
writes each verdict ("supported", "refuted" or "not proven") as one
sentence. type = "floor" tests the weakest end of the group under a
monotone fit of the rate instead of its average.scr_operating() finds the cut that maximizes the value of targeting,
alerting or approval under volume, share, budget, event-rate and daily
capacity constraints, and reports the binding constraint and its
shadow price.scr_score_cross() crosses two scores on the same rows: the table of
their bands with one or two outcomes, Spearman's rho and Kendall's
tau-b, and the overlap of the rows each selects, with the swap-in and
swap-out rates.scr_export(); the vignette
"Score studies" walks through them on credit, fraud and churn scores.scr_mix_shift() splits the change of the event rate between two
samples, or between each period and a base, into a mix effect and a
rate effect per band; the effects add up to the change exactly.scr_segments() reads one score on many segments: AUC with the DeLong
standard error and a test of equal AUC, observed against expected
events, the log-odds offset and the slope ratio, with a suggested
action per segment.scr_maturity() estimates the cumulative incidence of the event by
band and horizon under censoring (Kaplan-Meier with Greenwood's
variance), and the discrimination at each horizon.scr_uplift() reads a score on a treated and a control group: uplift
per band with the Newcombe interval, the Qini coefficient and the AUUC
with bootstrap intervals, and a randomization check.scr_overlap() compares rule flags with the alerts of a score: what
each catches, the incremental recall of one over the other and the
rules the score makes redundant.scr_detection() measures, per alert threshold, the fraud episodes
detected, the fraudulent transactions and the time before the first
alert, and the loss prevented.scr_export().study_bands,
study_level, tier_min_pct, tier_min_events and tier_max_bins.scr_pd_validate() gives the light "grey" to a row without a testable
result (a missing p-value) instead of NA, and the overall light is
"grey", not "green", when no row has a testable result.
scr_ead_validate() does the same and adds the overall light. In
scr_lgd_validate(), a summary row over pools or drivers that are all
grey is now grey, not green.scr_monitor() and the CSI timelines of scr_export() compute the CSI
as scr_psi() does: a bin empty in both samples is left out of the
smoothing and of the degrees of freedom of the adjusted threshold, and
the CSI and its critical value are missing when the period has no rows
in the bins or fewer than two bins are populated. Other results are
unchanged.scr_strategy() freezes crossing$cut on the training scores, like the
bands: midway between the training scores on either side of the band
edge of the crossing. score >= cut, the convention of scr_cutoff(),
then reproduces the split of the bands on train and on any score seen in
training, so rows at a band edge seen in training no longer change side
between the two functions. breaks given as a number of intervals now
also gets a cut, taken on the evaluated sample.scr_scorecard() test each band against
the previous one with a one-sided Fisher exact test. The binomial test
used before took the previous band's rate as known and flagged too many
breaks when that band was small.scr_scorecard() says when ties in the training score give fewer score
bands than score_groups.scr_metrics() draws its bootstrap on the counts per score value when
the score has many ties (at most one distinct value for every ten rows:
scorecard points, a grade scale, a WOE score on a large sample), so the
cost of a resample follows the number of distinct scores, not of rows.
The intervals of such scores keep their distribution but change for a
given seed, wherever they are reported; scores with fewer ties keep the
row resampling and their intervals. The band tables of the scorecard,
scr_strategy(), scr_reject() and scr_psi() assign the bands as
integer indices, without a factor per row. The DeLong standard error of
scr_pd_validate() is computed from the counts and can differ in the
last digits (about 1e-16). No other result changes.scr_strategy() reports the event and non-event distributions of each
band (pct_event, pct_nonevent, odds_event and log_odds, the band
WOE) and the score where they cross (crossing); rule = "crossing"
sets the decisions at that boundary.scr_strategy() follows the objective of the scorecard: under propensity
the table starts at the most likely band, revenue_good is the revenue
of an event and the decisions are "target", "review" and "skip".
Credit and fraud results are unchanged.scr_score_gains() adds pct_event, pct_nonevent and woe. Under
higher_is_riskier, odds and log_odds are now events per non-event,
the orientation of the scale, so log_odds rises with the score in both
directions.Variable_Gains_IV sheet of scr_export() add
pct_event and pct_nonevent per bin.utilisation, realised and the "grey" traffic light).scr_lgd_downturn() and scr_ead_downturn() estimate the observed
downturn impact, and the LGD reference value, on the training rows only,
like the long-run averages; the hold-out stays independent evidence.scr_export(), scr_sql(file = ) and the classing lab functions follow
the verbose key of the object's configuration, as the function that
fitted it does.IV_RATIO_UNSTABLE, an advisory warning, when the
train IV of a proposal is below iv_min and the hold-out/train IV ratio
carries little information.scr_irb_params() lists the regulatory texts behind the presets; users
check the tables against the texts in force before any regulatory use.inst/cheatsheet (PDF and its HTML
source).The IRB layer: from the scorecard to regulatory risk parameters, with the same contracts as the scorecard pipeline (one configuration, ledgers with mandatory reasons, hold-out revalidation with frozen bins, hardened workbooks, production SQL verified against DuckDB and SQLite). Regimes are parameter tables selected by a preset, never prose.
scr_irb_params() ships the numbers of three presets ("bcb",
"basel3_final", "crr3"): PD floors, LGD input floors, foundation LGD,
standardised CCFs, asset correlations, maturity rules, output floor and
standardised risk weights; the tables are editable and edits are recorded.scr_default() builds the default flag from a monthly panel (days past
due with absolute and relative materiality, unlikeliness to pay,
probation, restructuring, obligor-level pulling effect);
scr_default_rate() gives the default rates by cohort, grade, segment
and exposure, with the long-run average and its benchmark.scr_bin_continuous() bins drivers against a bounded continuous target
(LGD, CCF) and returns an object with the shape of the engine's, so
OptimalBinningWoE::obwoe_apply() and obwoe_sql() reproduce the bin
means in R and in every SQL dialect; hold-out revalidation with frozen
cut points and PSI.scr_config() gains the keys of stages 8 to 12 (default_*, pd_*,
lgd_*, ccf_*, framework, capital_*, ecl_*), all registered in
scr_config_keys() and validated.scr_demo_panel, scr_demo_lgd, scr_demo_lgd_cashflows,
scr_demo_rates, scr_demo_ead and scr_demo_portfolio are new
demonstration data.scr_master_scale(), scr_calibrate() (intercept shift, log-odds
(a, b), scaling, quasi-moment matching; a new alignment, the scorecard
untouched), scr_grades() (geometric, quantile or supplied grades, merges
below the minimum counts, monotone repair recorded), scr_moc()
(estimation error computed; other categories with a mandatory reason),
scr_pd() (floors from the preset), scr_migration(),
scr_pd_validate() (Jeffreys, binomial, normal, Hosmer-Lemeshow,
multi-period, AUC against the initial value, PSI, migration bandwidths,
concentration; traffic lights), scr_pd_pit_ttc(), with predict(),
scr_apply(), scr_sql() (grade and PD as a CASE on the score) and
scr_export() methods.scr_workout() (discounted recoveries and costs, cures, merged
re-defaults, extrapolated incomplete workouts, named funnel rules),
scr_lgd() (cure stage on the binary engine, severity stage on the
continuous binner with a fractional logit or a beta regression, hold-out
revalidation, pools), scr_lgd_downturn(), scr_lgd_floor(),
scr_elbe(), scr_lgd_validate(), with scr_apply(), scr_sql() (both
stages, pool CASE, floored result) and scr_export() methods.scr_ead_data() (realised conversion factors under a fixed, cohort
or variable horizon; conversion factor below and limit factor above a
utilisation threshold; named funnel rules), scr_ead() (driver bins with
admission rules, pools, estimation-error margin, standardised floor),
scr_ead_downturn(), scr_ead_validate(), with scr_apply(),
scr_sql() and scr_export() methods.scr_el(), scr_irb_rw() (the risk-weight
function with correlations, size adjustment, maturity, floors and the
defaulted case), scr_sa_rw(), scr_capital() (reconciliation by
segment, output floor, provisions shortfall and excess, floors impact,
sensitivity grid, concentration), scr_pd_stress(), scr_ecl()
(survival-weighted 12-month and lifetime expected credit loss with
stages and scenarios), with scr_sql() (constants per pool, no normal
quantile at run time) and scr_export() methods.scr_irb_rw() and scr_capital() read the supervisory LGD of the
foundation approach from params$lgd_firb through a claim type;
scr_sa_rw() and scr_capital() apply the non-granular retail weight
with granular = FALSE; the Hosmer-Lemeshow light of
scr_pd_validate() is green when every grade sits on the conservative
side (the PD above the observed rate), since the statistic is two-sided.betareg (Suggests) powers the beta severity engine of scr_lgd().scr_sql() on a scorecard gains what = "all" (bin label, WOE and
points of every variable next to the exact score and the whole-points
score) and keep_columns (key columns carried into the output), for a
deployment that reports the band of each variable with the score.scr_iv() ignores NA for every group
type; scr_classing_read() validates the separator and the spec carries
it into scr_classing_import(); the TOO_MANY_BINS screening rule can
fire (the screen reads max_bins); scr_psi() stores and prints its
thresholds; scr_default_rate() reports one long-run mean and
benchmarks an optional lra_adjusted; one asset_class configuration
key replaces pd_asset_class and capital_asset_class;
scr_lgd_downturn() always records a reason; the traffic-light
convention is red at or below the first threshold in PD, LGD and EAD;
scr_apply() on an scr_ead takes what; scr_fetch() gains
verbose and scr_run() follows config$verbose; the scr_demo
columns carry English names (vl_partial_*, vl_noise_*,
vl_constant, vl_near_const, vl_duplicate, vl_redundant,
vl_late, ds_region, ds_band, ds_channel, ds_high_card).data.table) and read the columns
without copying them. The kernels are tested against their reference R
implementations.scr_bin() (Pearson or Spearman): each WOE
column is ranked once and the correlation matrix comes from one BLAS
cross-product, instead of ranking both columns of every pair. The greedy
sweep gives exactly the same result as
OptimalBinningWoE::obwoe_prune(), and is about 50 times faster at 150
columns.O(n log n) (Knight's algorithm). It replaces an O(n^2) Kendall
computation inside a 200-resample bootstrap. The EAD version is also
exact now: it no longer groups the prediction into 60 quantile buckets.scr_ecl() streams the survival-weighted loss row by row and applies
the scenario shocks on the fly. Memory is O(n) whatever the term (the
matrix version built several n x T copies, about 2.9 GB each at
n = 1e6, T = 360). Results are the same to 1e-15.scr_metrics() ranks the scores once; each bootstrap resample then
re-tabulates counts, with no sort and no grouping by a double key. The
cut-off sweep sorts once per sample. The monitor tabulates the base
once for every period. Default rates by cohort use one rolling join.
Every LGD cash-flow aggregation and EAD reference date is vectorised.config$nthread.scr_triage() and the R pre-processing of scr_apply()
reserve column slots before adding columns, so they no longer fail past
about 1024 columns..Random.seed is restored
on exit, and a bootstrap advances the user's stream only by the
replicate seeds it draws. Results for a given seed are
unchanged.scr_split():
integer64 columns are read
correctly;scr_bin(): under allow_derived_final = FALSE, derived flags leave
before the redundancy pruning, so a flag can no longer remove a real
column. The Rcpp subset-proxy warnings of
OptimalBinningWoE::obwoe_gains_score() are muffled; its values are
correct, and the fix belongs upstream.scr_config() validates every key of stages 0 to 7.min_sum_hessian_in_leaf. The xgboost API is detected
from xgb.train() (it works with xgboost 3).obwoe_sql() does. A row that falls in no fitted bin takes the points
of WOE 0, in R and in SQL.scr_metrics() refuses a factor or a non-0/1 outcome, and counts are
kept in double to avoid integer overflow;scr_psi() leaves bands empty in both samples out of the index and
out of the degrees of freedom;scr_monitor() keeps undated rows as a period;scr_default() could assign one unit's flags to another under
locales where the grouping order differed from the C-locale sort; it
is fixed, and the state machine is now vectorised;PSI_ACTION gate of the LGD and EAD drivers compared against a
flag scr_psi() never returns, so it never fired; it now does;K degrees of freedom;sd(DR_t - PD_t) (BCBS WP 14);scr_moc() no longer edits the caller's ledger;scr_ead_data() paired default dates with facilities after a sort,
so unsorted input put defaults on the wrong facility; it is fixed;scr_ead() no longer needs data.table 1.15;ccf_measure = "lf" keeps its pools;ead + drawings - recovered;scr_elbe() uplift starts at zero at the date of default;scr_lgd_downturn(), scr_lgd_floor() and scr_ead_validate() no
longer modify their input.PD = 1e-5 below that point,
where 1 - 1.5 b approaches zero and the risk weight exploded or
turned negative without a PD floor;scr_monitoring_plan() is the monitoring contract: created by
scr_scorecard(), written to the Monitoring_Plan sheet, and read back by
scr_monitor(plan = ) (a table or the strategy workbook), which now takes
its PSI/CSI thresholds, alpha and min_events_per_period from it.scr_scorecard() stores the hold-out bin index of every variable, so the
Stability_CSI_Timeline sheet is a real timeline by vintage without a
scr_monitor() object.options(scorecraft.parallel = "fork" | "psock" | "serial") selects
the parallel backend; results are identical under the three.scr_triage() is parallel by column as well.NULL that surfaces later as a subscript error), warnings
raised in a worker are re-raised in the parent, PSOCK workers run with a
single data.table thread, and a data.table returned by a worker is
re-allocated so that := works on it.options(scorecraft.fork_mem_fraction = 0.75) caps the fork workers by
the memory available on Linux (Inf to disable), since forked workers
duplicate the parent heap once the garbage collector runs.First release. A production-grade scorecard engine for binary targets, built on 'OptimalBinningWoE': audit funnel, single configuration, named relaxation, first-class scale alignment, cut-off strategy and hardened deliverables.
scr_select() and
scr_scorecard(): scr_split(), scr_triage(), scr_bin(),
scr_model(), scr_align(), scr_cutoff()/scr_strategy()/scr_reject().scr_align() is a first-class stage: banded log-odds regression on the raw
score composed with the PDO map, odds_orientation recorded, applied to
any engine. It runs automatically inside scr_scorecard().scr_sql() emits production SQL in fourteen dialects: a pre-processing
CTE, the WOE/BIN transformation from the authoritative cut points and, for
a scorecard, the exact score plus whole points from the bin index. R-SQL
equivalence is verified by test against DuckDB and SQLite.scr_bin() is parallelised by column (nthread), with the fits merged;
the result is identical to the serial one.scr_metrics() always reports a bootstrap confidence interval for
AUC/KS/Gini; scr_psi() reports the fixed threshold next to the
sample-size-adjusted critical value of Yurdakul and Naranjo (2020).scr_scorecard(challenger = ) fits a tree challenger (xgboost or
lightgbm) on the same WOE columns, aligned to the same scale, with
supports_scorecard = FALSE.scr_reject() implements honest reject inference: population scope, band
coverage and a 2x/4x/8x sensitivity band, never parcelling by default.scr_monitor() recomputes PSI/CSI (with the signed points shift) and the
performance by vintage on new data; it never schedules itself.scr_export() writes four hardened .xlsx workbooks (selection,
scorecard, validation, strategy), the SQL files and a Markdown summary.scr_coarse_classing() opens a manual binning lab: scr_classing_view(),
scr_classing_propose() (breaks, groups, merge, split, missing_to,
other_to, reset), scr_classing_accept()/scr_classing_discard() with a
mandatory reason, scr_classing_choose() (keep/drop/force),
scr_classing_spec()/scr_classing_read()/scr_classing_import() for a
CSV/xlsx round trip, scr_classing_apply() to commit into a new
scr_result, and scr_decisions() for the append-only ledger. Manual
bins share the engine's contract, so scr_scorecard(), scr_apply() and
scr_sql() follow them unchanged; the funnel gains provenance.scr_connect() accepts any DBI driver next to an ODBC DSN, and
scr_fetch() samples server-side with a dialect-aware random expression.