The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.

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
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.