Source code for scitex_stats.auto._rules

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: "2025-12-10 (ywatanabe)"
# File: scitex_stats/auto/_rules.py

"""
Test Rules - Applicability rules for statistical tests.

This module defines TestRule dataclass and the TEST_RULES registry that
maps test names to their applicability conditions. Used by check_applicable()
to determine which tests can be applied to a given StatContext.

The priority field is used for test recommendation - higher priority tests
are recommended first when multiple tests are applicable.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, Literal, Optional, Set

# =============================================================================
# Type Aliases
# =============================================================================

TestFamily = Literal[
    "parametric",
    "nonparametric",
    "categorical",
    "correlation",
    "normality",
    "effect_size",
    "posthoc",
    "other",
]


# =============================================================================
# TestRule
# =============================================================================


[docs] @dataclass class TestRule: """ Applicability rule for a specific statistical test. Each TestRule defines the conditions under which a test is applicable. The check_applicable() function uses these rules to filter tests for a given StatContext. Parameters ---------- name : str Internal name of the test (e.g., "ttest_ind", "brunner_munzel"). family : TestFamily High-level family of the test: - "parametric": t-test, ANOVA, etc. - "nonparametric": Mann-Whitney, Kruskal-Wallis, etc. - "categorical": Chi-square, Fisher's exact, etc. - "correlation": Pearson, Spearman, etc. - "normality": Shapiro-Wilk, etc. - "effect_size": Cohen's d, eta-squared, etc. - "posthoc": Tukey, Dunnett, etc. - "other": Other tests (Levene, etc.) min_groups : int Minimum required number of groups. max_groups : int or None Maximum allowed number of groups. None means no upper bound. outcome_types : set of str Allowed outcome types for this test. supports_paired : bool Whether the test supports paired/repeated measures. supports_unpaired : bool Whether the test supports independent groups. design_allowed : set of str Allowed designs, e.g., {"between", "within"}. requires_control_group : bool Whether a dedicated control group is required (e.g., Dunnett). min_n_total : int or None Minimum total sample size. None means no constraint. min_n_per_group : int or None Minimum sample size per group. needs_normality : bool Whether test assumes normality (check normality_ok). needs_equal_variance : bool Whether test assumes equal variances (check variance_homogeneity_ok). min_factors : int or None Minimum number of factors. max_factors : int or None Maximum number of factors. priority : int Priority score for recommendation. Higher = more recommended. Brunner-Munzel has priority 110 as the recommended default for 2 groups. description : str Human-readable description for tooltips. Examples -------- >>> rule = TestRule( ... name="ttest_ind", ... family="parametric", ... min_groups=2, ... max_groups=2, ... outcome_types={"continuous"}, ... supports_paired=False, ... supports_unpaired=True, ... design_allowed={"between"}, ... requires_control_group=False, ... min_n_total=4, ... min_n_per_group=2, ... needs_normality=True, ... needs_equal_variance=False, ... min_factors=1, ... max_factors=1, ... priority=90, ... description="Independent samples t-test (Welch)" ... ) """ name: str family: TestFamily min_groups: int max_groups: Optional[int] outcome_types: Set[str] supports_paired: bool supports_unpaired: bool design_allowed: Set[str] requires_control_group: bool min_n_total: Optional[int] min_n_per_group: Optional[int] needs_normality: bool needs_equal_variance: bool min_factors: Optional[int] max_factors: Optional[int] priority: int = 0 description: str = ""
# ============================================================================= # TEST_RULES Registry # ============================================================================= TEST_RULES: Dict[str, TestRule] = { # ========================================================================= # Parametric Tests - Mean Comparisons # ========================================================================= # Independent 2-sample t-test (Welch) "ttest_ind": TestRule( name="ttest_ind", family="parametric", min_groups=2, max_groups=2, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=True, needs_equal_variance=False, # Welch doesn't require equal variance min_factors=1, max_factors=1, priority=90, description="Independent samples t-test (Welch)", ), # Paired t-test "ttest_rel": TestRule( name="ttest_rel", family="parametric", min_groups=2, max_groups=2, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=False, design_allowed={"within"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=True, needs_equal_variance=False, min_factors=1, max_factors=1, priority=95, description="Paired samples t-test", ), # One-way ANOVA (between) "anova_oneway": TestRule( name="anova_oneway", family="parametric", min_groups=3, max_groups=None, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=6, min_n_per_group=2, needs_normality=True, needs_equal_variance=True, min_factors=1, max_factors=1, priority=80, description="One-way ANOVA (between subjects)", ), # Repeated-measures one-way ANOVA "anova_rm_oneway": TestRule( name="anova_rm_oneway", family="parametric", min_groups=3, max_groups=None, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=False, design_allowed={"within"}, requires_control_group=False, min_n_total=6, min_n_per_group=2, needs_normality=True, needs_equal_variance=True, min_factors=1, max_factors=1, priority=85, description="Repeated-measures one-way ANOVA", ), # Welch ANOVA (unequal variances) "welch_anova": TestRule( name="welch_anova", family="parametric", min_groups=3, max_groups=None, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=6, min_n_per_group=2, needs_normality=True, needs_equal_variance=False, # Welch doesn't require equal variance min_factors=1, max_factors=1, priority=82, description="Welch's ANOVA (heterogeneous variances)", ), # Two-way ANOVA (between) "anova_twoway": TestRule( name="anova_twoway", family="parametric", min_groups=2, max_groups=None, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=8, min_n_per_group=2, needs_normality=True, needs_equal_variance=True, min_factors=2, max_factors=2, priority=78, description="Two-way ANOVA (between subjects)", ), # Two-way ANOVA (mixed) "anova_twoway_mixed": TestRule( name="anova_twoway_mixed", family="parametric", min_groups=2, max_groups=None, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=True, design_allowed={"mixed", "within"}, requires_control_group=False, min_n_total=8, min_n_per_group=2, needs_normality=True, needs_equal_variance=True, min_factors=2, max_factors=2, priority=80, description="Two-way mixed-design ANOVA", ), # ========================================================================= # Nonparametric Tests - Rank Comparisons # ========================================================================= # Brunner-Munzel test (RECOMMENDED DEFAULT for 2 groups) "brunner_munzel": TestRule( name="brunner_munzel", family="nonparametric", min_groups=2, max_groups=2, outcome_types={"continuous", "ordinal"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=6, min_n_per_group=3, needs_normality=False, needs_equal_variance=False, # Most robust - no assumptions min_factors=1, max_factors=1, priority=110, # HIGHEST PRIORITY - recommended default description="Brunner-Munzel test (most robust, recommended)", ), # Mann-Whitney U test "mannwhitneyu": TestRule( name="mannwhitneyu", family="nonparametric", min_groups=2, max_groups=2, outcome_types={"continuous", "ordinal"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=85, description="Mann-Whitney U test (rank-sum)", ), # Wilcoxon signed-rank test (paired) "wilcoxon": TestRule( name="wilcoxon", family="nonparametric", min_groups=2, max_groups=2, outcome_types={"continuous", "ordinal"}, supports_paired=True, supports_unpaired=False, design_allowed={"within"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=90, description="Wilcoxon signed-rank test (paired)", ), # Kruskal-Wallis (3+ groups, between) "kruskal": TestRule( name="kruskal", family="nonparametric", min_groups=3, max_groups=None, outcome_types={"continuous", "ordinal"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=6, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=75, description="Kruskal-Wallis H test", ), # Friedman test (3+ groups, within) "friedman": TestRule( name="friedman", family="nonparametric", min_groups=3, max_groups=None, outcome_types={"continuous", "ordinal"}, supports_paired=True, supports_unpaired=False, design_allowed={"within"}, requires_control_group=False, min_n_total=6, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=80, description="Friedman test (repeated measures)", ), # ========================================================================= # Categorical Tests # ========================================================================= # Chi-square test of independence "chi2_independence": TestRule( name="chi2_independence", family="categorical", min_groups=2, max_groups=None, outcome_types={"binary", "categorical"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=10, min_n_per_group=None, # Uses expected counts needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=None, priority=80, description="Chi-square test of independence", ), # Fisher's exact test (2x2) "fisher_exact": TestRule( name="fisher_exact", family="categorical", min_groups=2, max_groups=2, outcome_types={"binary"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=1, min_n_per_group=1, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=90, description="Fisher's exact test (2x2)", ), # McNemar's test (paired binary) "mcnemar": TestRule( name="mcnemar", family="categorical", min_groups=2, max_groups=2, outcome_types={"binary"}, supports_paired=True, supports_unpaired=False, design_allowed={"within"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=85, description="McNemar's test (paired binary)", ), # ========================================================================= # Correlation Tests # ========================================================================= # Pearson correlation "pearsonr": TestRule( name="pearsonr", family="correlation", min_groups=1, max_groups=1, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=True, design_allowed={"between", "within", "mixed"}, requires_control_group=False, min_n_total=3, min_n_per_group=None, needs_normality=True, needs_equal_variance=False, min_factors=None, max_factors=None, priority=80, description="Pearson correlation coefficient", ), # Spearman correlation "spearmanr": TestRule( name="spearmanr", family="correlation", min_groups=1, max_groups=1, outcome_types={"continuous", "ordinal"}, supports_paired=True, supports_unpaired=True, design_allowed={"between", "within", "mixed"}, requires_control_group=False, min_n_total=3, min_n_per_group=None, needs_normality=False, needs_equal_variance=False, min_factors=None, max_factors=None, priority=85, description="Spearman rank correlation", ), # ========================================================================= # Normality Tests # ========================================================================= # Shapiro-Wilk test "shapiro": TestRule( name="shapiro", family="normality", min_groups=1, max_groups=None, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=True, design_allowed={"between", "within", "mixed"}, requires_control_group=False, min_n_total=3, min_n_per_group=None, needs_normality=False, needs_equal_variance=False, min_factors=None, max_factors=None, priority=60, description="Shapiro-Wilk normality test", ), # Levene's test for homogeneity of variance "levene": TestRule( name="levene", family="other", min_groups=2, max_groups=None, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=None, priority=70, description="Levene's test for homogeneity of variance", ), # ========================================================================= # Post-hoc Tests # ========================================================================= # Tukey HSD "tukey_hsd": TestRule( name="tukey_hsd", family="posthoc", min_groups=3, max_groups=None, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=6, min_n_per_group=2, needs_normality=True, needs_equal_variance=True, min_factors=1, max_factors=1, priority=88, description="Tukey HSD post-hoc test", ), # Dunnett (control vs treatments) "dunnett": TestRule( name="dunnett", family="posthoc", min_groups=3, max_groups=None, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=True, # REQUIRES control group min_n_total=6, min_n_per_group=2, needs_normality=True, needs_equal_variance=True, min_factors=1, max_factors=1, priority=86, description="Dunnett's test (control vs treatments)", ), # Games-Howell (unequal variances) "games_howell": TestRule( name="games_howell", family="posthoc", min_groups=3, max_groups=None, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=6, min_n_per_group=2, needs_normality=True, needs_equal_variance=False, # Does NOT require equal variance min_factors=1, max_factors=1, priority=89, description="Games-Howell post-hoc (unequal variances)", ), # ========================================================================= # Effect Size Measures # ========================================================================= # Cohen's d (independent) "cohens_d_ind": TestRule( name="cohens_d_ind", family="effect_size", min_groups=2, max_groups=2, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=90, description="Cohen's d (independent samples)", ), # Cohen's d (paired) "cohens_d_paired": TestRule( name="cohens_d_paired", family="effect_size", min_groups=2, max_groups=2, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=False, design_allowed={"within"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=92, description="Cohen's d (paired samples)", ), # Hedges' g "hedges_g": TestRule( name="hedges_g", family="effect_size", min_groups=2, max_groups=2, outcome_types={"continuous"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=88, description="Hedges' g (bias-corrected effect size)", ), # Cliff's delta "cliffs_delta": TestRule( name="cliffs_delta", family="effect_size", min_groups=2, max_groups=2, outcome_types={"continuous", "ordinal"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=86, description="Cliff's delta (nonparametric effect size)", ), # Eta-squared "eta_squared": TestRule( name="eta_squared", family="effect_size", min_groups=2, max_groups=None, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=True, design_allowed={"between", "within"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=80, description="Eta-squared (variance explained)", ), # Partial eta-squared "partial_eta_squared": TestRule( name="partial_eta_squared", family="effect_size", min_groups=2, max_groups=None, outcome_types={"continuous"}, supports_paired=True, supports_unpaired=True, design_allowed={"between", "within", "mixed"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=None, priority=85, description="Partial eta-squared (multi-factor designs)", ), # Effect size r (for correlations) "effect_size_r": TestRule( name="effect_size_r", family="effect_size", min_groups=1, max_groups=1, outcome_types={"continuous", "ordinal"}, supports_paired=True, supports_unpaired=True, design_allowed={"between", "within", "mixed"}, requires_control_group=False, min_n_total=3, min_n_per_group=None, needs_normality=False, needs_equal_variance=False, min_factors=None, max_factors=None, priority=80, description="Effect size r (correlation)", ), # Odds ratio "odds_ratio": TestRule( name="odds_ratio", family="effect_size", min_groups=2, max_groups=2, outcome_types={"binary"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=1, min_n_per_group=1, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=88, description="Odds ratio (2x2 table)", ), # Risk ratio "risk_ratio": TestRule( name="risk_ratio", family="effect_size", min_groups=2, max_groups=2, outcome_types={"binary"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=1, min_n_per_group=1, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=86, description="Risk ratio (relative risk)", ), # Probability of superiority P(X>Y) "prob_superiority": TestRule( name="prob_superiority", family="effect_size", min_groups=2, max_groups=2, outcome_types={"continuous", "ordinal"}, supports_paired=False, supports_unpaired=True, design_allowed={"between"}, requires_control_group=False, min_n_total=4, min_n_per_group=2, needs_normality=False, needs_equal_variance=False, min_factors=1, max_factors=1, priority=84, description="Probability of superiority P(X>Y)", ), } # ============================================================================= # Utility Functions # ============================================================================= def get_test_rule(name: str) -> Optional[TestRule]: """ Get a TestRule by name. Parameters ---------- name : str Test name (e.g., "ttest_ind", "brunner_munzel"). Returns ------- TestRule or None The TestRule if found, else None. """ return TEST_RULES.get(name) def list_tests_by_family(family: TestFamily) -> Dict[str, TestRule]: """ Get all tests in a specific family. Parameters ---------- family : TestFamily Test family to filter by. Returns ------- dict Dictionary of test name -> TestRule for the family. """ return {name: rule for name, rule in TEST_RULES.items() if rule.family == family} # ============================================================================= # Public API # ============================================================================= __all__ = [ "TestRule", "TestFamily", "TEST_RULES", "get_test_rule", "list_tests_by_family", ] # EOF