Source code for hypotestx.tests.parametric

from typing import List

from ..core.result import HypoResult
from ..core.validators import (
    validate_alpha,
    validate_alternative,
    validate_data,
    validate_paired_data,
    validate_two_groups,
)
from ..math.basic import abs_value, sqrt
from ..math.distributions import StudentT
from ..math.statistics import mean, std, variance


[docs] def one_sample_ttest( data: List[float], mu: float = 0.0, alpha: float = 0.05, alternative: str = "two-sided", ) -> HypoResult: """ One-sample t-test implemented from scratch Args: data: Sample data mu: Hypothesized population mean alpha: Significance level alternative: "two-sided", "greater", or "less" Returns: HypoResult object with test results """ # Validate input data = validate_data(data, min_size=2, name="data") validate_alpha(alpha) validate_alternative(alternative) n = len(data) sample_mean = mean(data) sample_std = std(data, ddof=1) if sample_std == 0: raise ValueError("Sample standard deviation is zero") # Calculate t-statistic standard_error = sample_std / sqrt(n) t_stat = (sample_mean - mu) / standard_error # Degrees of freedom df = n - 1 # Calculate p-value t_dist = StudentT(df) if alternative == "two-sided": p_value = 2 * (1 - t_dist.cdf(abs_value(t_stat))) elif alternative == "greater": p_value = 1 - t_dist.cdf(t_stat) elif alternative == "less": p_value = t_dist.cdf(t_stat) else: raise ValueError("Alternative must be 'two-sided', 'greater', or 'less'") # Calculate effect size (Cohen's d) cohens_d = (sample_mean - mu) / sample_std # Calculate confidence interval t_critical = t_dist.ppf(1 - alpha / 2) if alternative == "two-sided" else t_dist.ppf(1 - alpha) margin_of_error = t_critical * standard_error if alternative == "two-sided": ci = (sample_mean - margin_of_error, sample_mean + margin_of_error) elif alternative == "greater": ci = (sample_mean - margin_of_error, float("inf")) else: # less ci = (float("-inf"), sample_mean + margin_of_error) # Data summary data_summary = { "sample_mean": sample_mean, "sample_std": sample_std, "sample_size": n, "hypothesized_mean": mu, "standard_error": standard_error, } # Generate interpretation significance = "significant" if p_value < alpha else "not significant" direction = "" if alternative == "greater" and p_value < alpha: direction = f"Sample mean ({sample_mean:.3f}) is significantly greater than {mu}" elif alternative == "less" and p_value < alpha: direction = f"Sample mean ({sample_mean:.3f}) is significantly less than {mu}" elif alternative == "two-sided" and p_value < alpha: direction = f"Sample mean ({sample_mean:.3f}) is significantly different from {mu}" else: direction = ( f"No significant difference found between sample mean ({sample_mean:.3f}) and {mu}" ) interpretation = f"The one-sample t-test is {significance} (p = {p_value:.4f}). {direction}" return HypoResult( test_name="One-Sample t-test", statistic=t_stat, p_value=p_value, effect_size=cohens_d, effect_size_name="Cohen's d", confidence_interval=ci, degrees_of_freedom=df, sample_sizes=n, alpha=alpha, alternative=alternative, interpretation=interpretation, data_summary=data_summary, )
[docs] def two_sample_ttest( group1: List[float], group2: List[float], alpha: float = 0.05, alternative: str = "two-sided", equal_var: bool = True, ) -> HypoResult: """ Two-sample t-test (Student's t-test or Welch's t-test) Args: group1: First group data group2: Second group data alpha: Significance level alternative: "two-sided", "greater", or "less" equal_var: Whether to assume equal variances (Student's vs Welch's) Returns: HypoResult object with test results """ # Validate input group1, group2 = validate_two_groups(group1, group2, min_size=2) validate_alpha(alpha) validate_alternative(alternative) n1, n2 = len(group1), len(group2) mean1, mean2 = mean(group1), mean(group2) var1, var2 = variance(group1, ddof=1), variance(group2, ddof=1) std1, std2 = sqrt(var1), sqrt(var2) # Calculate pooled or separate variance if equal_var: # Student's t-test (pooled variance) pooled_var = ((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2) if pooled_var == 0: raise ValueError( "Both groups have zero variance (constant data). " "A t-test is undefined for constant groups. " "Verify your data or use an exact test." ) standard_error = sqrt(pooled_var * (1 / n1 + 1 / n2)) df = n1 + n2 - 2 test_name = "Student's t-test (equal variances)" pooled_std = sqrt(pooled_var) cohens_d = (mean1 - mean2) / pooled_std else: # Welch's t-test (unequal variances) se_sq = var1 / n1 + var2 / n2 if se_sq == 0: raise ValueError( "Both groups have zero variance (constant data). " "A t-test is undefined for constant groups. " "Verify your data or use an exact test." ) standard_error = sqrt(se_sq) # Welch-Satterthwaite equation for degrees of freedom numerator = se_sq**2 denom1 = (var1 / n1) ** 2 / (n1 - 1) if n1 > 1 else 0.0 denom2 = (var2 / n2) ** 2 / (n2 - 1) if n2 > 1 else 0.0 denominator = denom1 + denom2 if denominator == 0: # Both variances are zero — undefined df; raise a clear error raise ValueError( "Welch-Satterthwaite degrees of freedom are undefined because " "both group variances are zero. " "The data in one or both groups is constant. " "Check your input data." ) df = numerator / denominator test_name = "Welch's t-test (unequal variances)" pooled_sd = sqrt((var1 + var2) / 2) cohens_d = (mean1 - mean2) / pooled_sd if pooled_sd > 0 else float("nan") # Calculate t-statistic t_stat = (mean1 - mean2) / standard_error # Calculate p-value t_dist = StudentT(df) if alternative == "two-sided": p_value = 2 * (1 - t_dist.cdf(abs_value(t_stat))) elif alternative == "greater": p_value = 1 - t_dist.cdf(t_stat) elif alternative == "less": p_value = t_dist.cdf(t_stat) else: raise ValueError("Alternative must be 'two-sided', 'greater', or 'less'") # Calculate confidence interval for difference in means t_critical = t_dist.ppf(1 - alpha / 2) if alternative == "two-sided" else t_dist.ppf(1 - alpha) margin_of_error = t_critical * standard_error mean_diff = mean1 - mean2 if alternative == "two-sided": ci = (mean_diff - margin_of_error, mean_diff + margin_of_error) elif alternative == "greater": ci = (mean_diff - margin_of_error, float("inf")) else: # less ci = (float("-inf"), mean_diff + margin_of_error) # Data summary data_summary = { "group1_mean": mean1, "group1_std": std1, "group1_size": n1, "group2_mean": mean2, "group2_std": std2, "group2_size": n2, "mean_difference": mean_diff, "standard_error": standard_error, "pooled_variance": pooled_var if equal_var else None, } # Generate interpretation significance = "significant" if p_value < alpha else "not significant" direction = "" if alternative == "greater" and p_value < alpha: direction = ( f"Group 1 mean ({mean1:.3f}) is significantly greater than Group 2 mean ({mean2:.3f})" ) elif alternative == "less" and p_value < alpha: direction = ( f"Group 1 mean ({mean1:.3f}) is significantly less than Group 2 mean ({mean2:.3f})" ) elif alternative == "two-sided" and p_value < alpha: direction = f"Significant difference between Group 1 mean ({mean1:.3f}) and Group 2 mean ({mean2:.3f})" # noqa: E501 else: direction = f"No significant difference between Group 1 mean ({mean1:.3f}) and Group 2 mean ({mean2:.3f})" # noqa: E501 interpretation = f"The {test_name.lower()} is {significance} (p = {p_value:.4f}). {direction}" return HypoResult( test_name=test_name, statistic=t_stat, p_value=p_value, effect_size=cohens_d, effect_size_name="Cohen's d", confidence_interval=ci, degrees_of_freedom=df, sample_sizes=(n1, n2), alpha=alpha, alternative=alternative, interpretation=interpretation, data_summary=data_summary, )
[docs] def paired_ttest( before: List[float], after: List[float], alpha: float = 0.05, alternative: str = "two-sided", ) -> HypoResult: """ Paired t-test for dependent samples Args: before: Before measurements after: After measurements alpha: Significance level alternative: "two-sided", "greater", or "less" Returns: HypoResult object with test results """ if len(before) != len(after): raise ValueError("Before and after groups must have same length") if len(before) < 2: raise ValueError("Need at least 2 paired observations") # Validate input before, after = validate_paired_data(before, after) validate_alpha(alpha) validate_alternative(alternative) # Calculate differences differences = [after[i] - before[i] for i in range(len(before))] # Use one-sample t-test on differences result = one_sample_ttest(differences, mu=0.0, alpha=alpha, alternative=alternative) # Update test name and interpretation result.test_name = "Paired t-test" n = len(differences) mean_diff = mean(differences) std_diff = std(differences, ddof=1) # Update data summary result.data_summary.update( { "before_mean": mean(before), "after_mean": mean(after), "before_std": std(before, ddof=1), "after_std": std(after, ddof=1), "mean_difference": mean_diff, "difference_std": std_diff, "n_pairs": n, } ) # Update interpretation significance = "significant" if result.p_value < alpha else "not significant" if alternative == "greater" and result.p_value < alpha: direction = f"After values are significantly greater than before values (mean difference = {mean_diff:.3f})" # noqa: E501 elif alternative == "less" and result.p_value < alpha: direction = f"After values are significantly less than before values (mean difference = {mean_diff:.3f})" # noqa: E501 elif alternative == "two-sided" and result.p_value < alpha: direction = f"Significant difference between before and after values (mean difference = {mean_diff:.3f})" # noqa: E501 else: direction = f"No significant difference between before and after values (mean difference = {mean_diff:.3f})" # noqa: E501 result.interpretation = ( f"The paired t-test is {significance} (p = {result.p_value:.4f}). {direction}" ) return result
# --------------------------------------------------------------------------- # One-Way ANOVA # ---------------------------------------------------------------------------
[docs] def anova_one_way( *groups: List[float], alpha: float = 0.05, ) -> HypoResult: """ One-way analysis of variance (ANOVA). Tests whether the population means of three or more independent groups are equal. Assumes normality and homogeneity of variance within groups. Args: *groups: Two or more group samples alpha: Significance level Returns: HypoResult with statistic=F, effect_size=eta-squared Examples: >>> result = anova_one_way([5, 6, 7], [8, 9, 10], [3, 4, 5]) >>> print(result.summary()) """ from ..core.validators import validate_groups from ..math.distributions import F as FDist groups = validate_groups(*groups, min_size=2, min_groups=2) k = len(groups) group_sizes = [len(g) for g in groups] N = sum(group_sizes) group_means = [mean(g) for g in groups] grand_mean = sum(v for g in groups for v in g) / N # Between-group SS SS_between = sum(group_sizes[i] * (group_means[i] - grand_mean) ** 2 for i in range(k)) df_between = k - 1 # Within-group SS SS_within = sum(sum((x - group_means[i]) ** 2 for x in groups[i]) for i in range(k)) df_within = N - k if df_within <= 0: raise ValueError("Insufficient degrees of freedom within groups; add more observations") MS_between = SS_between / df_between MS_within = SS_within / df_within if MS_within == 0: F_stat = float("inf") p_value = 0.0 else: F_stat = MS_between / MS_within f_dist = FDist(df_between, df_within) p_value = max(0.0, min(1.0, 1 - f_dist.cdf(F_stat))) # Eta-squared SS_total = SS_between + SS_within eta_sq = SS_between / SS_total if SS_total > 0 else 0.0 # Omega-squared (less biased estimate) omega_sq = (SS_between - df_between * MS_within) / (SS_total + MS_within) omega_sq = max(0.0, omega_sq) data_summary = { "k_groups": k, "group_sizes": group_sizes, "group_means": group_means, "grand_mean": grand_mean, "SS_between": SS_between, "SS_within": SS_within, "SS_total": SS_total, "MS_between": MS_between, "MS_within": MS_within, "df_between": df_between, "df_within": df_within, "omega_squared": omega_sq, } significance = "significant" if p_value < alpha else "not significant" interpretation = ( f"One-way ANOVA is {significance} " f"(F({df_between}, {df_within}) = {F_stat:.4f}, p = {p_value:.4f}). " + ( "At least one group mean differs significantly from the others." if p_value < alpha else "No significant difference in means across groups." ) ) return HypoResult( test_name="One-Way ANOVA", statistic=F_stat, p_value=p_value, effect_size=eta_sq, effect_size_name="eta-squared", confidence_interval=None, degrees_of_freedom=(df_between, df_within), sample_sizes=tuple(group_sizes), alpha=alpha, alternative="two-sided", interpretation=interpretation, data_summary=data_summary, )