44 Gradient Boosting and XGBoost
XGBoost (eXtreme Gradient Boosting) was introduced by Chen and Guestrin in 2016 (Chen and Guestrin 2016) and has since dominated machine learning competitions on Kaggle and benchmark platforms. It is a regularized, second-order optimized implementation of the gradient boosting algorithm (Friedman 2001), incorporating key innovations in approximate split finding, efficient sparse data handling, and system-level scalability. This chapter covers the mathematical foundations of the algorithm, its primary hyperparameters, variable importance interpretation using SHAP, and practical implementation in Python.
1 From Decision Trees to Boosting
1.1 The Bias–Variance Trade-off and Individual Trees
A deep decision tree has the capacity to memorize training data: as depth increases, the model fits observed data ever more closely and its variance increases. Conversely, a shallow tree (or single-level decision stump) exhibits high bias due to limited expressive capacity. This bias–variance trade-off is central to machine learning and motivates ensemble learning.
Ensemble methods combine multiple weak learners to form a strong predictor. In bagging (e.g., Random Forest (Breiman 2001)), decision trees are trained independently on bootstrap samples, and predictions are averaged to reduce variance. In boosting, the strategy differs: models are constructed sequentially, where each new tree learns from the residual errors of the preceding ensemble, progressively reducing bias.
Show code
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from sklearn.tree import DecisionTreeRegressor
rng = np.random.default_rng(42)
n = 80
X_raw = np.sort(rng.uniform(0, 2 * np.pi, n))
y_raw = np.sin(X_raw) + rng.normal(0, 0.35, n)
X_fit = X_raw.reshape(-1, 1)
X_plot = np.linspace(0, 2 * np.pi, 300).reshape(-1, 1)
profundidades = [1, 3, 10]
titulos = [
"Depth 1 Tree (High Bias)",
"Depth 3 Tree (Balanced)",
"Depth 10 Tree (High Variance)"
]
fig = make_subplots(rows=1, cols=3, subplot_titles=titulos)
for idx, (depth, titulo) in enumerate(zip(profundidades, titulos), start=1):
tree = DecisionTreeRegressor(max_depth=depth, random_state=0)
tree.fit(X_fit, y_raw)
y_pred = tree.predict(X_plot)
fig.add_trace(
go.Scatter(
x=X_raw, y=y_raw,
mode="markers",
marker=dict(color="#94a3b8", size=5),
name="Data",
showlegend=(idx == 1)
),
row=1, col=idx
)
fig.add_trace(
go.Scatter(
x=X_plot.flatten(), y=y_pred,
mode="lines",
line=dict(color="#2563eb", width=2.5),
name="Prediction",
showlegend=(idx == 1)
),
row=1, col=idx
)
fig.add_trace(
go.Scatter(
x=X_plot.flatten(), y=np.sin(X_plot.flatten()),
mode="lines",
line=dict(color="black", dash="dash", width=1.5),
name="True Function",
showlegend=(idx == 1)
),
row=1, col=idx
)
fig.update_layout(
height=380,
template="plotly_white",
legend=dict(orientation="h", yanchor="bottom", y=-0.25, xanchor="center", x=0.5)
)
fig.update_xaxes(title_text="x")
fig.update_yaxes(title_text="y", col=1)
fig.show()Figure 44.1 demonstrates how tree depth controls the balance between bias and variance. A depth-3 tree achieves a reasonable fit without overfitting noise.
1.2 Boosting: Sequential Residual Learning
Boosting builds an additive model sequentially. Starting from a constant initial prediction:
\[F_0(\mathbf{x}) = \underset{\gamma}{\mathrm{argmin}} \sum_{i=1}^{n} L(y_i, \gamma) \tag{1}\]
At each iteration \(m = 1, \ldots, M\), negative gradients (pseudo-residuals) of the loss function evaluated at the current ensemble prediction are computed:
\[r_i^{(m)} = -\left[\frac{\partial L(y_i, F(\mathbf{x}_i))}{\partial F(\mathbf{x}_i)}\right]_{F = F_{m-1}} \tag{2}\]
A weak base learner \(h_m\) is fitted to these pseudo-residuals, and the ensemble is updated:
\[F_m(\mathbf{x}) = F_{m-1}(\mathbf{x}) + \eta \cdot h_m(\mathbf{x}) \tag{3}\]
where \(\eta \in (0, 1]\) is the learning rate or shrinkage parameter. For squared error loss \(L(y, F) = \tfrac{1}{2}(y - F)^2\), the negative gradient simplifies to ordinary residuals \(r_i = y_i - F_{m-1}(\mathbf{x}_i)\). Thus, the algorithm iteratively fits ensemble errors: each new tree targets what previous trees failed to explain.
The following interactive plot illustrates the iterative convergence of gradient boosting implemented from scratch:
Show code
import numpy as np
import plotly.graph_objects as go
rng2 = np.random.default_rng(7)
n2 = 60
X2 = np.sort(rng2.uniform(0, 2 * np.pi, n2)).reshape(-1, 1)
y2 = np.sin(X2.flatten()) + rng2.normal(0, 0.3, n2)
X_line = np.linspace(0, 2 * np.pi, 300).reshape(-1, 1)
y_true_line = np.sin(X_line.flatten())
class Stump:
def fit(self, X, r):
best_feat, best_thr, best_loss = 0, 0, float("inf")
for feat in range(X.shape[1]):
thresholds = np.unique(X[:, feat])
for thr in thresholds:
left = r[X[:, feat] <= thr]
right = r[X[:, feat] > thr]
if len(left) == 0 or len(right) == 0:
continue
loss = np.sum((left - left.mean()) ** 2) + np.sum((right - right.mean()) ** 2)
if loss < best_loss:
best_loss = loss
best_feat, best_thr = feat, thr
self.val_left, self.val_right = left.mean(), right.mean()
self.feat, self.thr = best_feat, best_thr
def predict(self, X):
return np.where(X[:, self.feat] <= self.thr, self.val_left, self.val_right)
eta = 0.5
M_max = 20
stumps = []
F_current = np.full(n2, y2.mean())
snapshots = {}
rondas = [1, 2, 3, 5, 10, 20]
for m in range(1, M_max + 1):
residuos = y2 - F_current
stump = Stump()
stump.fit(X2, residuos)
stumps.append(stump)
F_current = F_current + eta * stump.predict(X2)
if m in rondas:
F_line = np.full(300, y2.mean())
for s in stumps:
F_line = F_line + eta * s.predict(X_line)
snapshots[m] = {
"F_line": F_line.copy(),
"residuos": (y2 - F_current).copy()
}
frames = []
for m in rondas:
snap = snapshots[m]
frame = go.Frame(
data=[
go.Scatter(
x=X2.flatten(), y=y2,
mode="markers",
marker=dict(color="#94a3b8", size=6),
name="Data"
),
go.Scatter(
x=X_line.flatten(), y=y_true_line,
mode="lines",
line=dict(color="black", dash="dash", width=1.5),
name="True Function"
),
go.Scatter(
x=X_line.flatten(), y=snap["F_line"],
mode="lines",
line=dict(color="#2563eb", width=2.5),
name="Prediction F_m"
),
go.Scatter(
x=X2.flatten(), y=snap["residuos"],
mode="markers",
marker=dict(color="#f97316", size=6, symbol="diamond"),
name="Residuals"
)
],
name=str(m)
)
frames.append(frame)
snap0 = snapshots[1]
fig2 = go.Figure(
data=[
go.Scatter(x=X2.flatten(), y=y2, mode="markers",
marker=dict(color="#94a3b8", size=6), name="Data"),
go.Scatter(x=X_line.flatten(), y=y_true_line, mode="lines",
line=dict(color="black", dash="dash", width=1.5), name="True Function"),
go.Scatter(x=X_line.flatten(), y=snap0["F_line"], mode="lines",
line=dict(color="#2563eb", width=2.5), name="Prediction F_m"),
go.Scatter(x=X2.flatten(), y=snap0["residuos"], mode="markers",
marker=dict(color="#f97316", size=6, symbol="diamond"), name="Residuals")
],
frames=frames
)
steps = []
for m in rondas:
step = dict(
method="animate",
args=[[str(m)], {"frame": {"duration": 600, "redraw": True}, "mode": "immediate"}],
label=str(m)
)
steps.append(step)
fig2.update_layout(
template="plotly_white",
height=440,
xaxis_title="x",
yaxis_title="y",
legend=dict(orientation="h", yanchor="bottom", y=-0.3, xanchor="center", x=0.5),
sliders=[dict(
active=0,
currentvalue=dict(prefix="M = ", font=dict(size=14)),
pad=dict(t=50),
steps=steps
)],
updatemenus=[dict(
type="buttons",
showactive=False,
y=1.1, x=0.5, xanchor="center",
buttons=[
dict(label="Play", method="animate",
args=[None, {"frame": {"duration": 800, "redraw": True}, "fromcurrent": True}]),
dict(label="Pause", method="animate",
args=[[None], {"frame": {"duration": 0, "redraw": False}, "mode": "immediate"}])
]
)]
)
fig2.show()As seen in Figure 44.2, residual errors (orange diamonds) progressively shrink as trees are added, and prediction curves (blue) converge toward the underlying true function (dashed black).
Learning rate \(\eta\) in Eq. 44.3 acts as a convergence rate controller. Setting \(\eta=1\) aggressively corrects errors at each step, accelerating overfitting risks. Smaller \(\eta\) values require more boosting rounds but yield superior generalization (shrinkage, Friedman (2002)).
Gradient boosting (Eq. 44.2) represents functional gradient descent: updating target functions directly in function space rather than updating parametric weights in Euclidean space (Friedman 2001).
2 XGBoost Optimization Objective
2.1 Additive Model and Regularized Objective
XGBoost builds additive tree models:
\[\hat{y}_i^{(t)} = \sum_{k=1}^{t} f_k(\mathbf{x}_i), \quad f_k \in \mathcal{F} \tag{4}\]
where \(\mathcal{F}\) is the space of regression trees. At round \(t\), tree \(f_t\) is chosen to minimize the regularized objective:
\[\text{Obj}^{(t)} = \sum_{i=1}^{n} l\!\left(y_i,\, \hat{y}_i^{(t-1)} + f_t(\mathbf{x}_i)\right) + \Omega(f_t) + \text{constant} \tag{5}\]
Regularization penalty \(\Omega\) penalizes leaf counts and weight magnitudes:
\[\Omega(f) = \gamma T + \frac{1}{2}\lambda\|\mathbf{w}\|^2 \tag{6}\]
where \(T\) is the number of terminal leaves in \(f_t\) and \(\mathbf{w}\) denotes leaf weights. Parameter \(\gamma\) controls pruning thresholding while \(\lambda\) imposes L2 weight regularization.
2.2 Second-Order Taylor Approximation
Applying a second-order Taylor expansion around \(\hat{y}_i^{(t-1)}\):
\[l\!\left(y_i, \hat{y}_i^{(t-1)} + f_t\right) \approx l\!\left(y_i, \hat{y}_i^{(t-1)}\right) + g_i f_t(\mathbf{x}_i) + \frac{1}{2} h_i f_t^2(\mathbf{x}_i) \tag{7}\]
where:
\[g_i = \frac{\partial\, l(y_i, \hat{y}^{(t-1)})}{\partial\, \hat{y}^{(t-1)}} \quad \text{(first-order gradient)} \tag{8}\]
\[h_i = \frac{\partial^2\, l(y_i, \hat{y}^{(t-1)})}{\partial\, (\hat{y}^{(t-1)})^2} \quad \text{(second-order hessian / local curvature)} \tag{9}\]
Removing constant terms yields the simplified objective at step \(t\):
\[\tilde{\text{Obj}}^{(t)} = \sum_{i=1}^{n}\left[g_i f_t(\mathbf{x}_i) + \frac{1}{2}h_i f_t^2(\mathbf{x}_i)\right] + \gamma T + \frac{1}{2}\lambda\sum_{j=1}^{T}w_j^2 \tag{10}\]
This quadratic formulation is exact for squared error loss and provides an effective local quadratic approximation for any twice-differentiable loss function.
Show code
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
y_obs = 1.0
F0 = -0.3
F_vals = np.linspace(-2.0, 2.5, 400)
loss_true = 0.5 * (y_obs - F_vals) ** 2
g_i = -(y_obs - F0)
h_i = 1.0
loss_linear = 0.5 * (y_obs - F0) ** 2 + g_i * (F_vals - F0)
loss_quadratic = 0.5 * (y_obs - F0) ** 2 + g_i * (F_vals - F0) + 0.5 * h_i * (F_vals - F0) ** 2
lam = 1.0
delta_vals = np.linspace(-3.0, 3.0, 400)
obj_delta = g_i * delta_vals + 0.5 * (h_i + lam) * delta_vals ** 2
delta_opt = -g_i / (h_i + lam)
obj_opt = g_i * delta_opt + 0.5 * (h_i + lam) * delta_opt ** 2
fig3 = make_subplots(
rows=1, cols=2,
subplot_titles=[
"Loss Curve & Taylor Approximation",
"Quadratic Objective Contribution (with lambda)"
]
)
fig3.add_trace(
go.Scatter(x=F_vals, y=loss_true, mode="lines",
line=dict(color="black", width=2.5),
name="True Loss L(y, F)"),
row=1, col=1
)
fig3.add_trace(
go.Scatter(x=F_vals, y=loss_linear, mode="lines",
line=dict(color="#f97316", width=1.8, dash="dot"),
name="Linear Approx (1st Order)"),
row=1, col=1
)
fig3.add_trace(
go.Scatter(x=F_vals, y=loss_quadratic, mode="lines",
line=dict(color="#2563eb", width=2.0, dash="dash"),
name="Quadratic Approx (2nd Order)"),
row=1, col=1
)
fig3.add_trace(
go.Scatter(x=[F0], y=[0.5 * (y_obs - F0) ** 2],
mode="markers",
marker=dict(color="red", size=10, symbol="circle"),
name="Expansion Point F_0"),
row=1, col=1
)
fig3.add_trace(
go.Scatter(x=delta_vals, y=obj_delta, mode="lines",
line=dict(color="#2563eb", width=2.5),
name="g*Delta + (1/2)(h+lambda)*Delta^2",
showlegend=True),
row=1, col=2
)
fig3.add_trace(
go.Scatter(
x=[delta_opt, delta_opt],
y=[obj_delta.min() - 0.3, obj_delta.max()],
mode="lines",
line=dict(color="red", dash="dash", width=1.8),
name="Optimal Delta* = -g/(h+lambda)"
),
row=1, col=2
)
fig3.add_trace(
go.Scatter(x=[delta_opt], y=[obj_opt],
mode="markers",
marker=dict(color="red", size=10),
name="Analytical Minimum",
showlegend=False),
row=1, col=2
)
fig3.add_annotation(
x=delta_opt, y=obj_opt - 0.5,
text=f"Delta* = {delta_opt:.2f}",
showarrow=True, arrowhead=2,
font=dict(size=12, color="red"),
row=1, col=2
)
fig3.update_xaxes(title_text="F (Prediction)", row=1, col=1)
fig3.update_yaxes(title_text="Loss L(y, F)", row=1, col=1)
fig3.update_xaxes(title_text="Delta (Prediction Change)", row=1, col=2)
fig3.update_yaxes(title_text="Objective Contribution", row=1, col=2)
fig3.update_layout(
height=420,
template="plotly_white",
legend=dict(orientation="h", yanchor="bottom", y=-0.35, xanchor="center", x=0.5)
)
fig3.show()2.3 Optimal Leaf Weights
Grouping sample indices by terminal leaf node \(j\) (\(I_j = \{i : q(\mathbf{x}_i) = j\}\)):
\[\tilde{\text{Obj}}^{(t)} = \sum_{j=1}^{T}\left[\left(\sum_{i\in I_j}g_i\right)w_j + \frac{1}{2}\left(\sum_{i\in I_j}h_i + \lambda\right)w_j^2\right] + \gamma T \tag{11}\]
Defining \(G_j = \sum_{i\in I_j} g_i\) and \(H_j = \sum_{i\in I_j} h_i\), solving \(\frac{\partial \tilde{\text{Obj}}}{\partial w_j} = 0\) yields optimal leaf weight \(w_j^*\):
\[w_j^* = -\frac{G_j}{H_j + \lambda} \tag{12}\]
Substituting optimal weights back into the objective yields the structure score:
\[\text{Obj}^* = -\frac{1}{2}\sum_{j=1}^{T}\frac{G_j^2}{H_j + \lambda} + \gamma T \tag{13}\]
2.4 Split Gain Metric
For a node split dividing parent node \(P\) into left (\(L\)) and right (\(R\)) children:
\[\text{Gain} = \frac{1}{2}\left[\frac{G_L^2}{H_L+\lambda} + \frac{G_R^2}{H_R+\lambda} - \frac{G_P^2}{H_P+\lambda}\right] - \gamma \tag{14}\]
If \(\text{Gain} \le 0\), the candidate split is pruned.
Show code
import numpy as np
import plotly.graph_objects as go
rng3 = np.random.default_rng(21)
n3 = 150
X3 = np.sort(rng3.uniform(0, 10, n3))
y3 = 2.0 * np.sin(X3 * 0.8) + rng3.normal(0, 0.5, n3)
F_prev = np.full(n3, y3.mean())
g_vals = -(y3 - F_prev)
h_vals = np.ones(n3)
lam3 = 1.0
G_total = g_vals.sum()
H_total = h_vals.sum()
score_parent = G_total ** 2 / (H_total + lam3)
thresholds = np.unique(X3)[1:-1]
gains = []
for thr in thresholds:
left_mask = X3 <= thr
right_mask = ~left_mask
G_L = g_vals[left_mask].sum()
H_L = h_vals[left_mask].sum()
G_R = g_vals[right_mask].sum()
H_R = h_vals[right_mask].sum()
if H_L + lam3 <= 0 or H_R + lam3 <= 0:
gains.append(float("nan"))
continue
gain = 0.5 * (G_L ** 2 / (H_L + lam3) + G_R ** 2 / (H_R + lam3) - score_parent)
gains.append(gain)
gains = np.array(gains)
best_idx = np.nanargmax(gains)
best_thr = thresholds[best_idx]
best_gain = gains[best_idx]
fig4 = go.Figure()
fig4.add_trace(go.Scatter(
x=thresholds, y=gains,
mode="lines",
fill="tozeroy",
fillcolor="rgba(37, 99, 235, 0.15)",
line=dict(color="#2563eb", width=2.0),
name="Gain"
))
fig4.add_trace(go.Scatter(
x=[best_thr, best_thr],
y=[gains.min() - 0.5, best_gain],
mode="lines",
line=dict(color="red", dash="dash", width=2.0),
name=f"Optimal Threshold = {best_thr:.2f}"
))
fig4.add_trace(go.Scatter(
x=[thresholds.min(), thresholds.max()],
y=[0, 0],
mode="lines",
line=dict(color="gray", dash="dash", width=1.5),
name="Gain = 0 (Pruning)"
))
fig4.add_annotation(
x=best_thr, y=best_gain + 0.3,
text=f"Optimal: thr={best_thr:.2f}, Gain={best_gain:.2f}",
showarrow=True, arrowhead=2,
font=dict(size=12, color="red")
)
fig4.update_layout(
template="plotly_white",
height=400,
xaxis_title="Split Threshold",
yaxis_title="Gain",
legend=dict(orientation="h", yanchor="bottom", y=-0.3, xanchor="center", x=0.5)
)
fig4.show()2.5 Approximate Split Finding and Sparsity Awareness
For massive datasets, exact greedy split search is computationally prohibitive. XGBoost employs a weighted quantile sketch strategy to propose candidate split points \(\mathcal{S}_k = \{s_{k1}, s_{k2}, \ldots, s_{kl}\}\) based on feature distributions weighted by hessians \(h_i\) (Chen and Guestrin 2016):
\[\text{rank}_k(z) = \frac{\sum_{(x,h): x_k < z} h}{\sum_{(x,h)} h} \tag{15}\]
XGBoost also features native sparsity awareness: it learns default split directions for missing or zero values at each node, bypassing pre-imputation requirements.
3 Key Hyperparameters
| Parameter | Description | Typical Value |
|---|---|---|
n_estimators |
Total boosting rounds (trees) | 100–1000 |
max_depth |
Maximum tree depth per weak learner | 3–8 |
learning_rate (\(\eta\)) |
Shrinkage step size | 0.01–0.3 |
subsample |
Row subsampling ratio per tree | 0.6–1.0 |
colsample_bytree |
Feature subsampling ratio per tree | 0.6–1.0 |
min_child_weight |
Minimum sum of instance hessian \(H_j\) per leaf | 1–10 |
gamma (\(\gamma\)) |
Minimum gain threshold required for node split | 0–5 |
reg_alpha |
L1 regularization weight on leaf values | 0–1 |
reg_lambda |
L2 regularization weight on leaf values | 1 (default) |
3.1 Early Stopping Demonstration
import numpy as np
import plotly.graph_objects as go
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
import xgboost as xgb
data_ca = fetch_california_housing()
X_ca, y_ca = data_ca.data, data_ca.target
X_tr, X_va, y_tr, y_va = train_test_split(X_ca, y_ca, test_size=0.2, random_state=42)
dtrain = xgb.DMatrix(X_tr, label=y_tr)
dval = xgb.DMatrix(X_va, label=y_va)
params = {
"objective": "reg:squarederror",
"max_depth": 4,
"learning_rate": 0.1,
"subsample": 0.8,
"colsample_bytree": 0.8,
"reg_lambda": 1.0,
"seed": 42
}
evals_result = {}
model_conv = xgb.train(
params,
dtrain,
num_boost_round=300,
evals=[(dtrain, "train"), (dval, "validation")],
early_stopping_rounds=20,
evals_result=evals_result,
verbose_eval=False
)
best_round = model_conv.best_iteration
print(f"Optimal boosting round (early stopping): {best_round + 1}")4 Variable Importance Metrics
XGBoost evaluates feature importance via three metrics:
- Weight (frequency): Relative count of splits on feature \(k\) across all trees.
- Gain: Average loss reduction achieved by splits on feature \(k\) (\(\text{Gain}_k = \frac{1}{|\mathcal{T}_k|} \sum_{t \in \mathcal{T}_k} \text{Gain}_t\)).
- Coverage: Average sample count passing through splits on feature \(k\).
5 Feature Contributions (SHAP Values)
SHAP values (Lundberg and Lee (2017); Lundberg et al. (2020)) decompose individual predictions into additive feature contributions:
\[\hat{y}_i = \phi_0 + \sum_{j=1}^{p} \phi_j \tag{16}\]
where \(\phi_0 = \mathbb{E}[\hat{y}]\) is baseline expectation and \(\phi_j\) is feature \(j\)’s localized contribution for instance \(i\).
Show code
import numpy as np
import plotly.graph_objects as go
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
import xgboost as xgb
data_wf = fetch_california_housing()
X_wf, y_wf = data_wf.data, data_wf.target
feat_names_wf = list(data_wf.feature_names)
X_tr_wf, X_te_wf, y_tr_wf, y_te_wf = train_test_split(X_wf, y_wf, test_size=0.2, random_state=42)
dtrain_wf = xgb.DMatrix(X_tr_wf, label=y_tr_wf, feature_names=feat_names_wf)
params_wf = {
"objective": "reg:squarederror",
"max_depth": 4,
"learning_rate": 0.1,
"subsample": 0.8,
"colsample_bytree": 0.8,
"reg_lambda": 1.0,
"seed": 42
}
evals_wf = {}
model_wf = xgb.train(
params_wf, dtrain_wf,
num_boost_round=300,
evals=[(dtrain_wf, "train")],
evals_result=evals_wf,
verbose_eval=False
)
obs_idx = 42
X_single = X_te_wf[obs_idx:obs_idx + 1]
d_single = xgb.DMatrix(X_single, feature_names=feat_names_wf)
contribs = model_wf.predict(d_single, pred_contribs=True)[0]
bias = contribs[-1]
feature_contribs = contribs[:-1]
sorted_idx = np.argsort(np.abs(feature_contribs))[::-1]
sorted_feats = [feat_names_wf[i] for i in sorted_idx]
sorted_contribs = feature_contribs[sorted_idx]
running = [bias]
for c in sorted_contribs:
running.append(running[-1] + c)
final_pred = running[-1]
labels = ["Base"] + sorted_feats
measures = ["absolute"] + ["relative"] * len(sorted_feats)
fig9 = go.Figure(go.Waterfall(
orientation="v",
measure=measures,
x=labels,
y=[bias] + sorted_contribs.tolist(),
connector=dict(line=dict(color="gray", width=1)),
increasing=dict(marker=dict(color="#16a34a")),
decreasing=dict(marker=dict(color="#dc2626")),
totals=dict(marker=dict(color="#6366f1"))
))
fig9.add_annotation(
x=len(labels) - 1,
y=final_pred + 0.15,
text=f"Final Prediction: {final_pred:.3f}",
showarrow=True,
arrowhead=2,
font=dict(size=12, color="black")
)
fig9.update_layout(
title="Individual Feature Contributions (TreeSHAP)",
template="plotly_white",
height=480,
xaxis_title="Feature",
yaxis_title="Prediction Contribution",
showlegend=False
)
fig9.show()6 XGBoost for Classification
- Binary Classification:
objective="binary:logistic", \(p_i = \sigma(\hat{y}_i)\), \(g_i = p_i - y_i\), \(h_i = p_i(1-p_i)\). - Multiclass Classification:
objective="multi:softprob"withnum_class=K.
from sklearn.datasets import load_breast_cancer
from sklearn.metrics import roc_auc_score
import xgboost as xgb
data_clf = load_breast_cancer()
X_c, y_c = data_clf.data, data_clf.target
X_tr_c, X_te_c, y_tr_c, y_te_c = train_test_split(X_c, y_c, test_size=0.2, random_state=42)
clf = xgb.XGBClassifier(
n_estimators=100, max_depth=4, learning_rate=0.1,
subsample=0.8, colsample_bytree=0.8, eval_metric="logloss",
random_state=42, verbosity=0
)
clf.fit(X_tr_c, y_tr_c)
auc_score = roc_auc_score(y_te_c, clf.predict_proba(X_te_c)[:, 1])
print(f"Test ROC-AUC: {auc_score:.4f}")7 Best Practices and Ensemble Comparisons
| Method | Complexity | Interpretability | Typical Performance |
|---|---|---|---|
| Single Tree | Low | High | Low |
| Random Forest | Medium | Medium | Good |
| Gradient Boosting (sklearn) | Medium | Medium | Very Good |
| XGBoost | Medium-High | Medium (+ SHAP) | Excellent |
| LightGBM / CatBoost | Medium-High | Medium (+ SHAP) | Excellent |
Key operational recommendations:
- Always enforce early stopping (
early_stopping_rounds) on validation sets. - Enable feature and instance subsampling (
subsample=0.8,colsample_bytree=0.8). - Evaluate global feature importance via
gainrather thanweight. - Utilize SHAP waterfall plots for granular localized prediction explanations.