CausalEstimate 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. causalestimate-0.1.0/.github/workflows/format.yml +25 -0
  2. causalestimate-0.1.0/.github/workflows/lint.yml +26 -0
  3. causalestimate-0.1.0/.github/workflows/unittest.yml +27 -0
  4. causalestimate-0.1.0/.gitignore +6 -0
  5. causalestimate-0.1.0/CausalEstimate/api/estimator.py +132 -0
  6. causalestimate-0.1.0/CausalEstimate/estimators/aipw.py +31 -0
  7. causalestimate-0.1.0/CausalEstimate/estimators/functional/aipw.py +24 -0
  8. causalestimate-0.1.0/CausalEstimate/estimators/functional/ipw.py +106 -0
  9. causalestimate-0.1.0/CausalEstimate/estimators/functional/matching.py +42 -0
  10. causalestimate-0.1.0/CausalEstimate/estimators/functional/tmle.py +41 -0
  11. causalestimate-0.1.0/CausalEstimate/estimators/ipw.py +37 -0
  12. causalestimate-0.1.0/CausalEstimate/estimators/matching.py +29 -0
  13. causalestimate-0.1.0/CausalEstimate/estimators/tmle.py +35 -0
  14. causalestimate-0.1.0/CausalEstimate/filter/filter.py +10 -0
  15. causalestimate-0.1.0/CausalEstimate/matching/assignment.py +50 -0
  16. causalestimate-0.1.0/CausalEstimate/matching/distance.py +44 -0
  17. causalestimate-0.1.0/CausalEstimate/matching/helpers.py +22 -0
  18. causalestimate-0.1.0/CausalEstimate/matching/matching.py +91 -0
  19. causalestimate-0.1.0/CausalEstimate/simulation/binary_simulation.py +92 -0
  20. causalestimate-0.1.0/CausalEstimate/utils/helpers.py +31 -0
  21. causalestimate-0.1.0/CausalEstimate/vis/plotting.py +0 -0
  22. causalestimate-0.1.0/CausalEstimate.egg-info/PKG-INFO +25 -0
  23. causalestimate-0.1.0/CausalEstimate.egg-info/SOURCES.txt +38 -0
  24. causalestimate-0.1.0/CausalEstimate.egg-info/dependency_links.txt +1 -0
  25. causalestimate-0.1.0/CausalEstimate.egg-info/requires.txt +15 -0
  26. causalestimate-0.1.0/CausalEstimate.egg-info/top_level.txt +1 -0
  27. causalestimate-0.1.0/PKG-INFO +25 -0
  28. causalestimate-0.1.0/README.md +93 -0
  29. causalestimate-0.1.0/pyproject.toml +44 -0
  30. causalestimate-0.1.0/requirements.txt +6 -0
  31. causalestimate-0.1.0/setup.cfg +4 -0
  32. causalestimate-0.1.0/tests/__init__.py +0 -0
  33. causalestimate-0.1.0/tests/helpers/simulate_data.py +0 -0
  34. causalestimate-0.1.0/tests/test_functional/__init__.py +0 -0
  35. causalestimate-0.1.0/tests/test_functional/test_aipw.py +22 -0
  36. causalestimate-0.1.0/tests/test_functional/test_matching.py +58 -0
  37. causalestimate-0.1.0/tests/test_matching/__init__.py +0 -0
  38. causalestimate-0.1.0/tests/test_matching/test_matching.py +229 -0
  39. causalestimate-0.1.0/tests/test_simulation/__init__.py +0 -0
  40. causalestimate-0.1.0/tests/test_simulation/test_binary_simulation.py +31 -0
@@ -0,0 +1,25 @@
1
+ # This workflow will install Python dependencies, run tests and lint with a single version of Python
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3
+ name: Formatting using black
4
+ on:
5
+ push:
6
+ branches: [ "main" ]
7
+ pull_request:
8
+ branches: [ "main" ]
9
+ permissions:
10
+ contents: read
11
+ jobs:
12
+ format:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - uses: actions/setup-python@v4
17
+ with:
18
+ python-version: '3.11'
19
+ cache: 'pip' # caching pip dependencies
20
+ - name: Install dependencies
21
+ run: |
22
+ pip install black>=23.10.1
23
+ - name: Run black
24
+ run: |
25
+ black --check CausalEstimate tests
@@ -0,0 +1,26 @@
1
+ # This workflow will install Python dependencies and lint with a single version of Python
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3
+ name: Lint using flake8
4
+ on:
5
+ push:
6
+ branches: [ "main" ]
7
+ pull_request:
8
+ branches: [ "main" ]
9
+ permissions:
10
+ contents: read
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - uses: actions/setup-python@v4
17
+ with:
18
+ python-version: '3.11'
19
+ cache: 'pip' # caching pip dependencies
20
+ - name: Install dependencies
21
+ run: |
22
+ pip install flake8>=6.1.0
23
+ - name: Lint with flake8
24
+ run: |
25
+ # Check syntax errors
26
+ flake8 CausalEstimate tests --count --select=E9,F63,F7,F82,U100,E711,E712,E713,E714,E721,F401,F402,F405,F811,F821,F822,F823,F831,F841,F901, --show-source --statistics
@@ -0,0 +1,27 @@
1
+ name: 'Unittests'
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: read
10
+
11
+ jobs:
12
+ tests:
13
+ runs-on: "ubuntu-latest"
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup dependencies.
19
+ run: |
20
+ python -m venv .venv
21
+ source .venv/bin/activate
22
+ pip install -r requirements.txt
23
+
24
+ - name: Running tests
25
+ run: |
26
+ source .venv/bin/activate
27
+ python -m unittest
@@ -0,0 +1,6 @@
1
+ *.egg-info
2
+ *.pyc
3
+ dist/
4
+ __pycache__/
5
+ notebooks/test*
6
+ CausalEstimate/_version.py
@@ -0,0 +1,132 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from CausalEstimate.estimators.tmle import TMLE
4
+
5
+ # !TODO: Write test for all functions
6
+
7
+
8
+ class Estimator:
9
+ def __init__(self, methods=None, effect_type="ATE", **kwargs):
10
+ """
11
+ Initialize the Estimator class with one or more methods.
12
+
13
+ Args:
14
+ methods (list or str): A list of estimator method names (e.g., ["AIPW", "TMLE"])
15
+ or a single method name (e.g., "AIPW").
16
+ effect_type (str): The type of effect to estimate (e.g., "ATE", "ATT").
17
+ **kwargs: Additional keyword arguments for each estimator.
18
+ """
19
+ if methods is None:
20
+ methods = ["AIPW"] # Default to AIPW if no method is provided.
21
+
22
+ # Allow single method or list of methods
23
+ self.methods = methods if isinstance(methods, list) else [methods]
24
+ self.effect_type = effect_type
25
+ self.estimators = self._initialize_estimators(effect_type, **kwargs)
26
+
27
+ def _initialize_estimators(self, effect_type, **kwargs):
28
+ """
29
+ Initialize the specified estimators based on the methods provided.
30
+ """
31
+ estimators = {
32
+ "TMLE": TMLE,
33
+ # Add other estimators as needed
34
+ }
35
+ initialized_estimators = []
36
+
37
+ for method in self.methods:
38
+ if method in estimators:
39
+ initialized_estimators.append(
40
+ estimators[method](effect_type=effect_type, **kwargs)
41
+ )
42
+ else:
43
+ raise ValueError(f"Method '{method}' is not supported.")
44
+
45
+ return initialized_estimators
46
+
47
+ def _validate_inputs(self, df, treatment_col, outcome_col):
48
+ """
49
+ Validate the input DataFrame and columns for all estimators.
50
+ """
51
+ required_columns = [treatment_col, outcome_col]
52
+ # Check if all required columns exist in the DataFrame
53
+ for col in required_columns:
54
+ if col not in df.columns:
55
+ raise ValueError(f"Column '{col}' is missing from the DataFrame.")
56
+
57
+ # Additional validation logic if needed (e.g., check for NaN, etc.)
58
+ if df[treatment_col].isnull().any():
59
+ raise ValueError(f"Treatment column '{treatment_col}' contains NaN values.")
60
+ if df[outcome_col].isnull().any():
61
+ raise ValueError(f"Outcome column '{outcome_col}' contains NaN values.")
62
+
63
+ def _bootstrap_sample(self, df: pd.DataFrame, n_bootstraps: int):
64
+ """
65
+ Generate bootstrap samples.
66
+ """
67
+ n = len(df)
68
+ return [df.sample(n=n, replace=True) for _ in range(n_bootstraps)]
69
+
70
+ def compute_effect(
71
+ self,
72
+ df,
73
+ treatment_col,
74
+ outcome_col,
75
+ bootstrap=False,
76
+ n_bootstraps=100,
77
+ **kwargs,
78
+ ):
79
+ """
80
+ Compute treatment effects using the initialized estimators.
81
+ Can also run bootstrap on all estimators if specified.
82
+
83
+ Args:
84
+ df (pd.DataFrame): The input DataFrame.
85
+ treatment_col (str): The name of the treatment column.
86
+ outcome_col (str): The name of the outcome column.
87
+ bootstrap (bool): Whether to run bootstrapping for the estimators.
88
+ n_bootstraps (int): Number of bootstrap iterations.
89
+ sample_size (int): Size of each bootstrap sample.
90
+ **kwargs: Additional arguments for the estimators.
91
+
92
+ Returns:
93
+ dict: A dictionary where keys are method names and values are computed effects (and optionally standard errors).
94
+ """
95
+ # Validate input data and columns
96
+ self._validate_inputs(df, treatment_col, outcome_col)
97
+
98
+ results = {method.__class__.__name__: [] for method in self.estimators}
99
+
100
+ if bootstrap:
101
+ # Perform bootstrapping
102
+ bootstrap_samples = self._bootstrap_sample(df, n_bootstraps)
103
+
104
+ for sample in bootstrap_samples:
105
+ # For each bootstrap sample, compute the effect using all estimators
106
+ for estimator in self.estimators:
107
+ method_name = type(estimator).__name__
108
+ effect = estimator.compute_effect(
109
+ sample, treatment_col, outcome_col, **kwargs
110
+ )
111
+ results[method_name].append(effect)
112
+
113
+ # After collecting all bootstrap samples, compute the mean and standard error for each estimator
114
+ final_results = {}
115
+ for method_name, effects in results.items():
116
+ effects_array = np.array(effects)
117
+ mean_effect = np.mean(effects_array)
118
+ std_err = np.std(effects_array)
119
+ final_results[method_name] = (mean_effect, std_err)
120
+
121
+ else:
122
+ # If no bootstrapping, compute the effect directly for each estimator
123
+ for estimator in self.estimators:
124
+ method_name = type(estimator).__name__
125
+ effect = estimator.compute_effect(
126
+ df, treatment_col, outcome_col, **kwargs
127
+ )
128
+ results[method_name] = effect
129
+
130
+ final_results = results
131
+
132
+ return final_results
@@ -0,0 +1,31 @@
1
+ from CausalEstimate.estimators.functional.aipw import compute_aipw_ate
2
+ import pandas as pd
3
+
4
+
5
+ class IPW:
6
+ def __init__(self, effect_type="ATE", **kwargs):
7
+ self.effect_type = effect_type
8
+ self.kwargs = kwargs
9
+
10
+ def compute_effect(
11
+ self,
12
+ df: pd.DataFrame,
13
+ treatment_col: str,
14
+ outcome_col: str,
15
+ ps_col: str,
16
+ predicted_outcome_treated_col: str,
17
+ predicted_outcome_control_col: str,
18
+ ) -> float:
19
+ """
20
+ Compute the effect using the functional IPW.
21
+ Available effect types: ATE, ATT, RR, RRT
22
+ """
23
+
24
+ A = df[treatment_col]
25
+ Y = df[outcome_col]
26
+ ps = df[ps_col]
27
+
28
+ if self.effect_type == "ATE":
29
+ return compute_aipw_ate(A, Y, ps)
30
+ else:
31
+ raise ValueError(f"Effect type '{self.effect_type}' is not supported.")
@@ -0,0 +1,24 @@
1
+ """
2
+ Augmented Inverse Probability of Treatment Weighting (AIPW)
3
+ References:
4
+
5
+ ATE:
6
+ Glynn, Adam N., and Kevin M. Quinn.
7
+ "An introduction to the augmented inverse propensity weighted estimator."
8
+ Political analysis 18.1 (2010): 36-56.
9
+ note: This also provides a variance estimator for the AIPW estimator.
10
+ """
11
+
12
+ from CausalEstimate.estimators.functional.ipw import compute_ipw_ate
13
+
14
+
15
+ def compute_aipw_ate(A, Y, ps, Y0_hat, Y1_hat):
16
+ """
17
+ Augmented Inverse Probability of Treatment Weighting (AIPW) for ATE.
18
+ A: treatment assignment, Y: outcome, ps: propensity score
19
+ Y0_hat: P[Y|A=0], Y1_hat: P[Y|A=1]
20
+ """
21
+ ate_ipw = compute_ipw_ate(A, Y, ps)
22
+ adjustment_factor = (A - ps) / (ps * (1 - ps))
23
+ ate = ate_ipw - adjustment_factor * ((1 - ps) * Y1_hat + ps * Y0_hat)
24
+ return ate.mean()
@@ -0,0 +1,106 @@
1
+ """
2
+ Inverse Probability Weighting (IPW) estimators
3
+
4
+ References:
5
+ ATE:
6
+ Estimation of Average Treatment Effects Honors Thesis Peter Zhang
7
+ https://lsa.umich.edu/content/dam/econ-assets/Econdocs/HonorsTheses/Estimation%20of%20Average%20Treatment%20Effects.pdf
8
+
9
+ Austin, P.C., 2016. Variance estimation when using inverse probability of
10
+ treatment weighting (IPTW) with survival analysis.
11
+ Statistics in medicine, 35(30), pp.5642-5655.
12
+
13
+ ATT:
14
+ Reifeis et. al. (2022).
15
+ On variance of the treatment effect in the treated when estimated by
16
+ inverse probability weighting.
17
+ American Journal of Epidemiology, 191(6), 1092-1097.
18
+ https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9271225/
19
+ """
20
+
21
+
22
+ def compute_ipw_risk_ratio(A, Y, ps):
23
+ """
24
+ Relative Risk
25
+ A: treatment assignment, Y: outcome, ps: propensity score
26
+ """
27
+ mu_1, mu_0 = compute_mean_potential_outcomes(A, Y, ps)
28
+ return mu_1 / mu_0
29
+
30
+
31
+ def compute_ipw_ate(A, Y, ps):
32
+ """
33
+ Average Treatment Effect
34
+ A: treatment assignment, Y: outcome, ps: propensity score
35
+ """
36
+ mu1, mu0 = compute_mean_potential_outcomes(A, Y, ps)
37
+ return mu1 - mu0
38
+
39
+
40
+ def compute_mean_potential_outcomes(A, Y, ps):
41
+ """
42
+ Compute E[Y|A=1] and E[Y|A=0] for Y=0/1
43
+ """
44
+ mu_1 = (A * Y / ps).mean()
45
+ mu_0 = ((1 - A) * Y / (1 - ps)).mean()
46
+ return mu_1, mu_0
47
+
48
+
49
+ def compute_ipw_ate_stabilized(A, Y, ps):
50
+ """
51
+ Given by Austin (2016)
52
+ Average Treatment Effect with stabilized weights.
53
+ A: treatment assignment, Y: outcome, ps: propensity score
54
+ """
55
+ W = compute_stabilized_ate_weights(A, ps)
56
+ Y1_weighed = W * A * Y
57
+ Y0_weighed = W * (1 - A) * Y
58
+ return Y1_weighed - Y0_weighed
59
+
60
+
61
+ def compute_ipw_att(A, Y, ps):
62
+ """
63
+ Average Treatment Effect on the Treated with stabilized weights.
64
+ Reifeis et. al. (2022).
65
+ A: treatment assignment, Y: outcome, ps: propensity score
66
+ """
67
+ mu_1, mu_0 = compute_mean_potential_outcomes_treated(A, Y, ps)
68
+ return mu_1 - mu_0
69
+
70
+
71
+ def compute_ipw_risk_ratio_treated(A, Y, ps):
72
+ """
73
+ Relative Risk of the Treated with stabilized weights. Reifeis et. al. (2022)
74
+ A: treatment assignment, Y: outcome, ps: propensity score
75
+ """
76
+ mu_1, mu_0 = compute_mean_potential_outcomes_treated(A, Y, ps)
77
+ return mu_1 / mu_0
78
+
79
+
80
+ def compute_mean_potential_outcomes_treated(A, Y, ps):
81
+ """
82
+ Compute E[Y|A=1] for Y=0/1
83
+ """
84
+ W = compute_stabilized_att_weights(A, ps)
85
+ mu_1 = (W * A * Y).sum() / (W * A).sum()
86
+ mu_0 = (W * (1 - A) * Y).sum() / (W * (1 - A)).sum()
87
+ return mu_1, mu_0
88
+
89
+
90
+ def compute_stabilized_ate_weights(A, ps):
91
+ """
92
+ Compute the (stabilized) weights for the ATE estimator
93
+ Austin (2016)
94
+ """
95
+ weight_treated = A.mean() * A / ps
96
+ weight_control = (1 - A).mean() * (1 - A) / (1 - ps)
97
+ return weight_treated + weight_control
98
+
99
+
100
+ def compute_stabilized_att_weights(A, ps):
101
+ """
102
+ Compute the (stabilized) weights for the ATT estimator
103
+ As given in the web appendix of Reifeis et. al. (2022)
104
+ """
105
+ h = ps / (1 - ps)
106
+ return A + (1 - A) * h
@@ -0,0 +1,42 @@
1
+ import pandas as pd
2
+ from CausalEstimate.utils.helpers import check_required_columns
3
+
4
+
5
+ def compute_matching_ate(
6
+ Y: pd.Series,
7
+ matching_df: pd.DataFrame,
8
+ treated_col: str = "treated_pid",
9
+ control_col: str = "control_pid",
10
+ ) -> float:
11
+ """
12
+ Compute the effect using matching with vectorized Pandas operations.
13
+
14
+ Args:
15
+ Y (pd.Series): Outcomes for both treated and control units.
16
+ matching_df (pd.DataFrame): DataFrame containing matching results with columns
17
+ treated_col and control_col indicating the
18
+ matched treated and control unit IDs.
19
+
20
+ Returns:
21
+ float: The estimated ATE using the matched data.
22
+ """
23
+ check_required_columns(matching_df, [treated_col, control_col])
24
+ # Merge the treated outcomes with the control outcomes
25
+ merged_df = matching_df.merge(
26
+ Y.rename("treated_outcome"), left_on=treated_col, right_index=True
27
+ )
28
+ merged_df = merged_df.merge(
29
+ Y.rename("control_outcome"), left_on=control_col, right_index=True
30
+ )
31
+
32
+ # Compute the average control outcome for each treated unit using groupby
33
+ avg_control_outcomes = merged_df.groupby(treated_col)["control_outcome"].mean()
34
+
35
+ # Compute the difference between treated outcome and control outcome for each treated unit
36
+ treated_outcomes = Y[
37
+ avg_control_outcomes.index
38
+ ] # Ensure we're aligning with grouped treated pids
39
+ diffs = treated_outcomes - avg_control_outcomes
40
+
41
+ # Return the average difference (ATE)
42
+ return diffs.mean()
@@ -0,0 +1,41 @@
1
+ from scipy.special import expit, logit
2
+ from statsmodels.api import add_constant
3
+ from statsmodels.genmod.families import Binomial
4
+ from statsmodels.genmod.generalized_linear_model import GLM
5
+
6
+
7
+ def compute_tmle_ate(A, Y, ps, Y0_hat, Y1_hat, Yhat):
8
+ """
9
+ Estimate the average treatment effect using the targeted maximum likelihood estimation (TMLE) method.
10
+ A: treatment assignment, Y: outcome, ps: propensity score,
11
+ Y0_hat: P[Y|A=0], Y1_hat: P[Y|A=1], Yhat: P[Y]
12
+ """
13
+ epsilon = estimate_fluctuation_parameter(A, Y, ps, Yhat)
14
+ return update_ate_estimate(ps, Y0_hat, Y1_hat, epsilon)
15
+
16
+
17
+ def update_ate_estimate(ps, Y0_hat, Y1_hat, epsilon) -> tuple:
18
+ """Update the Q_star values using the fluctuation parameter epsilon."""
19
+ H_1 = 1 / ps
20
+ Q_star_1 = expit(logit(Y1_hat) + epsilon * H_1)
21
+
22
+ H_0 = 1 / (1 - ps)
23
+ Q_star_0 = expit(logit(Y0_hat) - epsilon * H_0)
24
+
25
+ return (Q_star_1 - Q_star_0).mean()
26
+
27
+
28
+ def estimate_fluctuation_parameter(A, Y, ps, Yhat) -> float:
29
+ """
30
+ Estimate the fluctuation parameter epsilon using a logistic regression model.
31
+ Returns the estimated epsilon.
32
+ """
33
+ # compute the clever covariate H
34
+ H = A / ps - (1 - A) / (1 - ps)
35
+
36
+ # Use logit of the current outcome as offset
37
+ offset = logit(Y)
38
+
39
+ # Fit the model with offset
40
+ model = GLM(Yhat, add_constant(H), family=Binomial(), offset=offset).fit()
41
+ return model.params[0]
@@ -0,0 +1,37 @@
1
+ from CausalEstimate.estimators.functional.ipw import (
2
+ compute_ipw_ate,
3
+ compute_ipw_ate_stabilized,
4
+ compute_ipw_att,
5
+ compute_ipw_risk_ratio_treated,
6
+ compute_ipw_risk_ratio,
7
+ )
8
+
9
+
10
+ class IPW:
11
+ def __init__(self, effect_type="ATE", **kwargs):
12
+ self.effect_type = effect_type
13
+ self.kwargs = kwargs
14
+
15
+ def compute_effect(self, df, treatment_col, outcome_col, ps_col) -> float:
16
+ """
17
+ Compute the effect using the functional IPW.
18
+ Available effect types: ATE, ATT, RR, RRT
19
+ """
20
+
21
+ A = df[treatment_col]
22
+ Y = df[outcome_col]
23
+ ps = df[ps_col]
24
+
25
+ if self.effect_type == "ATE":
26
+ if self.kwargs.get("stabilized", False):
27
+ return compute_ipw_ate(A, Y, ps)
28
+ else:
29
+ return compute_ipw_ate_stabilized(A, Y, ps)
30
+ elif self.effect_type == "ATT":
31
+ return compute_ipw_att(A, Y, ps)
32
+ elif self.effect_type == "RR":
33
+ return compute_ipw_risk_ratio(A, Y, ps)
34
+ elif self.effect_type == "RRT":
35
+ return compute_ipw_risk_ratio_treated(A, Y, ps)
36
+ else:
37
+ raise ValueError(f"Effect type '{self.effect_type}' is not supported.")
@@ -0,0 +1,29 @@
1
+ from CausalEstimate.matching.matching import match_optimal
2
+ from CausalEstimate.estimators.functional.matching import compute_matching_ate
3
+
4
+
5
+ class MATCHING:
6
+ def __init__(self, effect_type="ATE", **kwargs):
7
+ self.effect_type = effect_type
8
+ self.kwargs = kwargs
9
+
10
+ def compute_effect(self, df, treatment_col, outcome_col, ps_col) -> float:
11
+ """
12
+ Compute the effect using the functional IPW.
13
+ Available effect types: ATE, RR, OR
14
+ """
15
+
16
+ Y = df[outcome_col].values
17
+
18
+ df["index"] = df.index # temporary index column
19
+ matched = match_optimal(
20
+ df,
21
+ treatment_col=treatment_col,
22
+ ps_col=ps_col,
23
+ pid_col="index",
24
+ **self.kwargs,
25
+ )
26
+ if self.effect_type == "ATE":
27
+ return compute_matching_ate(Y, matched)
28
+ else:
29
+ raise ValueError(f"Effect type '{self.effect_type}' is not supported.")
@@ -0,0 +1,35 @@
1
+ from CausalEstimate.estimators.functional.tmle import compute_tmle_ate
2
+ import pandas as pd
3
+
4
+
5
+ class TMLE:
6
+ def __init__(self, effect_type="ATE", **kwargs):
7
+ self.effect_type = effect_type
8
+ self.kwargs = kwargs
9
+
10
+ def compute_effect(
11
+ self,
12
+ df: pd.DataFrame,
13
+ treatment_col: str,
14
+ outcome_col: str,
15
+ ps_col: str,
16
+ predicted_outcome_col: str,
17
+ predicted_outcome_treated_col: str,
18
+ predicted_outcome_control_col: str,
19
+ ) -> float:
20
+ """
21
+ Compute the effect using the functional IPW.
22
+ Available effect types: ATE
23
+ """
24
+
25
+ A = df[treatment_col]
26
+ Y = df[outcome_col]
27
+ ps = df[ps_col]
28
+ Y0_hat = df[predicted_outcome_control_col]
29
+ Y1_hat = df[predicted_outcome_treated_col]
30
+ Yhat = df[predicted_outcome_col]
31
+
32
+ if self.effect_type == "ATE":
33
+ return compute_tmle_ate(A, Y, ps, Y0_hat, Y1_hat, Yhat)
34
+ else:
35
+ raise ValueError(f"Effect type '{self.effect_type}' is not supported.")
@@ -0,0 +1,10 @@
1
+ import pandas as pd
2
+
3
+
4
+ def filter_by_column(df: pd.DataFrame, column: str, value: int) -> pd.DataFrame:
5
+ """
6
+ Filters the DataFrame by a specifeid column and value.
7
+ """
8
+ if column not in df.columns:
9
+ raise ValueError(f"Column '{column}' not found in the DataFrame.")
10
+ return df[df[column] == value].reset_index(drop=True)
@@ -0,0 +1,50 @@
1
+ from typing import Tuple
2
+ import numpy as np
3
+ import pandas as pd
4
+ from scipy.optimize import linear_sum_assignment
5
+ from scipy.sparse import csr_matrix
6
+ from scipy.sparse.csgraph import min_weight_full_bipartite_matching
7
+ from CausalEstimate.matching.helpers import is_sparse
8
+
9
+
10
+ def assign_controls(distance_matrix: np.array) -> Tuple[np.array, np.array]:
11
+ """
12
+ Assigns controls to treated individuals using either sparse or dense assignment.
13
+ Uses sparse assignment if the distance matrix is sparse; otherwise, falls back to dense assignment.
14
+ """
15
+ if is_sparse(distance_matrix):
16
+ try:
17
+ row_indices, col_indices = sparse_assignment(distance_matrix)
18
+ except ValueError:
19
+ raise ValueError("Cannot assign unique controls to treated.")
20
+ else:
21
+ distance_matrix[distance_matrix == 0] = np.inf
22
+ row_indices, col_indices = linear_sum_assignment(distance_matrix)
23
+
24
+ return row_indices, col_indices
25
+
26
+
27
+ def sparse_assignment(dist_mat: np.array) -> Tuple[np.array, np.array]:
28
+ """
29
+ Performs assignment using sparse matching.
30
+ """
31
+ sparse_d_mat = csr_matrix(dist_mat)
32
+ row_ind, col_ind = min_weight_full_bipartite_matching(sparse_d_mat)
33
+ return row_ind, col_ind
34
+
35
+
36
+ def validate_control_availability(
37
+ treated: pd.DataFrame, controls: pd.DataFrame, n_controls: int
38
+ ) -> None:
39
+ """
40
+ Validates whether there are enough controls to match the treated individuals.
41
+ Raises an error if there aren't enough controls.
42
+ """
43
+ if len(treated) == 0:
44
+ raise ValueError("No treated units have sufficient controls")
45
+ if len(controls) < n_controls * len(treated):
46
+ raise ValueError(
47
+ f"Not enough controls to match.\nN_controls: {len(controls)}\
48
+ N_treated: {len(treated)}\
49
+ Required N_controls: {n_controls*len(treated)} (n_controls x n_treated)"
50
+ )
@@ -0,0 +1,44 @@
1
+ from typing import Tuple
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+
7
+ def compute_distance_matrix(
8
+ treated: pd.DataFrame, control: pd.DataFrame, ps_col: str
9
+ ) -> np.array:
10
+ """
11
+ Computes the distance matrix (absolute differences) between treated and control individuals based on propensity scores.
12
+ Args:
13
+ treated_df (pd.DataFrame): DataFrame of treated individuals.
14
+ control_df (pd.DataFrame): DataFrame of control individuals.
15
+ ps_col (str): Column name for propensity scores.
16
+ Returns:
17
+ np.array: Distance matrix with treated individuals as rows and controls as columns.
18
+ """
19
+ treated_ps = treated[ps_col].to_numpy()
20
+ control_ps = control[ps_col].to_numpy()
21
+ dist_mat = np.abs(treated_ps.reshape(-1, 1) - control_ps.reshape(1, -1))
22
+ return dist_mat
23
+
24
+
25
+ def filter_treated_w_insufficient_controls(
26
+ dist_mat: np.array, treated_df: pd.DataFrame, n_controls: int
27
+ ) -> Tuple[np.array, pd.DataFrame]:
28
+ """
29
+ Filters out treated individuals who do not have enough valid controls based on the distance matrix.
30
+ Args:
31
+ distance_matrix (np.array): Distance matrix with treated individuals as rows and controls as columns.
32
+ zero entries indicate invalid matches.
33
+ treated_df (pd.DataFrame): DataFrame of treated individuals.
34
+ n_controls (int): Minimum number of controls required for each treated individual.
35
+ Returns:
36
+ Tuple[np.array, pd.DataFrame]: Updated distance matrix and treated DataFrame after filtering.
37
+ """
38
+ valid_controls_per_treated = np.count_nonzero(dist_mat, axis=1)
39
+ sufficient_controls_mask = valid_controls_per_treated >= n_controls
40
+
41
+ if np.any(~sufficient_controls_mask):
42
+ dist_mat = dist_mat[sufficient_controls_mask].reshape(-1, dist_mat.shape[1])
43
+ treated_df = treated_df[sufficient_controls_mask]
44
+ return dist_mat, treated_df