liveBook · Manning

Weight of evidence

As explained in Financial AI in Practice

Weight of evidence is a transformation that expresses the strength of association between a predictor category and a binary outcome using event and non-event proportions.

A credit team needs to turn borrower characteristics into risk signals that officers can inspect. Weight of evidence (WoE) assigns each category or numeric bin a value based on its share of defaults versus its share of non-defaults. With “bad” defined as default, WoE(bin) = ln[(bads in bin / all bads) / (goods in bin / all goods)]. Positive values indicate greater relative default risk; negative values indicate lower relative risk; values near zero resemble the overall population. section 4.4.3

Information value (IV) measures how strongly the bins of a feature separate good and bad borrowers: IV = Σ[(bad share − good share) × WoE] across bins. It helps prioritize features, but a very large IV—such as above 0.5—warrants a check for leakage or overfitting. Review the WoE pattern across ordered bins as well as the aggregate IV; a strong total does not replace inspection of individual risk groups. section 4.4.3

Inspect age-group risk

Suppose borrowers are grouped into <20, 20–24, 25–29, 30–39, and 40+. Count good and bad borrowers in each group, divide by the respective totals, and calculate each group’s WoE and IV contribution. The 20–24 group has 30 bad and 120 good borrowers; the 40+ group has 5 bad and 280 good borrowers. Their WoE values make that contrast visible on a common scale. section 4.4.3

Listing 4.3 Understand WoE and IV with data
import numpy as np
import pandas as pd

data = {
    'AgeGroup':   ['<20', '20-24', '25-29', '30-39', '40+'],
    'GoodCount':  [90, 120, 160, 300, 280],
    'BadCount':   [10,  30,  40,   25,   5]
}     #1

df_bins = pd.DataFrame(data)

total_good = df_bins['GoodCount'].sum()
total_bad  = df_bins['BadCount'].sum()     #2

df_bins['GoodRate'] = df_bins['GoodCount'] / total_good
df_bins['BadRate']  = df_bins['BadCount']  / total_bad    #3

df_bins['WoE'] = np.log(
    (df_bins['BadRate'] + 1e-9) / (df_bins['GoodRate'] + 1e-9)
)     #4

df_bins['IV_bin'] = (
    (df_bins['BadRate'] - df_bins['GoodRate']) * df_bins['WoE']
)
IV_age = df_bins['IV_bin'].sum()   #5
print(df_bins[['AgeGroup','GoodCount','BadCount','WoE','IV_bin']])
print(f"Information Value (Age) = {IV_age:.4f}")

The pandas and NumPy example calculates group shares, WoE, per-bin IV, and total age IV. Its small constant prevents a zero share from making the logarithm undefined. section 4.4.3

For a manual credit-modeling pipeline:

  • Clean the inputs first. Treat sentinel codes, missing values, and outliers deliberately; a dedicated missing bin may preserve a risk signal that imputation would hide. section 4.4.4
  • Group categorical values and bin numeric features. One implementation uses ten quantile bins where possible, adds 0.5 to stabilize event and non-event proportions, and clips WoE to ±10. section 5.3.3
  • Calculate IV, inspect bin-level results, and select features above a chosen threshold, such as 0.02. Retain the bin boundaries and WoE mappings so selected raw features can be converted to WoE-coded inputs. section 5.3.3 section 5.3.4
  • Train and evaluate the downstream model. In the illustrated workflow, WoE-coded features feed a baseline XGBoost model; cross-validated AUC and confusion matrices are reviewed before probabilities are mapped to credit scores. section 5.0 section 5.2.3
Listing 5.13 Execute the “woe” function
def transform_to_woe(
    df,
    selected_features,
    cat_features,
    bin_edges_dict,
    woe_dict,
    target="target"
):
    df_woe = df[selected_features + [target]].copy()
    for feat in selected_features:
        if feat in cat_features:
            df_woe[feat] = df_woe[feat].map(woe_dict[feat])
        else:
            bins = bin_edges_dict[feat]
            df_woe[feat] = pd.cut(
                df_woe[feat],
                bins=bins,
                include_lowest=True
            ).astype(str)
            df_woe[feat] = df_woe[feat].map(woe_dict[feat])
    return df_woe


df_woe = transform_to_woe(
    df,
    selected_feats,
    cat_features,
    bin_edges,
    woe_maps,
    target=target_col
)
print("df_woe shape:", df_woe.shape)
df_woe.head(5)

The transformation function shows how stored category mappings and numeric bin edges replace selected raw columns with their WoE values. section 5.3.4

Document binning, missing-data decisions, and outlier treatment for audit and fairness review. WoE makes risk differences inspectable, but it does not resolve poor data quality or establish that a feature is safe to use. Automated binning and advanced drift checks are separate extensions to this manual approach. section 4.4.4 section 5.2.3

How Financial AI in Practice covers it

How deep it goes
Works through the formula and manual Python calculation, then applies binning, smoothing, clipping, selection, and mapping; defers automated binning.
Examples
Uses age-group good/bad counts and pandas calculations, followed by numeric and categorical WoE mappings.
What it assumes
Assumes tabular borrower data and a good/bad target; introduces WoE proportions and their log-ratio explicitly.
What you can do after
Build it from scratch with pandas and NumPy to calculate WoE and IV, select features, and produce WoE-coded model inputs.
How it connects
Links binning and information value to feature selection, XGBoost credit modeling, and auditable scorecards.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Financial AI in Practice. The section links point to the parts of the book it draws on.