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

Exact Tree SHAP for a Small Tree in C++

The easiest way to implement Tree SHAP correctly is to start with the definition. For a tiny tree with only a few features, enumerate every coalition, evaluate the tree when only that coalition is known, and average each feature's marginal contribution.

Tree Representation

#include <bits/stdc++.h>
using namespace std;

struct Node {
    bool leaf = false;
    int feature = -1;
    double threshold = 0.0;
    double value = 0.0;
    int cover = 0;
    Node *left = nullptr, *right = nullptr;
};

Evaluate with Missing Features

If a split feature is known, follow the row's branch. If it is missing, average both branches by training cover. This gives the coalition value for the row.

double eval_coalition(Node* n,
                      const vector<double>& x,
                      const vector<int>& known) {
    if (n->leaf) return n->value;
    if (known[n->feature]) {
        return x[n->feature] < n->threshold
             ? eval_coalition(n->left, x, known)
             : eval_coalition(n->right, x, known);
    }
    double wl = (double)n->left->cover / n->cover;
    double wr = (double)n->right->cover / n->cover;
    return wl * eval_coalition(n->left, x, known)
         + wr * eval_coalition(n->right, x, known);
}

Exact Shapley Loop

vector<double> exact_tree_shap(Node* root,
                               const vector<double>& x,
                               int m) {
    vector<double> phi(m, 0.0);
    int full = 1 << m;

    for (int j = 0; j < m; ++j) {
        for (int mask = 0; mask < full; ++mask) {
            if (mask & (1 << j)) continue;
            int s = __builtin_popcount((unsigned)mask);
            double weight = 1.0 / m / (double)tgamma(m) *
                            tgamma(s + 1) * tgamma(m - s);

            vector<int> known0(m, 0), known1(m, 0);
            for (int k = 0; k < m; ++k) known0[k] = (mask >> k) & 1;
            known1 = known0;
            known1[j] = 1;

            double without = eval_coalition(root, x, known0);
            double with = eval_coalition(root, x, known1);
            phi[j] += weight * (with - without);
        }
    }
    return phi;
}
baseline = 0.400 phi[0] = -0.080 phi[1] = -0.010 prediction = 0.310 baseline + sum(phi) = 0.310
This is exact but exponential. The loop is useful for understanding and testing. Production Tree SHAP uses dynamic programming along paths rather than enumerating all masks.