MAIF / shapash

🔅 Shapash: User-friendly Explainability and Interpretability to Develop Reliable and Transparent Machine Learning Models
https://maif.github.io/shapash/
Apache License 2.0
2.74k stars 335 forks source link

Reference for the metrics #420

Open vidushi27 opened 1 year ago

vidushi27 commented 1 year ago

Dear contributors,

Please direct me toward the research paper/reference for the metrics "Compacity" used here. I would like to understand it's background in detail. I've gone through the medium article mentioned.

Thank you so much.

Best regards, Vidushi

Francesco-Marini commented 1 year ago

Hello @vidushi27,

There is no research paper associated to this metric as it has been created ad hoc. However, I can give you some additional explanation of how it works. We also wrote a notebook tutorial here.

The idea behind the compacity metric is to determine how well a small set of features can approximate the model (if you rely on too many features for the explanation, it will be hard to get a sense of how the model actually works, so we usually prefer to deal with fewer). Thus, two experiences are proposed in the compacity metric to help quantify how model predictions can be explained by smaller sets of features.

Input: You select an explainability method of your choice and you calculate the contributions of each feature on all data points. You end up having a matrix of contributions of the same size as the original data.

Some metrics are calculated (you can find the related Python code here) and the following graphs are built:

Unknown

Left graph: For a given model approximation level, the graph is used to determine the minimum number of required features to reach that approximation (i.e. approximation level is fixed, number of features varies)

Implementation is detailed below :

def get_min_nb_features(selection, contributions, mode, distance):
    assert 0 <= distance <= 1

    if mode == "classification" and len(contributions) == 2:
        contributions = contributions[1]
    contributions = contributions.loc[selection].values
    features_needed = []
    # For each instance, add features one by one (ordered by SHAP) until we get close enough
    for i in range(contributions.shape[0]):
        ids = np.flip(np.argsort(np.abs(contributions[i, :])))
        output_value = np.sum(contributions[i, :])

        score = 0
        for j, idx in enumerate(ids):
            # j : number of features needed
            # idx : positions of the j top shap values
            score += contributions[i, idx]
            # CLOSE_ENOUGH
            if mode == "regression":
                if abs(score - output_value) < distance * abs(output_value):
                    break
            elif mode == "classification":
                if abs(score - output_value) < distance:
                    break
        features_needed.append(j + 1)
    return features_needed

Right graph: It is the opposite. This time we keep the number of selected features fixed, and we want to determine how close we get to the model. The iteration process is very similar :

Implementation is detailed below:

def get_distance(selection, contributions, mode, nb_features):
    if mode == "classification" and len(contributions) == 2:
        contributions = contributions[1]
    assert nb_features <= contributions.shape[1]

    contributions = contributions.loc[selection].values
    top_features = np.array([sorted(row, key=abs, reverse=True) for row in contributions])[:, :nb_features]
    output_top_features = np.sum(top_features[:, :], axis=1)
    output_all_features = np.sum(contributions[:, :], axis=1)

    if mode == "regression":
        distance = abs(output_top_features - output_all_features) / abs(output_all_features)
    elif mode == "classification":
        distance = abs(output_top_features - output_all_features)
    return 

Hope this helps