Кратко: изучено создание кодов причин для бинарной модели повышения градиента с использованием пакета Python SHAP. Использовали набор данных по раку молочной железы, рассчитали значения SHAP и построили сводную диаграмму для всего набора данных.

В машинном обучении часто сложно интерпретировать процесс принятия решений в сложных моделях. Вот тут-то и появляются коды причин. Коды причин дают представление о факторах, влияющих на процесс принятия решений в модели, что делает их мощным инструментом для интерпретации и понимания моделей машинного обучения. Одним из популярных методов создания кодов причин является использование значений SHAP (аддитивных пояснений Шэпли), которые измеряют влияние каждой функции на прогноз модели.

В этой статье мы рассмотрим, как генерировать коды причин для двоичной модели повышения градиента с использованием пакета Python SHAP. Мы будем использовать набор данных о раке молочной железы, который содержит функции, связанные с диагностикой рака молочной железы, для обучения и оценки нашей модели.

  1. Предварительные требования: Прежде чем продолжить, убедитесь, что в вашей системе установлен Python. Кроме того, вам потребуется установить следующие пакеты Python: scikit-learn, shap и numpy. Вы можете установить эти пакеты, используя pip/ или conda:
pip install shap
conda install -c conda-forge shap

2. Импортируйте необходимые библиотеки. Сначала мы импортируем необходимые библиотеки для нашего анализа:

import numpy as np
import shap
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer

3. Загрузите и предварительно обработайте данные. Затем мы загрузим набор данных о раке молочной железы и разделим его на наборы для обучения и тестирования:

# Load the dataset (in this example, we are using the breast cancer dataset)
data = load_breast_cancer()
X = data.data
y = data.target

# Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

4. Подходящая модель: мы будем использовать классификатор повышения градиента для обучения нашей модели:

# Create and fit the Gradient Boosting Classifier
gbm = GradientBoostingClassifier(n_estimators=100, random_state=42)
gbm.fit(X_train, y_train)

5. Сгенерируйте код причины для каждой записи: мы будем использовать SHAP для расчета влияния каждой функции на прогнозы нашей модели:

# Create a TreeExplainer object for the Gradient Boosting Model
explainer = shap.Explainer(gbm)

# Calculate the SHAP values for the test dataset
shap_values = explainer(X_test)

Функция generate_reason_codes принимает три входных данных: индекс записи, для которой создаются коды причин, значения SHAP, рассчитанные для всего набора данных, и имена функций. Затем он сортирует значения SHAP для указанной записи в порядке убывания и извлекает соответствующие имена и значения функций для создания словаря кодов причин.

# Create a function to generate reason codes
def generate_reason_codes(instance, shap_values, feature_names):
    instance_shap_values = shap_values.values[instance]
    sorted_indices = np.argsort(np.abs(instance_shap_values))[::-1]
    sorted_values = instance_shap_values[sorted_indices]
    sorted_feature_names = [feature_names[i] for i in sorted_indices]

    reason_codes = {
        "reasons": [
            {"feature": feature, "value": value, "impact": shap_value}
            for feature, value, shap_value in zip(sorted_feature_names, X_test[instance][sorted_indices], sorted_values)
        ]
    }

    return reason_codes

# Choose an instance from the test dataset for which you want to generate reason codes
instance = 0

# Generate the reason codes for the chosen instance
reason_codes = generate_reason_codes(instance, shap_values, data.feature_names)

# Display the reason codes
print(reason_codes)

Например, мы можем сгенерировать коды причин для первой записи в нашем тестовом наборе следующим образом:

reason_codes = generate_reason_codes(0, shap_values, data.feature_names)
print(reason_codes)
#output
{'reasons': [{'feature': 'mean concave points', 'value': 0.03821, 'impact': 1.141686222814163}, {'feature': 'worst concave points', 'value': 0.1015, 'impact': 1.1184778345560713}, {'feature': 'worst perimeter', 'value': 96.05, 'impact': 0.8204550414359093}, {'feature': 'worst radius', 'value': 14.97, 'impact': 0.5790399081464063}, {'feature': 'worst area', 'value': 677.9, 'impact': 0.5694348669874683}, {'feature': 'worst texture', 'value': 24.64, 'impact': 0.5303815545796332}, {'feature': 'worst concavity', 'value': 0.2671, 'impact': -0.37583321906063066}, {'feature': 'area error', 'value': 30.29, 'impact': 0.1693429393875545}, {'feature': 'mean texture', 'value': 18.6, 'impact': 0.16523431635099597}, {'feature': 'concave points error', 'value': 0.01037, 'impact': 0.14739714810086949}, {'feature': 'compactness error', 'value': 0.01911, 'impact': 0.12625250824628753}, {'feature': 'mean area', 'value': 481.9, 'impact': 0.11046554929205113}, {'feature': 'worst smoothness', 'value': 0.1426, 'impact': -0.0998261853314333}, {'feature': 'radius error', 'value': 0.3961, 'impact': 0.07462456955534748}, {'feature': 'worst symmetry', 'value': 0.3014, 'impact': 0.045209247408322197}, {'feature': 'mean perimeter', 'value': 81.09, 'impact': -0.03405795132824116}, {'feature': 'mean compactness', 'value': 0.1058, 'impact': 0.032791401757891886}, {'feature': 'mean concavity', 'value': 0.08005, 'impact': 0.018596441817801013}, {'feature': 'smoothness error', 'value': 0.006953, 'impact': 0.015290154162438207}, {'feature': 'mean radius', 'value': 12.47, 'impact': -0.012456545606318975}, {'feature': 'concavity error', 'value': 0.02701, 'impact': -0.011218565263938064}, {'feature': 'mean fractal dimension', 'value': 0.06373, 'impact': 0.007748485048966116}, {'feature': 'worst compactness', 'value': 0.2378, 'impact': 0.00738117485163141}, {'feature': 'fractal dimension error', 'value': 0.003586, 'impact': -0.0025826647471062607}, {'feature': 'perimeter error', 'value': 2.497, 'impact': 0.002458367139715965}, {'feature': 'texture error', 'value': 1.044, 'impact': 0.0012489303650987113}, {'feature': 'mean symmetry', 'value': 0.1925, 'impact': 0.0007953040626127801}, {'feature': 'symmetry error', 'value': 0.01782, 'impact': -0.0004817781327405499}, {'feature': 'worst fractal dimension', 'value': 0.0875, 'impact': -0.00010543410320955719}, {'feature': 'mean smoothness', 'value': 0.09965, 'impact': -3.488178749232912e-05}]}

6. Создайте сводку SHAP для всего набора данных: постройте сводку SHAP для всего набора данных, вы можете использовать функцию shap.summary_plot() после расчета значений SHAP для всего набора данных. Вот исправленный код:

# Create a SHAP explainer object for the Gradient Boosting Model
explainer = shap.Explainer(gbm)

# Calculate the SHAP values for the entire dataset (combining both the training and testing sets)
shap_values = explainer(np.vstack((X_train, X_test)))

# Plot the SHAP summary for the entire dataset
shap.summary_plot(shap_values, np.vstack((X_train, X_test)), feature_names=data.feature_names)

# Display the plot
plt.show()

8. Чтобы сгенерировать коды причин для модели повышения градиента (GBM), запускаемой с обработанными переменными Weight of Evidence (WOE) с использованием OptBinning, вам необходимо сначала предварительно обработать переменные с помощью OptBinning, подогнать GBM к обработанным переменным WOE, а затем генерировать коды причин с помощью пакета SHAP. Вот код:

import numpy as np
import pandas as pd
import shap
from optbinning import OptimalBinning
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer

# Load the dataset (in this example, we are using the breast cancer dataset)
data = load_breast_cancer()
X = data.data
y = data.target

# Convert the dataset to a DataFrame
df = pd.DataFrame(X, columns=data.feature_names)
df['target'] = y

# Process the dataset with OptBinning
woe_df = pd.DataFrame()
optb_objects = {}
for feature in data.feature_names:
    optb = OptimalBinning(name=feature, dtype="numerical", solver="cp", monotonic_trend="auto", min_n_bins=2, max_n_bins=5)
    optb.fit(df[feature], df['target'])
    woe_df[feature] = optb.transform(df[feature], metric="woe")
    optb_objects[feature] = optb

# Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(woe_df, y, test_size=0.2, random_state=42)

# Create and fit the Gradient Boosting Classifier
gbm = GradientBoostingClassifier(n_estimators=100, random_state=42)
gbm.fit(X_train, y_train)

# Create a SHAP explainer object for the Gradient Boosting Model
explainer = shap.Explainer(gbm)

# Calculate the SHAP values for the test dataset
shap_values = explainer(X_test)

# Helper function to find the corresponding WOE bucket for a given original value
def get_woe_bucket(feature, original_value, optb_object):
    # print(feature,original_value)
    import re
    binning_table = optb_object.binning_table.build()
    return_value = None
    for index, row in binning_table.iterrows():
        row["Bin"] = row["Bin"].replace('inf','1000000000.0')
        if (row["Bin"].startswith("Special") or row["Bin"].startswith("Missing")):
            continue
        interval_match = re.match(r'[\[\(]([^,]+), ([^\]\)]+)[\]\)]', row["Bin"])
        if interval_match:
            left, right = float(interval_match.group(1)), float(interval_match.group(2))
            if left <= original_value < right:
                return_value = str(row["Bin"])
                break
    return return_value
    # return None

# Create a function to generate reason codes
def generate_reason_codes(instance, shap_values, feature_names, original_df, woe_df, optb_objects):
   
    instance_shap_values = shap_values.values[instance]
    sorted_indices = np.argsort(np.abs(instance_shap_values))[::-1]
    sorted_values = instance_shap_values[sorted_indices]
    sorted_feature_names = [feature_names[i] for i in sorted_indices]
    # print(sorted_feature_names)

    woe_buckets = [
        optb_objects[feature].binning_table.build().loc[:, ["Bin", "Count", "Count (%)", "WoE"]]
        for feature in sorted_feature_names
    ]

    original_values_sorted = original_df.iloc[instance][sorted_indices]
    # print(original_values_sorted)
    corresponding_woe_buckets = [
        get_woe_bucket(feature, original_value , optb_objects[feature])
        for feature,original_value in zip(sorted_feature_names, original_values_sorted)
    ]
    # print(corresponding_woe_buckets)

    reason_codes = {
        "reasons": [
            {
                "feature": feature,
                "original_value": original_value,
                "woe_value": woe_value,
                # "woe_bucket": woe_bucket,
                "corresponding_woe_bucket": corresponding_woe_buckets,
                "impact": shap_value
            }
            for feature, original_value, woe_value, corresponding_woe_buckets, shap_value in zip(
                sorted_feature_names,
                original_df.iloc[instance][sorted_indices],
                woe_df.iloc[instance][sorted_indices],
                # woe_buckets,
                corresponding_woe_buckets,
                sorted_values
            )
        ]
    }

    return reason_codes


# Process the dataset with OptBinning

# Choose an instance from the test dataset for which you want to generate reason codes
instance = 0

# Generate the reason codes for the chosen instance
reason_codes = generate_reason_codes(instance, shap_values, data.feature_names, df.iloc[X_test.index], woe_df.iloc[X_test.index], optb_objects)

# Display the reason codes
print(reason_codes)
{'reasons': [{'feature': 'mean concave points', 'original_value': 0.03821, 'woe_value': -1.1048177072776844, 'corresponding_woe_bucket': '[0.03, 0.05)', 'impact': 1.0140255442433102}, {'feature': 'worst perimeter', 'original_value': 96.05, 'woe_value': -2.491112068397574, 'corresponding_woe_bucket': '[87.37, 101.65)', 'impact': 1.002508592414365}, {'feature': 'worst concave points', 'original_value': 0.1015, 'woe_value': -1.906598728840426, 'corresponding_woe_bucket': '[0.09, 0.11)', 'impact': 0.7714232490527647}, {'feature': 'worst concavity', 'original_value': 0.2671, 'woe_value': 0.8030006592486145, 'corresponding_woe_bucket': '[0.26, 0.38)', 'impact': -0.7018065132671553}, {'feature': 'worst smoothness', 'original_value': 0.1426, 'woe_value': 0.5040550737483265, 'corresponding_woe_bucket': '[0.14, 0.15)', 'impact': -0.5958850659565252}, {'feature': 'worst area', 'original_value': 677.9, 'woe_value': -2.656904323240317, 'corresponding_woe_bucket': '[553.30, 691.75)', 'impact': 0.5273139784644225}, {'feature': 'worst radius', 'original_value': 14.97, 'woe_value': -2.656904323240317, 'corresponding_woe_bucket': '[13.22, 14.98)', 'impact': 0.3155327743268687}, {'feature': 'area error', 'original_value': 30.29, 'woe_value': -0.8759557701164795, 'corresponding_woe_bucket': '[22.12, 31.28)', 'impact': 0.2939706727482147}, {'feature': 'mean texture', 'original_value': 18.6, 'woe_value': 0.1884437532818903, 'corresponding_woe_bucket': '[18.46, 20.20)', 'impact': 0.2512034956533681}, {'feature': 'mean area', 'original_value': 481.9, 'woe_value': -2.180211705843788, 'corresponding_woe_bucket': '[390.60, 529.80)', 'impact': 0.23886420963043303}, {'feature': 'radius error', 'original_value': 0.3961, 'woe_value': 0.6998412958510026, 'corresponding_woe_bucket': '[0.38, 0.55)', 'impact': -0.1300811860157289}, {'feature': 'compactness error', 'original_value': 0.01911, 'woe_value': 0.07486240447920735, 'corresponding_woe_bucket': '[0.01, 0.02)', 'impact': 0.11771221918785298}, {'feature': 'worst texture', 'original_value': 24.64, 'woe_value': 0.2364129448856026, 'corresponding_woe_bucket': '[23.35, 29.30)', 'impact': 0.08330434898707008}, {'feature': 'mean radius', 'original_value': 12.47, 'woe_value': -1.6438142080103721, 'corresponding_woe_bucket': '[12.33, 13.09)', 'impact': -0.048831812029431945}, {'feature': 'worst fractal dimension', 'original_value': 0.0875, 'woe_value': 0.10129566154736286, 'corresponding_woe_bucket': '[0.08, 0.09)', 'impact': 0.02906002843415805}, {'feature': 'perimeter error', 'original_value': 2.497, 'woe_value': -0.6626205899007903, 'corresponding_woe_bucket': '[1.75, 2.76)', 'impact': 0.025539535517094845}, {'feature': 'concave points error', 'original_value': 0.01037, 'woe_value': -0.1600214824056033, 'corresponding_woe_bucket': '[0.01, 1000000000.0)', 'impact': 0.022289989505712746}, {'feature': 'mean smoothness', 'original_value': 0.09965, 'woe_value': 0.12615569886675773, 'corresponding_woe_bucket': '[0.09, 0.10)', 'impact': 0.02120193826867159}, {'feature': 'texture error', 'original_value': 1.044, 'woe_value': 0.45215663562067504, 'corresponding_woe_bucket': '[0.82, 1.08)', 'impact': -0.017274668655885797}, {'feature': 'fractal dimension error', 'original_value': 0.003586, 'woe_value': -0.04053453389185499, 'corresponding_woe_bucket': '[0.00, 0.01)', 'impact': -0.008231218777864029}, {'feature': 'concavity error', 'original_value': 0.02701, 'woe_value': 0.4746294914727335, 'corresponding_woe_bucket': '[0.02, 0.03)', 'impact': 0.006857779968393732}, {'feature': 'worst compactness', 'original_value': 0.2378, 'woe_value': 0.32907177536830745, 'corresponding_woe_bucket': '[0.20, 0.37)', 'impact': 0.006126590181754702}, {'feature': 'mean concavity', 'original_value': 0.08005, 'woe_value': 0.64129381894969, 'corresponding_woe_bucket': '[0.07, 0.12)', 'impact': -0.004604469863141343}, {'feature': 'symmetry error', 'original_value': 0.01782, 'woe_value': -0.09321679559920444, 'corresponding_woe_bucket': '(-1000000000.0, 0.02)', 'impact': -0.004145070495276751}, {'feature': 'worst symmetry', 'original_value': 0.3014, 'woe_value': 0.24630458141652653, 'corresponding_woe_bucket': '[0.28, 0.36)', 'impact': 0.0036792364983070187}, {'feature': 'mean symmetry', 'original_value': 0.1925, 'woe_value': 0.2109945788037871, 'corresponding_woe_bucket': '[0.17, 0.21)', 'impact': -0.002702521917813353}, {'feature': 'mean compactness', 'original_value': 0.1058, 'woe_value': 0.8449365842015237, 'corresponding_woe_bucket': '[0.10, 0.16)', 'impact': -0.0021190473675165513}, {'feature': 'mean perimeter', 'original_value': 81.09, 'woe_value': -2.203429995945793, 'corresponding_woe_bucket': '[75.01, 85.25)', 'impact': 0.0015084559097972466}, {'feature': 'smoothness error', 'original_value': 0.006953, 'woe_value': -0.03665632558255679, 'corresponding_woe_bucket': '[0.00, 0.01)', 'impact': 0.00030350262870593907}, {'feature': 'mean fractal dimension', 'original_value': 0.06373, 'woe_value': -0.2844756568790088, 'corresponding_woe_bucket': '[0.06, 0.07)', 'impact': 0.0}]}

В этом коде я использовал OptBinning для предварительной обработки переменных с кодировкой Weight of Evidence (WOE). Затем набор данных разделяется на наборы для обучения и тестирования, а классификатор повышения градиента применяется к обработанным переменным WOE. Наконец, пакет SHAP используется для генерации кодов причин для прогнозов модели.