Source code for causalkit.inference.att.ttest

"""
T-test inference for causaldata objects (ATT context).
"""

import numpy as np
import pandas as pd
from scipy import stats
from typing import Dict, Any

from causalkit.data.causaldata import CausalData


[docs] def ttest(data: CausalData, confidence_level: float = 0.95) -> Dict[str, Any]: """ Perform a t-test on a CausalData object to compare the outcome variable between treated (T=1) and control (T=0) groups. Returns differences and confidence intervals. Parameters ---------- data : CausalData The CausalData object containing treatment and outcome variables. confidence_level : float, default 0.95 The confidence level for calculating confidence intervals (between 0 and 1). Returns ------- Dict[str, Any] A dictionary containing: - p_value: The p-value from the t-test - absolute_difference: The absolute difference between treatment and control means - absolute_ci: Tuple of (lower, upper) bounds for the absolute difference confidence interval - relative_difference: The relative difference (percentage change) between treatment and control means - relative_ci: Tuple of (lower, upper) bounds for the relative difference confidence interval Raises ------ ValueError If the CausalData object doesn't have both treatment and outcome variables defined, or if the treatment variable is not binary. """ # Basic validation: ensure treatment and outcome are proper Series and non-empty treatment_var = data.treatment target_var = data.target if not isinstance(treatment_var, pd.Series) or treatment_var.empty: raise ValueError("causaldata object must have a treatment variable defined") if not isinstance(target_var, pd.Series) or target_var.empty: raise ValueError("causaldata object must have a outcome variable defined") # Ensure binary treatment unique_treatments = treatment_var.unique() if len(unique_treatments) != 2: raise ValueError("Treatment variable must be binary (have exactly 2 unique values)") # Build groups by conventional 0/1 coding control_data = target_var[treatment_var == 0] treatment_data = target_var[treatment_var == 1] # Independent two-sample t-test (pooled variance by default equal_var=True) t_stat, p_value = stats.ttest_ind(treatment_data, control_data, equal_var=True) # Means control_mean = float(control_data.mean()) treatment_mean = float(treatment_data.mean()) # Absolute difference (ATT style: treated - control) absolute_diff = treatment_mean - control_mean # Standard error using pooled variance n1 = int(len(treatment_data)) n2 = int(len(control_data)) s1_squared = float(treatment_data.var(ddof=1)) s2_squared = float(control_data.var(ddof=1)) # Guard against degenerate cases with very small groups if n1 < 2 or n2 < 2: raise ValueError("Not enough observations in one of the groups for t-test (need at least 2 per group)") pooled_var = ((n1 - 1) * s1_squared + (n2 - 1) * s2_squared) / (n1 + n2 - 2) se_diff = float(np.sqrt(pooled_var * (1 / n1 + 1 / n2))) # Confidence interval if not 0 < confidence_level < 1: raise ValueError("confidence_level must be between 0 and 1 (exclusive)") alpha = 1 - confidence_level df = n1 + n2 - 2 t_critical = float(stats.t.ppf(1 - alpha / 2, df)) margin_of_error = t_critical * se_diff absolute_ci = (absolute_diff - margin_of_error, absolute_diff + margin_of_error) # Relative difference (%), relative CI via delta method on denominator if control_mean == 0: relative_diff = np.inf if absolute_diff > 0 else -np.inf if absolute_diff < 0 else 0.0 relative_ci = (np.nan, np.nan) else: relative_diff = (absolute_diff / abs(control_mean)) * 100.0 relative_margin = (margin_of_error / abs(control_mean)) * 100.0 relative_ci = (relative_diff - relative_margin, relative_diff + relative_margin) return { "p_value": float(p_value), "absolute_difference": float(absolute_diff), "absolute_ci": (float(absolute_ci[0]), float(absolute_ci[1])), "relative_difference": float(relative_diff), "relative_ci": (float(relative_ci[0]), float(relative_ci[1])), }