739 lines
30 KiB
Python
739 lines
30 KiB
Python
"""育种统计 路由(一期 2 个最基础:ABLUP·EBV + 配合力 GCA/SCA)"""
|
||||
|
|
from typing import Annotated
|
|||
|
|
|
|||
|
|
from fastapi import APIRouter, Body, Depends, Query
|
|||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
|
|
|||
|
|
from app.api.v1.module_bre.statistics.schema import (
|
|||
|
|
AnovaIn,
|
|||
|
|
AnovaOut,
|
|||
|
|
ClusterIn,
|
|||
|
|
CombiningAbilityOut,
|
|||
|
|
CorrelationIn,
|
|||
|
|
CvFoldOut,
|
|||
|
|
CvResultOut,
|
|||
|
|
CvRunIn,
|
|||
|
|
DataQualityIn,
|
|||
|
|
DecisionPreviewIn,
|
|||
|
|
DescribeStatsIn,
|
|||
|
|
GblupRunIn,
|
|||
|
|
GeneticCorrIn,
|
|||
|
|
GeneticCorrOut,
|
|||
|
|
GeneticGainIn,
|
|||
|
|
GenotypingDatasetOut,
|
|||
|
|
GwasQtlXEIn,
|
|||
|
|
GwasResultOut,
|
|||
|
|
GwasRunIn,
|
|||
|
|
GwasSnpOut,
|
|||
|
|
InbreedingDepressionIn,
|
|||
|
|
KinshipIn,
|
|||
|
|
MatingRecommendIn,
|
|||
|
|
MabcIn,
|
|||
|
|
MasPanelIn,
|
|||
|
|
MasPanelOut,
|
|||
|
|
MasPanelSetMarkersIn,
|
|||
|
|
OcsIn,
|
|||
|
|
PredictionOut,
|
|||
|
|
PredictionValueOut,
|
|||
|
|
QtlIn,
|
|||
|
|
QtlOut,
|
|||
|
|
RunStatsIn,
|
|||
|
|
SelectionIndexApplyIn,
|
|||
|
|
SelectionIndexIn,
|
|||
|
|
SelectionIndexOut,
|
|||
|
|
StabilityOut,
|
|||
|
|
StabilityRunIn,
|
|||
|
|
StatisticsJobOut,
|
|||
|
|
TrialDesignIn,
|
|||
|
|
TypeBIn,
|
|||
|
|
TypeBOut,
|
|||
|
|
)
|
|||
|
|
from app.api.v1.module_bre.statistics.service import StatisticsService
|
|||
|
|
from app.core.base_schema import AuthSchema
|
|||
|
|
from app.core.router_class import OperationLogRoute
|
|||
|
|
from app.core.dependencies import db_getter
|
|||
|
|
from app.core.dependencies import AuthPermission
|
|||
|
|
from app.common.response import SuccessResponse
|
|||
|
|
|
|||
|
|
StatisticsRouter = APIRouter(route_class=OperationLogRoute, prefix="/statistics")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/ablup/run", summary="运行 ABLUP/EBV 估计")
|
|||
|
|
async def run_ablup(
|
|||
|
|
data: RunStatsIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_ablup(
|
|||
|
|
data.trait_id, data.trait_code, data.year, data.fixed_effects, data.covariate,
|
|||
|
|
data.data_gate, data.min_clone_n, data.min_pedigree_rate, data.max_missing_rate,
|
|||
|
|
data.gxe, data.gxe_env, data.stage, data.gxr, data.spatial, data.spatial_aniso,
|
|||
|
|
data.block,
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/combining/run", summary="运行配合力 GCA/SCA 分析")
|
|||
|
|
async def run_combining(
|
|||
|
|
data: RunStatsIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(
|
|||
|
|
await StatisticsService(auth, db).run_combining(
|
|||
|
|
data.trait_id, data.trait_code, data.year, data.design_type,
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/predictions", summary="育种值模型列表")
|
|||
|
|
async def list_predictions(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_predictions()
|
|||
|
|
return SuccessResponse([PredictionOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/predictions/{prediction_id}/values", summary="EBV 排行")
|
|||
|
|
async def ebv_ranking(
|
|||
|
|
prediction_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).ebv_ranking(prediction_id)
|
|||
|
|
return SuccessResponse([PredictionValueOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/predictions/{prediction_id}/clones", summary="无性系级 EBV 排行")
|
|||
|
|
async def clone_ranking(
|
|||
|
|
prediction_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
result = await StatisticsService(auth, db).clone_ranking(prediction_id)
|
|||
|
|
return SuccessResponse(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/combining", summary="配合力结果列表")
|
|||
|
|
async def list_combining(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_combining()
|
|||
|
|
return SuccessResponse([CombiningAbilityOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/combining/{ca_id}", summary="配合力结果详情(GCA/SCA)")
|
|||
|
|
async def combining_detail(
|
|||
|
|
ca_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).combining_detail(ca_id)
|
|||
|
|
return SuccessResponse(CombiningAbilityOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/jobs", summary="统计任务列表")
|
|||
|
|
async def list_jobs(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_jobs()
|
|||
|
|
return SuccessResponse([StatisticsJobOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/jobs/{job_id}", summary="统计任务状态")
|
|||
|
|
async def job_status(
|
|||
|
|
job_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).job_status(job_id)
|
|||
|
|
return SuccessResponse(StatisticsJobOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- k-fold 交叉验证(外部验证) ----------
|
|||
|
|
@StatisticsRouter.post("/cv/run", summary="运行 k-fold 交叉验证(外部验证预测准确度)")
|
|||
|
|
async def run_cv(
|
|||
|
|
data: CvRunIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_cv(
|
|||
|
|
data.trait_id, data.trait_code, data.year, data.fixed_effects, data.covariate,
|
|||
|
|
data.gxe, data.gxe_env, data.k,
|
|||
|
|
data.dataset_id, data.method, data.maf_min,
|
|||
|
|
data.split, data.seed,
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/cv", summary="k-fold CV 结果批次列表")
|
|||
|
|
async def list_cv(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_cv()
|
|||
|
|
return SuccessResponse([CvResultOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/cv/{cv_id}", summary="k-fold CV 结果详情(含折明细)")
|
|||
|
|
async def cv_detail(
|
|||
|
|
cv_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
result = await StatisticsService(auth, db).cv_detail(cv_id)
|
|||
|
|
return SuccessResponse({
|
|||
|
|
"result": CvResultOut.model_validate(result["result"]),
|
|||
|
|
"folds": [CvFoldOut.model_validate(f) for f in result["folds"]],
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- 纯 Python 统计分析(不依赖 R) ----------
|
|||
|
|
@StatisticsRouter.get("/describe", summary="描述性统计(无需R)")
|
|||
|
|
async def describe_stats(
|
|||
|
|
trait_codes: list[str] | None = Query(None),
|
|||
|
|
group_by: str = Query("none"),
|
|||
|
|
year: int | None = Query(None),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).describe_stats(trait_codes, group_by, year))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/correlation", summary="性状相关性矩阵(无需R;mode=pheno表型/genetic遗传)")
|
|||
|
|
async def correlation(
|
|||
|
|
trait_codes: list[str] | None = Query(None),
|
|||
|
|
mode: str = Query("pheno", description="pheno=皮尔逊表型相关;genetic=成对双性状BLUP遗传相关"),
|
|||
|
|
g_method: str = Query("mtblup", description="genetic 模式遗传相关来源:mtblup / calo(缺批次回退)"),
|
|||
|
|
year: int | None = Query(None, description="年份过滤(仅 genetic 模式生效)"),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).correlation_matrix(
|
|||
|
|
trait_codes, mode, g_method, year))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/selection-index", summary="选择指数排名(EBV或表型加权,落库)")
|
|||
|
|
async def selection_index(
|
|||
|
|
data: SelectionIndexIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).selection_index(
|
|||
|
|
data.weights, data.year, data.top_n, data.batch_ids, data.use_h2, data.method, data.aggregate,
|
|||
|
|
data.stage, data.g_method, data.auto_weights,
|
|||
|
|
data.restricted_traits, data.economic_weights,
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/selection-index", summary="选择指数批次列表")
|
|||
|
|
async def list_index_batches(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_index_batches()
|
|||
|
|
return SuccessResponse([SelectionIndexOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/selection-index/{index_id}/apply", summary="选择指数前N名写入决选")
|
|||
|
|
async def apply_selection(
|
|||
|
|
index_id: int,
|
|||
|
|
data: SelectionIndexApplyIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(
|
|||
|
|
await StatisticsService(auth, db).apply_selection(
|
|||
|
|
index_id, data.top_n, data.selection_year, data.rule_id, data.min_reliability)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/kinship", summary="亲缘/近交分析(复用系谱A矩阵)")
|
|||
|
|
async def kinship(
|
|||
|
|
data: KinshipIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).kinship_matrix(data.threshold, data.tree_ids))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/inbreeding-depression", summary="近交衰退分析(F→表型线性回归,纯计算)")
|
|||
|
|
async def inbreeding_depression(
|
|||
|
|
data: InbreedingDepressionIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).inbreeding_depression(
|
|||
|
|
data.trait_id, data.trait_code, data.year, data.trial_study_id, data.min_n))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/data-quality", summary="数据质量/异常值诊断(IQR+MAD稳健z)")
|
|||
|
|
async def data_quality(
|
|||
|
|
data: DataQualityIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).data_quality_report(
|
|||
|
|
data.trait_id, data.trait_code, data.year, data.trial_study_id, data.dataset_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/genetic-gain", summary="ΔG 遗传增益投影(截断选择强度×PA×σ_A)")
|
|||
|
|
async def genetic_gain(
|
|||
|
|
data: GeneticGainIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).genetic_gain(
|
|||
|
|
data.trait_id, data.trait_code, data.prediction_id,
|
|||
|
|
data.top_p, data.top_n, data.generation_interval))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/mating-recommend", summary="主动选配推荐(EBV互补−近交惩罚,S-等位硬过滤)")
|
|||
|
|
async def mating_recommend(
|
|||
|
|
data: MatingRecommendIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).mating_recommend(
|
|||
|
|
data.candidate_germplasm_ids, data.prediction_id, data.kinship_threshold,
|
|||
|
|
data.w_ebv, data.w_kin, data.max_pairs))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/ocs", summary="最优贡献选择 OCS(群体配种贡献优化,纯计算)")
|
|||
|
|
async def ocs(
|
|||
|
|
data: OcsIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).ocs(
|
|||
|
|
data.candidate_germplasm_ids, data.n_select, data.lam, data.prediction_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/mabc-progress", summary="MABC 标记辅助回交进度(前景MAS+背景恢复率+回交代建议,纯计算)")
|
|||
|
|
async def mabc_progress(
|
|||
|
|
data: MabcIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).mabc_progress(
|
|||
|
|
data.candidate_tree_ids, data.foreground_panel_ids, data.background_panel_ids,
|
|||
|
|
data.recurrent_parent_tree_id, data.foreground_min_hits, data.generation,
|
|||
|
|
data.background_target))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/trial-design", summary="试验设计生成:RCBD/增广/α-格子(落库 block_no)")
|
|||
|
|
async def trial_design(
|
|||
|
|
data: TrialDesignIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).trial_design(
|
|||
|
|
data.trial_study_id, data.design_type, data.seed,
|
|||
|
|
data.check_germplasm_ids, data.block_size, data.reps))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/anova/run", summary="运行 ANOVA / 广义遗传力 H²")
|
|||
|
|
async def run_anova(
|
|||
|
|
data: AnovaIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_anova(
|
|||
|
|
data.trait_id, data.trait_code, data.year, data.block))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/anova", summary="ANOVA 结果列表")
|
|||
|
|
async def list_anova(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_anova()
|
|||
|
|
return SuccessResponse([AnovaOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/anova/{anova_id}", summary="ANOVA 结果详情")
|
|||
|
|
async def anova_detail(
|
|||
|
|
anova_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).anova_detail(anova_id)
|
|||
|
|
return SuccessResponse(AnovaOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/trait-values", summary="统一性状值视图(长表)")
|
|||
|
|
async def trait_values(
|
|||
|
|
trait_codes: list[str] | None = Query(None),
|
|||
|
|
group_by: str = Query("none"),
|
|||
|
|
year: int | None = Query(None),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).trait_values(trait_codes, group_by, year))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/fairness", summary="按site EBV偏差/公平性报告(只读)")
|
|||
|
|
async def fairness_report(
|
|||
|
|
prediction_id: int = Query(..., description="EBV 预测批次 id"),
|
|||
|
|
group_by: str = Query("site", description="分组维度(当前仅支持 site)"),
|
|||
|
|
threshold_sd: float = Query(1.0, description="偏差标记阈值:|site均值-全体均值| > threshold_sd×全体sd 标记站点"),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).fairness_report(
|
|||
|
|
prediction_id, group_by, threshold_sd))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/predictions/{prediction_id}/compare", summary="模型健康监控(相邻两轮ABLUP对比)")
|
|||
|
|
async def compare_predictions(
|
|||
|
|
prediction_id: int,
|
|||
|
|
prev_prediction_id: int | None = Query(None),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).compare_predictions(prediction_id, prev_prediction_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/decision-preview", summary="选择规则决策预览(表型+EBV)")
|
|||
|
|
async def decision_preview(
|
|||
|
|
data: DecisionPreviewIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(
|
|||
|
|
auth, db).decision_preview(data.rule_ids, data.prediction_id, data.year, data.min_reliability,
|
|||
|
|
data.stage))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/combination-funnel", summary="组合得失漏斗(花→果→种→苗→定植→树→入选,只读)")
|
|||
|
|
async def combination_funnel(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).combination_funnel())
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/traits", summary="统计可用性状下拉(只读bre_trait)")
|
|||
|
|
async def list_traits(
|
|||
|
|
core_only: bool = Query(True, description="仅返回核心性状(对应 tree_evaluation 列)"),
|
|||
|
|
selection_only: bool = Query(False, description="仅返回选种目标性状(into_ebv=1,供指数/EBV排行/决策候选)"),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).list_traits(core_only, selection_only))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- AMMI / Finlay-Wilkinson 稳定性(§8.18) ----------
|
|||
|
|
@StatisticsRouter.post("/stability/run", summary="运行 AMMI/Finlay-Wilkinson 稳定性分析")
|
|||
|
|
async def run_stability(
|
|||
|
|
data: StabilityRunIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_stability(
|
|||
|
|
data.trait_id, data.trait_code, data.gxe_env, data.year, tuple(data.methods),
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/stability", summary="稳定性分析结果列表")
|
|||
|
|
async def list_stability(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_stability()
|
|||
|
|
return SuccessResponse([StabilityOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/stability/{sid}", summary="稳定性分析结果详情(AMMI/FW明细)")
|
|||
|
|
async def stability_detail(
|
|||
|
|
sid: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).stability_detail(sid)
|
|||
|
|
return SuccessResponse(StabilityOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- 遗传相关矩阵(MT-BLUP 成对双性状,§8.18) ----------
|
|||
|
|
@StatisticsRouter.post("/genetic-corr/run", summary="运行遗传相关矩阵(MT-BLUP成对双性状)")
|
|||
|
|
async def run_genetic_corr(
|
|||
|
|
data: GeneticCorrIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_genetic_corr(
|
|||
|
|
data.trait_ids, data.year, data.full_mtblup))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/genetic-corr", summary="遗传相关结果列表")
|
|||
|
|
async def list_genetic_corr(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_genetic_corr()
|
|||
|
|
return SuccessResponse([GeneticCorrOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/genetic-corr/{cid}", summary="遗传相关结果详情")
|
|||
|
|
async def genetic_corr_detail(
|
|||
|
|
cid: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).genetic_corr_detail(cid)
|
|||
|
|
return SuccessResponse(GeneticCorrOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- Type-B 多环境遗传相关 / UPGMA 聚类(§8.27) ----------
|
|||
|
|
@StatisticsRouter.post("/type-b-heredity", summary="运行 Type-B 多环境遗传相关(环境当性状逐对BLUP,落库)")
|
|||
|
|
async def run_type_b_heredity(
|
|||
|
|
data: TypeBIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_type_b_heredity(
|
|||
|
|
data.trait_id, data.trait_code, data.env_dim, data.method, data.year))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/type-b", summary="Type-B 多环境遗传相关结果列表")
|
|||
|
|
async def list_type_b(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_type_b()
|
|||
|
|
return SuccessResponse([TypeBOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/type-b/{rid}", summary="Type-B 多环境遗传相关结果详情")
|
|||
|
|
async def type_b_detail(
|
|||
|
|
rid: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).type_b_detail(rid)
|
|||
|
|
return SuccessResponse(TypeBOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/cluster", summary="UPGMA 层次聚类(计算端点,不落库)")
|
|||
|
|
async def run_cluster(
|
|||
|
|
data: ClusterIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_cluster(
|
|||
|
|
data.trait_ids, data.entity_type, data.mode, data.distance, data.k))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- GBLUP / ssGBLUP 基因组选择(§8.18) ----------
|
|||
|
|
@StatisticsRouter.post("/gblup/run", summary="运行 GBLUP/ssGBLUP 基因组选择")
|
|||
|
|
async def run_gblup(
|
|||
|
|
data: GblupRunIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_gblup(
|
|||
|
|
data.dataset_id, data.trait_id, data.trait_code, data.year, data.method, data.maf_min,
|
|||
|
|
data.seed,
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/gblup/datasets", summary="基因型数据集列表(含样本数)")
|
|||
|
|
async def list_genotyping_datasets(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
datasets = await StatisticsService(auth, db).list_genotyping_datasets()
|
|||
|
|
return SuccessResponse([GenotypingDatasetOut(**d) for d in datasets])
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- MLOps 漂移检测 / 手动重训 / 版本回滚(§8.18) ----------
|
|||
|
|
@StatisticsRouter.get("/model/drift", summary="模型漂移检测(输入快照哈希比对)")
|
|||
|
|
async def model_drift(
|
|||
|
|
prediction_id: int = Query(..., description="预测批次 id"),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).model_drift(prediction_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/model/retrain", summary="手动重训(漂移才重跑run_ablup,active自动转移)")
|
|||
|
|
async def model_retrain(
|
|||
|
|
prediction_id: int = Query(..., description="预测批次 id"),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).model_retrain(prediction_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/model/activate/{prediction_id}", summary="版本回滚(该批次设为当前生效版本)")
|
|||
|
|
async def model_activate(
|
|||
|
|
prediction_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).model_activate(prediction_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- GWAS / QTL / MAS 标记辅助选择(§8.22) ----------
|
|||
|
|
@StatisticsRouter.post("/gwas/run", summary="运行 GWAS 关联分析(GLM+PC)")
|
|||
|
|
async def run_gwas(
|
|||
|
|
data: GwasRunIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).run_gwas(
|
|||
|
|
data.dataset_id, data.trait_id, data.trait_code, data.year,
|
|||
|
|
data.method, data.maf_min, data.n_pc, data.sig_level, data.qtl_window,
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/gwas", summary="GWAS 批次列表")
|
|||
|
|
async def list_gwas(
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).list_gwas()
|
|||
|
|
return SuccessResponse([GwasResultOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/gwas/{gwas_id}", summary="GWAS 批次详情(Manhattan/QQ 数据 + QTL)")
|
|||
|
|
async def gwas_detail(
|
|||
|
|
gwas_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
result = await StatisticsService(auth, db).gwas_detail(gwas_id)
|
|||
|
|
return SuccessResponse({
|
|||
|
|
"result": GwasResultOut.model_validate(result["result"]),
|
|||
|
|
"snps": [GwasSnpOut.model_validate(s) for s in result["snps"]],
|
|||
|
|
"qtls": [QtlOut.model_validate(q) for q in result["qtls"]],
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/gwas-qtl-x-e", summary="QTL×E 分层 GWAS + 稳定性判定(计算端点不建表)")
|
|||
|
|
async def gwas_qtl_x_e(
|
|||
|
|
data: GwasQtlXEIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).gwas_qtl_x_e(
|
|||
|
|
data.dataset_id, data.trait_id, data.trait_code, data.year,
|
|||
|
|
data.method, data.maf_min, data.n_pc, data.sig_level, data.qtl_window,
|
|||
|
|
data.env_dim,
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/qtl", summary="录入已知 QTL")
|
|||
|
|
async def qtl_create(
|
|||
|
|
data: QtlIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).qtl_create(data))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.put("/qtl/{qtl_id}", summary="更新 QTL")
|
|||
|
|
async def qtl_update(
|
|||
|
|
qtl_id: int,
|
|||
|
|
data: QtlIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).qtl_update(qtl_id, data)
|
|||
|
|
return SuccessResponse(QtlOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.delete("/qtl/delete", summary="删除 QTL")
|
|||
|
|
async def qtl_delete(
|
|||
|
|
ids: Annotated[list[int], Body(description="ID列表")],
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:delete"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
await StatisticsService(auth, db).qtl_delete(ids)
|
|||
|
|
return SuccessResponse()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/qtl", summary="QTL 列表(可按性状)")
|
|||
|
|
async def qtl_list(
|
|||
|
|
trait_id: int | None = Query(None, description="按性状过滤"),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).qtl_list(trait_id)
|
|||
|
|
return SuccessResponse([QtlOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/qtl/{qtl_id}", summary="QTL 详情")
|
|||
|
|
async def qtl_detail(
|
|||
|
|
qtl_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).qtl_detail(qtl_id)
|
|||
|
|
return SuccessResponse(QtlOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/mas-panel", summary="创建 MAS 标记辅助选择面板")
|
|||
|
|
async def mas_panel_create(
|
|||
|
|
data: MasPanelIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
return SuccessResponse(await StatisticsService(auth, db).mas_panel_create(data))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.put("/mas-panel/{panel_id}", summary="更新 MAS 面板")
|
|||
|
|
async def mas_panel_update(
|
|||
|
|
panel_id: int,
|
|||
|
|
data: MasPanelIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
obj = await StatisticsService(auth, db).mas_panel_update(panel_id, data)
|
|||
|
|
return SuccessResponse(MasPanelOut.model_validate(obj))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.delete("/mas-panel/delete", summary="删除 MAS 面板")
|
|||
|
|
async def mas_panel_delete(
|
|||
|
|
ids: Annotated[list[int], Body(description="ID列表")],
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:delete"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
await StatisticsService(auth, db).mas_panel_delete(ids)
|
|||
|
|
return SuccessResponse()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/mas-panel", summary="MAS 面板列表(可按性状)")
|
|||
|
|
async def mas_panel_list(
|
|||
|
|
trait_id: int | None = Query(None, description="按性状过滤"),
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
objs = await StatisticsService(auth, db).mas_panel_list(trait_id)
|
|||
|
|
return SuccessResponse([MasPanelOut.model_validate(o) for o in objs])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.get("/mas-panel/{panel_id}", summary="MAS 面板详情(含标记)")
|
|||
|
|
async def mas_panel_detail(
|
|||
|
|
panel_id: int,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:query"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
result = await StatisticsService(auth, db).mas_panel_detail(panel_id)
|
|||
|
|
return SuccessResponse({
|
|||
|
|
"panel": MasPanelOut.model_validate(result["panel"]),
|
|||
|
|
"markers": [m for m in result["markers"]],
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
|
|
@StatisticsRouter.post("/mas-panel/{panel_id}/markers", summary="设置面板标记(全量替换)")
|
|||
|
|
async def mas_panel_set_markers(
|
|||
|
|
panel_id: int,
|
|||
|
|
data: MasPanelSetMarkersIn,
|
|||
|
|
auth: AuthSchema = Depends(AuthPermission(permissions=["module_bre:statistics:create"])),
|
|||
|
|
db: AsyncSession = Depends(db_getter),
|
|||
|
|
):
|
|||
|
|
n = await StatisticsService(auth, db).mas_panel_set_markers(panel_id, data.markers)
|
|||
|
|
return SuccessResponse({"panel_id": panel_id, "n_markers": n})
|