Skip to contents

Opens a lab on an scr_select() result. Inside it the analyst inspects the optimal bins of any binned variable (scr_classing_view()), proposes new breaks or groupings (scr_classing_propose()), reads the comparison against the optimal bins, accepts or discards each proposal with a mandatory reason (scr_classing_accept(), scr_classing_discard()), chooses the final variable list (scr_classing_choose()) and commits everything to a new scr_result (scr_classing_apply()) that the rest of the pipeline consumes unchanged: scr_scorecard(), scr_apply(), scr_sql(), scr_export().

Usage

scr_coarse_classing(
  x,
  features = NULL,
  laplace = 0,
  max_iv_loss = NULL,
  author = Sys.info()[["user"]]
)

Arguments

x

An object from scr_select().

features

Variables the lab covers. Default: every variable that reached binning (names(x$fit$results)), so a variable failed by screening can be rebinned and forced in with a reason.

laplace

Smoothing added to the bin counts when recomputing WOE. 0 (default) is exactly the engine's formula.

max_iv_loss

Advisory threshold: a manual bin whose hold-out IV falls more than this fraction below the optimal one raises IV_LOSS_VS_OPTIMAL. NULL uses config$lab_max_iv_loss.

author

Free text recorded in the ledger.

Value

An scr_classing object (the lab), with a print method that summarizes the session: variables touched, before/after IV, verdicts, reasons, pending proposals and the final choice.

Contract of a manual bin

A manual bin is recomputed on the training rows only (hold-out rows can never define a bin), with the engine's own WOE formula (ln(%event / %non-event), event-oriented, so glm coefficients stay positive), then revalidated on the hold-out with the bins frozen (IV, PSI with both thresholds, unbinned share) and screened with the eight engine rules, so the lab and the pipeline can never disagree. Numeric intervals are right-closed, (a, b], exactly as the engine and its SQL. Re-declaring the optimal cut points of a numeric reproduces the engine's WOE exactly; for a categorical the engine applies a small internal smoothing of its own, so the raw log-ratio of the lab differs from it in the third decimal.

What is never allowed silently

An empty bin, a degenerate bin (no events or no non-events, unless laplace > 0), a bin below lab_min_bin_pct_hard, a manual IV crossing iv_max (the lab must not manufacture leakage), a category left unassigned, a missing reason. Those block the proposal (BLOCKED); accepting one needs override = TRUE, and the override is itself a ledger row.

Examples

cfg <- scr_config(verbose = FALSE, nthread = 1, use_ranger = FALSE,
                  xgb_rounds = 60, n_boot = 20)
res <- scr_select(scr_demo, "default", config = cfg, drop = "id",
                  date_col = "ref_date")
lab <- scr_coarse_classing(res)
lab
#> <scr_classing> target "default" | opened 2026-10-02 00:02 by runner | 37 variables | 0 proposals: 0 accepted, 0 discarded
#>   final choice: 12 variables | consensus 12 | force: (none) | drop: (none)
scr_classing_view(lab, "ds_region")
#> <scr_classing> ds_region (categorical) | current: optimal (jedi) | 5 bins | train IV 0.0846, hold-out IV 0.0971 (ratio 1.14)
#>   monotone: yes | min bin 7.9% | PSI 0.0064 (stable) | KS 0.114 | degenerate bins: 0 | verdict: ACCEPTABLE
#>    id  bin                                  n      %  events   rate      WOE      IV |  n.hold      %    rate WOE.hold
#>     1  CENTRE                             539  19.2%      57  10.6%   -0.341   0.020 |     301  21.5%   13.0%   -0.130
#>     2  EAST                             1,139  40.7%     144  12.6%   -0.139   0.008 |     551  39.4%   13.8%   -0.058
#>     3  WEST                               582  20.8%      82  14.1%   -0.014   0.000 |     269  19.2%   10.0%   -0.419
#>     4  NORTH                              318  11.4%      66  20.8%    0.453   0.027 |     178  12.7%   23.6%    0.599
#>     5  SOUTH                              222   7.9%      50  22.5%    0.557   0.030 |     101   7.2%   18.8%    0.312
#>   event rate by bin (train | hold-out)
#>     1  ########            10.6% | ##########          13.0%
#>     2  ##########          12.6% | ###########         13.8%
#>     3  ###########         14.1% | ########            10.0%
#>     4  ################    20.8% | ##################  23.6%
#>     5  #################   22.5% | ##############      18.8%
p <- scr_classing_propose(lab, "ds_region",
                          groups = list(edge = c("NORTH", "SOUTH"),
                                        core = c("EAST", "WEST", "CENTRE")))
p
#> <scr_classing_proposal> P001 ds_region | groups = list(edge = c("NORTH", "SOUTH"), core = c("EAST", "WEST", "CENTRE")) | 2026-10-02 00:02
#>                         optimal     manual      delta
#>   n_bins                      5          2         -3
#>   iv_train               0.0846     0.0739    -0.0107
#>   iv_holdout             0.0971     0.0786    -0.0186
#>   iv_ratio               1.1432     1.0568    -0.0864
#>   ks                     0.1141     0.1141     0.0000
#>   psi                    0.0064     0.0003    -0.0061
#>   min_bin_pct            0.0793     0.1929     0.1136
#>   largest_bin_pct        0.4068     0.8071     0.4004
#>   n_degenerate                0          0          0
#>   monotonic                   1          1          0
#>   manual bins (train | hold-out)
#>     1  NORTH | SOUTH                      540  19.3%  21.5%   0.499 |     279  19.9%  21.9%   0.501
#>     2  EAST | WEST | CENTRE             2,260  80.7%  12.5%  -0.149 |   1,121  80.1%  12.7%  -0.156
#>   Warnings
#>     - IV_LOSS_VS_OPTIMAL
#>   Verdict: REVIEW - advisory warnings only; accept with a reason or discard.
lab <- scr_classing_accept(lab, p, reason = "edge/core is what pricing uses")
lab <- scr_classing_choose(lab, drop = "vl_score_10",
                           reason = "not available at decision time")
lab
#> <scr_classing> target "default" | opened 2026-10-02 00:02 by runner | 37 variables | 1 proposals: 1 accepted, 0 discarded
#>   variable                   action      bins          IV train       IV hold-out verdict     reason
#>   ds_region                  accepted  5->2     0.0846->0.0739     0.0971->0.0786   REVIEW      edge/core is what pricing uses
#>   final choice: 11 variables | consensus 12 | force: (none) | drop: vl_score_10
res2 <- scr_classing_apply(lab)
scr_selected(res2)
#>  [1] "vl_score_01" "vl_score_02" "vl_score_04" "ds_band"     "vl_late"    
#>  [6] "ds_region"   "vl_score_06" "vl_score_07" "vl_score_05" "ds_channel" 
#> [11] "vl_hist_04" 
scr_selected(res2, which = "consensus")
#>  [1] "vl_score_01" "vl_score_02" "vl_score_04" "ds_band"     "vl_late"    
#>  [6] "ds_region"   "vl_score_06" "vl_score_07" "vl_score_05" "ds_channel" 
#> [11] "vl_hist_04"  "vl_score_10"
sc <- scr_scorecard(res2)
sc$model_card$binning_algorithm
#> [1] "jedi, manual"