Ilustración técnica para: IA Explicable (XAI): Desentrañando la Caja Negra de tus Modelos de Machine Learning con LIME y SHAP

LIME y SHAP en 2026: qué API sigue funcionando y cuál rompió


La mayoría de los tutoriales de LIME y SHAP que circulan hoy se escribieron contra versiones de hace dos o tres años, y en el caso de SHAP eso ya es suficiente para que el código no funcione tal cual. Antes de repetir el patrón habitual de "entrena un modelo, explica una predicción", vale la pena hacer el experimento inverso: correr el mismo código de un tutorial de 2025 contra las versiones vigentes y ver exactamente dónde rompe.

Método: mismo dataset, mismo modelo, dos librerías

Usamos el dataset Breast Cancer Wisconsin incluido en scikit-learn y un RandomForestClassifier, la combinación estándar para comparar explicadores porque es un problema de clasificación binaria con features numéricas e interpretables (radio medio, textura, etc.). El objetivo no es solo generar una explicación, sino verificar qué de la API de cada librería sigue vigente.

pip install "scikit-learn>=1.9" "shap>=0.52" lime pandas numpy
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
print(f"Precisión: {accuracy_score(y_test, model.predict(X_test)):.4f}")

Resultado medido con LIME: funciona, pero es código de 2020

La ficha de lime en PyPI muestra su última publicación, la 0.2.0.1, fechada el 26 de junio de 2020. Seis años sin un release no significa que esté rota (la API pública es pequeña y estable, y el paquete sigue instalándose sin problemas contra scikit-learn 1.9), pero sí significa que no vas a encontrar soporte para tipos de modelo nuevos ni corrección de bugs futuros: es una dependencia congelada, no mantenida activamente.

import lime
import lime.lime_tabular

explainer_lime = lime.lime_tabular.LimeTabularExplainer(
    training_data=X_train.values,
    feature_names=X.columns.tolist(),
    class_names=data.target_names.tolist(),
    mode="classification",
)

instance = X_test.iloc[0]
explanation = explainer_lime.explain_instance(
    data_row=instance.values,
    predict_fn=model.predict_proba,
    num_features=7,
)

for feature, weight in explanation.as_list():
    print(f"  {feature}: {weight:.4f}")

Este código no cambió respecto a como funcionaba hace tres años, y ese es exactamente el punto: la superficie de la API de LimeTabularExplainer es tan pequeña y estable que un paquete sin mantenimiento sigue siendo funcional. LIME entrena, para cada instancia, un modelo lineal local sobre versiones perturbadas de esa instancia y usa los coeficientes de ese modelo simple como explicación, por lo que dos ejecuciones consecutivas sobre la misma instancia pueden dar pesos ligeramente distintos: es una limitación conocida de su diseño, no un bug de una versión concreta.

Resultado medido con SHAP: el código de 2025 falla directamente

Aquí es donde el experimento da un resultado distinto. La versión estable vigente de shap es la 0.52.0 y, a diferencia de LIME, exige Python 3.12 o superior según su propia ficha de PyPI: si tu entorno todavía corre 3.10 u 3.11, tienes que fijar una versión anterior del paquete o actualizar el interprete antes de instalar nada.

El cambio que de verdad rompe el código de tutoriales antiguos es otro: la documentación oficial de TreeExplainer señala explícitamente que, desde la versión 0.45.0, el tipo de retorno de shap_values() para modelos con múltiples salidas (como un clasificador multiclase) cambió de lista de arrays a un único array de NumPy con el eje de clase como última dimensión. El patrón shap_values[predicted_class] que aparece en casi todo tutorial de antes de 2024 ya no indexa por clase: indexa por muestra, y falla o da un resultado incorrecto sin lanzar ningún error obvio.

import shap

explainer_shap = shap.TreeExplainer(model)

# Forma vigente desde shap>=0.45: (n_muestras, n_features, n_clases)
shap_values = explainer_shap.shap_values(X_test)
print(shap_values.shape)  # (114, 30, 2) para este dataset y modelo

instance_idx = 0
predicted_class = model.predict(X_test.iloc[[instance_idx]])[0]

# Indexar la clase va en el ÚLTIMO eje, no como un índice de lista
values_for_instance = shap_values[instance_idx, :, predicted_class]
base_value = explainer_shap.expected_value[predicted_class]

ranking = sorted(
    zip(X.columns.tolist(), values_for_instance),
    key=lambda item: abs(item[1]),
    reverse=True,
)
for feature, value in ranking[:7]:
    print(f"  {feature}: {value:.4f}")

print(f"Valor base: {base_value:.4f}")
print(f"Suma SHAP + base: {values_for_instance.sum() + base_value:.4f}")

La propiedad de aditividad sigue siendo el ancla para verificar que la explicación es correcta: la suma de los valores SHAP de una instancia más el valor base debe aproximarse a la salida real del modelo para esa clase. Si esa suma no cuadra, algo en el indexado de ejes está mal, casi siempre por mezclar la convención vieja (lista) con la nueva (array con eje de clase).

Limitaciones que no cambiaron con la versión

Independientemente de qué API uses, hay tres límites estructurales que ninguna versión nueva resuelve:

  • Correlación no es causalidad: tanto LIME como SHAP muestran contribución a la predicción, no una relación causal real. Una característica puede pesar mucho porque está correlacionada con la causa real, no porque sea la causa.
  • Coste computacional del explicador agnóstico: TreeExplainer es rápido porque explota la estructura del árbol, pero KernelExplainer (el explicador agnóstico al modelo, necesario si no usas un modelo basado en árboles) sigue siendo costoso porque aproxima valores de Shapley cuyo cálculo exacto es NP-hard con muchas características.
  • Las explicaciones heredan los sesgos del modelo: si el modelo aprendió una correlación espuria del dataset de entrenamiento, la explicación la va a mostrar como "importante" con la misma confianza que una característica genuinamente causal. Ninguna de las dos librerías distingue una cosa de la otra.

Mini proyecto: clasificador de cáncer de mama con las dos librerías a la vez

Este es el script completo, con la API corregida para las versiones vigentes, que entrena el modelo y genera explicación local con LIME, explicación local con SHAP y explicación global con SHAP en una sola pasada:

import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
import lime
import lime.lime_tabular
import shap

# --- 1. Datos y modelo ---
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
print(f"Precisión: {accuracy_score(y_test, model.predict(X_test)):.4f}")

# --- 2. Instancia a explicar ---
idx = 5
instance = X_test.iloc[idx]
actual = data.target_names[y_test[idx]]
predicted_idx = model.predict(instance.to_frame().T)[0]
predicted = data.target_names[predicted_idx]
print(f"Real: {actual} | Predicha: {predicted}")

# --- 3. Explicación local con LIME ---
explainer_lime = lime.lime_tabular.LimeTabularExplainer(
    training_data=X_train.values,
    feature_names=X.columns.tolist(),
    class_names=data.target_names.tolist(),
    mode="classification",
)
exp_lime = explainer_lime.explain_instance(
    data_row=instance.values, predict_fn=model.predict_proba, num_features=7
)
print("\nLIME:")
for feature, weight in exp_lime.as_list():
    print(f"  {feature}: {weight:.4f}")

# --- 4. Explicación local con SHAP (API vigente desde 0.45.0) ---
explainer_shap = shap.TreeExplainer(model)
shap_values_test = explainer_shap.shap_values(X_test)  # (n_muestras, n_features, n_clases)
values_for_instance = shap_values_test[idx, :, predicted_idx]
base_value = explainer_shap.expected_value[predicted_idx]

print("\nSHAP (local):")
ranking = sorted(zip(X.columns.tolist(), values_for_instance), key=lambda x: abs(x[1]), reverse=True)
for feature, value in ranking[:7]:
    print(f"  {feature}: {value:.4f}")
print(f"Valor base: {base_value:.4f} | Suma + base: {values_for_instance.sum() + base_value:.4f}")

# --- 5. Explicación global con SHAP ---
importancia_global = pd.DataFrame({
    "feature": X.columns.tolist(),
    "importancia_media": np.abs(shap_values_test[:, :, 1]).mean(axis=0),
}).sort_values("importancia_media", ascending=False)

print("\nImportancia global (SHAP, clase 'benigno'):")
print(importancia_global.head(10).to_string(index=False))

Sobre este dataset concreto, LIME y SHAP suelen coincidir en el grupo de características más influyentes (típicamente worst radius, worst concave points y mean concave points aparecen en ambos rankings), pero rara vez en el orden exacto ni en la magnitud: es el resultado esperado de dos aproximaciones distintas al mismo problema, no un error de una de las dos.

Errores de dimensiones que vas a ver al migrar código viejo

  • IndexError o resultados sin sentido al indexar shap_values[clase]: síntoma directo de código escrito contra shap <0.45 corriendo contra una versión reciente. Revisa el eje: la clase ahora va al final, no como primer índice de una lista.
  • Instalación de shap que falla en un entorno con Python 3.11 o anterior: shap 0.52.x requiere 3.12+; o subes el interprete o fijas shap<0.5 en tu requirements.txt, sabiendo que perderás las correcciones posteriores.
  • predict_fn de LIME recibiendo un DataFrame en vez de un array: LimeTabularExplainer espera arrays de NumPy tanto en training_data como en data_row; usa siempre .values al pasarle datos de un DataFrame de pandas.
  • Rendimiento de KernelExplainer sobre datasets grandes: si tu modelo no es un ensemble de árboles y necesitas SHAP, KernelExplainer es la única opción agnóstica, pero es sensiblemente más lento; muestrea el conjunto de prueba en vez de pasarlo completo.
Compartir X LinkedIn