causalscience/Member_Rewards_Effect
0
1# Import required libraries2import pandas as pd3import numpy as np4import matplotlib.pyplot as plt5import seaborn as sns6import os7import tempfile8from PIL import Image9import gradio as gr10import statsmodels.formula.api as smf11import dowhy12from dowhy import CausalModel13 14# -----------------------------------------15# Common Data Loading and Processing16# -----------------------------------------17 18def load_data(file_obj, signup_month=3):19 """20 Load and prepare data for analysis.21 Returns the full dataset and the filtered dataset for the specified signup month.22 """23 try:24 # Read file based on extension25 file_path = file_obj.name26 if file_path.endswith('.csv'):27 df = pd.read_csv(file_path)28 elif file_path.endswith(('.xls', '.xlsx')):29 df = pd.read_excel(file_path)30 else:31 return None, None, "Unsupported file format. Please upload a CSV or Excel file."32 33 # Process for the specific signup month34 i = signup_month35 36 # Keep only users with signup_month of 0 (control) or equal to i (treated)37 df_i = df[df.signup_month.isin([0, i])].copy()38 39 # Validate data40 if len(df_i) == 0:41 return None, None, f"No data found for signup month {i}."42 43 # Check if required columns exist44 required_columns = ['user_id', 'month', 'spend', 'signup_month', 'treatment']45 missing_columns = [col for col in required_columns if col not in df.columns]46 if missing_columns:47 return None, None, f"Missing required columns: {', '.join(missing_columns)}"48 49 # Aggregate spending for each user50 df_i_agg = df_i.groupby(["user_id", "signup_month", "treatment"]).apply(51 lambda x: pd.Series({52 "pre_spends": x.loc[x.month < i, "spend"].mean(),53 "post_spends": x.loc[x.month > i, "spend"].mean()54 })55 ).reset_index()56 57 # Drop rows with missing pre or post spend58 df_i_agg = df_i_agg.dropna()59 60 # Check if we have enough data after dropping NA values61 if len(df_i_agg) == 0:62 return None, None, "No complete data available for analysis. All observations have missing pre or post spending."63 64 # Ensure we have both treatment groups represented65 if df_i_agg['treatment'].nunique() < 2:66 return None, None, "Cannot estimate treatment effect: data contains only one treatment group after filtering."67 68 return df, df_i_agg, None69 70 except Exception as e:71 return None, None, f"Error during data loading: {str(e)}"72 73# Create common data visualization for both methods74def create_common_visualization(df, df_i_agg, signup_month, title="Data Visualization"):75 """76 Create visualizations common to both analysis methods77 """78 plt.figure(figsize=(12, 8))79 80 # Plot 1: Pre vs Post Treatment Spending81 plt.subplot(2, 1, 1)82 83 # Handle both boolean and integer treatment indicators safely84 treatment_col = df_i_agg['treatment']85 if treatment_col.dtype == bool:86 control_mask = ~treatment_col87 treated_mask = treatment_col88 else: # Assume numeric (0/1)89 control_mask = treatment_col == 090 treated_mask = treatment_col == 191 92 control = df_i_agg.loc[control_mask]93 treated = df_i_agg.loc[treated_mask]94 95 # Check if we have data for both groups96 if not control.empty:97 plt.scatter(control["pre_spends"], control["post_spends"], alpha=0.5, label="No Signup")98 if not treated.empty:99 plt.scatter(treated["pre_spends"], treated["post_spends"], alpha=0.5, label="Signed Up")100 101 plt.xlabel("Pre-Treatment Spend")102 plt.ylabel("Post-Treatment Spend")103 plt.title(f"Pre vs Post Treatment Spending (Signup Month = {signup_month})")104 plt.legend()105 106 # Plot 2: Average Monthly Spending Over Time107 plt.subplot(2, 1, 2)108 109 try:110 monthly_spend = df.groupby(["month", "treatment"])["spend"].mean().reset_index()111 sns.lineplot(data=monthly_spend, x="month", y="spend", hue="treatment")112 plt.axvline(x=signup_month, color='red', linestyle='--', alpha=0.7)113 except Exception as e:114 # If line plot fails, just show a message115 plt.text(0.5, 0.5, f"Could not create monthly spend plot: {str(e)}", 116 ha='center', va='center', transform=plt.gca().transAxes)117 118 plt.title("Average Monthly Spend Over Time")119 plt.xlabel("Month")120 plt.ylabel("Average Spend")121 plt.tight_layout()122 123 # Save to a temporary file124 with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file:125 viz_path = temp_file.name126 plt.savefig(viz_path)127 plt.close()128 129 return viz_path130 131# -----------------------------------------132# DoWhy Analysis Implementation133# -----------------------------------------134 135def analyze_dowhy(df, df_i_agg, signup_month, method="backdoor.propensity_score_matching", refutation_method="None"):136 """137 Perform causal analysis using DoWhy138 """139 try:140 i = signup_month141 142 # Define the causal graph143 causal_graph = """digraph {144 treatment[label="Program Signup in month i"];145 pre_spends;146 post_spends;147 Z->treatment;148 pre_spends -> treatment;149 treatment->post_spends;150 signup_month->post_spends;151 signup_month->treatment;152 }"""153 154 # Create model and estimate effect155 model = CausalModel(156 data=df_i_agg,157 graph=causal_graph.replace("\n", " "),158 treatment="treatment",159 outcome="post_spends"160 )161 162 # Identify estimand163 identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)164 165 # Estimate effect166 estimate = model.estimate_effect(167 identified_estimand,168 method_name=method,169 target_units="att"170 )171 172 effect_value = float(estimate.value)173 174 # Save causal model visualization175 try:176 # Some environments may not support graphviz, so we'll handle potential errors177 model.view_model(layout="dot")178 causal_graph_path = "causal_model.png"179 except Exception as graph_error:180 causal_graph_path = None181 print(f"Warning: Could not generate causal graph visualization: {graph_error}")182 183 # Add refutation if requested - format exactly as requested by user184 refutation_result = ""185 if refutation_method != "None":186 try:187 refutation = model.refute_estimate(188 identified_estimand,189 estimate,190 method_name=refutation_method,191 placebo_type="permute" if refutation_method == "placebo_treatment_refuter" else None,192 num_simulations=20193 )194 195 # Format refutation results as requested196 refutation_result = f"\nRefutation using {refutation_method}:\n"197 refutation_result += f"Original estimate: {effect_value:.2f}\n"198 199 # Access attributes safely - maintain original format200 if hasattr(refutation, 'new_effect'):201 refutation_result += f"New effect: {refutation.new_effect:.2f}\n"202 203 # Reproduce the result line exactly as requested204 if hasattr(refutation, 'p_value') and hasattr(refutation, 'is_statistically_significant'):205 refutation_result += f"Result: {{'p_value': np.float64({refutation.p_value}), 'is_statistically_significant': np.{str(refutation.is_statistically_significant)}_}}\n"206 207 # Add full refutation details208 refutation_result += f"Full refutation details:\n"209 refutation_result += f"Refute: Use a Placebo Treatment\n"210 if hasattr(refutation, 'estimated_effect'):211 refutation_result += f"Estimated effect:{refutation.estimated_effect}\n"212 elif hasattr(refutation, 'estimated_effect_'):213 refutation_result += f"Estimated effect:{refutation.estimated_effect_}\n"214 else:215 refutation_result += f"Estimated effect:{effect_value}\n"216 217 if hasattr(refutation, 'new_effect'):218 refutation_result += f"New effect:{refutation.new_effect}\n"219 220 if hasattr(refutation, 'p_value'):221 refutation_result += f"p value:{refutation.p_value}"222 223 except Exception as e:224 refutation_result = f"\n\nRefutation attempt failed: {str(e)}"225 226 # Create common data visualization227 viz_path = create_common_visualization(228 df, df_i_agg, signup_month, 229 title="DoWhy Data Visualization"230 )231 232 # Result text - formatted exactly as requested233 result_text = f"The causal effect of the rewards program (ATT) is ${effect_value:.2f}."234 if refutation_result:235 result_text += refutation_result236 237 return result_text, causal_graph_path, viz_path238 239 except Exception as e:240 return f"Error during DoWhy analysis: {str(e)}", None, None241 242# -----------------------------------------243# Statsmodels Analysis Implementation244# -----------------------------------------245 246def analyze_statsmodels(df, df_i_agg, signup_month):247 """248 Perform analysis using Statsmodels OLS249 """250 try:251 # Run a regression controlling for pre-treatment spending252 model = smf.ols("post_spends ~ treatment + pre_spends", data=df_i_agg)253 results = model.fit()254 255 # Extract the treatment coefficient256 try:257 # Try different ways to access the treatment coefficient258 if "treatment" in results.params:259 effect_value = results.params["treatment"]260 effect_pvalue = results.pvalues["treatment"]261 elif "treatment[T.1]" in results.params:262 effect_value = results.params["treatment[T.1]"]263 effect_pvalue = results.pvalues["treatment[T.1]"]264 elif "treatment[T.True]" in results.params:265 effect_value = results.params["treatment[T.True]"]266 effect_pvalue = results.pvalues["treatment[T.True]"]267 else:268 # Find any parameter that contains 'treatment'269 treatment_params = [p for p in results.params.index if 'treatment' in p.lower()]270 if treatment_params:271 effect_value = results.params[treatment_params[0]]272 effect_pvalue = results.pvalues[treatment_params[0]]273 else:274 # As a last resort, assume the second coefficient is treatment275 if len(results.params) > 1:276 effect_value = results.params[1]277 effect_pvalue = results.pvalues[1]278 else:279 raise KeyError("Cannot identify treatment parameter")280 except Exception as e:281 return f"Error extracting treatment effect: {str(e)}\nAvailable parameters: {results.params.index.tolist()}", None282 283 # Create the summary text - simplified version without full OLS summary284 result_text = f"The causal effect of the rewards program (ATT) using Statsmodels OLS is ${effect_value:.2f}."285 result_text += f"\np-value: {effect_pvalue:.4f} {'(statistically significant)' if effect_pvalue < 0.05 else '(not statistically significant)'}"286 287 # Add R-squared and sample size information288 result_text += f"\n\nR-squared: {results.rsquared:.4f}"289 result_text += f"\nAdjusted R-squared: {results.rsquared_adj:.4f}"290 result_text += f"\nNumber of observations: {results.nobs}"291 292 # Add coefficient for pre-spends293 pre_spends_param = [p for p in results.params.index if 'pre_spends' in p.lower()]294 if pre_spends_param:295 pre_spends_coef = results.params[pre_spends_param[0]]296 pre_spends_pval = results.pvalues[pre_spends_param[0]]297 result_text += f"\n\nPre-treatment spending coefficient: {pre_spends_coef:.4f} (p-value: {pre_spends_pval:.4f})"298 299 # Create common data visualization300 viz_path = create_common_visualization(301 df, df_i_agg, signup_month,302 title="Statsmodels Data Visualization"303 )304 305 return result_text, viz_path306 307 except Exception as e:308 return f"Error during Statsmodels analysis: {str(e)}", None309 310# -----------------------------------------311# Gradio Interface312# -----------------------------------------313 314def gradio_setup(file_obj, signup_month):315 """316 Setup function for the first tab317 """318 if file_obj is None:319 return "Please upload a file to continue."320 321 try:322 # Load and process data323 df, df_i_agg, error_msg = load_data(file_obj, signup_month)324 if error_msg:325 return error_msg326 327 # Basic data statistics 328 stats_text = f"Full dataset: {df.shape[0]} rows, {df.shape[1]} columns\n"329 stats_text += f"Filtered dataset for signup month {signup_month}: {df_i_agg.shape[0]} users\n"330 331 # Calculate treatment group sizes - handle both numeric and boolean treatments332 treatment_col = df_i_agg['treatment']333 if treatment_col.dtype == bool:334 n_control = sum(~treatment_col)335 n_treated = sum(treatment_col)336 control_mask = ~treatment_col337 treated_mask = treatment_col338 else: # Assume numeric (0/1)339 n_control = sum(treatment_col == 0)340 n_treated = sum(treatment_col == 1)341 control_mask = treatment_col == 0342 treated_mask = treatment_col == 1343 344 stats_text += f"Control group: {n_control} users\n"345 stats_text += f"Treatment group: {n_treated} users\n"346 347 # Add pre/post spending averages348 avg_pre_control = df_i_agg.loc[control_mask, 'pre_spends'].mean()349 avg_pre_treated = df_i_agg.loc[treated_mask, 'pre_spends'].mean()350 avg_post_control = df_i_agg.loc[control_mask, 'post_spends'].mean()351 avg_post_treated = df_i_agg.loc[treated_mask, 'post_spends'].mean()352 353 stats_text += f"Average pre-treatment spending (control): ${avg_pre_control:.2f}\n"354 stats_text += f"Average pre-treatment spending (treated): ${avg_pre_treated:.2f}\n"355 stats_text += f"Average post-treatment spending (control): ${avg_post_control:.2f}\n"356 stats_text += f"Average post-treatment spending (treated): ${avg_post_treated:.2f}\n"357 358 # Simple difference-in-differences calculation359 diff_control = avg_post_control - avg_pre_control360 diff_treated = avg_post_treated - avg_pre_treated361 naive_effect = diff_treated - diff_control362 363 stats_text += f"Naive difference-in-differences estimate: ${naive_effect:.2f}"364 365 return stats_text366 367 except Exception as e:368 return f"Error during setup: {str(e)}"369 370def gradio_dowhy(file_obj, signup_month, method, refutation):371 """372 Wrapper for DoWhy analysis373 """374 if file_obj is None:375 return "Please upload a file in the Setup tab first.", None, None376 377 # Load and process data378 df, df_i_agg, error_msg = load_data(file_obj, signup_month)379 if error_msg:380 return error_msg, None, None381 382 # Run DoWhy analysis383 result_text, causal_graph, data_viz = analyze_dowhy(384 df, df_i_agg, signup_month, method, refutation385 )386 387 return result_text, causal_graph, data_viz388 389def gradio_statsmodels(file_obj, signup_month):390 """391 Wrapper for Statsmodels analysis392 """393 if file_obj is None:394 return "Please upload a file in the Setup tab first.", None395 396 # Load and process data397 df, df_i_agg, error_msg = load_data(file_obj, signup_month)398 if error_msg:399 return error_msg, None400 401 # Run Statsmodels analysis402 result_text, data_viz = analyze_statsmodels(df, df_i_agg, signup_month)403 404 return result_text, data_viz405 406# Create the Gradio interface407def create_interface():408 with gr.Blocks(title="Membership Rewards Program Analysis", theme='freddyaboulton/dracula_revamped') as demo:409 gr.Markdown("""# Membership Rewards Program Effect Analysis410 411This application analyzes the causal effect of a membership rewards program on customer spending using three approaches:4121. A Naive difference-in-differences estimate.4132. DoWhy - A causal inference library that implements causal graphs and various causal estimation methods4143. Statsmodels - A simpler approach using OLS regression to control for pre-treatment spending415 416Start by configuring parameters in the Setup tab, then explore both analysis methods in their respective tabs.417 """)418 419 # Shared signup month variable420 signup_month = gr.State(value=3)421 422 # Create tabs423 with gr.Tabs():424 # Tab 1: Setup425 with gr.TabItem("1. Setup"):426 gr.Markdown("""## Data Setup and Configuration427 428Upload your data file and select the signup month to analyze. This tab shows basic statistics about your data.429 """)430 431 file_input = gr.File(label="Upload Data (CSV/Excel)")432 signup_month_slider = gr.Slider(minimum=1, maximum=11, value=3, step=1, label="Signup Month")433 setup_button = gr.Button("Load Data and Calculate Statistics")434 setup_results = gr.Textbox(label="Data Statistics", lines=10)435 436 # Update signup_month state when the slider changes437 def update_signup_month(month):438 return month439 440 signup_month_slider.change(441 fn=update_signup_month,442 inputs=[signup_month_slider],443 outputs=[signup_month]444 )445 446 setup_button.click(447 fn=gradio_setup,448 inputs=[file_input, signup_month_slider],449 outputs=[setup_results]450 )451 452 # Tab 2: DoWhy Analysis453 with gr.TabItem("2. DoWhy Analysis"):454 gr.Markdown("""## Causal Analysis with DoWhy455 456DoWhy implements a formal approach to causal inference based on causal graphs and provides various estimation methods along with refutation techniques.457 """)458 459 method = gr.Dropdown(460 choices=[461 "backdoor.propensity_score_matching",462 "backdoor.linear_regression",463 "backdoor.propensity_score_stratification",464 "backdoor.propensity_score_weighting"465 ],466 value="backdoor.propensity_score_matching",467 label="Estimation Method"468 )469 470 refutation = gr.Dropdown(471 choices=[472 "None",473 "placebo_treatment_refuter",474 "random_common_cause_refuter",475 "data_subset_refuter"476 ],477 value="placebo_treatment_refuter",478 label="Refutation Method"479 )480 481 dowhy_button = gr.Button("Run DoWhy Analysis")482 dowhy_results = gr.Textbox(label="DoWhy Results", lines=15)483 484 with gr.Row():485 causal_graph = gr.Image(label="Causal Graph")486 dowhy_viz = gr.Image(label="Data Visualization")487 488 # Use the stored signup_month from state489 def run_dowhy_with_state_month(file_obj, current_month, method, refutation):490 # Load and process data491 df, df_i_agg, error_msg = load_data(file_obj, current_month)492 if error_msg:493 return error_msg, None, None494 495 # Run DoWhy analysis496 return analyze_dowhy(df, df_i_agg, current_month, method, refutation)497 498 dowhy_button.click(499 fn=run_dowhy_with_state_month,500 inputs=[file_input, signup_month, method, refutation],501 outputs=[dowhy_results, causal_graph, dowhy_viz]502 )503 504 # Tab 3: Statsmodels Analysis505 with gr.TabItem("3. Statsmodels Analysis"):506 gr.Markdown("""## Analysis with Statsmodels OLS507 508This approach uses a simple OLS regression model to estimate the treatment effect while controlling for pre-treatment spending.509 """)510 511 statsmodels_button = gr.Button("Run Statsmodels Analysis")512 statsmodels_results = gr.Textbox(label="Statsmodels Results", lines=15)513 statsmodels_viz = gr.Image(label="Data Visualization")514 515 # Use the stored signup_month from state516 def run_statsmodels_with_state_month(file_obj, current_month):517 # Load and process data518 df, df_i_agg, error_msg = load_data(file_obj, current_month)519 if error_msg:520 return error_msg, None521 522 # Run Statsmodels analysis523 return analyze_statsmodels(df, df_i_agg, current_month)524 525 statsmodels_button.click(526 fn=run_statsmodels_with_state_month,527 inputs=[file_input, signup_month],528 outputs=[statsmodels_results, statsmodels_viz]529 )530 531 return demo532 533# Run the application534if __name__ == "__main__":535 demo = create_interface()536 demo.launch()