init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
"""单因素随机效应 ANOVA + 广义遗传力 H²(纯 Python,numpy + fdist)。
|
||||
|
||||
模型:y_ij = μ + a_i + e_ij,a_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/MSW,p = 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 双因素 ANOVA:y_ij = μ + a_i(组合/家系) + b_j(区组) + e_ij。
|
||||
|
||||
区组效应序贯析出(Type I:grand 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,
|
||||
}
|
||||
Reference in New Issue
Block a user