Files

264 lines
11 KiB
Python
Raw Permalink Normal View History

"""纯 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))