IA causale, esplorando l'integrazione del ragionamento causale nell'apprendimento automatico

fotografato da Irina Inga SU Unsplash

Benvenuti nella mia serie sull'intelligenza artificiale causale, in cui esploreremo l'integrazione del ragionamento causale nei modelli di apprendimento automatico. Aspettatevi di esplorare una serie di applicazioni pratiche in diversi contesti aziendali.

Nell'ultimo articolo abbiamo trattato misurare l'influenza causale intrinseca delle tue campagne di marketing. In questo articolo andremo avanti convalidare l’impatto causale dei controlli sintetici.

Se ti sei perso l’ultimo articolo sull’influenza causale intrinseca, dai un’occhiata qui:

In questo articolo ci concentreremo sulla comprensione del metodo di controllo sintetico e sull'esplorazione di come convalidare l'impatto causale stimato.

Verranno trattati i seguenti aspetti:

  • Qual è il metodo di controllo sintetico?
  • Quale sfida cerca di superare?
  • Come possiamo convalidare l’impatto causale stimato?
  • Un caso di studio Python che utilizza dati realistici sulle tendenze di Google, dimostrando come possiamo convalidare l'impatto causale stimato dei controlli sintetici.

Il quaderno completo lo trovate qui:

Che cos'è?

Il metodo di controllo sintetico è una tecnica causale che può essere utilizzata per valutare l’impatto causale di un intervento o trattamento quando uno studio di controllo randomizzato (RCT) o un test A/B non è stato possibile. È stato originariamente proposto nel 2003 da Abadie e Gardezabal. Il seguente documento include un ottimo caso di studio per aiutarti a comprendere il metodo proposto:

https://web.stanford.edu/~jhain/Paper/JASA2010.pdf

Immagine generata dall'utente

Copriamo noi stessi alcuni nozioni di base… Il metodo di controllo sintetico crea una versione controfattuale dell'unità di trattamento creando una combinazione ponderata di unità di controllo che non hanno ricevuto l'intervento o il trattamento.

  • Unità trattata: L'unità che riceve l'intervento.
  • Unità di controllo: Un insieme di unità simili che non hanno ricevuto l'intervento.
  • Controfattuale: Creato come combinazione ponderata delle centraline. Lo scopo è trovare pesi per ciascuna unità di controllo che risultino in un controfattuale che corrisponda strettamente all'unità trattata nel periodo precedente all'intervento.
  • Impatto causale: La differenza tra unità di trattamento post intervento e controfattuale.

Se volessimo davvero semplificare le cose, potremmo pensarla come una regressione lineare in cui ciascuna unità di controllo è una caratteristica e l'unità di trattamento è l'obiettivo. Il periodo pre-intervento è il nostro insieme di treni e utilizziamo il modello per valutare il nostro periodo post-intervento. La differenza tra l’impatto reale e quello previsto è l’impatto causale.

Di seguito sono riportati un paio di esempi per dargli vita quando potremmo prendere in considerazione l'utilizzo:

  • Quando eseguiamo una campagna di marketing televisivo, non siamo in grado di assegnare casualmente il pubblico a coloro che possono e non possono vedere la campagna. Potremmo tuttavia selezionare attentamente una regione per provare la campagna e utilizzare le regioni rimanenti come unità di controllo. Una volta misurato l’effetto, la campagna potrebbe essere estesa ad altre regioni. Questo è spesso chiamato test di geo-lift.
  • Cambiamenti politici introdotti in alcune regioni ma non in altre – Ad esempio, un consiglio locale può introdurre un cambiamento politico per ridurre la disoccupazione. Altre regioni in cui la politica non era in atto potrebbero essere utilizzate come unità di controllo.

Quale sfida cerca di superare?

Quando combiniamo l'elevata dimensionalità (molte funzionalità) con osservazioni limitate, possiamo ottenere un modello che si adatta eccessivamente.

Prendiamo l'esempio del geolift per illustrare. Se utilizziamo i dati settimanali dell'ultimo anno come periodo pre-intervento, otteniamo 52 osservazioni. Se poi decidiamo di testare il nostro intervento in tutti i paesi europei, ciò ci darà un rapporto tra osservazione e caratteristiche di 1:1!

In precedenza abbiamo parlato di come il metodo di controllo sintetico potrebbe essere implementato utilizzando la regressione lineare. Tuttavia, il rapporto tra osservazione e caratteristiche indica che è molto probabile che la regressione lineare si adatti eccessivamente, determinando una stima dell’impatto causale inadeguata nel periodo post-intervento.

Nella regressione lineare i pesi (coefficienti) per ciascuna caratteristica (unità di controllo) potrebbero essere negativi o positivi e la loro somma potrebbe dare un numero maggiore di 1. Tuttavia, il metodo di controllo sintetico apprende i pesi applicando i seguenti vincoli:

  • Vincolare i pesi affinché la somma sia 1
  • Vincolare i pesi a essere ≥ 0
L'utente genera un'immagine

Questi vincoli aiutano con la regolarizzazione ed evitano l’estrapolazione oltre l’intervallo dei dati osservati.

Vale la pena notare che in termini di regolarizzazione, la regressione Ridge e Lasso possono raggiungere questo obiettivo e in alcuni casi rappresentano alternative ragionevoli. Ma lo testeremo nel caso di studio!

Come possiamo convalidare l’impatto causale stimato?

Una sfida probabilmente più grande è il fatto che non siamo in grado di convalidare l’impatto causale stimato nel periodo post-intervento.

Quanto dovrebbe durare il periodo pre-intervento? Siamo sicuri di non aver superato il periodo pre-intervento? Come possiamo sapere se il nostro modello si generalizza bene nel periodo post intervento? Cosa succede se voglio provare diverse implementazioni del metodo di controllo sintetico?

Potremmo selezionare in modo casuale alcune osservazioni dal periodo pre-intervento e trattenerle per la convalida. Ma abbiamo già evidenziato la sfida che deriva dall’avere osservazioni limitate, quindi potremmo peggiorare le cose!

E se potessimo eseguire una sorta di simulazione pre-intervento? Ciò potrebbe aiutarci a rispondere ad alcune delle domande evidenziate sopra e ad acquisire fiducia nell’impatto causale stimato dei nostri modelli? Tutto sarà spiegato nel case study!

Sfondo

Dopo aver convinto il dipartimento finanziario che il marketing del marchio sta generando un valore significativo, il team di marketing ti contatta per chiederti informazioni sui test di incremento geografico. Qualcuno da Facebook ha detto loro che è la prossima grande novità (anche se è stata la stessa persona a dire loro che Prophet era un buon modello di previsione) e vogliono sapere se possono usarlo per misurare la loro nuova campagna televisiva che sta arrivando.

Sei un po' preoccupato, poiché l'ultima volta che hai eseguito un test di geo-lift il team di analisi di marketing ha pensato che fosse una buona idea giocare con il periodo pre-intervento utilizzato fino a quando non si è avuto un impatto causale significativo.

Questa volta suggerisci di eseguire una “simulazione pre-intervento”, dopodiché proponi che il periodo pre-intervento venga concordato prima dell’inizio del test.

Esploriamo quindi come si presenta una “simulazione pre-intervento”!

Creazione dei dati

Per renderlo il più realistico possibile, ho estratto alcuni dati sulle tendenze di Google per la maggior parte dei paesi europei. Il termine di ricerca non è rilevante, fai semplicemente finta che siano le vendite della tua azienda (e che operi in tutta Europa).

Tuttavia, se sei interessato a come ho ottenuto i dati sulle tendenze di Google, controlla il mio taccuino:

Di seguito possiamo vedere il dataframe. Abbiamo vendite negli ultimi 3 anni in 50 paesi europei. Il team di marketing prevede di condurre la campagna televisiva in Gran Bretagna.

Immagine generata dall'utente

Ora arriva la parte intelligente. Simuleremo un intervento nelle ultime 7 settimane della serie temporale.

np.random.seed(1234)

# Create intervention flag
mask = (df('date') >= "2024-04-14") & (df('date') <= "2024-06-02")
df('intervention') = mask.astype(int)

row_count = len(df)

# Create intervention uplift
df('uplift_perc') = np.random.uniform(0.10, 0.20, size=row_count)
df('uplift_abs') = round(df('uplift_perc') * df('GB'))
df('y') = df('GB')
df.loc(df('intervention') == 1, 'y') = df('GB') + df('uplift_abs')

Ora tracciamo le vendite effettive e controfattuali in GB per dare vita a ciò che abbiamo fatto:

def synth_plot(df, counterfactual):

plt.figure(figsize=(14, 8))
sns.set_style("white")

# Create plot
sns.lineplot(data=df, x='date', y='y', label='Actual', color='b', linewidth=2.5)
sns.lineplot(data=df, x='date', y=counterfactual, label='Counterfactual', color='r', linestyle='--', linewidth=2.5)
plt.title('Synthetic Control Method: Actual vs. Counterfactual', fontsize=24)
plt.xlabel('Date', fontsize=20)
plt.ylabel('Metric Value', fontsize=20)
plt.legend(fontsize=16)
plt.gca().xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%Y-%m-%d'))
plt.xticks(rotation=90)
plt.grid(True, linestyle='--', alpha=0.5)

# High the intervention point
intervention_date = '2024-04-07'
plt.axvline(pd.to_datetime(intervention_date), color='k', linestyle='--', linewidth=1)
plt.text(pd.to_datetime(intervention_date), plt.ylim()(1)*0.95, 'Intervention', color='k', fontsize=18, ha='right')

plt.tight_layout()
plt.show()

synth_plot(df, 'GB')
Immagine generata dall'utente

Quindi, ora che abbiamo simulato un intervento, possiamo esplorare come funzionerà il metodo di controllo sintetico.

Pre-elaborazione

Tutti i paesi europei tranne la Gran Bretagna sono impostati come unità di controllo (caratteristiche). L'unità di trattamento (target) sono le vendite in GB con l'intervento applicato.

# Delete the original target column so we don't use it as a feature by accident
del df('GB')

# set feature & targets
X = df.columns(1:50)
y = 'y'

Regressione

Di seguito ho impostato una funzione che possiamo riutilizzare con diversi periodi pre-intervento e diversi modelli di regressione (ad esempio Ridge, Lasso):

def train_reg(df, start_index, reg_class):

df_temp = df.iloc(start_index:).copy().reset_index()

X_pre = df_temp(df_temp('intervention') == 0)(X)
y_pre = df_temp(df_temp('intervention') == 0)(y)

X_train, X_test, y_train, y_test = train_test_split(X_pre, y_pre, test_size=0.10, random_state=42)

model = reg_class
model.fit(X_train, y_train)

yhat_train = model.predict(X_train)
yhat_test = model.predict(X_test)

mse_train = mean_squared_error(y_train, yhat_train)
mse_test = mean_squared_error(y_test, yhat_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")

r2_train = r2_score(y_train, yhat_train)
r2_test = r2_score(y_test, yhat_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")

df_temp('pred') = model.predict(df_temp.loc(:, X))
df_temp('delta') = df_temp('y') - df_temp('pred')

pred_lift = df_temp(df_temp('intervention') == 1)('delta').sum()
actual_lift = df_temp(df_temp('intervention') == 1)('uplift_abs').sum()
abs_error_perc = abs(pred_lift - actual_lift) / actual_lift
print(f"Predicted lift: {round(pred_lift, 2)}")
print(f"Actual lift: {round(actual_lift, 2)}")
print(f"Absolute error percentage: {round(abs_error_perc, 2)}")

return df_temp, abs_error_perc

Per iniziare manteniamo le cose semplici e utilizziamo la regressione lineare per stimare l’impatto causale, utilizzando un piccolo periodo pre-intervento:

df_lin_reg_100, pred_lift_lin_reg_100 = train_reg(df, 100, LinearRegression())
Immagine generata dall'utente

Guardando i risultati, la regressione lineare non funziona molto bene. Ma questo non è sorprendente dato il rapporto tra osservazione e caratteristiche.

synth_plot(df_lin_reg_100, 'pred')
Immagine generata dall'utente

Metodo di controllo sintetico

Entriamo subito e vediamo come si confronta con il metodo di controllo sintetico. Di seguito ho impostato una funzione simile a quella precedente, ma applicando il metodo di controllo sintetico utilizzando sciPy:

def synthetic_control(weights, control_units, treated_unit):

synthetic = np.dot(control_units.values, weights)

return np.sqrt(np.sum((treated_unit - synthetic)**2))

def train_synth(df, start_index):

df_temp = df.iloc(start_index:).copy().reset_index()

X_pre = df_temp(df_temp('intervention') == 0)(X)
y_pre = df_temp(df_temp('intervention') == 0)(y)

X_train, X_test, y_train, y_test = train_test_split(X_pre, y_pre, test_size=0.10, random_state=42)

initial_weights = np.ones(len(X)) / len(X)

constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})

bounds = ((0, 1) for _ in range(len(X)))

result = minimize(synthetic_control,
initial_weights,
args=(X_train, y_train),
method='SLSQP',
bounds=bounds,
constraints=constraints,
options={'disp': False, 'maxiter': 1000, 'ftol': 1e-9},
)

optimal_weights = result.x

yhat_train = np.dot(X_train.values, optimal_weights)
yhat_test = np.dot(X_test.values, optimal_weights)

mse_train = mean_squared_error(y_train, yhat_train)
mse_test = mean_squared_error(y_test, yhat_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")

r2_train = r2_score(y_train, yhat_train)
r2_test = r2_score(y_test, yhat_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")

df_temp('pred') = np.dot(df_temp.loc(:, X).values, optimal_weights)
df_temp('delta') = df_temp('y') - df_temp('pred')

pred_lift = df_temp(df_temp('intervention') == 1)('delta').sum()
actual_lift = df_temp(df_temp('intervention') == 1)('uplift_abs').sum()
abs_error_perc = abs(pred_lift - actual_lift) / actual_lift
print(f"Predicted lift: {round(pred_lift, 2)}")
print(f"Actual lift: {round(actual_lift, 2)}")
print(f"Absolute error percentage: {round(abs_error_perc, 2)}")

return df_temp, abs_error_perc

Mantengo lo stesso periodo pre-intervento per creare un confronto equo con la regressione lineare:

df_synth_100, pred_lift_synth_100 = train_synth(df, 100)
Immagine generata dall'utente

Oh! Sarò il primo ad ammettere che non mi aspettavo un miglioramento così significativo!

synth_plot(df_synth_100, 'pred')
Immagine generata dall'utente

Confronto dei risultati

Non lasciamoci ancora trasportare troppo. Di seguito eseguiamo alcuni altri esperimenti esplorando i tipi di modello e i periodi pre-interventi:

# run regression experiments
df_lin_reg_00, pred_lift_lin_reg_00 = train_reg(df, 0, LinearRegression())
df_lin_reg_100, pred_lift_lin_reg_100 = train_reg(df, 100, LinearRegression())
df_ridge_00, pred_lift_ridge_00 = train_reg(df, 0, RidgeCV())
df_ridge_100, pred_lift_ridge_100 = train_reg(df, 100, RidgeCV())
df_lasso_00, pred_lift_lasso_00 = train_reg(df, 0, LassoCV())
df_lasso_100, pred_lift_lasso_100 = train_reg(df, 100, LassoCV())

# run synthetic control experiments
df_synth_00, pred_lift_synth_00 = train_synth(df, 0)
df_synth_100, pred_lift_synth_100 = train_synth(df, 100)

experiment_data = {
"Method": ("Linear", "Linear", "Ridge", "Ridge", "Lasso", "Lasso", "Synthetic Control", "Synthetic Control"),
"Data Size": ("Large", "Small", "Large", "Small", "Large", "Small", "Large", "Small"),
"Value": (pred_lift_lin_reg_00, pred_lift_lin_reg_100, pred_lift_ridge_00, pred_lift_ridge_100,pred_lift_lasso_00, pred_lift_lasso_100, pred_lift_synth_00, pred_lift_synth_100)
}

df_experiments = pd.DataFrame(experiment_data)

Utilizzeremo il codice seguente per visualizzare i risultati:

# Set the style
sns.set_style="whitegrid"

# Create the bar plot
plt.figure(figsize=(10, 6))
bar_plot = sns.barplot(x="Method", y="Value", hue="Data Size", data=df_experiments, palette="muted")

# Add labels and title
plt.xlabel("Method")
plt.ylabel("Absolute error percentage")
plt.title("Synthetic Controls - Comparison of Methods Across Different Data Sizes")
plt.legend(title="Data Size")

# Show the plot
plt.show()

Immagine generata dall'utente

I risultati per il piccolo set di dati sono davvero interessanti! Come previsto, la regolarizzazione ha contribuito a migliorare le stime dell’impatto causale. Il controllo sintetico ha poi fatto un ulteriore passo avanti!

I risultati dell’ampio set di dati suggeriscono che periodi pre-intervento più lunghi non sono sempre migliori.

Tuttavia, la cosa che voglio che tu comprenda è quanto sia prezioso effettuare una simulazione pre-intervento. Ci sono così tante strade che potresti esplorare con il tuo set di dati!

Oggi abbiamo esplorato il metodo di controllo sintetico e come convalidare l'impatto causale. Vi lascio con alcune considerazioni finali:

  • La semplicità del metodo di controllo sintetico lo rende una delle tecniche più utilizzate tra gli strumenti di intelligenza artificiale causale.
  • Sfortunatamente è anche quello più abusato: eseguiamo il pacchetto R CausalImpact, modificando il periodo pre-intervento finché non vediamo un miglioramento che ci piace. 😭
  • È qui che consiglio vivamente di eseguire simulazioni pre-intervento per concordare in anticipo la progettazione del test.
  • Il metodo di controllo sintetico è un’area ampiamente studiata. Vale la pena dare un'occhiata agli adattamenti proposti Augmented SC, Robust SC e Penalized SC.

Alberto Abadie, Alexis Diamond e Jens Hainmueller (2010) Metodi di controllo sintetici per studi di casi comparativi: stima dell'effetto del programma di controllo del tabacco della California, Journal of the American Statistical Association, 105:490, 493–505, DOI: 10.1198/jasa.2009 .ap08746

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *