CausalBootstrapping is an easy-access implementation and extention of causal bootstrapping (CB) technique for causal analysis. With certain input of observational data, causal graph and variable distributions, CB resamples the data by adjusting the variable distributions which follow intended causal effects, so an appropriate and unbiased causal effects between the cause variable and effect variable can be captured.
In a backdoor setting, an existing confounder may lead to so-called "selection bias". And thus a machine leanring model which is blind to the backend causal relationships between variables is exposed to risks of learning biased and unreliable associations between the predicting target and the features. A simple and intuitive example is as below:
In the figure, the model trained on confounded dataset (for example, the observational data collected from uncontrolled experiments) is biased due to the existence of the confounder. Causal Bootstrapping can aid this challenge by adjusting the observational data's distribution, and thus the model is supposed to learn from the data given the generative distribution of
Please use one of the following to cite the code of this repository.
@article{little2019causal,
title={Causal bootstrapping},
author={Little, Max A and Badawy, Reham},
journal={arXiv preprint arXiv:1910.09648},
year={2019}
}
We currently offer seamless installation with pip
.
Simply:
pip install CausalBootstrapping
Alternatively, download the current distribution of the package, and run:
pip install .
in the root directory of the decompressed package.
To import the package:
import causalBootstrapping as cb
Please refer to Tutorials for more instructions and examples.
- Import causalBootstrapping lib and other libs for demo.
import causalBootstrapping as cb
from distEst_lib import MultivarContiDistributionEstimator
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.metrics import classification_report
- Define a causal graph
causal_graph = '"General Causal Graph"; \
Y; X; U; Z; \
U -> Y; \
Y -> Z; \
U -> Z; \
Z -> X; \
X <-> Y;'
The above causal graph is equivalent to:
- Analyse the causal graph and output the weights function expression and required distributions
weight_func_lam, weight_func_str = cb.general_cb_analysis(causal_graph = causal_graph,
effect_var_name = 'X',
cause_var_name = 'Y',
info_print = True)
This code is expected to output as below:
Interventional prob.:p_{Y}(X)=\sum_{U,Z,Y'}[p(X|U,Z,Y')p(Z|U,Y)p(U,Y')]
Causal bootstrapping weights function: [P(U,Y')P(U,Y,Z)]/N*[P(U,Y',Z)P(U,Y)]
Required distributions:
1: P(U,Y')
2: P(U,Y,Z)
3: P(U,Y',Z)
4: P(U,Y)
- Read the demo. data for causal bootstrapping bootstraping
# Read demo data
testdata_dir = "../test_data/complex_scenario/"
X_train = pd.read_csv(testdata_dir + "X_train.csv")
Y_train = pd.read_csv(testdata_dir + "Y_train.csv")
Z_train = pd.read_csv(testdata_dir + "Z_train.csv")
U_train = pd.read_csv(testdata_dir + "U_train.csv")
# Reform the data to the acceptable format for the causalbootstrapping interfaces
X_train = np.array(X_train)
Y_train = np.array(Y_train)
Z_train = np.array(Z_train)
U_train = np.array(U_train)
data = {"Y'": Y_train,
"X": X_train,
"Z": Z_train,
"U": U_train}
- Estimate the desired distributions (as shown in previous output of
general_cb_analysis()
). User is also encourged to define the distribution functions if certain domain knowledge has been obtained.
#Set number of the bins for histogram becasue all variables follow discrete distributions.
n_bins_uyz = [0,0,0,0]
n_bins_uy = [0,0]
data_uyz = np.concatenate((U_train, Y_train, Z_train), axis = 1)
data_uy = np.concatenate((U_train, Y_train), axis = 1)
dist_estimator_uyz = MultivarContiDistributionEstimator(data_fit=data_uyz, n_bins = n_bins_uyz)
pdf_uyz, puyz = dist_estimator_uyz.fit_histogram()
dist_estimator_uy = MultivarContiDistributionEstimator(data_fit=data_uy, n_bins = n_bins_uy)
pdf_uy, puy = dist_estimator_uy.fit_histogram()
- Construct the distribution mapping dict
dist_map = {"U,Y,Z": lambda U, Y, Z: pdf_uyz([U, Y, Z]),
"U,Y',Z": lambda U, Y_prime, Z: pdf_uyz([U, Y_prime, Z]),
"U,Y'": lambda U, Y_prime: pdf_uy([U,Y_prime]),
"U,Y": lambda U, Y: pdf_uy([U, Y])}
- bootstrap the dataset given the weight function expression
cb_data = cb.general_causal_bootstrapping_simple(weight_func_lam = weight_func_lam,
dist_map = dist_map, data = data,
intv_var_name = "Y", kernel = None)
- Train two linear support vector machines using confounded and de-confounded datasets
clf_conf = svm.SVC(kernel = 'linear', C=2)
clf_conf.fit(X_train, Y_train.reshape(-1))
clf_cb = svm.SVC(kernel = 'linear', C=2)
clf_cb.fit(cb_data['X'], cb_data["intv_Y"].reshape(-1))
- Compare their performance on an un-confounded test set
X_test = pd.read_csv(testdata_dir + "X_test.csv")
Y_test = pd.read_csv(testdata_dir + "Y_test.csv")
X_test = np.array(X_test)
Y_test = np.array(Y_test)
y_pred_conf = clf_conf.predict(X_test)
print("Report of confonded model:")
print(classification_report(Y_test, y_pred_conf))
y_pred_deconf = clf_cb.predict(X_test)
print("Report of de-confonded model:")
print(classification_report(Y_test, y_pred_deconf))
The expected output should be similar to:
Report of confonded model:
precision recall f1-score support
1 0.56 0.88 0.68 865
2 0.84 0.46 0.60 1135
accuracy 0.65 2000
macro avg 0.70 0.67 0.64 2000
weighted avg 0.72 0.65 0.63 2000
Report of de-confonded model:
precision recall f1-score support
1 0.63 0.84 0.72 865
2 0.84 0.63 0.72 1135
accuracy 0.72 2000
macro avg 0.73 0.73 0.72 2000
weighted avg 0.75 0.72 0.72 2000
- Compare models' decision boundaries
#confounding boundary
conf_x2, conf_x3 = np.meshgrid(np.linspace(-6, 6, 20), np.linspace(-6, 6, 20))
conf_x1 = np.zeros((20,20))
# real boundary
real_x1, real_x2 = np.meshgrid(np.linspace(-6, 6, 20), np.linspace(-6, 6, 20))
real_x3 = np.full_like(real_x1, 0)
# confounded svm boundary
xx1, xx2= np.meshgrid(np.linspace(-6, 6, 50), np.linspace(-6, 6, 50))
xx_conf = (-clf_conf.intercept_[0] - clf_conf.coef_[0][0] * xx1 - clf_conf.coef_[0][1] * xx2) / clf_conf.coef_[0][2]
# deconfounded svm boundary
xx1, xx2= np.meshgrid(np.linspace(-6, 6, 50), np.linspace(-6, 6, 50))
xx_cb = (-clf_cb.intercept_[0] - clf_cb.coef_[0][0] * xx1 - clf_cb.coef_[0][1] * xx2) / clf_cb.coef_[0][2]
plt.figure()
ax = plt.axes(projection='3d')
ax.scatter3D(X_test[:,0],X_test[:,1],X_test[:,2],c=Y_test, s = 5, alpha = 0.5)
surf1 = ax.plot_surface(conf_x1, conf_x2, conf_x3, alpha=0.5, rstride=100, cstride=100, color = "yellow", label = "confounding boundary")
surf2 = ax.plot_surface(real_x1, real_x2, real_x3, alpha=0.5, rstride=100, cstride=100, color = "green", label = "real boundary")
surf3 = ax.plot_surface(xx1, xx2, xx_conf, color='red', alpha=0.5, rstride=100, cstride=100, label = "confounded decision boundary")
surf4 = ax.plot_surface(xx1, xx2, xx_cb, color='blue', alpha=0.5, rstride=100, cstride=100, label = "confounded decision boundary")
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.set_zlabel('X3')
surf1._facecolors2d=surf1._facecolors
surf1._edgecolors2d=surf1._edgecolors
surf2._facecolors2d=surf2._facecolors
surf2._edgecolors2d=surf2._edgecolors
surf3._facecolors2d=surf3._facecolors
surf3._edgecolors2d=surf3._edgecolors
surf4._facecolors2d=surf4._facecolors
surf4._edgecolors2d=surf4._edgecolors
ax.legend(["Unconfounded test data", "confounding boundary", "real boundary", "confounded decision boundary", "deconfounded decision boundary"])
plt.title('Decision boundary comparison')
plt.tight_layout()
plt.show()
The expected output of the image should be similar to: