
Optimal binning and Weight of Evidence transformation for credit scoring and risk modelling, with 37 C++ binning algorithms behind one R interface.
The package covers the whole path from a raw feature store to a deployed scorecard: fit the binning, screen the variables against IV strength and bin ordering, transform the data, and export the same transformation as SQL so the scoring runs where the data lives.
| 37 algorithms | 21 numerical, 16 categorical — entropy, \(\chi^2\), exact optimisation, shape-constrained and streaming methods |
| C++ engine | Rcpp/RcppEigen throughout; 500 variables over 20,000 rows bin and screen in about two seconds |
| Automated screening | obwoe_select() returns a verdict and a reason for every
candidate, never dropping a row |
| In-database scoring | obwoe_sql() emits exact CASE expressions
for 14 SQL dialects |
| Regulatory fit | monotonic binning, auditable bin-level evidence, reason codes |
| tidymodels ready | step_obwoe() is a first-class, tunable
recipes step |
install.packages("OptimalBinningWoE")
# development version
# install.packages("pak")
pak::pak("evandeilton/OptimalBinningWoE")The Statlog (German Credit) benchmark ships with the package, so the example below runs as-is.
library(OptimalBinningWoE)
german <- read.csv(
gzfile(system.file("extdata", "germancredit.csv.gz",
package = "OptimalBinningWoE")),
stringsAsFactors = FALSE
)
german$default <- 1L - german$credit_risk
german$credit_risk <- NULL
model <- obwoe(german, target = "default", min_bins = 2, max_bins = 6)
summary(model)1. Fit. One call bins every column, routing numerical and categorical variables to the right algorithm.
model <- obwoe(german, target = "default", max_bins = 6)2. Screen. obwoe_select() applies the
two criteria that govern variable admission — Information Value strength
and guaranteed rank ordering — and returns one row per candidate with
the decision and the reason behind it.
sel <- obwoe_select(model)
sel[, c("feature", "total_iv", "iv_class", "ks", "monotonic", "selected", "reason")]
#> feature total_iv iv_class ks monotonic selected reason
#> 1: credit_history 0.2932 Medium 0.1805 TRUE TRUE OK
#> 2: duration 0.2727 Medium 0.1900 TRUE TRUE OK
#> ...
#> 14: status 0.6660 Suspicious 0.3671 TRUE FALSE IV_SUSPICIOUS
#> 15: number_credits 0.0101 Unpredictive 0.0481 TRUE FALSE IV_BELOW_MINstatus is the strongest variable in the file and is
rejected for it: an IV above 0.50 is far more often leakage than signal.
Nothing is silently dropped — 500 candidates return 500 rows, so the
automatic verdict can be reviewed.
3. Transform. Apply the fitted binning to any frame,
in R or inside a recipes pipeline.
scored <- obwoe_apply(new_data, model)
rec <- recipes::recipe(default ~ ., data = german) |>
step_obwoe(recipes::all_predictors(), outcome = "default", max_bins = 6)4. Deploy. Export the same transformation as SQL.
obwoe_sql(model, table = "risk.applications",
features = sel$feature[sel$selected], dialect = "postgres")CASE
WHEN duration IS NULL THEN 0
WHEN duration <= 7 THEN -1.3121863889661687
WHEN duration > 7 AND duration <= 10 THEN -0.45198512374305744
WHEN duration > 10 AND duration <= 16 THEN -0.3028722379457563
WHEN duration > 16 AND duration <= 33 THEN 0.10461674470498177
WHEN duration > 33 AND duration <= 39 THEN 0.5728610146854435
WHEN duration > 39 THEN 0.993901334579079
ELSE 0
END AS duration_woeIntervals are half-open on the right, (a, b],
reproducing obwoe_apply() exactly; cut points are written
at full round-trip precision; and every expression opens with an
explicit IS NULL branch, because NULL <= 5
is NULL in SQL, not FALSE.
obwoe_scorecard() runs the four steps above end to end —
split, bin, screen by IV and correlation, fit, scale to points — and
writes the result as an .xlsx model document.
card <- obwoe_scorecard(
german,
target = "default",
split = 0.7,
engine = "glm",
file = "scorecard.xlsx"
)
card
head(card$points[, c("variable", "bin", "woe", "points")])
predict(card, german, type = "score")The workbook holds thirteen sheets: the model summary, the scorecard points table, the coefficients with standard errors, the bin statistics of the variables that entered, the screening funnel with a reason per rejected variable, the correlation matrix before and after pruning, score gains, PSI stability between samples, a cut-off strategy table, the deployment SQL in both WoE and points form, and a reproducibility record.
Three properties are enforced rather than assumed. The binning is fitted on the training rows only, so the hold-out numbers are not inflated by supervised leakage. A variable whose WoE coefficient comes out negative — the WoE already carries the direction, so a negative slope means the model is reversing it — is dropped and the model refitted. And the generated points SQL reproduces the R card score exactly, unseen categories included.
Two long-form articles carry the detailed documentation and worked cases.
Optimal
Binning and Weight of Evidence: A Practical Guide — the
working reference. What WoE and IV measure and why monotonicity of the
event rate and of the WoE are the same statement; reading a bin table
and a gains table; screening a whole base with
obwoe_select(); how the algorithm families differ and how
to pick one; applying the transformation to new data; exporting to SQL;
preprocessing missing values and outliers. Uses the bundled German
Credit benchmark throughout.
An
Industrial Scorecard Pipeline — an origination scorecard
built the way a risk department builds one. A wide synthetic base
carrying the pathologies that matter (missing bureau data, rare dealer
codes, near-duplicate vendor fields, pure noise, and a leaky
post-booking field), screening at scale, redundancy pruning with
obcorr() in the WoE space, a recipes pipeline
around step_obwoe(), logistic regression with the
coefficient sign check, scorecard points and PDO scaling, out-of-time
validation with gains and PSI, deployment to SQL, tuning with
tidymodels, and the governance checklist that closes a model
document.
Three questions settle it in practice.
flowchart TD
A[What is the variable?] --> B[Numerical]
A --> C[Categorical]
B --> D{Must the WoE be<br/>monotone?}
D -->|"Yes — regulatory"| E["<b>ir</b> · <b>mrblp</b> · <b>mblp</b><br/><b>mob</b> · oslp"]
D -->|No| F{Millions of rows<br/>or streaming?}
F -->|Yes| G["<b>sketch</b> · ewb · kmb"]
F -->|No| H{Need the global<br/>optimum?}
H -->|Yes| I["<b>dp</b> · milp · bb · sblp"]
H -->|No| J["<b>jedi</b> · <b>mdlp</b><br/>fast_mdlp · dmiv · cm"]
C --> K{High cardinality<br/>with rare levels?}
K -->|Yes| L["<b>sketch</b> · mba · swb"]
K -->|No| M{Must the WoE be<br/>monotone?}
M -->|Yes| N["<b>gmb</b> · <b>mob</b> · udt"]
M -->|No| O["<b>jedi</b> · <b>cm</b><br/>ivb · dmiv · fetb · dp"]
E --> P([obwoe algorithm = ...])
G --> P
I --> P
J --> P
L --> P
N --> P
O --> P
Bold entries are the defaults worth trying first.
algorithm = "auto" picks jedi, a good
general-purpose choice for both types.
| Family | Algorithms | Optimises |
|---|---|---|
| Information-theoretic | mdlp, fast_mdlp, dmiv,
ivb, jedi |
entropy or IV gain per split, with an MDL stopping rule |
| Statistical merging | cm, fetb, mob |
merges neighbours whose difference fails a \(\chi^2\) or Fisher test |
| Shape-constrained | ir, mrblp, mblp,
oslp, gmb |
best fit subject to a monotonicity constraint |
| Exact optimisation | dp, milp, sblp,
bb |
global optimum of IV under bin-count and size constraints |
| Metaheuristic | sab, mba, swb,
udt |
simulated annealing, agglomerative or tree-based search |
| Unsupervised | ewb, kmb, ubsd,
sketch |
equal width, k-means, standard deviation, streaming quantiles |
obwoe_algorithms() lists all 37 with the feature types
each supports; every one is also callable directly as
ob_numerical_*() or ob_categorical_*().
| Function | Purpose |
|---|---|
obwoe() |
Fit optimal binning and WoE across a data frame |
obwoe_select() |
Screen variables by IV strength and bin ordering |
obwoe_apply() |
Apply a fitted binning to new data |
obwoe_gains() |
Gains table with KS, Gini, lift and capture rates |
obwoe_sql() |
Generate the equivalent SQL CASE expressions |
obwoe_scorecard() |
Run the full pipeline and write the .xlsx model
document |
obwoe_report() |
Write the workbook for an existing scorecard |
obwoe_scale() / obwoe_score() |
PDO scaling of log-odds to points |
obwoe_prune() |
Drop redundant variables by correlation in the WoE space |
obwoe_psi() |
Population Stability Index between two samples |
step_obwoe() |
tidymodels recipe step, tunable |
obcorr() |
Fast pairwise correlations for redundancy pruning |
ob_preprocess() |
Missing-value and outlier handling before binning |
obwoe_algorithms() |
List the available algorithms |
control.obwoe() |
Algorithm control parameters |
The package grades IV with the bands from Siddiqi (2006), and
obwoe_select() acts on them.
| IV | Band | Reading |
|---|---|---|
| \(< 0.02\) | Unpredictive | drop |
| \([0.02,\ 0.10)\) | Weak | keep only if it adds diversity |
| \([0.10,\ 0.30)\) | Medium | the workhorses of a scorecard |
| \([0.30,\ 0.50)\) | Strong | strong, verify it is not a proxy |
| \(\ge 0.50\) | Suspicious | almost always leakage |
Contributions are welcome. See the Contributing Guidelines and the Code of Conduct.
@software{optimalbinningwoe,
author = {José Evandeilton Lopes},
title = {OptimalBinningWoE: Optimal Binning and Weight of Evidence Framework for Modeling},
year = {2026},
url = {https://github.com/evandeilton/OptimalBinningWoE}
}MIT License © 2026 José Evandeilton Lopes