Files
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

187 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""单因素随机效应 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,
}