Source code for hypotestx.reporting.generator

"""
Report generator for hypothesis test results.

Provides APA-style text reports from HypoResult objects and from lists of
HypoResult objects (batch reports).

Provides
--------
apa_report(result)              -> str   APA-style paragraph
text_report(result, verbose)    -> str   detailed plain-text report
batch_report(results, title)    -> str   multi-test summary table
export_csv(results, path)       -> None  write batch results to CSV
"""

from typing import List, Optional

from ..core.result import HypoResult
from .formatters import apa_stat, format_p

# ---------------------------------------------------------------------------
# APA-style paragraph
# ---------------------------------------------------------------------------

# Map test names to APA symbol names
_APA_SYMBOLS = {
    "one-sample t-test": ("t", "d"),
    "two-sample t-test": ("t", "d"),
    "welch's t-test": ("t", "d"),
    "paired t-test": ("t", "d"),
    "one-way anova": ("F", "eta-squared"),
    "mann-whitney u test": ("U", "rank-biserial r"),
    "wilcoxon signed-rank test": ("W", "r"),
    "kruskal-wallis test": ("H", "eta-squared"),
    "chi-square test of independence": ("chi2", "phi"),
    "chi-square goodness-of-fit test": ("chi2", "w"),
    "fisher's exact test": ("OR", None),
    "pearson correlation": ("r", "r"),
    "spearman correlation": ("rho", "rho"),
    "point-biserial correlation": ("r_pb", "r_pb"),
    "shapiro-wilk normality test": ("W", None),
    "levene test for equal variances": ("F", None),
    "bartlett test for equal variances": ("B", None),
    "jarque-bera normality test": ("JB", None),
}


def _lookup_symbols(test_name: str):
    """Return (stat_symbol, effect_symbol) for a given test name."""
    key = test_name.lower().strip()
    for k, v in _APA_SYMBOLS.items():
        if k in key or key in k:
            return v
    return (test_name[:6], None)


[docs] def apa_report(result: HypoResult) -> str: """ Generate an APA-style results paragraph for a single HypoResult. Parameters ---------- result : HypoResult from any test function Returns ------- str : APA-style citation suitable for use in a Results section Example ------- An independent-samples t-test revealed a significant difference between groups, t(28) = 3.45, p = .001, d = 0.62 (medium). """ stat_sym, eff_sym = _lookup_symbols(result.test_name) df = result.degrees_of_freedom # Build inline citation citation = apa_stat( stat_sym, result.statistic, df=df, p=result.p_value, effect_name=eff_sym if result.effect_size is not None and eff_sym else None, effect_value=result.effect_size if result.effect_size is not None else None, ) sig_word = "significant" if result.is_significant else "non-significant" direction = "" if result.interpretation: # Grab the first sentence of the interpretation for context first_sent = result.interpretation.split(".")[0].strip() direction = f" {first_sent}." # CI phrase ci_phrase = "" if result.confidence_interval is not None: ci_level = int((1 - result.alpha) * 100) ci_phrase = ( f" A {ci_level}% confidence interval for the effect was " f"[{result.confidence_interval[0]:.3f}, " f"{result.confidence_interval[1]:.3f}]." ) report = ( f"A {result.test_name.lower()} was conducted. " f"The result was {sig_word} ({citation}).{direction}{ci_phrase}" ) return report
# --------------------------------------------------------------------------- # Detailed plain-text report # ---------------------------------------------------------------------------
[docs] def text_report(result: HypoResult, verbose: bool = True) -> str: """ Generate a detailed plain-text report for a single HypoResult. Parameters ---------- result : HypoResult verbose : include sample sizes, assumptions, data summary (default True) Returns ------- str : multi-line report """ lines = [] width = 60 lines.append("=" * width) lines.append(f" {result.test_name}") lines.append("=" * width) lines.append("") # --- Hypothesis --- lines.append("HYPOTHESIS") lines.append("-" * width) lines.append(" H0: No effect / no difference") lines.append(f" H1: Alternative ({result.alternative})") lines.append(f" Significance level (alpha): {result.alpha}") lines.append("") # --- Test statistics --- lines.append("TEST RESULTS") lines.append("-" * width) lines.append(f" Test statistic : {result.statistic:.4f}") lines.append(f" p-value : {format_p(result.p_value)}") if result.degrees_of_freedom is not None: df = result.degrees_of_freedom if isinstance(df, tuple): df_str = f"({', '.join(str(d) for d in df)})" else: df_str = str(df) lines.append(f" df : {df_str}") decision = ( "REJECT H0 (significant)" if result.is_significant else "FAIL TO REJECT H0 (not significant)" ) lines.append(f" Decision : {decision}") lines.append("") # --- Effect size --- if result.effect_size is not None: lines.append("EFFECT SIZE") lines.append("-" * width) lines.append( f" {result.effect_size_name}: {result.effect_size:.4f} ({result.effect_magnitude})" ) lines.append("") # --- Confidence interval --- if result.confidence_interval is not None: ci_level = int((1 - result.alpha) * 100) lines.append(f"CONFIDENCE INTERVAL ({ci_level}%)") lines.append("-" * width) lines.append( f" [{result.confidence_interval[0]:.4f}, {result.confidence_interval[1]:.4f}]" ) lines.append("") if verbose: # --- Sample sizes --- if result.sample_sizes is not None: lines.append("SAMPLE") lines.append("-" * width) if isinstance(result.sample_sizes, tuple): lines.append(f" n per group : {result.sample_sizes}") lines.append(f" total N : {sum(result.sample_sizes)}") else: lines.append(f" n : {result.sample_sizes}") lines.append("") # --- Assumptions --- if result.assumptions_met: lines.append("ASSUMPTION CHECKS") lines.append("-" * width) for assumption, met in result.assumptions_met.items(): status = "Met" if met else "Violated (!)" lines.append(f" {assumption:<28}: {status}") lines.append("") # --- Data summary --- if result.data_summary: lines.append("DATA SUMMARY") lines.append("-" * width) for key, value in result.data_summary.items(): if isinstance(value, float): lines.append(f" {key:<28}: {value:.4f}") else: lines.append(f" {key:<28}: {value}") lines.append("") # --- APA citation --- lines.append("APA CITATION") lines.append("-" * width) lines.append(f" {apa_report(result)}") lines.append("") # --- Interpretation --- if result.interpretation: lines.append("INTERPRETATION") lines.append("-" * width) lines.append(f" {result.interpretation}") lines.append("") lines.append("=" * width) return "\n".join(lines)
# --------------------------------------------------------------------------- # Batch report (multiple tests) # --------------------------------------------------------------------------- def batch_report( results: List[HypoResult], title: str = "Hypothesis Testing Results", show_effect: bool = True, ) -> str: """ Generate a summary table for multiple test results. Parameters ---------- results : list of HypoResult objects title : report title show_effect : include effect size columns (default True) Returns ------- str : formatted summary table """ if not results: return "No results to display." lines = [] lines.append("=" * 80) lines.append(f" {title}") lines.append("=" * 80) lines.append("") # Header col_test = 32 col_stat = 10 col_df = 8 col_p = 10 col_sig = 5 header = ( f"{'Test':<{col_test}}" f"{'Statistic':>{col_stat}}" f"{'df':>{col_df}}" f"{'p-value':>{col_p}}" f"{'Sig':>{col_sig}}" ) if show_effect: header += f" {'Effect':<20}" lines.append(header) lines.append("-" * len(header)) sig_count = 0 for r in results: df_str = "" if r.degrees_of_freedom is not None: df = r.degrees_of_freedom if isinstance(df, tuple): df_str = "(" + ",".join(str(d) for d in df) + ")" else: df_str = str(df) sig_marker = "* " if r.is_significant else " " if r.is_significant: sig_count += 1 row = ( f"{r.test_name[:col_test]:<{col_test}}" f"{r.statistic:>{col_stat}.4f}" f"{df_str:>{col_df}}" f"{format_p(r.p_value):>{col_p}}" f"{sig_marker:>{col_sig}}" ) if show_effect and r.effect_size is not None: eff_str = f"{r.effect_size_name} = {r.effect_size:.3f} ({r.effect_magnitude})" row += f" {eff_str:<20}" lines.append(row) lines.append("-" * len(header)) lines.append(f" * significant at alpha = {results[0].alpha}") lines.append(f" {sig_count}/{len(results)} tests significant") lines.append("") return "\n".join(lines) # --------------------------------------------------------------------------- # CSV export # ---------------------------------------------------------------------------
[docs] def export_csv( results: List[HypoResult], path: str, sep: str = ",", ) -> None: """ Write a batch of HypoResult objects to a CSV file. Parameters ---------- results : list of HypoResult path : output file path sep : delimiter (default ',') """ if not results: raise ValueError("results list is empty") fieldnames = [ "test_name", "statistic", "p_value", "is_significant", "alpha", "alternative", "degrees_of_freedom", "sample_sizes", "effect_size", "effect_size_name", "effect_magnitude", "ci_lower", "ci_upper", ] rows = [] for r in results: ci_lower = r.confidence_interval[0] if r.confidence_interval else "" ci_upper = r.confidence_interval[1] if r.confidence_interval else "" rows.append( { "test_name": r.test_name, "statistic": round(r.statistic, 6), "p_value": round(r.p_value, 6), "is_significant": r.is_significant, "alpha": r.alpha, "alternative": r.alternative, "degrees_of_freedom": r.degrees_of_freedom or "", "sample_sizes": r.sample_sizes or "", "effect_size": (round(r.effect_size, 6) if r.effect_size is not None else ""), "effect_size_name": r.effect_size_name or "", "effect_magnitude": (r.effect_magnitude if r.effect_size is not None else ""), "ci_lower": round(ci_lower, 6) if ci_lower != "" else "", "ci_upper": round(ci_upper, 6) if ci_upper != "" else "", } ) with open(path, "w", newline="", encoding="utf-8") as f: # Write header f.write(sep.join(fieldnames) + "\n") for row in rows: f.write(sep.join(str(row[k]) for k in fieldnames) + "\n")
# --------------------------------------------------------------------------- # HTML export # ---------------------------------------------------------------------------
[docs] def export_html( result: "HypoResult", path: Optional[str] = None, ) -> str: """ Generate a self-contained HTML report for a single :class:`HypoResult`. Delegates to :func:`hypotestx.explore.visualize.generate_report` so that an embedded plot is included when matplotlib is installed. Parameters ---------- result : HypoResult path : optional output file path (e.g. ``"report.html"``). If *None*, the HTML string is returned without saving. Returns ------- str : HTML content """ from ..explore.visualize import generate_report return generate_report(result, path=path, fmt="html")
# --------------------------------------------------------------------------- # PDF export # ---------------------------------------------------------------------------
[docs] def export_pdf( result: "HypoResult", path: str, ) -> None: """ Save a PDF report for a single :class:`HypoResult`. Requires ``weasyprint``:: pip install weasyprint Parameters ---------- result : HypoResult path : output file path (e.g. ``"report.pdf"``). """ from ..explore.visualize import generate_report generate_report(result, path=path, fmt="pdf")
__all__ = [ "apa_report", "text_report", "batch_report", "export_csv", "export_html", "export_pdf", ]