# Algorithm Reference: the 37 Binning Engines

#### José Evandeilton Lopes

#### 2026-08-22

    library(OptimalBinningWoE)

## Scope, and how this was written

The package ships 28 algorithms covering 37 algorithm/feature-type combinations: 21 accept numerical features, 16 accept categorical ones, 9 accept both, and 1 accepts a multinomial target.

This reference was produced by reading the C++ implementation of every engine and comparing it against the literature its documentation invokes. Two rules governed the writing:

1. A claim is made only if it can be traced — either to a specific line of the shipped source, or to a publication whose existence was verified. Where neither was possible, the text says so rather than filling the gap.
2. The name is not evidence. Several engines in this package are named for methods they do not implement. Each entry below states what the code does, and separately whether the name describes it.

That second rule is not a criticism of the package so much as a description of it: much of the documentation already says the same thing. `R/obn_mblp.R`, `R/obn_mrblp.R` and `R/obn_dp.R` disclaim their own names in detail, and `R/obn_oslp.R` goes as far as recommending against its own algorithm. This vignette collects those disclosures in one place and adds the ones that had not been made.

    alg <- obwoe_algorithms()
    c(algorithms = nrow(alg),
      numerical = sum(alg$numerical),
      categorical = sum(alg$categorical),
      both = sum(alg$numerical & alg$categorical),
      multinomial = sum(alg$multinomial))
    #>  algorithms   numerical categorical        both multinomial
    #>          28          21          16           9           1

## Two ways to run an algorithm

Through `obwoe()`, which detects feature types, dispatches, and returns a model object the rest of the package understands:

    data_path <- system.file("extdata", "germancredit.csv.gz",
                             package = "OptimalBinningWoE")
    gc_data <- utils::read.csv(gzfile(data_path), stringsAsFactors = FALSE)
    gc_data$target <- as.integer(1L - gc_data$credit_risk)
    gc_data$credit_risk <- NULL

    fit <- obwoe(gc_data, target = "target", feature = c("duration", "purpose"),
                 algorithm = "jedi", min_bins = 2, max_bins = 5)
    fit$summary[, c("feature", "type", "algorithm", "n_bins", "total_iv")]
    #>    feature        type algorithm n_bins total_iv
    #> 1 duration   numerical      jedi      5   0.2474
    #> 2  purpose categorical      jedi      5   0.1653

Or by calling the engine directly, which returns a plain list for one feature:

    direct <- ob_numerical_jedi(target = gc_data$target,
                                feature = gc_data$duration,
                                min_bins = 2, max_bins = 5)
    data.frame(bin = direct$bin, count = direct$count,
               woe = round(direct$woe, 4), iv = round(direct$iv, 4))
    #>                     bin count     woe     iv
    #> 1       (-Inf;7.000000]    87 -1.3122 0.1068
    #> 2  (7.000000;10.000000]    84 -0.4520 0.0155
    #> 3 (10.000000;33.000000]   659 -0.0489 0.0016
    #> 4 (33.000000;39.000000]    88  0.5729 0.0316
    #> 5      (39.000000;+Inf]    82  0.9939 0.0918

### `obwoe()` does not forward algorithm-specific parameters

`obwoe()` builds a fixed argument list — `min_bins`, `max_bins`, `bin_cutoff`, `max_n_prebins`, `convergence_threshold`, `max_iterations`, and `bin_separator` for categorical features — and passes nothing else. An engine’s own parameters are unreachable through it, and routing them via `control.obwoe()` fails silently:

    via_obwoe <- obwoe(gc_data, target = "target", feature = "duration",
                       algorithm = "dmiv",
                       control = control.obwoe(divergence_method = "kl"))
    direct_dmiv <- ob_numerical_dmiv(target = gc_data$target,
                                     feature = gc_data$duration,
                                     divergence_method = "kl")

    c(`through obwoe()` = via_obwoe$results$duration$divergence_method,
      `direct wrapper`  = direct_dmiv$divergence_method)
    #> through obwoe()  direct wrapper
    #>            "l2"            "kl"

If you need an algorithm-specific parameter, call the engine directly.

## Coverage

    coverage <- alg
    coverage$feature_types <- ifelse(coverage$numerical & coverage$categorical, "both",
                              ifelse(coverage$numerical, "numerical", "categorical"))
    coverage$target <- ifelse(coverage$multinomial, "binary + multinomial", "binary")
    coverage[order(coverage$feature_types, coverage$algorithm),
             c("algorithm", "feature_types", "target")]
    #>    algorithm feature_types               target
    #> 1         cm          both               binary
    #> 2       dmiv          both               binary
    #> 3         dp          both               binary
    #> 4       fetb          both               binary
    #> 5       jedi          both               binary
    #> 6  jedi_mwoe          both binary + multinomial
    #> 7        mob          both               binary
    #> 8     sketch          both               binary
    #> 9        udt          both               binary
    #> 10       gmb   categorical               binary
    #> 11       ivb   categorical               binary
    #> 12       mba   categorical               binary
    #> 13      milp   categorical               binary
    #> 14       sab   categorical               binary
    #> 15      sblp   categorical               binary
    #> 16       swb   categorical               binary
    #> 17        bb     numerical               binary
    #> 18       ewb     numerical               binary
    #> 19 fast_mdlp     numerical               binary
    #> 20        ir     numerical               binary
    #> 21       kmb     numerical               binary
    #> 22       ldb     numerical               binary
    #> 23      lpdb     numerical               binary
    #> 24      mblp     numerical               binary
    #> 25      mdlp     numerical               binary
    #> 26     mrblp     numerical               binary
    #> 27      oslp     numerical               binary
    #> 28      ubsd     numerical               binary

## The quantity being optimised

Nearly every engine here optimises Information Value, so it is worth stating what that is. For bins \(i\) with good/bad counts \(g_i, b_i\) and totals \(g, b\):

\[ \mathrm{WoE}_i = \ln\frac{g_i/g}{b_i/b}, \qquad \mathrm{IV} = \sum_i \left(\frac{g_i}{g} - \frac{b_i}{b}\right)\mathrm{WoE}_i \]

IV is the symmetrised Kullback–Leibler divergence between the good and bad distributions — the Jeffreys divergence. Zeng (2013, §3.2) states this identity explicitly, which is what makes the divergence-based engine `dmiv` a generalisation rather than a different idea.

The weight of evidence itself long predates credit scoring. Good (1985, p. 249) records that the log-likelihood-ratio form was first published by Wrinch and Jeffreys (1921); that Turing named the quantity informally in classified work at Bletchley Park in 1941; and that Good (1950) published and popularised the term. Kullback–Leibler divergence is its expectation.

Smoothing is not uniform across the package. Most engines add a pseudo-count before taking the logarithm, but the constant, its scaling, and whether IV uses the same smoothed proportions as WoE all vary. Four engines — `ldb`, `lpdb`, `kmb` and numerical `dp` — compute WoE from smoothed proportions but IV from raw ones. In `ldb` this is visible at `src/OBN_LDB_v5.cpp:117-150`: `calculateWOE()` divides by `total_pos + num_bins * ALPHA`, while `calculateIV()` divides by `total_pos`. The effect is small at \(\alpha = 0.5\) and moderate \(n\), but it is real, and it means reported IV is not exactly the IV of the model whose WoE is reported.

## Choosing an algorithm

`algorithm = "auto"` resolves to `jedi` for a binary target and `jedi_mwoe` for a multinomial one. It is a fixed alias, not a search.

On German Credit with five-fold held-out IV, `jedi` is within one standard error of the best available engine on 18 of 20 variables. For most variables the choice of engine is worth close to nothing; where it matters, it tends to matter on continuous variables with a long tail.

If you need | Reach for | Because
--- | --- | ---
A sound general default | `jedi` | Fast, monotonicity-aware, rarely beaten by enough to matter
A multinomial target | `jedi_mwoe` | The only engine that accepts one
A genuine statistical stopping rule | `cm` | ChiMerge with a significance threshold
Provably IV-optimal bins given the ordering | `ivb`, `dp` (categorical), `sblp` | Real dynamic programming over the ordered levels
The bin count decided by theory | `fast_mdlp` | The only faithful Fayyad–Irani implementation here
Guaranteed monotone WoE | `ir` | Isotonic regression, not a merge heuristic
Very high cardinality | `sketch` (categorical) | Memory independent of the number of levels
An unsupervised baseline | `ewb` | Equal-width initialisation, honestly labelled

## Reference, organised by mechanism

Grouping engines by what they do rather than by what they are called produces a different and more useful taxonomy than the names suggest.

### Exact dynamic programming

Three engines genuinely solve an optimal-partition problem: having fixed an ordering of the levels, they find the contiguous partition maximising total IV by Bellman recursion, not by greedy merging.

`ivb` (`ob_categorical_ivb()`) — sorts levels by event rate, then runs an interval DP with cumulative-sum caching, so each DP cell is \(O(1)\) (`src/OBC_IVB_v5.cpp:34-158, 369-442`). \(O(L^2 k)\). No own parameters.

`dp`, categorical (`ob_categorical_dp()`) — the same DP, but with the monotonicity constraint built into the transition rather than repaired afterwards (`src/OBC_DP_v5.cpp:666-717`): transitions that would violate the requested trend are simply never considered. Own parameter: `monotonic_trend` (`"auto"`, `"ascending"`, `"descending"`, `"none"`). The `"auto"` direction is detected by a Welford-stable online correlation (Welford, 1962).

`sblp` (`ob_categorical_sblp()`) — event-rate ordering, a rate-similarity pre-merge for rare levels, then interval DP. Its DP re-sums each candidate segment instead of using cumulative sums (`src/OBC_SBLP_v5.cpp:485-505`), making it \(O(L^3 k)\) where `ivb` and `dp` are \(O(L^2 k)\). Own parameter: `alpha` (Laplace constant). Name note: no logistic regression is fitted anywhere; the R documentation’s analogy to Jenks natural breaks has no counterpart in the code.

There is a fourth, hidden DP: `sketch`, numerical contains a correct Bellman DP over “best IV splitting the first i points into j bins” (`src/OBN_Sketch_v5.cpp:277-470`) — but it runs only when \(n \le 50\), a threshold hardcoded at `src/OBN_Sketch_v5.cpp:979` and not exposed. For any real dataset, `sketch` takes its greedy branch.

### Recursive entropy partitioning

`fast_mdlp` (`ob_numerical_fast_mdlp()`) — the only engine implementing the Fayyad–Irani (1993) criterion as published. It recursively splits top-down, accepting a cut when the information gain exceeds

\[ \frac{\log_2(N-1)}{N} + \frac{\Delta}{N}, \qquad \Delta = \log_2(3^k - 2) - \bigl[k\,\mathrm{Ent}(S) - k_1\mathrm{Ent}(S_1) - k_2\mathrm{Ent}(S_2)\bigr] \]

which for binary targets reduces to the `log2(7) - 2*E_parent` at `src/OBN_FastMDLPM_v5.cpp:176`. Own parameter: `force_monotonicity`.

Two caveats. `max_n_prebins` and `bin_cutoff` appear exactly once each in the file — in the signature — and have no effect. And `max_bins` is enforced by discarding the last-discovered splits (`src/OBN_FastMDLPM_v5.cpp:548-575`), by recursion order rather than by information content; the source comment admits this is “for simplicity”.

`udt`, numerical (`ob_numerical_udt()`) — scores every candidate midpoint by information gain in a single prefix-sum sweep, keeps the top `max_n_prebins` by gain, then merges bottom-up. Own parameters: `laplace_smoothing`, `monotonicity_direction`. Its split phase rescans the full feature vector per bin (`src/OBN_UDT_v5.cpp:778-880`), which the R documentation correctly warns is expensive. Name note: the package’s own roxygen states it plainly — the method is supervised and builds no tree.

### Bottom-up MDL cost

`mdlp` (`ob_numerical_mdlp()`) — merges the adjacent pair that most reduces a global cost \(\log_2(k-1) + N H(S) - \sum_i n_i H(S_i)\) (`src/OBN_MDLP_v5.cpp:915-966`), then enforces `max_bins` as a hard post-condition. Own parameter: `laplace_smoothing`.

Name note. This is an MDL-flavoured cost, but it is not the Fayyad–Irani criterion: the term \(\log_2(3^k-2)\) appears nowhere in the file, and the algorithm is agglomerative bottom-up where the cited paper is recursive top-down. `R/obn_mdlp.R:134` nonetheless claims a “Theoretical Guarantee (Fayyad & Irani, 1993)”. The guarantee does not transfer. Confusingly, the engine that does implement the paper is the one named `fast_mdlp`, whose name suggests it is merely a faster variant of this one.

### Statistical merging

`cm` (`ob_numerical_cm()`, `ob_categorical_cm()`) — ChiMerge (Kerber, 1992): repeatedly merge the adjacent pair with the smallest \(\chi^2\), stop when the smallest exceeds a critical value. Own parameters: `chi_merge_threshold`, `use_chi2_algorithm`, and `init_method` on the numerical side. Setting `use_chi2_algorithm = TRUE` gives the genuine Chi2 extension of Liu and Setiono (1995): the same merge swept over a decreasing schedule of significance levels, stopping on an inconsistency-rate criterion.

Two implementation notes. The statistic includes a Yates continuity correction, \(\sum(|O-E| - 0.5)^2/E\) (`src/OBN_CM_v5.cpp:266-310`), which is not the form usually attributed to Kerber; the primary source could not be consulted directly to confirm whether Kerber’s own text includes it. And critical values come from a hard-coded 14-entry table for one degree of freedom, with the nearest tabulated level substituted for an untabulated `chi_merge_threshold`.

`fetb` (`ob_numerical_fetb()`, `ob_categorical_fetb()`) — merges the adjacent pair whose 2×2 table is least distinguishable. No own parameters.

What it computes. `fisherProb()` returns the hypergeometric point probability of the single observed table (`src/OBN_FETB_v5.cpp:58-65`), which is one term of a Fisher exact-test p-value, not the summed tail. The package is internally inconsistent about this: the function’s own comment says “point probability” and the roxygen says “the exact hypergeometric probability of independence” — both accurate — while the file header at `src/OBN_FETB_v5.cpp:23` calls it a “two‑tail Fisher p‑value”, which it is not. As a similarity criterion the statistic is defensible; as a significance test it is not, and there is no α-based stopping rule. On the categorical side, `max_n_prebins` is accepted and never read.

### Isotonic regression

`ir` (`ob_numerical_ir()`) — the one engine whose name fully survives inspection. Weighted Pool Adjacent Violators on bin event rates, correctly implemented (`src/OBN_IR_v5.cpp:776-856`), and — importantly — the bins that PAVA pools are then genuinely merged rather than having fitted values written over them, so the reported counts and the isotonic fit stay consistent. Own parameter: `auto_monotonicity`. `min_bins` is a target, not a guarantee: pooling can legitimately end below it, and the roxygen says so.

Two notes. The source comment at `src/OBN_IR_v5.cpp:761` claims \(O(n)\), but the block merge shifts the whole vector (`:822-827`), making it \(O(k^2)\). And `R/obn_ir.R:55` credits PAVA to Best and Chakravarti (1990) — a real paper about active-set methods for isotonic regression, but not the algorithm’s origin, which is Ayer, Brunk, Ewing, Reid and Silverman (1955).

### Density-based

Both engines locate cut points from a Gaussian kernel density estimate, computed by linear binning on a grid and convolution — the technique of Silverman (1982), with linear binning as formalised by Wand (1994) — in the shared helper `gaussian_kde_grid()`. The grid is 512 points and is not user-adjustable, so structure narrower than 1/511 of the feature’s range is not resolved.

`ldb` (`ob_numerical_ldb()`) — cuts at local minima of the density. Bandwidth `0.9 * min(sd, IQR/1.34) * n^(-1/5)` (`src/OBN_LDB_v5.cpp:383`), an exact match to Silverman’s rule of thumb (1986, p. 48, eq. 3.31). Own parameter: `enforce_monotonic`.

`lpdb` (`ob_numerical_lpdb()`) — cuts at extrema and inflection points, found by finite differences of the gridded density. Own parameters: `polynomial_degree`, `enforce_monotonic`.

Name note. No polynomial is fitted. `polynomial_degree` is validated and never read — setting it changes nothing. The R documentation already says the density is “currently approximated via KDE”. Its bandwidth also drops the robustness term, using `0.9 * sd * n^(-1/5)` (`src/OBN_LPDB_v5.cpp:784`), which makes it more outlier-sensitive than `ldb`.

### Distribution-based initialisation

`ewb` (`ob_numerical_ewb()`) — equal-width intervals, then supervised merging. Honestly labelled as hybrid. Own parameter: `is_monotonic`. Its WoE and IV use the same smoothed proportions, unlike `ldb`/`lpdb`/`kmb`/`dp`.

`kmb` (`ob_numerical_kmb()`) — centroids placed at `min + (i+0.5)·range/n_bins` and boundaries at their midpoints (`src/OBN_KMB_v5.cpp:279-332`). Name note: there is no assignment/update iteration, so this is not Lloyd’s algorithm (1982) — evenly spaced centroids with midpoint boundaries is equal-width binning under another name. The roxygen is careful and says “k-means inspired”. Two further quirks: bin assignment is a linear scan per observation, \(O(nk)\), the only engine not using binary search; and the initial bin count is capped at `max_bins`, so raising `max_n_prebins` above `max_bins` does nothing.

`ubsd` (`ob_numerical_ubsd()`) — edges at \(\mu \pm \sigma, \mu \pm 2\sigma\) unioned with equal-width points, then supervised refinement. Own parameter: `laplace_smoothing`. Note: alone among the numerical engines it offers no way to disable monotonicity enforcement — there is no such argument in the constructor or the exported function. The roxygen already corrects the “unsupervised” in its name: only initialisation is unsupervised.

### Divergence-driven

`dmiv` (`ob_numerical_dmiv()`, `ob_categorical_dmiv()`) — merges by one of nine divergences: Hellinger, KL, triangular, J-divergence, symmetric \(\chi^2\), Jensen–Shannon (Lin, 1991), L1, L2, L∞. Own parameters: `divergence_method`, `bin_method`, and `is_monotonic` on the numerical side.

`bin_method` defaults to `"woe1"`, which is not standard WoE. Zeng’s WOE1 is \(\ln(g_i/b_i)\) — a per-bin log-odds with no normalisation by the totals — so it differs from standard WoE by the constant \(\ln(g/b)\). IV is unaffected, because the offset cancels in the sum, and the code computes IV separately for exactly that reason (`src/OBN_DMIV_v5.cpp:724-761`). But the WoE values are not comparable with the other 27 engines, and a scorecard fitted on them carries a different intercept. Pass `bin_method = "woe"` to the wrapper for the standard definition.

Attribution note. The package presents the smoothed form \(\ln((g_i+0.5)/(b_i+0.5))\) as Zeng’s (`R/obc_dmiv.R:38`). Zeng’s published WOE1 is unsmoothed; the pseudo-count is this package’s own numerical-stability addition. Two further citation issues: `R/obn_dmiv.R:86` gives the venue as Journal of the Operational Research Society 64(5), 712–731, but the verified publication is Journal of Mathematics 2013, article 848271; and two divergence methods behave globally rather than per bin — `l2` rescales each bin’s share of the global norm, and `l∞` assigns the maximum to whichever bins attain it and zero to all others.

### Streaming

`sketch` (`ob_numerical_sketch()`, `ob_categorical_sketch()`) — the only engine whose memory does not grow with the data. The categorical variant uses Count-Min sketches (Cormode and Muthukrishnan, 2005) for category frequencies, making its cost independent of the number of levels; the numerical variant uses a multi-level compactor for approximate quantiles. Own parameters: `sketch_k` and `monotonic` (numerical); `sketch_width`, `sketch_depth` (categorical).

Two qualifications on the numerical side. The compactor uses the same capacity at every level and a deterministic parity rule to choose which item of a compacted pair survives (`src/OBN_Sketch_v5.cpp:81, 107-108`). Karnin, Lang and Liberty (2016) obtain their optimal bound from geometrically decreasing capacities and a randomised coin flip; neither is present here. The implementation is in the KLL family, but the \(\varepsilon \approx O(1/k)\) guarantee quoted in the documentation is not established for this variant. And `max_n_prebins` is never read — the candidate quantile grid is hardcoded.

### Greedy merging under an IV or WoE criterion

The largest group. All follow the same shape — pre-bin, merge rare bins, merge until `max_bins`, repair monotonicity — and differ in three choices: how the levels are ordered, how the merge direction is decided, and which IV expression selects the merge.

Engine | Types | Ordering / direction rule | Merge criterion
--- | --- | --- | ---
`jedi` | both | Majority vote over WoE increments | Smallest summed current IV
`jedi_mwoe` | both | Per-class, all classes checked | Smallest IV summed over classes
`mob` | both | First two bins’ WoE | Minimum IV loss after merge
`mrblp` | numerical | Majority vote | Smallest \(|IV_i - IV_{i+1}|\)
`mblp` | numerical | Pearson correlation | Minimum IV loss after merge
`oslp` | numerical | Majority vote | Smallest \(IV_i + IV_{i+1}\)
`bb` | numerical | Both directions simulated, higher IV wins | Globally minimum-IV bin
`gmb` | categorical | Event rate, adjacent only | Highest post-merge total IV
`mba` | categorical | Count, then all-pairs rate similarity | Minimum IV loss
`milp` | categorical | All-pairs rate similarity | Minimum \(|IV|\)
`swb` | categorical | WoE order, JS divergence | Minimum JS divergence
`udt` | categorical | WoE order, JS divergence | Minimum JS divergence

Own parameters: `laplace_smoothing` for `mob`, `mrblp`, `oslp`; `force_monotonic_direction` for `mblp`; `is_monotonic` for `bb`; none for the rest.

`jedi` and `jedi_mwoe` are the package author’s own construction, combining adjacent published techniques rather than implementing a named method — there is no paper to cite and none is claimed. The numerical heuristic is: quantile pre-binning; merge bins below `bin_cutoff` into the smaller neighbour; infer trend direction by majority vote over pairwise WoE increments; merge violating adjacent pairs until monotone; then merge until `max_bins`, each time taking the adjacent pair with the smallest summed current IV; iterate until total IV stops changing by more than `convergence_threshold` (`src/OBN_JEDI_v5.cpp:113-129, 235-425`). `jedi_mwoe` is the same procedure with one-vs-rest WoE per class, requiring monotonicity in every class simultaneously. Name note: despite “Joint Entropy-Driven” in the title, the numerical implementation computes no entropy — the file contains no entropy, `log2` or information-gain term anywhere. The optimisation is entirely WoE/IV-driven. The roxygen also describes the merge as choosing the smallest decrease in IV, while the code chooses the smallest summed current IV; those are different rules.

`bb` is named “Branch and Bound” but neither branches nor bounds: it repeatedly merges the single lowest-IV bin and never reconsiders (`src/OBN_BB_v5.cpp:449-517`). `dp`, numerical is named “Dynamic Programming” and builds no DP table — its own source comment says so and cites Navas-Palencia (2020) as what a real formulation would look like. `milp` solves no mixed-integer programme, and its header says so. `mblp` and `oslp` contain no linear programme. `mrblp` does no likelihood-ratio pre-binning: the word “likelihood” appears only in comments. `swb` slides no window — for unordered levels there is no axis to slide along; it is greedy divergence merging with an adjacency preference.

`R/obn_oslp.R:135` recommends against `oslp` outright: “Use OSLP: Never. Use MBLP or MOB instead.”

### Metaheuristic

`sab` (`ob_categorical_sab()`) — simulated annealing (Kirkpatrick, Gelatt and Vecchi, 1983) over category-to-bin assignments. Alone among the categorical engines it can move a level between non-adjacent bins, so it is not restricted to a single ordering. Rare-bin and monotonicity violations enter the objective as penalties rather than being repaired afterwards (`src/OBC_SAB_v5.cpp:161-215`). Own parameters: `initial_temperature`, `cooling_rate`, `adaptive_cooling`. Its generator is seeded from R’s RNG stream, so `set.seed()` makes it reproducible. `max_n_prebins` is validated and never used, so it anneals over the full level set.

## Measured behaviour

All 37 combinations on the bundled German Credit data, identical constraints:

    run_all <- function(feature, type) {
      ids <- alg$algorithm[alg[[type]]]
      out <- lapply(ids, function(a) {
        r <- try(suppressWarnings(suppressMessages(
          obwoe(gc_data, target = "target", feature = feature,
                algorithm = a, min_bins = 2, max_bins = 5))), silent = TRUE)
        if (inherits(r, "try-error") || r$summary$error) {
          data.frame(algorithm = a, bins = NA_integer_, total_iv = NA_real_)
        } else {
          data.frame(algorithm = a, bins = r$summary$n_bins,
                     total_iv = round(r$summary$total_iv, 4))
        }
      })
      do.call(rbind, out)
    }
    num_res <- run_all("duration", "numerical")
    knitr::kable(num_res[order(-num_res$total_iv), ], row.names = FALSE,
                 caption = "Numerical engines on `duration`")

Numerical engines on `duration`
algorithm | bins | total_iv
--- | --- | ---
jedi_mwoe | 5 | 0.4947
ewb | 5 | 0.2747
ubsd | 5 | 0.2680
lpdb | 5 | 0.2635
dp | 5 | 0.2568
udt | 5 | 0.2546
fetb | 5 | 0.2531
mob | 5 | 0.2531
mrblp | 5 | 0.2531
jedi | 5 | 0.2474
oslp | 5 | 0.2458
dmiv | 5 | 0.2395
ir | 5 | 0.2338
kmb | 4 | 0.2290
bb | 5 | 0.2281
cm | 5 | 0.2237
sketch | 4 | 0.2134
mblp | 4 | 0.1961
mdlp | 3 | 0.1910
fast_mdlp | 2 | 0.1556
ldb | 2 | 0.0923

    cat_res <- run_all("purpose", "categorical")
    knitr::kable(cat_res[order(-cat_res$total_iv), ], row.names = FALSE,
                 caption = "Categorical engines on `purpose`")

Categorical engines on `purpose`
algorithm | bins | total_iv
--- | --- | ---
gmb | 5 | 0.1656
mba | 5 | 0.1656
jedi | 5 | 0.1653
cm | 5 | 0.1645
sblp | 5 | 0.1629
dmiv | 5 | 0.1549
dp | 5 | 0.1533
ivb | 5 | 0.1525
fetb | 5 | 0.1513
sketch | 5 | 0.1464
udt | 5 | 0.1464
swb | 5 | 0.1464
jedi_mwoe | 2 | 0.1272
mob | 4 | 0.0687
milp | 2 | 0.0576
sab | 5 | 0.0436

Read those as a sanity check, not a ranking. IV measured on the same data that produced the bins rewards more bins automatically, and one variable cannot separate engines differing by thousandths. A defensible comparison needs held-out data.

As of version 1.13.3 every engine bins a 100,000-row numerical feature in under a quarter of a second. Before that release `ldb`, `lpdb` and numerical `udt` scaled quadratically and took 10.7s, 10.7s and 7.4s at only 50,000 rows.

## Known limitations

Collected so they are findable, with the source location for each.

Parameters accepted and never read. `polynomial_degree` in `lpdb`; `max_n_prebins` in `fast_mdlp`, numerical `sketch`, categorical `fetb` and `sab`; `bin_cutoff` in `fast_mdlp` and categorical `dmiv`, whose rare-level handling uses a hardcoded threshold of 5 observations instead.

`convergence_threshold` is inert in five numerical engines — `ldb`, `lpdb`, `ewb`, `kmb` and `dp` accept and validate it but never compare against it. Only `bb`, `ubsd` and `sketch` use it.

Silent level truncation in `ivb` and `gmb`. When more levels survive the rare-level merge than `max_n_prebins` allows, both discard the excess rather than pooling it (`src/OBC_IVB_v5.cpp:315`, `src/OBC_GMB_v5.cpp:273`), and the observations leave the binning entirely. Default settings hide this — with `bin_cutoff = 0.05` at most 20 levels can survive — but at a lower cutoff it is reachable, and no warning is emitted. Prefer `dp` or `jedi` for high-cardinality features under a small `bin_cutoff`.

Multiclass monotonicity is not simultaneous. In categorical `jedi_mwoe`, `ensure_monotonic_order()` re-sorts by each class in turn without re-checking earlier classes (`src/OBC_JEDIMWoE_v5.cpp:854-869`), so a later class’s sort can reintroduce a violation in an earlier one.

Citation errors in the shipped documentation. `R/obc_cm.R:94` gives the Chi2 author as “Liu, B.” (it is Huan Liu) and pages 372–377 (they are 388–391; `R/obn_cm.R` has them right). `src/OBN_OSLP_v5.cpp:46` attributes “Optimal Binning: Mathematical Programming Formulation” to “Belcastro, L., et al.”; the paper is by Guillermo Navas-Palencia. `R/obc_sab.R` has no references at all, though simulated annealing is its namesake.

## References

Every entry below was verified to exist. Works the package cites that could not be confirmed are listed in the following section instead.

Ayer, M., Brunk, H. D., Ewing, G. M., Reid, W. T., & Silverman, E. (1955). An empirical distribution function for sampling with incomplete information. Annals of Mathematical Statistics, 26, 641–647.

Barlow, R. E., Bartholomew, D. J., Bremner, J. M., & Brunk, H. D. (1972). Statistical Inference Under Order Restrictions. Wiley.

Best, M. J., & Chakravarti, N. (1990). Active set algorithms for isotonic regression: a unifying framework. Mathematical Programming, 47, 425–439.

Cormode, G., & Muthukrishnan, S. (2005). An improved data stream summary: the count-min sketch and its applications. Journal of Algorithms, 55(1), 58–75.

Fayyad, U. M., & Irani, K. B. (1993). Multi-interval discretization of continuous-valued attributes for classification learning. IJCAI-93, 1022–1027.

Fisher, R. A. (1922). On the interpretation of χ² from contingency tables, and the calculation of P. Journal of the Royal Statistical Society, 85(1), 87–94.

Good, I. J. (1950). Probability and the Weighing of Evidence. Charles Griffin.

Good, I. J. (1985). Weight of evidence: a brief survey. In Bayesian Statistics 2, 249–270. Elsevier.

Karnin, Z., Lang, K., & Liberty, E. (2016). Optimal quantile approximation in streams. FOCS 2016, 71–78.

Kerber, R. (1992). ChiMerge: discretization of numeric attributes. AAAI-92, 123–128.

Kirkpatrick, S., Gelatt, C. D., & Vecchi, M. P. (1983). Optimization by simulated annealing. Science, 220(4598), 671–680.

Kullback, S., & Leibler, R. A. (1951). On information and sufficiency. Annals of Mathematical Statistics, 22(1), 79–86.

Lin, J. (1991). Divergence measures based on the Shannon entropy. IEEE Transactions on Information Theory, 37(1), 145–151.

Liu, H., & Setiono, R. (1995). Chi2: feature selection and discretization of numeric attributes. ICTAI-95, 388–391.

Lloyd, S. P. (1982). Least squares quantization in PCM. IEEE Transactions on Information Theory, 28(2), 129–137.

Navas-Palencia, G. (2020). Optimal binning: mathematical programming formulation. arXiv:2001.08025.

Parzen, E. (1962). On estimation of a probability density function and mode. Annals of Mathematical Statistics, 33(3), 1065–1076.

Rosenblatt, M. (1956). Remarks on some nonparametric estimates of a density function. Annals of Mathematical Statistics, 27(3), 832–837.

Siddiqi, N. (2006). Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring. Wiley.

Silverman, B. W. (1982). Algorithm AS 176: kernel density estimation using the fast Fourier transform. Journal of the Royal Statistical Society, Series C, 31(1), 93–99.

Silverman, B. W. (1986). Density Estimation for Statistics and Data Analysis. Chapman & Hall.

Wand, M. P. (1994). Fast computation of multivariate kernel estimators. Journal of Computational and Graphical Statistics, 3(4), 433–445.

Welford, B. P. (1962). Note on a method for calculating corrected sums of squares and products. Technometrics, 4(3), 419–420.

Wrinch, D., & Jeffreys, H. (1921). On certain fundamental principles of scientific inquiry. Philosophical Magazine, 42, 369–390.

Zeng, G. (2013). Metric divergence measures and information value in credit scoring. Journal of Mathematics, 2013, article 848271.

Zeng, G. (2014). A necessary condition for a good binning algorithm in credit scoring. Applied Mathematical Sciences, 8(65), 3229–3242.

## What could not be verified

Stated so that absence of a citation is not mistaken for absence of a source.

The IV interpretation bands. The scale (< 0.02 unpredictive, 0.02–0.1 weak, 0.1–0.3 medium, 0.3+ strong) is attributed to Siddiqi (2006) by two independent sources that quote it with page numbers, and Zeng (2013, §3.2) cites Siddiqi for it as well. The book’s text was not consulted directly, and the sources disagree on whether a fifth band exists and what it says — the widely repeated “> 0.5 suspiciously high, check for overfitting” wording could not be traced to Siddiqi’s own text.

Whether Kerber’s ChiMerge uses a continuity correction. The primary source could not be retrieved. Secondary descriptions give the uncorrected \(\sum (O-E)^2/E\); this package implements the Yates-corrected form.

The mechanism of Mironchyk and Tchistiakov (2017), cited by `mob`, `mrblp`, `mblp`, `gmb` and `mba`. The paper is real; access was blocked, and its characterisation as a PAVA-based construction rests on secondary descriptions. If that characterisation is right, none of the five engines citing it reproduces its mechanism — `ir` is the package’s only PAVA implementation.

The error bound of this package’s compactor sketch. What it does differently from Karnin, Lang and Liberty (2016) is documented above; deriving a corrected bound for the deterministic equal-capacity variant would require a proof not attempted here.

Whether a Journal of the Operational Research Society version of Zeng (2013) exists, as `R/obn_dmiv.R:86` states. Only the Journal of Mathematics version was found.