Skip to content

Commit d2ce313

Browse files
committed
more test coverage
1 parent 061ab17 commit d2ce313

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed

causalpy/tests/test_integration_pymc_examples.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,3 +1298,264 @@ def test_transfer_function_ar_bayesian(mock_pymc_sample):
12981298
assert (rho_samples > -1).all() and (rho_samples < 1).all(), (
12991299
"Rho should be between -1 and 1"
13001300
)
1301+
1302+
1303+
@pytest.mark.integration
1304+
def test_transfer_function_bayesian_with_saturation(mock_pymc_sample):
1305+
"""
1306+
Test Bayesian Transfer Function with Hill saturation.
1307+
1308+
This test covers saturation transform code paths in build_model.
1309+
"""
1310+
np.random.seed(42)
1311+
n_weeks = 100
1312+
t = np.arange(n_weeks)
1313+
1314+
# Simple baseline
1315+
baseline = 1000 + 2 * t + np.random.normal(0, 50, n_weeks)
1316+
1317+
# Treatment with saturation
1318+
treatment_raw = np.zeros(n_weeks)
1319+
treatment_raw[30:70] = np.random.uniform(5, 15, 40)
1320+
1321+
# Apply Hill saturation + adstock for data generation
1322+
from causalpy.transforms import GeometricAdstock, HillSaturation
1323+
1324+
sat = HillSaturation(slope=2.0, kappa=10)
1325+
treatment_sat = sat.apply(treatment_raw)
1326+
1327+
adstock = GeometricAdstock(half_life=2.0, normalize=True)
1328+
treatment_transformed = adstock.apply(treatment_sat)
1329+
1330+
outcome = baseline - 100 * treatment_transformed + np.random.normal(0, 30, n_weeks)
1331+
1332+
df = pd.DataFrame({"t": t, "y": outcome, "treatment": treatment_raw})
1333+
1334+
# Fit Bayesian model WITH saturation
1335+
model = cp.pymc_models.TransferFunctionLinearRegression(
1336+
saturation_type="hill",
1337+
saturation_config={
1338+
"slope_prior": {"dist": "Gamma", "alpha": 3, "beta": 1.5},
1339+
"kappa_prior": {"dist": "Gamma", "alpha": 10, "beta": 1},
1340+
},
1341+
adstock_config={
1342+
"half_life_prior": {"dist": "Gamma", "alpha": 4, "beta": 2},
1343+
"l_max": 8,
1344+
"normalize": True,
1345+
},
1346+
sample_kwargs=sample_kwargs,
1347+
)
1348+
1349+
result = cp.GradedInterventionTimeSeries(
1350+
data=df,
1351+
y_column="y",
1352+
treatment_names=["treatment"],
1353+
base_formula="1 + t",
1354+
model=model,
1355+
)
1356+
1357+
# Test that saturation parameters are in posterior
1358+
assert "slope" in result.model.idata.posterior
1359+
assert "kappa" in result.model.idata.posterior
1360+
assert "half_life" in result.model.idata.posterior
1361+
1362+
# Test plotting works with saturation
1363+
fig, ax = result.plot()
1364+
assert isinstance(fig, plt.Figure)
1365+
1366+
1367+
@pytest.mark.integration
1368+
def test_transfer_function_bayesian_halfnormal_prior(mock_pymc_sample):
1369+
"""
1370+
Test Bayesian Transfer Function with HalfNormal priors.
1371+
1372+
This test covers HalfNormal prior code paths (lines 1233-1239).
1373+
"""
1374+
np.random.seed(42)
1375+
n_weeks = 80
1376+
t = np.arange(n_weeks)
1377+
1378+
baseline = 1000 + 2 * t + np.random.normal(0, 50, n_weeks)
1379+
treatment = np.zeros(n_weeks)
1380+
treatment[20:60] = np.random.uniform(5, 10, 40)
1381+
1382+
outcome = baseline - 50 * treatment + np.random.normal(0, 30, n_weeks)
1383+
df = pd.DataFrame({"t": t, "y": outcome, "treatment": treatment})
1384+
1385+
# Use HalfNormal prior for half_life
1386+
model = cp.pymc_models.TransferFunctionLinearRegression(
1387+
saturation_type=None,
1388+
adstock_config={
1389+
"half_life_prior": {
1390+
"dist": "HalfNormal",
1391+
"sigma": 3,
1392+
}, # HalfNormal instead of Gamma
1393+
"l_max": 8,
1394+
"normalize": True,
1395+
},
1396+
sample_kwargs=sample_kwargs,
1397+
)
1398+
1399+
result = cp.GradedInterventionTimeSeries(
1400+
data=df,
1401+
y_column="y",
1402+
treatment_names=["treatment"],
1403+
base_formula="1 + t",
1404+
model=model,
1405+
)
1406+
1407+
# Verify model fitted
1408+
assert "half_life" in result.model.idata.posterior
1409+
assert hasattr(result.model, "idata")
1410+
1411+
1412+
@pytest.mark.integration
1413+
def test_transfer_function_bayesian_logistic_saturation(mock_pymc_sample):
1414+
"""
1415+
Test Bayesian Transfer Function with Logistic saturation.
1416+
1417+
This test covers logistic saturation code paths (lines 1277-1286).
1418+
"""
1419+
np.random.seed(42)
1420+
n_weeks = 80
1421+
t = np.arange(n_weeks)
1422+
1423+
baseline = 1000 + 2 * t + np.random.normal(0, 50, n_weeks)
1424+
treatment = np.zeros(n_weeks)
1425+
treatment[20:60] = np.random.uniform(1, 5, 40)
1426+
1427+
outcome = baseline - 50 * treatment + np.random.normal(0, 30, n_weeks)
1428+
df = pd.DataFrame({"t": t, "y": outcome, "treatment": treatment})
1429+
1430+
# Use Logistic saturation
1431+
model = cp.pymc_models.TransferFunctionLinearRegression(
1432+
saturation_type="logistic",
1433+
saturation_config={
1434+
"lam_prior": {"dist": "HalfNormal", "sigma": 1},
1435+
},
1436+
adstock_config={
1437+
"half_life_prior": {"dist": "Gamma", "alpha": 4, "beta": 2},
1438+
"l_max": 8,
1439+
"normalize": True,
1440+
},
1441+
sample_kwargs=sample_kwargs,
1442+
)
1443+
1444+
result = cp.GradedInterventionTimeSeries(
1445+
data=df,
1446+
y_column="y",
1447+
treatment_names=["treatment"],
1448+
base_formula="1 + t",
1449+
model=model,
1450+
)
1451+
1452+
# Verify logistic parameters are in posterior
1453+
assert "lam" in result.model.idata.posterior
1454+
assert "half_life" in result.model.idata.posterior
1455+
1456+
1457+
@pytest.mark.integration
1458+
def test_transfer_function_bayesian_michaelis_menten_saturation(mock_pymc_sample):
1459+
"""
1460+
Test Bayesian Transfer Function with Michaelis-Menten saturation.
1461+
1462+
This test covers Michaelis-Menten saturation code paths (lines 1289-1313).
1463+
"""
1464+
np.random.seed(42)
1465+
n_weeks = 80
1466+
t = np.arange(n_weeks)
1467+
1468+
baseline = 1000 + 2 * t + np.random.normal(0, 50, n_weeks)
1469+
treatment = np.zeros(n_weeks)
1470+
treatment[20:60] = np.random.uniform(5, 15, 40)
1471+
1472+
outcome = baseline - 50 * treatment + np.random.normal(0, 30, n_weeks)
1473+
df = pd.DataFrame({"t": t, "y": outcome, "treatment": treatment})
1474+
1475+
# Use Michaelis-Menten saturation
1476+
model = cp.pymc_models.TransferFunctionLinearRegression(
1477+
saturation_type="michaelis_menten",
1478+
saturation_config={
1479+
"alpha_prior": {"dist": "HalfNormal", "sigma": 1},
1480+
"lam_prior": {"dist": "HalfNormal", "sigma": 100},
1481+
},
1482+
adstock_config={
1483+
"half_life_prior": {"dist": "Gamma", "alpha": 4, "beta": 2},
1484+
"l_max": 8,
1485+
"normalize": True,
1486+
},
1487+
sample_kwargs=sample_kwargs,
1488+
)
1489+
1490+
result = cp.GradedInterventionTimeSeries(
1491+
data=df,
1492+
y_column="y",
1493+
treatment_names=["treatment"],
1494+
base_formula="1 + t",
1495+
model=model,
1496+
)
1497+
1498+
# Verify Michaelis-Menten parameters are in posterior
1499+
assert "alpha_sat" in result.model.idata.posterior
1500+
assert "lam" in result.model.idata.posterior
1501+
assert "half_life" in result.model.idata.posterior
1502+
1503+
1504+
@pytest.mark.integration
1505+
def test_transfer_function_ar_bayesian_with_saturation(mock_pymc_sample):
1506+
"""
1507+
Test Bayesian AR(1) Transfer Function with Hill saturation.
1508+
1509+
This test covers saturation transform code paths in AR model (lines 1746-1826).
1510+
"""
1511+
np.random.seed(42)
1512+
n_weeks = 80
1513+
t = np.arange(n_weeks)
1514+
1515+
baseline = 1000 + 2 * t
1516+
treatment = np.zeros(n_weeks)
1517+
treatment[20:60] = np.random.uniform(5, 15, 40)
1518+
1519+
# Add AR(1) errors
1520+
rho = 0.6
1521+
errors = np.zeros(n_weeks)
1522+
errors[0] = np.random.normal(0, 30 / np.sqrt(1 - rho**2))
1523+
for i in range(1, n_weeks):
1524+
errors[i] = rho * errors[i - 1] + np.random.normal(0, 30)
1525+
1526+
outcome = baseline - 50 * treatment + errors
1527+
df = pd.DataFrame({"t": t, "y": outcome, "treatment": treatment})
1528+
1529+
# AR model WITH saturation
1530+
model = cp.pymc_models.TransferFunctionARRegression(
1531+
saturation_type="hill",
1532+
saturation_config={
1533+
"slope_prior": {"dist": "Gamma", "alpha": 3, "beta": 1.5},
1534+
"kappa_prior": {"dist": "Gamma", "alpha": 10, "beta": 1},
1535+
},
1536+
adstock_config={
1537+
"half_life_prior": {"dist": "Gamma", "alpha": 4, "beta": 2},
1538+
"l_max": 8,
1539+
"normalize": True,
1540+
},
1541+
sample_kwargs={
1542+
"chains": 2,
1543+
"draws": 50,
1544+
"tune": 50,
1545+
"random_seed": 42,
1546+
},
1547+
)
1548+
1549+
result = cp.GradedInterventionTimeSeries(
1550+
data=df,
1551+
y_column="y",
1552+
treatment_names=["treatment"],
1553+
base_formula="1 + t",
1554+
model=model,
1555+
)
1556+
1557+
# Verify saturation parameters and rho are in posterior
1558+
assert "slope" in result.model.idata.posterior
1559+
assert "kappa" in result.model.idata.posterior
1560+
assert "rho" in result.model.idata.posterior
1561+
assert "half_life" in result.model.idata.posterior

0 commit comments

Comments
 (0)