Search code examples
lightgbm

LightGBM predict with pred_contrib=True for multiclass: order of SHAP values in the returned array


LightGBM predict method with pred_contrib=True returns an array of shape =(n_samples, (n_features + 1) * n_classes).

What is the order of data in the second dimension of this array?

In other words, there are two questions:

  1. What is the correct way to reshape this array to use it: shape = (n_samples, n_features + 1, n_classes) or shape = (n_samples, n_classes, n_features + 1)?
  2. In the feature dimension, there are n_features entries, one for each feature, and a (useless) entry for the contribution not related to any feature. What is the order of these entries: feature contributions in the entries 1,..., n_features in the same order they appear in the dataset, with the remaining (useless) entry at index 0, or some other way?

Solution

  • The answers are as follows:

    1. The correct shape is (n_samples, n_classes, n_features + 1).
    2. The feature contributions are in the entries 1,..., n_features in the same order they appear in the dataset, with the remaining (useless) entry at index 0.

    The following code shows it convincingly:

    import lightgbm, pandas, numpy
    params = {'objective': 'multiclass', 'num_classes': 4, 'num_iterations': 10000,
              'metric': 'multiclass', 'early_stopping_rounds': 10}
    train_df = pandas.DataFrame({'f0': [0, 1, 2, 3] * 50, 'f1': [0, 0, 1] * 66 + [1, 2]}, dtype=float)
    val_df = train_df.copy()
    train_target = pandas.Series([0, 1, 2, 3] * 50)
    val_target = pandas.Series([0, 1, 2, 3] * 50)
    train_set = lightgbm.Dataset(train_df, train_target)
    val_set = lightgbm.Dataset(val_df, val_target)
    model = lightgbm.train(params=params, train_set=train_set, valid_sets=[val_set, train_set])
    feature_contribs = model.predict(val_df, pred_contrib=True)
    print('Shape of SHAP:', feature_contribs.shape)
    # Shape of SHAP: (200, 12)
    print('Averages over samples:', numpy.mean(feature_contribs, axis=0))
    # Averages over samples: [ 3.99942301e-13 -4.02281771e-13 -4.30029167e+00 -1.90606677e-05
    #  1.90606677e-05 -4.04157656e+00  2.24205077e-05 -2.24205077e-05
    #  -4.04265615e+00 -3.70370401e-15  5.20335728e-18 -4.30029167e+00]
    feature_contribs.shape = (200, 4, 3)
    print('Mean feature contribs:', numpy.mean(feature_contribs, axis=(0, 1)))
    # Mean feature contribs: [ 8.39960111e-07 -8.39960113e-07 -4.17120401e+00]
    

    (Each output appears as a comment in the following line.)

    The explanation is as follows.

    I have created a dataset with two features and with labels identical to the second of these features.

    I would expect significant contribution from the second feature only.

    After averaging the SHAP output over the samples, we get an array of the shape (12,) with nonzero values at the positions 2, 5, 8, 11 (zero-based).

    This shows that the correct shape of this array is (4, 3).

    After reshaping this way and averaging over the samples and the classes, we get an array of the shape (3,) with the nonzero entry at the end.

    This shows that the last entry of this array corresponds to the last feature. This means that the entry at the position 0 does not correspond to any feature and the following entries correspond to features.