0n4 Mqdaqxel3usx6.jpeg

IA causal, que explora la integración del razonamiento causal en el aprendizaje automático

Foto por Jordan McDonald en desempaquetar

Bienvenido a mi serie sobre IA causal, donde exploraremos la integración del razonamiento causal en modelos de aprendizaje automático. Espere explorar una serie de aplicaciones prácticas en diferentes contextos comerciales.

En el último artículo exploramos Efectos del tratamiento antisesgado con Double Machine Learning. En esta ocasión profundizaremos más en el potencial que tiene DML cubriendo Uso de aprendizaje automático doble y programación lineal para optimizar las estrategias de tratamiento..

Si te perdiste el último artículo sobre Double Machine Learning, consúltalo aquí:

Este artículo mostrará cómo se pueden utilizar el aprendizaje automático doble y la programación lineal para optimizar las estrategias de tratamiento:

Espere obtener una amplia comprensión de:

  • Por qué las empresas quieren optimizar las estrategias de tratamiento.
  • Cómo los efectos del tratamiento promedio condicional (CATE) pueden ayudar a personalizar las estrategias de tratamiento (también conocido como modelado Uplift).
  • Cómo se puede utilizar la programación lineal para optimizar la asignación de tratamientos dadas las restricciones presupuestarias.
  • Un estudio de caso trabajado en Python que ilustra cómo podemos utilizar el aprendizaje automático doble para estimar CATE y la programación lineal para optimizar las estrategias de tratamiento.

El cuaderno completo se puede encontrar aquí:

Hay una pregunta común que surge en la mayoría de las empresas: “¿Cuál es el trato óptimo para un cliente para maximizar las ventas futuras minimizando los costos?”.

Analicemos esta idea con un ejemplo sencillo.

Tu negocio vende calcetines online. No vende un producto esencial, por lo que debe alentar a los clientes existentes a repetir la compra. Su principal palanca para esto es enviar descuentos. Entonces la estrategia de tratamiento en este caso es enviar descuentos:

  • 10% de descuento
  • 20% de descuento
  • 50% de descuento

Cada descuento tiene un retorno de la inversión diferente. Si recuerda el último artículo sobre los efectos promedio del tratamiento, probablemente pueda ver cómo podemos calcular el ATE para cada uno de estos descuentos y luego seleccionar el que tenga el mayor rendimiento.

Sin embargo, ¿qué pasa si tenemos efectos de tratamiento heterogéneos? El efecto del tratamiento varía entre los diferentes subgrupos de la población.

¡Aquí es cuando debemos comenzar a considerar los efectos del tratamiento promedio condicional (CATE)!

CATA

CATE es el impacto promedio de un tratamiento o intervención en diferentes subgrupos de una población. ATE se trataba principalmente de «¿funciona este tratamiento?» mientras que CATE nos permite cambiar la pregunta a «¿a quién debemos tratar?».

«Condicionamos» nuestras funciones de control para permitir que los efectos del tratamiento varíen según las características del cliente.

Piense en el ejemplo en el que enviamos descuentos. Si los clientes con un mayor número de pedidos anteriores responden mejor a los descuentos, podemos condicionar esta característica del cliente.

Vale la pena señalar que en marketing, la estimación de CATE a menudo se denomina Uplift Modeling.

Estimación de CATE con aprendizaje automático doble

Cubrimos DML en el último artículo, pero en caso de que necesites un repaso:

«Primera etapa:

  • Modelo de tratamiento (dessesgo): Modelo de aprendizaje automático utilizado para estimar la probabilidad de asignación de tratamiento (a menudo denominado puntuación de propensión). Luego se calculan los residuos del modelo de tratamiento.
  • Modelo de resultados (eliminación de ruido): Modelo de aprendizaje automático utilizado para estimar el resultado utilizando solo las funciones de control. Luego se calculan los residuos del modelo de resultados.

Segunda etapa:

  • Los residuos del modelo de tratamiento se utilizan para predecir los residuos del modelo de resultados”.

Podemos utilizar el aprendizaje automático doble para estimar CATE interactuando nuestras funciones de control (X) con el efecto del tratamiento en el modelo de la segunda etapa.

Imagen generada por el usuario

¡Esto puede ser realmente poderoso ya que ahora podemos obtener efectos de tratamiento a nivel del cliente!

¿Qué es?

La programación lineal es un método de optimización que se puede utilizar para encontrar la solución óptima de una función lineal dadas algunas restricciones. A menudo se utiliza para resolver problemas de transporte, programación y asignación de recursos. Un término más genérico que quizás vea utilizado es Investigación de operaciones.

Analicemos la programación lineal con un ejemplo simple:

  • Variables de decisión: Estas son las cantidades desconocidas para las que queremos estimar valores óptimos: el gasto en marketing en redes sociales, televisión y búsqueda paga.
  • Función de objetivos: La ecuación lineal que intentamos minimizar o maximizar: el retorno de la inversión (ROI) en marketing.
  • Restricciones: Algunas restricciones en las variables de decisión, generalmente representadas por desigualdades lineales: gasto total en marketing entre £ 100 000 y £ 500 000.

La intersección de todas las restricciones forma una región factible, que es el conjunto de todas las soluciones posibles que satisfacen las restricciones dadas. El objetivo de la programación lineal es encontrar el punto dentro de la región factible que optimice la función objetivo.

Problemas de asignación

Los problemas de asignación son un tipo específico de problema de programación lineal donde el objetivo es asignar un conjunto de «tareas» a un conjunto de «agentes». Usemos un ejemplo para darle vida:

Realiza un experimento en el que envía diferentes descuentos a 4 grupos aleatorios de clientes existentes (al cuarto de los cuales en realidad no envía ningún descuento). Usted construye 2 modelos CATE: (1) Estimar cómo el valor de la oferta afecta el valor del pedido y (2) Estimar cómo el valor de la oferta afecta el costo.

  • Agentes: su base de clientes existente
  • Tareas: Ya sea que les envíes un 10%, 20% o 50% de descuento
  • Variables de decisión: variable de decisión binaria
  • Función objetivo: el valor total del pedido menos los costos.
  • Restricción 1: a cada agente se le asigna como máximo 1 tarea
  • Restricción 2: El costo ≥ £10,000
  • Restricción 3: El costo ≤ £100 000
Imagen generada por el usuario

Básicamente queremos descubrir el tratamiento óptimo para cada cliente dadas algunas restricciones de costos generales. ¡Y la programación lineal puede ayudarnos a hacer esto!

Vale la pena señalar que este problema es “NP difícil”, una clasificación de problemas que son al menos tan difíciles como los problemas más difíciles en NP (tiempo polinómico no determinista).

La programación lineal es un tema realmente complicado pero gratificante. Intenté presentar la idea para comenzar. Si desea obtener más información, le recomiendo este recurso:

O herramientas

OR tools es un paquete de código abierto desarrollado por Google que puede resolver una variedad de problemas de programación lineal, incluidos problemas de asignación. Lo demostraremos en acción más adelante en este artículo.

Fondo

Continuaremos con el ejemplo del problema de asignación e ilustraremos cómo podemos resolverlo en Python.

Proceso de generación de datos

Configuramos un proceso de generación de datos con las siguientes características:

  • Parámetros difíciles y molestos (b)
  • Heterogeneidad del efecto del tratamiento (tau)

Las características X son características del cliente tomadas antes del tratamiento:

Imagen generada por el usuario

T es una bandera binaria que indica si el cliente recibió la oferta. Creamos tres interacciones de tratamiento diferentes para permitirnos simular diferentes efectos de tratamiento.

Imagen generada por el usuario
def data_generator(tau_weight, interaction_num):

# Set number of observations
n=10000

# Set number of features
p=10

# Create features
X = np.random.uniform(size=n * p).reshape((n, -1))

# Nuisance parameters
b = (
np.sin(np.pi * X[:, 0] * X[:, 1])
+ 2 * (X[:, 2] - 0.5) ** 2
+ X[:, 3]
+ 0.5 * X[:, 4]
+ X[:, 5] * X[:, 6]
+ X[:, 7] ** 3
+ np.sin(np.pi * X[:, 8] * X[:, 9])
)

# Create binary treatment
T = np.random.binomial(1, expit(b))

# treatment interactions
interaction_1 = X[:, 0] * X[:, 1] + X[:, 2]
interaction_2 = X[:, 3] * X[:, 4] + X[:, 5]
interaction_3 = X[:, 6] * X[:, 7] + X[:, 9]

# Set treatment effect
if interaction_num==1:
tau = tau_weight * interaction_1
elif interaction_num==2:
tau = tau_weight * interaction_2
elif interaction_num==3:
tau = tau_weight * interaction_3

# Calculate outcome
y = b + T * tau + np.random.normal(size=n)

return X, T, tau, y

Podemos utilizar el generador de datos para simular tres tratamientos, cada uno con un efecto de tratamiento diferente.

Imagen generada por el usuario
np.random.seed(123)

# Generate samples for 3 different treatments
X1, T1, tau1, y1 = data_generator(0.75, 1)
X2, T2, tau2, y2 = data_generator(0.50, 2)
X3, T3, tau3, y3 = data_generator(0.90, 3)

Como en el artículo anterior, el código Python del proceso de generación de datos se basa en el creador de datos sintéticos del paquete Ubers Causal ML:

Estimación de CATE con DML

Luego entrenamos tres modelos DML utilizando LightGBM como modelos flexibles de primera etapa. Esto debería permitirnos capturar los parámetros molestos difíciles mientras calculamos correctamente el efecto del tratamiento.

Preste atención a cómo pasamos las características X a través de X en lugar de W (a diferencia del último artículo donde pasamos las características X a través de W). Las características que pasan a través de X se usarán en los modelos de primera y segunda etapa. En el modelo de segunda etapa, las características se usan para crear términos de interacción con el tratamiento residual.

np.random.seed(123)

# Train DML model using flexible stage 1 models
dml1 = LinearDML(model_y=LGBMRegressor(), model_t=LGBMClassifier(), discrete_treatment=True)
dml1.fit(y1, T=T1, X=X1, W=None)

# Train DML model using flexible stage 1 models
dml2 = LinearDML(model_y=LGBMRegressor(), model_t=LGBMClassifier(), discrete_treatment=True)
dml2.fit(y2, T=T2, X=X2, W=None)

# Train DML model using flexible stage 1 models
dml3 = LinearDML(model_y=LGBMRegressor(), model_t=LGBMClassifier(), discrete_treatment=True)
dml3.fit(y3, T=T3, X=X3, W=None)

Cuando trazamos el CATE real versus el estimado, vemos que el modelo hace un trabajo razonable.

# Create a figure and subplots
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# Plot scatter plots on each subplot
sns.scatterplot(x=dml1.effect(X1), y=tau1, ax=axes[0])
axes[0].set_title('Treatment 1')
axes[0].set_xlabel('Estimated CATE')
axes[0].set_ylabel('Actual CATE')

sns.scatterplot(x=dml2.effect(X2), y=tau2, ax=axes[1])
axes[1].set_title('Treatment 2')
axes[1].set_xlabel('Estimated CATE')
axes[1].set_ylabel('Actual CATE')

sns.scatterplot(x=dml3.effect(X3), y=tau3, ax=axes[2])
axes[2].set_title('Treatment 3')
axes[2].set_xlabel('Estimated CATE')
axes[2].set_ylabel('Actual CATE')

# Add labels to the entire figure
fig.suptitle('Actual vs Estimated')

# Show plots
plt.show()

Imagen generada por el usuario

Optimización ingenua

Comenzaremos explorando esto como un problema de optimización. Disponemos de tres tratamientos que un cliente podría recibir. A continuación creamos un mapeo para el costo de cada tratamiento y establecemos una restricción de costo general.

# Create mapping for cost of each treatment
cost_dict = {'T1': 0.1, 'T2': 0.2, 'T3': 0.3}

# Set constraints
max_cost = 3000

Luego podemos estimar el CATE para cada cliente y luego seleccionar inicialmente el mejor tratamiento para cada cliente. Sin embargo, seleccionar el mejor tratamiento no nos mantiene dentro de la restricción de coste máximo. Por lo tanto, seleccione los clientes con el CATE más alto hasta que alcancemos nuestra restricción de costo máximo.

# Concatenate features
X = np.concatenate((X1, X2, X3), axis=0)

# Estimate CATE for each treatment using DML models
Treatment_1 = dml1.effect(X)
Treatment_2 = dml2.effect(X)
Treatment_3 = dml3.effect(X)
cate = pd.DataFrame({"T1": Treatment_1, "T2": Treatment_2, "T3": Treatment_3})

# Select the best treatment for each customer
best_treatment = cate.idxmax(axis=1)
best_value = cate.max(axis=1)

# Map cost for each treatment
best_cost = pd.Series([cost_dict[value] for value in best_treatment])

# Create dataframe with each customers best treatment and associated cost
best_df = pd.concat([best_value, best_cost], axis=1)
best_df.columns = ["value", "cost"]
best_df = best_df.sort_values(by=['value'], ascending=False).reset_index(drop=True)

# Naive optimisation
best_df_cum = best_df.cumsum()
opt_index = best_df_cum['cost'].searchsorted(max_cost)
naive_order_value = round(best_df_cum.iloc[opt_index]['value'], 0)
naive_cost_check = round(best_df_cum.iloc[opt_index]['cost'], 0)

print(f'The total order value from the naive treatment strategy is {naive_order_value} with a cost of {naive_cost_check}')

Imagen generada por el usuario

Optimización de estrategias de tratamiento con Programación Lineal

Empezamos creando un dataframe con el coste de cada tratamiento para cada cliente.

# Cost mapping for all treatments
cost_mapping = {'T1': [cost_dict["T1"]] * 30000,
'T2': [cost_dict["T2"]] * 30000,
'T3': [cost_dict["T3"]] * 30000}

# Create DataFrame
df_costs = pd.DataFrame(cost_mapping)

¡Ahora es el momento de utilizar el paquete OR Tools para resolver este problema de asignación! El código toma las siguientes entradas:

  • Restricciones de costos
  • Matriz que contiene el costo de cada tratamiento para cada cliente.
  • Matriz que contiene el valor estimado del pedido para cada tratamiento para cada cliente

El código genera un marco de datos con el tratamiento potencial de cada cliente y una columna que indica cuál es la asignación óptima.

solver = pywraplp.Solver.CreateSolver('SCIP')

# Set constraints
max_cost = 3000
min_cost = 3000

# Create input arrays
costs = df_costs.to_numpy()
order_value = cate.to_numpy()

num_custs = len(costs)
num_treatments = len(costs[0])

# x[i, j] is an array of 0-1 variables, which will be 1 if customer i is assigned to treatment j.
x = {}
for i in range(num_custs):
for j in range(num_treatments):
x[i, j] = solver.IntVar(0, 1, '')

# Each customer is assigned to at most 1 treatment.
for i in range(num_custs):
solver.Add(solver.Sum([x[i, j] for j in range(num_treatments)]) <= 1)

# Cost constraints
solver.Add(sum([costs[i][j] * x[i, j] for j in range(num_treatments) for i in range(num_custs)]) <= max_cost)
solver.Add(sum([costs[i][j] * x[i, j] for j in range(num_treatments) for i in range(num_custs)]) >= min_cost)

# Objective
objective_terms = []
for i in range(num_custs):
for j in range(num_treatments):
objective_terms.append((order_value[i][j] * x[i, j] - costs[i][j] * x[i, j] ))
solver.Maximize(solver.Sum(objective_terms))

# Solve
status = solver.Solve()

assignments = []
values = []

if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
for i in range(num_custs):
for j in range(num_treatments):
# Test if x[i,j] is 1 (with tolerance for floating point arithmetic).
if x[i, j].solution_value() > -0.5:
assignments.append([i, j])
values.append([x[i, j].solution_value(), costs[i][j] * x[i, j].solution_value(), order_value[i][j]])

# Create a DataFrame from the collected data
df = pd.DataFrame(assignments, columns=['customer', 'treatment'])
df['assigned'] = [x[0] for x in values]
df['cost'] = [x[1] for x in values]
df['order_value'] = [x[2] for x in values]

df

Imagen generada por el usuario

Si bien mantenemos la restricción de costos de £ 3 mil, podemos generar £ 18 mil en valor de pedido utilizando la estrategia de tratamiento optimizada. ¡Esto es un 36% más que el enfoque ingenuo!

opt_order_value = round(df['order_value'][df['assigned'] == 1].sum(), 0)
opt_cost_check = round(df['cost'][df['assigned'] == 1].sum(), 0)

print(f'The total order value from the optimised treatment strategy is {opt_order_value} with a cost of {opt_cost_check}')

Imagen generada por el usuario

Hoy cubrimos el uso del aprendizaje automático doble y la programación lineal para optimizar las estrategias de tratamiento. Aquí hay algunos pensamientos finales:

  • Cubrimos DML lineal, es posible que desee explorar enfoques alternativos que sean más adecuados para lidiar con efectos de interacción complejos en el modelo de la segunda etapa:
  • Pero recuerde también que no es necesario utilizar DML; se pueden utilizar otros métodos como T-Learner o DR-Learner.
  • Para que este artículo sea de lectura rápida, no ajusté los hiperparámetros. A medida que aumentamos la complejidad del problema y el enfoque utilizado, debemos prestar más atención a esta parte.
  • Los problemas de programación/asignación lineal son NP difíciles, por lo que si tiene una gran base de clientes y/o varios tratamientos, esta parte del código puede tardar mucho en ejecutarse.
  • Puede ser un desafío poner en funcionamiento un proceso diario con problemas de programación/asignación lineal. Una alternativa es ejecutar la optimización periódicamente y aprender la política óptima en función de los resultados para crear una segmentación para usar en un proceso diario.