Files
dpb/backend/scripts/breeding_stats/anova.py
T

187 lines
7.5 KiB
Python
Raw Normal View History

"""单因素随机效应 ANOVA + 广义遗传力 H²(纯 Pythonnumpy + fdist)。
模型:y_ij = μ + a_i + e_ija_i ~ N(0, σ²g)e_ij ~ N(0, σ²e)。
分组(group)= 家系/组合,用于估计家系方差组分:
n₀ = (N - Σ n_i²/N)/(k-1) 加权平均每组样本量
σ²e = MSW 组内均方 = 环境/误差方差
σ²g = (MSB - MSW)/n₀ 组间方差组分(家系遗传方差)
H² = σ²g/(σ²g + σ²e) 广义遗传力(单株 basis)
F = MSB/MSWp = 1-P(F≤f)
鲁棒性:组数/样本不足、组内方差为 0(全组同值)、n₀≤0 时均不崩溃,
按合理默认(H²=0、F=1、p=0.5)返回并给 warning。
"""
from __future__ import annotations
import numpy as np
from . import fdist
def solve(rows: list[dict]) -> dict:
"""rows: [{"group": str|int, "value": float}]。
返回:方差分析表字段 + σ²g/σ²e + H² + F/p + 各组均值 + warning。
"""
if not rows:
raise ValueError("无分组表型数据")
y = np.array([float(r["value"]) for r in rows], dtype=float)
g = np.array([r["group"] for r in rows])
if len(y) < 2 or len(np.unique(g)) < 2:
raise ValueError("至少需要 2 个组且总样本 ≥ 2 才能做方差分析")
n = len(y)
overall = float(y.mean())
group_keys = sorted(set(g))
k = len(group_keys)
sizes: list[int] = []
means: list[float] = []
for gv in group_keys:
mask = g == gv
sizes.append(int(mask.sum()))
means.append(float(y[mask].mean()))
ss_total = float(((y - overall) ** 2).sum())
ss_between = float(sum(sz * (m - overall) ** 2 for sz, m in zip(sizes, means)))
ss_within = ss_total - ss_between
ms_between = ss_between / (k - 1) if k > 1 else 0.0
ms_within = ss_within / (n - k) if n > k else 0.0
n0 = (n - sum(sz * sz for sz in sizes) / n) / (k - 1) if k > 1 else 0.0
sigma_e = ms_within
sigma_g = (ms_between - ms_within) / n0 if n0 > 0 else 0.0
h2 = sigma_g / (sigma_g + sigma_e) if (sigma_g + sigma_e) > 1e-12 else 0.0
h2 = float(np.clip(h2, 0.0, 1.0))
f = ms_between / ms_within if ms_within > 0 else 1.0
p = fdist.f_pvalue(f, k - 1, n - k) if (k > 1 and n > k and ms_within > 0) else 0.5
warning = None
if ms_within == 0:
warning = "组内方差为 0(各组观测完全一致),方差组分不可分,H² 无信息。"
elif sigma_g <= 1e-12:
warning = "家系方差组分 ≈ 0,组间差异不显著,H² 接近 0。"
return {
"n_groups": k,
"n": n,
"overall_mean": round(overall, 4),
"ss_total": round(ss_total, 4),
"ss_between": round(ss_between, 4),
"ss_within": round(ss_within, 4),
"ms_between": round(ms_between, 4),
"ms_within": round(ms_within, 4),
"sigma_g": round(sigma_g, 4),
"sigma_e": round(sigma_e, 4),
"h2_broad": h2,
"f": round(float(f), 4),
"p_value": round(p, 6),
"groups": [
{"group": str(gv), "n": sz, "mean": round(m, 4)}
for gv, sz, m in zip(group_keys, sizes, means)
],
"warning": warning,
}
def solve_rcbd(rows: list[dict]) -> dict:
"""RCBD 双因素 ANOVAy_ij = μ + a_i(组合/家系) + b_j(区组) + e_ij。
区组效应序贯析出(Type Igrand mean → group → block),遗传差异为区组校正后
SS_group 先于区组入模型、SS_block 为其后增量、残差为两者剔除后)。
输出区组行 SS_block/MS_block/F_block/p_block 与区组均值。区组数不足或无残余
自由度时按数据现状返回 warning,不崩溃。
rows: [{"group": str|int, "block": str|int, "value": float}]
"""
if not rows:
raise ValueError("无 RCBD 表型数据")
y = np.array([float(r["value"]) for r in rows], dtype=float)
g = np.array([str(r["group"]) for r in rows])
b = np.array([str(r["block"]) for r in rows])
n = len(y)
if n < 2:
raise ValueError("至少需要 2 个观测")
g_keys = sorted(set(g))
b_keys = sorted(set(b))
kg, kb = len(g_keys), len(b_keys)
if kg < 2:
raise ValueError("至少需要 2 个组合")
if kb < 2:
raise ValueError("至少需要 2 个区组才能做 RCBD 分析")
overall = float(y.mean())
def design(*factors: tuple[list, np.ndarray]) -> np.ndarray:
# 截距 + 每个因子 drop 第一个水平的哑变量 → 满秩序贯设计阵
cols = [np.ones(n)]
for keys, arr in factors:
for kv in keys[1:]:
cols.append((arr == kv).astype(float))
return np.column_stack(cols)
X1 = np.ones((n, 1))
Xg = design((g_keys, g))
Xgb = design((g_keys, g), (b_keys, b))
def rss(X: np.ndarray) -> float:
beta, *_ = np.linalg.lstsq(X, y, rcond=None)
resid = y - X @ beta
return float(resid @ resid)
ss_total = float(((y - overall) ** 2).sum())
ss_group = max(rss(X1) - rss(Xg), 0.0) # 组合效应(区组校正前)
ss_block = max(rss(Xg) - rss(Xgb), 0.0) # 区组效应(组合剔除后析出)
ss_error = max(rss(Xgb), 0.0) # 残余误差
df_group = kg - 1
df_block = kb - 1
df_error = n - kg - kb + 1
ms_group = ss_group / df_group if df_group > 0 else 0.0
ms_block = ss_block / df_block if df_block > 0 else 0.0
ms_error = ss_error / df_error if df_error > 0 else 0.0
sizes = [int((g == gv).sum()) for gv in g_keys]
means = [float(y[g == gv].mean()) for gv in g_keys]
n0 = (n - sum(sz * sz for sz in sizes) / n) / (kg - 1) if kg > 1 else 0.0
sigma_e = ms_error
sigma_g = (ms_group - ms_error) / n0 if n0 > 0 else 0.0
h2 = sigma_g / (sigma_g + sigma_e) if (sigma_g + sigma_e) > 1e-12 else 0.0
h2 = float(np.clip(h2, 0.0, 1.0))
f_group = ms_group / ms_error if ms_error > 0 else 1.0
f_block = ms_block / ms_error if ms_error > 0 else 1.0
p_group = fdist.f_pvalue(f_group, df_group, df_error) if (df_error > 0 and ms_error > 0) else 0.5
p_block = fdist.f_pvalue(f_block, df_block, df_error) if (df_error > 0 and ms_error > 0) else 0.5
warning = None
if df_error <= 0:
warning = "区组设计过饱和:无残余误差自由度(每组合每区组仅 1 株),无法估计误差项。"
elif ms_error == 0:
warning = "残余误差为 0(区组+组合完全解释变异),F 值无信息。"
elif sigma_g <= 1e-12:
warning = "家系方差组分 ≈ 0,组间差异不显著,H² 接近 0。"
return {
"n_groups": kg,
"n_blocks": kb,
"n": n,
"overall_mean": round(overall, 4),
"ss_total": round(ss_total, 4),
"ss_between": round(ss_group, 4),
"ss_block": round(ss_block, 4),
"ss_within": round(ss_error, 4),
"ms_between": round(ms_group, 4),
"ms_block": round(ms_block, 4),
"ms_within": round(ms_error, 4),
"sigma_g": round(sigma_g, 4),
"sigma_e": round(sigma_e, 4),
"h2_broad": h2,
"f": round(float(f_group), 4),
"p_value": round(p_group, 6),
"f_block": round(float(f_block), 4),
"p_block": round(p_block, 6),
"groups": [
{"group": str(gv), "n": sz, "mean": round(m, 4)}
for gv, sz, m in zip(g_keys, sizes, means)
],
"blocks": [
{"block": str(bv), "n": int((b == bv).sum()), "mean": round(float(y[b == bv].mean()), 4)}
for bv in b_keys
],
"warning": warning,
}