Отладка SHAP значений: практические проблемы объяснимости моделей в Python | AiManual
AiManual Logo Ai / Manual.
15 Янв 2026 Гайд

Где ломаются Shapley Values: практическое руководство по отладке объяснимости моделей на Python

Разбираем реальные случаи, когда Shapley Values дают неверные результаты. Готовый код на Python для диагностики и исправления ошибок объяснимости.

Красивые графики, которые врут

Открываешь тетрадку с Jupyter, запускаешь shap.summary_plot() и получаешь красивые цветные полоски. Кажется, теперь ты понимаешь свою модель. Знаешь, какие фичи важны. Можешь объяснить бизнесу, почему клиент получил отказ.

А потом заказчик спрашивает: "Почему у этого человека высокий доход, но модель предсказывает дефолт?" Ты смотришь на Shapley values и видишь, что фича 'возраст' имеет отрицательный вклад. Но возраст у клиента 45 лет - вроде нормально. Что-то не так.

Или ещё хуже: запускаешь shap.plots.waterfall() для двух почти одинаковых наблюдений, а значения признаков скачут на 300%. Без видимой причины.

Shapley Values - не волшебная палочка. Это математический инструмент с кучей подводных камней. И если ты не знаешь, где эти камни лежат, разобьёшься об них при первом же серьёзном запросе от регулятора.

Три типа ошибок, которые не покажет документация SHAP

Библиотека shap работает по принципу "black box" - дал модель, получи объяснения. Но что происходит внутри этого black box? Ты доверяешь результатам, потому что они математически обоснованы. И это первая ошибка.

1. Проблема с baseline: откуда берутся эти странные значения?

Открой любой пример из документации shap:

import shap
import xgboost
import numpy as np

# Обучаем модель
X, y = shap.datasets.adult()
model = xgboost.XGBClassifier().fit(X, y)

# Считаем SHAP значения
explainer = shap.Explainer(model)
shap_values = explainer(X)

Всё просто, правда? А теперь посмотри на это:

print(f"Baseline (ожидаемое значение модели): {explainer.expected_value}")
print(f"Это что вообще такое? {explainer.expected_value}")

Ты получишь какое-то число. Например, -1.37. Что это значит? Это значение модели на "нулевом" входе. Но что такое "нулевой" вход для XGBoost? Это не нули в массиве. Это... зависит от реализации.

💡
В TreeExplainer (который используется для XGBoost, LightGBM, CatBoost) baseline вычисляется как среднее предсказание по всему тренировочному датасету. Звучит логично, пока не поймёшь, что это делает с интерпретацией.

Представь: у тебя есть модель кредитного скоринга. Baseline = 0.3. Это значит, что "средний" клиент имеет 30% вероятность дефолта. Все Shapley values показывают отклонение от этой цифры.

А если твой тренировочный датасет несбалансирован? 90% хороших клиентов, 10% плохих. Baseline будет около 0.1. И все объяснения будут относительно этого значения. Клиент с реальной вероятностью дефолта 50% получит огромные положительные Shapley values для всех рискованных признаков.

Но заказчик спрашивает: "Почему у этого клиента такой высокий риск?" Ты показываешь график, где фича 'просрочки_90_дней' добавляет +0.2 к вероятности. А заказчик: "Но у него же нет просрочек 90 дней!"

Вот она, первая ловушка. Shapley values показывают вклад относительно среднего по тренировочным данным. Не относительно нуля. Не относительно какого-то "нейтрального" клиента. Относительно среднего.

2. Корреляции убивают объяснимость

Ещё одна классика. Берёшь датасет, где 'возраст' и 'стаж_работы' коррелируют на 0.8. Обучаешь модель. Смотришь на Shapley values.

Фича 'возраст' важна. 'Стаж_работы' тоже важна. Всё логично?

А теперь сделай так:

# Создаём синтетические данные с сильной корреляцией
np.random.seed(42)
n_samples = 1000
age = np.random.normal(40, 10, n_samples)
experience = age * 0.7 + np.random.normal(0, 3, n_samples)  # Сильная корреляция
salary = 1000 * age + 500 * experience + np.random.normal(0, 5000, n_samples)

X = pd.DataFrame({'age': age, 'experience': experience})
y = salary

model = xgboost.XGBRegressor().fit(X, y)
explainer = shap.Explainer(model)
shap_values = explainer(X)

# Смотрим важность признаков
shap.plots.bar(shap_values)

Увидишь, что обе фичи получат примерно равную важность. Но подожди. Целевая переменная линейно зависит от обеих фичей. Модель XGBoost может выучить эту линейную зависимость. Но Shapley values распределят "заслуги" между коррелированными признаками.

Почему это проблема? Потому что в реальности 'стаж_работы' может быть производным от 'возраста'. Или наоборот. Shapley values не могут это определить. Они работают в предположении, что признаки независимы.

Коррелированные признаки - бич Shapley values. Модель может использовать только один из них, но SHAP покажет важность обоих. Или распределит вклад как попало.

3. Категориальные переменные и их кодирование

One-hot encoding. Все его используют. Все его ненавидят. И Shapley values его тоже ненавидят.

Возьмём фичу 'город'. 100 уникальных значений. После one-hot encoding получаем 100 бинарных признаков. Запускаем SHAP.

Что увидим? Каждый бинарный признак получит крошечный Shapley value. Потому что вклад одного хот-энкодированного признака в модель минимален. Но вместе они могут определять решение!

SHAP этого не покажет. На графике важности признаков эти 100 бинарных колонок займут последние места. И ты решишь, что 'город' не важен для модели. Хотя на самом деле модель сильно зависит от географического региона.

Практическая отладка: пошаговый план

1Проверь baseline

Первое, что делаешь после вычисления SHAP значений:

print(f"Expected value: {explainer.expected_value}")
print(f"Average prediction: {model.predict_proba(X_train)[:, 1].mean()}")

# Они должны быть примерно равны
# Если нет - что-то не так с explainer

Потом смотри на распределение предсказаний модели:

import matplotlib.pyplot as plt

preds = model.predict_proba(X_train)[:, 1]
plt.hist(preds, bins=50)
plt.axvline(explainer.expected_value, color='red', linestyle='--', label='SHAP baseline')
plt.legend()
plt.show()

Baseline должен быть где-то в центре распределения. Если он с краю - проверь, не переобучилась ли модель. Или не сбалансирован ли датасет.

2Найди коррелированные признаки

Не доверяй глазам. Посчитай:

import seaborn as sns

corr_matrix = X_train.corr().abs()
high_corr_pairs = []

for i in range(len(corr_matrix.columns)):
    for j in range(i+1, len(corr_matrix.columns)):
        if corr_matrix.iloc[i, j] > 0.7:  # Порог 0.7
            high_corr_pairs.append((
                corr_matrix.columns[i], 
                corr_matrix.columns[j],
                corr_matrix.iloc[i, j]
            ))

print(f"Найдено {len(high_corr_pairs)} пар с корреляцией > 0.7")
for feat1, feat2, corr in high_corr_pairs[:10]:  # Покажем первые 10
    print(f"{feat1} - {feat2}: {corr:.3f}")

Для каждой такой пары сделай простой тест. Обучи две модели:

  1. Со всеми признаками
  2. Без одного из коррелированных признаков

Сравни важность оставшегося признака в обеих моделях. Если важность сильно меняется - Shapley values для этих признаков ненадёжны.

3Проверь согласованность

Shapley values должны быть согласованы. Если фича X увеличивает предсказание на 0.1 для наблюдения A, и у наблюдения B значение фичи X больше, то вклад фичи X для наблюдения B должен быть не меньше 0.1.

Проверяем:

def check_monotonicity(shap_values, feature_idx, X_data):
    """Проверяем монотонность вклада фичи"""
    # Берем значения фичи и соответствующие SHAP значения
    feature_vals = X_data.iloc[:, feature_idx]
    shap_vals = shap_values.values[:, feature_idx]
    
    # Сортируем по значению фичи
    sorted_idx = np.argsort(feature_vals)
    sorted_shap = shap_vals[sorted_idx]
    
    # Проверяем монотонность
    # (упрощённо - считаем, сколько раз следующий элемент меньше предыдущего)
    violations = 0
    for i in range(1, len(sorted_shap)):
        if sorted_shap[i] < sorted_shap[i-1]:
            violations += 1
    
    violation_rate = violations / len(sorted_shap)
    return violation_rate

# Проверяем каждую фичу
for i in range(X_train.shape[1]):
    rate = check_monotonicity(shap_values, i, X_train)
    if rate > 0.3:  # Если больше 30% нарушений
        print(f"Фича {X_train.columns[i]}: {rate:.1%} нарушений монотонности")

Больше 30% нарушений - красный флаг. Значит, Shapley values для этой фичи нестабильны.

Что делать, если SHAP врёт?

Случай 1: Модель слишком сложная

XGBoost с depth=10 на маленьком датасете. Random Forest со 1000 деревьев. Нейросеть с кучей слоёв.

Сложные модели имеют сложные, нелинейные, перекрученные decision boundaries. Shapley values пытаются объяснить эту сложность простыми аддитивными вкладами. Получается ерунда.

Решение: упрости модель. Или используй более подходящий explainer. Для глубоких нейросетей попробуй Integrated Gradients вместо SHAP. Для ансамблей - посмотри на альтернативные методы валидации.

Случай 2: Признаки взаимодействуют нелинейно

Возраст и доход. По отдельности не очень предсказывают дефолт. Но вместе - молодой с высоким доходом = риск. Пожилой с низким доходом = риск.

SHAP покажет вклад каждого признака по отдельности. Взаимодействие потеряется.

Решение: используй shap.interaction_values(). Или создай фичи взаимодействия вручную. Или переходи на модели, которые лучше捕获 взаимодействия, вроде SplineTransformer.

Случай 3: Данные имеют временную зависимость

Кредитная история. Погода. Цены на акции. Всё это временные ряды.

SHAP предполагает, что наблюдения независимы. Во временных рядах это не так. Вчерашняя цена влияет на сегодняшнюю. SHAP этого не учтёт.

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

Контрпримеры: когда Shapley values точно не работают

Есть ситуации, где SHAP гарантированно даст неправильные результаты. Запомни их:

СитуацияПочему SHAP ломаетсяЧто делать
Модель использует только взаимодействия признаковSHAP показывает нулевой вклад для каждого признака в отдельностиИспользовать shap.interaction_values() или перейти на другую модель
Выбросы в данныхShapley values для выбросов нестабильны и могут доминировать на графикахУбрать выбросы перед вычислением SHAP или использовать robust scaler
Очень разреженные данныеБольшинство значений признаков = 0, SHAP значения становятся шумнымиАгрегировать признаки или использовать модели для sparse данных
Модель переобученаSHAP значения отражают шум, а не реальные зависимостиУпростить модель, добавить регуляризацию

Альтернативы, когда SHAP не справляется

SHAP - не единственный метод объяснимости. Иногда лучше использовать другие инструменты:

  • LIME - локальные объяснения, лучше работает с нелинейностями
  • Partial Dependence Plots (PDP) - показывает усреднённый эффект признака
  • ALE plots - как PDP, но лучше справляется с корреляциями
  • Permutation Importance - проще, устойчивее к переобучению
  • Anchors - правила типа "если X > 5, то модель предсказывает класс 1"

Главное - не цепляться за один инструмент. Если SHAP показывает странности, проверь другими методами. Если все методы показывают разное - возможно, модель действительно неинтерпретируема. Или данные такие грязные, что никакая объяснимость не поможет.

Чеклист перед показом SHAP графиков заказчику

  1. Проверил baseline? Он имеет смысл для бизнеса?
  2. Нашёл коррелированные признаки? Знаю, как они влияют на интерпретацию?
  3. Проверил монотонность для ключевых признаков?
  4. Убрал выбросы из визуализации?
  5. Агрегировал one-hot encoded фичи?
  6. Сравнил с другими методами объяснимости (хотя бы с permutation importance)?
  7. Подготовил ответ на вопрос "А почему вот этот признак так влияет?"
  8. Знаю ограничения SHAP для своей конкретной модели и данных?

Если на все вопросы ответил "да" - можно показывать. Если нет - вернись и исправь. Потому что однажды тебя спросят: "А вы уверены в этих цифрах?" И лучше подготовить ответ заранее.

💡
Shapley values - это как рентген для модели. Показывает внутренности, но требует квалификации для интерпретации. Неправильно прочитанный снимок хуже, чем отсутствие снимка.

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

Такие истории заканчиваются потерянными контрактами. А иногда - судебными исками. Особенно в регулируемых отраслях вроде кредитования или медицины.

Поэтому отлаживай объяснимость так же тщательно, как отлаживаешь саму модель. Проверяй предположения. Тестируй на синтетических данных. И всегда имей запасной вариант объяснения.

Потому что когда придёт аудитор и спросит "почему?", красивых картинок будет недостаточно. Нужны будут доказательства. И понимание, как эти доказательства получены.