1528 lines
60 KiB
Python
1528 lines
60 KiB
Python
"""纯 Python 单性状动物模型 BLUP(Henderson MME + 剖面 REML)。
|
||
|
||
仅依赖 numpy,无 R / 外部运行时。
|
||
|
||
系谱锚点(本系统约定):
|
||
- 个体 = bre_tree 单株(子代),其 dam/sire = bre_germplasm 种质(base/founder,无父母)。
|
||
- 同组合单株互为全同胞(A=0.5),跨组合共享单一亲本为半同胞(A=0.25),
|
||
单株↔亲本 A=0.5 —— 全部由 A 矩阵递归自动得出,无需人工指定。
|
||
- 无父母的个体一律视为 founder(base population,彼此不相关)。
|
||
|
||
算法:
|
||
1. A 矩阵(稠密,O(n²) 递归)→ 各体自交系数 F;
|
||
2. A⁻¹ 用 Henderson 稀疏规则构造(无需求 A 的逆);
|
||
3. MME 求解:y = Xb + Zu + e,X=截距+固定效应虚拟列+协变量(默认仅截距),λ = σ²e/σ²a;
|
||
默认稠密精确求解;个体数 > N_SPARSE(默认 1000)且系谱存在时切稀疏 A⁻¹ + 共轭梯度
|
||
迭代(solve 的 solver="sparse-cg"):EBV/h²/σ² 为 CG 精确解,reliability 用
|
||
Hutchinson 随机探测估计 C22 对角线(reliability_approx=True,均值/PA 可靠、个体级近似);
|
||
4. 剖面 REML:对每个候选 h² 固定 λ,用 MME 解的 EM 条件式估计 σ²e、σ²a,
|
||
再用精确 REML 对数似然评分,黄金分割一维最大化(稳健、可收敛到边界);
|
||
5. h² = σ²a/(σ²a+σ²e);reliability = 1 - PEV/σ²a,PEV = C22[ii]·σ²e。
|
||
|
||
无系谱(所有个体均无父母)时:不背书遗传力——
|
||
h2 置 None、reliability 全部置 0、EBV 取中心化表型,并返回 warning。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
import random
|
||
|
||
import numpy as np
|
||
|
||
TOL = 1e-4
|
||
MAX_PROFILE_EVALS = 100
|
||
MAX_GXE_ITERS = 25 # G×E 分支坐标上升迭代上限
|
||
_FLOOR = 1e-8 # 方差下界,避免除零/负方差
|
||
H2_MIN, H2_MAX = 1e-3, 1.0 - 1e-3
|
||
N_SPARSE = 1000 # 个体数超过该阈值 → 稀疏 A⁻¹ + 共轭梯度(回避 O(n³) 稠密 MME 逆)
|
||
CG_MAX_ITER = 800 # 共轭梯度迭代上限
|
||
CG_TOL = 1e-9 # 共轭梯度相对残差收敛阈值
|
||
# Hutchinson 随机探测次数(估计 C22 对角线 → PEV → reliability)。稀疏路径 EBV/h²/σ² 为
|
||
# CG 精确解;reliability 是 MC 对角估计——同家族强相关的 C22 令其收敛慢(~1/√k),
|
||
# 误差随 k 减小但非精确,作为文档标注的近似(排序可靠、量级可信)。
|
||
HUTCH_K = 100
|
||
|
||
ENGINE_VERSION = "1.5.0" # 单性状动物模型 BLUP 求解器版本(落 bre_prediction.engine_version;1.1.0: 供 mtblup 单性状方差复用;1.2.0: G×R 砧木×接穗互作;1.3.0: AR1×AR1 空间协方差 solve_spatial + 稀疏 A⁻¹/共轭梯度/Hutchinson 可靠性;1.4.0: AR1×AR1 各向异性双参数 ρ_row/ρ_col;1.5.0: 区组随机效应 _solve_block(增广/α-格子 block_no 第二随机效应))
|
||
|
||
|
||
def _pearson(x: list[float], y: list[float]) -> float:
|
||
n = len(x)
|
||
if n < 2:
|
||
return 0.0
|
||
mx, my = sum(x) / n, sum(y) / n
|
||
cov = sum((a - mx) * (b - my) for a, b in zip(x, y))
|
||
dx = sum((a - mx) ** 2 for a in x) ** 0.5
|
||
dy = sum((b - my) ** 2 for b in y) ** 0.5
|
||
if dx == 0 or dy == 0:
|
||
return 0.0
|
||
return cov / (dx * dy)
|
||
|
||
|
||
def _order_pedigree(base: list[int], non_base: list[tuple[int, int | None, int | None]]) -> tuple[list[int], dict[int, int]]:
|
||
"""返回拓扑序(祖先恒在子代之前,base 恒在最前)与 {个体id: 序号}。"""
|
||
order: list[int] = list(base)
|
||
placed: set[int] = set(base)
|
||
remaining = list(non_base)
|
||
while remaining:
|
||
progressed = False
|
||
for it in list(remaining):
|
||
i, d, s = it
|
||
if (d is None or d in placed) and (s is None or s in placed):
|
||
order.append(i)
|
||
placed.add(i)
|
||
remaining.remove(it)
|
||
progressed = True
|
||
if not progressed:
|
||
raise ValueError(
|
||
"系谱存在环或父母引用缺失,无法拓扑排序"
|
||
"(父母 id 必须是已知个体或 None)"
|
||
)
|
||
return order, {ind: idx for idx, ind in enumerate(order)}
|
||
|
||
|
||
def _build_ainv(order: list[int], n_base: int,
|
||
parent_of: dict[int, tuple[int | None, int | None]]) -> tuple[np.ndarray, np.ndarray]:
|
||
"""返回 (A, A⁻¹)。A 稠密递归 O(n²);A⁻¹ 用 Henderson 稀疏规则。"""
|
||
n = len(order)
|
||
idx = {ind: pos for pos, ind in enumerate(order)}
|
||
A = np.eye(n)
|
||
for pos, ind in enumerate(order):
|
||
d, s = parent_of.get(ind, (None, None))
|
||
if d is None and s is None:
|
||
continue
|
||
di = idx.get(d) if d is not None else None
|
||
si = idx.get(s) if s is not None else None
|
||
row = np.zeros(n)
|
||
if di is not None:
|
||
row += A[di]
|
||
if si is not None:
|
||
row += A[si]
|
||
A[pos, :] = 0.5 * row
|
||
A[:pos, pos] = A[pos, :pos]
|
||
apq = A[di, si] if (di is not None and si is not None) else 0.0
|
||
A[pos, pos] = 1.0 + 0.5 * apq
|
||
|
||
Ainv = np.zeros((n, n))
|
||
for i in range(n_base):
|
||
Ainv[i, i] += 1.0
|
||
for ind, (d, s) in parent_of.items():
|
||
i = idx[ind]
|
||
di = idx.get(d) if d is not None else None
|
||
si = idx.get(s) if s is not None else None
|
||
fp = A[di, di] - 1.0 if di is not None else 0.0
|
||
fq = A[si, si] - 1.0 if si is not None else 0.0
|
||
t = 0.5 - 0.25 * (fp + fq)
|
||
# 极端近交(双亲 F→1)时 t→0,除零保护;1e-6 下界对正常系谱无感
|
||
inv_t = 1.0 / max(t, 1e-6)
|
||
Ainv[i, i] += inv_t
|
||
for p in (di, si):
|
||
if p is not None:
|
||
Ainv[p, p] += 0.25 * inv_t
|
||
Ainv[i, p] -= 0.5 * inv_t
|
||
Ainv[p, i] -= 0.5 * inv_t
|
||
if di is not None and si is not None:
|
||
Ainv[di, si] += 0.25 * inv_t
|
||
Ainv[si, di] += 0.25 * inv_t
|
||
return A, Ainv
|
||
|
||
|
||
def _build_ainv_sparse(order: list[int], n_base: int,
|
||
parent_of: dict[int, tuple[int | None, int | None]]
|
||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||
"""返回 (A, ii, jj, vv):A 稠密(供 REML 似然与近交系数);A⁻¹ 以 COO 稀疏三元组存储。
|
||
|
||
Henderson 规则与 _build_ainv 逐项一致,但 A⁻¹ 不落稠密矩阵,只收集非零元
|
||
(对角 + 2 亲本 × (对角/双向共祖先)),供共轭梯度 matvec 使用:
|
||
(Ainv·u)[i] = Σ_j Ainv_ij·u_j,np.add.at 按三元组累加即可。
|
||
"""
|
||
n = len(order)
|
||
idx = {ind: pos for pos, ind in enumerate(order)}
|
||
A = np.eye(n)
|
||
for pos, ind in enumerate(order):
|
||
d, s = parent_of.get(ind, (None, None))
|
||
if d is None and s is None:
|
||
continue
|
||
di = idx.get(d) if d is not None else None
|
||
si = idx.get(s) if s is not None else None
|
||
row = np.zeros(n)
|
||
if di is not None:
|
||
row += A[di]
|
||
if si is not None:
|
||
row += A[si]
|
||
A[pos, :] = 0.5 * row
|
||
A[:pos, pos] = A[pos, :pos]
|
||
apq = A[di, si] if (di is not None and si is not None) else 0.0
|
||
A[pos, pos] = 1.0 + 0.5 * apq
|
||
|
||
ii: list[int] = []
|
||
jj: list[int] = []
|
||
vv: list[float] = []
|
||
|
||
def _put(i: int, j: int, v: float) -> None:
|
||
if v == 0.0:
|
||
return
|
||
ii.append(i)
|
||
jj.append(j)
|
||
vv.append(v)
|
||
|
||
for i in range(n_base):
|
||
_put(i, i, 1.0)
|
||
for ind, (d, s) in parent_of.items():
|
||
i = idx[ind]
|
||
di = idx.get(d) if d is not None else None
|
||
si = idx.get(s) if s is not None else None
|
||
fp = A[di, di] - 1.0 if di is not None else 0.0
|
||
fq = A[si, si] - 1.0 if si is not None else 0.0
|
||
t = 0.5 - 0.25 * (fp + fq)
|
||
# 极端近交(双亲 F→1)时 t→0,除零保护;1e-6 下界对正常系谱无感
|
||
inv_t = 1.0 / max(t, 1e-6)
|
||
_put(i, i, inv_t)
|
||
for pp in (di, si):
|
||
if pp is not None:
|
||
_put(pp, pp, 0.25 * inv_t)
|
||
_put(i, pp, -0.5 * inv_t)
|
||
_put(pp, i, -0.5 * inv_t)
|
||
if di is not None and si is not None:
|
||
_put(di, si, 0.25 * inv_t)
|
||
_put(si, di, 0.25 * inv_t)
|
||
return (A, np.array(ii, dtype=np.int64), np.array(jj, dtype=np.int64),
|
||
np.array(vv, dtype=float))
|
||
|
||
|
||
def _cg_solve(matvec, rhs: np.ndarray, max_iter: int = CG_MAX_ITER,
|
||
tol: float = CG_TOL) -> tuple[np.ndarray, int, bool]:
|
||
"""共轭梯度求解 C·x = rhs(C 对称正定,仅需 matvec 回调)。
|
||
|
||
收敛判据:‖r‖ ≤ tol·‖rhs‖(相对残差);达到 max_iter 未达标 → 返回 (解, max_iter, False)。
|
||
调用方对未收敛结果回退稠密路径(小 n)或加 warning 并采用已探明最优。
|
||
"""
|
||
n = rhs.shape[0]
|
||
x = np.zeros(n)
|
||
r = rhs.astype(float).copy()
|
||
p = r.copy()
|
||
rsold = float(r @ r)
|
||
rhs_norm = float(np.linalg.norm(rhs))
|
||
if rhs_norm == 0.0 or rsold == 0.0:
|
||
return x, 0, True
|
||
target2 = (tol * rhs_norm) ** 2
|
||
for it in range(1, max_iter + 1):
|
||
Ap = matvec(p)
|
||
pAp = float(p @ Ap)
|
||
if pAp <= 1e-300:
|
||
return x, it, False
|
||
alpha = rsold / pAp
|
||
x += alpha * p
|
||
r -= alpha * Ap
|
||
rsnew = float(r @ r)
|
||
if rsnew <= target2:
|
||
return x, it, True
|
||
p = r + (rsnew / rsold) * p
|
||
rsold = rsnew
|
||
return x, max_iter, False
|
||
|
||
|
||
def _golden_max(f, lo: float, hi: float, tol: float = TOL, max_iter: int = MAX_PROFILE_EVALS) -> tuple[float, int]:
|
||
"""黄金分割一维最大化(f 单调时收敛到边界)。返回 (极值点, 函数求值次数)。"""
|
||
invphi = (np.sqrt(5.0) - 1.0) / 2.0
|
||
a, b = lo, hi
|
||
x1 = b - invphi * (b - a)
|
||
x2 = a + invphi * (b - a)
|
||
f1, f2 = f(x1), f(x2)
|
||
n = 2
|
||
while abs(b - a) > tol and n < max_iter:
|
||
if f1 > f2:
|
||
b, x2, f2 = x2, x1, f1
|
||
x1 = b - invphi * (b - a)
|
||
f1 = f(x1)
|
||
else:
|
||
a, x1, f1 = x1, x2, f2
|
||
x2 = a + invphi * (b - a)
|
||
f2 = f(x2)
|
||
n += 1
|
||
return (a + b) / 2.0, n
|
||
|
||
|
||
def relationship_matrix(pedigree: list[dict]) -> dict:
|
||
"""计算个体间加性亲缘矩阵 A(个体数少时稠密递归),供亲缘/近交分析。
|
||
|
||
亲缘系数 r_ij = A_ij(加性相关,双列之间 0.5=亲子/全同胞、0.25=半同胞);
|
||
近交系数 F_i = A_ii - 1。
|
||
|
||
参数:
|
||
pedigree: 同 solve() 的系谱结构(id 唯一,dam/sire 为 None 表示 founder)。
|
||
|
||
返回:
|
||
individuals: [个体id](拓扑序:founder 在前);
|
||
matrix: A 矩阵(len×len,6 位小数);
|
||
inbreeding: {individual_id: F}(仅 F>0 的个体)。
|
||
"""
|
||
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:
|
||
raise ValueError(f"系谱个体重复: {i}")
|
||
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 not seen:
|
||
raise ValueError("系谱为空")
|
||
order, _ = _order_pedigree(base, non_base)
|
||
parent_of = {i: (d, s) for (i, d, s) in non_base}
|
||
A, _ = _build_ainv(order, len(base), parent_of)
|
||
names = [str(o) for o in order]
|
||
inbreeding = {
|
||
str(o): float(round(A[i, i] - 1.0, 6))
|
||
for i, o in enumerate(order)
|
||
if A[i, i] - 1.0 > 1e-8
|
||
}
|
||
return {
|
||
"individuals": names,
|
||
"matrix": A.round(6).tolist(),
|
||
"inbreeding": inbreeding,
|
||
}
|
||
|
||
|
||
def _build_design(obs_ids: list[int], fixed: dict | None = None,
|
||
covariate: dict | None = None) -> np.ndarray:
|
||
"""构建设计阵 X:截距 + 固定效应水平虚拟列 + 协变量列。
|
||
|
||
fixed: {因子名: {individual_id: 水平}}——每因子对基准水平(排序首个)加虚拟列,
|
||
缺失水平的个体归基准(全 0);仅 1 个水平的因子无信息,跳过。
|
||
covariate: {individual_id: float}——单一协变量列,缺失按观测均值填充;
|
||
全部同值(无变异)时无信息,跳过。
|
||
"""
|
||
m = len(obs_ids)
|
||
cols: list[np.ndarray] = [np.ones(m)]
|
||
for _fname, mapping in (fixed or {}).items():
|
||
levels = sorted({str(mapping[i]) for i in obs_ids if mapping.get(i) is not None})
|
||
if len(levels) <= 1:
|
||
continue
|
||
ref = levels[0]
|
||
for lev in levels[1:]:
|
||
col = np.array(
|
||
[1.0 if str(mapping.get(i)) == lev else 0.0 for i in obs_ids],
|
||
dtype=float,
|
||
)
|
||
cols.append(col)
|
||
if covariate:
|
||
raw = [covariate.get(i) for i in obs_ids]
|
||
obs_vals = [float(v) for v in raw if v is not None]
|
||
if obs_vals:
|
||
mean = float(np.mean(obs_vals))
|
||
filled = np.array([float(v) if v is not None else mean for v in raw])
|
||
if np.std(filled) > 1e-12:
|
||
cols.append(filled)
|
||
return np.column_stack(cols)
|
||
|
||
|
||
def solve(pedigree: list[dict], phenotypes: dict, *, tol: float = TOL,
|
||
fixed: dict | None = None, covariate: dict | None = None,
|
||
gxe: dict | None = None, record_map: dict | None = None,
|
||
block: dict | None = None,
|
||
factor_label: str = "G×E", factor_name: str = "环境") -> dict:
|
||
"""运行单性状动物模型 BLUP(可含固定效应与协变量,如站点、crop_load)。
|
||
|
||
参数:
|
||
pedigree: [{"individual": id, "dam": id|None, "sire": id|None}];
|
||
个体 id 唯一;dam/sire 为 None 表示 founder(如种质)。
|
||
phenotypes: {记录id: float};默认每记录=一个加性个体(树级均值);
|
||
提供 record_map 时记录可多条映射到同一个体(如同一 clone 多年观测)。
|
||
fixed: {因子名: {记录id: 水平}}——固定效应(如 {"site": {1:"A", 2:"B"}});
|
||
每因子对基准水平加虚拟列,缺失个体归基准。
|
||
covariate: {记录id: float}——一个协变量列(如 crop_load,缺失按观测均值)。
|
||
gxe: {记录id: (基因型分组, 环境水平)}——提供即启用 G×E 分支:
|
||
增加第二随机效应 v(基因型×环境,协方差 I·σ²gxe),
|
||
剖面 REML 坐标上升估三方差分量(比率边界与 h² 网格一致,λ 恒正)。
|
||
block: {记录id: 区组水平}——提供即启用区组随机效应分支(不完全区组/增广/α-格子):
|
||
增加第二随机效应 v(区组,协方差 I·σ²block),与 gxe 互斥(共用 Z₂ 槽)。
|
||
record_map: {记录id: 加性个体id}——记录→个体映射(gxe/block 分支用),默认恒等。
|
||
factor_label / factor_name: 交互随机效应文案(默认 G×E/环境;G×R 传 G×R/砧木),仅影响警告字符串。
|
||
|
||
返回:
|
||
ebv: {id: float} 全部个体(含 base 种质)的育种值;
|
||
reliability: {id: float} 个体可靠性(无系谱时为 0);
|
||
h2: float | None 窄义遗传力(无系谱时为 None);
|
||
sigma_a / sigma_e: float;sigma_gxe: float(G×E 分支);sigma_block: float(区组分支);
|
||
gxe_ratio / n_gxe / n_cross_env / n_genotypes(G×E 分支);
|
||
block_ratio / n_blocks / block_effects(区组分支);
|
||
n_obs / n_individuals / n_base / n_with_parents / n_fixed: int;
|
||
converged: bool;n_iter: int(剖面求值次数/EM 迭代数);warning: str | None;
|
||
solver: str("dense" 稠密精确 / "sparse-cg" 稀疏共轭梯度,n>N_SPARSE 时触发);
|
||
稀疏路径另含 cg_iter(CG 迭代数)与 reliability_approx=True(可靠性为 Hutchinson 近似,
|
||
EBV/h²/σ² 仍为精确解)。
|
||
"""
|
||
if gxe is not None:
|
||
return _solve_gxe(pedigree, phenotypes, fixed=fixed, covariate=covariate,
|
||
gxe=gxe, record_map=record_map or {}, tol=tol,
|
||
factor_label=factor_label, factor_name=factor_name)
|
||
if block is not None:
|
||
return _solve_block(pedigree, phenotypes, fixed=fixed, covariate=covariate,
|
||
block_of=block, record_map=record_map or {}, tol=tol,
|
||
factor_label=factor_label, factor_name=factor_name)
|
||
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:
|
||
raise ValueError(f"系谱个体重复: {i}")
|
||
seen.add(i)
|
||
d = rec.get("dam")
|
||
s = rec.get("sire")
|
||
if d is None and s is None:
|
||
base.append(i)
|
||
else:
|
||
non_base.append((i, d, s))
|
||
# 表型中出现的个体若不在系谱中,自动并入 base(鲁棒)。
|
||
for pid in phenotypes:
|
||
if pid not in seen:
|
||
seen.add(pid)
|
||
base.append(pid)
|
||
if not seen:
|
||
raise ValueError("系谱与表型均为空")
|
||
|
||
order, idx = _order_pedigree(base, non_base)
|
||
n = len(order)
|
||
n_base = len(base)
|
||
parent_of: dict[int, tuple[int | None, int | None]] = {i: (d, s) for (i, d, s) in non_base}
|
||
n_with_parents = len(parent_of)
|
||
|
||
obs_ids = [ind for ind in order if ind in phenotypes]
|
||
m = len(obs_ids)
|
||
if m < 2:
|
||
raise ValueError("有效表型个体不足 2 个,无法估计方差组分")
|
||
|
||
X = _build_design(obs_ids, fixed, covariate)
|
||
p = X.shape[1]
|
||
n_fixed = p - 1
|
||
y = np.array([float(phenotypes[i]) for i in obs_ids], dtype=float)
|
||
|
||
if n_with_parents == 0:
|
||
# 无系谱:不背书遗传力。EBV 取表型残差(固定效应已校正)。
|
||
beta = np.linalg.lstsq(X, y, rcond=None)[0]
|
||
resid = y - X @ beta
|
||
ebv: dict[int, float] = {}
|
||
for k, ind in enumerate(obs_ids):
|
||
ebv[ind] = float(resid[k])
|
||
for ind in order:
|
||
ebv.setdefault(ind, 0.0)
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": {ind: 0.0 for ind in order},
|
||
"h2": None,
|
||
"sigma_a": 0.0,
|
||
"sigma_e": 0.0,
|
||
"n_obs": m,
|
||
"n_individuals": n,
|
||
"n_base": n_base,
|
||
"n_with_parents": 0,
|
||
"n_fixed": n_fixed,
|
||
"converged": True,
|
||
"n_iter": 0,
|
||
"warning": "无系谱信息(所有个体均无父母):EBV 仅为表型残差(固定效应已校正),未估计遗传力与可靠性。",
|
||
}
|
||
|
||
Z = np.zeros((m, n))
|
||
for k, ind in enumerate(obs_ids):
|
||
Z[k, idx[ind]] = 1.0
|
||
XtX = X.T @ X
|
||
Xty = X.T @ y
|
||
Zty = Z.T @ y
|
||
ZtZ = Z.T @ Z
|
||
XtZ = X.T @ Z
|
||
|
||
# A7 阈值分派:个体数 > N_SPARSE 且系谱存在 → 稀疏 A⁻¹ + 共轭梯度迭代求解
|
||
# (回避 O(n³) 稠密 MME 逆);可靠性用 Hutchinson 随机探测估计 C22 对角线。
|
||
use_sparse = n_with_parents > 0 and n > N_SPARSE
|
||
if use_sparse:
|
||
A, ainv_ii, ainv_jj, ainv_vv = _build_ainv_sparse(order, n_base, parent_of)
|
||
else:
|
||
A, Ainv = _build_ainv(order, n_base, parent_of)
|
||
|
||
def _reml_ll(Va: float, Ve: float) -> float:
|
||
"""精确 REML 对数似然(直接 V 计算,O(m³))。"""
|
||
V = (Z @ A @ Z.T) * Va + np.eye(m) * Ve
|
||
VinvX = np.linalg.solve(V, X) # p 个 RHS,1 次 LU
|
||
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)
|
||
|
||
if use_sparse:
|
||
def _matvec(lam: float):
|
||
def mv(v: np.ndarray) -> np.ndarray:
|
||
b, u = v[:p], v[p:]
|
||
out1 = XtX @ b + XtZ @ u
|
||
out2 = XtZ.T @ b + ZtZ @ u
|
||
np.add.at(out2, ainv_ii, lam * ainv_vv * u[ainv_jj])
|
||
return np.concatenate([out1, out2])
|
||
return mv
|
||
|
||
mme_rhs = np.concatenate([Xty, Zty])
|
||
|
||
best = {"h2": None, "ll": -np.inf, "Va": 0.0, "Ve": 0.0}
|
||
|
||
def _profile(h2: float) -> float:
|
||
lam = (1.0 - h2) / h2
|
||
sol, _it, _ok = _cg_solve(_matvec(lam), mme_rhs)
|
||
b = sol[:p]
|
||
u = sol[p:]
|
||
yPy = float(y @ y - b @ Xty - u @ Zty)
|
||
Ve = max(yPy / max(m - p, 1), _FLOOR)
|
||
Va = Ve / lam
|
||
ll = _reml_ll(Va, Ve)
|
||
if ll > best["ll"]:
|
||
best.update(h2=h2, ll=ll, Va=Va, Ve=Ve)
|
||
return ll
|
||
|
||
h2_opt, n_iter = _golden_max(_profile, H2_MIN, H2_MAX, tol=tol)
|
||
_profile(h2_opt)
|
||
h2 = best["h2"] if best["h2"] is not None else h2_opt
|
||
Va, Ve = best["Va"], best["Ve"]
|
||
|
||
lam = (1.0 - h2) / h2
|
||
mv = _matvec(lam)
|
||
sol, cg_iter, cg_ok = _cg_solve(mv, mme_rhs)
|
||
u = sol[p:]
|
||
# Hutchinson:E[P⊙(C22·P)] = diag(C22),C22·P 由 CG 解 big·x=[0;P] 取 x[p:] 得到
|
||
rng = np.random.RandomState(20260804)
|
||
diag_c22 = np.zeros(n)
|
||
for _ in range(HUTCH_K):
|
||
pv = rng.choice([-1.0, 1.0], size=n)
|
||
rhs2 = np.zeros(p + n)
|
||
rhs2[p:] = pv
|
||
sol2, _it2, _ok2 = _cg_solve(mv, rhs2)
|
||
diag_c22 += sol2[p:] * pv
|
||
diag_c22 /= HUTCH_K
|
||
|
||
ebv = {ind: float(u[idx[ind]]) for ind in order}
|
||
pev = diag_c22 * Ve
|
||
rel = {ind: float(np.clip(1.0 - pev[idx[ind]] / Va, 0.0, 1.0)) for ind in order}
|
||
|
||
warning = None
|
||
if n_iter >= MAX_PROFILE_EVALS:
|
||
warning = "剖面 REML 未完全收敛(似然面极平/边界最优),结果已采用已探明最优。"
|
||
if not cg_ok:
|
||
resid = mme_rhs - mv(sol)
|
||
rel_res = float(np.linalg.norm(resid) / (np.linalg.norm(mme_rhs) + 1e-12))
|
||
w = (f"共轭梯度 {cg_iter} 次迭代未达收敛阈值(相对残差 {rel_res:.2e}),"
|
||
"结果采用已探明最优,可靠性为 Hutchinson 近似。")
|
||
warning = f"{warning} {w}" if warning else w
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": rel,
|
||
"h2": float(h2),
|
||
"sigma_a": float(Va),
|
||
"sigma_e": float(Ve),
|
||
"n_obs": m,
|
||
"n_individuals": n,
|
||
"n_base": n_base,
|
||
"n_with_parents": n_with_parents,
|
||
"n_fixed": n_fixed,
|
||
"converged": n_iter < MAX_PROFILE_EVALS and cg_ok,
|
||
"n_iter": n_iter,
|
||
"solver": "sparse-cg",
|
||
"cg_iter": cg_iter,
|
||
"reliability_approx": True,
|
||
"warning": warning,
|
||
}
|
||
|
||
def _mme_solve(lam: float) -> tuple[np.ndarray, np.ndarray]:
|
||
big = np.zeros((p + n, p + n))
|
||
big[:p, :p] = XtX
|
||
big[:p, p:] = XtZ
|
||
big[p:, :p] = XtZ.T
|
||
big[p:, p:] = ZtZ + lam * Ainv
|
||
rhs = np.concatenate([Xty, Zty])
|
||
try:
|
||
sol = np.linalg.solve(big, rhs)
|
||
big_inv = np.linalg.inv(big)
|
||
except np.linalg.LinAlgError:
|
||
sol = np.linalg.pinv(big) @ rhs
|
||
big_inv = np.linalg.pinv(big)
|
||
return sol, big_inv
|
||
|
||
best = {"h2": None, "ll": -np.inf, "Va": 0.0, "Ve": 0.0}
|
||
|
||
def _profile(h2: float) -> float:
|
||
lam = (1.0 - h2) / h2
|
||
sol, _ = _mme_solve(lam)
|
||
b = sol[:p]
|
||
u = sol[p:]
|
||
yPy = float(y @ y - b @ Xty - u @ Zty)
|
||
Ve = max(yPy / max(m - p, 1), _FLOOR)
|
||
Va = Ve / lam
|
||
ll = _reml_ll(Va, Ve)
|
||
if ll > best["ll"]:
|
||
best.update(h2=h2, ll=ll, Va=Va, Ve=Ve)
|
||
return ll
|
||
|
||
h2_opt, n_iter = _golden_max(_profile, H2_MIN, H2_MAX, tol=tol)
|
||
# 黄金分割端点通常已探明;再用最优 h2 精确求一次
|
||
_profile(h2_opt)
|
||
h2 = best["h2"] if best["h2"] is not None else h2_opt
|
||
Va, Ve = best["Va"], best["Ve"]
|
||
|
||
lam = (1.0 - h2) / h2
|
||
sol, big_inv = _mme_solve(lam)
|
||
u = sol[p:]
|
||
C22 = big_inv[p:, p:]
|
||
|
||
ebv = {ind: float(u[idx[ind]]) for ind in order}
|
||
pev = np.diag(C22) * Ve
|
||
rel = {ind: float(np.clip(1.0 - pev[idx[ind]] / Va, 0.0, 1.0)) for ind in order}
|
||
|
||
warning = None
|
||
if n_iter >= MAX_PROFILE_EVALS:
|
||
warning = "剖面 REML 未完全收敛(似然面极平/边界最优),结果已采用已探明最优。"
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": rel,
|
||
"h2": float(h2),
|
||
"sigma_a": float(Va),
|
||
"sigma_e": float(Ve),
|
||
"n_obs": m,
|
||
"n_individuals": n,
|
||
"n_base": n_base,
|
||
"n_with_parents": n_with_parents,
|
||
"n_fixed": n_fixed,
|
||
"converged": n_iter < MAX_PROFILE_EVALS,
|
||
"n_iter": n_iter,
|
||
"solver": "dense",
|
||
"warning": warning,
|
||
}
|
||
|
||
|
||
def solve_spatial(pedigree: list[dict], phenotypes: dict, *, coords: dict,
|
||
tol: float = TOL, fixed: dict | None = None,
|
||
covariate: dict | None = None, aniso: bool = False) -> dict:
|
||
"""AR1×AR1 空间协方差动物模型 BLUP(v1:ρ_row=ρ_col=ρ 降维;aniso=True:双参数)。
|
||
|
||
y = Xb + Zu + e,e~N(0, σ²e·R),R_ij = ρ^(|Δrow|+|Δcol|)(行/列 AR1 之积,
|
||
即 AR1(ρ)⊗AR1(ρ))。R⁻¹ 用纯 numpy eigh 对角化;REML 对 (h², ρ) 坐标上升黄金分割。
|
||
aniso=True 时 R_ij = ρ_row^|Δrow|·ρ_col^|Δcol|,对 (h², ρ_row, ρ_col) 坐标上升
|
||
(方法标签 method="AR1×AR1(aniso)"、落 rho_row/rho_col);aniso=False 时输出与 v1 逐字节一致。
|
||
|
||
参数:
|
||
pedigree / phenotypes / fixed / covariate: 同 solve()。
|
||
coords: {个体id: (row_no, col_no)}——每个观测个体必须提供行/列坐标,
|
||
缺失 → ValueError(门禁:缺坐标无法建模空间残差)。
|
||
aniso: 是否启用各向异性双参数 ρ_row/ρ_col(默认 False=单 ρ 降维)。
|
||
|
||
返回:
|
||
ebv / reliability / h2 / sigma_a / sigma_e / n_obs / n_individuals /
|
||
n_base / n_with_parents / n_fixed / converged / n_iter / warning(同 solve);
|
||
另含 rho(单参,aniso=False)或 rho_row/rho_col(aniso=True)、
|
||
spatial=True、method="AR1×AR1" 或 "AR1×AR1(aniso)"。
|
||
"""
|
||
if aniso:
|
||
return _solve_spatial_aniso(pedigree, phenotypes, coords=coords, tol=tol,
|
||
fixed=fixed, covariate=covariate)
|
||
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:
|
||
raise ValueError(f"系谱个体重复: {i}")
|
||
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))
|
||
for pid in phenotypes:
|
||
if pid not in seen:
|
||
seen.add(pid)
|
||
base.append(pid)
|
||
if not seen:
|
||
raise ValueError("系谱与表型均为空")
|
||
|
||
order, idx = _order_pedigree(base, non_base)
|
||
n = len(order)
|
||
n_base = len(base)
|
||
parent_of: dict[int, tuple[int | None, int | None]] = {i: (d, s) for (i, d, s) in non_base}
|
||
n_with_parents = len(parent_of)
|
||
|
||
obs_ids = [ind for ind in order if ind in phenotypes]
|
||
m = len(obs_ids)
|
||
if m < 2:
|
||
raise ValueError("有效表型个体不足 2 个,无法估计方差组分")
|
||
missing = [i for i in obs_ids if coords.get(i) is None]
|
||
if missing:
|
||
raise ValueError(f"缺行/列坐标的个体(空间协方差需每个观测提供 row_no/col_no): {missing[:5]}")
|
||
|
||
X = _build_design(obs_ids, fixed, covariate)
|
||
p = X.shape[1]
|
||
n_fixed = p - 1
|
||
y = np.array([float(phenotypes[i]) for i in obs_ids], dtype=float)
|
||
|
||
if n_with_parents == 0:
|
||
beta = np.linalg.lstsq(X, y, rcond=None)[0]
|
||
resid = y - X @ beta
|
||
ebv: dict[int, float] = {}
|
||
for k, ind in enumerate(obs_ids):
|
||
ebv[ind] = float(resid[k])
|
||
for ind in order:
|
||
ebv.setdefault(ind, 0.0)
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": {ind: 0.0 for ind in order},
|
||
"h2": None, "sigma_a": 0.0, "sigma_e": 0.0,
|
||
"rho": 0.0, "spatial": True, "method": "AR1×AR1",
|
||
"n_obs": m, "n_individuals": n, "n_base": n_base,
|
||
"n_with_parents": 0, "n_fixed": n_fixed,
|
||
"converged": True, "n_iter": 0,
|
||
"solver": "dense",
|
||
"warning": "无系谱信息(所有个体均无父母):EBV 仅为表型残差,未估计遗传力与可靠性。",
|
||
}
|
||
|
||
A, Ainv = _build_ainv(order, n_base, parent_of)
|
||
Z = np.zeros((m, n))
|
||
for k, ind in enumerate(obs_ids):
|
||
Z[k, idx[ind]] = 1.0
|
||
rows = np.array([coords[i][0] for i in obs_ids], dtype=float)
|
||
cols = np.array([coords[i][1] for i in obs_ids], dtype=float)
|
||
# R_ij = ρ^(|Δrow|+|Δcol|)
|
||
dmat = np.abs(rows[:, None] - rows[None, :]) + np.abs(cols[:, None] - cols[None, :])
|
||
XtX, Xty = X.T @ X, X.T @ y
|
||
Zty = Z.T @ y
|
||
ZtZ = Z.T @ Z
|
||
XtZ = X.T @ Z
|
||
|
||
def _rinv(rho: float) -> np.ndarray:
|
||
R = np.power(rho, dmat)
|
||
evals, Q = np.linalg.eigh(R)
|
||
return Q @ np.diag(1.0 / np.maximum(evals, _FLOOR)) @ Q.T
|
||
|
||
def _mme_solve(lam: float, rho: float) -> tuple[np.ndarray, np.ndarray]:
|
||
Rinv = _rinv(rho)
|
||
XrX = X.T @ Rinv @ X
|
||
XrZ = X.T @ Rinv @ Z
|
||
ZrZ = Z.T @ Rinv @ Z
|
||
Xry = X.T @ Rinv @ y
|
||
Zry = Z.T @ Rinv @ y
|
||
big = np.zeros((p + n, p + n))
|
||
big[:p, :p] = XrX
|
||
big[:p, p:] = XrZ
|
||
big[p:, :p] = XrZ.T
|
||
big[p:, p:] = ZrZ + lam * Ainv
|
||
rhs = np.concatenate([Xry, Zry])
|
||
try:
|
||
sol = np.linalg.solve(big, rhs)
|
||
big_inv = np.linalg.inv(big)
|
||
except np.linalg.LinAlgError:
|
||
sol = np.linalg.pinv(big) @ rhs
|
||
big_inv = np.linalg.pinv(big)
|
||
return sol, big_inv
|
||
|
||
def _reml_ll(Va: float, Ve: float, rho: float) -> float:
|
||
R = np.power(rho, dmat)
|
||
V = (Z @ A @ Z.T) * Va + R * Ve
|
||
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)
|
||
|
||
RHO_LO, RHO_HI = 0.02, 0.98
|
||
best = {"h2": None, "rho": 0.0, "ll": -np.inf, "Va": 0.0, "Ve": 0.0}
|
||
h2_l, rho_l = 0.5, 0.5
|
||
|
||
def _track(h2: float, rho: float) -> float:
|
||
lam = (1.0 - h2) / h2
|
||
sol, _ = _mme_solve(lam, rho)
|
||
b, u = sol[:p], sol[p:]
|
||
resid = y - X @ b - Z @ u
|
||
yPy = float(resid @ _rinv(rho) @ resid)
|
||
Ve = max(yPy / max(m - p, 1), _FLOOR)
|
||
Va = Ve / lam
|
||
ll = _reml_ll(Va, Ve, rho)
|
||
if ll > best["ll"]:
|
||
best.update(h2=h2, rho=rho, ll=ll, Va=Va, Ve=Ve)
|
||
return ll
|
||
|
||
def _f_h2(h2: float) -> float:
|
||
return _track(h2, rho_l)
|
||
|
||
def _f_rho(rho: float) -> float:
|
||
return _track(h2_l, rho)
|
||
|
||
converged = False
|
||
n_iter = 0
|
||
prev_best = -np.inf
|
||
for _ in range(MAX_GXE_ITERS):
|
||
n_iter += 1
|
||
h2_l, _ = _golden_max(_f_h2, H2_MIN, H2_MAX, tol=1e-3)
|
||
rho_l, _ = _golden_max(_f_rho, RHO_LO, RHO_HI, tol=1e-3)
|
||
if best["ll"] - prev_best < 1e-2:
|
||
converged = True
|
||
break
|
||
prev_best = best["ll"]
|
||
_track(h2_l, rho_l)
|
||
|
||
h2 = best["h2"] if best["h2"] is not None else h2_l
|
||
Va, Ve = best["Va"], best["Ve"]
|
||
rho = best["rho"]
|
||
|
||
lam = (1.0 - h2) / h2
|
||
sol, big_inv, *_ = _mme_solve(lam, rho)
|
||
u = sol[p:]
|
||
C22 = big_inv[p:, p:]
|
||
|
||
ebv = {ind: float(u[idx[ind]]) for ind in order}
|
||
pev = np.diag(C22) * Ve
|
||
rel = {ind: float(np.clip(1.0 - pev[idx[ind]] / Va, 0.0, 1.0)) for ind in order}
|
||
|
||
warning = None
|
||
if rho <= RHO_LO * 1.05:
|
||
warning = "空间自相关 ρ 收敛到下界,残差接近独立(空间结构弱)。"
|
||
elif not converged:
|
||
warning = "剖面 REML 坐标上升未完全收敛(空间似然面极平/边界最优),结果采用已探明最优。"
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": rel,
|
||
"h2": float(h2),
|
||
"sigma_a": float(Va),
|
||
"sigma_e": float(Ve),
|
||
"rho": float(rho),
|
||
"spatial": True,
|
||
"method": "AR1×AR1",
|
||
"n_obs": m,
|
||
"n_individuals": n,
|
||
"n_base": n_base,
|
||
"n_with_parents": n_with_parents,
|
||
"n_fixed": n_fixed,
|
||
"converged": converged,
|
||
"n_iter": n_iter,
|
||
"solver": "dense",
|
||
"warning": warning,
|
||
}
|
||
|
||
|
||
def _solve_spatial_aniso(pedigree: list[dict], phenotypes: dict, *, coords: dict,
|
||
tol: float = TOL, fixed: dict | None = None,
|
||
covariate: dict | None = None) -> dict:
|
||
"""AR1×AR1 各向异性空间协方差 BLUP(aniso=True 分支,双参数 ρ_row/ρ_col)。
|
||
|
||
R_ij = ρ_row^|Δrow| · ρ_col^|Δcol|(行/列独立 AR1 之积)。REML 对 (h², ρ_row, ρ_col)
|
||
三变量坐标上升黄金分割;门禁/互斥/无系谱语义与 v1 solve_spatial 逐项一致,
|
||
仅 ρ 由单参扩展为行/列双参。方法标签 method="AR1×AR1(aniso)"、落 rho_row/rho_col。
|
||
"""
|
||
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:
|
||
raise ValueError(f"系谱个体重复: {i}")
|
||
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))
|
||
for pid in phenotypes:
|
||
if pid not in seen:
|
||
seen.add(pid)
|
||
base.append(pid)
|
||
if not seen:
|
||
raise ValueError("系谱与表型均为空")
|
||
|
||
order, idx = _order_pedigree(base, non_base)
|
||
n = len(order)
|
||
n_base = len(base)
|
||
parent_of: dict[int, tuple[int | None, int | None]] = {i: (d, s) for (i, d, s) in non_base}
|
||
n_with_parents = len(parent_of)
|
||
|
||
obs_ids = [ind for ind in order if ind in phenotypes]
|
||
m = len(obs_ids)
|
||
if m < 2:
|
||
raise ValueError("有效表型个体不足 2 个,无法估计方差组分")
|
||
missing = [i for i in obs_ids if coords.get(i) is None]
|
||
if missing:
|
||
raise ValueError(f"缺行/列坐标的个体(空间协方差需每个观测提供 row_no/col_no): {missing[:5]}")
|
||
|
||
X = _build_design(obs_ids, fixed, covariate)
|
||
p = X.shape[1]
|
||
n_fixed = p - 1
|
||
y = np.array([float(phenotypes[i]) for i in obs_ids], dtype=float)
|
||
|
||
if n_with_parents == 0:
|
||
beta = np.linalg.lstsq(X, y, rcond=None)[0]
|
||
resid = y - X @ beta
|
||
ebv: dict[int, float] = {}
|
||
for k, ind in enumerate(obs_ids):
|
||
ebv[ind] = float(resid[k])
|
||
for ind in order:
|
||
ebv.setdefault(ind, 0.0)
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": {ind: 0.0 for ind in order},
|
||
"h2": None, "sigma_a": 0.0, "sigma_e": 0.0,
|
||
"rho_row": 0.0, "rho_col": 0.0, "spatial": True, "method": "AR1×AR1(aniso)",
|
||
"n_obs": m, "n_individuals": n, "n_base": n_base,
|
||
"n_with_parents": 0, "n_fixed": n_fixed,
|
||
"converged": True, "n_iter": 0,
|
||
"solver": "dense",
|
||
"warning": "无系谱信息(所有个体均无父母):EBV 仅为表型残差,未估计遗传力与可靠性。",
|
||
}
|
||
|
||
A, Ainv = _build_ainv(order, n_base, parent_of)
|
||
Z = np.zeros((m, n))
|
||
for k, ind in enumerate(obs_ids):
|
||
Z[k, idx[ind]] = 1.0
|
||
rows = np.array([coords[i][0] for i in obs_ids], dtype=float)
|
||
cols = np.array([coords[i][1] for i in obs_ids], dtype=float)
|
||
dmat_row = np.abs(rows[:, None] - rows[None, :])
|
||
dmat_col = np.abs(cols[:, None] - cols[None, :])
|
||
XtX, Xty = X.T @ X, X.T @ y
|
||
Zty = Z.T @ y
|
||
ZtZ = Z.T @ Z
|
||
XtZ = X.T @ Z
|
||
|
||
def _rinv(rr: float, rc: float) -> np.ndarray:
|
||
R = np.power(rr, dmat_row) * np.power(rc, dmat_col)
|
||
evals, Q = np.linalg.eigh(R)
|
||
return Q @ np.diag(1.0 / np.maximum(evals, _FLOOR)) @ Q.T
|
||
|
||
def _mme_solve(lam: float, rr: float, rc: float) -> tuple[np.ndarray, np.ndarray]:
|
||
Rinv = _rinv(rr, rc)
|
||
XrX = X.T @ Rinv @ X
|
||
XrZ = X.T @ Rinv @ Z
|
||
ZrZ = Z.T @ Rinv @ Z
|
||
Xry = X.T @ Rinv @ y
|
||
Zry = Z.T @ Rinv @ y
|
||
big = np.zeros((p + n, p + n))
|
||
big[:p, :p] = XrX
|
||
big[:p, p:] = XrZ
|
||
big[p:, :p] = XrZ.T
|
||
big[p:, p:] = ZrZ + lam * Ainv
|
||
rhs = np.concatenate([Xry, Zry])
|
||
try:
|
||
sol = np.linalg.solve(big, rhs)
|
||
big_inv = np.linalg.inv(big)
|
||
except np.linalg.LinAlgError:
|
||
sol = np.linalg.pinv(big) @ rhs
|
||
big_inv = np.linalg.pinv(big)
|
||
return sol, big_inv
|
||
|
||
def _reml_ll(Va: float, Ve: float, rr: float, rc: float) -> float:
|
||
R = np.power(rr, dmat_row) * np.power(rc, dmat_col)
|
||
V = (Z @ A @ Z.T) * Va + R * Ve
|
||
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)
|
||
|
||
RHO_LO, RHO_HI = 0.02, 0.98
|
||
best = {"h2": None, "rho_row": 0.5, "rho_col": 0.5, "ll": -np.inf,
|
||
"Va": 0.0, "Ve": 0.0}
|
||
h2_l, rho_row_l, rho_col_l = 0.5, 0.5, 0.5
|
||
|
||
def _track(h2: float, rr: float, rc: float) -> float:
|
||
lam = (1.0 - h2) / h2
|
||
sol, _ = _mme_solve(lam, rr, rc)
|
||
b, u = sol[:p], sol[p:]
|
||
resid = y - X @ b - Z @ u
|
||
yPy = float(resid @ _rinv(rr, rc) @ resid)
|
||
Ve = max(yPy / max(m - p, 1), _FLOOR)
|
||
Va = Ve / lam
|
||
ll = _reml_ll(Va, Ve, rr, rc)
|
||
if ll > best["ll"]:
|
||
best.update(h2=h2, rho_row=rr, rho_col=rc, ll=ll, Va=Va, Ve=Ve)
|
||
return ll
|
||
|
||
def _f_h2(h2: float) -> float:
|
||
return _track(h2, rho_row_l, rho_col_l)
|
||
|
||
def _f_rr(rr: float) -> float:
|
||
return _track(h2_l, rr, rho_col_l)
|
||
|
||
def _f_rc(rc: float) -> float:
|
||
return _track(h2_l, rho_row_l, rc)
|
||
|
||
converged = False
|
||
n_iter = 0
|
||
prev_best = -np.inf
|
||
for _ in range(MAX_GXE_ITERS):
|
||
n_iter += 1
|
||
h2_l, _ = _golden_max(_f_h2, H2_MIN, H2_MAX, tol=1e-3)
|
||
rho_row_l, _ = _golden_max(_f_rr, RHO_LO, RHO_HI, tol=1e-3)
|
||
rho_col_l, _ = _golden_max(_f_rc, RHO_LO, RHO_HI, tol=1e-3)
|
||
if best["ll"] - prev_best < 1e-2:
|
||
converged = True
|
||
break
|
||
prev_best = best["ll"]
|
||
_track(h2_l, rho_row_l, rho_col_l)
|
||
|
||
h2 = best["h2"] if best["h2"] is not None else h2_l
|
||
Va, Ve = best["Va"], best["Ve"]
|
||
rho_row, rho_col = best["rho_row"], best["rho_col"]
|
||
|
||
lam = (1.0 - h2) / h2
|
||
sol, big_inv, *_ = _mme_solve(lam, rho_row, rho_col)
|
||
u = sol[p:]
|
||
C22 = big_inv[p:, p:]
|
||
|
||
ebv = {ind: float(u[idx[ind]]) for ind in order}
|
||
pev = np.diag(C22) * Ve
|
||
rel = {ind: float(np.clip(1.0 - pev[idx[ind]] / Va, 0.0, 1.0)) for ind in order}
|
||
|
||
warning = None
|
||
if rho_row <= RHO_LO * 1.05 and rho_col <= RHO_LO * 1.05:
|
||
warning = "空间自相关 ρ_row/ρ_col 均收敛到下界,残差接近独立(空间结构弱)。"
|
||
elif not converged:
|
||
warning = "剖面 REML 坐标上升未完全收敛(空间似然面极平/边界最优),结果采用已探明最优。"
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": rel,
|
||
"h2": float(h2),
|
||
"sigma_a": float(Va),
|
||
"sigma_e": float(Ve),
|
||
"rho_row": float(rho_row),
|
||
"rho_col": float(rho_col),
|
||
"spatial": True,
|
||
"method": "AR1×AR1(aniso)",
|
||
"n_obs": m,
|
||
"n_individuals": n,
|
||
"n_base": n_base,
|
||
"n_with_parents": n_with_parents,
|
||
"n_fixed": n_fixed,
|
||
"converged": converged,
|
||
"n_iter": n_iter,
|
||
"solver": "dense",
|
||
"warning": warning,
|
||
}
|
||
|
||
|
||
def _solve_gxe(pedigree: list[dict], phenotypes: dict, *, fixed: dict | None,
|
||
covariate: dict | None, gxe: dict, record_map: dict, tol: float,
|
||
factor_label: str = "G×E", factor_name: str = "环境") -> dict:
|
||
"""G×E 分支:y = Xb + Z₁u + Z₂v + e,u~N(0,A·σ²a)、v~N(0,I·σ²gxe)、e~N(0,I·σ²e)。
|
||
|
||
交互随机效应槽 Z₂ 按 (基因型, 因子水平) 分组,不限定因子语义——
|
||
G×R(砧木)复用同一求解器:factor_label/factor_name 仅影响警告文案
|
||
(默认「G×E/环境」,G×R 传「G×R/砧木」),求解逻辑逐字节不变。
|
||
|
||
三方差分量用剖面 REML 估计:对 (log10 σ²a/σ²e, log10 σ²gxe/σ²e) 坐标上升黄金分割,
|
||
比率边界 λ∈[1e-3,1e3] 恒正,避免 EM-REML 边界崩塌(σ²e→0 → λ→0 → 插值)。
|
||
记录级:phenotypes/gxe/fixed/covariate 均以记录 id 为键;record_map 记录→加性个体。
|
||
"""
|
||
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:
|
||
raise ValueError(f"系谱个体重复: {i}")
|
||
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))
|
||
for pid in phenotypes:
|
||
ind = record_map.get(pid, pid)
|
||
if ind not in seen:
|
||
seen.add(ind)
|
||
base.append(ind)
|
||
if not seen:
|
||
raise ValueError("系谱与表型均为空")
|
||
|
||
order, idx = _order_pedigree(base, non_base)
|
||
n = len(order)
|
||
n_base = len(base)
|
||
parent_of: dict[int, tuple[int | None, int | None]] = {i: (d, s) for (i, d, s) in non_base}
|
||
n_with_parents = len(parent_of)
|
||
|
||
rec_ids = list(phenotypes.keys())
|
||
m = len(rec_ids)
|
||
if m < 2:
|
||
raise ValueError("有效表型个体不足 2 个,无法估计方差组分")
|
||
|
||
A, Ainv = _build_ainv(order, n_base, parent_of)
|
||
X = _build_design(rec_ids, fixed, covariate)
|
||
p = X.shape[1]
|
||
n_fixed = p - 1
|
||
y = np.array([float(phenotypes[i]) for i in rec_ids], dtype=float)
|
||
|
||
Z1 = np.zeros((m, n))
|
||
for k, rid in enumerate(rec_ids):
|
||
Z1[k, idx[record_map.get(rid, rid)]] = 1.0
|
||
|
||
pairs: dict[tuple[str, str], int] = {}
|
||
pair_list: list[tuple[str, str]] = []
|
||
for rid in rec_ids:
|
||
key = (str(gxe[rid][0]), str(gxe[rid][1]))
|
||
if key not in pairs:
|
||
pairs[key] = len(pair_list)
|
||
pair_list.append(key)
|
||
q = len(pair_list)
|
||
if q < 1:
|
||
raise ValueError("无 G×E 效应水平")
|
||
Z2 = np.zeros((m, q))
|
||
for k, rid in enumerate(rec_ids):
|
||
Z2[k, pairs[(str(gxe[rid][0]), str(gxe[rid][1]))]] = 1.0
|
||
|
||
XtX, Xty = X.T @ X, X.T @ y
|
||
Z1tZ1, Z1ty = Z1.T @ Z1, Z1.T @ y
|
||
Z2tZ2, Z2ty = Z2.T @ Z2, Z2.T @ y
|
||
XtZ1, XtZ2, Z1tZ2 = X.T @ Z1, X.T @ Z2, Z1.T @ Z2
|
||
|
||
def _mme(la: float, lg: float) -> tuple[np.ndarray, np.ndarray]:
|
||
big = np.zeros((p + n + q, p + n + q))
|
||
big[:p, :p] = XtX
|
||
big[:p, p:p + n] = XtZ1
|
||
big[:p, p + n:] = XtZ2
|
||
big[p:p + n, :p] = XtZ1.T
|
||
big[p:p + n, p:p + n] = Z1tZ1 + la * Ainv
|
||
big[p:p + n, p + n:] = Z1tZ2
|
||
big[p + n:, :p] = XtZ2.T
|
||
big[p + n:, p:p + n] = Z1tZ2.T
|
||
big[p + n:, p + n:] = Z2tZ2 + lg * np.eye(q)
|
||
rhs = np.concatenate([Xty, Z1ty, Z2ty])
|
||
try:
|
||
sol = np.linalg.solve(big, rhs)
|
||
big_inv = np.linalg.inv(big)
|
||
except np.linalg.LinAlgError:
|
||
sol = np.linalg.pinv(big) @ rhs
|
||
big_inv = np.linalg.pinv(big)
|
||
return sol, big_inv
|
||
|
||
# 剖面 REML:对 (log10 σ²a/σ²e, log10 σ²gxe/σ²e) 坐标上升黄金分割最大化。
|
||
# 比率边界与单随机效应分支的 h² 网格一致(λ∈[1e-3,1e3] 恒正),
|
||
# 避免 EM-REML 边界崩塌(σ²e→0 → λ→0 → MME 退化为插值)。
|
||
rank_x = max(np.linalg.matrix_rank(X), 1)
|
||
df = max(m - rank_x, 1)
|
||
|
||
def _profile(ra: float, rg: float) -> tuple[float, float]:
|
||
"""固定比率 (ra,rg)=(σ²a/σ²e, σ²gxe/σ²e),剖面出 σ²e,返回 (REML LL, σ²e)。"""
|
||
V0 = (Z1 @ A @ Z1.T) * ra + (Z2 @ Z2.T) * rg + np.eye(m)
|
||
VinvX = np.linalg.solve(V0, X)
|
||
Vinvy = np.linalg.solve(V0, y)
|
||
XtVinvX = X.T @ VinvX
|
||
XtVinvY = X.T @ Vinvy
|
||
yP0y = float(y @ Vinvy) - float(XtVinvY @ np.linalg.solve(XtVinvX, XtVinvY))
|
||
_, lv = np.linalg.slogdet(V0)
|
||
_, lx = np.linalg.slogdet(XtVinvX)
|
||
ve = max(yP0y / df, _FLOOR)
|
||
ll = -0.5 * (df * np.log(ve) + float(lv) + float(lx))
|
||
return ll, ve
|
||
|
||
RA_LO, RA_HI = np.log10(1e-3), np.log10(1e3)
|
||
best = {"ll": -np.inf, "ra": 1.0, "rg": 1.0, "ve": 1.0}
|
||
ra_l, rg_l = 0.0, 0.0 # 起始比率=1(等方差)
|
||
|
||
def _track(ra: float, rg: float) -> float:
|
||
ll, ve = _profile(ra, rg)
|
||
if ll > best["ll"]:
|
||
best.update(ll=ll, ra=ra, rg=rg, ve=ve)
|
||
return ll
|
||
|
||
def _f_ra(lra: float) -> float:
|
||
return _track(10.0 ** lra, 10.0 ** rg_l)
|
||
|
||
def _f_rg(lrg: float) -> float:
|
||
return _track(10.0 ** ra_l, 10.0 ** lrg)
|
||
|
||
converged = False
|
||
n_iter = 0
|
||
prev_best = -np.inf
|
||
for _ in range(MAX_GXE_ITERS):
|
||
n_iter += 1
|
||
new_ra_l, _ = _golden_max(_f_ra, RA_LO, RA_HI, tol=1e-3)
|
||
ra_l = new_ra_l
|
||
new_rg_l, _ = _golden_max(_f_rg, RA_LO, RA_HI, tol=1e-3)
|
||
rg_l = new_rg_l
|
||
# 坐标上升在似然山脊上 LL 增益趋零即收敛;阈值放宽避免斜坡上反复横跳
|
||
if best["ll"] - prev_best < 1e-2:
|
||
converged = True
|
||
break
|
||
prev_best = best["ll"]
|
||
_track(10.0 ** ra_l, 10.0 ** rg_l)
|
||
|
||
ra, rg, ve = best["ra"], best["rg"], best["ve"]
|
||
va, vg = ra * ve, rg * ve
|
||
la, lg = 1.0 / ra, 1.0 / rg
|
||
sol, big_inv = _mme(la, lg)
|
||
u, v = sol[p:p + n], sol[p + n:]
|
||
C22 = big_inv[p:p + n, p:p + n]
|
||
|
||
ebv = {ind: float(u[idx[ind]]) for ind in order}
|
||
pev = np.diag(C22) * ve
|
||
rel = {ind: float(np.clip(1.0 - pev[idx[ind]] / va, 0.0, 1.0)) for ind in order}
|
||
gxe_eff = {f"{g}|{e}": float(v[i]) for i, (g, e) in enumerate(pair_list)}
|
||
|
||
total = va + vg + ve
|
||
h2 = va / total if total > 0 else None
|
||
gxe_ratio = vg / total if total > 0 else None
|
||
|
||
geno_env: dict[str, set[str]] = {}
|
||
for rid in rec_ids:
|
||
grp, env = str(gxe[rid][0]), str(gxe[rid][1])
|
||
geno_env.setdefault(grp, set()).add(env)
|
||
n_cross_env = sum(1 for s in geno_env.values() if len(s) >= 2)
|
||
n_genotypes = len(geno_env)
|
||
|
||
warning = None
|
||
if not converged:
|
||
warning = f"剖面 REML 坐标上升未完全收敛({factor_label} 似然面可能极平/边界最优),结果采用已探明最优。"
|
||
if n_with_parents == 0:
|
||
w = "无系谱信息:σ²a 与 σ²gxe 的遗传学解释受限(EBV 无加性依据)。"
|
||
warning = w if warning is None else f"{warning} {w}"
|
||
if vg <= _FLOOR * 1.01:
|
||
w = f"{factor_label} 方差分量收敛到 0(数据不支持基因型×{factor_name}互作)。"
|
||
warning = w if warning is None else f"{warning} {w}"
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": rel,
|
||
"h2": float(h2) if h2 is not None else None,
|
||
"sigma_a": float(va),
|
||
"sigma_e": float(ve),
|
||
"sigma_gxe": float(vg),
|
||
"gxe_ratio": float(gxe_ratio) if gxe_ratio is not None else None,
|
||
"gxe_effects": gxe_eff,
|
||
"n_gxe": q,
|
||
"n_cross_env": n_cross_env,
|
||
"n_genotypes": n_genotypes,
|
||
"n_obs": m,
|
||
"n_individuals": n,
|
||
"n_base": n_base,
|
||
"n_with_parents": n_with_parents,
|
||
"n_fixed": n_fixed,
|
||
"converged": converged,
|
||
"n_iter": n_iter,
|
||
"solver": "dense",
|
||
"warning": warning,
|
||
}
|
||
|
||
|
||
def _solve_block(pedigree: list[dict], phenotypes: dict, *, fixed: dict | None,
|
||
covariate: dict | None, block_of: dict, record_map: dict, tol: float,
|
||
factor_label: str = "区组", factor_name: str = "区组") -> dict:
|
||
"""区组随机效应分支:y = Xb + Z₁u + Z₂v + e,u~N(0,A·σ²a)、v~N(0,I·σ²block)、e~N(0,I·σ²e)。
|
||
|
||
为不完全区组设计(增广/α-格子)服务:trial_design 已把 block_no 落库到观测株,
|
||
run_ablup 把区组作第二随机效应,精度收益进入遗传评估。Z₂ 按记录所属区组水平分组
|
||
(block_of: {记录id: 区组水平}),求解器与 G×E 分支同构(剖面 REML 坐标上升估
|
||
(log10 σ²a/σ²e, log10 σ²block/σ²e) 三方差分量),仅 Z₂ 分组与输出键改名。
|
||
"""
|
||
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:
|
||
raise ValueError(f"系谱个体重复: {i}")
|
||
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))
|
||
for pid in phenotypes:
|
||
ind = record_map.get(pid, pid)
|
||
if ind not in seen:
|
||
seen.add(ind)
|
||
base.append(ind)
|
||
if not seen:
|
||
raise ValueError("系谱与表型均为空")
|
||
|
||
order, idx = _order_pedigree(base, non_base)
|
||
n = len(order)
|
||
n_base = len(base)
|
||
parent_of: dict[int, tuple[int | None, int | None]] = {i: (d, s) for (i, d, s) in non_base}
|
||
n_with_parents = len(parent_of)
|
||
|
||
rec_ids = list(phenotypes.keys())
|
||
m = len(rec_ids)
|
||
if m < 2:
|
||
raise ValueError("有效表型个体不足 2 个,无法估计方差组分")
|
||
|
||
A, Ainv = _build_ainv(order, n_base, parent_of)
|
||
X = _build_design(rec_ids, fixed, covariate)
|
||
p = X.shape[1]
|
||
n_fixed = p - 1
|
||
y = np.array([float(phenotypes[i]) for i in rec_ids], dtype=float)
|
||
|
||
Z1 = np.zeros((m, n))
|
||
for k, rid in enumerate(rec_ids):
|
||
Z1[k, idx[record_map.get(rid, rid)]] = 1.0
|
||
|
||
pairs: dict[str, int] = {}
|
||
pair_list: list[str] = []
|
||
for rid in rec_ids:
|
||
key = str(block_of[rid])
|
||
if key not in pairs:
|
||
pairs[key] = len(pair_list)
|
||
pair_list.append(key)
|
||
q = len(pair_list)
|
||
if q < 1:
|
||
raise ValueError("无区组效应水平")
|
||
Z2 = np.zeros((m, q))
|
||
for k, rid in enumerate(rec_ids):
|
||
Z2[k, pairs[str(block_of[rid])]] = 1.0
|
||
|
||
XtX, Xty = X.T @ X, X.T @ y
|
||
Z1tZ1, Z1ty = Z1.T @ Z1, Z1.T @ y
|
||
Z2tZ2, Z2ty = Z2.T @ Z2, Z2.T @ y
|
||
XtZ1, XtZ2, Z1tZ2 = X.T @ Z1, X.T @ Z2, Z1.T @ Z2
|
||
|
||
def _mme(la: float, lg: float) -> tuple[np.ndarray, np.ndarray]:
|
||
big = np.zeros((p + n + q, p + n + q))
|
||
big[:p, :p] = XtX
|
||
big[:p, p:p + n] = XtZ1
|
||
big[:p, p + n:] = XtZ2
|
||
big[p:p + n, :p] = XtZ1.T
|
||
big[p:p + n, p:p + n] = Z1tZ1 + la * Ainv
|
||
big[p:p + n, p + n:] = Z1tZ2
|
||
big[p + n:, :p] = XtZ2.T
|
||
big[p + n:, p:p + n] = Z1tZ2.T
|
||
big[p + n:, p + n:] = Z2tZ2 + lg * np.eye(q)
|
||
rhs = np.concatenate([Xty, Z1ty, Z2ty])
|
||
try:
|
||
sol = np.linalg.solve(big, rhs)
|
||
big_inv = np.linalg.inv(big)
|
||
except np.linalg.LinAlgError:
|
||
sol = np.linalg.pinv(big) @ rhs
|
||
big_inv = np.linalg.pinv(big)
|
||
return sol, big_inv
|
||
|
||
rank_x = max(np.linalg.matrix_rank(X), 1)
|
||
df = max(m - rank_x, 1)
|
||
|
||
def _profile(ra: float, rb: float) -> tuple[float, float]:
|
||
"""固定比率 (ra,rb)=(σ²a/σ²e, σ²block/σ²e),剖面出 σ²e,返回 (REML LL, σ²e)。"""
|
||
V0 = (Z1 @ A @ Z1.T) * ra + (Z2 @ Z2.T) * rb + np.eye(m)
|
||
VinvX = np.linalg.solve(V0, X)
|
||
Vinvy = np.linalg.solve(V0, y)
|
||
XtVinvX = X.T @ VinvX
|
||
XtVinvY = X.T @ Vinvy
|
||
yP0y = float(y @ Vinvy) - float(XtVinvY @ np.linalg.solve(XtVinvX, XtVinvY))
|
||
_, lv = np.linalg.slogdet(V0)
|
||
_, lx = np.linalg.slogdet(XtVinvX)
|
||
ve = max(yP0y / df, _FLOOR)
|
||
ll = -0.5 * (df * np.log(ve) + float(lv) + float(lx))
|
||
return ll, ve
|
||
|
||
RA_LO, RA_HI = np.log10(1e-3), np.log10(1e3)
|
||
best = {"ll": -np.inf, "ra": 1.0, "rb": 1.0, "ve": 1.0}
|
||
ra_l, rb_l = 0.0, 0.0 # 起始比率=1(等方差)
|
||
|
||
def _track(ra: float, rb: float) -> float:
|
||
ll, ve = _profile(ra, rb)
|
||
if ll > best["ll"]:
|
||
best.update(ll=ll, ra=ra, rb=rb, ve=ve)
|
||
return ll
|
||
|
||
def _f_ra(lra: float) -> float:
|
||
return _track(10.0 ** lra, 10.0 ** rb_l)
|
||
|
||
def _f_rb(lrb: float) -> float:
|
||
return _track(10.0 ** ra_l, 10.0 ** lrb)
|
||
|
||
converged = False
|
||
n_iter = 0
|
||
prev_best = -np.inf
|
||
for _ in range(MAX_GXE_ITERS):
|
||
n_iter += 1
|
||
new_ra_l, _ = _golden_max(_f_ra, RA_LO, RA_HI, tol=1e-3)
|
||
ra_l = new_ra_l
|
||
new_rb_l, _ = _golden_max(_f_rb, RA_LO, RA_HI, tol=1e-3)
|
||
rb_l = new_rb_l
|
||
if best["ll"] - prev_best < 1e-2:
|
||
converged = True
|
||
break
|
||
prev_best = best["ll"]
|
||
_track(10.0 ** ra_l, 10.0 ** rb_l)
|
||
|
||
ra, rb, ve = best["ra"], best["rb"], best["ve"]
|
||
va, vb = ra * ve, rb * ve
|
||
la, lb = 1.0 / ra, 1.0 / rb
|
||
sol, big_inv = _mme(la, lb)
|
||
u, v = sol[p:p + n], sol[p + n:]
|
||
C22 = big_inv[p:p + n, p:p + n]
|
||
|
||
ebv = {ind: float(u[idx[ind]]) for ind in order}
|
||
pev = np.diag(C22) * ve
|
||
rel = {ind: float(np.clip(1.0 - pev[idx[ind]] / va, 0.0, 1.0)) for ind in order}
|
||
block_eff = {f"b{pair_list[i]}": float(v[i]) for i in range(q)}
|
||
|
||
total = va + vb + ve
|
||
h2 = va / total if total > 0 else None
|
||
block_ratio = vb / total if total > 0 else None
|
||
|
||
warning = None
|
||
if not converged:
|
||
warning = f"剖面 REML 坐标上升未完全收敛({factor_label} 似然面可能极平/边界最优),结果采用已探明最优。"
|
||
if n_with_parents == 0:
|
||
w = "无系谱信息:σ²a 与 σ²block 的遗传学解释受限(EBV 无加性依据)。"
|
||
warning = w if warning is None else f"{warning} {w}"
|
||
if vb <= _FLOOR * 1.01:
|
||
w = f"{factor_label} 方差分量收敛到 0(数据不支持{factor_name}随机效应)。"
|
||
warning = w if warning is None else f"{warning} {w}"
|
||
return {
|
||
"ebv": ebv,
|
||
"reliability": rel,
|
||
"h2": float(h2) if h2 is not None else None,
|
||
"sigma_a": float(va),
|
||
"sigma_e": float(ve),
|
||
"sigma_block": float(vb),
|
||
"block_ratio": float(block_ratio) if block_ratio is not None else None,
|
||
"block_effects": block_eff,
|
||
"n_blocks": q,
|
||
"n_obs": m,
|
||
"n_individuals": n,
|
||
"n_base": n_base,
|
||
"n_with_parents": n_with_parents,
|
||
"n_fixed": n_fixed,
|
||
"converged": converged,
|
||
"n_iter": n_iter,
|
||
"solver": "dense",
|
||
"warning": warning,
|
||
}
|
||
|
||
|
||
def kfold_cv(pedigree: list[dict], phenotypes: dict, *, k: int = 5,
|
||
fixed: dict | None = None, covariate: dict | None = None,
|
||
gxe: dict | None = None, record_map: dict | None = None,
|
||
tol: float = TOL, seed: int = 20260804) -> dict:
|
||
"""k 折交叉验证(外部验证:预测准确度,区别于 PEV 可靠性)。
|
||
|
||
折划分按**加性个体**分层:优先按父本(sire)家系分组留出(同一家系不跨训练/测试双折,
|
||
避免同家系信息泄漏导致精度高估);无 sire 的个体自成一家系(单个体);家系用固定种子
|
||
shuffle 后 round-robin 分配到 k 折。gxe 分支同一个体多环境记录一起留出。
|
||
|
||
每折:训练子集 re-estimate 方差组分(solve 复用)→ 留出个体 EBV 由系谱预测
|
||
(个体保留在 A 矩阵,仅掩蔽其表型)→ pearson(EBV, 观测表型均值) + RMSE。
|
||
单折失败(训练不足 / G×E 结构不可辨识)→ 该折 pearson=None 并累计 warning,不中断整体。
|
||
|
||
参数: 同 solve();phenotypes 键为记录/个体 id,gxe 分支按 record_map 归一到个体。
|
||
|
||
返回:
|
||
k / n_total / n_individuals: int;
|
||
folds: [{fold, n_train, n_test, n_eval, pearson, rmse, error?, h2?}];
|
||
mean_pearson / mean_rmse: 有效折均值;pooled_pearson / pooled_rmse: 合并全部
|
||
(pred, obs) 对计算;cv_accuracy: 预测准确度(=mean_pearson);
|
||
h2: 有效折均值;warning: str | None(失败折汇总)。
|
||
"""
|
||
if gxe is not None:
|
||
ind_of = {rid: record_map.get(rid, rid) for rid in phenotypes}
|
||
else:
|
||
ind_of = {ind: ind for ind in phenotypes}
|
||
individuals = sorted({v for v in ind_of.values()})
|
||
n_total = len(phenotypes)
|
||
n_individuals = len(individuals)
|
||
if n_individuals < 2 or n_total < 2:
|
||
return {
|
||
"k": k, "n_total": n_total, "n_individuals": n_individuals,
|
||
"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))
|
||
|
||
sire_of: dict = {}
|
||
for rec in pedigree:
|
||
if rec.get("sire") is not None:
|
||
sire_of.setdefault(rec["individual"], rec["sire"])
|
||
fam_of = {ind: sire_of.get(ind, ("self", ind)) for ind in individuals}
|
||
fams = sorted({f for f in fam_of.values()})
|
||
rng = random.Random(seed)
|
||
rng.shuffle(fams)
|
||
fam_bin = {f: i % k for i, f in enumerate(fams)}
|
||
ind_bin = {ind: fam_bin[fam_of[ind]] for ind in individuals}
|
||
|
||
def _subset(bin_idx: int) -> tuple[dict, dict, set[str], set[str]]:
|
||
test_inds = {ind for ind, b in ind_bin.items() if b == bin_idx}
|
||
train_inds = set(individuals) - test_inds
|
||
train_p, test_p = {}, {}
|
||
for key, ind in ind_of.items():
|
||
if ind in train_inds:
|
||
train_p[key] = phenotypes[key]
|
||
else:
|
||
test_p[key] = phenotypes[key]
|
||
return train_p, test_p, train_inds, test_inds
|
||
|
||
folds: list[dict] = []
|
||
all_x: list[float] = []
|
||
all_y: list[float] = []
|
||
for fold in range(k):
|
||
train_p, test_p, train_inds, test_inds = _subset(fold)
|
||
if not test_inds or len(train_inds) < 2:
|
||
folds.append({
|
||
"fold": fold + 1, "n_train": len(train_p), "n_test": len(test_inds),
|
||
"n_eval": 0, "pearson": None, "rmse": None,
|
||
"error": "训练集个体不足 2 或测试集为空",
|
||
})
|
||
continue
|
||
try:
|
||
res = solve(pedigree, train_p, tol=tol, fixed=fixed, covariate=covariate,
|
||
gxe=gxe, record_map=record_map)
|
||
except Exception as e: # noqa: BLE001
|
||
folds.append({
|
||
"fold": fold + 1, "n_train": len(train_p), "n_test": len(test_inds),
|
||
"n_eval": 0, "pearson": None, "rmse": None,
|
||
"error": f"求解失败: {e!s}",
|
||
})
|
||
continue
|
||
ebv = res.get("ebv", {})
|
||
pred: dict[str, list[float]] = {}
|
||
obs: dict[str, list[float]] = {}
|
||
for key, v in test_p.items():
|
||
ind = ind_of[key]
|
||
pv = ebv.get(ind)
|
||
if pv is None:
|
||
continue
|
||
pred.setdefault(ind, []).append(float(pv))
|
||
obs.setdefault(ind, []).append(float(v))
|
||
xs = [sum(pred[i]) / len(pred[i]) for i in pred]
|
||
ys = [sum(obs[i]) / len(obs[i]) for i in obs]
|
||
pearson = _pearson(xs, ys) if len(xs) >= 2 else 0.0
|
||
rmse = (math.sqrt(sum((a - b) ** 2 for a, b in zip(xs, ys)) / len(xs))
|
||
if xs else None)
|
||
if len(xs) >= 2:
|
||
all_x.extend(xs)
|
||
all_y.extend(ys)
|
||
folds.append({
|
||
"fold": fold + 1, "n_train": len(train_p), "n_test": len(test_inds),
|
||
"n_eval": len(xs), "pearson": round(pearson, 4) if xs else None,
|
||
"rmse": round(rmse, 6) if rmse is not None else None,
|
||
"h2": float(res["h2"]) if res.get("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(_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 = f"{len(errors)} 折失败: " + ";".join(errors) if errors else None
|
||
return {
|
||
"k": k, "n_total": n_total, "n_individuals": n_individuals,
|
||
"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, "warning": warning,
|
||
}
|