Briefly reiterating first what others have mentioned, one could use a surrogate model like a BDT that is intentionally overtrained on the output of the isolation forest (instead of using the binary output as Khurram Majeed suggested, you could directly train it on the anomaly score). However, we cannot be sure that the surrogate model is learning the same decision paths as the iForest.
On the other hand, the principle behind SHAP is clearer than any surrogate model used to explain the iForest (another good explanation of SHAP). Perhaps because the BDT is doing a poorer job, I found that the feature importances derived from a surrogate BDT and SHAP agreed only roughly, and sometimes disagreed wildly. Both local and global feature importances were considered in this comparison.
Despite all this the iForest is interpretable if its underlying principle is taken advantage of: that outliers have short decision paths on average over all the trees in the iForest. The features that were cut on in these short paths must be more important than those cut on in long decision paths. So here's what you can do to get feature importances:
- Determine a threshold for decision path length. The authors of the iForest algorithm recommend from empirical studies a subsampling size of 256 [ref]. This is the number of events (sampled from all the data) that is fed into each tree. If you're using this subsampling size, the trees in the iForest can only grow up to $\log_2 (256) = 8$ nodes in depth. Thus you might choose a path length threshold of 3 or 4.
- For each event, loop through the trees in the iForest and select paths that are shorter than the threshold path length. In these paths, count which features are being cut at each node, the depth of each node, and the numbers of events split at that node. If you're using sklearn's implementation of the iForest, this script may help you in digging through their tree structure. This plot shows what you should have at this stage.
- Now that you have feature counts of all the trees at each cut depth, you can condense these into a final feature ranking. This can be done by assigning weights to each node and adding the counts up. For instance you may want to assign larger weights to feature counts that are cut higher up in the tree (and are thus more responsible in creating a overall shorter decision path), and smaller weights to features cut further down. You can also incorporate the ratio of events split at each node into your weights - if there is a large disparity of events split by a certain cut, then that cut is probably important.
Note that there is a lot of freedom to assign weights here, and it may be that your choices are ad-hoc and dataset dependent. I'm not entirely sure, but this may be the reason that feature importances were not implemented in sklearn's iForest.
Anyway, here's a condensation of the feature counts shown above. I'm using a simple 'geometric weight' of 0.5 - essential the number of counts cut first get no modification, the number of counts counts cut second get halved, and those cut third get quartered.
As you can see, there are a lot of features that aren't very important to isolate this particular event. Here is the SHAP force plot for this event, nice agreement even with a very simple weight assignment!
I've shown the feature rankings for an individual event, but you can easily get global importances by averaging the feature counts over entire dataset. This way you can drop features that don't contribute much to the iForest classification.
If you're using sklearn or other Python based implementations, the biggest disadvantage to this technique is speed. It takes a while to root through all the trees, and if you're interested in global importances you'll have to loop though all the events as well.