A scorecard-format famework for logistic regression tasks with gradient-boosted decision trees (XGBoost and CatBoost). xbooster allows to convert a classification model into a logarithmic (point) scoring system.
In addition, it provides a suite of interpretability tools to understand the model's behavior.
The interpretability suite includes:
- Granular boosted tree statistics, including metrics such as Weight of Evidence (WOE) and Information Value (IV) for splits π³
- Tree visualization with customizations π¨
- Global and local feature importance π
xbooster also provides a scorecard deployment using SQL π¦.
Install the package using pip:
pip install xbooster
Here's a quick example of how to use xbooster to construct a scorecard for an XGBoost model:
import pandas as pd
import xgboost as xgb
from xbooster.constructor import XGBScorecardConstructor
from sklearn.model_selection import train_test_split
# Load data and train XGBoost model
url = (
"https://github.com/xRiskLab/xBooster/raw/main/examples/data/credit_data.parquet"
)
dataset = pd.read_parquet(url)
features = [
"external_risk_estimate",
"revolving_utilization_of_unsecured_lines",
"account_never_delinq_percent",
"net_fraction_revolving_burden",
"num_total_cc_accounts",
"average_months_in_file",
]
target = "is_bad"
X, y = dataset[features], dataset[target]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train the XGBoost model
best_params = {
'n_estimators': 100,
'learning_rate': 0.55,
'max_depth': 1,
'min_child_weight': 10,
'grow_policy': "lossguide",
'early_stopping_rounds': 5
}
model = xgb.XGBClassifier(**best_params, random_state=62)
model.fit(X_train, y_train)
# Initialize XGBScorecardConstructor
scorecard_constructor = XGBScorecardConstructor(model, X_train, y_train)
scorecard_constructor.construct_scorecard()
# Print the scorecard
print(scorecard_constructor.scorecard)
After this, we can create a scorecard and test its Gini score:
from sklearn.metrics import roc_auc_score
# Create scoring points
xgb_scorecard_with_points = scorecard_constructor.create_points(
pdo=50, target_points=600, target_odds=50
)
# Make predictions using the scorecard
credit_scores = scorecard_constructor.predict_score(X_test)
gini = roc_auc_score(y_test, -credit_scores) * 2 - 1
print(f"Test Gini score: {gini:.2%}")
We can also visualize the score distribution between the events of interest.
from xbooster import explainer
explainer.plot_score_distribution(
y_test,
credit_scores,
num_bins=30,
figsize=(8, 3),
dpi=100
)
We can further examine feature importances.
Below, we can visualize the global feature importances using Points as our metric:
from xbooster import explainer
explainer.plot_importance(
scorecard_constructor,
metric='Points',
method='global',
normalize=True,
figsize=(3, 3)
)
Alternatively, we can calculate local feature importances, which are important for boosters with a depth greater than 1.
explainer.plot_importance(
scorecard_constructor,
metric='Likelihood',
method='local',
normalize=True,
color='#ffd43b',
edgecolor='#1e1e1e',
figsize=(3, 3)
)
Finally, we can generate a scorecard in SQL format.
sql_query = scorecard_constructor.generate_sql_query(table_name='my_table')
print(sql_query)
For handling categorical features in XGBoost, you can use the DataPreprocessor
:
from xbooster._utils import DataPreprocessor
# Define features and target
numerical_features = [
"ApplicantIncome",
"CoapplicantIncome",
"LoanAmount",
"Loan_Amount_Term",
"Credit_History",
]
categorical_features = [
"Married",
"Dependents",
"Education",
"Self_Employed",
"Property_Area",
]
target = "Loan_Status"
# Initialize preprocessor
preprocessor = DataPreprocessor(
numerical_features,
categorical_features,
target
)
# Preprocess data
X, y = preprocessor.fit_transform(dataset)
# Get one-hot encoded feature names
features_ohe = [
col for col in X.columns
if col not in numerical_features
]
# Generate interaction constraints for XGBoost
interaction_constraints = preprocessor.generate_interaction_constraints(features_ohe)
The DataPreprocessor
provides:
- Automatic one-hot encoding of categorical features
- Proper handling of missing values
- Generation of interaction constraints for XGBoost
- Consistent feature naming for scorecard generation
xbooster provides experimental support for CatBoost models with reduced functionality compared to XGBoost. Here's how to use it:
import pandas as pd
from catboost import CatBoostClassifier, Pool
from xbooster.constructor import CatBoostScorecardConstructor
# Load data and prepare features
data_path = "examples/data/test_data_01d9ab8b.csv"
credit_data = pd.read_csv(data_path)
num_features = ["Gross_Annual_Income", "Application_Score", "Bureau_Score"]
categorical_features = ["Time_with_Bank"]
features = num_features + categorical_features
# Prepare X and y
X = credit_data[features]
y = credit_data["Final_Decision"].replace({"Accept": 1, "Decline": 0})
# Create CatBoost Pool
pool = Pool(
data=X,
label=y,
cat_features=categorical_features,
)
# Initialize and train CatBoost model
model = CatBoostClassifier(
iterations=100,
allow_writing_files=False,
depth=1,
learning_rate=0.1,
verbose=0,
one_hot_max_size=9999, # Key for interpretability
)
model.fit(pool)
# Create and fit the scorecard constructor
constructor = CatBoostScorecardConstructor(model, pool) # use_woe=False is the default, using raw LeafValue
# Alternatively, to use WOE values instead of raw leaf values:
# constructor = CatBoostScorecardConstructor(model, pool, use_woe=True)
# Construct the scorecard
scorecard = constructor.construct_scorecard()
print("\nScorecard:")
print(scorecard.head(3))
# Print raw leaf values
print("\nRaw Leaf Values:")
print(scorecard[["Tree", "LeafIndex", "LeafValue", "WOE"]].head(10))
# Make predictions using different methods - Do this BEFORE creating points
# Original CatBoost predictions
cb_preds = model.predict(X, prediction_type="RawFormulaVal")
# Get raw scores and WOE scores
raw_scores = constructor.predict_score(X, method="raw")
woe_scores = constructor.predict_score(X, method="woe")
# Now create points for the scorecard
scorecard_with_points = constructor.create_points(
pdo=50,
target_points=600,
target_odds=19,
precision_points=0
)
# Calculate points-based scores
points_scores = constructor.predict_score(X, method="pdo")
# Even after creating points, raw and WOE scores remain consistent
# This is because the constructor maintains the original mappings
new_raw_scores = constructor.predict_score(X, method="raw")
new_woe_scores = constructor.predict_score(X, method="woe")
# Verify that raw scores still match CatBoost predictions
np.testing.assert_allclose(new_raw_scores, cb_preds, rtol=1e-2, atol=1e-2)
# Calculate Gini scores
from sklearn.metrics import roc_auc_score
raw_gini = 2 * roc_auc_score(y, raw_scores) - 1
woe_gini = 2 * roc_auc_score(y, woe_scores) - 1
points_gini = 2 * roc_auc_score(y, points_scores) - 1
print("\nGini Coefficients:")
print(f"Raw Scores: {raw_gini:.4f}")
print(f"WOE Scores: {woe_gini:.4f}")
print(f"Points Scores: {points_gini:.4f}")
# Get feature importance
feature_importance = constructor.get_feature_importance()
print("\nFeature Importance:")
for feature, importance in feature_importance.items():
print(f"{feature}: {importance:.4f}")
# Visualize a tree
from xbooster._utils import CatBoostTreeVisualizer
visualizer = CatBoostTreeVisualizer(scorecard)
visualizer.plot_tree(tree_idx=0, title="CatBoost Tree Visualization")
The CatBoost implementation has some limitations compared to the XGBoost version:
- Only supports depth=1 trees for interpretability
- Limited support for categorical features
- No SQL query generation
- Reduced visualization options
- No support for local feature importance
- No support for score distribution plots
For high-cardinality categorical features, you can use the CatBoostPreprocessor
:
from xbooster._utils import CatBoostPreprocessor
# Initialize preprocessor
preprocessor = CatBoostPreprocessor(max_categories=10) # or top_p=0.9
# Fit and transform the data
X_processed = preprocessor.fit_transform(X, cat_features=categorical_features)
# Get the mapping of categories
category_maps = preprocessor.get_mapping()
The CatBoostTreeVisualizer
class provides basic tree visualization with customizable settings:
from xbooster._utils import CatBoostTreeVisualizer
# Initialize visualizer with custom configuration
plot_config = {
"font_size": 12,
"figsize": (12, 8),
"level_distance": 8.0,
"sibling_distance": 8.0,
"fontfamily": "monospace",
"yes_color": "#1f77b4",
"no_color": "#ff7f0e",
"leaf_color": "#2ca02c",
}
visualizer = CatBoostTreeVisualizer(scorecard, plot_config)
visualizer.plot_tree(tree_idx=0, title="Customized Tree Visualization")
A class for generating a scorecard from a trained XGBoost model. The methodology is inspired by the NVIDIA GTC Talk "Machine Learning in Retail Credit Risk" by Paul Edwards.
-
extract_leaf_weights() -> pd.DataFrame
:- Extracts the leaf weights from the booster's trees and returns a DataFrame.
- Returns:
pd.DataFrame
: DataFrame containing the extracted leaf weights.
-
extract_decision_nodes() -> pd.DataFrame
:- Extracts the split (decision) nodes from the booster's trees and returns a DataFrame.
- Returns:
pd.DataFrame
: DataFrame containing the extracted split (decision) nodes.
-
construct_scorecard() -> pd.DataFrame
:- Constructs a scorecard based on a booster.
- Returns:
pd.DataFrame
: The constructed scorecard.
-
create_points(pdo=50, target_points=600, target_odds=19, precision_points=0, score_type='XAddEvidence') -> pd.DataFrame
:- Creates a points card from a scorecard.
- Parameters:
pdo
(int, optional): The points to double the odds. Default is 50.target_points
(int, optional): The standard scorecard points. Default is 600.target_odds
(int, optional): The standard scorecard odds. Default is 19.precision_points
(int, optional): The points decimal precision. Default is 0.score_type
(str, optional): The log-odds to use for the points card. Default is 'XAddEvidence'.
- Returns:
pd.DataFrame
: The points card.
-
predict_score(X: pd.DataFrame) -> pd.Series
:- Predicts the score for a given dataset using the constructed scorecard.
- Parameters:
X
(pd.DataFrame
): Features of the dataset.
- Returns:
pd.Series
: Predicted scores.
-
sql_query
(property):- Property that returns the SQL query for deploying the scorecard.
- Returns:
str
: The SQL query for deploying the scorecard.
-
generate_sql_query(table_name: str = "my_table") -> str
:- Converts a scorecard into an SQL format.
- Parameters:
table_name
(str): The name of the input table in SQL.
- Returns:
str
: The final SQL query for deploying the scorecard.
This module provides functionalities for explaining XGBoost scorecards, including methods to extract split information, build interaction splits, visualize tree structures, plot feature importances, and more.
-
extract_splits_info(features: str) -> list
:- Extracts split information from the DetailedSplit feature.
- Inputs:
features
(str): A string containing split information.
- Outputs:
- Returns a list of tuples containing split information (feature, sign, value).
-
build_interactions_splits(scorecard_constructor: Optional[XGBScorecardConstructor] = None, dataframe: Optional[pd.DataFrame] = None) -> pd.DataFrame
:- Builds interaction splits from the XGBoost scorecard.
- Inputs:
scorecard_constructor
(Optional[XGBScorecardConstructor]): The XGBoost scorecard constructor.dataframe
(Optional[pd.DataFrame]): The dataframe containing split information.
- Outputs:
- Returns a pandas DataFrame containing interaction splits.
-
split_and_count(scorecard_constructor: Optional[XGBScorecardConstructor] = None, dataframe: Optional[pd.DataFrame] = None, label_column: Optional[str] = None) -> pd.DataFrame
:- Splits the dataset and counts events for each split.
- Inputs:
scorecard_constructor
(Optional[XGBScorecardConstructor]): The XGBoost scorecard constructor.dataframe
(Optional[pd.DataFrame]): The dataframe containing features and labels.label_column
(Optional[str]): The label column in the dataframe.
- Outputs:
- Returns a pandas DataFrame containing split information and event counts.
-
plot_importance(scorecard_constructor: Optional[XGBScorecardConstructor] = None, metric: str = "Likelihood", normalize: bool = True, method: Optional[str] = None, dataframe: Optional[pd.DataFrame] = None, **kwargs: Any) -> None
:- Plots the importance of features based on the XGBoost scorecard.
- Inputs:
scorecard_constructor
(Optional[XGBScorecardConstructor]): The XGBoost scorecard constructor.metric
(str): Metric to plot ("Likelihood" (default), "NegLogLikelihood", "IV", or "Points").normalize
(bool): Whether to normalize the importance values (default: True).method
(Optional[str]): The method to use for plotting the importance ("global" or "local").dataframe
(Optional[pd.DataFrame]): The dataframe containing features and labels.fontfamily
(str): The font family to use for the plot (default: "Monospace").fontsize
(int): The font size to use for the plot (default: 12).dpi
(int): The DPI of the plot (default: 100).title
(str): The title of the plot (default: "Feature Importance").**kwargs
(Any): Additional Matplotlib parameters.
-
plot_score_distribution(y_true: pd.Series = None, y_pred: pd.Series = None, n_bins: int = 25, scorecard_constructor: Optional[XGBScorecardConstructor] = None, **kwargs: Any)
:- Plots the distribution of predicted scores based on actual labels.
- Inputs:
y_true
(pd.Series): The true labels.y_pred
(pd.Series): The predicted labels.n_bins
(int): Number of bins for histogram (default: 25).scorecard_constructor
(Optional[XGBScorecardConstructor]): The XGBoost scorecard constructor.**kwargs
(Any): Additional Matplotlib parameters.
-
plot_local_importance(scorecard_constructor: Optional[XGBScorecardConstructor] = None, metric: str = "Likelihood", normalize: bool = True, dataframe: Optional[pd.DataFrame] = None, **kwargs: Any) -> None
:- Plots the local importance of features based on the XGBoost scorecard.
- Inputs:
scorecard_constructor
(Optional[XGBScorecardConstructor]): The XGBoost scorecard constructor.metric
(str): Metric to plot ("Likelihood" (default), "NegLogLikelihood", "IV", or "Points").normalize
(bool): Whether to normalize the importance values (default: True).dataframe
(Optional[pd.DataFrame]): The dataframe containing features and labels.fontfamily
(str): The font family to use for the plot (default: "Arial").fontsize
(int): The font size to use for the plot (default: 12).boxstyle
(str): The rounding box style to use for the plot (default: "round").title
(str): The title of the plot (default: "Local Feature Importance").**kwargs
(Any): Additional parameters to pass to the matplotlib function.
-
plot_tree(tree_index: int, scorecard_constructor: Optional[XGBScorecardConstructor] = None, show_info: bool = True) -> None
:- Plots the tree structure.
- Inputs:
tree_index
(int): Index of the tree to plot.scorecard_constructor
(Optional[XGBScorecardConstructor]): The XGBoost scorecard constructor.show_info
(bool): Whether to show additional information (default: True).**kwargs
(Any): Additional Matplotlib parameters.
Contributions are welcome! For bug reports or feature requests, please open an issue.
For code contributions, please open a pull request.
Current version: 0.2.5
- Minor changes in
catboost_wrapper.py
andcb_constructor.py
to improve the scorecard generation.
- Changed the build distribution in pyproject.toml.
- Added support for CatBoost classification models and switch to
uv
for packaging. - Python version requirement updated to 3.10-3.11.
- Updates in
explainer.py
module to improve kwargs handling and minor changes.
- Updates of dependencies
- Added tree visualization class (
explainer.py
) - Updated the local explanation algorithm for models with a depth > 1 (
explainer.py
) - Added a categorical preprocessor (
_utils.py
)
- Initial release
This project is licensed under the MIT License - see the LICENSE file for details.