Skip to content

Commit f66f854

Browse files
committed
Merge branch 'interaction-terms' of https://github.com/CITCOM-project/CausalTestingFramework into interaction-terms
2 parents 4e06f34 + 17b8692 commit f66f854

File tree

6 files changed

+71
-66
lines changed

6 files changed

+71
-66
lines changed

causal_testing/testing/causal_test_outcome.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,13 @@ class SomeEffect(CausalTestOutcome):
2828
"""An extension of TestOutcome representing that the expected causal effect should not be zero."""
2929

3030
def apply(self, res: CausalTestResult) -> bool:
31-
if res.test_value.type == "ate":
32-
return (0 < res.ci_low() < res.ci_high()) or (res.ci_low() < res.ci_high() < 0)
33-
if res.test_value.type == "coefficient":
34-
ci_low = res.ci_low() if isinstance(res.ci_low(), Iterable) else [res.ci_low()]
35-
ci_high = res.ci_high() if isinstance(res.ci_high(), Iterable) else [res.ci_high()]
36-
return any(0 < ci_low < ci_high or ci_low < ci_high < 0 for ci_low, ci_high in zip(ci_low, ci_high))
3731
if res.test_value.type == "risk_ratio":
38-
return (1 < res.ci_low() < res.ci_high()) or (res.ci_low() < res.ci_high() < 1)
32+
return any(
33+
1 < ci_low < ci_high or ci_low < ci_high < 1 for ci_low, ci_high in zip(res.ci_low(), res.ci_high()))
34+
if res.test_value.type in ('coefficient', 'ate'):
35+
return any(
36+
0 < ci_low < ci_high or ci_low < ci_high < 0 for ci_low, ci_high in zip(res.ci_low(), res.ci_high()))
37+
3938
raise ValueError(f"Test Value type {res.test_value.type} is not valid for this TestOutcome")
4039

4140

@@ -52,23 +51,20 @@ def __init__(self, atol: float = 1e-10, ctol: float = 0.05):
5251
self.ctol = ctol
5352

5453
def apply(self, res: CausalTestResult) -> bool:
55-
if res.test_value.type == "ate":
56-
return (res.ci_low() < 0 < res.ci_high()) or (abs(res.test_value.value) < self.atol)[0]
57-
if res.test_value.type == "coefficient":
58-
ci_low = res.ci_low() if isinstance(res.ci_low(), Iterable) else [res.ci_low()]
59-
ci_high = res.ci_high() if isinstance(res.ci_high(), Iterable) else [res.ci_high()]
54+
if res.test_value.type == "risk_ratio":
55+
return any(ci_low < 1 < ci_high or np.isclose(value, 1.0, atol=self.atol) for ci_low, ci_high, value in
56+
zip(res.ci_low(), res.ci_high(), res.test_value.value))
57+
if res.test_value.type in ('coefficient', 'ate'):
6058
value = res.test_value.value if isinstance(res.ci_high(), Iterable) else [res.test_value.value]
61-
value = value[0] if isinstance(value[0], pd.Series) else value
6259
return (
63-
sum(
64-
not ((ci_low < 0 < ci_high) or abs(v) < self.atol)
65-
for ci_low, ci_high, v in zip(ci_low, ci_high, value)
66-
)
67-
/ len(value)
68-
< self.ctol
60+
sum(
61+
not ((ci_low < 0 < ci_high) or abs(v) < self.atol)
62+
for ci_low, ci_high, v in zip(res.ci_low(), res.ci_high(), value)
63+
)
64+
/ len(value)
65+
< self.ctol
6966
)
70-
if res.test_value.type == "risk_ratio":
71-
return (res.ci_low() < 1 < res.ci_high()) or np.isclose(res.test_value.value, 1.0, atol=self.atol)
67+
7268
raise ValueError(f"Test Value type {res.test_value.type} is not valid for this TestOutcome")
7369

7470

causal_testing/testing/causal_test_result.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(
2727
self,
2828
estimator: Estimator,
2929
test_value: TestValue,
30-
confidence_intervals: [float, float] = None,
30+
confidence_intervals: [pd.Series, pd.Series] = None,
3131
effect_modifier_configuration: {Variable: Any} = None,
3232
adequacy=None,
3333
):
@@ -100,15 +100,15 @@ def ci_low(self):
100100
"""Return the lower bracket of the confidence intervals."""
101101
if self.confidence_intervals:
102102
if isinstance(self.confidence_intervals[0], pd.Series):
103-
return self.confidence_intervals[0][0]
103+
return self.confidence_intervals[0].to_list()
104104
return self.confidence_intervals[0]
105105
return None
106106

107107
def ci_high(self):
108108
"""Return the higher bracket of the confidence intervals."""
109109
if self.confidence_intervals:
110110
if isinstance(self.confidence_intervals[1], pd.Series):
111-
return self.confidence_intervals[1][0]
111+
return self.confidence_intervals[1].to_list()
112112
return self.confidence_intervals[1]
113113
return None
114114

causal_testing/testing/estimators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,8 @@ def _run_linear_regression(self) -> RegressionResultsWrapper:
453453
def _get_confidence_intervals(self, model, treatment):
454454
confidence_intervals = model.conf_int(alpha=self.alpha, cols=None)
455455
ci_low, ci_high = (
456-
confidence_intervals[0].loc[treatment],
457-
confidence_intervals[1].loc[treatment],
456+
pd.Series(confidence_intervals[0].loc[treatment]),
457+
pd.Series(confidence_intervals[1].loc[treatment]),
458458
)
459459
return [ci_low, ci_high]
460460

tests/data/scarf_data.csv

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
length_in,large_gauge,color,completed
2+
55,1,orange,1
3+
55,0,orange,1
4+
55,0,brown,1
5+
60,0,brown,1
6+
60,0,grey,0
7+
70,0,grey,1
8+
70,0,orange,0
9+
82,1,grey,1
10+
82,0,brown,0
11+
82,0,orange,0
12+
82,1,brown,0

tests/testing_tests/test_causal_test_outcome.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def test_Positive_fail_ci(self):
107107
ctr = CausalTestResult(
108108
estimator=self.estimator,
109109
test_value=test_value,
110-
confidence_intervals=[-1, 1],
110+
confidence_intervals=[pd.Series(-1), pd.Series(1)],
111111
effect_modifier_configuration=None,
112112
)
113113
ev = Positive()
@@ -151,7 +151,7 @@ def test_Negative_fail_ci(self):
151151
ctr = CausalTestResult(
152152
estimator=self.estimator,
153153
test_value=test_value,
154-
confidence_intervals=[-1, 1],
154+
confidence_intervals=[pd.Series(-1), pd.Series(1)],
155155
effect_modifier_configuration=None,
156156
)
157157
ev = Negative()
@@ -173,7 +173,7 @@ def test_exactValue_pass_ci(self):
173173
ctr = CausalTestResult(
174174
estimator=self.estimator,
175175
test_value=test_value,
176-
confidence_intervals=[4, 6],
176+
confidence_intervals=[pd.Series(4), pd.Series(6)],
177177
effect_modifier_configuration=None,
178178
)
179179
ev = ExactValue(5, 0.1)
@@ -199,7 +199,7 @@ def test_invalid(self):
199199
ctr = CausalTestResult(
200200
estimator=self.estimator,
201201
test_value=test_value,
202-
confidence_intervals=[4.8, 6.7],
202+
confidence_intervals=[pd.Series(4.8), pd.Series(6.7)],
203203
effect_modifier_configuration=None,
204204
)
205205
with self.assertRaises(ValueError):
@@ -216,7 +216,7 @@ def test_someEffect_pass_coefficient(self):
216216
ctr = CausalTestResult(
217217
estimator=self.estimator,
218218
test_value=test_value,
219-
confidence_intervals=[4.8, 6.7],
219+
confidence_intervals=[pd.Series(4.8), pd.Series(6.7)],
220220
effect_modifier_configuration=None,
221221
)
222222
self.assertTrue(SomeEffect().apply(ctr))
@@ -227,7 +227,7 @@ def test_someEffect_pass_ate(self):
227227
ctr = CausalTestResult(
228228
estimator=self.estimator,
229229
test_value=test_value,
230-
confidence_intervals=[4.8, 6.7],
230+
confidence_intervals=[pd.Series(4.8), pd.Series(6.7)],
231231
effect_modifier_configuration=None,
232232
)
233233
self.assertTrue(SomeEffect().apply(ctr))
@@ -238,7 +238,7 @@ def test_someEffect_pass_rr(self):
238238
ctr = CausalTestResult(
239239
estimator=self.estimator,
240240
test_value=test_value,
241-
confidence_intervals=[4.8, 6.7],
241+
confidence_intervals=[pd.Series(4.8), pd.Series(6.7)],
242242
effect_modifier_configuration=None,
243243
)
244244
self.assertTrue(SomeEffect().apply(ctr))
@@ -249,7 +249,7 @@ def test_someEffect_fail(self):
249249
ctr = CausalTestResult(
250250
estimator=self.estimator,
251251
test_value=test_value,
252-
confidence_intervals=[-0.1, 0.2],
252+
confidence_intervals=[pd.Series(-0.1), pd.Series(0.2)],
253253
effect_modifier_configuration=None,
254254
)
255255
self.assertFalse(SomeEffect().apply(ctr))
@@ -260,7 +260,7 @@ def test_someEffect_str(self):
260260
ctr = CausalTestResult(
261261
estimator=self.estimator,
262262
test_value=test_value,
263-
confidence_intervals=[-0.1, 0.2],
263+
confidence_intervals=[pd.Series(-0.1), pd.Series(0.2)],
264264
effect_modifier_configuration=None,
265265
)
266266
ev = SomeEffect()
@@ -274,8 +274,8 @@ def test_someEffect_str(self):
274274
"adjustment_set": set(),
275275
"effect_estimate": 0,
276276
"effect_measure": "ate",
277-
"ci_low": -0.1,
278-
"ci_high": 0.2,
277+
"ci_low": [-0.1],
278+
"ci_high": [0.2],
279279
},
280280
)
281281

@@ -284,7 +284,7 @@ def test_someEffect_dict(self):
284284
ctr = CausalTestResult(
285285
estimator=self.estimator,
286286
test_value=test_value,
287-
confidence_intervals=[-0.1, 0.2],
287+
confidence_intervals=[pd.Series(-0.1), pd.Series(0.2)],
288288
effect_modifier_configuration=None,
289289
)
290290
ev = SomeEffect()
@@ -298,8 +298,8 @@ def test_someEffect_dict(self):
298298
"adjustment_set": set(),
299299
"effect_estimate": 0,
300300
"effect_measure": "ate",
301-
"ci_low": -0.1,
302-
"ci_high": 0.2,
301+
"ci_low": [-0.1],
302+
"ci_high": [0.2],
303303
},
304304
)
305305

tests/testing_tests/test_estimators.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
CausalForestEstimator,
88
LogisticRegressionEstimator,
99
InstrumentalVariableEstimator,
10-
CubicSplineRegressionEstimator
10+
CubicSplineRegressionEstimator,
1111
)
1212
from causal_testing.specification.variable import Input
1313
from causal_testing.utils.validation import CausalValidator
@@ -78,21 +78,7 @@ class TestLogisticRegressionEstimator(unittest.TestCase):
7878

7979
@classmethod
8080
def setUpClass(cls) -> None:
81-
cls.scarf_df = pd.DataFrame(
82-
[
83-
{"length_in": 55, "large_gauge": 1, "color": "orange", "completed": 1},
84-
{"length_in": 55, "large_gauge": 0, "color": "orange", "completed": 1},
85-
{"length_in": 55, "large_gauge": 0, "color": "brown", "completed": 1},
86-
{"length_in": 60, "large_gauge": 0, "color": "brown", "completed": 1},
87-
{"length_in": 60, "large_gauge": 0, "color": "grey", "completed": 0},
88-
{"length_in": 70, "large_gauge": 0, "color": "grey", "completed": 1},
89-
{"length_in": 70, "large_gauge": 0, "color": "orange", "completed": 0},
90-
{"length_in": 82, "large_gauge": 1, "color": "grey", "completed": 1},
91-
{"length_in": 82, "large_gauge": 0, "color": "brown", "completed": 0},
92-
{"length_in": 82, "large_gauge": 0, "color": "orange", "completed": 0},
93-
{"length_in": 82, "large_gauge": 1, "color": "brown", "completed": 0},
94-
]
95-
)
81+
cls.scarf_df = pd.read_csv("tests/data/scarf_data.csv")
9682

9783
# Yes, this probably shouldn't be in here, but it uses the scarf data so it makes more sense to put it
9884
# here than duplicating the scarf data for a single test
@@ -231,7 +217,7 @@ def test_program_11_2(self):
231217
self.assertEqual(round(model.params["Intercept"] + 90 * model.params["treatments"], 1), 216.9)
232218

233219
# Increasing treatments from 90 to 100 should be the same as 10 times the unit ATE
234-
self.assertEqual(round(model.params["treatments"], 1), round(ate[0], 1))
220+
self.assertTrue(all(round(model.params["treatments"], 1) == round(ate_single, 1) for ate_single in ate))
235221

236222
def test_program_11_3(self):
237223
"""Test whether our linear regression implementation produces the same results as program 11.3 (p. 144)."""
@@ -251,7 +237,7 @@ def test_program_11_3(self):
251237
197.1,
252238
)
253239
# Increasing treatments from 90 to 100 should be the same as 10 times the unit ATE
254-
self.assertEqual(round(model.params["treatments"], 3), round(ate[0], 3))
240+
self.assertTrue(all(round(model.params["treatments"], 3) == round(ate_single, 3) for ate_single in ate))
255241

256242
def test_program_15_1A(self):
257243
"""Test whether our linear regression implementation produces the same results as program 15.1 (p. 163, 184)."""
@@ -329,6 +315,7 @@ def test_program_15_no_interaction(self):
329315
# terms_to_square = ["age", "wt71", "smokeintensity", "smokeyrs"]
330316
# for term_to_square in terms_to_square:
331317
ate, [ci_low, ci_high] = linear_regression_estimator.estimate_coefficient()
318+
332319
self.assertEqual(round(ate[0], 1), 3.5)
333320
self.assertEqual([round(ci_low[0], 1), round(ci_high[0], 1)], [2.6, 4.3])
334321

@@ -416,12 +403,11 @@ def test_program_11_2_with_robustness_validation(self):
416403

417404

418405
class TestCubicSplineRegressionEstimator(TestLinearRegressionEstimator):
419-
420406
@classmethod
421-
422407
def setUpClass(cls):
423408

424409
super().setUpClass()
410+
425411
def test_program_11_3_cublic_spline(self):
426412

427413
"""Test whether the cublic_spline regression implementation produces the same results as program 11.3 (p. 162).
@@ -431,8 +417,7 @@ def test_program_11_3_cublic_spline(self):
431417

432418
df = self.chapter_11_df.copy()
433419

434-
cublic_spline_estimator = CubicSplineRegressionEstimator(
435-
"treatments", 1, 0, set(), "outcomes", 3, df)
420+
cublic_spline_estimator = CubicSplineRegressionEstimator("treatments", 1, 0, set(), "outcomes", 3, df)
436421

437422
model = cublic_spline_estimator._run_linear_regression()
438423

@@ -453,8 +438,6 @@ def test_program_11_3_cublic_spline(self):
453438
self.assertAlmostEqual(ate_1[0] * 2, ate_2[0])
454439

455440

456-
457-
458441
class TestCausalForestEstimator(unittest.TestCase):
459442
"""Test the linear regression estimator against the programming exercises in Section 2 of Hernán and Robins [1].
460443
@@ -527,15 +510,29 @@ def setUpClass(cls) -> None:
527510
df = pd.DataFrame({"X1": np.random.uniform(-1000, 1000, 1000), "X2": np.random.uniform(-1000, 1000, 1000)})
528511
df["Y"] = 2 * df["X1"] - 3 * df["X2"] + 2 * df["X1"] * df["X2"] + 10
529512
cls.df = df
513+
cls.scarf_df = pd.read_csv("tests/data/scarf_data.csv")
530514

531515
def test_X1_effect(self):
532516
"""When we fix the value of X2 to 0, the effect of X1 on Y should become ~2 (because X2 terms are cancelled)."""
533-
x2 = Input("X2", float)
534517
lr_model = LinearRegressionEstimator(
535-
"X1", 1, 0, {"X2"}, "Y", effect_modifiers={x2.name: 0}, formula="Y ~ X1 + X2 + (X1 * X2)", df=self.df
518+
"X1", 1, 0, {"X2"}, "Y", effect_modifiers={"x2": 0}, formula="Y ~ X1 + X2 + (X1 * X2)", df=self.df
536519
)
537520
test_results = lr_model.estimate_ate()
538521
ate = test_results[0][0]
539522
self.assertAlmostEqual(ate, 2.0)
540523

524+
def test_categorical_confidence_intervals(self):
525+
lr_model = LinearRegressionEstimator(
526+
treatment="color",
527+
control_value=None,
528+
treatment_value=None,
529+
adjustment_set={},
530+
outcome="length_in",
531+
df=self.scarf_df,
532+
)
533+
coefficients, [ci_low, ci_high] = lr_model.estimate_coefficient()
541534

535+
# The precise values don't really matter. This test is primarily intended to make sure the return type is correct.
536+
self.assertTrue(coefficients.round(2).equals(pd.Series({"color[T.grey]": 0.92, "color[T.orange]": -4.25})))
537+
self.assertTrue(ci_low.round(2).equals(pd.Series({"color[T.grey]": -22.12, "color[T.orange]": -25.58})))
538+
self.assertTrue(ci_high.round(2).equals(pd.Series({"color[T.grey]": 23.95, "color[T.orange]": 17.08})))

0 commit comments

Comments
 (0)