3.2. Cost-Sensitive Gradient Boosting (CSBoost & B2Boost)#
CSBoostClassifier [1] is a cost-sensitive gradient boosting model that
injects a cost-sensitive objective directly into the boosting algorithm so that every tree split
minimises a business loss rather than a standard logistic loss.
It wraps three popular gradient boosting backends — XGBoost, LightGBM, and CatBoost — behind a
unified sklearn-compatible interface. If no estimator is supplied, an
xgboost.XGBClassifier with default hyperparameters is used.
Backend |
Class |
Notes |
|---|---|---|
XGBoost |
Default when no estimator is passed |
|
LightGBM |
Pass as |
|
CatBoost |
Pass as |
Note
You can install all three backends with pip install empulse[boosting].
If you try to use a backend that isn’t installed,
you’ll get an informative error message with installation instructions.
3.2.1. Quick Start#
import numpy as np
from sklearn.datasets import make_classification
from empulse.models import CSBoostClassifier
X, y = make_classification(n_samples=1000, random_state=0)
model = CSBoostClassifier(fp_cost=5, fn_cost=1)
model.fit(X, y)
y_proba = model.predict_proba(X)
3.2.2. Choosing a Backend#
Pass any supported estimator to the estimator argument. All three are installed by
pip install empulse[boosting]; the cost-sensitive objective is identical, so the choice is the
usual one between the libraries themselves.
The default. Used when estimator is left as None.
from xgboost import XGBClassifier
from empulse.models import CSBoostClassifier
model = CSBoostClassifier(
XGBClassifier(n_estimators=200, max_depth=4, learning_rate=0.05),
fp_cost=5,
fn_cost=1,
)
Usually the fastest to fit on wide data.
from lightgbm import LGBMClassifier
from empulse.models import CSBoostClassifier
model = CSBoostClassifier(
LGBMClassifier(n_estimators=200, num_leaves=31, verbose=-1),
fp_cost=5,
fn_cost=1,
)
Handles categorical features natively, but does not accept sample_weight — it uses
that argument internally to carry sample indices through to the cost-sensitive objective.
from catboost import CatBoostClassifier
from empulse.models import CSBoostClassifier
model = CSBoostClassifier(
CatBoostClassifier(iterations=200, depth=4, verbose=0),
fp_cost=5,
fn_cost=1,
)
Note
When using GridSearchCV to tune
estimator hyperparameters you must supply an explicit estimator instance.
Without one, sklearn would try to set the parameter on None, which raises
an error.
from sklearn.model_selection import GridSearchCV
from xgboost import XGBClassifier
from empulse.models import CSBoostClassifier
model = CSBoostClassifier(XGBClassifier(), fp_cost=5, fn_cost=1)
grid_search = GridSearchCV(
model,
param_grid={'estimator__max_depth': [3, 5, 7]},
cv=3,
)
3.2.3. Specifying costs#
CSBoostClassifier accepts costs the same two ways as every other cost-sensitive model in
Empulse: as plain tp_cost/tn_cost/fp_cost/fn_cost values, scalar or per-sample, or
as a Metric passed as loss. The rules — where each may be set, how
constructor and fit values interact, and how the two forms may be mixed — are in
Handing costs to an estimator.
One rule is specific to this model: arguments for the underlying booster go in a dedicated
fit_params dict, since anything else would be ambiguous with a cost or metric parameter.
from sklearn.datasets import make_classification
from empulse.models import CSBoostClassifier
X, y = make_classification(n_samples=500, random_state=0)
model = CSBoostClassifier(fp_cost=5, fn_cost=1)
model.fit(X, y, fit_params={'verbose': False})
3.2.4. Custom Loss Function#
The loss parameter accepts any Metric, so the objective can be
written from business parameters rather than four flat numbers.
Cost, LogCost and
Savings are fully supported;
MaxProfit works but is experimental here, and the two ranking-based
strategies are not available on a gradient-boosted model. Models by supported strategy has the
matrix.
import sympy
from empulse.metrics import CostMatrix, Metric, Cost
from empulse.models import CSBoostClassifier
clv, d, f, gamma = sympy.symbols('clv d f gamma')
cost_matrix = (
CostMatrix()
.add_tp_benefit(gamma * (clv - d - f))
.add_tp_benefit((1 - gamma) * -f)
.add_fp_cost(d + f)
.alias({'incentive_cost': 'd', 'contact_cost': 'f', 'accept_rate': 'gamma'})
)
expected_cost = Metric(cost_matrix, Cost())
clvs = 200 + 100 * abs(y - 0.5) # toy instance-dependent CLV
model = CSBoostClassifier(loss=expected_cost)
model.fit(X, y, clv=clvs, incentive_cost=10, contact_cost=1, accept_rate=0.3)
See Worked cost matrices for worked cost matrices to use here.
3.2.5. sklearn Integration#
CSBoostClassifier is fully sklearn-compatible and drops into
Pipeline,
cross_val_score and
GridSearchCV unchanged. Per-sample costs reach each fold
through metadata routing, covered in Costs that differ per row.
One caveat is specific to this model. Searching over the booster’s own hyperparameters means
addressing them as model__estimator__*, and GridSearchCV
cannot set attributes on the default estimator=None. Pass an explicit booster instance:
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from xgboost import XGBClassifier
from empulse.models import CSBoostClassifier
X, y = make_classification(n_samples=500, random_state=0)
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', CSBoostClassifier(XGBClassifier(n_jobs=2), fp_cost=5, fn_cost=1)),
])
grid_search = GridSearchCV(
pipeline,
param_grid={'model__estimator__learning_rate': np.logspace(-3, 0, 3)},
)
grid_search.fit(X, y)
print(grid_search.best_params_['model__estimator__learning_rate'])
3.2.6. B2Boost#
B2BoostClassifier is a use-case-specific subclass of
CSBoostClassifier designed for B2B customer churn retention
campaigns [2]. Rather than exposing the raw TP/FP/TN/FN cost matrix, it takes four
business-meaningful parameters:
Parameter |
Default |
Meaning |
|---|---|---|
|
200 |
Customer Lifetime Value — can be a per-sample array |
|
0.05 |
Fraction of CLV spent on the retention incentive |
|
15 |
Fixed cost of contacting a customer |
|
0.3 |
Probability that a churner accepts the retention offer |
The underlying cost model is:
where \(\gamma\) is the accept rate, \(\delta\) the incentive fraction, and
\(f\) the contact cost. CLV is instance-dependent (passed as an array to fit);
the remaining parameters are class-level constants.
3.2.6.1. Quick Start#
import numpy as np
from sklearn.datasets import make_classification
from xgboost import XGBClassifier
from empulse.models import B2BoostClassifier
X, y = make_classification(n_samples=1000, random_state=0)
clv = np.random.default_rng(0).uniform(100, 500, size=len(y))
model = B2BoostClassifier(
XGBClassifier(n_estimators=100, max_depth=3),
accept_rate=0.3,
incentive_fraction=0.05,
contact_cost=10,
)
model.fit(X, y, clv=clv)
y_proba = model.predict_proba(X)
3.2.6.2. Constant CLV#
When all customers have the same lifetime value, pass a scalar:
from empulse.models import B2BoostClassifier
model = B2BoostClassifier(clv=300, accept_rate=0.25, incentive_fraction=0.1, contact_cost=5)
model.fit(X, y)
3.2.6.3. Scoring a B2Boost model#
empb_score is the natural companion metric: it is built from the same B2B
churn cost matrix, so tuning against it optimises the quantity the model was trained on. Pass the
shared business parameters to both.
import numpy as np
from sklearn import config_context
from sklearn.datasets import make_classification
from sklearn.metrics import make_scorer
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from xgboost import XGBClassifier
from empulse.metrics import empb_score
from empulse.models import B2BoostClassifier
X, y = make_classification(n_samples=500, random_state=0)
clv = np.random.default_rng(0).uniform(100, 500, size=len(y))
contact_cost = 10.0
with config_context(enable_metadata_routing=True):
pipeline = Pipeline([
('scaler', StandardScaler()),
(
'model',
B2BoostClassifier(
XGBClassifier(), contact_cost=contact_cost
).set_fit_request(clv=True),
),
])
scorer = make_scorer(
empb_score,
response_method='predict_proba',
greater_is_better=True,
contact_cost=contact_cost,
).set_score_request(clv=True)
grid_search = GridSearchCV(
pipeline,
param_grid={'model__estimator__learning_rate': np.logspace(-3, 0, 3)},
scoring=scorer,
)
grid_search.fit(X, y, clv=clv)
print(grid_search.best_params_['model__estimator__learning_rate'])
See Costs that differ per row for the routing mechanics, and Customer Churn Metrics for the other churn metrics that pair with this model.