├── README.md ├── figures ├── figures3-4 │ ├── extra-files │ │ ├── figure-3-matrix-eigvals.csv │ │ └── figure-4-matrix-eigvals.csv │ ├── figure3-data.csv │ ├── figure3-simulate.py │ ├── figure4-data.csv │ ├── figure4-simulate.py │ └── figures3-4-plot.R ├── population_estimators.py └── tools.py ├── real-data-experiment ├── README.md ├── anchorRegression.py ├── data │ └── README.md ├── figs │ └── README.md ├── notebooks │ ├── results_tempC.ipynb │ ├── results_tempC_figure12.ipynb │ └── results_tempC_proxies.ipynb ├── results │ └── README.md ├── run_exp_temp.py ├── run_exp_temp_proxies.py └── utils.py └── synthetic-experiments ├── appendix-B-identification.ipynb ├── experiment1 ├── experiment1-plot.R └── experiment1-simulate.py ├── experiment2 ├── experiment2-plot.R └── experiment2-simulate.py ├── experiment3 ├── experiment3-plot.R └── experiment3-simulate.py ├── experiment4 ├── experiment4-plot.R └── experiment4-simulate.py ├── population_estimators.py └── tools.py /README.md: -------------------------------------------------------------------------------- 1 | # Code for replication of "Regularizing Towards Causal Invariance: Linear Models with Proxies" 2 | 3 | * Section 5 (Synthetic Experiments): `synthetic-experiments`, depends on 4 | + Computation: `numpy`, `pandas`, `tqdm` 5 | + Plotting: `R`, with `tidyverse`, `dplyr`, `readr`, `ggplot2`, `tikzDevice` 6 | * Section 6 (Pollution): `real-data-experiment`, depends on 7 | + Computation: `Pandas`, `numpy`, `scipy`, `scikit-learn` 8 | + Notebooks: `jupyter` 9 | + Plotting: `seaborn`, `matplotlib` 10 | -------------------------------------------------------------------------------- /figures/figures3-4/extra-files/figure-3-matrix-eigvals.csv: -------------------------------------------------------------------------------- 1 | 5.199122989862130950e+00;1.758349216789134672e+01;9.891977568801584297e-01;-1.465871678669836420e-01;9.891977568801583187e-01;-1.465871678669836142e-01 2 | 2.266055106894913962e+00;5.999999999999999112e+00;1.465871678669836420e-01;9.891977568801584297e-01;1.465871678669836142e-01;9.891977568801583187e-01 3 | -------------------------------------------------------------------------------- /figures/figures3-4/extra-files/figure-4-matrix-eigvals.csv: -------------------------------------------------------------------------------- 1 | 3.000000000000000000e+00;3.497041666068850141e+00;1.700000000000000000e+01;9.474786857846720922e-01;3.198189174888669273e-01 2 | -3.000000000000000000e+00;5.929583339311506052e-01;0.000000000000000000e+00;-3.198189174888669273e-01;9.474786857846720922e-01 3 | -------------------------------------------------------------------------------- /figures/figures3-4/figure3-simulate.py: -------------------------------------------------------------------------------- 1 | ### Loading libraries 2 | import numpy as np 3 | import pandas as pd 4 | from tqdm import tqdm 5 | 6 | # Load file tools and population_estimators from parent folder 7 | import os, sys, inspect 8 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 9 | parentdir = os.path.dirname(currentdir) 10 | sys.path.insert(0,parentdir) 11 | from population_estimators import pack_params, gamma_ar, gamma_par, gamma_ols, gamma_cross 12 | from tools import Id, simulate, N, get_mse, inv 13 | 14 | ### Set seed 15 | np.random.seed(1) 16 | 17 | ### Dimensions 18 | d = {"A": 2, "W": 2, 'Z': 2, "Y": 1, "X": 2, "H": 1} 19 | d['O'] = d['X'] + d['Y'] + d['H'] 20 | 21 | ### Parameters 22 | beta = Id(d['A'], d['W']) 23 | pars = {'M': np.random.poisson(lam=2, size=(d['O'], d['A'])), 24 | 'B': np.random.normal(size=(d['O'], d['O']))/3, 25 | 'beta': beta} 26 | np.fill_diagonal(pars['B'], 0) 27 | noise_W = np.array([[0.5, 0], [-0.8, 1.5]]) 28 | lamb = 5 29 | 30 | 31 | ### Save matrix eigendecomposition for plotting in R 32 | M_par = Id(2) + lamb*beta@inv(beta.T@beta + noise_W@noise_W.T)@beta.T 33 | d_par, U_par = np.linalg.eig(M_par) 34 | 35 | # Modifications to Figure 3 36 | lamb2 = lamb/np.linalg.eigvals(beta@inv(beta.T@beta + noise_W@noise_W.T)@beta.T).min() 37 | M_par2 = Id(2) + lamb2*beta@inv(beta.T@beta + noise_W@noise_W.T)@beta.T 38 | d_par2, U_par2 = np.linalg.eig(M_par2) 39 | 40 | # Save eigengalues for plotting ellipses 41 | np.savetxt("figures/figures3-4/extra-files/figure-3-matrix-eigvals.csv", np.concatenate((np.array([d_par, d_par2]).T, U_par, U_par2), axis=1), delimiter=";") 42 | 43 | # 1) Compute population estimators from parameters 44 | c = {"Y": [0], "X": [1, 2]} 45 | params = pack_params(pars, c, d, noise_W) 46 | 47 | gammas = {"ols": gamma_ols(params), 48 | "par5": gamma_par(params, lamb), 49 | "par10": gamma_par(params, lamb2), 50 | "cross": gamma_cross(params, lamb), 51 | "ar": gamma_ar(params, lamb) 52 | } 53 | 54 | # 2) Simulate interventions for scatter plot 55 | results = {k: [] for k in gammas.keys()} 56 | for intervention_strength in tqdm(np.arange(50)/8): 57 | # Interventions 58 | vs = N(int(8*(intervention_strength + 0.1)**1.1), d['A']) 59 | for v in vs: 60 | # Normalize 61 | v *= intervention_strength # /norm(v) 62 | 63 | # Evaluate estimators in intervened dataset 64 | for method, gamma in gammas.items(): 65 | results[method].append([intervention_strength, 66 | get_mse(simulate(n=50000, d=d, pars=pars, v=v, noise_W=noise_W), gamma), 67 | method, 68 | v]) 69 | 70 | # Convert to dataframe 71 | df = pd.concat(pd.DataFrame(results[method], columns=( 72 | "Strength", "MSE", "Method", "A")) for method in gammas.keys()).reset_index(drop=True) 73 | # Add columns with intervened value to plot in A-space 74 | df = df.join(pd.DataFrame(df.A.tolist(), index=df.index, 75 | columns=[f"A{i}" for i in range(d['A'])])) 76 | df.to_csv("figures/figures3-4/figure-3-data.csv") 77 | -------------------------------------------------------------------------------- /figures/figures3-4/figure4-simulate.py: -------------------------------------------------------------------------------- 1 | ### Loading libraries 2 | import numpy as np 3 | import pandas as pd 4 | from tqdm import tqdm 5 | 6 | # Load file tools from parent folder 7 | import os, sys, inspect 8 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 9 | parentdir = os.path.dirname(currentdir) 10 | sys.path.insert(0,parentdir) 11 | from tools import Id, simulate, ols, ar, N, get_mse, tar 12 | 13 | 14 | ### Set seed 15 | np.random.seed(1) 16 | 17 | ### Dimensions 18 | d = {"A": 2, "W": 2, 'Z': 2, "Y": 1, "X": 2, "H": 1} 19 | d['O'] = d['X'] + d['Y'] + d['H'] 20 | 21 | ### Parameters 22 | beta = Id(d['A'], d['W']) 23 | pars = {'M': np.random.poisson(lam=2, size=(d['O'], d['A'])), 24 | 'B': np.random.normal(size=(d['O'], d['O']))/3, 25 | 'beta': beta} 26 | np.fill_diagonal(pars['B'], 0) 27 | noise_W = np.array([[0.5, 0], [-0.8, 1.5]]) 28 | 29 | 30 | 31 | # 1) Simulate 32 | n = 100000 33 | data = simulate(n, d, pars, noise_W=noise_W) 34 | A, X, Y, W, Z = data['A'], data['X'], data['Y'], data['W'], data['Z'] 35 | 36 | ### Simulation setups 37 | # rotat = np.diag([np.sqrt(2), 1]) 38 | rotat = np.array([[1.6, 0.8], 39 | [-0.8, .5]]) 40 | shift = np.array([3, -3]) 41 | 42 | 43 | # Set lambda 44 | eta = shift.reshape(-1, 1) 45 | lamb = np.linalg.eigvals(eta@eta.T).max() - 1 46 | 47 | ### Save matrix eigendecomposition for plotting in R 48 | radius, U = np.linalg.eig(rotat@rotat.T) 49 | np.savetxt("figures/figures3-4/extra-files/figure-4-matrix-eigvals.csv", np.concatenate((np.array([shift, radius, np.array([lamb,0])]).T, U), axis=1), delimiter=";") 50 | 51 | 52 | 53 | # 1) Compute population estimators 54 | gamma_tar, alpha_tar = tar(X, Y, A, Sigma = rotat@rotat.T, nu=shift) 55 | gammas = {"ols": ols(X, Y), 56 | "tar": gamma_tar, 57 | "ar": ar(X, Y, A, lamb=lamb), 58 | } 59 | 60 | 61 | 62 | # 2) Simulate interventions for scatter plot 63 | results = {k: [] for k in gammas.keys()} 64 | for intervention_strength in tqdm(np.arange(80)/8): 65 | # Interventions 66 | vs = N(int(np.round(8*(intervention_strength + 0.1)**1.1)), d['A']) 67 | for v in vs: 68 | # Normalize 69 | v *= intervention_strength # /norm(v) 70 | 71 | # Evaluate estimators in intervened dataset 72 | for method, gamma in gammas.items(): 73 | results[method].append([intervention_strength, 74 | get_mse(simulate(n=1000, d=d, pars=pars, v = v, cov_A = rotat@rotat.T), gamma, (alpha_tar if method=="tar" else 0)), 75 | method, 76 | v]) 77 | 78 | # Convert to dataframe 79 | df = pd.concat(pd.DataFrame(results[method], columns=( 80 | "Strength", "MSE", "Method", "A")) for method in gammas.keys()).reset_index(drop=True) 81 | # Add columns with intervened value to plot in A-space 82 | df = df.join(pd.DataFrame(df.A.tolist(), index=df.index, 83 | columns=[f"A{i}" for i in range(d['A'])])) 84 | df.to_csv("figures/figures3-4/figure-4-data.csv") 85 | -------------------------------------------------------------------------------- /figures/figures3-4/figures3-4-plot.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(grid) 3 | set.seed(1) 4 | 5 | # Load data 6 | df1 = read_csv("figure-3-data.csv") 7 | df2 = read_csv("figure-4-data.csv") %>% subset(Method=="tar") 8 | 9 | # Combine (because OLS data in figure 3 is the same as in figure 4) 10 | df <- rbind(df1, df2) 11 | method.labels <- c('ols' = "OLS", 12 | 'ar' = "AR($A$) = xPAR($W,Z$)", 13 | 'par5' = "${PAR}_{\\lambda_1}(W$)", 14 | 'par10' = "${PAR}_{\\lambda_2}(W$)", 15 | 'tar' = "TAR($A$)" 16 | ) 17 | region.labels <- c("ols"="$C_{OLS}$", 18 | "ar"="$C_A(\\lambda_1) = C_{W,Z}(\\lambda_1)$", 19 | "par5"="$C_W(\\lambda_1)$", 20 | "par10"="$C_W(\\lambda_2)$", 21 | "tar"="Targeted distr.") 22 | 23 | #### Functions for making circles and ellipses 24 | ellipse <- function(center = c(0, 0), a = 1, b = 1, npoints=100, method="ols", rotation=diag(2), lty="solid"){ 25 | xx = seq(-a, a, length.out = npoints) 26 | yy = b/a * sqrt(a**2 - xx**2) 27 | out = t(rotation%*%rbind(c(xx, rev(xx)), c(yy, -rev(yy)))) 28 | out = sweep(out, 2, center, '+') 29 | return(data.frame(x=out[,1], y=out[,2], Method=method, lty=lty)) 30 | } 31 | 32 | # The theoretical ellipse is specified by eigvals and eigenvectors of E[AAT] + lamb*Omega_W 33 | matrix.info <- read.table("extra-files/figure-3-matrix-eigvals.csv", sep=";", header=F) 34 | radius.par5 = sqrt(matrix.info[,1]) 35 | radius.par10 = sqrt(matrix.info[,2]) 36 | 37 | U5 = as.matrix(matrix.info[,3:4]) 38 | U10 = as.matrix(matrix.info[,5:6]) 39 | 40 | # Compute guarantee sets 41 | regions <- rbind(ellipse(lty="ols"), # OLS 42 | ellipse(a=sqrt(1+5), b=sqrt(1+5), lty="ar"), #OLS <- AR 43 | ellipse(a=radius.par5[1],b=radius.par5[2], method="par5", rotation=U5, lty="par5"), #PAR5 44 | ellipse(a=sqrt(1+5), b=sqrt(1+5), method="par5", lty="ar"), #PAR5 <- AR 45 | ellipse(a=radius.par10[1],b=radius.par10[2], method="par10", rotation=U10, lty="par10"), #PAR10 46 | ellipse(a=sqrt(1+5), b=sqrt(1+5), method="par10", lty="ar"), #PAR10 <- AR 47 | ellipse(a=sqrt(1+5), b=sqrt(1+5), method="ar", lty="ar")) #AR 48 | regions$Method <- factor(regions$Method, levels=names(method.labels)) 49 | regions$lty <- factor(regions$lty, levels=c("ols", "par5", "par10", "ar")) 50 | 51 | regions$Primary <- (as.character(regions$lty) == as.character(regions$Method)) 52 | 53 | # The theoretical ellipse is specified by eigvals and eigenvectors of E[AAT] + lamb*Omega_W 54 | matrix.info.target <- read.table("extra-files/figure-4-matrix-eigvals.csv", sep=";", header=F) 55 | shift = as.vector(matrix.info.target[,1]) 56 | radius = as.vector(matrix.info.target[,2]) 57 | lamb = as.vector(matrix.info.target[,3])[1] 58 | rotat = as.matrix(matrix.info.target[,4:5]) 59 | 60 | targ <- ellipse(center=shift, a=sqrt(radius[1]), b=sqrt(radius[2]), rotation=rotat, method="tar", lty="tar") %>% select(-Method) 61 | targ$lty <- factor(targ$lty, levels=names(region.labels)) 62 | targ$Method <- factor(targ$Method, levels=names(region.labels)) 63 | 64 | # Cut points to be inside interval (to cut away whitespace in tikzDevice) 65 | lims = 5 66 | df_ = df %>% 67 | subset(Method %in% c("ols", "par5", "par10", "ar")) %>% 68 | subset((-lims < A0) & (A0 < lims) & (-lims < A1) & (A1 < lims)) 69 | 70 | midpoint <- mean(log10(subset(df_, Method=="ols")$MSE))*1.2 71 | 72 | # Order factors for plotting order 73 | df_$Method <- factor(df_$Method, levels=names(method.labels)) 74 | 75 | # Plot 76 | p <- ggplot(df_) + 77 | geom_point(aes(x=A0, y=A1, color=MSE), alpha=1, size=2) + 78 | geom_path(data=regions, aes(x,y, lty = lty, alpha=Primary), size=0.6, color="#000066", show.legend = T) + 79 | labs(x = "$\\nu_1$", y="$\\nu_2$")+ 80 | theme_bw(base_size = 9) + 81 | scale_color_gradient2( 82 | low="#1c0f00", mid="#fcf78f", high="#D1654C", 83 | trans = "log10", 84 | midpoint = midpoint, 85 | limits = c(1, 10), oob=scales::squish, 86 | ) + 87 | scale_linetype_manual(values=c("solid", "11", "22", "33"), 88 | breaks = c("ols", "ar", "par5", "par10"), 89 | labels=as_labeller(region.labels) 90 | )+ 91 | scale_x_continuous(breaks = c(-5, 0, 5)) + scale_y_continuous(breaks=c(-5, 0, 5)) + 92 | scale_alpha_manual(values=c(0.4, 1), name=NULL, breaks=NULL) + 93 | guides(lty=guide_legend(title=NULL), 94 | alpha=guide_legend(title=NULL), 95 | color=guide_colorbar(order=1, title="MSPE", 96 | barheight=unit(1.5, "cm") 97 | )) + 98 | coord_fixed(ratio = 1, xlim=c(-lims, lims), ylim = c(-lims, lims)) + 99 | theme(panel.grid.minor = element_blank(), 100 | plot.title = element_blank(), 101 | legend.spacing.x = unit(0.05, 'cm'), 102 | legend.text = element_text(margin = margin(l = 2, unit = "pt")), 103 | legend.margin=margin(-10,0,0,0), 104 | plot.margin = margin(0, 0, 0, -5), 105 | legend.title.align = 0) + 106 | facet_wrap(~Method, ncol=5, labeller=as_labeller(method.labels)) 107 | print(p) 108 | 109 | 110 | ### Target AR plot 111 | # Cut points to be inside interval 112 | lims = 5 113 | df_ = df %>% 114 | subset(Method %in% c("tar", "ols")) %>% 115 | subset((-lims < A0) & (A0 < lims) & (-lims < A1) & (A1 < lims)) 116 | 117 | # Order factors for plotting order 118 | df_$Method <- factor(df_$Method, levels=names(method.labels)) 119 | 120 | p <- ggplot(df_) + 121 | geom_point(aes(x=A0, y=A1, color=MSE), alpha=1, size=2) + 122 | geom_path(data=subset(regions, Method=="ols" & lty=="ols"), aes(x,y, lty = lty), size=0.6, color="#000066", alpha = 1, show.legend = T) + 123 | geom_path(data=targ, aes(x,y, lty=lty), size=0.6, color="#000066", alpha = 1, show.legend = T) + 124 | geom_point(aes(x=0, y=0), shape=4, size=0.8, color="#000066", show.legend=F) + 125 | labs(x = "$\\nu_1$", y="$\\nu_2$")+ 126 | theme_bw(base_size = 9) + 127 | scale_color_gradient2( 128 | low="#1c0f00", mid="#fcf78f", high="#D1654C", 129 | trans = "log10", 130 | midpoint = midpoint, 131 | limits = c(1, 10), oob=scales::squish, 132 | ) + 133 | scale_linetype_manual(values=c("solid", "11", "22", "33", "11"), 134 | breaks = c("ols", "ar", "par5", "par10", "tar"), 135 | labels=as_labeller(region.labels) 136 | )+ 137 | scale_x_continuous(breaks = c(-5, 0, 5)) + scale_y_continuous(breaks=c(-5, 0, 5)) + 138 | guides(lty=guide_legend(title=NULL), 139 | color=guide_colorbar(order=1, title="MSPE", 140 | barheight=unit(1.5, "cm") 141 | )) + 142 | coord_fixed(ratio = 1, xlim=c(-lims, lims), ylim = c(-lims, lims)) + 143 | theme(panel.grid.minor = element_blank(), 144 | plot.title = element_blank(), 145 | legend.spacing.x = unit(0.05, 'cm'), 146 | legend.text = element_text(margin = margin(l = 2, unit = "pt")), 147 | legend.margin=margin(-10,0,0,0), 148 | plot.margin = margin(0, 0, 0, -5), 149 | legend.title.align = 0) + 150 | facet_wrap(~Method, ncol=5, labeller=as_labeller(method.labels)) 151 | print(p) -------------------------------------------------------------------------------- /figures/population_estimators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | inv = np.linalg.inv; Id = lambda d, d2=None: np.eye(d, d2) 3 | """ 4 | For some experiments we need the population versions of estimators instead of sample estimators. 5 | This file has functions to such estimators, based on moments that are derived from parameter matrices. 6 | """ 7 | 8 | def pack_params(pars, c, d, noise_W): 9 | """ 10 | The population estimators are computed based on moments. 11 | We compute the moments based on the parameters MB and the noise distributions. 12 | 13 | In the naming below, we the variable XW corresponds to E[XW^T] etc. 14 | We let O denote the stacked outcome (Y, X, W) 15 | """ 16 | # Unpack inputs. c is a vector containing indices (e.g. O_1 is Y, O_2 is X_1,...) 17 | cY = c['Y']; cX = c['X'] # input 18 | beta, M, B = pars['beta'], pars['M'], pars['B'] 19 | 20 | # Store the inverse of the matrix (Id - B) 21 | IB = inv(Id(d['O']) - B) 22 | 23 | # Compute moments relating to the outcome O 24 | OA = IB@M 25 | OW = IB@M@beta 26 | OO = IB@(M@M.T + Id(d['O']))@IB.T 27 | 28 | # Compute moment E[WW^T] 29 | if len(np.shape(noise_W)) == 0: 30 | WW = beta.T@beta + noise_W**2*Id(d['W']) 31 | else: 32 | WW = beta.T@beta + noise_W@noise_W.T 33 | 34 | # Compute covariance of A and cross proxies 35 | AA = Id(d['A']) 36 | ZW = beta.T@beta 37 | #Covariances relating to X 38 | XX = OO[cX][:,cX]; XY = OO[cX][:,cY]; XW = OW[cX]; XA = OA[cX]; XZ = XW 39 | # Covariances relating to Y 40 | YW = OW[cY]; YA = OA[cY]; YZ = YW 41 | 42 | # Return dict with all moments 43 | return {"IB": IB, "OA": OA, "OW":OW, "OO":OO, "WW":WW, "AA":AA, "ZW":ZW,"XX":XX, 44 | "XY":XY, "XW":XW, "XA":XA, "XZ":XZ,"YW":YW, "YA":YA, "YZ":YZ, "M":M} 45 | 46 | # OLS 47 | def gamma_ols(params): 48 | # Unpack moments 49 | XX, XY = params['XX'], params['XY'] 50 | # Return estimator based on moments 51 | return inv(XX)@XY 52 | 53 | # Proxy anchor regression 54 | def gamma_par(params, lamb): 55 | # Unpack moments 56 | XX, XW, WW, XY, YW = params['XX'], params['XW'], params['WW'], params['XY'], params['YW'] 57 | # Return estimator based on moments 58 | return inv(XX + lamb*XW@inv(WW)@XW.T)@(XY + lamb*XW@inv(WW)@YW.T) 59 | 60 | # Anchor regression 61 | def gamma_ar(params, lamb): 62 | # Unpack moments 63 | XX, AA, XY, XA, YA = params['XX'], params['AA'], params['XY'], params['XA'], params['YA'] 64 | # Return estimator based on moments 65 | return inv(XX + lamb*XA@inv(AA)@XA.T)@(XY + lamb*XA@inv(AA)@YA.T) 66 | 67 | def gamma_cross(params, lamb): 68 | # Unpack moments 69 | XX, XW, ZW, XZ, XY, YW, YZ = params['XX'], params['XW'], params['ZW'], params['XZ'], params['XY'], params['YW'], params['YZ'] 70 | # Compute "denominator" (left-side inverse) 71 | denom = 2*XX + lamb*(XW@inv(ZW)@XZ.T + XZ@inv(ZW).T@XW.T) 72 | # Compute "numerator" 73 | num = 2*XY + lamb*(XW@inv(ZW)@YZ.T + XZ@inv(ZW).T@YW.T) 74 | return inv(denom)@num 75 | 76 | def get_mse_v(gamma, v, params, c): 77 | """Compute the population mse of using an estimator gamma""" 78 | # Unpack 79 | M, IB = params['M'], params['IB'] 80 | cY, cX = c['Y'], c['X'] 81 | # Compute w_gamma 82 | w_gamma = (IB[cY,] - gamma.T@IB[cX,]).T 83 | # Output population MSE 84 | return (w_gamma.T@M@v@v.T@M.T@w_gamma + w_gamma.T@w_gamma)[0,0] 85 | -------------------------------------------------------------------------------- /figures/tools.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | """ 4 | This file contains implementations of estimators and a function to simulate data, used for experiments in the paper 5 | """ 6 | 7 | ### Define convinience functions 8 | inv = np.linalg.inv; norm = np.linalg.norm; Id = lambda d, d2=None: np.eye(d, d2) 9 | # Column and row bind 10 | def cb(*args): return np.concatenate(args, axis=1) 11 | def rb(*args): return np.concatenate(args, axis=0) 12 | 13 | # Multivariate gaussian 14 | N = lambda d, n=1: np.random.normal(size=(d, n)) 15 | 16 | ### Simulate function 17 | def simulate(n, d, pars, v=None, shift=0, cov_A=None, noise_W=None, noise_Z=None): 18 | """ Simulate data from parameters. Dimensions d_A x n, etc. """ 19 | # Unpack 20 | d_A = d['A']; d_W = d['W']; d_X = d['X']; d_Y = d['Y']; d_O = d['O']; d_Z = d['Z'] 21 | M = pars['M']; B = pars['B']; beta = pars['beta'] 22 | 23 | # If no noise is provided, use spherical unit variance as proxy noise 24 | if noise_W is None: noise_W = Id(d_W) 25 | elif len(np.shape(noise_W)) == 0: noise_W = noise_W*Id(d_W) 26 | # If no noise for secondary proxy is supplied, use the same as W 27 | if noise_Z is None: noise_Z = noise_W 28 | elif len(np.shape(noise_Z)) == 0: noise_Z = noise_Z*Id(d_Z) 29 | 30 | # If no parameter beta_z is provided, use same as beta_W 31 | if "beta_z" in pars.keys(): beta_z = pars['beta_z'] 32 | else: beta_z = beta 33 | 34 | # If covariance matrix for A is given, use this, else use spherical noise 35 | # Since changed covariance matrices are only used for targeted, assumes also a v is given 36 | if cov_A is not None: 37 | A = np.random.multivariate_normal(v, cov=cov_A, size=n).T 38 | else: 39 | # Use either the intervention v tiled several times (fixed A), or a mean-zero gaussian 40 | A = (N(d_A, n) if v is None else np.tile(np.reshape(v, (d_A, 1)), n)) + shift 41 | # Compute the outcome O = (Y, X, H) 42 | O = inv(Id(d['O'])-B)@(M@A + N(d_O, n)) 43 | Y, X, H = np.split(O, [d_Y, d_Y+d_X]) 44 | #Simulate proxies 45 | W = beta.T@A + noise_W@N(d_W, n) 46 | Z = beta_z.T@A + noise_Z@N(d_Z, n) 47 | return {'A': A, 'W': W, 'Y': Y, 'X': X, 'H': H, 'Z': Z} 48 | 49 | # Mean function 50 | def E(X): 51 | return X.mean(axis=1).reshape(-1, 1) 52 | 53 | ### Estimators 54 | # Ordinary least squares 55 | def ols(X, Y, intercept=False): 56 | if intercept: 57 | X = np.concatenate((np.ones((1, X.shape[1])), X)) 58 | return inv(X@X.T)@X@Y.T 59 | 60 | # Anchor regression estimator 61 | def ar(X, Y, A, lamb=1, intercept=False): 62 | if intercept: 63 | X = np.concatenate((np.ones((1, X.shape[1])), X)) 64 | return inv(X@X.T + lamb*X@A.T@inv(A@A.T)@A@X.T)@(X@Y.T + lamb*X@A.T@inv(A@A.T)@A@Y.T) 65 | 66 | # Cross estimator 67 | def cross(X, Y, W, Z, lamb=1): 68 | ZW = inv(Z@W.T) 69 | denom = 2*X@X.T + lamb*(X@W.T@ZW@Z@X.T + X@Z.T@ZW.T@W@X.T) 70 | num = 2*X@Y.T + lamb*(X@W.T@ZW@Z@Y.T + X@Z.T@ZW.T@W@Y.T) 71 | return inv(denom)@num 72 | 73 | # Targeted anchor regression, targeted to covariance Sigma and mean shift nu 74 | def tar(X, Y, A, Sigma, nu=0): 75 | # Get dimensions 76 | d_A, n = A.shape 77 | if len(np.shape(nu)) == 0: 78 | nu = np.tile(nu, d_A).reshape(d_A, 1) 79 | 80 | # Compute alpha and gamma 81 | gamma = inv(X@X.T/n + X@A.T@inv(A@A.T)@(Sigma - A@A.T/n)@inv(A@A.T)@A@X.T)@(X@Y.T/n + X@A.T@inv(A@A.T)@(Sigma - A@A.T/n)@inv(A@A.T)@A@Y.T) 82 | alpha = (Y - gamma.T@X)@A.T@inv(A@A.T)@nu 83 | return gamma, alpha 84 | 85 | # IV estimator 86 | def iv(X, Y, A): 87 | return inv(X@A.T@inv(A@A.T)@A@X.T)@X@A.T@inv(A@A.T)@A@Y.T 88 | 89 | # Function to evaluate the prediction MSE of a dataset and some gamma 90 | def get_mse(data, gamma, alpha = 0): 91 | return ((data['Y'] - gamma.T@data['X'] - alpha)**2).mean() 92 | -------------------------------------------------------------------------------- /real-data-experiment/README.md: -------------------------------------------------------------------------------- 1 | # Executing real-data experiment 2 | 3 | Dependencies: 4 | * pandas, numpy, scipy, scikit-learn 5 | * seaborn, matplotlib 6 | 7 | 1. Execute scripts in this order 8 | * `run_exp_temp.py` 9 | * `run_exp_temp_proxies.py` 10 | 11 | 2. Run the notebooks 12 | * `results_tempC.ipynb` 13 | * `results_tempC_proxies.ipynb` 14 | * `results_tempC_figure12.ipynb` 15 | -------------------------------------------------------------------------------- /real-data-experiment/anchorRegression.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from sklearn.base import RegressorMixin, BaseEstimator 4 | from sklearn.linear_model._base import LinearModel 5 | from sklearn.utils.validation import check_X_y, check_array, check_is_fitted 6 | 7 | # Convenience functions 8 | inv = np.linalg.inv 9 | 10 | class AnchorRegression(LinearModel): 11 | def __init__(self, lamb=1, fit_intercept=False, normalize=False, copy_X=False): 12 | self.lamb = lamb 13 | self.fit_intercept=fit_intercept 14 | self.normalize=normalize 15 | self.copy_X = copy_X 16 | 17 | def fit(self, X, y, A=None): 18 | X, y = self._validate_data(X, y, y_numeric=True) 19 | 20 | X, y, X_offset, y_offset, X_scale = self._preprocess_data( 21 | X, y, fit_intercept=self.fit_intercept, normalize=self.normalize, 22 | copy=self.copy_X, sample_weight=None, 23 | return_mean=True) 24 | 25 | if type(A) is not np.ndarray: 26 | A = A.values 27 | 28 | # Center A 29 | A = A - A.mean(axis=0) 30 | 31 | self.coef_ = \ 32 | inv(X.T@X + self.lamb*X.T@A@inv(A.T@A)@A.T@X)@( 33 | X.T@y + self.lamb*X.T@A@inv(A.T@A)@A.T@y) 34 | 35 | self._set_intercept(X_offset, y_offset, X_scale) 36 | 37 | self.is_fitted_ = True 38 | return self 39 | 40 | class CrossProxyAnchorRegression(LinearModel): 41 | def __init__(self, lamb=1, fit_intercept=False, normalize=False, copy_X=False): 42 | self.lamb = lamb 43 | self.fit_intercept=fit_intercept 44 | self.normalize=normalize 45 | self.copy_X = copy_X 46 | 47 | def fit(self, X, y, W, Z): 48 | X, y = self._validate_data(X, y, y_numeric=True) 49 | 50 | X, y, X_offset, y_offset, X_scale = self._preprocess_data( 51 | X, y, fit_intercept=self.fit_intercept, normalize=self.normalize, 52 | copy=self.copy_X, sample_weight=None, 53 | return_mean=True) 54 | 55 | if type(W) is not np.ndarray: 56 | W = W.values 57 | if type(Z) is not np.ndarray: 58 | Z = Z.values 59 | 60 | # Center W 61 | W = W - W.mean(axis=0) 62 | Z = Z - Z.mean(axis=0) 63 | 64 | # Transpose to align with formatting of synth experiments 65 | W = W.T; Z = Z.T; X = X.T; Y = y.T 66 | 67 | ZW = inv(Z@W.T) 68 | denom = 2*X@X.T + self.lamb*(X@W.T@ZW@Z@X.T + X@Z.T@ZW.T@W@X.T) 69 | num = 2*X@Y.T + self.lamb*(X@W.T@ZW@Z@Y.T + X@Z.T@ZW.T@W@Y.T) 70 | self.coef_ = inv(denom)@num 71 | 72 | self._set_intercept(X_offset, y_offset, X_scale) 73 | self.is_fitted_ = True 74 | return self 75 | 76 | class TargetedAnchorRegression(LinearModel): 77 | def __init__(self, fit_intercept=False, normalize=False, copy_X=False): 78 | self.fit_intercept=fit_intercept 79 | self.normalize=normalize 80 | self.copy_X = copy_X 81 | 82 | def fit(self, X, y, A=None, nu=None): 83 | ''' 84 | Targeted shift where nu is the shifted A 85 | ''' 86 | X, y = self._validate_data(X, y, y_numeric=True) 87 | 88 | X, y, X_offset, y_offset, X_scale = self._preprocess_data( 89 | X, y, fit_intercept=self.fit_intercept, normalize=self.normalize, 90 | copy=self.copy_X, sample_weight=None, 91 | return_mean=True) 92 | 93 | if type(A) is not np.ndarray: 94 | A = A.values 95 | 96 | # Center columns of A and nu, with respect to A 97 | n, d_A = A.shape 98 | mean_A = A.mean(axis=0) 99 | A = A - mean_A 100 | nu = nu - mean_A 101 | 102 | Sig_A = np.cov(A.T, bias=True) 103 | Sig_nu = np.cov(nu.T, bias=True) 104 | mean_nu = np.mean(nu, axis=0).T 105 | 106 | if len(np.shape(Sig_A)) == 0: 107 | Sig_A = np.tile(Sig_A, d_A).reshape(d_A, 1) 108 | Sig_nu = np.tile(Sig_nu, d_A).reshape(d_A, 1) 109 | 110 | # Transpose to align with formatting of synth experiments 111 | A = A.T; X = X.T; Y = y.T 112 | 113 | Omega = inv(A@A.T)@(Sig_nu - Sig_A)@inv(A@A.T) 114 | 115 | gamma = inv(X@X.T/n + X@A.T @ Omega @ A@X.T)@( 116 | X@Y.T/n + X@A.T @ Omega @ A@Y.T) 117 | alpha = (Y - gamma.T@X)@A.T@inv(A@A.T)@mean_nu 118 | 119 | self.coef_ = gamma 120 | self.intercept_ = y_offset + alpha 121 | 122 | self.is_fitted_ = True 123 | return self 124 | 125 | class CrossTargetedAnchorRegression(LinearModel): 126 | def __init__(self, fit_intercept=False, normalize=False, copy_X=False): 127 | self.fit_intercept=fit_intercept 128 | self.normalize=normalize 129 | self.copy_X = copy_X 130 | 131 | def fit(self, X, y, W=None, nu=None, Z=None): 132 | ''' 133 | Targeted shift where nu is the shifted W 134 | ''' 135 | X, y = self._validate_data(X, y, y_numeric=True) 136 | 137 | X, y, X_offset, y_offset, X_scale = self._preprocess_data( 138 | X, y, fit_intercept=self.fit_intercept, normalize=self.normalize, 139 | copy=self.copy_X, sample_weight=None, 140 | return_mean=True) 141 | 142 | if type(W) is not np.ndarray: 143 | W = W.values 144 | 145 | # Center columns of W and nu, with respect to W 146 | n, d_W = W.shape 147 | mean_W = W.mean(axis=0) 148 | W = W - mean_W 149 | nu = nu - mean_W 150 | 151 | Sig_W = np.cov(W.T, bias=True) 152 | Sig_nu = np.cov(nu.T, bias=True) 153 | mean_nu = np.mean(nu, axis=0).T 154 | 155 | if len(np.shape(Sig_W)) == 0: 156 | Sig_W = np.tile(Sig_W, d_W).reshape(d_W, 1) 157 | Sig_nu = np.tile(Sig_nu, d_W).reshape(d_W, 1) 158 | 159 | # Transpose to align with formatting of synth experiments 160 | Z = Z.T; W = W.T; X = X.T; Y = y.T 161 | 162 | Omega = inv(W@Z.T)@(Sig_nu - Sig_W)@inv(Z@W.T) 163 | 164 | gamma = inv(X@X.T/n + X@W.T @ Omega @ W@X.T)@( 165 | X@Y.T/n + X@W.T @ Omega @ W@Y.T) 166 | alpha = (Y - gamma.T@X)@Z.T@inv(W@Z.T)@mean_nu 167 | 168 | self.coef_ = gamma 169 | self.intercept_ = y_offset + alpha 170 | 171 | self.is_fitted_ = True 172 | return self 173 | 174 | 175 | class MeanPredictor(BaseEstimator): 176 | def __init__(self): 177 | pass 178 | 179 | def fit(self, X, y): 180 | X, y = check_X_y(X, y, accept_sparse=True) 181 | self.mean_ = y.mean() 182 | 183 | self.is_fitted_ = True 184 | # `fit` should always return `self` 185 | return self 186 | 187 | def predict(self, X): 188 | X = check_array(X, accept_sparse=True) 189 | check_is_fitted(self, 'is_fitted_') 190 | return np.ones(X.shape[0], dtype=np.int64) * self.mean_ 191 | -------------------------------------------------------------------------------- /real-data-experiment/data/README.md: -------------------------------------------------------------------------------- 1 | # Data Folder 2 | 3 | Download data from https://archive.ics.uci.edu/ml/datasets/PM2.5+Data+of+Five+Chinese+Cities 4 | 5 | Place into this folder. Relevant files are: 6 | * 'BeijingPM20100101_20151231.csv' 7 | * 'GuangzhouPM20100101_20151231.csv' 8 | * 'ShenyangPM20100101_20151231.csv' 9 | * 'ChengduPM20100101_20151231.csv' 10 | * 'ShanghaiPM20100101_20151231.csv' 11 | -------------------------------------------------------------------------------- /real-data-experiment/figs/README.md: -------------------------------------------------------------------------------- 1 | # Figures 2 | 3 | Once the relevant notebooks are run, this will contain two figures 4 | * (Figure 11): best_case_performance_TempC.pdf 5 | * (Figure 12): coefficient_comparison.pdf 6 | 7 | -------------------------------------------------------------------------------- /real-data-experiment/notebooks/results_tempC.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tables 1, 2, and Figure 11 (Temperature as proxy)\n", 8 | "\n", 9 | "Builds the portions of Tables 1, 2 that do not include W, Z\n", 10 | "\n", 11 | "Requires that `run_exp_temp.py` is run" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "RPATH = '../results'\n", 21 | "FPATH = '../figs'\n", 22 | "\n", 23 | "import numpy as np\n", 24 | "import pandas as pd\n", 25 | "import seaborn as sns\n", 26 | "import matplotlib.pyplot as plt\n", 27 | "import pickle as pkl" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 2, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "results = pd.read_csv(f\"{RPATH}/all_res_test_TempC.csv\")\n", 37 | "results = results.drop('Unnamed: 0', axis=1)\n", 38 | "\n", 39 | "residuals = results.drop(\"Lambda\", axis=1).query('Environment == \"Test\"')\n", 40 | "lambs = results.drop(\"Residual\", axis=1).query('Environment == \"Test\"')\n", 41 | "\n", 42 | "mse = lambda v: np.mean(v**2)\n", 43 | "\n", 44 | "# Get RMSE by season, city\n", 45 | "pt = pd.pivot_table(residuals, \n", 46 | " index=['Test_Season', 'City'], \n", 47 | " columns = 'Estimator', \n", 48 | " aggfunc={'Residual': mse})\n", 49 | "\n", 50 | "pt.columns = pt.columns.droplevel(0)\n", 51 | "pt = pt.rename(columns = {\n", 52 | " 'OLS (All)': 'OLS (TempC)', \n", 53 | " 'PA (TempC)': 'OLS + Est. Bias',\n", 54 | " 'TAR (TempC)': 'PTAR (TempC)', \n", 55 | " 'AR (TempC)': 'PAR (TempC)'\n", 56 | "})\n", 57 | "newcols = [\n", 58 | " 'OLS',\n", 59 | " 'OLS (TempC)',\n", 60 | " 'OLS + Est. Bias',\n", 61 | " 'PAR (TempC)',\n", 62 | " 'PTAR (TempC)'\n", 63 | "]\n", 64 | "pt = pt[newcols].reset_index()" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "# Table 1" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 3, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "wins = lambda v: int(np.sum(v < 0))\n", 81 | "loss = lambda v: int(np.sum(v > 0))\n", 82 | "\n", 83 | "lambs_ar = lambs.query(f'Environment == \"Test\" & Estimator == \"AR (TempC)\"').groupby(\n", 84 | " ['City', 'Test_Season']).mean()[['Lambda']]\n", 85 | "\n", 86 | "lambs_ar = lambs_ar.reset_index().set_index(['City', 'Test_Season'])\n", 87 | "\n", 88 | "pt_diff = pt.copy()\n", 89 | "for est in newcols:\n", 90 | " pt_diff[est] = pt[est] - pt['OLS']\n", 91 | "\n", 92 | "pt_pos_lamb = pt.set_index(['City', 'Test_Season']).merge(lambs_ar, left_index=True, right_index=True)\n", 93 | "pt_pos_lamb = pt_pos_lamb.query(\"Lambda > 0\").drop(\"Lambda\", axis=1).reset_index()\n", 94 | "\n", 95 | "pt_diff_pos_lamb = pt_pos_lamb.copy()\n", 96 | "for est in newcols:\n", 97 | " pt_diff_pos_lamb[est] = pt_pos_lamb[est] - pt_pos_lamb['OLS']" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 4, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "\\begin{tabular}{lrrrr}\n", 110 | "\\toprule\n", 111 | "{} & Mean & Win & min & max \\\\\n", 112 | "Estimator & & & & \\\\\n", 113 | "\\midrule\n", 114 | "OLS & 0.537 & 0 & 0.000 & 0.000 \\\\\n", 115 | "OLS (TempC) & 0.536 & 5 & -0.028 & 0.026 \\\\\n", 116 | "OLS + Est. Bias & 0.569 & 4 & -0.072 & 0.150 \\\\\n", 117 | "PAR (TempC) & 0.531 & 6 & -0.041 & 0.006 \\\\\n", 118 | "PTAR (TempC) & 0.525 & 8 & -0.061 & 0.001 \\\\\n", 119 | "\\bottomrule\n", 120 | "\\end{tabular}\n", 121 | "\n" 122 | ] 123 | } 124 | ], 125 | "source": [ 126 | "lt = pd.melt(pt_pos_lamb, id_vars=['Test_Season', 'City'], value_name = 'MSE', var_name = 'Estimator')\n", 127 | "\n", 128 | "mean_result = lt.groupby('Estimator', as_index=True).agg(\n", 129 | " **{'Mean': pd.NamedAgg(column='MSE', aggfunc=np.mean)}\n", 130 | ").reindex(newcols)\n", 131 | "\n", 132 | "pt_diff_long = pd.melt(pt_diff_pos_lamb, id_vars=['Test_Season', 'City'], value_name = 'MSE', var_name='Estimator')\n", 133 | "\n", 134 | "diff_ols = pt_diff_long.groupby('Estimator', as_index=True).agg(\n", 135 | " **{'min': pd.NamedAgg(column='MSE', aggfunc=np.min), \n", 136 | " 'max': pd.NamedAgg(column='MSE', aggfunc=np.max)}\n", 137 | ").reindex(newcols)\n", 138 | "\n", 139 | "win_loss_ols = pt_diff_long.groupby('Estimator', as_index=True).agg(\n", 140 | " **{'Win': pd.NamedAgg(column='MSE', aggfunc=wins)}\n", 141 | ").reindex(newcols)\n", 142 | "\n", 143 | "print(pd.concat([mean_result, win_loss_ols.astype(int), diff_ols], axis=1).to_latex(float_format=\"{:.3f}\".format))" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "## Table 2\n", 151 | "\n", 152 | "This does not include W, Z, see `results_tempC_proxies.ipynb` for those" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 5, 158 | "metadata": { 159 | "scrolled": false 160 | }, 161 | "outputs": [ 162 | { 163 | "name": "stdout", 164 | "output_type": "stream", 165 | "text": [ 166 | "\\begin{tabular}{lrrrr}\n", 167 | "\\toprule\n", 168 | "{} & Mean & Diff & min & max \\\\\n", 169 | "Estimator & & & & \\\\\n", 170 | "\\midrule\n", 171 | "OLS & 0.457 & 0.000 & 0.000 & 0.000 \\\\\n", 172 | "OLS (TempC) & 0.455 & -0.002 & -0.028 & 0.026 \\\\\n", 173 | "OLS + Est. Bias & 0.474 & 0.018 & -0.072 & 0.150 \\\\\n", 174 | "PAR (TempC) & 0.454 & -0.003 & -0.041 & 0.006 \\\\\n", 175 | "PTAR (TempC) & 0.450 & -0.007 & -0.061 & 0.002 \\\\\n", 176 | "\\bottomrule\n", 177 | "\\end{tabular}\n", 178 | "\n" 179 | ] 180 | } 181 | ], 182 | "source": [ 183 | "pt_diff = pt.copy()\n", 184 | "for est in newcols:\n", 185 | " pt_diff[est] = pt[est] - pt['OLS']\n", 186 | "\n", 187 | "lt = pd.melt(pt, id_vars=['Test_Season', 'City'], value_name = 'MSE')\n", 188 | "\n", 189 | "mean_result = lt.groupby('Estimator', as_index=True).agg(\n", 190 | " **{'Mean': pd.NamedAgg(column='MSE', aggfunc=np.mean)}\n", 191 | ").reindex(newcols)\n", 192 | "\n", 193 | "pt_diff_long = pd.melt(pt_diff, id_vars=['Test_Season', 'City'], value_name = 'MSE')\n", 194 | "\n", 195 | "diff_ols = pt_diff_long.groupby('Estimator', as_index=True).agg(\n", 196 | " **{'Diff': pd.NamedAgg(column='MSE', aggfunc=np.mean),\n", 197 | " 'min': pd.NamedAgg(column='MSE', aggfunc=np.min), \n", 198 | " 'max': pd.NamedAgg(column='MSE', aggfunc=np.max)}\n", 199 | ").reindex(newcols)\n", 200 | "\n", 201 | "print(pd.concat([mean_result, diff_ols], axis=1).to_latex(float_format=\"{:.3f}\".format))" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 6, 207 | "metadata": { 208 | "scrolled": true 209 | }, 210 | "outputs": [ 211 | { 212 | "data": { 213 | "text/plain": [ 214 | "'City == 0 & Test_Season == 2'" 215 | ] 216 | }, 217 | "execution_count": 6, 218 | "metadata": {}, 219 | "output_type": "execute_result" 220 | } 221 | ], 222 | "source": [ 223 | "best_city, best_season = pt_diff.sort_values(\n", 224 | " f\"PAR (TempC)\", ascending=True).head(1)[['City', 'Test_Season']].values[0]\n", 225 | "\n", 226 | "best_case_query = f\"City == {best_city} & Test_Season == {best_season}\"\n", 227 | "\n", 228 | "best_case_query" 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "# Figure 11" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 7, 241 | "metadata": {}, 242 | "outputs": [], 243 | "source": [ 244 | "rmse_df = pd.read_csv(f\"{RPATH}/all_rmse_test_TempC.csv\")\n", 245 | "rmse_df = rmse_df.drop('Unnamed: 0', axis=1)" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": 8, 251 | "metadata": {}, 252 | "outputs": [ 253 | { 254 | "data": { 255 | "text/plain": [ 256 | "['AR (TempC)', 'Mean', 'OLS', 'OLS (All)', 'PA (TempC)', 'TAR (TempC)']" 257 | ] 258 | }, 259 | "execution_count": 8, 260 | "metadata": {}, 261 | "output_type": "execute_result" 262 | } 263 | ], 264 | "source": [ 265 | "plot_est = list(rmse_df.groupby('Estimator').mean().index.values)\n", 266 | "plot_est" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": 9, 272 | "metadata": {}, 273 | "outputs": [], 274 | "source": [ 275 | "plot_est = [f for f in plot_est if 'Mean' not in f]" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": 10, 281 | "metadata": {}, 282 | "outputs": [], 283 | "source": [ 284 | "plot_df = rmse_df.query('Environment == \"Test\" & Estimator in @plot_est').copy()\n", 285 | "plot_df['MSE'] = plot_df['RMSE'] **2" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 11, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "best_case = plot_df.query(best_case_query).copy()" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": 12, 300 | "metadata": {}, 301 | "outputs": [], 302 | "source": [ 303 | "order = [\n", 304 | " 'OLS',\n", 305 | " 'AR (TempC)',\n", 306 | " 'TAR (TempC)'\n", 307 | "]\n", 308 | "\n", 309 | "labels = [\n", 310 | " 'OLS',\n", 311 | " 'PAR (TempC)',\n", 312 | " 'PTAR (TempC)' \n", 313 | "]" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": 13, 319 | "metadata": {}, 320 | "outputs": [ 321 | { 322 | "data": { 323 | "image/png": "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\n", 324 | "text/plain": [ 325 | "
" 326 | ] 327 | }, 328 | "metadata": { 329 | "needs_background": "light" 330 | }, 331 | "output_type": "display_data" 332 | } 333 | ], 334 | "source": [ 335 | "plt.rcParams.update({'font.size': 20})\n", 336 | "fig, ax = plt.subplots(ncols = 1, sharey = True, figsize=(10, 10))\n", 337 | "sns.boxplot(y = 'Estimator', x='MSE', order = order, data=best_case, ax=ax)\n", 338 | "ax.set_ylabel(None)\n", 339 | "ax.set_yticklabels(labels)\n", 340 | "plt.tight_layout()\n", 341 | "plt.savefig(f\"{FPATH}/best_case_performance_TempC.pdf\")" 342 | ] 343 | } 344 | ], 345 | "metadata": { 346 | "kernelspec": { 347 | "display_name": "Python 3", 348 | "language": "python", 349 | "name": "python3" 350 | }, 351 | "language_info": { 352 | "codemirror_mode": { 353 | "name": "ipython", 354 | "version": 3 355 | }, 356 | "file_extension": ".py", 357 | "mimetype": "text/x-python", 358 | "name": "python", 359 | "nbconvert_exporter": "python", 360 | "pygments_lexer": "ipython3", 361 | "version": "3.7.9" 362 | } 363 | }, 364 | "nbformat": 4, 365 | "nbformat_minor": 4 366 | } 367 | -------------------------------------------------------------------------------- /real-data-experiment/notebooks/results_tempC_figure12.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Figure 12 (Temperature as proxy)\n", 8 | "\n", 9 | "To get coefficients, we retrain the model, since this is not saved during the larger experiment" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "import pandas as pd\n", 20 | "\n", 21 | "import pdb\n", 22 | "\n", 23 | "from sklearn import linear_model as lm\n", 24 | "from sklearn import preprocessing \n", 25 | "from sklearn import model_selection as ms\n", 26 | "from sklearn import pipeline\n", 27 | "from sklearn.model_selection import train_test_split as tt_split\n", 28 | "\n", 29 | "from scipy.stats import skew\n", 30 | "from scipy.stats.stats import pearsonr\n", 31 | "\n", 32 | "import seaborn as sns\n", 33 | "import matplotlib.pyplot as plt\n", 34 | "\n", 35 | "import sys; sys.path.insert(0, '../')\n", 36 | "\n", 37 | "import utils\n", 38 | "\n", 39 | "from anchorRegression import AnchorRegression as AR\n", 40 | "from anchorRegression import CrossProxyAnchorRegression as XAR\n", 41 | "from anchorRegression import TargetedAnchorRegression as TAR\n", 42 | "from anchorRegression import CrossTargetedAnchorRegression as XTAR\n", 43 | "from anchorRegression import MeanPredictor" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "## 1. Load data" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "Note that the best scenario is determined in `results_tempC.ipynb` " 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 2, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "CITY = 0\n", 67 | "TEST_SEASON = 2\n", 68 | "\n", 69 | "proxy = 'TempC'\n", 70 | "proxies = [proxy]" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 3, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "DATA_PATH = \"../data\"\n", 80 | "\n", 81 | "files = [\n", 82 | " 'BeijingPM20100101_20151231.csv',\n", 83 | " 'GuangzhouPM20100101_20151231.csv',\n", 84 | " 'ShenyangPM20100101_20151231.csv',\n", 85 | " 'ChengduPM20100101_20151231.csv',\n", 86 | " 'ShanghaiPM20100101_20151231.csv'\n", 87 | "]\n", 88 | "\n", 89 | "dfs = [pd.read_csv(f\"{DATA_PATH}/{f}\") for f in files]" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 4, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "raw_df = dfs[CITY].drop('No', axis=1)\n", 99 | "filt_df = raw_df.dropna()\n", 100 | "\n", 101 | "df, X, y = utils.process_df(filt_df)" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 5, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "dev_year = 2013\n", 111 | "drop_season = TEST_SEASON" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 6, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "import utils\n", 121 | "\n", 122 | "data = utils.get_dev_train_test_data(df, X, y, drop_season, dev_year, proxies)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": {}, 128 | "source": [ 129 | "## 2. Fit Estimators" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": 7, 135 | "metadata": {}, 136 | "outputs": [], 137 | "source": [ 138 | "from copy import deepcopy" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 8, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "lr = pipeline.Pipeline([('scaler', preprocessing.StandardScaler()), \n", 148 | " ('pred', lm.LinearRegression(fit_intercept=True, normalize=False))])\n", 149 | "\n", 150 | "baselines = {\n", 151 | " 'OLS': \n", 152 | " {'pipe': deepcopy(lr), \n", 153 | " 'drop_cols': proxies, \n", 154 | " 'fit_params': None}\n", 155 | "}" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 9, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "tar_estimators = utils.construct_tar(data, proxies, drop_all=True)\n", 165 | "ar_estimators = utils.construct_ar(data, proxies, drop_all=True)" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 10, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "estimators = {**baselines,\n", 175 | " **ar_estimators, \n", 176 | " **tar_estimators}" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 11, 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "name": "stdout", 186 | "output_type": "stream", 187 | "text": [ 188 | "AR (TempC): {'pred__lamb': 40.0}\n" 189 | ] 190 | } 191 | ], 192 | "source": [ 193 | "for k, est in estimators.items():\n", 194 | " if 'tune_lambda' in est.keys() and est['tune_lambda']:\n", 195 | " best_lambda = utils.get_best_lambda(est, data)\n", 196 | " \n", 197 | " print(f\"{k}: {best_lambda}\")\n", 198 | " \n", 199 | " estimators[k]['pipe'] = estimators[k]['pipe'].set_params(\n", 200 | " **best_lambda)\n", 201 | " \n", 202 | " estimators[k].update(best_lambda)" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 12, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "for k, est in estimators.items():\n", 212 | " \n", 213 | " perf = {}\n", 214 | " \n", 215 | " # Get cross-validated training errors\n", 216 | " this_X_train = utils.get_estimator_X(data['train']['X'], est)\n", 217 | " this_X_test = utils.get_estimator_X(data['test']['X'], est)\n", 218 | " y_train = data['train']['y']\n", 219 | " y_test = data['test']['y']\n", 220 | "\n", 221 | " preds_train_cv = ms.cross_val_predict(est['pipe'], this_X_train, y_train, fit_params=est['fit_params'], cv=10)\n", 222 | " resid_train_cv = preds_train_cv - y_train\n", 223 | " \n", 224 | " perf['Train (CV)'] = {\n", 225 | " 'preds': preds_train_cv,\n", 226 | " 'resid': resid_train_cv.values\n", 227 | " }\n", 228 | " \n", 229 | " # Train on the full training set \n", 230 | " if est['fit_params'] is not None:\n", 231 | " est['fit'] = est['pipe'].fit(this_X_train, y_train, **est['fit_params'])\n", 232 | " else:\n", 233 | " est['fit'] = est['pipe'].fit(this_X_train, y_train)\n", 234 | " \n", 235 | " # Evaluate on the test set\n", 236 | " preds_test = est['fit'].predict(this_X_test)\n", 237 | " resid_test = preds_test - y_test\n", 238 | "\n", 239 | " perf['Test'] = {\n", 240 | " 'preds': preds_test,\n", 241 | " 'resid': resid_test.values\n", 242 | " }\n", 243 | " \n", 244 | " estimators[k]['perf'] = perf" 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "metadata": {}, 250 | "source": [ 251 | "## Construct Figure 12\n", 252 | "\n", 253 | "First, examine the intercepts, then the coefficients." 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": 13, 259 | "metadata": {}, 260 | "outputs": [ 261 | { 262 | "name": "stdout", 263 | "output_type": "stream", 264 | "text": [ 265 | "Intercept: 4.087 [OLS]\n", 266 | "Intercept: 4.087 [AR (TempC)]\n", 267 | "Intercept: 3.885 [TAR (TempC)]\n" 268 | ] 269 | } 270 | ], 271 | "source": [ 272 | "for k, est in estimators.items():\n", 273 | " if k != 'Mean':\n", 274 | " print(f\"Intercept: {est['fit']['pred'].intercept_:.3f} [{k}]\")" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 14, 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "labels = {\n", 284 | " 'OLS' : 'OLS',\n", 285 | " 'AR (TempC)' : 'PAR (TempC)',\n", 286 | " 'TAR (TempC)' : 'PTAR (TempC)' \n", 287 | "}" 288 | ] 289 | }, 290 | { 291 | "cell_type": "code", 292 | "execution_count": 15, 293 | "metadata": {}, 294 | "outputs": [ 295 | { 296 | "data": { 297 | "image/png": "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LmERLXEPxIwZM/jFL37BPffcwyuvvML8+fPp3bs3++67L+eeey5rrbXWotecMIGNNtqIU089leuuu26JfU+aNIkrr7yS++67jzfffJPllluONddck+23356vfe1rHHzwwc2uX22D4VaSJNXcyy+/zN57782bb77JwQcfzPHHH88KK6zAk08+yS9+8QuGDh3Kn/70J3beeecm9/3mm2+y4447MmPGDI466ihOPvlkAF599VXuvfdeZs2aZbgtEMOtJEmqqdmzZ7Pffvvxzjvv8Kc//Yl99/10FPjEE0/klFNOYY899uCAAw5g7Nixi43gNubKK69k8uTJjBgxggMOOGCRY1dffTVvv/12i7wPtQ3OuZUkSTU1ePBgXn75Zc4666xFgm1J//79+fGPf8yUKVO44oormtz/K6+8AsD/+3//r+Lx9ddfv8l9qu0y3EqSpJr6/e9/D8C3vvWtBtsMGDCAFVZYgbvuuqvJ/W+yySYA/OY3vyGltHRFqt0w3EqSpJp64YUXWGWVVejTp0+Dbbp06ULfvn2ZMGECs2bNalL/55xzDt27d+fss89mww035KijjuKaa65hzJgxzS1dbZDhVpIk1dSMGTPo0aNHo+1KbaZPn96k/jfeeGOee+45Tj31VABuu+02zjrrLPr378/WW29tyC0Yw60kSaqp7t27M2PGjEbbldpUE4Tr6927N9dddx1vvfUW7777LnfccQf77bcfY8eO5atf/SrTpk1rcp9qmwy3kiSppj772c8yY8YMXn311QbbzJ49m3//+9/07t2bbt26Net666yzDocddhh//OMfOfLII5k0aRJ/+ctfmtWn2g7DrSRJqqnSGrM33nhjg22GDx/OJ5980uLr0e60004AvPPOOy3ar2rHcCtJkmrqhBNOoE+fPlx99dXcd999ix3/17/+xXe/+1169erFeeed1+T+R44cyZw5cxbbv2DBAv70pz8BsMUWWzS9cLVJPsRBkiTVVNeuXfnjH//IPvvsw7777sshhxxCXV0dnTp14p///Cc333wz3bp1Y8SIEay99tqLnT969Gh++MMfLra/U6dOXHDBBVx55ZU8/vjj7Lfffmy33Xb06NGDSZMmcddddzFmzBh23333iuvrqn0y3EqSpJrr168fzz//PL/4xS+4++67+ctf/sL8+fPZcMMNOf300zn33HMrBluAp556iqeeemqx/SuttBIXXHABF110EXfeeSePPvoo999/P9OmTaNr167069ePq666ilNPPZXllvOX2UURLmbcMfXv3z+NHj261mVIkpZg/Pjx9OvXr9ZlSMtUNf/dR8SYlFL/Ssf8Z4okSZIKw3ArSZKkwjDcSpIkqTAMt5IkSSoMw60kSZIKw3ArSZKkwjDcSpIkqTAMt5IkSSoMw60kSZIKw3ArSZKkwjDcSpIkqTAMt5IkSSoMw60kSVIVUkrsvPPOHHXUUbUupaaeffZZlltuOUaNGlXrUirqVOsCJEnSUhjUo9YVLGrQ9KU+deTIkey+++6L7OvatSt9+/blmGOO4bTTTmP55Zdf5Phf/vIX9t13X5ZbbjneeOMNNthgg8X6nTBhAhtttNEi+zp37szGG2/MoYceyvnnn0+XLl2qrvP222/n6aefZvjw4QAMGDCAm266qapzL774YgYNGlT1tWrh5Zdf5pprruHhhx/mP//5DwsWLOAzn/kMdXV1fOtb32KHHXYAYJtttuHAAw/knHPO4emnnyYialz5ogy3kiSpTfj617/OV77yFVJKvPvuuwwbNowzzzyTcePGccMNNyzSdsiQIXzmM5/hvffeY+jQoVx88cUN9rvnnntyzDHHADBlyhTuuusuLr30Up544gn+9re/VV3fpZdeyn777cemm24KwEknncQee+yxSJujjz6azTffnAsvvHCR/VtvvXXV16mFwYMHc/LJJ9O5c2e+/vWvs80229CpUydefvll7rrrLn7zm98wbtw4tthiCwDOPPNMdtttt4X/yGhLIqVU6xpUA/3790+jR4+udRmSpCUYP348/fr1q3ywgCO3V1xxBeeee+7C/TNmzKBfv35MnDiRiRMnstZaawFZQF1vvfX4/ve/zzPPPMMzzzzD66+/vtgIYmnk9tRTT+W6665buH/+/PnstNNOjB49mtGjR7P99ts3WuNDDz3EHnvswd13381BBx3UYLuIYLfddmPkyJFN/CnUzoMPPsjee+/NFltswf3338+66667yPF58+Zx7bXXLmwD2RSNjTfemM9+9rP86U9/atF6lvjffS4ixqSU+lc65pxbSZLUJnXv3p2dd96ZlBKvv/76wv0333wz8+bN4+ijj2bAgAFMmDCBhx56qOp+l19+eerq6gB45ZVXqjrnzjvvZPnll2evvfZq0nsoGT16NAcddBA9e/ZkpZVWom/fvvzoRz9i3rx5i7Srq6ujd+/eTJgwgYMOOohVV12V1VZbjQEDBjBr1iwWLFjAj3/8YzbaaCM6d+7Mdtttx+OPP75IHyNHjiQiGDZsGNdeey2bbbYZnTt3ZrPNNuPaa69drLbzzz+flBK/+93vFgu2AJ06deKss85aGGwhC/F777039913H7NmzVqqn0lrcVqCJElqk1JKvPrqqwD07Nlz4f4hQ4aw22670bt3b9Zff33WXHNNhgwZstgUgSV57bXXAFh99dWraj9q1Ci23HJLunbt2oR3kPnLX/7CQQcdRJ8+fTjnnHNYffXVeeKJJ/jBD37As88+y5133rlI+48++ogvfelL/M///A+XX345Tz/9NEOGDGHu3LmsscYaPPXUU5x++un897//5corr2S//fbjzTffZJVVVlmkn2uvvZZJkyZx0kknscoqq3D77bdzxhlnMG3atIXTON544w3+9a9/seuuuy4SXqux88478+tf/5q///3v7LPPPk3+ubQWw63UBmx101a1LkHA2GPH1roEqUObPXs2U6dOJaXExIkTufbaa3nuuefYaaedFs5zfeqppxg3bhxDhw4FslHFI488kuuvv54PPviA1VZbbbF+586dy9SpUwGYOnUqd9xxB/fccw/rr78+u+22W6N1zZ8/n5dffpkDDjigye9p7ty5fPOb3+Tzn/88Dz/8MJ06ZdHrpJNO4nOf+xxnn302I0eOXDiSXKrxO9/5Dueddx4AAwcO5IMPPuCOO+5gu+2244knnmCFFVYAoF+/fhxwwAHcdtttnHTSSYtc++WXX2b8+PGsv/76AJx66qnssssu/PCHP+T4449n/fXX54UXXgCym8SaapNNNgFg3LhxbSrcOi1BkiS1CRdffDG9evVizTXX5HOf+xxDhgxh//33Z8SIEQvbDB48mK5du3LooYcu3Hfccccxd+5cbrvttor9Dh48mF69etGrVy/69evHxRdfzO67785DDz3ESiut1Ghd77//PgsWLKh6lLfcAw88wHvvvcdxxx3Hhx9+yNSpUxe+vvKVrwAsdlPb8ssvz+mnn77Ivl133ZWUEgMHDlwYbEv7ofL0iqOOOmphsAVYccUVOeuss5g3b97CebIzZswAsikgTbXGGmsAMHny5Caf25ocuZUkSW3CiSeeyGGHHUZE0LVrVzbbbLNFAuXs2bP57W9/S11dHZMmTVq4v0uXLvTp04fBgwdz6qmnLtbvAQccwGmnncb8+fN55ZVX+NnPfsZ//vOfqoItsPBGtaW5CX/8+PEAfPOb32ywzXvvvbfI9+ussw6dO3deZF9pRLr+0mal/e+///5i/Va6Kas09aA0h7kUamfOnNnwm2hA6efhUmCSJEkVbLrppkucN3vHHXcwc+ZM7r33Xu69996KbZ599tnFfsW+/vrrL+x377335stf/jJbb701RxxxBP/4xz8aDWdrrLEGyy23HNOmTWvaG+LTAHjFFVc0+Kv/+jdx1V/Tt5pjlYJ3pfdVv91nP/tZAJ555pkGr9mQ0s+jV69eTT63NRluJUlSuzBkyBDWXXddfvGLXyx27JNPPuGYY45h8ODBFVcEKLfJJptw7rnncumll3L77bdz5JFHLrH9csstR79+/apeWaFcaa5w165dm3TDW0t48cUXF9tXGkneeOONgWwkeNttt+Xxxx/npZdeYvPNN6+6/9LNfqWA3FY451aSJLV5L7/8Mo899hiHHHIIhx566GKvI488kl133ZXbbruNjz/+uNH+zjrrLHr06MEll1zC/PnzG21fV1fH+PHjF85Rrdbee+/NmmuuyeWXX15x5HfOnDlLNSWgGrfeeitvv/32wu8/+eQTrr76apZffnm++tWvLtz/05/+FIAjjjhikekeJfPnz+eaa65ZLCw/+eSTdOrUiS9+8YutUv/SMtxKkqQ2b8iQIQAccsghDbY55JBDmDZtGvfcc0+j/a266qqcdtppvPzyyw3eiFbusMMOY8GCBdx3333VF002Yjt8+HAmT55M3759Of/88/nNb37DFVdcwfHHH8+6667LmDFjmtRntTbbbDM+//nPc9lll3HNNdewyy678PTTT/O9732Pz3zmMwvb7bnnntxwww28+OKL9O3bl4EDB3L99ddz4403csEFF7D55ptz9tlnL9J3Som//vWv7LPPPnTr1q1V6l9ahltJktSmzZ8/n+HDh9OrVy922WWXBtsddNBBRMTCINyYs846i27dunHppZc2Onq72267scUWW3DzzTc3qXbIRm+ffvpp9t57b2655RZOPfVUrrzySsaPH8/ZZ5/dao/mPf300zn//PMZPnw4559/Ph988AHXXHMNl1566WJtjz/+eMaOHcuRRx7Jww8/zNlnn81pp53GPffcw5e+9CXGjBmzyDq4jz76KG+99RYDBw5sldqbw8fvdlA+frdtcZ3btsF1btXWVPMYUi07v/3tb/nGN77BuHHj6Nu3b63LaVDpccZDhw5lwIABrXKNgw46iLfeeovRo0e3+GoJPn5XkiRpGTjiiCPYYYcduOSSS2pdSk09++yz/OEPf+DnP/95m1sGDFwtQZIkqWpPPPFErUuouW222YYFCxbUuowGOXIrSZKkwnDkVq1rUI9aV9AuONNTktRS6urqluppakXRoUduI6IuIlLZa35EfBARL0TETRGxT9RoMklEDKpX24KImBYRD0TEV+u1HRARZ9aiTkmSpLbEkdvM7cBfgABWAfoCBwLHAA9GxGEppQ9rVNsPgDfIPqvNgJOAP0XEUSml0sJ8A4DewDU1qE+SJKnNMNxm/pVSuqV8R0ScDfwMOJss/H65FoUBf00pLVyzKyLuAkYDFwKNrzotSZLUgXToaQlLklKan1I6B/g7sE9ELFw1OiJ6RMRPI+LViPg4IqZExO0RsXFZmw3z6QSDyvuNiL/l+8+st/+piFj8IdCL1zUGeB/ok583AdgN2LDeNIa6pXzrkiRJ7ZbhtnGD8+2+kAVb4B/AKcC9wOnAdcCXgKciYkOAlNKbZNMJ/l+po4hYEfgisKDe/u7A9sDDjRUTET2B1YCp+a4zgZfy748ue41fivcqSZLUrjktoXHP59vN8u2lwMbATiml50qNImIY2U3vl5DNgYUsrB4TEV1TSh8BOwFdgFuAAyKiU0ppHtnI6/JUDrc98kBbmnP7Y7J/lAwHSCmNyEeBV64/taK+iDgROBFggw02qPLtS5IktR+O3DZuRr7tnq+ccBTwKPBORPQsvYCPgCeBvcrOfRhYAShNafgSMBn4BdmNazvk+3cnG80dWeH6DwJTgInAKGBb4OfA95v6RlJKN6SU+qeU+vfq1aupp0uSJLV5jtw2rnu+nQH0AtYgC7BTGmhf/siO0kjsl4D78+0jwL+AD/Lvn8i3z6WUplXo71Tg5bzfD4HxKaU5S/leJEmSCs2R28ZtnW//TbZUGGSjqXs28Nq7dGJKaRLZ3NcvRUQX4PPAwymlBWSjsP8vItbIr9HQfNt/ppQeTCk9nFL6l8FWkqS2b+7cufTu3ZsLL7yw1qXU1IgRI1hxxRV55ZVXltk1Hblt3PH59l6y0doPge4ppQerPP9h4GRgP2BF4KF8/0PAlWRLjAVV3Ey2BB33MSSS1EFtddNWtS5hEWOPXfpnLY4cOZLdd999kX1du3alb9++HHPMMZx22mn85z//YaONNqq6z/IndP3yl7/k1FNPpXv37kycOJEuXbpUXcNmm23G0Ucfzemnn06nTtXHpp///Od8+OGHnHvuuUD21LBRo0ZVde7QoUMZMGBA1deqhdGjR3Pdddfx6KOPMnHiRJZbbjk22mgj9thjDwYOHMjmm28OwIEHHshWW23F+eefz913371MajPcNiAilgd+SjZf9i8ppcfz/bcCp0bEoSml31c4b82U0uSyXQ+TTS24GHgrpfRa2f6VgO8C84DHmlHuLGC1iIjUkZ+3J0lq177+9a/zla98hZQS7777LsOGDePMM89k3LhxXH311dx8882LtL/77ru55557+N73vke/fv0a7HfIkCFssskmvPbaa9x5550ce+yxVdUwadIkhg8fztlnn8348eO54YYbqnofc+bM4YorruC4445jtdVWA+DCCy/khBNOWNhm6tSpnHXWWey6666ceOKJi5z/hS98oarr1Mqll17KoEGD6NmzJ0ceeST9+vUjpcS4ceP43e9+x3XXXccHH3zAKqusAsC3v/1tjj32WMaNG8eWW27Z6vUZbjPbRcQ38q/Ln1C2IfA34MiytheSLed1R0TcQXYT2Sd5268AY/h0tQTI5tguAPoBw0o7U0ovRsQkYAvgiZTSzGbU/yTwVeC6iPgHMJ9s+sPkJZ+2DAya3uwuel9wbwsUIjVuQq0LkDq47bbbjm984xsLvz/55JPp168fN954I5dddtkixwBeffVV7rnnHvbcc0/q6uoq9vncc88xZswYhg8fztVXX82QIUOWGG7r13DKKaew+eabc+ONN/KjH/2Iam7Ivu222/jwww855phjFu7bc889F2kzYcIEzjrrLDbeeOPF3ldbNmTIEC6++GJ233137rnnHnr06LHI8Z/97Gdccskli4ycH3zwwZx88slcf/31XHvtta1eo3NuM18HbgZuAi4nmzs7CvhySmnvlNLChJZ//UWykdgtgZ+QjfDuTxYyf1XecUrpA+DZ/Nv6Uw8ebmB/U10DDAEOJVsi7Hay0CxJUrvVvXt3dt55Z1JKvP7660vVx+DBg+nWrRsHH3wwAwYM4NFHH23S/M+uXbuy0047kVLitddea/wE4M4772Tttddm2223XaqaH3zwQfbaay9WXXVVOnfuzNZbb83111+/WLvevXtTV1fHc889xx577EG3bt1Yc801Offcc5k3bx5z587l3HPPZb311qNz5878z//8D+PHL7oM/rBhw4gIHnzwQQYNGsSGG27ISiutxNZbb81vf/vbRdp+8sknXHjhhXTr1o3f/e53iwVbgJVXXpnLL7+c7t27L9zXrVs3dt11V+68886l+nk0VYceuU0pjeTTm8Sact5s4LL8VU377RvYfxTZ0mKVjg0CBlXZ/0d8OjdYkqRCSCnx6quvAtCzZ88mn//xxx9z6623cuihh9K1a1eOPPJIzj33XIYOHcqPf/zjqvsphdrVV1+90bbz58/nH//4x2Lzd6t1ww03MHDgQHbaaScuvPBCunbtygMPPMDJJ5/Ma6+9xhVXXLFI+7fffps999yTww8/nEMPPZS//e1vXHXVVSy//PKMGzeOOXPmcMEFFzB16lSuvPJKDjzwQMaPH89yyy06vnn++efz0UcfcfLJJxMRDB06lK9//evMnTt34fzfxx9/nEmTJnH00UdXNYJdbuedd+b+++/npZdeWjgft7V06HArSZLajtmzZzN16lRSSkycOJFrr72W5557jp122olNN920yf2NGDGCadOmLZyG0LNnT/bdd19uuukmLrvsMpZffvkl1jBp0iSuv/56nnnmGXbYYQc222yzxdrX99ZbbzFz5kw22WSTJtc7ceJEzjjjDI444ghuu+22hftPOeUUvv3tb/Pzn/+cgQMHLtL3a6+9xh133MFhhx0GwMCBA9l+++254oor2G+//XjwwQfJlumHNdZYg29/+9s88MAD7L333otce+rUqTz//PMLR2MHDhzI1ltvzdlnn83hhx/OyiuvzAsvvADANtts0+T3Vqp53LhxrR5unZYgSZLahIsvvphevXqx5ppr8rnPfY4hQ4aw//77M2LEiKXqb/DgwfTu3Zvddttt4b4BAwbw7rvvct999zVaw9Zbb80vf/lLDj74YP74xz9Wdc0pU7Jl8KsZ5a3v97//PR9//DHHH388U6dOXeS13377sWDBAh566KFFzllvvfUWBtuSXXbZhZQSp59++sJgC7DrrrsCVJyWcfLJJy8yzaBHjx4MHDiQDz74gJEjRwIwY0b2XKvyKQfVWmONNQCYPLn1bwdy5FaSJLUJJ554IocddhgRsXAZrqUJiQBvvvkmDz30ECeccMIic2X79u1L9+7dGTx4MPvuu2+DNfz3v/9l7Nix/PSnP+Xtt9+mc+fOVV23FCaXZvGi0nzYPfbYo8E277333iLfV1oerbRCQ/1jpf3vv//+YudUWm1iiy2y23dK851LoXbmzKbfA1/6eZSH7dZiuJUkSW3CpptuusRg1xRDhw5lwYIF3HDDDRWX8Przn//M5MmTWXPNNRus4ctf/jK77LILu+yyCwMHDlzsBqtKSnNRp02r9NDRJSsFwOHDh7POOutUbLPxxhsv8n2lqRWNHasUvCuFzvrtPvvZzwLwzDPPNHjNhpR+Hk2dq7s0DLeSJKlQUkoMGzaMbbbZpuITwiZNmsTpp5/OzTffzDnnnLPEvr7whS9w9NFHM3z4cM4444xG16D9zGc+Q/fu3ZfqiVylecU9e/ZssZBfrRdffJH9999/kX2lkeRSoP7iF7/I2muvzYgRI3j//fcXTjWoRunGwFJAbk3OuZUkSYXy4IMP8uabb3L00Udz6KGHLvY67bTT2GijjRgyZEhV/X3/+99n+eWX5wc/+EGjbZdffnl23XVXnnrqqSbX/bWvfY2VVlqJiy++mDlz5ix2fPr06Xz88cdN7rcav/rVr5g+/dO16adPn87111/PqquuunDO8oorrsiPfvQjZs6cyeGHH15xesLcuXP53ve+t3B+bsmTTz7JWmutRd++fVul/nKO3EqSpEIZPHgwkD08oCEHH3wwV111FU8++SQ77bTTEvvr06cPRxxxBLfeeiuPPfbYwhuzGnLYYYdx77338s9//pMdd9yx6rrXX399fvWrX3HCCSfQr18/jj76aDbccEOmTJnC2LFjGTFiBC+++CK9e/euus9q9ezZk89//vN885vfJKXE0KFDeeutt7jxxhsXeVzxN7/5Tf7zn/9wySWX0KdPH4488ki22GILFixYwPjx47nzzjuZPHky3/3udxeeM2vWLB577DG++c1vtnjdlThyK0mSCmPatGmMGDGC7bbbbokh8JBDDgGoevT2wgsvZLnllqtq9Pbwww9n9dVXX+xxwdU47rjjePTRR9l222359a9/zSmnnMK1117LxIkTueyyy1h77bWb3Gc1fvrTn3L44Ydz3XXX8YMf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\n", 298 | "text/plain": [ 299 | "
" 300 | ] 301 | }, 302 | "metadata": { 303 | "needs_background": "light" 304 | }, 305 | "output_type": "display_data" 306 | } 307 | ], 308 | "source": [ 309 | "plt.rcParams.update({'font.size': 18})\n", 310 | "f, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 10), sharex=True, sharey=True)\n", 311 | "\n", 312 | "coefs = []\n", 313 | "ests = []\n", 314 | "for k, est in estimators.items():\n", 315 | " if k in ['OLS', 'TAR (TempC)', 'AR (TempC)']:\n", 316 | " this_X = utils.get_estimator_X(data['train']['X'], est)\n", 317 | " coefs.append(pd.Series(est['fit']['pred'].coef_, index = this_X.columns).sort_values())\n", 318 | " ests.append(labels[k])\n", 319 | "\n", 320 | "coef = pd.concat(coefs, axis=1)\n", 321 | "coef.columns = ests\n", 322 | "\n", 323 | "coef.plot(kind = \"barh\", ax=ax)\n", 324 | "#ax.set_title(\"Comparison of Coefficients\")\n", 325 | "\n", 326 | "plt.tight_layout()\n", 327 | "plt.savefig(\"../figs/coefficient_comparison.pdf\")" 328 | ] 329 | } 330 | ], 331 | "metadata": { 332 | "kernelspec": { 333 | "display_name": "Python 3", 334 | "language": "python", 335 | "name": "python3" 336 | }, 337 | "language_info": { 338 | "codemirror_mode": { 339 | "name": "ipython", 340 | "version": 3 341 | }, 342 | "file_extension": ".py", 343 | "mimetype": "text/x-python", 344 | "name": "python", 345 | "nbconvert_exporter": "python", 346 | "pygments_lexer": "ipython3", 347 | "version": "3.7.9" 348 | } 349 | }, 350 | "nbformat": 4, 351 | "nbformat_minor": 4 352 | } 353 | -------------------------------------------------------------------------------- /real-data-experiment/notebooks/results_tempC_proxies.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tables 1, 2 (Proxies of Temperature)\n", 8 | "\n", 9 | "Builds the portions of Tables 1, 2 that include W, Z\n", 10 | "\n", 11 | "Requires that `run_exp_temp_proxies.py` is run" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "RPATH = '../results'\n", 21 | "FPATH = '../figs'" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 2, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import numpy as np\n", 31 | "import pandas as pd\n", 32 | "import seaborn as sns\n", 33 | "import matplotlib.pyplot as plt\n", 34 | "import pickle as pkl" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 3, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "results = pd.read_csv(f\"{RPATH}/all_res_test_anchor_prox_TempC_lamb_fixed.csv\")\n", 44 | "results = results.drop('Unnamed: 0', axis=1)\n", 45 | "\n", 46 | "residuals = results.drop(\"Lambda\", axis=1).query('Environment == \"Test\"')\n", 47 | "lambs = results.drop(\"Residual\", axis=1).query('Environment == \"Test\"')\n", 48 | "\n", 49 | "mse = lambda v: np.mean(v**2)\n", 50 | "\n", 51 | "# Get RMSE by season, city\n", 52 | "pt = pd.pivot_table(residuals, \n", 53 | " index=['Test_Season', 'City'], \n", 54 | " columns = 'Estimator', \n", 55 | " aggfunc={'Residual': mse})\n", 56 | "\n", 57 | "pt.columns = pt.columns.droplevel(0)\n", 58 | "pt = pt.rename(columns = {\n", 59 | " 'TAR (W)': 'PTAR (W)',\n", 60 | " 'xTAR (W, Z)': 'xPTAR (W, Z)',\n", 61 | " 'AR (W)': 'PAR (W)',\n", 62 | " 'xAR (W, Z)': 'xPAR (W, Z)'\n", 63 | "})\n", 64 | "newcols = [\n", 65 | " 'OLS',\n", 66 | " 'PAR (W)',\n", 67 | " 'xPAR (W, Z)',\n", 68 | " 'PTAR (W)',\n", 69 | " 'xPTAR (W, Z)', \n", 70 | "]\n", 71 | "pt = pt[newcols].reset_index()" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "# Table 1 (W, Z)" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 4, 84 | "metadata": { 85 | "scrolled": true 86 | }, 87 | "outputs": [ 88 | { 89 | "name": "stdout", 90 | "output_type": "stream", 91 | "text": [ 92 | "\\begin{tabular}{lrrrr}\n", 93 | "\\toprule\n", 94 | "{} & Mean & Win & min & max \\\\\n", 95 | "Estimator & & & & \\\\\n", 96 | "\\midrule\n", 97 | "OLS & 0.537 & 0 & 0.000 & 0.000 \\\\\n", 98 | "PAR (W) & 0.531 & 6 & -0.037 & 0.006 \\\\\n", 99 | "xPAR (W, Z) & 0.531 & 6 & -0.039 & 0.007 \\\\\n", 100 | "PTAR (W) & 0.529 & 8 & -0.038 & 0.001 \\\\\n", 101 | "xPTAR (W, Z) & 0.526 & 7 & -0.059 & 0.001 \\\\\n", 102 | "\\bottomrule\n", 103 | "\\end{tabular}\n", 104 | "\n" 105 | ] 106 | } 107 | ], 108 | "source": [ 109 | "wins = lambda v: int(np.sum(v < 0))\n", 110 | "loss = lambda v: int(np.sum(v > 0))\n", 111 | "\n", 112 | "lambs_ar = lambs.query(f'Environment == \"Test\" & Estimator == \"AR (W)\"').groupby(\n", 113 | " ['City', 'Test_Season']).mean()[['Lambda']]\n", 114 | "\n", 115 | "lambs_ar = lambs_ar.reset_index().set_index(['City', 'Test_Season'])\n", 116 | "\n", 117 | "pt_diff = pt.copy()\n", 118 | "for est in newcols:\n", 119 | " pt_diff[est] = pt[est] - pt['OLS']\n", 120 | "\n", 121 | "pt_pos_lamb = pt.set_index(['City', 'Test_Season']).merge(lambs_ar, left_index=True, right_index=True)\n", 122 | "pt_pos_lamb = pt_pos_lamb.query(\"Lambda > 0\").drop(\"Lambda\", axis=1).reset_index()\n", 123 | "\n", 124 | "pt_diff_pos_lamb = pt_pos_lamb.copy()\n", 125 | "for est in newcols:\n", 126 | " pt_diff_pos_lamb[est] = pt_pos_lamb[est] - pt_pos_lamb['OLS']\n", 127 | "\n", 128 | "lt = pd.melt(pt_pos_lamb, id_vars=['Test_Season', 'City'], value_name = 'MSE', var_name = 'Estimator')\n", 129 | "\n", 130 | "mean_result = lt.groupby('Estimator', as_index=True).agg(\n", 131 | " **{'Mean': pd.NamedAgg(column='MSE', aggfunc=np.mean)}\n", 132 | ").reindex(newcols)\n", 133 | "\n", 134 | "pt_diff_long = pd.melt(pt_diff_pos_lamb, id_vars=['Test_Season', 'City'], value_name = 'MSE', var_name='Estimator')\n", 135 | "\n", 136 | "diff_ols = pt_diff_long.groupby('Estimator', as_index=True).agg(\n", 137 | " **{'min': pd.NamedAgg(column='MSE', aggfunc=np.min), \n", 138 | " 'max': pd.NamedAgg(column='MSE', aggfunc=np.max)}\n", 139 | ").reindex(newcols)\n", 140 | "\n", 141 | "win_loss_ols = pt_diff_long.groupby('Estimator', as_index=True).agg(\n", 142 | " **{'Win': pd.NamedAgg(column='MSE', aggfunc=wins)}\n", 143 | ").reindex(newcols)\n", 144 | "\n", 145 | "print(pd.concat([mean_result, win_loss_ols.astype(int), diff_ols], axis=1).to_latex(float_format=\"{:.3f}\".format))" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "# Table 2 (W, Z)" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 5, 158 | "metadata": {}, 159 | "outputs": [ 160 | { 161 | "name": "stdout", 162 | "output_type": "stream", 163 | "text": [ 164 | "\\begin{tabular}{lrrrr}\n", 165 | "\\toprule\n", 166 | "{} & Mean & Diff & min & max \\\\\n", 167 | "Estimator & & & & \\\\\n", 168 | "\\midrule\n", 169 | "OLS & 0.457 & 0.000 & 0.000 & 0.000 \\\\\n", 170 | "PAR (W) & 0.454 & -0.002 & -0.037 & 0.006 \\\\\n", 171 | "xPAR (W, Z) & 0.454 & -0.003 & -0.039 & 0.007 \\\\\n", 172 | "PTAR (W) & 0.452 & -0.005 & -0.038 & 0.001 \\\\\n", 173 | "xPTAR (W, Z) & 0.450 & -0.007 & -0.059 & 0.003 \\\\\n", 174 | "\\bottomrule\n", 175 | "\\end{tabular}\n", 176 | "\n" 177 | ] 178 | } 179 | ], 180 | "source": [ 181 | "pt_diff = pt.copy()\n", 182 | "for est in newcols:\n", 183 | " pt_diff[est] = pt[est] - pt['OLS']\n", 184 | "\n", 185 | "lt = pd.melt(pt, id_vars=['Test_Season', 'City'], value_name = 'MSE')\n", 186 | "\n", 187 | "mean_result = lt.groupby('Estimator', as_index=True).agg(\n", 188 | " **{'Mean': pd.NamedAgg(column='MSE', aggfunc=np.mean)}\n", 189 | ").reindex(newcols)\n", 190 | "\n", 191 | "pt_diff_long = pd.melt(pt_diff, id_vars=['Test_Season', 'City'], value_name = 'MSE')\n", 192 | "\n", 193 | "diff_ols = pt_diff_long.groupby('Estimator', as_index=True).agg(\n", 194 | " **{'Diff': pd.NamedAgg(column='MSE', aggfunc=np.mean),\n", 195 | " 'min': pd.NamedAgg(column='MSE', aggfunc=np.min), \n", 196 | " 'max': pd.NamedAgg(column='MSE', aggfunc=np.max)}\n", 197 | ").reindex(newcols)\n", 198 | "\n", 199 | "print(pd.concat([mean_result, diff_ols], axis=1).to_latex(float_format=\"{:.3f}\".format))" 200 | ] 201 | } 202 | ], 203 | "metadata": { 204 | "kernelspec": { 205 | "display_name": "Python 3", 206 | "language": "python", 207 | "name": "python3" 208 | }, 209 | "language_info": { 210 | "codemirror_mode": { 211 | "name": "ipython", 212 | "version": 3 213 | }, 214 | "file_extension": ".py", 215 | "mimetype": "text/x-python", 216 | "name": "python", 217 | "nbconvert_exporter": "python", 218 | "pygments_lexer": "ipython3", 219 | "version": "3.7.9" 220 | } 221 | }, 222 | "nbformat": 4, 223 | "nbformat_minor": 4 224 | } 225 | -------------------------------------------------------------------------------- /real-data-experiment/results/README.md: -------------------------------------------------------------------------------- 1 | # Results Folder 2 | 3 | This is used to store outputs of the various scripts 4 | -------------------------------------------------------------------------------- /real-data-experiment/run_exp_temp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Real-Data Experiments: Pollution in 5 Chinese Cities 5 | # Link: https://archive.ics.uci.edu/ml/datasets/PM2.5+Data+of+Five+Chinese+Cities 6 | 7 | # * Year / Month / Day / Hour 8 | # * Season 9 | # * DEWP: Dew Point (Celsius Degree) 10 | # * TEMP: Temperature (Celsius Degree) 11 | # * HUMI: Humidity (%) 12 | # * PRES: Pressure (hPa) 13 | # * cbwd: Combined wind direction 14 | # * Iws: Cumulated wind speed (m/s) 15 | # * precipitation: hourly precipitation (mm) 16 | # * Iprec: Cumulated precipitation (mm) 17 | # * (Target) PM2.5 concentration (ug/m^3) 18 | 19 | import numpy as np 20 | import pandas as pd 21 | from copy import deepcopy 22 | from numpy.random import default_rng 23 | import pickle as pkl 24 | 25 | import pdb 26 | 27 | from sklearn import linear_model as lm 28 | from sklearn import preprocessing 29 | from sklearn import model_selection as ms 30 | from sklearn import pipeline 31 | from sklearn.model_selection import train_test_split as tt_split 32 | 33 | import seaborn as sns 34 | import matplotlib.pyplot as plt 35 | 36 | from anchorRegression import AnchorRegression as AR 37 | from anchorRegression import CrossProxyAnchorRegression as XAR 38 | from anchorRegression import TargetedAnchorRegression as TAR 39 | from anchorRegression import CrossTargetedAnchorRegression as XTAR 40 | from anchorRegression import MeanPredictor 41 | import utils 42 | 43 | drop_all = True 44 | 45 | proxies = ['TempC'] 46 | 47 | cities = np.arange(5) 48 | seasons = np.arange(1, 5) 49 | 50 | all_res_dfs = [] 51 | all_rmse_dfs = [] 52 | prox_info = {} 53 | 54 | for CITY in cities: 55 | print(f"City: {CITY}") 56 | 57 | DATA_PATH = "data" 58 | 59 | files = [ 60 | 'BeijingPM20100101_20151231.csv', 61 | 'GuangzhouPM20100101_20151231.csv', 62 | 'ShenyangPM20100101_20151231.csv', 63 | 'ChengduPM20100101_20151231.csv', 64 | 'ShanghaiPM20100101_20151231.csv' 65 | ] 66 | 67 | dfs = [pd.read_csv(f"{DATA_PATH}/{f}") for f in files] 68 | 69 | raw_df = dfs[CITY].drop('No', axis=1) 70 | filt_df = raw_df.dropna() 71 | 72 | df, X, y = utils.process_df(filt_df) 73 | 74 | # Get Proxy Info for this city / season 75 | lr = pipeline.Pipeline([('scaler', preprocessing.StandardScaler()), 76 | ('lr', lm.LinearRegression(fit_intercept=True))]) 77 | 78 | this_city_prox_info = {} 79 | 80 | for prox in proxies: 81 | this_city_prox_info[prox] = {} 82 | 83 | # Get leave-one-out correlation with error 84 | this_X = X.drop([prox], axis=1) 85 | resid = y - lr.fit(this_X, y).predict(this_X) 86 | this_city_prox_info[prox]['corr_resid_lone'] = \ 87 | np.corrcoef(resid, X[prox].values)[0, 1] 88 | 89 | # Get leave-all-out correlation with error 90 | this_X = X.drop(proxies, axis=1) 91 | resid = y - lr.fit(this_X, y).predict(this_X) 92 | this_city_prox_info[prox]['corr_resid_lall'] = \ 93 | np.corrcoef(resid, X[prox].values)[0, 1] 94 | 95 | prox_info[CITY] = this_city_prox_info 96 | 97 | for test_season in seasons: 98 | print(f"\t Season: {test_season}") 99 | 100 | dev_year = 2013 101 | data = utils.get_dev_train_test_data( 102 | df, X, y, test_season, dev_year, proxies) 103 | 104 | baselines = utils.construct_baselines( 105 | data, proxies, drop_all=drop_all) 106 | tar_baselines = utils.construct_tar_baseline( 107 | data, proxies, drop_all=drop_all) 108 | tar_estimators = utils.construct_tar( 109 | data, proxies, drop_all=drop_all) 110 | ar_estimators = utils.construct_ar( 111 | data, proxies, drop_all=drop_all) 112 | 113 | if len(proxies) > 1: 114 | xtar_estimators = utils.construct_xtar( 115 | data, proxies, drop_all=drop_all) 116 | xar_estimators = utils.construct_xar( 117 | data, proxies, drop_all=drop_all) 118 | 119 | estimators = { 120 | **baselines, 121 | **tar_baselines, 122 | **tar_estimators, **xtar_estimators, 123 | **ar_estimators, **xar_estimators} 124 | else: 125 | estimators = { 126 | **baselines, 127 | **tar_baselines, 128 | **tar_estimators, 129 | **ar_estimators} 130 | 131 | for k, est in estimators.items(): 132 | if 'tune_lambda' in est.keys() and est['tune_lambda']: 133 | best_lambda = utils.get_best_lambda(est, data) 134 | 135 | print(f"\t\t {k}: {best_lambda}") 136 | 137 | estimators[k]['pipe'] = estimators[k]['pipe'].set_params( 138 | **best_lambda) 139 | 140 | estimators[k].update(best_lambda) 141 | 142 | for k, est in estimators.items(): 143 | 144 | perf = {} 145 | 146 | # Get cross-validated training errors 147 | this_X_train = utils.get_estimator_X(data['train']['X'], est) 148 | this_X_test = utils.get_estimator_X(data['test']['X'], est) 149 | y_train = data['train']['y'] 150 | y_test = data['test']['y'] 151 | 152 | preds_train_cv = ms.cross_val_predict(est['pipe'], 153 | this_X_train, y_train, fit_params=est['fit_params'], cv=10) 154 | resid_train_cv = preds_train_cv - y_train 155 | 156 | perf['Train (CV)'] = { 157 | 'preds': preds_train_cv, 158 | 'resid': resid_train_cv.values 159 | } 160 | 161 | # Train on the full training set 162 | if est['fit_params'] is not None: 163 | est['fit'] = est['pipe'].fit(this_X_train, y_train, **est['fit_params']) 164 | else: 165 | est['fit'] = est['pipe'].fit(this_X_train, y_train) 166 | 167 | # Evaluate on the test set 168 | preds_test = est['fit'].predict(this_X_test) 169 | resid_test = preds_test - y_test 170 | 171 | perf['Test'] = { 172 | 'preds': preds_test, 173 | 'resid': resid_test.values 174 | } 175 | 176 | estimators[k]['perf'] = perf 177 | 178 | res_dfs = [] 179 | 180 | for key, est in estimators.items(): 181 | for env_name, perf in est['perf'].items(): 182 | rs = pd.DataFrame(perf['resid'], columns=['Residual']) 183 | rs['City'] = CITY 184 | rs['Test_Season'] = test_season 185 | rs['Type'] = key.split()[0] 186 | rs['Estimator'] = key 187 | rs['Environment'] = env_name 188 | if 'lamb' in estimators[key]['fit']['pred'].get_params(): 189 | rs['Lambda'] = estimators[key]['fit']['pred'].get_params()['lamb'] 190 | else: 191 | rs['Lambda'] = np.nan 192 | 193 | res_dfs.append(rs) 194 | all_res_dfs.append(rs) 195 | 196 | res_df = pd.concat(res_dfs, axis=0) 197 | 198 | rmse = lambda v: np.sqrt(np.mean(v**2)) 199 | 200 | rng = default_rng(0) 201 | 202 | # boostrap the RMSE 203 | n_boot_iter = 1000 204 | 205 | # For each estimator 206 | for key, est in estimators.items(): 207 | # For each environment (train / test) 208 | for env_name, perf in est['perf'].items(): 209 | # Bootstrap RMSE 210 | rmse_set = [] 211 | for _ in range(n_boot_iter): 212 | rmse_set.append(rmse(rng.choice(perf['resid'], size=len(perf['resid'])))) 213 | rmse_set = np.array(rmse_set) 214 | 215 | # Save distribution of results 216 | estimators[key]['perf'][env_name]['rmse_boot'] = rmse_set 217 | 218 | rmse_dfs = [] 219 | 220 | for key, est in estimators.items(): 221 | for env_name, perf in est['perf'].items(): 222 | rs = pd.DataFrame(perf['rmse_boot'], columns=['RMSE']) 223 | rs['City'] = CITY 224 | rs['Test_Season'] = test_season 225 | rs['Type'] = key.split()[0] 226 | rs['Estimator'] = key 227 | rs['Environment'] = env_name 228 | rmse_dfs.append(rs) 229 | all_rmse_dfs.append(rs) 230 | 231 | rmse_df = pd.concat(rmse_dfs, axis=0) 232 | 233 | all_res_df = pd.concat(all_res_dfs, axis=0) 234 | all_rmse_df = pd.concat(all_rmse_dfs, axis=0) 235 | all_res_df.to_csv(f"results/all_res_test_{proxies[0]}.csv") 236 | all_rmse_df.to_csv(f"results/all_rmse_test_{proxies[0]}.csv") 237 | with open(f'results/prox_info_test_{proxies[0]}.pkl', 'wb') as f: 238 | pkl.dump(prox_info, f, protocol=pkl.HIGHEST_PROTOCOL) 239 | -------------------------------------------------------------------------------- /real-data-experiment/run_exp_temp_proxies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # # Real-Data Experiments: Pollution in 5 Chinese Cities 5 | 6 | # https://archive.ics.uci.edu/ml/datasets/PM2.5+Data+of+Five+Chinese+Cities 7 | 8 | # This dataset contains hourly pollution readings (PM2.5 concentration) from five cities in China: Beijing, Guangzhou, Shenyang, Chengdu, and Shanghai, over the course of several years. In Shanghai, for instance, there are >20k readings, accompanied by several features 9 | 10 | # * Year / Month / Day / Hour 11 | # * Season 12 | # * DEWP: Dew Point (Celsius Degree) 13 | # * TEMP: Temperature (Celsius Degree) 14 | # * HUMI: Humidity (%) 15 | # * PRES: Pressure (hPa) 16 | # * cbwd: Combined wind direction 17 | # * Iws: Cumulated wind speed (m/s) 18 | # * precipitation: hourly precipitation (mm) 19 | # * Iprec: Cumulated precipitation (mm) 20 | # * (Target) PM2.5 concentration (ug/m^3) 21 | 22 | import numpy as np 23 | import pandas as pd 24 | from copy import deepcopy 25 | from numpy.random import default_rng 26 | import pickle as pkl 27 | 28 | import pdb 29 | 30 | from sklearn import linear_model as lm 31 | from sklearn import preprocessing 32 | from sklearn import model_selection as ms 33 | from sklearn import pipeline 34 | from sklearn.model_selection import train_test_split as tt_split 35 | 36 | import seaborn as sns 37 | import matplotlib.pyplot as plt 38 | 39 | from anchorRegression import AnchorRegression as AR 40 | from anchorRegression import CrossProxyAnchorRegression as XAR 41 | from anchorRegression import TargetedAnchorRegression as TAR 42 | from anchorRegression import CrossTargetedAnchorRegression as XTAR 43 | from anchorRegression import MeanPredictor 44 | import utils 45 | 46 | drop_all = True 47 | 48 | # From previous run, for comparability, set to None to re-tune 49 | lambda_list = { 50 | (0, 1): 40.0, 51 | (0, 2): 40.0, 52 | (0, 3): 40.0, 53 | (0, 4): 40.0, 54 | (1, 1): 0.0, 55 | (1, 2): 0.0, 56 | (1, 3): 0.0, 57 | (1, 4): 0.0, 58 | (2, 1): 40.0, 59 | (2, 2): 0.0, 60 | (2, 3): 0.0, 61 | (2, 4): 0.0, 62 | (3, 1): 40.0, 63 | (3, 2): 0.0, 64 | (3, 3): 0.0, 65 | (3, 4): 0.0, 66 | (4, 1): 0.0, 67 | (4, 2): 40.0, 68 | (4, 3): 40.0, 69 | (4, 4): 40.0 70 | } 71 | 72 | anchor = 'TempC' 73 | proxies = ['W', 'Z'] 74 | create_proxies = True 75 | rho = 0.9 76 | 77 | cities = np.arange(5) 78 | seasons = np.arange(1, 5) 79 | 80 | all_res_dfs = [] 81 | all_rmse_dfs = [] 82 | 83 | for CITY in cities: 84 | print(f"City: {CITY}") 85 | 86 | DATA_PATH = "data" 87 | 88 | files = [ 89 | 'BeijingPM20100101_20151231.csv', 90 | 'GuangzhouPM20100101_20151231.csv', 91 | 'ShenyangPM20100101_20151231.csv', 92 | 'ChengduPM20100101_20151231.csv', 93 | 'ShanghaiPM20100101_20151231.csv' 94 | ] 95 | 96 | dfs = [pd.read_csv(f"{DATA_PATH}/{f}") for f in files] 97 | 98 | raw_df = dfs[CITY].drop('No', axis=1) 99 | filt_df = raw_df.dropna() 100 | 101 | df, X, y = utils.process_df(filt_df) 102 | 103 | # Get Proxy Info for this city / season 104 | lr = pipeline.Pipeline([('scaler', preprocessing.StandardScaler()), 105 | ('lr', lm.LinearRegression(fit_intercept=True))]) 106 | 107 | for test_season in seasons: 108 | print(f"\t Season: {test_season}") 109 | 110 | dev_year = 2013 111 | if create_proxies: 112 | data = utils.get_dev_train_test_data_anchors( 113 | df, X, y, test_season, dev_year, 114 | anchor, proxies, rho) 115 | else: 116 | data = utils.get_dev_train_test_data( 117 | df, X, y, test_season, dev_year, proxies) 118 | 119 | baselines = utils.construct_baselines( 120 | data, proxies, drop_all=drop_all) 121 | tar_baselines = utils.construct_tar_baseline( 122 | data, proxies, drop_all=drop_all) 123 | tar_estimators = utils.construct_tar( 124 | data, proxies, drop_all=drop_all) 125 | ar_estimators = utils.construct_ar( 126 | data, proxies, drop_all=drop_all) 127 | 128 | if len(proxies) > 1: 129 | xtar_estimators = utils.construct_xtar( 130 | data, proxies, drop_all=drop_all) 131 | xar_estimators = utils.construct_xar( 132 | data, proxies, drop_all=drop_all) 133 | 134 | estimators = { 135 | **baselines, 136 | **tar_baselines, 137 | **tar_estimators, **xtar_estimators, 138 | **ar_estimators, **xar_estimators} 139 | else: 140 | estimators = { 141 | **baselines, 142 | **tar_baselines, 143 | **tar_estimators, 144 | **ar_estimators} 145 | 146 | for k, est in estimators.items(): 147 | if 'tune_lambda' in est.keys() and est['tune_lambda']: 148 | if lambda_list is not None: 149 | best_lambda = { 150 | 'pred__lamb': lambda_list[(CITY, test_season)] 151 | } 152 | print(f"\t\t {k}: {best_lambda}") 153 | else: 154 | best_lambda = utils.get_best_lambda(est, data) 155 | print(f"\t\t {k}: {best_lambda}") 156 | 157 | estimators[k]['pipe'] = estimators[k]['pipe'].set_params( 158 | **best_lambda) 159 | 160 | estimators[k].update(best_lambda) 161 | 162 | for k, est in estimators.items(): 163 | 164 | perf = {} 165 | 166 | # Get cross-validated training errors 167 | this_X_train = utils.get_estimator_X(data['train']['X'], est) 168 | this_X_test = utils.get_estimator_X(data['test']['X'], est) 169 | y_train = data['train']['y'] 170 | y_test = data['test']['y'] 171 | 172 | preds_train_cv = ms.cross_val_predict(est['pipe'], 173 | this_X_train, y_train, fit_params=est['fit_params'], cv=10) 174 | resid_train_cv = preds_train_cv - y_train 175 | 176 | perf['Train (CV)'] = { 177 | 'preds': preds_train_cv, 178 | 'resid': resid_train_cv.values 179 | } 180 | 181 | # Train on the full training set 182 | if est['fit_params'] is not None: 183 | est['fit'] = est['pipe'].fit(this_X_train, y_train, **est['fit_params']) 184 | else: 185 | est['fit'] = est['pipe'].fit(this_X_train, y_train) 186 | 187 | # Evaluate on the test set 188 | preds_test = est['fit'].predict(this_X_test) 189 | resid_test = preds_test - y_test 190 | 191 | perf['Test'] = { 192 | 'preds': preds_test, 193 | 'resid': resid_test.values 194 | } 195 | 196 | estimators[k]['perf'] = perf 197 | 198 | res_dfs = [] 199 | 200 | for key, est in estimators.items(): 201 | for env_name, perf in est['perf'].items(): 202 | rs = pd.DataFrame(perf['resid'], columns=['Residual']) 203 | rs['City'] = CITY 204 | rs['Test_Season'] = test_season 205 | rs['Type'] = key.split()[0] 206 | rs['Estimator'] = key 207 | rs['Environment'] = env_name 208 | if 'lamb' in estimators[key]['fit']['pred'].get_params(): 209 | rs['Lambda'] = estimators[key]['fit']['pred'].get_params()['lamb'] 210 | else: 211 | rs['Lambda'] = np.nan 212 | 213 | res_dfs.append(rs) 214 | all_res_dfs.append(rs) 215 | 216 | res_df = pd.concat(res_dfs, axis=0) 217 | 218 | rmse = lambda v: np.sqrt(np.mean(v**2)) 219 | 220 | rng = default_rng(0) 221 | 222 | # boostrap the RMSE 223 | n_boot_iter = 1000 224 | 225 | # For each estimator 226 | for key, est in estimators.items(): 227 | # For each environment (train / test) 228 | for env_name, perf in est['perf'].items(): 229 | # Bootstrap RMSE 230 | rmse_set = [] 231 | for _ in range(n_boot_iter): 232 | rmse_set.append(rmse(rng.choice(perf['resid'], size=len(perf['resid'])))) 233 | rmse_set = np.array(rmse_set) 234 | 235 | # Save distribution of results 236 | estimators[key]['perf'][env_name]['rmse_boot'] = rmse_set 237 | 238 | rmse_dfs = [] 239 | 240 | for key, est in estimators.items(): 241 | for env_name, perf in est['perf'].items(): 242 | rs = pd.DataFrame(perf['rmse_boot'], columns=['RMSE']) 243 | rs['City'] = CITY 244 | rs['Test_Season'] = test_season 245 | rs['Type'] = key.split()[0] 246 | rs['Estimator'] = key 247 | rs['Environment'] = env_name 248 | rmse_dfs.append(rs) 249 | all_rmse_dfs.append(rs) 250 | 251 | ar_types = ['AR', 'xAR', 'OLS'] 252 | tar_types = ['TAR', 'xTAR', 'OLS'] 253 | 254 | rmse_df = pd.concat(rmse_dfs, axis=0) 255 | 256 | all_res_df = pd.concat(all_res_dfs, axis=0) 257 | all_rmse_df = pd.concat(all_rmse_dfs, axis=0) 258 | all_res_df.to_csv(f"results/all_res_test_anchor_prox_{anchor}_lamb_fixed.csv") 259 | all_rmse_df.to_csv(f"results/all_rmse_test_anchor_prox_{anchor}_lamb_fixed.csv") 260 | -------------------------------------------------------------------------------- /real-data-experiment/utils.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | from scipy.stats import skew 4 | from copy import deepcopy 5 | 6 | from sklearn import linear_model as lm 7 | from sklearn import preprocessing 8 | from sklearn import model_selection as ms 9 | from sklearn import pipeline 10 | from sklearn.model_selection import train_test_split as tt_split 11 | 12 | import sys; sys.path.insert(0, '..') 13 | 14 | from anchorRegression import AnchorRegression as AR 15 | from anchorRegression import CrossProxyAnchorRegression as XAR 16 | from anchorRegression import TargetedAnchorRegression as TAR 17 | from anchorRegression import CrossTargetedAnchorRegression as XTAR 18 | from anchorRegression import MeanPredictor 19 | 20 | import itertools as it 21 | 22 | LAMBDA_RANGE = np.linspace(0, 40, 100) 23 | 24 | def process_df(df): 25 | # The dataset contains PM (pollution) readings across different posts in the city. We average them here 26 | PM_vars = [f for f in df.columns if "PM" in f] 27 | 28 | avg_pm = 0 29 | for f in PM_vars: avg_pm += df[f] 30 | avg_pm = avg_pm / len(PM_vars) 31 | 32 | df = df.drop(PM_vars, axis=1) 33 | df['avg_pm'] = avg_pm 34 | 35 | # Create a date-time index 36 | time_vars = ['year', 'month', 'day', 'hour'] 37 | time = pd.to_datetime(df[time_vars]) 38 | df.index = time 39 | 40 | df['target'] = np.log1p(df['avg_pm']) 41 | 42 | # Construct Features 43 | drop_vars = ['year', 'month', 'day', 'hour', 'season'] 44 | 45 | cat_feats = ['month', 'day', 'hour', 'season', 'cbwd'] 46 | for f in cat_feats: 47 | df[f] = pd.Categorical(df[f]) 48 | 49 | X = df.copy() 50 | X = X.drop(drop_vars, axis=1) 51 | X = X.drop(['target', 'avg_pm'], axis=1) 52 | 53 | # Better variable names 54 | X = X.rename(columns={'DEWP': 'DewPt', 55 | 'HUMI': 'Humidity', 56 | 'PRES': 'Press', 57 | 'TEMP': 'TempC', 58 | 'cbwd': 'WindDir', 59 | 'Iws': 'WindSp', 60 | 'precipitation': 'PrecipHr', 61 | 'Iprec': 'PrecipCm'}) 62 | 63 | # Note that we drop the first dummy variable, because we'll be using OLS 64 | X = pd.get_dummies(X, drop_first=True) 65 | y = df['target'] 66 | 67 | Xy = pd.concat([X, y], axis=1) 68 | 69 | numeric_feats = [f for f in X.columns if f not in time_vars and 70 | 'WindDir' not in f and 71 | 'season' not in f] 72 | 73 | X['PrecipCm'] = X['PrecipCm'] - X['PrecipHr'] 74 | 75 | #log transform skewed numeric features: 76 | skewed_feats = X[numeric_feats].apply(lambda x: skew(x.dropna())) #compute skewness 77 | skewed_feats = skewed_feats[skewed_feats > 0.75] 78 | skewed_feats = skewed_feats.index 79 | #print(skewed_feats) 80 | 81 | X[skewed_feats] = np.log1p(X[skewed_feats]) 82 | 83 | return df, X, y 84 | 85 | def get_dev_train_test_data_anchors(df, X, y, test_season, dev_year, 86 | anchor, proxy_names, rho=0.9): 87 | 88 | dev_ids = np.logical_and(df.season != test_season, df.year == dev_year) 89 | train_ids = np.logical_and(df.season != test_season, df.year != dev_year) 90 | test_ids = np.logical_and(df.season == test_season, df.year != dev_year) 91 | 92 | rng = np.random.default_rng(0) 93 | 94 | data = {'dev': {}, 'train': {}, 'test': {}} 95 | 96 | # Construct proxies, so that stv in train is equal to rho 97 | A = X[[anchor]] 98 | var_A = A[train_ids].var() 99 | # rho = var_A / (var_A + var_eps) 100 | sigma_eps = np.sqrt((var_A / rho) - var_A) 101 | print(f"\t\t\t Sigma for proxies is: {sigma_eps[0]:.3f}") 102 | 103 | for prox in proxy_names: 104 | X[[prox]] = pd.DataFrame(rng.normal(A, sigma_eps), 105 | index=A.index, columns=A.columns) 106 | 107 | data['dev'] = { 108 | 'G': df[dev_ids][['season']], 109 | 'X': X[dev_ids].drop(anchor, axis=1), 110 | 'y': y[dev_ids] 111 | } 112 | 113 | data['train'] = { 114 | 'G': df[train_ids][['season']], 115 | 'X': X[train_ids].drop(anchor, axis=1), 116 | 'y': y[train_ids] 117 | } 118 | 119 | data['test'] = { 120 | 'G': df[test_ids][['season']], 121 | 'X': X[test_ids].drop(anchor, axis=1), 122 | 'y': y[test_ids] 123 | } 124 | 125 | for prox in proxy_names: 126 | data['dev'][prox] = X[dev_ids][[prox]] 127 | data['train'][prox] = X[train_ids][[prox]] 128 | data['test'][prox] = X[test_ids][[prox]] 129 | 130 | return data 131 | 132 | def get_dev_train_test_data(df, X, y, test_season, dev_year, proxies): 133 | dev_ids = np.logical_and(df.season != test_season, df.year == dev_year) 134 | train_ids = np.logical_and(df.season != test_season, df.year != dev_year) 135 | test_ids = np.logical_and(df.season == test_season, df.year != dev_year) 136 | 137 | data = {'dev': {}, 'train': {}, 'test': {}} 138 | 139 | data['dev'] = { 140 | 'G': df[dev_ids][['season']], 141 | 'X': X[dev_ids], 142 | 'y': y[dev_ids] 143 | } 144 | 145 | data['train'] = { 146 | 'G': df[train_ids][['season']], 147 | 'X': X[train_ids], 148 | 'y': y[train_ids] 149 | } 150 | 151 | data['test'] = { 152 | 'G': df[test_ids][['season']], 153 | 'X': X[test_ids], 154 | 'y': y[test_ids] 155 | } 156 | 157 | for prox in proxies: 158 | data['dev'][prox] = X[dev_ids][[prox]] 159 | data['train'][prox] = X[train_ids][[prox]] 160 | data['test'][prox] = X[test_ids][[prox]] 161 | 162 | return data 163 | 164 | def construct_baselines(data, proxies, drop_all=False): 165 | mp = pipeline.Pipeline([('pred', MeanPredictor())]) 166 | 167 | lr = pipeline.Pipeline([('scaler', preprocessing.StandardScaler()), 168 | ('pred', lm.LinearRegression(fit_intercept=True, normalize=False))]) 169 | 170 | baselines = { 171 | 'Mean': 172 | {'pipe': mp, 173 | 'fit_params': None}, 174 | 'OLS (All)': 175 | {'pipe': deepcopy(lr), 176 | 'fit_params': None}, 177 | 'OLS': 178 | {'pipe': deepcopy(lr), 179 | 'fit_params': None, 180 | 'drop_cols': proxies}, 181 | } 182 | 183 | return baselines 184 | 185 | def construct_tar_baseline(data, proxies, drop_all=False): 186 | tar = pipeline.Pipeline( 187 | [('scaler', preprocessing.StandardScaler()), 188 | ('pred', TAR(fit_intercept=True, normalize=False))]) 189 | 190 | estimators = {} 191 | for prox in proxies: 192 | estimators[f"PA ({prox})"] = { 193 | 'pipe': deepcopy(tar), 194 | 'drop_cols': proxies if drop_all else [prox], 195 | 'fit_params': { 196 | 'pred__A': data['train'][prox], 197 | 'pred__nu': np.ones_like(data['test'][prox] 198 | )*data['test'][prox].mean().values[0] 199 | } 200 | } 201 | 202 | return estimators 203 | 204 | def construct_tar(data, proxies, drop_all=False): 205 | tar = pipeline.Pipeline( 206 | [('scaler', preprocessing.StandardScaler()), 207 | ('pred', TAR(fit_intercept=True, normalize=False))]) 208 | 209 | estimators = {} 210 | for prox in proxies: 211 | estimators[f"TAR ({prox})"] = { 212 | 'pipe': deepcopy(tar), 213 | 'drop_cols': proxies if drop_all else [prox], 214 | 'fit_params': { 215 | 'pred__A': data['train'][prox], 216 | 'pred__nu': data['test'][prox]} 217 | } 218 | 219 | return estimators 220 | 221 | def construct_xtar(data, proxies, drop_all=False): 222 | xtar = pipeline.Pipeline( 223 | [('scaler', preprocessing.StandardScaler()), 224 | ('pred', XTAR(fit_intercept=True, normalize=False))]) 225 | 226 | estimators = {} 227 | for prox_combo in it.permutations(proxies, 2): 228 | estimators[f"xTAR ({prox_combo[0]}, {prox_combo[1]})"] = { 229 | 'pipe': deepcopy(xtar), 230 | 'drop_cols': proxies if drop_all else [p for p in prox_combo], 231 | 'fit_params': { 232 | 'pred__W': data['train'][prox_combo[0]], 233 | 'pred__Z': data['train'][prox_combo[1]], 234 | 'pred__nu': data['test'][prox_combo[0]]} 235 | } 236 | 237 | return estimators 238 | 239 | def construct_ar(data, proxies, drop_all=False): 240 | ar = pipeline.Pipeline( 241 | [('scaler', preprocessing.StandardScaler()), 242 | ('pred', AR(lamb=0, fit_intercept=True, normalize=False))]) 243 | 244 | estimators = {} 245 | for prox in proxies: 246 | estimators[f"AR ({prox})"] = { 247 | 'pipe': deepcopy(ar), 248 | 'drop_cols': proxies if drop_all else [prox], 249 | 'fit_params_dev': { 250 | 'pred__A': data['dev'][prox], 251 | }, 252 | 'fit_params': { 253 | 'pred__A': data['train'][prox], 254 | }, 255 | 'tune_lambda': True 256 | } 257 | 258 | return estimators 259 | 260 | def construct_xar(data, proxies, drop_all=False): 261 | xar = pipeline.Pipeline( 262 | [('scaler', preprocessing.StandardScaler()), 263 | ('pred', XAR(lamb=0, fit_intercept=True, normalize=False))]) 264 | 265 | estimators = {} 266 | for prox_combo in it.combinations(proxies, 2): 267 | estimators[f"xAR ({prox_combo[0]}, {prox_combo[1]})"] = { 268 | 'pipe': deepcopy(xar), 269 | 'drop_cols': proxies if drop_all else [p for p in prox_combo], 270 | 'fit_params_dev': { 271 | 'pred__W': data['dev'][prox_combo[0]], 272 | 'pred__Z': data['dev'][prox_combo[1]] 273 | }, 274 | 'fit_params': { 275 | 'pred__W': data['train'][prox_combo[0]], 276 | 'pred__Z': data['train'][prox_combo[1]] 277 | }, 278 | 'tune_lambda': True 279 | } 280 | 281 | return estimators 282 | 283 | def get_estimator_X(X, est): 284 | if 'drop_cols' in est.keys(): 285 | return X.copy().drop(est['drop_cols'], axis=1) 286 | else: 287 | return X.copy() 288 | 289 | def get_best_lambda(est, data): 290 | X = data['dev']['X'] 291 | y = data['dev']['y'] 292 | G = data['dev']['G'] 293 | 294 | X = get_estimator_X(X, est) 295 | fit_params = est['fit_params_dev'] 296 | 297 | logo = ms.LeaveOneGroupOut() 298 | lamb_params = {'pred__lamb': LAMBDA_RANGE} 299 | 300 | est_cv = ms.GridSearchCV( 301 | est['pipe'], lamb_params, cv=logo, 302 | scoring='neg_root_mean_squared_error') 303 | est_cv = est_cv.fit(X, y, **fit_params, groups=np.ravel(G)) 304 | 305 | return est_cv.best_params_ 306 | -------------------------------------------------------------------------------- /synthetic-experiments/appendix-B-identification.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import pandas as pd\n", 11 | "\n", 12 | "import matplotlib.pyplot as plt\n", 13 | "import seaborn as sns" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Define the observed distribution" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "###############################\n", 30 | "# Define a covariance matrix\n", 31 | "###############################\n", 32 | "\n", 33 | "cov = {'wx': 1, 'xy': 3, 'wy': 2, 'ww': 9, 'xx': 9, 'yy': 9}\n", 34 | "\n", 35 | "#############################\n", 36 | "# Choose Lambda\n", 37 | "#############################\n", 38 | "lda = 5" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "## Generate valid parameters" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "Here we generate valid parameterizations. Notable restrictions implied by the model\n", 53 | "1. $\\rho_W$ is lower bounded by the squared covariance of W, X\n", 54 | "2. $\\sigma_Y^2$ cannot go below zero" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 3, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "[13.11309058 5.79808822 8.08882119]\n" 67 | ] 68 | } 69 | ], 70 | "source": [ 71 | "# Verify that the chosen covariance matrix is PD\n", 72 | "cov_mat = np.array(\n", 73 | " [[cov['xx'], cov['xy'], cov['wx']],\n", 74 | " [cov['xy'], cov['yy'], cov['wy']],\n", 75 | " [cov['wx'], cov['wy'], cov['ww']]])\n", 76 | "\n", 77 | "w, _ = np.linalg.eig(cov_mat)\n", 78 | "assert np.all(w > 0) # Verify that covariance is PD\n", 79 | "print(w)" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 4, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "corr_wx = cov['wx'] / np.sqrt(cov['xx'] * cov['ww'])\n", 89 | "assert corr_wx < 1 and corr_wx > 0\n", 90 | "\n", 91 | "# Generate a range of feasible rho_w\n", 92 | "# The smallest rw corresponds to the largest rx, which is 1.\n", 93 | "min_rw = corr_wx**2\n", 94 | "rw = np.linspace(min_rw+0.001, 1, 1000)\n", 95 | "#rw = np.linspace(0.06, 1, 1000)\n", 96 | "\n", 97 | "# Generate all remaining values\n", 98 | "rx = corr_wx**2 / rw\n", 99 | "\n", 100 | "sx = cov['xx'] * (1 - rx)\n", 101 | "sw = cov['ww'] * (1 - rw)\n", 102 | "\n", 103 | "bw = np.sqrt(cov['ww'] * rw)\n", 104 | "bx = np.sqrt(cov['xx'] * rx)\n", 105 | "by = (1 / (bw * (1 - rx))) * (cov['wy'] - (cov['xy'] * cov['wx']) / cov['xx'])\n", 106 | "a = (cov['xy'] - by * bx) / cov['xx']\n", 107 | "\n", 108 | "sy = cov['yy'] - by**2 - (2*a*by*bx) - (a**2) * cov['xx']" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 5, 114 | "metadata": { 115 | "scrolled": true 116 | }, 117 | "outputs": [ 118 | { 119 | "name": "stdout", 120 | "output_type": "stream", 121 | "text": [ 122 | "$\\rho_W$ cannot go below 0.012345679012345678\n" 123 | ] 124 | } 125 | ], 126 | "source": [ 127 | "print(f\"$\\\\rho_W$ cannot go below {min_rw}\")" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 6, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "# Verify that all observed moments match\n", 137 | "e_wx = bw * bx\n", 138 | "assert np.all(np.isclose(e_wx, cov['wx']))\n", 139 | "\n", 140 | "e_xy = by * bx + a * cov['xx']\n", 141 | "assert np.all(np.isclose(e_xy, cov['xy']))\n", 142 | "\n", 143 | "e_wy = bw * (by + a * bx)\n", 144 | "assert np.all(np.isclose(e_wy, cov['wy']))\n", 145 | "\n", 146 | "e_ww = bw**2 + sw\n", 147 | "assert np.all(np.isclose(e_ww, cov['ww']))\n", 148 | "\n", 149 | "e_xx = bx**2 + sx\n", 150 | "assert np.all(np.isclose(e_xx, cov['xx']))\n", 151 | "\n", 152 | "e_yy = (a**2)*cov['xx'] + (2*a*by*bx) + by**2 + sy\n", 153 | "assert np.all(np.isclose(e_yy, cov['yy']))" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "Later, we will filter out all instances where `sy < 0`, which will remove some of these" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "## Compute solutions\n", 168 | "\n", 169 | "We compute the anchor regression solution (which relies upon the unidentified variables) for each parameterization, and can then compare this to the solution when a single proxy is used, which is fixed (because it uses only the observed distribution over X, Y, W)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 7, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "# Anchor Regression Solution\n", 179 | "g_a = ((a * bx**2 + bx * by)*(1 + lda) + a * sx) \\\n", 180 | " / ((bx**2 * (1 + lda) + sx))\n", 181 | "\n", 182 | "# Proxy Anchor Regression solution\n", 183 | "g_w = (cov['xy'] * cov['ww'] + lda * cov['wy']) \\\n", 184 | " / (cov['xx'] * cov['ww'] + lda * cov['wx']) " 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "We can then compute the expected loss, under the worst-case shift" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 8, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "def get_loss(g, shift, noise=True):\n", 201 | " loss = (((a - g) * bx + by)**2)*(shift) + ((a - g)**2) * sx\n", 202 | " if noise:\n", 203 | " loss = loss + sy\n", 204 | " return loss\n", 205 | "\n", 206 | "loss_g_a = get_loss(g_a, 1 + lda, noise=True)\n", 207 | "loss_g_w = get_loss(g_w, 1 + lda, noise=True)" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 9, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "results = np.array([\n", 217 | " # Parameters\n", 218 | " rw, rx, sw, sx, sy, \n", 219 | " bw, bx, by, a, \n", 220 | " # AR solution\n", 221 | " g_a, \n", 222 | " # Loss under different models\n", 223 | " loss_g_a, loss_g_w, \n", 224 | " loss_g_w - loss_g_a]).T\n", 225 | "\n", 226 | "results_df = pd.DataFrame(results, columns=[\n", 227 | " 'rho_W', 'rho_X', 'sig_W', 'sig_X', 'sig_Y', \n", 228 | " 'b_W', 'b_X', 'b_Y', 'a', \n", 229 | " 'g_a', \n", 230 | " 'loss_g_a', 'loss_g_w', \n", 231 | " 'diff_loss_w_a'])\n", 232 | "\n", 233 | "# Filter for parameterizations that are valid\n", 234 | "results_df = results_df.query('sig_Y > 0 & rho_X < 1')" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": 10, 240 | "metadata": {}, 241 | "outputs": [ 242 | { 243 | "name": "stdout", 244 | "output_type": "stream", 245 | "text": [ 246 | "After filtering out negative values of sigma_Y, the minimum $\\rho_W$ is 0.05186371556741927\n" 247 | ] 248 | } 249 | ], 250 | "source": [ 251 | "print(f\"After filtering out negative values of sigma_Y, the minimum $\\\\rho_W$ is {results_df['rho_W'].min()}\")" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": {}, 257 | "source": [ 258 | "## Plot results" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 11, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "def plot_vars(var_list, df, id_vars=['rho_W']):\n", 268 | " \n", 269 | " long_df = pd.melt(df, id_vars=id_vars)\n", 270 | " long_df.rename(columns = {'variable': 'Parameter'}, inplace=True)\n", 271 | " \n", 272 | " f, ax = plt.subplots(figsize=(6.5, 6.5))\n", 273 | " sns.lineplot(x='rho_W', y='value', hue='Parameter', \n", 274 | " data=long_df.query('Parameter in @var_list'),\n", 275 | " ax=ax)\n", 276 | " ax.set_xlabel('$\\\\rho_W$')\n", 277 | " \n", 278 | " return f, ax" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "metadata": {}, 284 | "source": [ 285 | "### Figure 10(a)" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 13, 291 | "metadata": {}, 292 | "outputs": [ 293 | { 294 | "data": { 295 | "image/png": "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\n", 296 | "text/plain": [ 297 | "
" 298 | ] 299 | }, 300 | "metadata": { 301 | "needs_background": "light" 302 | }, 303 | "output_type": "display_data" 304 | } 305 | ], 306 | "source": [ 307 | "var_list = ['b_W', 'b_X', 'b_Y', 'a', 'sig_Y']\n", 308 | "f, ax = plot_vars(var_list, results_df)\n", 309 | "\n", 310 | "plt.legend(title='Parameters', loc='upper right', labels=['$\\\\sigma_Y$', '$\\\\beta_W$', '$\\\\beta_X$', '$\\\\beta_Y$', '$\\\\alpha$'])\n", 311 | "#plt.savefig(\"figs/id_example_parameters.pdf\")\n", 312 | "plt.show()" 313 | ] 314 | }, 315 | { 316 | "cell_type": "markdown", 317 | "metadata": {}, 318 | "source": [ 319 | "### Figure 10(b)" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": 14, 325 | "metadata": {}, 326 | "outputs": [ 327 | { 328 | "data": { 329 | "image/png": "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\n", 330 | "text/plain": [ 331 | "
" 332 | ] 333 | }, 334 | "metadata": { 335 | "needs_background": "light" 336 | }, 337 | "output_type": "display_data" 338 | } 339 | ], 340 | "source": [ 341 | "var_list = ['g_a']\n", 342 | "f, ax = plot_vars(var_list, results_df)\n", 343 | "\n", 344 | "# Plot the value of gamma when W is used \n", 345 | "ax.axhline(y=g_w, xmin=0, xmax=1, label='g_w', color='green')\n", 346 | "\n", 347 | "plt.legend(title='Parameters', loc='upper right')\n", 348 | " #colors=['green', 'blue'])\n", 349 | "#plt.savefig(\"figs/id_example_gamma.pdf\")\n", 350 | "plt.show()" 351 | ] 352 | } 353 | ], 354 | "metadata": { 355 | "kernelspec": { 356 | "display_name": "Python 3", 357 | "language": "python", 358 | "name": "python3" 359 | }, 360 | "language_info": { 361 | "codemirror_mode": { 362 | "name": "ipython", 363 | "version": 3 364 | }, 365 | "file_extension": ".py", 366 | "mimetype": "text/x-python", 367 | "name": "python", 368 | "nbconvert_exporter": "python", 369 | "pygments_lexer": "ipython3", 370 | "version": "3.7.9" 371 | } 372 | }, 373 | "nbformat": 4, 374 | "nbformat_minor": 4 375 | } 376 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment1/experiment1-plot.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(dplyr) 3 | 4 | # Import and convert data 5 | df = read_csv("experiment1-data.csv") 6 | method.names = c("ar" = "AR(A)", "cross" = "xPAR(W, Z)", "par" = "PAR(W)", "ols" = "OLS") 7 | df$method <- factor(df$method, levels=names(method.names), labels=method.names) 8 | 9 | # Dataframe containing theoretical values 10 | df.theo <- df %>% subset(n == "theo") %>% select(method, value, x) 11 | 12 | # Create data frame containing finite sample values 13 | df <- df %>% 14 | subset(n != "theo" & method %in% c("xPAR(W, Z)", "PAR(W)")) %>% 15 | transform(n = factor(as.integer(n))) 16 | levels(df$n) <- paste0("$n = ", levels(df$n), "$") 17 | 18 | # Plot 19 | p <- ggplot(df, aes(x=x, y=value, colour=method, fill=method)) + 20 | geom_line(data=df.theo, aes(linetype=method, colour=method), size=0.8) + 21 | stat_summary(geom="line", fun=median, alpha=1, size=1)+ 22 | stat_summary(geom="ribbon", fun.data=median_hilow, fun.args=list(conf.int=0.5), size=0.1, alpha=0.1, show.legend = F) + 23 | coord_cartesian(ylim=c(0.65, 2.5)) + 24 | scale_linetype_manual(values=c("22", "26", "22", "22"), breaks = c("PAR(W)"), labels="", name="Population")+ 25 | scale_color_brewer(palette = "Dark2", name="Method", breaks = levels(df.theo$method), labels = method.names) + 26 | scale_fill_brewer(palette = "Dark2", name="Method", labels = method.names) + 27 | labs(y="MSPE under $do(A:=\\nu)$", 28 | x="Signal-to-variance ratio") + 29 | scale_x_continuous(breaks = c(0:4)/4, labels=c("0\\%", "25\\%", "50\\%", "75\\%", "100\\%")) + 30 | theme_bw(base_size=9) + 31 | theme(legend.position = "bottom", 32 | legend.spacing.y = unit(0.05, 'cm'), 33 | legend.spacing.x = unit(0.05, 'cm'), 34 | legend.margin=margin(c(-10,0,-5, 0)), 35 | legend.key.width = unit(0.3,"cm"), 36 | legend.title = element_text(size = 9), 37 | plot.margin = margin(0, 0, 0, 0)) + 38 | guides(color=guide_legend(order=2, title=NULL), 39 | linetype=guide_legend(order=1, override.aes = list(lty = "22"))) + 40 | facet_wrap(~n) 41 | print(p) 42 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment1/experiment1-simulate.py: -------------------------------------------------------------------------------- 1 | ### Loading libraries 2 | import pandas as pd 3 | import numpy as np 4 | from tqdm import tqdm 5 | 6 | # Load file tools.py and population_estimators from parent folder 7 | import os, sys, inspect 8 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 9 | parentdir = os.path.dirname(currentdir) 10 | sys.path.insert(0,parentdir) 11 | 12 | from tools import ols, ar, get_mse, cross, simulate 13 | from population_estimators import pack_params, gamma_par, gamma_ar, gamma_ols, gamma_cross, get_mse_v 14 | 15 | """ 16 | This file simulates data for the experiment in Section 5.1 17 | See the appendix Section E.1 for details on this experiment 18 | """ 19 | 20 | ### Set seed 21 | np.random.seed(1) 22 | 23 | ### Dict with dimensions of variables 24 | d = {"A": 3, "W": 3, 'Z': 3, "Y": 1, "X": 3, "H": 1} 25 | # We store the joint dimension of the outcomes X, Y and H as d['O'] 26 | d['O'] = d['X'] + d['Y'] + d['H'] 27 | 28 | ### Specify locations of parameters. 29 | # E.g. c['X'] specifies indices of (Y, X, H) containing X 30 | c = {"Y": [0], "X": [1, 2, 3]} 31 | 32 | ### Create parameter matrix M 33 | M = np.array([[1, 0, -2], 34 | [0, 2, 1], 35 | [-1, 3, 0], 36 | [2, 2, -3], 37 | [0, -2, 2]]) 38 | 39 | ### Create parameter matrix B 40 | B = np.zeros((d['O'], d['O'])) 41 | B[0] = np.array([0, -2, 2, 0, 1]) 42 | B[3] = np.array([3, 0, 0, 0, 1]) 43 | 44 | # Pack parameters in dict 45 | pars = {'M': M, 46 | 'B': B, 47 | 'beta': np.diag([1.0, 1, 1]), 48 | 'beta_z': np.diag([1.0, 1, 1])} 49 | 50 | # Fix lambda 51 | lamb = 5 52 | 53 | 54 | # 1) Simulate 55 | results = [] 56 | 57 | # We label the population means with a number to avoid type-errors (but they are computed by closed form equations, not a sample) 58 | theo = 1e9 59 | 60 | # Select fixed intervention direction 61 | v = np.array([[-4, 0.5, 1.0]]).T 62 | 63 | # Normalize to unit length, scale to trust region boundary, and upscale by 20% 64 | v = 1.2*v*np.sqrt(1+lamb)/np.sqrt(v.T@v) 65 | 66 | # Select points over x-axis 67 | x_ax = np.arange(1, 21)/20 68 | for x in tqdm(x_ax): 69 | # Compute s^2 70 | s2 = (1-x)/x 71 | 72 | # Pack parameter inputs to population mean functions 73 | params = pack_params(pars, c, d, np.sqrt(s2)) 74 | 75 | # We only the population version once at every value of s^2 76 | results.append([theo, x] + [get_mse_v(gamma_ar(params, lamb), v, params, c), 77 | get_mse_v(gamma_par(params, lamb), v, params, c), 78 | get_mse_v(gamma_cross(params, lamb), v, params, c), 79 | get_mse_v(gamma_ols(params), v, params, c)]) 80 | 81 | # Loop over simulations 82 | for n in [250, 2500]: 83 | for _ in range(5000): 84 | # Simulate training data 85 | data = simulate(n, d, pars, noise_W=np.sqrt(s2), noise_Z=np.sqrt(s2)) 86 | X, Y, A, W, Z = data['X'], data['Y'], data['A'], data['W'], data['Z'] 87 | 88 | # Compute estimators from training data 89 | gammas = { 90 | 'ar': ar(X, Y, A, lamb=lamb), 91 | "par": ar(X, Y, W, lamb=lamb), 92 | "cross": cross(X, Y, W, Z, lamb=lamb), 93 | "ols": ols(X, Y)} 94 | 95 | # Simulate test data from intervention do(A:=v) 96 | test_data = simulate(n, d, pars, noise_W=np.sqrt(s2), noise_Z=np.sqrt(s2), v=v) 97 | 98 | # Append results 99 | results.append([n, x] + [get_mse(test_data, gamma) 100 | for gamma in gammas.values()]) 101 | 102 | # Store data in dataframe 103 | df = pd.DataFrame(np.array(results), columns=["n", "x"] + list(gammas.keys())) 104 | df = df.melt(["x", "n"], var_name="method") 105 | # Encode population values as "theo" instead of 10e9 (Panda handles, but numpy didnt) 106 | df['n'] = df['n'].replace(theo, "theo") 107 | 108 | # Plotting data is done with ggplot in R. See file 'experiment1-plot.R' 109 | df.to_csv("experiment1-data.csv") 110 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment2/experiment2-plot.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(dplyr) 3 | 4 | # Import and convert data 5 | df = read_csv("experiment2-data.csv") 6 | # The assumed signal-to-variance ratio 7 | svr = 0.4 8 | 9 | # Transform names 10 | var.names <- c("actual"="PAR($W$) MSPE, true", "belief" = "PAR($W$) MSPE, est.", "ols" = "OLS MSPE") 11 | df$variable <- factor(df$variable, levels = names(var.names), labels=var.names) 12 | 13 | # Plot 14 | p <- ggplot(df, aes(x=x, y=value, colour=variable, fill=variable, lty="Default")) + 15 | stat_summary(geom="line", fun=median, alpha=1, size=0.8, show.legend=T) + 16 | stat_summary(data=df, geom="ribbon", fun.data=median_hilow, fun.args=list(conf.int=0.5), size=0.00, alpha=0.1, show.legend=F)+ 17 | geom_vline(mapping=aes(xintercept=svr, lty="Assumed SVR"), size=0.6, show.legend=F) + 18 | labs(y="MSPE", x="True signal-to-variance ratio") + 19 | scale_x_continuous(breaks = (1:4)/4, labels = c("25\\%", "50\\%", "75\\%", "100\\%")) + 20 | scale_linetype_manual(values = c("22", "solid"), breaks="Assumed SVR", name=NULL) + 21 | scale_color_brewer(palette="Dark2", aesthetics = c("color", "fill"), name=NULL) + 22 | theme_bw(base_size=9) + 23 | theme(legend.margin = margin(0,0,0,0), 24 | legend.spacing.y = unit(3, 'pt'), 25 | plot.margin = margin(0, 0, 0, 0)) 26 | print(p) 27 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment2/experiment2-simulate.py: -------------------------------------------------------------------------------- 1 | ### Loading libraries 2 | import pandas as pd 3 | import numpy as np 4 | from tqdm import tqdm 5 | 6 | # Load file tools.py and population_estimators from parent folder 7 | import os, sys, inspect 8 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 9 | parentdir = os.path.dirname(currentdir) 10 | sys.path.insert(0,parentdir) 11 | 12 | from tools import Id, ols, ar, inv, simulate 13 | 14 | 15 | """ 16 | This file simulates data for the experiment in Section 5.2 17 | See the appendix Section E.2 for details on this experiment 18 | """ 19 | 20 | ### Set seed 21 | np.random.seed(1) 22 | 23 | ### Dict with dimensions of variables 24 | d = {"A": 3, "W": 3, 'Z': 3, "Y": 1, "X": 3, "H": 1} 25 | 26 | # We store the joint dimension of the outcomes X, Y and H as d['O'] 27 | d['O'] = d['X'] + d['Y'] + d['H'] 28 | 29 | ### Specify locations of parameters. 30 | # E.g. cX specifies indices of (Y, X, H) containing X 31 | cY = [0]; cX = [1, 2, 3] 32 | 33 | ### Create parameter matrix M 34 | M = np.array([[1, 0, -2], 35 | [0, 2, 1], 36 | [-1, 3, 0], 37 | [2, 2, -3], 38 | [0, -2, 2]]) 39 | 40 | ### Create parameter matrix B 41 | B = np.zeros((d['O'], d['O'])) 42 | B[0] = np.array([0, -2, 2, 0, 1]) 43 | B[3] = np.array([3, 0, 0, 0, 1]) 44 | 45 | # Pack parameters 46 | pars = {'M': M, 47 | 'B': B, 48 | 'beta': np.diag([1.0, 1, 1]), 49 | 'beta_z': np.diag([1.0, 1, 1]) 50 | } 51 | 52 | # Fix lambda 53 | lamb = 5 54 | 55 | # 1) Simulate 56 | results = [] 57 | 58 | # For computing the actual worst case losses, we compute the inverse of the matrix Id - B 59 | IB = inv(Id(d['O']) - B) 60 | 61 | # We specify the assumed signal-to-variance ratio to 40% 62 | svr = 0.4 63 | 64 | # Loop over x axis 65 | x_ax = np.arange(1, 21)/20 66 | for x in tqdm(x_ax): 67 | 68 | # The noise variance s^2 is set as (1-x)/x, where x is the actual signal-to-variance 69 | s2 = (1-x)/x 70 | for n in [1000]: 71 | for _ in range(1000): 72 | # Simulate data set 73 | data = simulate(n, d, pars, noise_W=np.sqrt(s2), noise_Z=np.sqrt(s2)) 74 | X, Y, A, W, Z = data['X'], data['Y'], data['A'], data['W'], data['Z'] 75 | 76 | # Compute believed worst case loss (PAR) 77 | gamma_par = ar(X, Y, W, lamb=lamb) 78 | R = (Y - gamma_par.T@X) 79 | WCL_belief = np.mean(R**2) + lamb*R@W.T@inv(W@W.T)@W@R.T/n 80 | 81 | 82 | # Find actual worst case intervention v in Omega_W(0.5) set (PAR) 83 | w_gamma = (IB[cY,] - gamma_par.T@IB[cX,]).T 84 | b_gamma = (w_gamma.T@M).T 85 | v = b_gamma*np.sqrt((1+lamb*svr)/(b_gamma.T@b_gamma)) 86 | 87 | # Compute actual worst case loss (PAR) 88 | WCL_actual = (b_gamma.T@v)**2 + w_gamma.T@w_gamma 89 | 90 | # Find worst case intervention and loss (OLS) 91 | gamma_ols = ols(X, Y) 92 | w_gamma = (IB[cY,] - gamma_ols.T@IB[cX,]).T 93 | b_gamma = (w_gamma.T@M).T 94 | v = b_gamma*np.sqrt((1+lamb*svr)/(b_gamma.T@b_gamma)) 95 | WCL_ols = (b_gamma.T@v)**2 + w_gamma.T@w_gamma 96 | 97 | # Append results 98 | results.append([n, x, WCL_belief[0,0], WCL_actual[0,0], WCL_ols[0,0]]) 99 | 100 | # Convert to data frame 101 | df = pd.DataFrame(np.array(results), columns=("n", "x", "belief", "actual", "ols")) 102 | df = df.melt(["x", "n"]) 103 | 104 | # Plotting data is done with ggplot in R. See file 'experiment2-plot.R' 105 | df.to_csv("experiment2-data.csv") 106 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment3/experiment3-plot.R: -------------------------------------------------------------------------------- 1 | library(ggplot2) 2 | library(dplyr) 3 | library(readr) 4 | library(tikzDevice) 5 | 6 | # Naming 7 | method.names <- c("par5" = "PAR($W$)", "cross5" = "xPAR($W,Z$)", "ar5" = "AR($A$)") 8 | predictor.names <- c("$X_{\\rm{causal}}^1$", "$X_{\\rm{causal}}^2$", "$X_{\\rm{causal}}^3$", 9 | "$X_{\\rm{anti-causal}}^1$", "$X_{\\rm{anti-causal}}^2$", "$X_{\\rm{anti-causal}}^3$") 10 | 11 | # Read data 12 | df = read_csv("experiment3-data.csv") %>% 13 | transform(Method = factor(Method, levels=names(method.names)), Causal <- factor(Causal)) %>% 14 | subset(Method!="ar5") %>% 15 | group_by(X.coord, Method, Causal) %>% 16 | summarize(sd = sd(abs(Weight)), Weight = mean(abs(Weight)), ymin = Weight - sd, ymax = Weight+sd) %>% 17 | ungroup() 18 | 19 | # Plot 20 | p <- ggplot(subset(df, Method != "ar5"), aes(x=X.coord, y=abs(Weight), fill = Method)) + 21 | geom_bar(stat="identity", position=position_dodge(width=0.9)) + 22 | geom_errorbar(aes(ymin = ymin,ymax = ymax), stat="identity", position = position_dodge(width=0.9), size=0.1, width=0.25) + 23 | coord_flip() + 24 | labs(x=NULL, y = "$|$Regression coefficients$|$") + 25 | scale_x_continuous(breaks=c(1:6), labels=predictor.names) + 26 | scale_fill_brewer(palette="Dark2", breaks=c("cross5", "par5"), labels=as_labeller(method.names)) + 27 | theme_bw(base_size=9) + 28 | theme(legend.key.size = unit(0.8,"line")) + 29 | guides(fill = guide_legend(title=NULL, override.aes = list(size = 0.3))) 30 | print(p) 31 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment3/experiment3-simulate.py: -------------------------------------------------------------------------------- 1 | ### Loading libraries 2 | import pandas as pd 3 | import numpy as np 4 | from tqdm import tqdm 5 | 6 | # Load file tools from parent folder 7 | import os, sys, inspect 8 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 9 | parentdir = os.path.dirname(currentdir) 10 | sys.path.insert(0,parentdir) 11 | from tools import Id, ar, cross, cb, simulate 12 | 13 | """ 14 | This file simulates data for the experiment in Section 5.3 15 | See the appendix Section E.3 for details on this experiment 16 | """ 17 | 18 | ### Set seed 19 | np.random.seed(1) 20 | 21 | ### Dict with dimensions of variables 22 | d = {"A": 6, 23 | "W": 6, 24 | 'Z': 6, 25 | "Y": 1, 26 | "X": 6, 27 | "H": 1} 28 | # We store the joint dimension of the outcomes X, Y and H as d['O'] 29 | d['O'] = d['X'] + d['Y'] + d['H'] 30 | 31 | ### Specify locations of parameters. 32 | # E.g. cX1 specifies indices of (Y, X, H) containing X1 33 | # cA2 specifies indices of A containing A2 34 | cY = [0] 35 | cX1 = [1, 2, 3] 36 | cX2 = [4, 5, 6] 37 | cA1 = [0, 1, 2] 38 | cA2 = [3, 4, 5] 39 | 40 | # Create parameter matrix M 41 | M = np.zeros((d['O'], d['A'])) 42 | M[np.ix_(cX1,cA1)] = np.ones((len(cX1), len(cA1))) 43 | M[np.ix_(cX2,cA2)] = np.ones((len(cX1), len(cA1))) 44 | 45 | # Create parameter matrix B 46 | B = np.zeros((d['O'], d['O'])) 47 | B[np.ix_(cY, cX1)] = [1/4, 1/4, 1/4] 48 | B[np.ix_(cX2, cY)] = np.array([[4, 4, 4]]).T 49 | 50 | # Pack parameters in dict 51 | pars = {'M': M, 52 | 'B': B, 53 | 'beta': Id(d['A']), 54 | 'beta_z': Id(d['A']) 55 | } 56 | 57 | # Variable 'noise' specifies the error variance of the proxies. 58 | # The experiment regards considers a larger variance in proxy of A1 than in A2. 59 | noise = np.diag([1 for i in cA1] + [3 for i in cA2]) 60 | 61 | 62 | # 1) Simulate 63 | n = 10000 64 | out = None 65 | 66 | # Loop repeats experiment 1000 times 67 | for _ in tqdm(range(1000)): 68 | # Simulate data 69 | data = simulate(n, d, pars, noise_W=noise) 70 | X, Y, A, W, Z = data['X'], data['Y'], data['A'], data['W'], data['Z'] 71 | 72 | # Fit estimators 73 | par5 = ar(X, Y, W, lamb=5) 74 | cross5 = cross(X, Y, W, Z, lamb=5) 75 | ar5 = ar(X, Y, A, lamb=5) 76 | 77 | # Cast to dataframe 78 | df = pd.DataFrame(cb(par5, cross5, ar5), columns=["par5", "cross5", "ar5"]) 79 | 80 | # 'Causal' encodes for whether predictor is causal. 81 | df['Causal'] = 3*[1] + 3*[0] 82 | # 'X.coord' encodes variable number (e.g. X_1, X_2, X_3, ...) 83 | df['X.coord'] = np.arange(1, 7) 84 | # Melt dataframe 85 | df = df.melt(id_vars=["Causal", "X.coord"], var_name="Method", value_name = "Weight") 86 | # Add results from this simulation to overall results 87 | out = df if out is None else pd.concat((out, df)) 88 | 89 | # Plotting data is done with ggplot in R. See file 'experiment3-plot.R' 90 | out.to_csv("experiment3-data.csv") 91 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment4/experiment4-plot.R: -------------------------------------------------------------------------------- 1 | library(ggplot2) 2 | library(dplyr) 3 | 4 | # Load data 5 | df = read.csv2("experiment4-data.csv", sep=",", stringsAsFactors = T) 6 | df$mse <- as.numeric(as.character(df$mse)) 7 | 8 | # Name methods 9 | method.names = c( 10 | "ols" = "OLS", 11 | "ar" = "AR(A)", 12 | "tar" = "TAR(A)" 13 | ) 14 | df$method <- factor(df$method, levels=c('tar', 'ar', 'ols')) 15 | 16 | # Label settings 17 | setup.labeller = c( 18 | "correct_shift" = "Anticipated shift occuring", 19 | "incorrect_shift" = "Anticipated shift not occuring" 20 | ) 21 | 22 | # Make plots 23 | p <- ggplot(df, aes(x=mse, color=method, fill=method)) + 24 | geom_histogram(aes(y = ..density..), position="identity", bins = 100, alpha=0.8) + 25 | facet_wrap(~setup, ncol=1, labeller = as_labeller(setup.labeller)) + 26 | scale_fill_brewer(palette = "Dark2", name="Method", labels = method.names) + 27 | scale_color_brewer(palette = "Dark2", name="Method", labels = method.names) + 28 | theme_bw(base_size=9) + 29 | theme(legend.title=element_blank(), 30 | panel.grid = element_blank(), 31 | legend.key.size = unit(0.8,"line"), 32 | legend.spacing.y = unit(0.1, 'cm'), 33 | plot.margin = margin(0,0,0,0)) + 34 | labs(x="MSPE", y=NULL) 35 | print(p) 36 | -------------------------------------------------------------------------------- /synthetic-experiments/experiment4/experiment4-simulate.py: -------------------------------------------------------------------------------- 1 | ### Loading libraries 2 | import numpy as np 3 | import pandas as pd 4 | from tqdm import tqdm 5 | 6 | # Load file tools.py and population_estimators from parent folder 7 | import os, sys, inspect 8 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 9 | parentdir = os.path.dirname(currentdir) 10 | sys.path.insert(0,parentdir) 11 | from tools import Id, simulate, ols, ar, get_mse, tar 12 | 13 | """ 14 | This file simulates data for the experiment in Section 5.4 15 | See the appendix Section E.4 for details on this experiment 16 | """ 17 | 18 | 19 | ### Set seed 20 | np.random.seed(1) 21 | 22 | 23 | ### Dict with dimensions of variables 24 | d = {"A": 2, "W": 2, 'Z': 2, "Y": 1, "X": 2, "H": 1} 25 | # We store the joint dimension of the outcomes X, Y and H as d['O'] 26 | d['O'] = d['X'] + d['Y'] + d['H'] 27 | 28 | ### Create parameter matrix M 29 | M = np.array([[2, 1], 30 | [0, 1], 31 | [2, 2], 32 | [0, 3]]) 33 | 34 | ### Create parameter matrix B 35 | B = np.array([[ 0, -0.06, 0.07, 0.04], 36 | [ 0.05, 0, 0.19, 0.03], 37 | [ 0.11, -0.11, 0, 0.1 ], 38 | [-0.02, 0.02, 0.09, 0]]) 39 | 40 | ### Pack parameters 41 | pars = {'M': M, 42 | 'B': B, 43 | 'beta': Id(d['A'], d['W'])} 44 | 45 | 46 | ### Specify rotation and mean-shift of the anticipated distribution shift 47 | rotat = np.diag([np.sqrt(2), 1]) 48 | shift = np.array([0, 2]) 49 | # Store setups 50 | sim_setups = { 51 | "incorrect_shift": {"eta_tar": shift, "eta_sim": np.zeros(2), 52 | "cov_A_tar": rotat@rotat.T, "cov_A_sim": Id(d['A'])}, 53 | "correct_shift": {"eta_tar": shift, "eta_sim": shift, 54 | "cov_A_tar": rotat@rotat.T, "cov_A_sim": rotat@rotat.T} 55 | } 56 | # We select lambda such that B B.T + eta eta.T <= (1+lambda) Id (EAA.T = Id) 57 | eta = shift.reshape(-1, 1) 58 | lamb = np.linalg.eigvals(rotat@rotat.T + eta@eta.T).max() - 1 59 | 60 | ### Simulate 61 | results = [] 62 | n = 10000 # training size 63 | m = 10000 # test size 64 | for i in tqdm(range(10000)): 65 | # Simulate training data 66 | data = simulate(n, d, pars) 67 | A, X, Y, W, Z = data['A'], data['X'], data['Y'], data['W'], data['Z'] 68 | 69 | # Fit estimators 70 | lamb = 4 71 | gammas = {"ols": ols(X, Y), 72 | "ar": ar(X, Y, A, lamb=lamb)} 73 | 74 | for setup, s in sim_setups.items(): 75 | # Get simulation settings 76 | eta_tar, eta_sim, cov_A_tar, cov_A_sim = list(s.values()) 77 | 78 | # Target etstimator 79 | gamma_tar, alpha_tar = tar(X, Y, A, Sigma = cov_A_tar, nu=eta_tar) 80 | 81 | # Simulate test data 82 | _data = simulate(m, d, pars, v = eta_sim, cov_A = cov_A_sim) #v=eta_sim 83 | 84 | # Append results 85 | results.append([ 86 | get_mse(_data, gammas['ols']), 87 | get_mse(_data, gammas['ar']), 88 | get_mse(_data, gamma_tar, alpha=alpha_tar), 89 | setup 90 | ]) 91 | 92 | # Cast results to data frame 93 | df = pd.DataFrame(results, columns=["ols", "ar", "tar", "setup"]).melt(id_vars = "setup", value_name="mse", var_name="method") 94 | 95 | # Plotting data is done with ggplot in R. See file 'experiment4-plot.R' 96 | df.to_csv("experiment4-data.csv") 97 | -------------------------------------------------------------------------------- /synthetic-experiments/population_estimators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | inv = np.linalg.inv; Id = lambda d, d2=None: np.eye(d, d2) 3 | """ 4 | For some experiments we need the population versions of estimators instead of sample estimators. 5 | This file has functions to such estimators, based on moments that are derived from parameter matrices. 6 | """ 7 | 8 | def pack_params(pars, c, d, noise_W): 9 | """ 10 | The population estimators are computed based on moments. 11 | We compute the moments based on the parameters MB and the noise distributions. 12 | 13 | In the naming below, we the variable XW corresponds to E[XW^T] etc. 14 | We let O denote the stacked outcome (Y, X, W) 15 | """ 16 | # Unpack inputs. c is a vector containing indices (e.g. O_1 is Y, O_2 is X_1,...) 17 | cY = c['Y']; cX = c['X'] # input 18 | beta, M, B = pars['beta'], pars['M'], pars['B'] 19 | 20 | # Store the inverse of the matrix (Id - B) 21 | IB = inv(Id(d['O']) - B) 22 | 23 | # Compute moments relating to the outcome O 24 | OA = IB@M 25 | OW = IB@M@beta 26 | OO = IB@(M@M.T + Id(d['O']))@IB.T 27 | 28 | # Compute moment E[WW^T] 29 | if len(np.shape(noise_W)) == 0: 30 | WW = beta.T@beta + noise_W**2*Id(d['W']) 31 | else: 32 | WW = beta.T@beta + noise_W@noise_W.T 33 | 34 | # Compute covariance of A and cross proxies 35 | AA = Id(d['A']) 36 | ZW = beta.T@beta 37 | #Covariances relating to X 38 | XX = OO[cX][:,cX]; XY = OO[cX][:,cY]; XW = OW[cX]; XA = OA[cX]; XZ = XW 39 | # Covariances relating to Y 40 | YW = OW[cY]; YA = OA[cY]; YZ = YW 41 | 42 | # Return dict with all moments 43 | return {"IB": IB, "OA": OA, "OW":OW, "OO":OO, "WW":WW, "AA":AA, "ZW":ZW,"XX":XX, 44 | "XY":XY, "XW":XW, "XA":XA, "XZ":XZ,"YW":YW, "YA":YA, "YZ":YZ, "M":M} 45 | 46 | # OLS 47 | def gamma_ols(params): 48 | # Unpack moments 49 | XX, XY = params['XX'], params['XY'] 50 | # Return estimator based on moments 51 | return inv(XX)@XY 52 | 53 | # Proxy anchor regression 54 | def gamma_par(params, lamb): 55 | # Unpack moments 56 | XX, XW, WW, XY, YW = params['XX'], params['XW'], params['WW'], params['XY'], params['YW'] 57 | # Return estimator based on moments 58 | return inv(XX + lamb*XW@inv(WW)@XW.T)@(XY + lamb*XW@inv(WW)@YW.T) 59 | 60 | # Anchor regression 61 | def gamma_ar(params, lamb): 62 | # Unpack moments 63 | XX, AA, XY, XA, YA = params['XX'], params['AA'], params['XY'], params['XA'], params['YA'] 64 | # Return estimator based on moments 65 | return inv(XX + lamb*XA@inv(AA)@XA.T)@(XY + lamb*XA@inv(AA)@YA.T) 66 | 67 | def gamma_cross(params, lamb): 68 | # Unpack moments 69 | XX, XW, ZW, XZ, XY, YW, YZ = params['XX'], params['XW'], params['ZW'], params['XZ'], params['XY'], params['YW'], params['YZ'] 70 | # Compute "denominator" (left-side inverse) 71 | denom = 2*XX + lamb*(XW@inv(ZW)@XZ.T + XZ@inv(ZW).T@XW.T) 72 | # Compute "numerator" 73 | num = 2*XY + lamb*(XW@inv(ZW)@YZ.T + XZ@inv(ZW).T@YW.T) 74 | return inv(denom)@num 75 | 76 | def get_mse_v(gamma, v, params, c): 77 | """Compute the population mse of using an estimator gamma""" 78 | # Unpack 79 | M, IB = params['M'], params['IB'] 80 | cY, cX = c['Y'], c['X'] 81 | # Compute w_gamma 82 | w_gamma = (IB[cY,] - gamma.T@IB[cX,]).T 83 | # Output population MSE 84 | return (w_gamma.T@M@v@v.T@M.T@w_gamma + w_gamma.T@w_gamma)[0,0] 85 | -------------------------------------------------------------------------------- /synthetic-experiments/tools.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | """ 4 | This file contains implementations of estimators and a function to simulate data, used for experiments in the paper 5 | """ 6 | 7 | ### Define convinience functions 8 | inv = np.linalg.inv; norm = np.linalg.norm; Id = lambda d, d2=None: np.eye(d, d2) 9 | # Column and row bind 10 | def cb(*args): return np.concatenate(args, axis=1) 11 | def rb(*args): return np.concatenate(args, axis=0) 12 | 13 | # Multivariate gaussian 14 | N = lambda d, n=1: np.random.normal(size=(d, n)) 15 | 16 | ### Simulate function 17 | def simulate(n, d, pars, v=None, shift=0, cov_A=None, noise_W=None, noise_Z=None): 18 | """ Simulate data from parameters. Dimensions d_A x n, etc. """ 19 | # Unpack 20 | d_A = d['A']; d_W = d['W']; d_X = d['X']; d_Y = d['Y']; d_O = d['O']; d_Z = d['Z'] 21 | M = pars['M']; B = pars['B']; beta = pars['beta'] 22 | 23 | # If no noise is provided, use spherical unit variance as proxy noise 24 | if noise_W is None: noise_W = Id(d_W) 25 | elif len(np.shape(noise_W)) == 0: noise_W = noise_W*Id(d_W) 26 | # If no noise for secondary proxy is supplied, use the same as W 27 | if noise_Z is None: noise_Z = noise_W 28 | elif len(np.shape(noise_Z)) == 0: noise_Z = noise_Z*Id(d_Z) 29 | 30 | # If no parameter beta_z is provided, use same as beta_W 31 | if "beta_z" in pars.keys(): beta_z = pars['beta_z'] 32 | else: beta_z = beta 33 | 34 | # If covariance matrix for A is given, use this, else use spherical noise 35 | # Since changed covariance matrices are only used for targeted, assumes also a v is given 36 | if cov_A is not None: 37 | A = np.random.multivariate_normal(v, cov=cov_A, size=n).T 38 | else: 39 | # Use either the intervention v tiled several times (fixed A), or a mean-zero gaussian 40 | A = (N(d_A, n) if v is None else np.tile(np.reshape(v, (d_A, 1)), n)) + shift 41 | # Compute the outcome O = (Y, X, H) 42 | O = inv(Id(d['O'])-B)@(M@A + N(d_O, n)) 43 | Y, X, H = np.split(O, [d_Y, d_Y+d_X]) 44 | #Simulate proxies 45 | W = beta.T@A + noise_W@N(d_W, n) 46 | Z = beta_z.T@A + noise_Z@N(d_Z, n) 47 | return {'A': A, 'W': W, 'Y': Y, 'X': X, 'H': H, 'Z': Z} 48 | 49 | # Mean function 50 | def E(X): 51 | return X.mean(axis=1).reshape(-1, 1) 52 | 53 | ### Estimators 54 | # Ordinary least squares 55 | def ols(X, Y, intercept=False): 56 | if intercept: 57 | X = np.concatenate((np.ones((1, X.shape[1])), X)) 58 | return inv(X@X.T)@X@Y.T 59 | 60 | # Anchor regression estimator 61 | def ar(X, Y, A, lamb=1, intercept=False): 62 | if intercept: 63 | X = np.concatenate((np.ones((1, X.shape[1])), X)) 64 | return inv(X@X.T + lamb*X@A.T@inv(A@A.T)@A@X.T)@(X@Y.T + lamb*X@A.T@inv(A@A.T)@A@Y.T) 65 | 66 | # Cross estimator 67 | def cross(X, Y, W, Z, lamb=1): 68 | ZW = inv(Z@W.T) 69 | denom = 2*X@X.T + lamb*(X@W.T@ZW@Z@X.T + X@Z.T@ZW.T@W@X.T) 70 | num = 2*X@Y.T + lamb*(X@W.T@ZW@Z@Y.T + X@Z.T@ZW.T@W@Y.T) 71 | return inv(denom)@num 72 | 73 | # Targeted anchor regression, targeted to covariance Sigma and mean shift nu 74 | def tar(X, Y, A, Sigma, nu=0): 75 | # Get dimensions 76 | d_A, n = A.shape 77 | if len(np.shape(nu)) == 0: 78 | nu = np.tile(nu, d_A).reshape(d_A, 1) 79 | 80 | # Compute alpha and gamma 81 | gamma = inv(X@X.T/n + X@A.T@inv(A@A.T)@(Sigma - A@A.T/n)@inv(A@A.T)@A@X.T)@(X@Y.T/n + X@A.T@inv(A@A.T)@(Sigma - A@A.T/n)@inv(A@A.T)@A@Y.T) 82 | alpha = (Y - gamma.T@X)@A.T@inv(A@A.T)@nu 83 | return gamma, alpha 84 | 85 | # IV estimator 86 | def iv(X, Y, A): 87 | return inv(X@A.T@inv(A@A.T)@A@X.T)@X@A.T@inv(A@A.T)@A@Y.T 88 | 89 | # Function to evaluate the prediction MSE of a dataset and some gamma 90 | def get_mse(data, gamma, alpha = 0): 91 | return ((data['Y'] - gamma.T@data['X'] - alpha)**2).mean() 92 | --------------------------------------------------------------------------------