@@ -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