Este artículo ofrece una introducción práctica al potencial de los gráficos causales.
Está dirigido a cualquier persona que quiera entender más sobre:
- Qué son los gráficos causales y cómo funcionan
- Un estudio de caso trabajado en Python que ilustra cómo construir gráficos causales
- Cómo se comparan con ML
- Los principales desafíos y consideraciones futuras
El cuaderno completo se puede encontrar aquí:
Los gráficos causales nos ayudan a separar las causas de las correlaciones. Son una parte clave de la caja de herramientas de inferencia causal/ML causal/IA causal y pueden usarse para responder preguntas causales.
A menudo denominado DAG (gráfico acíclico dirigido), un gráfico causal contiene nodos y bordes: los bordes vinculan nodos que están relacionados causalmente.
Hay dos formas de determinar una gráfica causal:
- Conocimiento experto del dominio
- Algoritmos de descubrimiento causal
Por ahora, asumiremos que tenemos conocimiento experto del dominio para determinar el gráfico causal (cubriremos los algoritmos de descubrimiento causal más adelante).
El objetivo de ML es clasificar o predecir con la mayor precisión posible dados algunos datos de entrenamiento. No existe ningún incentivo para que un algoritmo de ML garantice que las funciones que utiliza estén vinculadas causalmente con el objetivo. No hay garantía de que la dirección (efecto positivo/negativo) y la fuerza de cada característica se alineen con el verdadero proceso de generación de datos. ML no tendrá en cuenta las siguientes situaciones:
- Correlaciones espurias: dos variables que tienen una correlación espuria cuando tienen una causa común, por ejemplo, las altas temperaturas aumentan el número de ventas de helados y ataques de tiburones.
- Factores de confusión: una variable afecta su tratamiento y resultado, por ejemplo, la demanda afecta cuánto gastamos en marketing y cuántos nuevos clientes se registran.
- Colisionadores: una variable que se ve afectada por dos variables independientes, por ejemplo, Calidad de la atención al cliente -> Satisfacción del usuario <- Tamaño de la empresa.
- Mediadores: dos variables vinculadas (indirectamente) a través de un mediador, por ejemplo, ejercicio regular -> aptitud cardiovascular (el mediador) -> salud general
Debido a estas complejidades y a la naturaleza de caja negra del ML, no podemos confiar en su capacidad para responder preguntas causales.
Dado un gráfico causal conocido y datos observados, podemos entrenar un modelo causal estructural (SCM). Se puede considerar un SCM como una serie de modelos causales, uno por nodo. Cada modelo utiliza un nodo como objetivo y sus padres directos como características. Si las relaciones en nuestros datos observados son lineales, un SCM será una serie de ecuaciones lineales. Esto podría modelarse mediante una serie de modelos de regresión lineal. Si las relaciones en nuestros datos observados no son lineales, esto podría modelarse con una serie de árboles potenciados.
La diferencia clave con el ML tradicional es que un SCM modela relaciones causales y tiene en cuenta correlaciones espurias, factores de confusión, colisionadores y mediadores.
Es común utilizar un modelo de ruido aditivo (ANM) para cada nodo no raíz (lo que significa que tiene al menos un padre). Esto nos permite utilizar una variedad de algoritmos de aprendizaje automático (más un término de ruido) para estimar cada nodo no raíz.
Y := f(X) + norte
Los nodos raíz se pueden modelar utilizando un modelo estocástico para describir la distribución.
Un SCM puede verse como un modelo generativo que puede generar nuevas muestras de datos, lo que le permite responder a una variedad de preguntas causales. Genera nuevos datos tomando muestras de los nodos raíz y luego propagando los datos a través del gráfico.
El valor de un SCM es que nos permite responder preguntas causales calculando contrafactuales y simulando intervenciones:
- Contrafactuales: uso de datos observados históricamente para calcular qué le habría pasado a y si hubiéramos cambiado xeg. ¿Qué habría pasado con la cantidad de clientes que abandonaron si hubiéramos reducido el tiempo de espera de llamadas en un 20% el mes pasado?
- Intervenciones: muy similares a los contrafactuales (y a menudo se usan indistintamente), pero las intervenciones simulan lo que sucedería en el futuro, por ejemplo, ¿qué pasará con la cantidad de clientes que abandonan si reducimos el tiempo de espera de llamadas en un 20% el próximo año?
Hay varios KPI que monitorea el equipo de atención al cliente. Uno de ellos son los tiempos de espera de llamadas. Aumentar el número de personal del centro de llamadas reducirá los tiempos de espera de las llamadas.
Pero, ¿cómo afectará la disminución del tiempo de espera de las llamadas a los niveles de abandono de clientes? ¿Y esto compensará el costo del personal adicional del centro de llamadas?
Se pide al equipo de ciencia de datos que cree y evalúe el caso de negocio.
La población de interés son los clientes que realizan una llamada entrante. Diariamente se recopilan los siguientes datos de series temporales:
En este ejemplo, utilizamos datos de series temporales, pero los gráficos causales también pueden funcionar con datos a nivel de cliente.
En este ejemplo, utilizamos conocimiento de dominio experto para determinar el gráfico causal.
# Create node lookup for channels
node_lookup = {0: 'Demand',
1: 'Call waiting time',
2: 'Call abandoned',
3: 'Reported problems',
4: 'Discount sent',
5: 'Churn'
}total_nodes = len(node_lookup)
# Create adjacency matrix - this is the base for our graph
graph_actual = np.zeros((total_nodes, total_nodes))
# Create graph using expert domain knowledge
graph_actual[0, 1] = 1.0 # Demand -> Call waiting time
graph_actual[0, 2] = 1.0 # Demand -> Call abandoned
graph_actual[0, 3] = 1.0 # Demand -> Reported problems
graph_actual[1, 2] = 1.0 # Call waiting time -> Call abandoned
graph_actual[1, 5] = 1.0 # Call waiting time -> Churn
graph_actual[2, 3] = 1.0 # Call abandoned -> Reported problems
graph_actual[2, 5] = 1.0 # Call abandoned -> Churn
graph_actual[3, 4] = 1.0 # Reported problems -> Discount sent
graph_actual[3, 5] = 1.0 # Reported problems -> Churn
graph_actual[4, 5] = 1.0 # Discount sent -> Churn
A continuación, necesitamos generar datos para nuestro estudio de caso.
Queremos generar algunos datos que nos permitan comparar el cálculo de contrafactuales utilizando gráficos causales versus ML (para simplificar las cosas, regresión de cresta).
Como identificamos el gráfico causal en la última sección, podemos utilizar este conocimiento para crear un proceso de generación de datos.
def data_generator(max_call_waiting, inbound_calls, call_reduction):
'''
A data generating function that has the flexibility to reduce the value of node 0 (Call waiting time) - this enables us to calculate ground truth counterfactualsArgs:
max_call_waiting (int): Maximum call waiting time in seconds
inbound_calls (int): Total number of inbound calls (observations in data)
call_reduction (float): Reduction to apply to call waiting time
Returns:
DataFrame: Generated data
'''
df = pd.DataFrame(columns=node_lookup.values())
df[node_lookup[0]] = np.random.randint(low=10, high=max_call_waiting, size=(inbound_calls)) # Demand
df[node_lookup[1]] = (df[node_lookup[0]] * 0.5) * (call_reduction) + np.random.normal(loc=0, scale=40, size=inbound_calls) # Call waiting time
df[node_lookup[2]] = (df[node_lookup[1]] * 0.5) + (df[node_lookup[0]] * 0.2) + np.random.normal(loc=0, scale=30, size=inbound_calls) # Call abandoned
df[node_lookup[3]] = (df[node_lookup[2]] * 0.6) + (df[node_lookup[0]] * 0.3) + np.random.normal(loc=0, scale=20, size=inbound_calls) # Reported problems
df[node_lookup[4]] = (df[node_lookup[3]] * 0.7) + np.random.normal(loc=0, scale=10, size=inbound_calls) # Discount sent
df[node_lookup[5]] = (0.10 * df[node_lookup[1]] ) + (0.30 * df[node_lookup[2]]) + (0.15 * df[node_lookup[3]]) + (-0.20 * df[node_lookup[4]]) # Churn
return df
# Generate data
np.random.seed(999)
df = data_generator(max_call_waiting=600, inbound_calls=10000, call_reduction=1.00)sns.pairplot(df)
Ahora tenemos una matriz de adyacencia que representa nuestro gráfico causal y algunos datos. Usamos el módulo gcm del paquete Dowhy Python para entrenar un SCM.
Es importante pensar qué mecanismo causal utilizar para los nodos raíz y no raíz. Si observa nuestra función de generación de datos, verá que todas las relaciones son lineales. Por lo tanto, elegir la regresión de crestas debería ser suficiente.
# Setup graph
graph = nx.from_numpy_array(graph_actual, create_using=nx.DiGraph)
graph = nx.relabel_nodes(graph, node_lookup)# Create SCM
causal_model = gcm.InvertibleStructuralCausalModel(graph)
causal_model.set_causal_mechanism('Demand', gcm.EmpiricalDistribution()) # Root node
causal_model.set_causal_mechanism('Call waiting time', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root node
causal_model.set_causal_mechanism('Call abandoned', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root node
causal_model.set_causal_mechanism('Reported problems', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root node
causal_model.set_causal_mechanism('Discount sent', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root
causal_model.set_causal_mechanism('Churn', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root
gcm.fit(causal_model, df)
También puede utilizar la función de asignación automática para asignar automáticamente los mecanismos causales en lugar de asignarlos manualmente.
Para obtener más información sobre el paquete gcm, consulte los documentos:
También utilizamos la regresión de crestas para ayudar a crear una comparación de referencia. Podemos volver a mirar el generador de datos y ver que estima correctamente los coeficientes de cada variable. Sin embargo, además de influir directamente en la deserción, el tiempo de espera de llamadas influye indirectamente en la deserción a través de llamadas abandonadas, problemas reportados y descuentos enviados.
Cuando se trata de estimar contrafactuales, será interesante ver cómo se compara el SCM con la regresión de crestas.
# Ridge regression
y = df['Churn'].copy()
X = df.iloc[:, 1:-1].copy()
model = RidgeCV()
model = model.fit(X, y)
y_pred = model.predict(X)print(f'Intercept: {model.intercept_}')
print(f'Coefficient: {model.coef_}')
# Ground truth[0.10 0.30 0.15 -0.20]
Antes de pasar a calcular contrafactuales utilizando gráficos causales y regresión de crestas, necesitamos un punto de referencia de verdad fundamental. Podemos utilizar nuestro generador de datos para crear muestras contrafactuales después de haber reducido el tiempo de espera de llamadas en un 20%.
No podríamos hacer esto con problemas del mundo real, pero este método nos permite evaluar qué tan efectivos son el gráfico causal y la regresión de crestas.
# Set call reduction to 20%
reduce = 0.20
call_reduction = 1 - reduce# Generate counterfactual data
np.random.seed(999)
df_cf = data_generator(max_call_waiting=600, inbound_calls=10000, call_reduction=call_reduction)
Ahora podemos estimar qué habría sucedido si hubiéramos disminuido el tiempo de espera de la llamada en un 20% utilizando nuestros 3 métodos:
- Verdad fundamental (del generador de datos)
- Regresión de cresta
- gráfico causal
Vemos que la regresión de crestas subestima significativamente el impacto en la deserción, mientras que el gráfico causal está muy cerca de la verdad fundamental.
# Ground truth counterfactual
ground_truth = round((df['Churn'].sum() - df_cf['Churn'].sum()) / df['Churn'].sum(), 2)# Causal graph counterfactual
df_counterfactual = gcm.counterfactual_samples(causal_model, {'Call waiting time': lambda x: x*call_reduction}, observed_data=df)
causal_graph = round((df['Churn'].sum() - df_counterfactual['Churn'].sum()) / (df['Churn'].sum()), 3)
# Ridge regression counterfactual
ridge_regression = round((df['Call waiting time'].sum() * 1.0 * model.coef_[0] - (df['Call waiting time'].sum() * call_reduction * model.coef_[0])) / (df['Churn'].sum()), 3)
Este fue un ejemplo sencillo para empezar a pensar en el poder de los gráficos causales.
Para situaciones más complejas, varios desafíos que necesitarían consideración:
- ¿Qué suposiciones se hacen y cuál es el impacto de su violación?
- ¿Qué pasa si no tenemos el conocimiento experto para identificar el gráfico causal?
- ¿Qué pasa si existen relaciones no lineales?
- ¿Qué tan dañina es la multicolinealidad?
- ¿Qué pasa si algunas variables tienen efectos rezagados?
- ¿Cómo podemos lidiar con conjuntos de datos de alta dimensión (muchas variables)?
Todos estos puntos se tratarán en futuros blogs.
Si está interesado en aprender más sobre la IA causal, le recomiendo los siguientes recursos:
“Conozca a Ryan, un científico de datos líder experimentado con un enfoque especializado en el empleo de técnicas causales dentro de contextos comerciales, que abarcan marketing, operaciones y servicio al cliente. Su competencia radica en desentrañar las complejidades de las relaciones de causa y efecto para impulsar la toma de decisiones informadas y mejoras estratégicas en diversas funciones organizacionales”.