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

153 lines
6.6 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.
"""稳定性分析:AMMI(加性主效应乘性互作 SVD)与 Finlay-Wilkinson(环境指数回归)。
仅依赖 numpy。输入为基因型×环境两因素均值表 {genotype: {env: mean}}(缺格 NaN)。
- Finlay-Wilkinson:每基因型对该环境指数(该环境全部基因型均值)回归 y_gj = b·I_j + a。
b≈1|b-1| ≤ 2·se(b))且残差小 → 稳定(与总体响应一致);b>1 高响应(好环境下增益更大)、
b<1 低响应。
- AMMI:两因素主效应分解(μ+g+e)后残差矩阵 SVD → IPC1/IPC2 得分、
ASVPurchase et al. 2000:两轴权重按各自 SS 归一后合成)、Wricke ecovalence
(Σ_j (y_ij y_i. y_.j + y..)²)。
稳定排名:AMMI 按 ASV 升序(越小越稳定,rank=1 最稳);FW 由 flag_stable 直接标注。
"""
from __future__ import annotations
import math
import numpy as np
ENGINE_VERSION = "1.0.0"
def _table_to_arrays(table: dict[str, dict[str, float]]):
genotypes = sorted(table)
envs: list[str] = []
seen: set[str] = set()
for g in genotypes:
for e in table[g]:
if e not in seen:
seen.add(e)
envs.append(e)
envs.sort()
Y = np.full((len(genotypes), len(envs)), np.nan)
for gi, g in enumerate(genotypes):
for e, v in table[g].items():
Y[gi, envs.index(e)] = float(v)
return Y, genotypes, envs
def finlay_wilkinson(table: dict) -> dict:
"""Finlay-Wilkinson:每基因型对环境指数回归,返回 b/截距/R²/残差MS/稳定标记。"""
Y, genotypes, envs = _table_to_arrays(table)
env_index = np.nanmean(Y, axis=0) # 环境指数 = 该环境全基因型均值
rows: list[dict] = []
for gi, g in enumerate(genotypes):
y = Y[gi]
mask = (~np.isnan(y)) & (~np.isnan(env_index))
n = int(mask.sum())
if n < 2:
rows.append({"genotype": g, "n_env": n, "b": None, "intercept": None,
"r2": None, "se_b": None, "dev_ms": None,
"flag_stable": None, "note": "环境数不足 2,无法回归"})
continue
xs = env_index[mask]
ys = y[mask]
A = np.column_stack([np.ones(n), xs])
coef, *_ = np.linalg.lstsq(A, ys, rcond=None)
intercept, b = float(coef[0]), float(coef[1])
resid = ys - A @ coef
dof = n - 2
dev_ms = float(resid @ resid / dof) if dof > 0 else float("nan")
ss_tot = float(((ys - ys.mean()) ** 2).sum())
r2 = 1.0 - float(resid @ resid) / ss_tot if ss_tot > 0 else float("nan")
var_x = float(((xs - xs.mean()) ** 2).sum())
se_b = math.sqrt(dev_ms / var_x) if var_x > 0 and not math.isnan(dev_ms) else float("nan")
y_scale = float(np.ptp(ys)) if len(ys) > 1 else 0.0
if math.isnan(se_b):
flag_stable = False
elif dev_ms <= 1e-12 * max(1.0, y_scale ** 2):
# 完美拟合(残差≈0)时 se_b 无信息量;b 精确已知,b≈1 才视为稳定
flag_stable = abs(b - 1.0) <= 1e-6
else:
flag_stable = abs(b - 1.0) <= 2.0 * se_b
rows.append({
"genotype": g, "n_env": n, "b": round(b, 4), "intercept": round(intercept, 4),
"r2": round(r2, 4) if not math.isnan(r2) else None,
"se_b": round(se_b, 4) if not math.isnan(se_b) else None,
"dev_ms": round(dev_ms, 6) if not math.isnan(dev_ms) else None,
"flag_stable": bool(flag_stable),
})
return {
"method": "finlay_wilkinson",
"genotypes": genotypes, "environments": envs,
"env_index": [None if math.isnan(v) else round(float(v), 4) for v in env_index],
"rows": rows,
}
def ammi(table: dict, n_components: int = 2) -> dict:
"""AMMI:两因素主效应分解 + 残差 SVD,输出 IPC 得分/ASV/ecovalence 与稳定排名。"""
Y, genotypes, envs = _table_to_arrays(table)
g, e = Y.shape
if g < 2 or e < 2:
return {"method": "ammi", "genotypes": genotypes, "environments": envs,
"rows": [], "ipc_variance": [], "n": 0,
"warning": "基因型或环境数不足 2,无法 AMMI"}
has_missing = bool(np.isnan(Y).any())
grand = float(np.nanmean(Y))
row_mean = np.nanmean(Y, axis=1)
col_mean = np.nanmean(Y, axis=0)
resid = np.nan_to_num(Y - row_mean[:, None] - col_mean[None, :] + grand, nan=0.0)
U, S, _Vt = np.linalg.svd(resid, full_matrices=False)
k = min(g, e)
ss_total = float(np.sum(resid ** 2))
ipc_var = []
for i in range(k):
prop = (S[i] ** 2) / ss_total if ss_total > 0 else 0.0
ipc_var.append({"ipc": i + 1, "singular_value": round(float(S[i]), 6),
"ss": round(float(S[i] ** 2), 6), "proportion": round(prop, 4)})
n_keep = min(n_components, k)
g_ipc = U[:, :n_keep] * S[:n_keep] # 基因型 IPC 得分(sqrt(λ)·U
ss_ipc1 = float((g_ipc[:, 0] ** 2).sum()) if n_keep >= 1 else 0.0
ss_ipc2 = float((g_ipc[:, 1] ** 2).sum()) if n_keep >= 2 else 0.0
rows: list[dict] = []
for gi in range(g):
ipc1 = float(g_ipc[gi, 0]) if n_keep >= 1 else 0.0
ipc2 = float(g_ipc[gi, 1]) if n_keep >= 2 else 0.0
# ASVPurchase 2000):IPC1 按 SS 比例缩放到 IPC2 尺度,与 IPC2 正交合成
if n_keep >= 2 and ss_ipc1 > 0:
asv = math.sqrt((ipc1 * ss_ipc2 / ss_ipc1) ** 2 + ipc2 ** 2)
else:
asv = abs(ipc1)
ecoval = float(np.nansum((Y[gi] - row_mean[gi] - col_mean + grand) ** 2))
rows.append({
"genotype": genotypes[gi],
"ipc1": round(ipc1, 4),
"ipc2": round(ipc2, 4) if n_keep >= 2 else None,
"asv": round(asv, 4),
"ecovalence": round(ecoval, 6),
})
rows.sort(key=lambda r: r["asv"])
for rank, r in enumerate(rows, 1):
r["rank"] = rank
warning = None
if has_missing:
warning = "两因素表存在缺格,SVD 前缺格按 0 填充,IPC/ASV 估计仅供参考"
return {
"method": "ammi", "genotypes": genotypes, "environments": envs,
"rows": rows, "ipc_variance": ipc_var, "n": g * e,
"ss_interaction": round(ss_total, 6), "warning": warning,
}
def stability_analysis(table: dict, *, methods: tuple[str, ...] = ("ammi", "finlay"),
n_components: int = 2) -> dict:
"""稳定性综合分析:AMMI + Finlay-Wilkinson 可选。"""
out: dict = {"genotypes": sorted(table), "methods": list(methods)}
if "finlay" in methods:
out["finlay"] = finlay_wilkinson(table)
if "ammi" in methods:
out["ammi"] = ammi(table, n_components=n_components)
return out