Firstly, the high-level show_weights
function is not the best way to report results and importances.
After you've run perm.fit(X,y)
, your perm
object has a number of attributes containing the full results, which are listed in the eli5
reference docs.
perm.feature_importances_
returns the array of mean feature importance for each feature, though unranked - it will be in the order that the features are given in the input data. These will match the data in your show_weights
output (the values to the left of the ± symbol).
I would also be more interested in the standard deviation of the permuted results rather than the full ± range given by show_weights
. This is accessed with perm.feature_importances_std_
You could do your own ranking by putting these into a dataframe, e.g.
df_fi = pd.DataFrame(dict(feature_names=X.columns.tolist(),
feat_imp=perm.feature_importances_,
std=perm.feature_importances_std_,
))
df_fi = df_fi.round(4)
df_fi.sort_values('feat_imp', ascending=False)
You can also access the full results with perm.results_
- this returns an array with the results from each cross-validation for each permutation. In your case, with 6 splits and 100 repeats, there will be 600 arrays, each length of X.columns.
Ok, onto the more important question - what do these results mean?
- Each result in that array of arrays is the change in score when a
feature is shuffled to random noise.
- What is the 'score'?
eli5.sklearn.PermutationImportance
takes a kwarg scoring
, where
you can give it any scorer object you like. Otherwise I believe it
uses the default scoring of the sklearn estimator object, which for
RandomForestRegressor is indeed R2.
- So, behind the scenes eli5 has calculated a baseline score with no
shuffling. Each shuffle (per feature per cv per permutation) a model is refit and scored. This score is used to calculate a delta, so each 'result' in the array is
baseline - score
.
Positive vs Negative feature importances?
Somewhat confusingly, positive results indicate that:
- the score got worse when the feature was removed (i.e. score decreased)
- therefore the feature has some importance to the accuracy of the model
A negative result means the accuracy actually improved relative to the baseline when the feature was removed. This could occur for various reasons. It is definitely a good idea to remove features with negative feature importances.
So it's a change in score relative to the baseline...
You can calculate your own baseline with
from sklearn.model_selection import cross_val_score
baseline_cv_results = cross_val_score(regressor, X, y, cv=6) # use the same number of cvs
baseline_score = baseline_cv_results.mean()
You could then, for example, scale the feature importance results in the example df_fi
above with
df_fi['percent_change'] = ((df_fi['feat_imp'] / baseline) * 100).round(2)
Though it's always important to be careful when scaling scores like this, it can lead to odd behaviour if the denominator is close to zero.
There's several points to consider when interpreting results:
- the features with the largest (positive) feature importance are definitely the most important features.
- features with negative importances are probably confusing your model and should be removed
- features close to zero contain little-to-no useful data
- however, depending on the nature of your data, it may be that the change in score for even the top-ranked feature is small relative to the baseline. This occurs due to interaction with other features.
- if you create a 'percent_change' column as suggested above, you'll find that the percentages probably won't sum to 100%, even if ignoring negative values. Again, this would be due to interactions, where the effect of removing one feature on its own may not be huge, but if more were removed / shuffled at the same time, the model performance could deteriorate non-linearly.
Lastly, I always love a good graph.
Showing the full results as a set of boxplots is a good way to visualise these data. You'll need plotly for this example:
import plotly.express as px
import plotly.graph_objects as go
# create df with columns = each feature, rows = score for each permutation each cv (600 in your case)
df_results = pd.DataFrame(data=perm.results_, columns=X.columns)
# feat_imps values will be same as perm.feature_importances_, but as a pd.Series with index labels corresponding to the feature names
feat_imps = df_results.mean().sort_values(ascending=False)
# reorder columns from most to least important
df_results = df_results[feat_imps.index]
# create boxplots of full results. pd.melt() is a handy way to reformat into a longform dataframe that plays well with plotly express.
fig = px.box(df_results.melt(), x='variable', y='value', orientation='v')
# add a marker showing the mean feature importance for each
fig.add_trace(go.Scatter(x=feat_imps.index, y=feat_imps.values, mode='markers', marker=dict(color='red'), name = 'Mean'))
fig
An example from some of my own data is below:
