Open tomasfryda opened 1 year ago
Good one, I fixed only the binomial version that time.
Related to this: https://github.com/h2oai/h2o-3/issues/6851
Do I understand right that the shapley for DRF are totally wrong, if one of the feature is not used?
On a walk with my dog, I thought about it a little bit more and I think I used way too much complicated approach - I got confused by every contribution being subtracted from a constant.
I will test it tomorrow more thoroughly but I think this is how it should be done:
for (int i = 0; i < contribs.length-1; i++) {
contribs[i] = -(contribs[i] / _output._ntrees);
}
contribs[contribs.length-1] = 1 - (contribs[contribs.length-1]/_output._ntrees);
Why?
Let me show an example for the SHAP with the background sample (but I think it's transferable to the original TreeSHAP):
$$P(Y=0|x) = 0.3$$
$$P(Y=0|b) = 0.45$$
$$ \sum\phi_i =P(Y=0|x) - P(Y=0|b) = -0.15$$
Here ^^^ we call the $P(Y=0|b)$ "bias term" and here it should be obvious that it corresponds to the prediction of the background sample.
If we rewrite it we can see that the sum "lifts" the prediction from the background sample prediction to the prediction for the point that we calculate the SHAP for.
$$P(Y=0|x) = \sum\phi_i - P(Y=0|b)$$
Now if we are interested in the contributions to P(Y=1|x) we can calculate those probabilities like:
$$P(Y=1|x) = 1-P(Y=0|x) = 0.7$$
$$P(Y=1|b) = 1- P(Y=0|b) = 0.55$$
Now the important part comes in:
$$P(Y=1|x) - P(Y=1|b) = 1- P(Y=0|x) - 1 + P(Y=0|b) = - (P(Y=0|x) - P(Y=0|b)) = - \sum\phi_i = 0.15$$
So the contributions for $P(Y=1|x)$ sum up to the negative value of contributions for $P(Y=0|x)$.
And the second important thing is that the bias term now should corresponds to $P(Y=1|b) = 1 - P(Y=0|b)$.
So I think the only place where we should subtract from a constant is the bias term.
Visually it corresponds roughly to the following figure:
To sum it up, I think all binomial DRF SHAP values are calculated wrong. And when we fix this there will still be https://github.com/h2oai/h2o-3/issues/7385 which I think is caused by wrongly calculated weights.
The problem is caused by transformation of the contributions which are computed for $P(Y=0)$ but we are actually interested in contributions to $P(Y=1)$.
The transformation is done by subtracting from a fraction of one as follows:
This is incorrect since it doesn't satisfy the dummy property - meaning if a variable is not used it should not contribute (contribution == 0) and since we are subtracting zero from a
featurePlusBiasRatio
we get a non-zero value. (See Steps to reproduce)A better solution is to use the solution that is present in the
ScoreContibutionsTaskDRF::addContribToNewChunk
.However, I'm afraid that might not be the correct solution either.
I'm not sure if it's applicable here but in my PR where I add SHAP with background sample I have to use a different approach:
Let denote $x$ as a data point that I want to know the contributions $\phi_i$ for and $b$ a background data point that I use to calculate the contributions for $x$.
In other words:
$$f(x) = \sum_{i\in\text{features}} \phi_i + f(b)$$
$$\sum_{i\in\text{features}} \phi_i = f(x) - f(b)$$
Since $f(x)$ corresponds to $P(Y=0)$ and we want $P(Y=1)$ we need to transform it somehow:
$$P(Y=1|x) = g(x) = 1-P(y=0|x) = 1 - f(x)$$
Let's denote $\tilde\phi_i$ as contributions for the $P(Y=1)$, then
$$\sum_{i\in\text{features}} \tilde\phi_i = g(x) - g(b) = 1-f(x) - 1 +f(b) = f(b)-f(x)$$
So we can just swap the data point and the background data point to get the contributions for the P(Y=1). This has the nice property that if contribution(a|x,b) shifts the response from f(b) to f(x), then contribution(a|b,x) moves the response by the same magnitude but in the opposite direction (from f(x) to f(b)) which does not happen when using the simple transformation with
1/n - contrib
.Then i have to do little magic to get the correct bias term and for cases when
output_format="original"
which I implement using one hot encoding I have to move the contribution from the categorical level from $b$ to the categorical level from $x$ to make shap less confusion (and to satisfy the dummy property if we assume we can censor some categorical levels).The same explanation but from the code in case I forgot to write here something:
Steps to reproduce Steps to reproduce the behavior (with working code on a sample dataset, if possible):