mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 20:16:29 +03:00
PhD-level data science expertise with decision framework, five reference documents (statistical methodology, experimental design, causal inference, regression modeling, Bayesian workflow), five automation scripts (power analysis, assumption diagnostics, model comparison, effect size calculator, experimental design generator), and two report templates. Python default with --engine r flag for R output. Dual language support.
408 lines
15 KiB
Python
408 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Effect Size Calculator
|
||
|
||
Computes effect sizes with confidence intervals for common designs.
|
||
|
||
Usage:
|
||
python effect-size-calculator.py --design cohens-d --mean1 10 --mean2 8 --sd1 2.5 --sd2 2.8 --n1 30 --n2 30
|
||
python effect-size-calculator.py --design cohens-d --t-stat 3.41 --df 58
|
||
python effect-size-calculator.py --design eta-squared --ss-between 120 --ss-total 350
|
||
python effect-size-calculator.py --design cramers-v --chi-sq 12.4 --n 200 --min-dim 2
|
||
python effect-size-calculator.py --design odds-ratio --a 45 --b 55 --c 30 --d 70
|
||
python effect-size-calculator.py --design correlation --r 0.45 --n 100
|
||
python effect-size-calculator.py --design cohens-f2 --r-squared 0.34
|
||
python effect-size-calculator.py --design r-squared --r 0.58
|
||
python effect-size-calculator.py ... --json
|
||
python effect-size-calculator.py ... --engine r
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import math
|
||
import sys
|
||
|
||
try:
|
||
import numpy as np
|
||
from scipy import stats as sp_stats
|
||
HAS_NUMERIC = True
|
||
except ImportError:
|
||
HAS_NUMERIC = False
|
||
|
||
|
||
DESIGNS = {
|
||
"cohens-d": "Cohen's d (independent groups)",
|
||
"cohens-d-paired": "Cohen's d_z (paired groups)",
|
||
"hedges-g": "Hedges' g (corrected Cohen's d for small samples)",
|
||
"eta-squared": "Eta-squared (ANOVA)",
|
||
"partial-eta-squared": "Partial eta-squared (factorial ANOVA)",
|
||
"cohens-f": "Cohen's f (ANOVA effect size)",
|
||
"cohens-f2": "Cohen's f² (regression effect size)",
|
||
"r-squared": "R-squared from correlation",
|
||
"cramers-v": "Cramér's V (chi-square associations)",
|
||
"odds-ratio": "Odds ratio (2×2 tables)",
|
||
"risk-ratio": "Risk ratio / Relative risk (2×2 tables)",
|
||
"risk-difference": "Risk difference (2×2 tables)",
|
||
"pearson-r": "Pearson correlation",
|
||
"cohens-h": "Cohen's h (arcsine transformation for proportions)",
|
||
"glass-delta": "Glass's Δ (experimental SD as reference)",
|
||
}
|
||
|
||
|
||
def _check_deps():
|
||
if not HAS_NUMERIC:
|
||
print("Error: scipy required. pip install scipy numpy", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
def cohens_d_from_means(m1, m2, sd1, sd2, n1=None, n2=None):
|
||
"""Cohen's d with pooled SD."""
|
||
pooled_sd = math.sqrt(((n1 - 1) * sd1**2 + (n2 - 1) * sd2**2) / (n1 + n2 - 2)) if n1 and n2 else math.sqrt((sd1**2 + sd2**2) / 2)
|
||
d = (m1 - m2) / pooled_sd
|
||
return d, pooled_sd
|
||
|
||
|
||
def cohens_d_from_t(t, df, n1, n2):
|
||
"""Cohen's d from t-statistic."""
|
||
d = t * math.sqrt((n1 + n2) / (n1 * n2))
|
||
return d
|
||
|
||
|
||
def hedges_g(d, n1, n2):
|
||
"""Convert Cohen's d to Hedges' g (small sample correction)."""
|
||
df = n1 + n2 - 2
|
||
correction = 1 - (3 / (4 * df - 1))
|
||
return d * correction
|
||
|
||
|
||
def ci_cohens_d(d, n1, n2, alpha=0.05):
|
||
"""Confidence interval for Cohen's d using non-central t."""
|
||
_check_deps()
|
||
t_val = d * math.sqrt((n1 * n2) / (n1 + n2))
|
||
df = n1 + n2 - 2
|
||
try:
|
||
# Non-central t confidence interval
|
||
ncp_lower = sp_stats.nct.ppf(alpha / 2, df, t_val)
|
||
ncp_upper = sp_stats.nct.ppf(1 - alpha / 2, df, t_val)
|
||
d_lower = ncp_lower * math.sqrt((n1 + n2) / (n1 * n2))
|
||
d_upper = ncp_upper * math.sqrt((n1 + n2) / (n1 * n2))
|
||
return round(d_lower, 4), round(d_upper, 4)
|
||
except Exception:
|
||
# Fallback: delta method approximation
|
||
se_d = math.sqrt((n1 + n2) / (n1 * n2) + d**2 / (2 * (n1 + n2)))
|
||
z = sp_stats.norm.ppf(1 - alpha / 2)
|
||
return round(d - z * se_d, 4), round(d + z * se_d, 4)
|
||
|
||
|
||
def eta_squared(ss_between, ss_total):
|
||
"""η² = SS_between / SS_total"""
|
||
return ss_between / ss_total
|
||
|
||
|
||
def cohens_f_from_eta2(eta2):
|
||
"""f = sqrt(η² / (1 - η²))"""
|
||
return math.sqrt(eta2 / (1 - eta2))
|
||
|
||
|
||
def cohens_f2_from_r2(r2):
|
||
"""f² = R² / (1 - R²)"""
|
||
return r2 / (1 - r2)
|
||
|
||
|
||
def cramers_v(chi2, n, min_dim):
|
||
"""V = sqrt(χ² / (n * min(r-1, c-1)))"""
|
||
return math.sqrt(chi2 / (n * min_dim))
|
||
|
||
|
||
def odds_ratio(a, b, c, d):
|
||
"""OR = (a/c) / (b/d) = ad / bc"""
|
||
return (a * d) / (b * c)
|
||
|
||
|
||
def risk_ratio(a, b, c, d):
|
||
"""RR = (a/(a+b)) / (c/(c+d))"""
|
||
p1 = a / (a + b) if (a + b) > 0 else 0
|
||
p2 = c / (c + d) if (c + d) > 0 else 0
|
||
return p1 / p2 if p2 > 0 else float('inf')
|
||
|
||
|
||
def risk_difference(a, b, c, d):
|
||
"""RD = a/(a+b) - c/(c+d)"""
|
||
p1 = a / (a + b) if (a + b) > 0 else 0
|
||
p2 = c / (c + d) if (c + d) > 0 else 0
|
||
return p1 - p2
|
||
|
||
|
||
def ci_odds_ratio(a, b, c, d, alpha=0.05):
|
||
"""Woolf's CI for odds ratio."""
|
||
_check_deps()
|
||
or_val = odds_ratio(a, b, c, d)
|
||
se_log_or = math.sqrt(1/a + 1/b + 1/c + 1/d)
|
||
z = sp_stats.norm.ppf(1 - alpha / 2)
|
||
lo = math.exp(math.log(or_val) - z * se_log_or)
|
||
hi = math.exp(math.log(or_val) + z * se_log_or)
|
||
return round(lo, 4), round(hi, 4)
|
||
|
||
|
||
def ci_pearson_r(r, n, alpha=0.05):
|
||
"""Fisher z-transformation CI for Pearson r."""
|
||
_check_deps()
|
||
z_r = 0.5 * math.log((1 + r) / (1 - r))
|
||
se_z = 1 / math.sqrt(n - 3)
|
||
z = sp_stats.norm.ppf(1 - alpha / 2)
|
||
lo = math.tanh(z_r - z * se_z)
|
||
hi = math.tanh(z_r + z * se_z)
|
||
return round(lo, 4), round(hi, 4)
|
||
|
||
|
||
def cohens_h(p1, p2):
|
||
"""h = 2 * arcsin(sqrt(p1)) - 2 * arcsin(sqrt(p2))"""
|
||
return 2 * math.asin(math.sqrt(p1)) - 2 * math.asin(math.sqrt(p2))
|
||
|
||
|
||
def r_squared_from_r(r):
|
||
"""R² = r²"""
|
||
return r ** 2
|
||
|
||
|
||
def interpret_cohens_d(d):
|
||
if abs(d) < 0.2:
|
||
return "negligible"
|
||
elif abs(d) < 0.5:
|
||
return "small"
|
||
elif abs(d) < 0.8:
|
||
return "medium"
|
||
else:
|
||
return "large"
|
||
|
||
|
||
def run(args):
|
||
result = {}
|
||
|
||
if args.design == "cohens-d":
|
||
if args.mean1 is not None and args.mean2 is not None and args.sd1 is not None and args.sd2 is not None and args.n1 and args.n2:
|
||
d, pooled = cohens_d_from_means(args.mean1, args.mean2, args.sd1, args.sd2, args.n1, args.n2)
|
||
ci = ci_cohens_d(d, args.n1, args.n2)
|
||
result = {
|
||
"effect_size": "Cohen's d",
|
||
"d": round(d, 4),
|
||
"pooled_sd": round(pooled, 4),
|
||
"interpretation": interpret_cohens_d(d),
|
||
"ci_95": ci,
|
||
"parameters": {"mean1": args.mean1, "mean2": args.mean2, "n1": args.n1, "n2": args.n2}
|
||
}
|
||
elif args.t_stat is not None and args.df is not None:
|
||
# Need n1 and n2 to compute d from t
|
||
result = {"error": "t-statistic requires also --n1 and --n2 for Cohen's d"}
|
||
else:
|
||
result = {"error": "Provide --mean1 --mean2 --sd1 --sd2 --n1 --n2"}
|
||
|
||
elif args.design == "hedges-g":
|
||
if args.mean1 is not None and args.mean2 is not None and args.sd1 is not None and args.sd2 is not None and args.n1 and args.n2:
|
||
d, pooled = cohens_d_from_means(args.mean1, args.mean2, args.sd1, args.sd2, args.n1, args.n2)
|
||
g = hedges_g(d, args.n1, args.n2)
|
||
result = {
|
||
"effect_size": "Hedges' g",
|
||
"g": round(g, 4),
|
||
"cohens_d": round(d, 4),
|
||
"interpretation": interpret_cohens_d(d),
|
||
"parameters": {"mean1": args.mean1, "mean2": args.mean2, "n1": args.n1, "n2": args.n2}
|
||
}
|
||
else:
|
||
result = {"error": "Provide --mean1 --mean2 --sd1 --sd2 --n1 --n2"}
|
||
|
||
elif args.design == "eta-squared":
|
||
if args.ss_between is not None and args.ss_total is not None:
|
||
eta2 = eta_squared(args.ss_between, args.ss_total)
|
||
f = cohens_f_from_eta2(eta2)
|
||
result = {
|
||
"effect_size": "Eta-squared",
|
||
"eta_squared": round(eta2, 4),
|
||
"cohens_f": round(f, 4),
|
||
"interpretation": "small" if eta2 < 0.01 else ("medium" if eta2 < 0.06 else ("large" if eta2 < 0.14 else "very large")),
|
||
}
|
||
else:
|
||
result = {"error": "Provide --ss-between and --ss-total"}
|
||
|
||
elif args.design == "cohens-f2":
|
||
if args.r_squared is not None:
|
||
f2 = cohens_f2_from_r2(args.r_squared)
|
||
result = {
|
||
"effect_size": "Cohen's f²",
|
||
"f_squared": round(f2, 4),
|
||
"r_squared": args.r_squared,
|
||
"interpretation": "small" if f2 < 0.02 else ("medium" if f2 < 0.15 else "large"),
|
||
}
|
||
else:
|
||
result = {"error": "Provide --r-squared"}
|
||
|
||
elif args.design == "cramers-v":
|
||
if args.chi_sq is not None and args.n is not None and args.min_dim is not None:
|
||
v = cramers_v(args.chi_sq, args.n, args.min_dim)
|
||
result = {
|
||
"effect_size": "Cramér's V",
|
||
"V": round(v, 4),
|
||
"parameters": {"chi_sq": args.chi_sq, "n": args.n, "min_dim": args.min_dim},
|
||
"interpretation": "small" if v < 0.1 else ("medium" if v < 0.3 else "large"),
|
||
}
|
||
else:
|
||
result = {"error": "Provide --chi-sq --n --min-dim"}
|
||
|
||
elif args.design == "odds-ratio":
|
||
if args.a is not None and args.b is not None and args.c is not None and args.d is not None:
|
||
or_val = odds_ratio(args.a, args.b, args.c, args.d)
|
||
ci = ci_odds_ratio(args.a, args.b, args.c, args.d)
|
||
rr = risk_ratio(args.a, args.b, args.c, args.d)
|
||
rd = risk_difference(args.a, args.b, args.c, args.d)
|
||
result = {
|
||
"effect_size": "Odds ratio",
|
||
"OR": round(or_val, 4),
|
||
"ci_95": ci,
|
||
"RR": round(rr, 4),
|
||
"risk_difference": round(rd, 4),
|
||
"log_OR": round(math.log(or_val), 4),
|
||
"interpretation": "OR = 1 (no effect)" if 0.95 <= or_val <= 1.05 else
|
||
f"OR > 1 (increased odds)" if or_val > 1 else "OR < 1 (decreased odds)",
|
||
}
|
||
else:
|
||
result = {"error": "Provide --a --b --c --d (2×2 table counts)"}
|
||
|
||
elif args.design == "risk-ratio":
|
||
if args.a is not None and args.b is not None and args.c is not None and args.d is not None:
|
||
rr = risk_ratio(args.a, args.b, args.c, args.d)
|
||
result = {
|
||
"effect_size": "Risk ratio",
|
||
"RR": round(rr, 4),
|
||
"interpretation": "RR = 1 (no effect)" if 0.95 <= rr <= 1.05 else ("RR > 1 (increased risk)" if rr > 1 else "RR < 1 (decreased risk)"),
|
||
}
|
||
else:
|
||
result = {"error": "Provide --a --b --c --d"}
|
||
|
||
elif args.design == "risk-difference":
|
||
if args.a is not None and args.b is not None and args.c is not None and args.d is not None:
|
||
rd = risk_difference(args.a, args.b, args.c, args.d)
|
||
result = {
|
||
"effect_size": "Risk difference",
|
||
"RD": round(rd, 4),
|
||
"interpretation": f"Absolute difference of {abs(rd):.1%} in risk",
|
||
}
|
||
else:
|
||
result = {"error": "Provide --a --b --c --d"}
|
||
|
||
elif args.design in ("pearson-r", "correlation"):
|
||
if args.r is not None and args.n is not None:
|
||
ci = ci_pearson_r(args.r, args.n)
|
||
result = {
|
||
"effect_size": "Pearson r",
|
||
"r": args.r,
|
||
"r_squared": round(args.r ** 2, 4),
|
||
"ci_95": ci,
|
||
"n": args.n,
|
||
"interpretation": "small" if abs(args.r) < 0.1 else ("medium" if abs(args.r) < 0.3 else "large"),
|
||
}
|
||
else:
|
||
result = {"error": "Provide --r and --n"}
|
||
|
||
elif args.design == "r-squared":
|
||
if args.r is not None:
|
||
r2 = r_squared_from_r(args.r)
|
||
result = {
|
||
"effect_size": "R-squared",
|
||
"r_squared": round(r2, 4),
|
||
"r": args.r,
|
||
}
|
||
else:
|
||
result = {"error": "Provide --r"}
|
||
|
||
elif args.design == "cohens-h":
|
||
if args.p1 is not None and args.p2 is not None:
|
||
h = cohens_h(args.p1, args.p2)
|
||
result = {
|
||
"effect_size": "Cohen's h",
|
||
"h": round(h, 4),
|
||
"parameters": {"p1": args.p1, "p2": args.p2},
|
||
"interpretation": "small" if abs(h) < 0.2 else ("medium" if abs(h) < 0.5 else "large"),
|
||
}
|
||
else:
|
||
result = {"error": "Provide --p1 and --p2"}
|
||
|
||
else:
|
||
result = {"error": f"Unknown design '{args.design}'"}
|
||
|
||
if "error" in result:
|
||
print(f"Error: {result['error']}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if args.json:
|
||
print(json.dumps(result, indent=2, default=str))
|
||
else:
|
||
print(f"## {result.get('effect_size', 'Effect Size')}")
|
||
print(f"Value: {result.get(list(result.keys())[1], '?')}")
|
||
if "ci_95" in result:
|
||
print(f"95% CI: ({result['ci_95'][0]:.4f}, {result['ci_95'][1]:.4f})")
|
||
if "interpretation" in result:
|
||
print(f"Interpretation: {result['interpretation']}")
|
||
if "r_squared" in result:
|
||
print(f"R²: {result['r_squared']}")
|
||
print()
|
||
# Report template
|
||
print("Reporting template:")
|
||
if result.get("effect_size") == "Cohen's d":
|
||
print(f" d = {result['d']:.2f}, 95% CI [{result['ci_95'][0]:.2f}, {result['ci_95'][1]:.2f}]")
|
||
elif result.get("effect_size") == "Odds ratio":
|
||
print(f" OR = {result['OR']:.2f}, 95% CI [{result['ci_95'][0]:.2f}, {result['ci_95'][1]:.2f}]")
|
||
elif result.get("effect_size") == "Cramér's V":
|
||
print(f" V = {result['V']:.2f}")
|
||
|
||
if args.engine == "r":
|
||
print("\n--- R equivalent ---")
|
||
print("library(effectsize)")
|
||
if result.get("effect_size") == "Cohen's d":
|
||
print(f"cohens_d({args.mean1}, {args.mean2}, pooled_sd = TRUE)" if args.mean1 else "# Provide data vectors")
|
||
elif result.get("effect_size") == "Pearson r":
|
||
print(f"library(psych); r.con(r = {args.r}, n = {args.n}, p = 0.95)")
|
||
elif result.get("effect_size") == "Cramér's V":
|
||
print(f"cramers_v(chi2 = {args.chi_sq}, n = {args.n}, nrow = {args.min_dim + 1})")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Effect Size Calculator")
|
||
parser.add_argument("--design", choices=list(DESIGNS.keys()), required=True,
|
||
help="Effect size type")
|
||
# Means/SDs
|
||
parser.add_argument("--mean1", type=float)
|
||
parser.add_argument("--mean2", type=float)
|
||
parser.add_argument("--sd1", type=float)
|
||
parser.add_argument("--sd2", type=float)
|
||
parser.add_argument("--n1", type=int)
|
||
parser.add_argument("--n2", type=int)
|
||
parser.add_argument("--t-stat", type=float)
|
||
parser.add_argument("--df", type=int)
|
||
# ANOVA
|
||
parser.add_argument("--ss-between", type=float)
|
||
parser.add_argument("--ss-total", type=float)
|
||
# Regression
|
||
parser.add_argument("--r-squared", type=float)
|
||
parser.add_argument("--r", type=float)
|
||
# Categorical
|
||
parser.add_argument("--chi-sq", type=float)
|
||
parser.add_argument("--n", type=int)
|
||
parser.add_argument("--min-dim", type=int, help="Cramér's V: min(rows-1, cols-1). For a 2×2 table pass 1; for 3×4 table pass 2 (min(2,3)=2)")
|
||
parser.add_argument("--a", type=float)
|
||
parser.add_argument("--b", type=float)
|
||
parser.add_argument("--c", type=float)
|
||
parser.add_argument("--d", type=float)
|
||
# Proportions
|
||
parser.add_argument("--p1", type=float)
|
||
parser.add_argument("--p2", type=float)
|
||
# General
|
||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||
parser.add_argument("--engine", choices=["python", "r"], default="python",
|
||
help="Output language")
|
||
args = parser.parse_args()
|
||
run(args)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|