init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
+720
View File
@@ -0,0 +1,720 @@
"""基因组选择:VanRaden G 矩阵 + GBLUP + ssGBLUP(仅 numpy,无 R)。
- build_g_matrixdosage(0/1/2) 样本×标记矩阵 → VanRaden Gmethod1=ZZ'/2Σp(1-p)、
method2=ZDZ'D=1/[2p(1-p)]),缺格按列均值(2p)填充;MAF 过滤 + blend 岭回归保证可逆。
- solve_gblup:仅基因型个体入模型,MME 用 G⁻¹ 替代 A⁻¹;方差组分用剖面 REML
(黄金分割最大化精确 REML 对数似然,与 blup._solve_gxe 同模板)。
- solve_ssgblup:单步法 H⁻¹ = A⁻¹ + [[0,0],[0,G⁻¹ A22⁻¹]]Aguilar et al. 2010
A22 = 基因型个体在 A 中的子阵),基因型+非基因型个体都出 EBV。
可靠性 = 1 PEV/σ²aPEV = C22[ii]·σ²e(C 为 MME 系数矩阵逆的加性块)。
"""
from __future__ import annotations
import math
import random
import numpy as np
from scripts.breeding_stats import blup
ENGINE_VERSION = "1.4.0" # 1.4.0: 非线性基因组预测 rrBLUP + BayesB; 1.3.0: kfold split=family; 1.2.0: SSR 多等位 + EMMAX GRM 复用
_RIDGE = 1e-6 # A22⁻¹ 病态加小岭
_GBLUP_KEYS = ["ebv", "reliability", "h2", "sigma_a", "sigma_e",
"n_obs", "n_individuals", "n_markers", "converged", "warning"]
def build_g_matrix(genotypes: dict[str, dict[str, float]],
markers: list[str], *, maf_min: float = 0.05,
method: int = 2, blend: float = 0.02):
"""VanRaden G 矩阵。
genotypes: {sample: {marker: dosage(0/1/2)}}markers: 有序标记列表。
返回 (G 按 sorted(genotypes) 排序, meta{G 统计量})。
"""
samples = sorted(genotypes)
n = len(samples)
if n < 2 or not markers:
raise ValueError("基因型样本或标记数不足(需 n≥2、m≥1)")
m = len(markers)
M = np.full((n, m), np.nan)
n_missing = 0
for i, s in enumerate(samples):
g = genotypes[s]
for j, mk in enumerate(markers):
v = g.get(mk)
if v is None or (isinstance(v, float) and math.isnan(v)):
n_missing += 1
else:
M[i, j] = float(v)
p = np.zeros(m)
for j in range(m):
col = M[:, j]
valid = col[~np.isnan(col)]
if len(valid) == 0:
p[j] = 0.5
else:
p[j] = float(np.mean(valid)) / 2.0
M[:, j] = np.where(np.isnan(M[:, j]), 2.0 * p[j], M[:, j])
maf = np.minimum(p, 1.0 - p)
keep = maf >= maf_min
if int(keep.sum()) == 0:
raise ValueError(f"MAF≥{maf_min} 的标记为 0,请降低 maf_min 或检查标记等位频率")
M = M[:, keep]
p = p[keep]
m_keep = int(keep.sum())
Z = M - 2.0 * p[None, :]
if method == 1:
denom = 2.0 * float(np.sum(p * (1.0 - p)))
G = (Z @ Z.T) / denom if denom > 0 else np.eye(n)
else:
D = 1.0 / (2.0 * p * (1.0 - p))
G = Z @ np.diag(D) @ Z.T
if blend and blend > 0:
diag_mean = float(np.mean(np.diag(G)))
G = (1.0 - blend) * G + blend * diag_mean * np.eye(n)
off = G[np.triu_indices(n, 1)] if n > 1 else np.array([0.0])
return G, {
"n_individuals": n,
"n_markers": m,
"m_after_maf": m_keep,
"n_missing": n_missing,
"maf_min": maf_min,
"method": method,
"blend": blend,
"diag_mean": round(float(np.mean(np.diag(G))), 6),
"diag_range": [round(float(np.min(np.diag(G))), 6), round(float(np.max(np.diag(G))), 6)],
"offdiag_range": [round(float(np.min(off)), 6), round(float(np.max(off)), 6)],
}
def _scale_g_to_a22(G: np.ndarray, A22: np.ndarray) -> np.ndarray:
"""把基因关系阵 G 缩放到系谱关系阵 A22 的尺度(Christensen et al. 调整)。
VanRaden method=2 的 G 对角线≈标记数 m(远大于 A22 的 ≈1),若直接组 H,
基因型块会被 G 主导、h²/EBV 尺度偏置。用两参数对齐 G*=a·G+b 使
对角均值/非对角均值与 A22 一致;G 退化(对角≈非对角)时跳过并原样返回。
"""
n = len(A22)
if n < 2:
return G
diag_g = np.mean(np.diag(G))
diag_a = np.mean(np.diag(A22))
off_g = float(np.mean(G[np.triu_indices(n, 1)]))
off_a = float(np.mean(A22[np.triu_indices(n, 1)]))
denom = diag_g - off_g
if abs(denom) < 1e-12:
return G
a = (diag_a - off_a) / denom
b = off_a - a * off_g
return a * G + b
def _reml_ll(ratio: float, ZGZ: np.ndarray, X: np.ndarray, y: np.ndarray) -> float:
"""V = ZGZ·ratio + IVe=1 尺度),精确 REML 对数似然(slogdet + y'Py)。"""
m = len(y)
V = ZGZ * ratio + np.eye(m)
try:
VinvX = np.linalg.solve(V, X)
Vinvy = np.linalg.solve(V, y)
XtVinvX = X.T @ VinvX
XtVinvY = X.T @ Vinvy
yPy = float(y @ Vinvy) - float(XtVinvY @ np.linalg.solve(XtVinvX, XtVinvY))
_, lv = np.linalg.slogdet(V)
_, lx = np.linalg.slogdet(XtVinvX)
return -0.5 * (float(lv) + float(lx) + yPy)
except np.linalg.LinAlgError:
return -np.inf
def _profile_ve(V: np.ndarray, X: np.ndarray, y: np.ndarray, df: int) -> float:
"""REML 剖面 σ²e = y'Py/dfV 固定尺度下)。"""
VinvX = np.linalg.solve(V, X)
Vinvy = np.linalg.solve(V, y)
XtVinvX = X.T @ VinvX
XtVinvY = X.T @ Vinvy
yPy = float(y @ Vinvy) - float(XtVinvY @ np.linalg.solve(XtVinvX, XtVinvY))
return yPy / df if df > 0 else float("nan")
def _profile_solve(ZGZ: np.ndarray, X: np.ndarray, y: np.ndarray,
Ginv: np.ndarray, n_all: int, z: np.ndarray):
"""共享剖面 REML + MME 求解。返回 (ebv, rel, h2, sigma_a, sigma_e, n_iter)。"""
m = len(y)
df = m - X.shape[1]
# 剖面参数为 log10(σ²a/σ²e)。h²=ratio/(1+ratio),故 h²∈[H2_MIN,H2_MAX]
# 对应 ratio∈[H2_MIN/(1H2_MIN), H2_MAX/(1H2_MAX)](直接用 log10(H2_MAX)≈−0.0004
# 会把 σ²a/σ²e 封顶 0.999,令 h² 上限只有 0.5)。
lo, hi = math.log10(blup.H2_MIN / (1.0 - blup.H2_MIN)), math.log10(blup.H2_MAX / (1.0 - blup.H2_MAX))
def _ll(r: float) -> float:
return _reml_ll(10.0 ** r, ZGZ, X, y)
r_opt, n_iter = blup._golden_max(_ll, lo, hi, tol=1e-4, max_iter=blup.MAX_PROFILE_EVALS)
ratio = 10.0 ** r_opt
V = ZGZ * ratio + np.eye(m)
ve = _profile_ve(V, X, y, df)
va = ratio * ve if not math.isnan(ve) else 1.0
lam = ve / va if va > 0 else 1.0
Zmat = np.zeros((n_all, m))
Zmat[z, np.arange(m)] = 1.0
C = np.block([
[X.T @ X, X.T @ Zmat.T],
[Zmat @ X, Zmat @ Zmat.T + lam * Ginv],
])
rhs = np.concatenate([X.T @ y, Zmat @ y])
try:
sol = np.linalg.solve(C, rhs)
except np.linalg.LinAlgError:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None,
"sigma_e": None, "n_iter": n_iter, "converged": False,
"warning": "MME 系数矩阵奇异(基因型共线/样本不足),求解失败"}
C_inv = np.linalg.inv(C)
C22inv = C_inv[1:, 1:]
rel = np.clip(1.0 - np.diag(C22inv) * ve / va, 0.0, 1.0) if va > 0 else np.zeros(n_all)
h2 = va / (va + ve) if (va + ve) > 0 else None
return {"ebv": sol[1:], "reliability": rel, "h2": h2, "sigma_a": float(va),
"sigma_e": float(ve) if not math.isnan(ve) else None,
"n_iter": n_iter, "converged": True, "warning": None}
def solve_gblup(individuals: list[str], phenos: dict[str, float], G: np.ndarray,
*, tol: float = blup.TOL):
"""GBLUP:仅基因型个体。individuals 为 G 行序(个体 id);phenos 子集有观测。"""
n = len(individuals)
obs = [i for i in individuals if i in phenos]
m = len(obs)
if m < 3:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": m, "n_individuals": n, "n_markers": None,
"converged": False, "warning": "表型观测不足 3,无法估计方差组分"}
idx = {i: k for k, i in enumerate(individuals)}
z = np.array([idx[i] for i in obs])
y = np.array([float(phenos[i]) for i in obs])
X = np.ones((m, 1))
ZGZ = G[np.ix_(z, z)]
try:
Ginv = np.linalg.inv(G)
except np.linalg.LinAlgError:
Ginv = np.linalg.pinv(G)
res = _profile_solve(ZGZ, X, y, Ginv, n, z)
out = dict(res)
ebv = {individuals[i]: float(out["ebv"][i]) for i in range(n)}
rel = {individuals[i]: float(out["reliability"][i]) for i in range(n)}
out.update(ebv=ebv, reliability=rel, n_obs=m, n_individuals=n,
n_markers=None if not isinstance(G, np.ndarray) else G.shape[0])
if out["warning"] is None and out.get("h2") is not None and (out["h2"] >= blup.H2_MAX or out["h2"] <= blup.H2_MIN):
out["warning"] = "遗传力估计逼近边界,剖面 REML 未完全收敛(样本小/信噪比低)"
return out
def _build_h(pedigree: list[dict], genotyped: list[str], G: np.ndarray,
*, ridge: float = _RIDGE, extra_base: list[str] | None = None):
"""ssGBLUP 合并关系阵(H 协方差 + H⁻¹ 仅进 MMEAguilar et al. 2010)。
返回 (order, idx, H, Hinv)。系谱中缺失的基因型个体按 base 补入(与 phenos 兜底一致)。
"""
base: list[int] = []
non_base: list[tuple[int, int | None, int | None]] = []
seen: set[int] = set()
for rec in pedigree:
i = rec["individual"]
if i in seen:
continue
seen.add(i)
d, s = rec.get("dam"), rec.get("sire")
if d is None and s is None:
base.append(i)
else:
non_base.append((i, d, s))
if extra_base:
for g in extra_base:
if g not in seen:
seen.add(g)
base.append(g)
for g in genotyped: # 无表型/无系谱基因型树也按 base 补入 H(不再 raise
if g not in seen:
seen.add(g)
base.append(g)
if not seen:
raise ValueError("系谱与基因型均为空")
order, idx = blup._order_pedigree(base, non_base)
n = len(order)
parent_of = {i: (d, s) for (i, d, s) in non_base}
A, Ainv = blup._build_ainv(order, len(base), parent_of)
gen_indices = [idx[g] for g in genotyped]
if len(gen_indices) < 2:
raise ValueError("基因型个体不足 2,无法 ssGBLUP")
A22 = A[np.ix_(gen_indices, gen_indices)]
G = _scale_g_to_a22(G, A22)
try:
A22inv = np.linalg.inv(A22 + ridge * np.eye(len(gen_indices)))
Ginv = np.linalg.inv(G)
except np.linalg.LinAlgError:
A22inv = np.linalg.pinv(A22)
Ginv = np.linalg.pinv(G)
Hinv = Ainv.copy()
Hinv[np.ix_(gen_indices, gen_indices)] += Ginv - A22inv
# H 合并关系阵(协方差;H⁻¹=A⁻¹+[[0,0],[0,G⁻¹−A22⁻¹]]Aguilar et al. 2010
# H = A + [[A12·A22⁻¹·D·A22⁻¹·A21, A12·A22⁻¹·D], [D·A22⁻¹·A21, D]]D=GA22
gen_set = set(gen_indices)
non_gen = [i for i in range(n) if i not in gen_set]
if non_gen:
D = G - A22
L = A[np.ix_(non_gen, gen_indices)] @ A22inv
LD = L @ D
H = A.copy()
H[np.ix_(non_gen, non_gen)] += LD @ L.T
H[np.ix_(non_gen, gen_indices)] += LD
H[np.ix_(gen_indices, non_gen)] += LD.T
H[np.ix_(gen_indices, gen_indices)] = G
else:
H = G
return order, idx, H, Hinv
def solve_ssgblup(pedigree: list[dict], phenos: dict[str, float],
G: np.ndarray, genotyped: list[str], *, tol: float = blup.TOL,
ridge: float = _RIDGE):
"""ssGBLUP:单步法。genotyped 为有序列表(与 G 行对应)。全系谱个体出 EBV。"""
if not pedigree and not phenos:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": 0, "n_individuals": 0, "n_markers": None,
"converged": False, "warning": "系谱与表型均为空"}
try:
order, idx, H, Hinv = _build_h(
pedigree, genotyped, G, ridge=ridge, extra_base=list(phenos))
except ValueError as e:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": 0, "n_individuals": len({r["individual"] for r in pedigree}),
"n_markers": None, "converged": False, "warning": str(e)}
n = len(order)
obs = [i for i in order if i in phenos]
m = len(obs)
if m < 3:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": m, "n_individuals": n, "n_markers": None,
"converged": False, "warning": "表型观测不足 3,无法估计方差组分"}
z = np.array([idx[i] for i in obs])
y = np.array([float(phenos[i]) for i in obs])
X = np.ones((m, 1))
ZGZ = H[np.ix_(z, z)] # V 用 H 协方差块(H⁻¹ 仅进 MME 系数矩阵)
res = _profile_solve(ZGZ, X, y, Hinv, n, z)
out = dict(res)
ebv = {order[i]: float(out["ebv"][i]) for i in range(n)}
rel = {order[i]: float(out["reliability"][i]) for i in range(n)}
out.update(ebv=ebv, reliability=rel, n_obs=m, n_individuals=n, n_markers=len(genotyped))
if out["warning"] is None and out.get("h2") is not None and (out["h2"] >= blup.H2_MAX or out["h2"] <= blup.H2_MIN):
out["warning"] = "遗传力估计逼近边界,剖面 REML 未完全收敛"
return out
def _build_dosage_matrix(genotypes: dict[str, dict[str, float]],
markers: list[str], *, maf_min: float = 0.05):
"""dosage dict → 有序样本×标记剂量矩阵(与 build_g_matrix 同 MAF 过滤口径)。
返回 (X[n×q], 保留标记列表, 等位频率 p[q])。X 已按列均值(≈2p)填充缺失。
rrBLUP/BayesB 直接消费 XXc = X 2p 即 GBLUP 的 Z)。
"""
samples = sorted(genotypes)
n = len(samples)
m = len(markers)
M = np.full((n, m), np.nan)
for i, s in enumerate(samples):
g = genotypes[s]
for j, mk in enumerate(markers):
v = g.get(mk)
if v is not None and not (isinstance(v, float) and math.isnan(v)):
M[i, j] = float(v)
p = np.zeros(m)
for j in range(m):
col = M[:, j]
valid = col[~np.isnan(col)]
p[j] = 0.5 if len(valid) == 0 else float(np.mean(valid)) / 2.0
M[:, j] = np.where(np.isnan(M[:, j]), 2.0 * p[j], M[:, j])
keep = np.minimum(p, 1.0 - p) >= maf_min
if int(keep.sum()) == 0:
raise ValueError(f"MAF≥{maf_min} 的标记为 0,请降低 maf_min 或检查标记等位频率")
return M[:, keep], [mk for mk, k in zip(markers, keep) if k], p[keep]
def solve_rrblup(individuals: list[str], phenos: dict[str, float],
X: np.ndarray, marker_order: list[str], p: np.ndarray,
*, tol: float = blup.TOL):
"""RR-BLUP(岭回归逐标记):y = μ + Xu + eu=(Xc'Xc+λI)⁻¹Xc'yc。
与 GBLUP 数学对偶(G = XcXc'/c 时 EBV 一致):方差组分仍用剖面 REML
_profile_solve 同一求解器)估 σa²/σe² → σu²=σa²/cc=2Σp(1-p))→
λ=σe²/σu²。输出 EBV(基因型个体,含表型截距)+ marker effectsGBLUP
无的解释能力)+ 与 GBLUP 同构的可靠性(PEV 近似)。
"""
n = len(individuals)
obs = [i for i in individuals if i in phenos]
m = len(obs)
if m < 3:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": m, "n_individuals": n, "n_markers": None, "marker_effects": {},
"converged": False, "warning": "表型观测不足 3,无法估计方差组分"}
idx = {i: k for k, i in enumerate(individuals)}
z = np.array([idx[i] for i in obs])
y = np.array([float(phenos[i]) for i in obs])
Xc = X - 2.0 * p[None, :] # 中心化剂量 = GBLUP 的 Z
q = Xc.shape[1]
c = 2.0 * float(np.sum(p * (1.0 - p)))
if c <= 0:
c = 1.0
G = Xc @ Xc.T / c
try:
Ginv = np.linalg.inv(G)
except np.linalg.LinAlgError:
Ginv = np.linalg.pinv(G)
res = _profile_solve(G[np.ix_(z, z)], np.ones((m, 1)), y, Ginv, n, z)
va, ve = res.get("sigma_a"), res.get("sigma_e")
if not res.get("converged") or va is None or ve is None or va <= 0 or ve <= 0:
return {"ebv": {}, "reliability": {}, "h2": res.get("h2"), "sigma_a": va,
"sigma_e": ve, "n_obs": m, "n_individuals": n, "n_markers": q,
"marker_effects": {},
"converged": False, "warning": res.get("warning") or "方差组分估计失败,rrBLUP 未收敛"}
lam = ve / (va / c) if va > 0 else 1.0 # λ = σe²/σu² = σe²·c/σa²
yc = y - float(y.mean())
Xo = Xc[z]
try:
u = np.linalg.solve(Xo.T @ Xo + lam * np.eye(q), Xo.T @ yc)
except np.linalg.LinAlgError:
u = np.linalg.pinv(Xo.T @ Xo + lam * np.eye(q)) @ (Xo.T @ yc)
mu = float(y.mean())
ebv = {individuals[i]: float(Xc[i] @ u + mu) for i in range(n)}
rel = res["reliability"]
out = {"ebv": ebv, "reliability": {individuals[i]: float(rel[i]) for i in range(n)},
"h2": res["h2"], "sigma_a": float(va), "sigma_e": float(ve),
"n_obs": m, "n_individuals": n, "n_markers": q,
"marker_effects": {marker_order[j]: float(u[j]) for j in range(q)},
"converged": True, "warning": res.get("warning")}
return out
def _bayesb_gibbs(Xo: np.ndarray, yc: np.ndarray, *, n_iter: int, burn_in: int,
seed: int, pi: float, df_s: float, scale0: float,
df_e: float, scale_e0: float) -> tuple[np.ndarray, float, float, int]:
"""BayesB 逐标记 Gibbs 核心:给定中心化剂量观测子阵 Xo 与中心化表型 yc。
返回 (u_hat 后验均值, σe² 末段, SSE 末段, 收敛样本数 n_save)。
numpy 固定 seeddefault_rng)保证同数据同参数逐位可复现。
"""
q = Xo.shape[1]
m = len(yc)
rng = np.random.default_rng(seed)
u = np.zeros(q)
e = yc.astype(float).copy()
sigma_e2 = float(np.var(yc))
if sigma_e2 <= 0:
sigma_e2 = 1.0
u_acc = np.zeros(q)
n_save = 0
last_sse = float("inf")
for it in range(n_iter):
for j in range(q):
e_star = e + Xo[:, j] * u[j]
xjxj = float(Xo[:, j] @ Xo[:, j])
xjy = float(Xo[:, j] @ e_star)
if rng.uniform() > pi: # 1−π 概率标记入模型
s2 = (df_s * scale0 + u[j] * u[j]) / (rng.chisquare(df_s + 1))
if s2 <= 0 or xjxj <= 0:
u[j] = 0.0
else:
denom = xjxj + sigma_e2 / s2
u[j] = rng.normal(xjy / denom, math.sqrt(sigma_e2 / denom))
else:
u[j] = 0.0
e = e_star - Xo[:, j] * u[j]
sse = float(e @ e)
sigma_e2 = (df_e * scale_e0 + sse) / rng.chisquare(df_e + m)
if sigma_e2 <= 0:
sigma_e2 = 1e-8
if it >= burn_in:
u_acc += u
n_save += 1
last_sse = sse
if n_save == 0:
raise ValueError("后验样本为空(burn_in 覆盖全部迭代)")
return u_acc / n_save, sigma_e2, last_sse, n_save
def solve_bayesb(individuals: list[str], phenos: dict[str, float],
X: np.ndarray, marker_order: list[str], p: np.ndarray,
*, n_iter: int = 2000, burn_in: int = 500, seed: int = 20260805,
pi: float = 0.95, df_s: float = 5.0, scale0: float = 0.001,
df_e: float = 5.0, scale_e0: float = 1.0,
tol: float = blup.TOL):
"""BayesBMeuwissen et al. 2001):贝叶斯可变选择,逐标记 Gibbs 抽样。
模型 y = μ + Σ Xc_j·u_j + e;每标记 δ_j ~ Bernoulli(1−π) 概率入模型,
u_j | δ=1 ~ N(0, σ²j)、σ²j ~ inv-χ²(df_s, scale);σe² ~ inv-χ²(df_e, scale_e0)。
固定 seednumpy default_rng)保证可复现(MLOps 铁律:同数据同参数重跑逐位一致)。
输出 EBV(后验均值,基因型个体,含表型截距)+ marker effects + 收敛诊断。
可靠性未单独估计(后验方差才是正确不确定性度量),按 0 保守处理(note 说明)。
"""
n = len(individuals)
obs = [i for i in individuals if i in phenos]
m = len(obs)
if m < 3:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": m, "n_individuals": n, "n_markers": None, "marker_effects": {},
"converged": False, "warning": "表型观测不足 3,无法估计方差组分"}
if n_iter <= burn_in:
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": m, "n_individuals": n, "n_markers": None, "marker_effects": {},
"converged": False, "warning": "n_iter 必须大于 burn_in"}
idx = {i: k for k, i in enumerate(individuals)}
z = np.array([idx[i] for i in obs])
y = np.array([float(phenos[i]) for i in obs])
Xc = X - 2.0 * p[None, :]
q = Xc.shape[1]
yc = y - float(y.mean())
Xo = Xc[z]
try:
u_hat, sigma_e2, sse, n_save = _bayesb_gibbs(
Xo, yc, n_iter=n_iter, burn_in=burn_in, seed=seed, pi=pi,
df_s=df_s, scale0=scale0, df_e=df_e, scale_e0=scale_e0)
except Exception as exc: # noqa: BLE001
return {"ebv": {}, "reliability": {}, "h2": None, "sigma_a": None, "sigma_e": None,
"n_obs": m, "n_individuals": n, "n_markers": q, "marker_effects": {},
"converged": False, "warning": f"BayesB 抽样异常: {exc!s}"}
mu = float(yc.mean())
ebv = {individuals[i]: float(Xc[i] @ u_hat + mu) for i in range(n)}
note = (f"BayesB π={pi}·{n_iter}iter·{burn_in}burn-in·seed={seed}(后验均值,"
f"收敛样本 {n_save};σe² 末段 SSE={sse:.3f}")
return {"ebv": ebv,
"reliability": {individuals[i]: 0.0 for i in range(n)},
"h2": None, "sigma_a": None, "sigma_e": float(sigma_e2),
"n_obs": m, "n_individuals": n, "n_markers": q,
"marker_effects": {marker_order[j]: float(u_hat[j]) for j in range(q)},
"converged": True, "warning": note}
def kfold_cv_genomic(phenos: dict[str, float], G: np.ndarray, genotyped: list[str],
*, k: int = 5, seed: int = 20260804, method: str = "gblup",
pedigree: list[dict] | None = None,
ridge: float = _RIDGE,
split: str = "random",
family_of: dict[str, str] | None = None,
genotypes: dict[str, dict[str, float]] | None = None,
markers: list[str] | None = None,
n_iter: int = 1000, burn_in: int = 300, pi: float = 0.95,
df_s: float = 5.0, scale0: float = 0.001,
df_e: float = 5.0, scale_e0: float = 1.0) -> dict:
"""GS k 折交叉验证(实证预测准确度:留出 GEBV vs 表型 pearson/RMSE)。
与 blup.kfold_cv 区别:关系矩阵为 VanRaden GGBLUP)或单步 HssGBLUP),
折划分默认按**基因型个体随机分层**(固定种子;GS 同世代样本无系谱家系树,
随机分层即标准做法,区别于 ABLUP 的 sire 家系留出)。若样本确有家系结构
(同父半同胞),随机分层会把同家系亲缘个体摊入 train/test → 泄漏亲缘、
高估准确度,此时应传 `split="family"` + `family_of` 走**家系阻塞折**
(同家系个体同折,照 blup.kfold_cv 的 sire 家系留出模式);无家系个体自成一族。
家系数 < 折数 k 时折叠为 n_families 折并附 warning。
method 分派:
- `gblup`VanRaden G(折内 _profile_solve,掩蔽留出经 G⁻¹ 交叉关系预测)。
- `ssgblup`:单步 H(需 pedigree)。
- `rrblup`:岭回归逐标记(需 genotypes+markers 剂量矩阵;折内 REML 定 λ,
EBV=Xc·u+μ 直接由标记基因型预测,不经关系矩阵逆)。
- `bayesb`:贝叶斯可变选择(需 genotypes+markers;折内 Gibbs,固定 seed 可复现)。
rrblup/bayesb 缺 genotypes/markers 时返回含 warning 的空结果。
掩蔽留出:个体保留在 G/H 中,仅掩蔽其表型(_profile_solve 的 MME 非观测个体
经 G⁻¹/H⁻¹ 交叉关系预测)。G/H/Hinv 只建一次,逐折只换 z_train 子阵。
剂量矩阵(Xc)同样只建一次,逐折只取观测行。
输出 dict 与 blup.kfold_cv 逐键对齐 + `n_families`。cand=genotyped∩phenos(可验证个体)。
"""
method = (method or "gblup").lower()
if method == "ssgblup":
order, idx, H, Hinv = _build_h(
pedigree or [], genotyped, G, ridge=ridge, extra_base=list(phenos))
individuals = order
zmat_full = H
relmat_inv = Hinv
elif method in ("rrblup", "bayesb"):
if genotypes is None or markers is None:
return {
"k": k, "n_total": 0, "n_individuals": 0,
"folds": [], "mean_pearson": None, "mean_rmse": None,
"pooled_pearson": None, "pooled_rmse": None, "cv_accuracy": None,
"h2": None, "n_families": None,
"warning": f"{method} 需要 genotypes+markers 剂量矩阵,无法交叉验证",
}
X_full, marker_order, p_full = _build_dosage_matrix(genotypes, markers)
Xc_full = X_full - 2.0 * p_full[None, :]
marker_c = 2.0 * float(np.sum(p_full * (1.0 - p_full)))
if marker_c <= 0:
marker_c = 1.0
individuals = sorted(genotypes)
idx = {i: k for k, i in enumerate(individuals)}
# _profile_solve 需全 n×n GinvMME 随机效应块为 Zmat@Zmat.T + λ·Ginv_full),
# 折内只取 zmat_full[np.ix_(z,z)] 观测子阵(= XoXo'/c),与 GBLUP 分支同构。
zmat_full = Xc_full @ Xc_full.T / marker_c
try:
relmat_inv = np.linalg.inv(zmat_full)
except np.linalg.LinAlgError:
relmat_inv = np.linalg.pinv(zmat_full)
else:
individuals = list(genotyped)
idx = {i: k for k, i in enumerate(individuals)}
zmat_full = G
try:
relmat_inv = np.linalg.inv(G)
except np.linalg.LinAlgError:
relmat_inv = np.linalg.pinv(G)
n = len(individuals)
cand = sorted([i for i in genotyped if i in phenos])
n_total = len(cand)
if n_total < 2:
return {
"k": k, "n_total": n_total, "n_individuals": n,
"folds": [], "mean_pearson": None, "mean_rmse": None,
"pooled_pearson": None, "pooled_rmse": None, "cv_accuracy": None,
"h2": None, "warning": "可验证个体(有基因型+表型)不足 2,无法交叉验证",
}
k = max(2, min(int(k or 5), 10))
rng = random.Random(seed)
n_families: int | None = None
fam_warning: str | None = None
split = (split or "random").lower()
if split == "family":
fam_of = {ind: (family_of or {}).get(ind, ("self", ind)) for ind in cand}
fams = sorted({f for f in fam_of.values()})
n_families = len(fams)
if n_families < 2:
return {
"k": k, "n_total": n_total, "n_individuals": n,
"folds": [], "mean_pearson": None, "mean_rmse": None,
"pooled_pearson": None, "pooled_rmse": None, "cv_accuracy": None,
"h2": None, "n_families": n_families,
"warning": "可验证个体仅构成 1 个家系,无法家系阻塞交叉验证(请改用随机分层)",
}
if n_families < k:
k = n_families
fam_warning = f"家系数 {n_families} < 请求折数,折叠为 {k} 折(family-blocked"
rng.shuffle(fams)
fam_bin = {f: i % k for i, f in enumerate(fams)}
bin_of = {c: fam_bin[fam_of[c]] for c in cand}
else:
order_shuf = list(cand)
rng.shuffle(order_shuf)
bin_of = {c: i % k for i, c in enumerate(order_shuf)}
folds: list[dict] = []
all_x: list[float] = []
all_y: list[float] = []
for fold in range(k):
test_set = {c for c, b in bin_of.items() if b == fold}
train_phenos = {i: v for i, v in phenos.items() if i not in test_set}
train_obs = [i for i in individuals if i in train_phenos]
if not test_set or len(train_obs) < 2:
folds.append({
"fold": fold + 1, "n_train": len(train_obs), "n_test": len(test_set),
"n_eval": 0, "pearson": None, "rmse": None,
"error": "训练集个体不足 2 或测试集为空",
})
continue
z = np.array([idx[i] for i in train_obs])
y = np.array([float(train_phenos[i]) for i in train_obs])
ebv: dict[str, float] | None = None
fold_h2: float | None = None
res: dict | None = None
try:
if method in ("rrblup", "bayesb"):
Xo = Xc_full[z]
yc = y - float(y.mean())
if method == "rrblup":
res = _profile_solve(zmat_full[np.ix_(z, z)], np.ones((len(train_obs), 1)),
y, relmat_inv, n, z)
va, ve = res.get("sigma_a"), res.get("sigma_e")
if (res.get("converged") and va is not None and ve is not None
and va > 0 and ve > 0):
lam = ve / (va / marker_c)
q = Xc_full.shape[1]
try:
u = np.linalg.solve(Xo.T @ Xo + lam * np.eye(q), Xo.T @ yc)
except np.linalg.LinAlgError:
u = np.linalg.pinv(Xo.T @ Xo + lam * np.eye(q)) @ (Xo.T @ yc)
mu = float(y.mean())
ebv = np.array([float(Xc_full[i] @ u + mu) for i in range(n)])
fold_h2 = res.get("h2")
else:
u_hat, _sigma_e2, _sse, _n_save = _bayesb_gibbs(
Xo, yc, n_iter=n_iter, burn_in=burn_in, seed=seed, pi=pi,
df_s=df_s, scale0=scale0, df_e=df_e, scale_e0=scale_e0)
mu = float(y.mean())
ebv = np.array([float(Xc_full[i] @ u_hat + mu) for i in range(n)])
res = {"h2": None, "converged": True}
else:
X = np.ones((len(train_obs), 1))
res = _profile_solve(zmat_full[np.ix_(z, z)], X, y, relmat_inv, n, z)
if res.get("converged"):
ebv = res["ebv"]
fold_h2 = res.get("h2")
except Exception as e: # noqa: BLE001
folds.append({
"fold": fold + 1, "n_train": len(train_obs), "n_test": len(test_set),
"n_eval": 0, "pearson": None, "rmse": None,
"error": f"求解失败: {e!s}",
})
continue
if not res or not res.get("converged") or ebv is None:
folds.append({
"fold": fold + 1, "n_train": len(train_obs), "n_test": len(test_set),
"n_eval": 0, "pearson": None, "rmse": None,
"error": (res or {}).get("warning") or "求解失败",
})
continue
xs = [float(ebv[idx[c]]) for c in sorted(test_set)]
ys = [float(phenos[c]) for c in sorted(test_set)]
if len(xs) < 2:
folds.append({
"fold": fold + 1, "n_train": len(train_obs), "n_test": len(test_set),
"n_eval": len(xs), "pearson": None, "rmse": None,
"error": "测试集有效个体不足 2,无法计算 pearson",
})
continue
pearson = blup._pearson(list(xs), list(ys))
rmse = math.sqrt(sum((a - b) ** 2 for a, b in zip(xs, ys)) / len(xs))
all_x.extend(xs)
all_y.extend(ys)
folds.append({
"fold": fold + 1, "n_train": len(train_obs), "n_test": len(test_set),
"n_eval": len(xs), "pearson": round(pearson, 4),
"rmse": round(rmse, 6),
"h2": float(fold_h2) if fold_h2 is not None else None,
})
valid = [f for f in folds if f.get("pearson") is not None]
mean_pearson = round(sum(f["pearson"] for f in valid) / len(valid), 4) if valid else None
rmse_vals = [f["rmse"] for f in valid if f.get("rmse") is not None]
mean_rmse = round(sum(rmse_vals) / len(rmse_vals), 6) if rmse_vals else None
pooled_pearson = round(blup._pearson(all_x, all_y), 4) if len(all_x) >= 2 else None
pooled_rmse = (round(math.sqrt(sum((a - b) ** 2 for a, b in zip(all_x, all_y)) / len(all_x)), 6)
if len(all_x) else None)
h2_vals = [f["h2"] for f in valid if f.get("h2") is not None]
h2_mean = round(sum(h2_vals) / len(h2_vals), 4) if h2_vals else None
errors = [f.get("error") for f in folds if f.get("error")]
warning_parts = []
if fam_warning:
warning_parts.append(fam_warning)
if errors:
warning_parts.append(f"{len(errors)} 折失败: " + "".join(errors))
warning = "".join(warning_parts) if warning_parts else None
return {
"k": k, "n_total": n_total, "n_individuals": n,
"folds": folds,
"mean_pearson": mean_pearson, "mean_rmse": mean_rmse,
"pooled_pearson": pooled_pearson, "pooled_rmse": pooled_rmse,
"cv_accuracy": mean_pearson, "h2": h2_mean,
"n_families": n_families, "warning": warning,
}