← All Posts
From Scratch · C++ · ML · Explainability · Tree SHAP · Part 2

Path-Dependent Tree SHAP in C++

The exact coalition implementation is clear but exponential. A practical tree-specific implementation uses the tree path and training covers. This post keeps the implementation intentionally small: it computes expectation changes along the hot path and assigns each change to the split feature. It is not a full optimized Tree SHAP library, but it exposes the structural advantage over black-box sampling.

Expected Value from Covers

double expected_value(Node* n) {
    if (n->leaf) return n->value;
    double wl = (double)n->left->cover / n->cover;
    double wr = (double)n->right->cover / n->cover;
    return wl * expected_value(n->left)
         + wr * expected_value(n->right);
}

Path Contribution Walk

vector<double> path_dependent_contrib(Node* root,
                                      const vector<double>& x,
                                      int m) {
    vector<double> phi(m, 0.0);
    Node* cur = root;
    double before = expected_value(cur);

    while (!cur->leaf) {
        int f = cur->feature;
        Node* next = x[f] < cur->threshold ? cur->left : cur->right;
        double after = expected_value(next);
        phi[f] += after - before;
        before = after;
        cur = next;
    }
    return phi;
}

This compact version attributes each conditional-expectation jump to the feature that caused the branch. Full Tree SHAP is more careful: it averages over all orders in which path features could be revealed. The simplified walk is still useful for understanding why tree structure makes row-level explanations cheap.

Ensemble Additivity

vector<double> ensemble_contrib(const vector<Node*>& trees,
                                const vector<double>& x,
                                int m,
                                double learningRate) {
    vector<double> total(m, 0.0);
    for (Node* t : trees) {
        vector<double> one = path_dependent_contrib(t, x, m);
        for (int j = 0; j < m; ++j)
            total[j] += learningRate * one[j];
    }
    return total;
}
Where production Tree SHAP goes further. Libraries maintain path fractions and Shapley weights while recursing through the tree. That gives exact Shapley values under the chosen tree semantics, not just a path-difference approximation.