init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""稳定性分析: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 得分、
|
||||
ASV(Purchase 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
|
||||
# ASV(Purchase 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
|
||||
Reference in New Issue
Block a user