机器学习肝病诊断系统实战:从数据清洗到FastAPI部署 简介这份资源面向机器学习入门者、医学数据挖掘学习者及需要搭建预测系统的开发者围绕印度肝病患者数据集展开肝病智能诊断的完整实践。数据集包含416名肝病患者与167名非肝病患者记录涵盖441名男性与142名女性样本标签列用于区分患病与否适合作为二分类建模与特征分析的练手素材。资源包共114个文件以83个csv数据文件为主辅以8个py脚本、3个html与3个css前端页面、4个xml配置及pkl模型文件等压缩包约8.13MB结构上兼顾数据、模型训练与Web部署。作者实现了ANN模型进行肝病预测并基于Flask框架搭建应用界面输入相关指标即可实时输出预测结果。已有593人学习下载读者可借此掌握从数据预处理、神经网络建模到系统上线的完整链路理解分类模型在医疗辅助诊断场景中的落地方式并参考前端页面与后端接口的配合思路快速复现或二次开发自己的智能诊断系统。1. 从一份肝功能报告说起机器学习怎么把肝病诊断做成能跑的系统体检报告上 ALT、AST、ALB、TBIL 这些指标摆在一起医生扫一眼就能判断倾向但一个刚入行的工程师拿到同样的表格往往只能看出「有几个箭头朝上」。肝病诊断的难点不在于指标多而在于指标之间的非线性组合关系——单个指标轻度异常可能没事三个指标同时往某个方向偏就可能是纤维化信号。传统评分公式比如 FIB-4、APRI本质是线性加权对早期病变的敏感度一直上不去。机器学习切入这件事的价值就在这里它不要求你事先写出公式而是从带标签的病例数据里自己找组合模式。但「训一个模型」和「做一个能用的诊断系统」之间隔着一条河——数据怎么清洗、特征怎么选、模型怎么解释、接口怎么设计、误诊了怎么回溯每一步都能让项目翻车。这篇笔记就按「数据→特征→模型→服务→验证」这条线把基于机器学习的肝病患者智能诊断及系统实现拆开讲清楚适合手里有临床数据集、想把它做成可演示系统的开发者也适合想理解医疗 AI 落地边界的算法同学。2. 数据层肝病数据集怎么清洗成模型能吃的格式2.1 先搞清楚你的数据长什么样肝病相关的公开数据集里最常被拿来练手的是 UCI 的 Indian Liver Patient DatasetILPD583 条记录10 个特征加 1 个标签。特征包括 Age、Gender、Total_Bilirubin、Direct_Bilirubin、Alkaline_Phosphotase、Alamine_Aminotransferase也就是 ALT、Aspartate_AminotransferaseAST、Total_Protiens、Albumin、Albumin_and_Globulin_Ratio。标签是 1肝病患者和 2非患者注意这个编码方式很多教程直接当 0/1 用会出问题。真实医院数据比这个复杂得多但清洗逻辑是相通的。我一般先把字段分成三类连续数值型胆红素、酶类、类别型性别、以及需要计算的衍生型A/G 比值。分类清楚了后面的缺失值处理和标准化才不会一刀切。2.2 缺失值、异常值和类别不平衡的处理顺序处理顺序错了后面全白干。血泪经验是先处理异常值再处理缺失值最后做类别不平衡。原因很简单如果你先填了缺失值异常值检测的分布就被污染了。import pandas as pd import numpy as np from sklearn.impute import KNNImputer # 读取数据注意 ILPD 原始文件没有表头 df pd.read_csv(indian_liver_patient.csv) df.columns [Age,Gender,TB,DB,ALP,ALT,AST,TP,ALB,AGR,Target] # 1. 异常值处理用 IQR 法标记但不直接删先看比例 def iqr_outlier_mask(series): q1, q3 series.quantile(0.25), series.quantile(0.75) iqr q3 - q1 lower, upper q1 - 1.5*iqr, q3 1.5*iqr return (series lower) | (series upper) num_cols [Age,TB,DB,ALP,ALT,AST,TP,ALB,AGR] outlier_ratio {c: iqr_outlier_mask(df[c]).mean() for c in num_cols} print(outlier_ratio) # 超过 5% 的列要谨慎可能是真实重症分布 # 2. 缺失值AGR 列缺失最多用 KNN 填充比均值填充更合理 df[Gender] df[Gender].map({Male:1, Female:0}) imputer KNNImputer(n_neighbors5) df[num_cols] imputer.fit_transform(df[num_cols]) # 3. 标签重编码1-1(患病), 2-0(健康) df[Target] df[Target].apply(lambda x: 1 if x 1 else 0) print(df[Target].value_counts())这段代码的逻辑说明IQR 法先标记异常值但不急着删因为肝病数据里高胆红素本身就是疾病信号删掉等于把正样本扔了。KNNImputer 用 5 个最近邻的加权平均填充比全局均值更贴近个体情况尤其对 AGR 这种和 ALB、TP 强相关的字段。标签重编码那一步是必须的sklearn 的分类器默认把 0 当负类如果保留 1/2 编码混淆矩阵会读得你怀疑人生。参数方面n_neighbors5是经验值样本量小于 500 时可以降到 3大于 2000 可以升到 7。IQR 的 1.5 倍系数是标准做法但医疗数据偏态严重时可以用 3 倍宁可漏标也别误删。2.3 类别不平衡别急着上 SMOTEILPD 里患病和健康的比例大约是 2.5:1不算极端。很多人一看到不平衡就上 SMOTE 合成少数类结果在医疗场景里翻车——合成的样本在特征空间里可能对应根本不存在的病理组合。我一般先试class_weightbalanced让模型在损失函数层面自己调整权重实在不行再考虑 SMOTE而且合成后必须做交叉验证确认没有过拟合。from sklearn.model_selection import train_test_split X df.drop(Target, axis1) y df[Target] # 分层抽样保证训练集和测试集里正负比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) print(f训练集正样本比例: {y_train.mean():.3f}) print(f测试集正样本比例: {y_test.mean():.3f})stratifyy这个参数在医疗数据里是必加的不加的话可能测试集里正样本只有个位数评估结果完全没有统计意义。random_state42是为了复现实际项目里应该跑 5 个不同的随机种子取平均。3. 特征工程与模型选型为什么我不建议一上来就上深度学习3.1 特征筛选从 10 个指标里挑出真正有用的肝病数据的特征不多但冗余度不低。TB 和 DB 高度相关DB 是 TB 的一部分AST 和 ALT 也经常同向变化。全塞给模型不是不行但树模型的特征重要性会被稀释逻辑回归的系数会不稳定。我一般用两步走先算 Spearman 相关系数矩阵把相关系数大于 0.85 的配对拎出来保留临床意义更明确的那个再用随机森林的 feature_importances_ 做二次筛选。from sklearn.ensemble import RandomForestClassifier import seaborn as sns # 第一步相关性筛查 corr_matrix X_train.corr(methodspearman).abs() upper corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k1).astype(bool)) to_drop [col for col in upper.columns if any(upper[col] 0.85)] print(f高相关待删特征: {to_drop}) # 第二步随机森林重要性 rf RandomForestClassifier(n_estimators200, random_state42, class_weightbalanced) rf.fit(X_train, y_train) importance pd.Series(rf.feature_importances_, indexX_train.columns).sort_values(ascendingFalse) print(importance)逻辑说明Spearman 相关系数比 Pearson 更适合医疗数据因为指标分布往往不是正态的。np.triu取上三角是为了避免自己和自己比、以及重复计算。随机森林的n_estimators200是精度和速度的平衡点小于 100 方差大大于 500 收益递减。class_weightbalanced在这里同时解决了不平衡问题。从 ILPD 上的实测结果看ALB、AGR、TB 通常排前三ALT 和 AST 反而没那么靠前——这和很多人的直觉相反但临床上白蛋白反映的是肝脏合成功能比转氨酶更能说明慢性损伤程度。3.2 模型对比逻辑回归、XGBoost 和 SVM 在肝病数据上的表现选型不能只看准确率。医疗场景里召回率Recall比精确率重要漏诊一个肝病患者的代价远大于误诊一个健康人。我一般同时跑三个模型用 5 折交叉验证对比 AUC 和 Recall。模型优点缺点肝病数据上的典型 AUC逻辑回归可解释强系数能直接看非线性关系拟合弱0.72-0.78XGBoost精度高自带特征重要性调参多小样本易过拟合0.80-0.85SVM (RBF核)小样本表现稳概率输出需要额外校准0.76-0.82from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from xgboost import XGBClassifier from sklearn.model_selection import cross_val_score, StratifiedKFold from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) models { LR: Pipeline([(scaler, StandardScaler()), (clf, LogisticRegression(class_weightbalanced, max_iter1000))]), SVM: Pipeline([(scaler, StandardScaler()), (clf, SVC(kernelrbf, probabilityTrue, class_weightbalanced))]), XGB: XGBClassifier(n_estimators300, max_depth4, learning_rate0.05, scale_pos_weight2.5, eval_metriclogloss, random_state42) } for name, model in models.items(): auc cross_val_score(model, X_train, y_train, cvcv, scoringroc_auc) recall cross_val_score(model, X_train, y_train, cvcv, scoringrecall) print(f{name}: AUC{auc.mean():.3f}(±{auc.std():.3f}), Recall{recall.mean():.3f})参数说明逻辑回归的max_iter1000是因为加了 class_weight 后收敛变慢默认的 100 不够。SVM 的probabilityTrue会启用 Platt 校准训练变慢但能输出概率系统里要显示「患病概率 78%」就必须开。XGBoost 的scale_pos_weight2.5对应正负样本比例max_depth4是防止小样本过拟合的关键肝病数据超过 5 层几乎必过拟合。3.3 模型可解释性SHAP 值怎么让医生愿意信你的系统这是整个项目里最容易被忽略、但决定系统能不能落地的一步。医生不会用一个黑匣子你得告诉他「这个患者被判为高风险主要是因为 ALB 偏低贡献了 0.15AGR 偏低贡献了 0.12」。import shap # 用训练好的 XGBoost 模型做解释 explainer shap.TreeExplainer(models[XGB]) shap_values explainer.shap_values(X_test) # 单个患者的解释 patient_idx 0 shap.force_plot(explainer.expected_value, shap_values[patient_idx], X_test.iloc[patient_idx], matplotlibTrue)逻辑说明TreeExplainer 对树模型是精确计算不是近似速度也快。shap_values的维度是样本数特征数每个值表示该特征把预测概率推高或拉低了多少。系统前端可以把 force_plot 转成静态图展示或者只取 top-3 特征做文字说明。注意 SHAP 在类别不平衡数据上会有基线偏移expected_value反映的是训练集的平均预测不是 0.5。展示时要跟医生说明「基线是 62%这个患者被推到 85%」。4. 系统实现从模型文件到能调用的诊断接口4.1 用 FastAPI 把模型包成 REST 接口模型训完存成 pickle 只是第一步系统要实现的是「输入一组指标返回诊断结果和解释」。FastAPI 比 Flask 更适合这个场景自带请求体校验和 Swagger 文档前端联调时省一半沟通成本。from fastapi import FastAPI from pydantic import BaseModel, Field import joblib import numpy as np app FastAPI(title肝病智能诊断接口) model joblib.load(liver_xgb_model.pkl) scaler joblib.load(liver_scaler.pkl) class PatientFeatures(BaseModel): age: int Field(..., ge1, le120, description年龄) gender: int Field(..., ge0, le1, description0女1男) tb: float Field(..., ge0, description总胆红素) db: float Field(..., ge0, description直接胆红素) alp: float Field(..., ge0, description碱性磷酸酶) alt: float Field(..., ge0, description谷丙转氨酶) ast: float Field(..., ge0, description谷草转氨酶) tp: float Field(..., ge0, description总蛋白) alb: float Field(..., ge0, description白蛋白) agr: float Field(..., ge0, description白球比) app.post(/predict) def predict(features: PatientFeatures): arr np.array([[features.age, features.gender, features.tb, features.db, features.alp, features.alt, features.ast, features.tp, features.alb, features.agr]]) arr_scaled scaler.transform(arr) prob model.predict_proba(arr_scaled)[0][1] label 高风险 if prob 0.5 else 低风险 return {risk_label: label, probability: round(float(prob), 4)}逻辑说明Pydantic 的Field校验是必须的临床数据里年龄填 999、胆红素填负数的情况太常见了在入口拦住比在模型里报错好排查。ge0对生化指标是硬约束。返回概率而不是只返回标签是因为医生需要知道置信度0.51 和 0.95 的临床处理完全不同。参数方面阈值 0.5 不是固定的。如果系统定位是筛查工具应该降到 0.35 提高召回率如果是辅助确诊可以升到 0.6 减少假阳性。这个阈值要跟临床方一起定不能工程师拍脑袋。4.2 前端展示与数据存储的最小设计系统实现不需要一上来就搞微服务。一个 Streamlit 页面加 SQLite 就能跑通全流程等验证了价值再重构。import streamlit as st import requests import sqlite3 from datetime import datetime st.title(肝病智能诊断辅助系统) with st.form(patient_form): col1, col2 st.columns(2) age col1.number_input(年龄, 1, 120, 45) gender col1.selectbox(性别, [女, 男]) tb col2.number_input(总胆红素 (μmol/L), 0.0, 500.0, 12.0) alb col2.number_input(白蛋白 (g/L), 0.0, 60.0, 40.0) # 其余字段省略实际项目要补全 submitted st.form_submit_button(开始诊断) if submitted: payload {age: age, gender: 0 if gender 女 else 1, tb: tb, db: tb*0.4, alp: 100.0, alt: 30.0, ast: 28.0, tp: 70.0, alb: alb, agr: alb/30.0} resp requests.post(http://localhost:8000/predict, jsonpayload).json() st.metric(风险等级, resp[risk_label]) st.progress(resp[probability]) # 写入 SQLite 留痕 conn sqlite3.connect(diagnosis_log.db) conn.execute(INSERT INTO records VALUES (?,?,?), (datetime.now().isoformat(), str(payload), resp[probability])) conn.commit()逻辑说明Streamlit 的st.form把多个输入框包在一起避免每改一个值就触发一次重渲染。st.progress直观展示概率比纯数字更容易被非技术用户理解。SQLite 记录每次诊断的输入和输出这是医疗系统的合规要求——任何一次辅助诊断都要能回溯。注意db和agr这里用了简化计算实际系统必须让医生直接输入实测值不能由前端推算否则引入的误差会污染模型输入。5. 避坑与排查肝病诊断系统落地时最容易翻车的 5 个地方5.1 现象模型在测试集上 AUC 0.9上线后医生反馈「不准」原因测试集和训练集来自同一批数据分布一致。上线后遇到的是新时间段、新科室、甚至新检测仪器的数据特征分布漂移了。肝病指标尤其敏感不同医院的 ALP 参考范围能差 30%。解决上线前必须做分布对比用 KS 检验或 PSIPopulation Stability Index检查每个特征的分布差异。PSI 大于 0.2 就要重新校准。系统里加一个「数据漂移监控」定时任务每周跑一次。5.2 现象SHAP 解释里出现「年龄越大风险越低」这种反常识结论原因年龄和患病标签在数据集里存在辛普森悖论。ILPD 里年轻样本多数来自某类特定人群导致模型学到了虚假关联。解决分年龄段做分层分析确认每个年龄段内的特征方向是否一致。如果发现悖论要么在特征工程阶段对年龄做分箱要么在解释时加免责说明。不要为了让解释「好看」而强行改模型。5.3 现象接口返回概率全是 0.5 附近没有区分度原因标准化 scaler 是在包含异常值的数据上 fit 的导致正常样本被压缩到均值附近。或者模型训练时用了scale_pos_weight但预测时忘了同步。解决检查 scaler 的mean_和scale_属性看是否被极端值拉偏。标准化应该在异常值处理之后做而且 scaler 必须和模型一起保存、一起加载。用 joblib 打包成字典{model: model, scaler: scaler}最稳妥。5.4 现象Streamlit 页面输入 10 个指标后点击诊断没反应原因FastAPI 服务没启动或者端口被占用或者 Pydantic 校验失败但前端没显示错误。解决先在浏览器直接访问http://localhost:8000/docs确认服务活着。然后在 Streamlit 里加 try-except 把resp.status_code和resp.text打出来。Pydantic 的校验错误会返回 422 和详细字段信息照着改就行。5.5 现象SQLite 写入越来越慢诊断记录超过 10 万条后页面卡顿原因每次诊断都开新连接、没有索引、没有分页查询。解决连接改成全局单例或连接池在时间戳字段上建索引查询记录时加LIMIT和OFFSET。如果数据量继续涨换 PostgreSQLSQLite 的并发写入本来就是短板。6. 进阶用交叉验证 校准曲线把「概率」变成医生敢用的数字模型输出的 0.78 到底意味着什么如果模型说「78% 概率患病」那在所有被预测为 0.78 的样本里实际患病比例应该接近 78%。很多模型做不到这一点XGBoost 尤其容易把概率推到 0 和 1 两端中间段反而不准。验证方法是用校准曲线Calibration Curve也叫可靠性图。把预测概率分箱横轴是预测均值纵轴是实际正例率理想情况是一条对角线。from sklearn.calibration import calibration_curve, CalibratedClassifierCV import matplotlib.pyplot as plt # 未校准的曲线 prob_uncal models[XGB].predict_proba(X_test)[:, 1] fop_uncal, mpv_uncal calibration_curve(y_test, prob_uncal, n_bins10) # 用 Platt 校准sigmoid或 Isotonic 校准 calibrated CalibratedClassifierCV(models[XGB], methodisotonic, cv5) calibrated.fit(X_train, y_train) prob_cal calibrated.predict_proba(X_test)[:, 1] fop_cal, mpv_cal calibration_curve(y_test, prob_cal, n_bins10) plt.plot(mpv_uncal, fop_uncal, s-, label未校准) plt.plot(mpv_cal, fop_cal, o-, labelIsotonic校准) plt.plot([0,1], [0,1], k--, label理想) plt.legend(); plt.show()逻辑说明n_bins10表示把概率分成 10 段样本量小于 500 时建议降到 5否则每段样本太少曲线会抖得没法看。Isotonic 校准比 Sigmoid 更灵活但需要更多数据样本少于 1000 时优先用 Sigmoid。校准之后还要看 Brier Score它同时衡量了区分度和校准度比单独看 AUC 更全面。Brier Score 越低越好0.25 相当于随机猜0.1 以下算不错。from sklearn.metrics import brier_score_loss print(f未校准 Brier: {brier_score_loss(y_test, prob_uncal):.4f}) print(f校准后 Brier: {brier_score_loss(y_test, prob_cal):.4f})我自己的习惯是任何要展示给医生看的概率都必须过一遍校准并且把校准曲线图放进系统文档里。医生不需要懂 Isotonic 是什么但他需要知道「这个 78% 是经过验证的 78%」。另外校准模型要跟主模型一起版本管理换了训练数据就得重新校准不然概率又会飘。这套流程跑下来从一份 CSV 到一个能演示、能解释、能回溯的诊断系统大概两到三周的工作量。值不值得做如果你的场景里医生需要快速筛查、且你能拿到至少几百条带标签的病例那这套方案的投入产出比是成立的。如果数据量不到 200 条建议先老老实实用 FIB-4 评分等数据攒够了再上模型。希望帮到你。本文还有配套的精品资源点击获取