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

264 lines
11 KiB
Python
Raw 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.
"""纯 Python 配合力分析(GCA / SCA),按交配设计分支。
仅依赖 numpy,无 R / 外部运行时。
模型 A —— 双列(full_diallel / partial_diallelGriffing 对称双亲,每组合一条均值观测):
y_ij = μ + g_i + g_j + s_ij
- μ:总体均值;
- g_i、g_j:亲本一般配合力(GCA,施加 Σg=0 约束);
- s_ij:组合特殊配合力(SCA= 残差 y_ij - (μ + g_i + g_j)。
模型 B —— line×testerline_tester / NCII)与 NCIIInciii)两因素模型:
y_ij = μ + l_i + t_j + (lt)_ij
- l_i:line(母本)一般配合力,t_j:tester(父本)一般配合力(各施加 Σ=0 约束);
- (lt)_ijline×tester 互作 = 特殊配合力 SCA(残差)。
NCIII 即 tester 恰好为 2 个的同一模型(测交种 T1/T2)。
与旧 R(lme4) 随机效应版的语义一致(GCA 归并到亲本级、SCA 归并到组合级),
但用最小二乘 BLUE(经典 Griffing / 经典 NCII),确定、可复现、无 R 依赖。
"""
from __future__ import annotations
import numpy as np
from . import fdist
DIALLEL_DESIGNS = {"full_diallel", "partial_diallel"}
TWO_FACTOR_DESIGNS = {"line_tester", "ncii", "nciii"}
DESIGNS = DIALLEL_DESIGNS | TWO_FACTOR_DESIGNS
def solve(rows: list[dict], design_type: str = "full_diallel") -> dict:
"""运行配合力 GCA / SCA,按交配设计分支。
参数:
rows: [{"combo": 组合标识(str), "parent1": 母本种质id(int),
"parent2": 父本种质id(int), "value": 组合表型均值(float)}]
design_type: full_diallel(完全双列,默认) / partial_diallel(部分双列) /
line_tester(line×tester NCII 两因素) / nciii(NCIII 测交)。
NCII/NCIII 中 parent1=line(母本)、parent2=tester(父本),两角色集合须不相交。
返回:
gca: {parent_id(str): float} 各亲本一般配合力(Σg=0);
sca: {combo: {"parent1": int, "parent2": int, "sca": float}} 组合特殊配合力;
gca_se: {parent_id(str): float} GCA 估计标准误;
anova: 配合力变异分解(GCA vs SCA / line vs tester vs 互作),
F/p 给出「对应配合力是否显著」。NCII/NCIII 附 roles 标注 line/tester。
"""
if not rows:
raise ValueError("无组合表型数据")
dt = (design_type or "full_diallel").lower()
if dt not in DESIGNS:
raise ValueError(f"未知交配设计: {design_type}(可用: {sorted(DESIGNS)}")
if dt in TWO_FACTOR_DESIGNS:
return _solve_two_factor(rows, dt)
return _solve_diallel(rows, dt)
def _solve_diallel(rows: list[dict], design_type: str) -> dict:
"""模型 AGriffing 对称双亲。full_diallel 与 partial_diallel 同一最小二乘。"""
parents = sorted({r["parent1"] for r in rows} | {r["parent2"] for r in rows})
if len(parents) < 2:
raise ValueError("至少需要 2 个不同亲本才能估计配合力")
n_p = len(parents)
p_idx = {p: i for i, p in enumerate(parents)}
n = len(rows)
last = parents[-1]
# 设计阵:截距 + 每个亲本一列(自交时亲本同格计数 2)。X 保持原样用于预测。
X = np.zeros((n, 1 + n_p))
X[:, 0] = 1.0
for k, r in enumerate(rows):
for p in (r["parent1"], r["parent2"]):
X[k, 1 + p_idx[p]] += 1.0
# 施加 Σg=0:在副本上用最后一亲本列吸收其余亲本列,得到可识别的设计阵。
Xd = X[:, :n_p].copy()
for j in range(n_p - 1):
Xd[:, 1 + j] -= X[:, 1 + (n_p - 1)]
y = np.array([float(r["value"]) for r in rows])
beta, *_ = np.linalg.lstsq(Xd, y, rcond=None)
mu = float(beta[0])
g = np.zeros(n_p)
g[:-1] = beta[1:]
g[-1] = -float(g[:-1].sum())
pred = X @ np.concatenate([[mu], g])
gca = {str(p): round(float(g[p_idx[p]]), 6) for p in parents}
sca: dict[str, dict] = {}
sca_vals = np.zeros(n)
for k, r in enumerate(rows):
s = float(y[k] - pred[k])
sca_vals[k] = s
sca[r["combo"]] = {
"parent1": int(r["parent1"]),
"parent2": int(r["parent2"]),
"sca": round(s, 6),
}
# 配合力方差分解:SS_gca(亲本列贡献)、SS_sca(残差,无重复观测时作误差项)。
df_gca = n_p - 1
df_sca = n - 1 - df_gca
ss_total = float((y - y.mean()) @ (y - y.mean()))
ss_gca = float((pred - y.mean()) @ (pred - y.mean()))
ss_sca = float(sca_vals @ sca_vals)
ms_gca = ss_gca / df_gca if df_gca > 0 else 0.0
ms_sca = ss_sca / df_sca if df_sca > 0 else 0.0
f_gca = ms_gca / ms_sca if ms_sca > 0 else 1.0
p_gca = fdist.f_pvalue(f_gca, df_gca, df_sca) if (df_sca > 0 and ms_sca > 0) else 0.5
# GCA 标准误:SE = sqrt(MSe / n_i)n_i = 亲本出现的观测次数;MSe 用 MS_sca 近似。
gca_se: dict[str, float] = {}
warnings: list[str] = []
for p in parents:
cnt = sum(1 for r in rows if r["parent1"] == p or r["parent2"] == p)
gca_se[str(p)] = round(float(np.sqrt(ms_sca / cnt)) if ms_sca > 0 and cnt > 0 else 0.0, 6)
if cnt == 1:
warnings.append(f"亲本 {p} 仅出现在 1 个组合,GCA 与 SCA 部分混杂(设计不完备)")
return {
"gca": gca,
"sca": sca,
"gca_se": gca_se,
"anova": {
"design": design_type,
"df_gca": df_gca,
"df_sca": df_sca,
"ss_gca": round(ss_gca, 4),
"ss_sca": round(ss_sca, 4),
"ms_gca": round(ms_gca, 4),
"ms_sca": round(ms_sca, 4),
"f_gca": round(float(f_gca), 4),
"p_gca": round(p_gca, 6),
"warnings": warnings,
},
}
def _solve_two_factor(rows: list[dict], design_type: str) -> dict:
"""模型 Bline×tester 两因素(NCII/ NCIII。
parent1=line(母本)、parent2=tester(父本)。line GCA 与 tester GCA 分别估计,
SCA = line×tester 互作(残差)。NCIII 要求 tester 恰好 2 个。
"""
lines = sorted({r["parent1"] for r in rows})
testers = sorted({r["parent2"] for r in rows})
if not lines or not testers:
raise ValueError("line×tester 设计需母本(line)与父本(tester)亲本")
overlap = set(lines) & set(testers)
if overlap:
raise ValueError(
f"line×tester 设计父本角色重叠: {sorted(overlap)}——同一亲本不能既作 line 又作 tester"
)
if design_type == "nciii" and len(testers) != 2:
raise ValueError(f"NCIII 设计需恰好 2 个测交种(tester),当前 {len(testers)} 个")
n_l, n_t = len(lines), len(testers)
l_idx = {p: i for i, p in enumerate(lines)}
t_idx = {p: i for i, p in enumerate(testers)}
n = len(rows)
# 设计阵:截距 + 每 line 一列 + 每 tester 一列(全哑变量)。X 保持原样用于预测。
X = np.zeros((n, 1 + n_l + n_t))
X[:, 0] = 1.0
for k, r in enumerate(rows):
X[k, 1 + l_idx[r["parent1"]]] = 1.0
X[k, 1 + n_l + t_idx[r["parent2"]]] = 1.0
# 施加 Σl=0、Σt=0:选取截距 + 前 n_l-1 个 line 列 + 前 n_t-1 个 tester 列(基线 line/tester 列
# 被吸收进截距),再在副本上用基线列吸收其余列,得到可识别的 (1+(n_l-1)+(n_t-1)) 列设计阵。
# 注意 tester 列位于 1+n_l 起,不能整段切片。
sel = [0] + list(range(1, n_l)) + list(range(1 + n_l, 1 + n_l + n_t - 1))
Xd = X[:, sel].copy()
for j in range(n_l - 1):
Xd[:, 1 + j] -= X[:, 1 + (n_l - 1)]
for j in range(n_t - 1):
Xd[:, 1 + (n_l - 1) + j] -= X[:, 1 + n_l + (n_t - 1)]
y = np.array([float(r["value"]) for r in rows])
beta, *_ = np.linalg.lstsq(Xd, y, rcond=None)
mu = float(beta[0])
g_l = np.zeros(n_l)
g_l[:-1] = beta[1:n_l]
g_l[-1] = -float(g_l[:-1].sum())
g_t = np.zeros(n_t)
g_t[:-1] = beta[n_l:n_l + n_t - 1]
g_t[-1] = -float(g_t[:-1].sum())
pred = X @ np.concatenate([[mu], g_l, g_t])
gca: dict[str, float] = {}
for i, p in enumerate(lines):
gca[str(p)] = round(float(g_l[i]), 6)
for j, p in enumerate(testers):
gca[str(p)] = round(float(g_t[j]), 6)
sca: dict[str, dict] = {}
sca_vals = np.zeros(n)
for k, r in enumerate(rows):
s = float(y[k] - pred[k])
sca_vals[k] = s
sca[r["combo"]] = {
"parent1": int(r["parent1"]),
"parent2": int(r["parent2"]),
"sca": round(s, 6),
}
# 顺序方差分解:SS_line(截距→line)、SS_tester(→tester,扣除 line)、SS_sca(残差=互作)。
ss_total = float((y - y.mean()) @ (y - y.mean()))
rss_line = _residual_ss(y, _design(X, n_l))
rss_tester = _residual_ss(y, Xd) # 全模型(含 line+tester)残差 = Σsca²
ss_line = ss_total - rss_line
ss_tester = rss_line - rss_tester
ss_sca = float(sca_vals @ sca_vals)
df_line = n_l - 1
df_tester = n_t - 1
df_sca = n - 1 - df_line - df_tester
ms_line = ss_line / df_line if df_line > 0 else 0.0
ms_tester = ss_tester / df_tester if df_tester > 0 else 0.0
ms_sca = ss_sca / df_sca if df_sca > 0 else 0.0
f_line = ms_line / ms_sca if ms_sca > 0 else 1.0
f_tester = ms_tester / ms_sca if ms_sca > 0 else 1.0
p_line = fdist.f_pvalue(f_line, df_line, df_sca) if (df_sca > 0 and ms_sca > 0) else 0.5
p_tester = fdist.f_pvalue(f_tester, df_tester, df_sca) if (df_sca > 0 and ms_sca > 0) else 0.5
# GCA 标准误:SE = sqrt(MSe / n_i)n_i = 亲本出现次数(line 每 tester 组 n_t 次等)。
gca_se: dict[str, float] = {}
for p in lines:
cnt = sum(1 for r in rows if r["parent1"] == p)
gca_se[str(p)] = round(float(np.sqrt(ms_sca / cnt)) if ms_sca > 0 and cnt > 0 else 0.0, 6)
for p in testers:
cnt = sum(1 for r in rows if r["parent2"] == p)
gca_se[str(p)] = round(float(np.sqrt(ms_sca / cnt)) if ms_sca > 0 and cnt > 0 else 0.0, 6)
return {
"gca": gca,
"sca": sca,
"gca_se": gca_se,
"anova": {
"design": design_type,
"roles": {"lines": lines, "testers": testers},
"df_line": df_line,
"df_tester": df_tester,
"df_sca": df_sca,
"ss_line": round(ss_line, 4),
"ss_tester": round(ss_tester, 4),
"ss_sca": round(ss_sca, 4),
"ms_line": round(ms_line, 4),
"ms_tester": round(ms_tester, 4),
"ms_sca": round(ms_sca, 4),
"f_line": round(float(f_line), 4),
"f_tester": round(float(f_tester), 4),
"p_line": round(p_line, 6),
"p_tester": round(p_tester, 6),
},
}
def _design(X: np.ndarray, n_l: int) -> np.ndarray:
"""截距 + 全 line 哑变量的子设计阵(SS_line 用)。"""
return X[:, : 1 + n_l].copy()
def _residual_ss(y: np.ndarray, A: np.ndarray) -> float:
"""最小二乘残差平方和 RSS = y^T y - y^T A (A^T A)^-1 A^T ylstsq 求拟合格)。"""
beta, *_ = np.linalg.lstsq(A, y, rcond=None)
pred = A @ beta
return float((y - pred) @ (y - pred))