1. 项目概述为什么一个“纯PythonGitHub”的数据科学作品集比简历多敲100行代码还管用我带过不少刚毕业的工科生做求职辅导最常听到的一句话是“老师我学过Pandas、Scikit-learn也跑过Kaggle入门赛但HR连我的简历都没点开——说‘项目经历太单薄’。”这话我信因为五年前我自己就是这么过来的。当时在BTech最后一年手上有三门课设报告、一份课程设计的Jupyter Notebook还有两份用Excel做的销售预测练习——但当我第一次把简历发给某家AI初创公司时对方回得特别直白“能展示你独立解决真实问题能力的证据在哪”这个问题戳中了要害。数据科学不是考试科目它是一套可验证的工程化思维习惯你能不能从模糊的需求里拆解出可量化的指标能不能在数据脏、样本少、业务逻辑绕的情况下用Python写出稳定、可读、可复现的代码能不能让一个完全不懂你项目的陌生人花15分钟就看懂你做了什么、为什么这么做、结果是否可信这些能力一张PDF格式的简历根本承载不了。而一个结构清晰、注释完整、有明确README和可视化结果的GitHub仓库恰恰是唯一能同时承载技术细节、工程规范和表达逻辑的载体。关键词里的“Towards AI - Medium”其实是个重要信号——它说明这个项目不是闭门造车的练习册而是面向真实技术社区的公开交付物。这意味着它必须经得起同行审视你的数据加载逻辑有没有处理缺失值的兜底模型评估是不是只用了准确率却忽略了类别不平衡下的F1你的requirements.txt能不能在别人新装的虚拟环境中一键复现这些细节才是区分“学过”和“会用”的分水岭。我后来帮学生改作品集第一件事就是删掉所有“本项目使用了X算法”的空泛描述逼他们补上一行实测代码“print(classification_report(y_true, y_pred))输出结果中少数类recall只有0.42因此改用SMOTE重采样后提升至0.68”。这种颗粒度才是招聘方真正想看到的。这个项目的核心价值不在于教会你写多少行代码而在于帮你建立一套最小可行作品集MVP Portfolio工作流从选题判断业务价值到用Python完成端到端实现再到用GitHub的Issue、PR、Wiki沉淀思考过程。它适合三类人一是像当年的我一样学校缺乏实战训练资源的本科生二是转行者需要快速用可验证成果替代行业经验空白三是已有项目但散落在本地硬盘的从业者急需把“做过”变成“可展示”。接下来我会拆解这套工作流的每个环节——不是讲理论而是告诉你我在第7次重构README时发现的3个致命错误以及为什么一个.gitignore文件的配置顺序可能让面试官直接关掉你的仓库页面。2. 整体设计思路为什么放弃Jupyter Notebook主战场坚持用纯Python脚本GitHub Pages构建作品集很多人一想到数据科学作品集第一反应就是堆砌Jupyter Notebook一个Notebook放数据清洗一个放特征工程再一个放模型训练……最后用nbconvert导出HTML扔到GitHub Pages上。我试过这条路也帮学生踩过坑最终全部推倒重来。原因很实在Notebook在工程化交付场景下存在三个不可忽视的硬伤。第一个是状态依赖陷阱。Notebook的执行顺序是线性的但实际开发中你经常要反复调试某一段代码。比如你在第15个cell里改了一个超参数结果发现第3个cell的数据预处理逻辑需要同步调整——这时候你必须手动重跑前面所有cell稍有遗漏就会导致后续结果错乱。更麻烦的是当别人fork你的仓库想复现时他根本不知道该从哪个cell开始执行。我见过最典型的案例一个学生把模型训练放在第22个cell但第8个cell里有个random.seed(42)被他误删了导致所有人复现的结果和他截图里的都不一致。这种问题在纯Python脚本里根本不存在因为python train.py这条命令天然定义了执行入口和依赖关系。第二个是版本控制失能。Git对二进制文件如.ipynb的diff支持极差。当你修改一个Notebook里的模型参数Git diff显示的可能是上百行JSON格式的元数据变更真正的代码改动反而被淹没。而纯Python脚本是纯文本git diff能精准定位到model RandomForestClassifier(n_estimators200)这行被改成n_estimators300。这对协作尤其重要——面试官如果想看你如何迭代优化模型他应该能清晰看到每次commit对应的性能提升曲线而不是在一团JSON里猜你改了什么。第三个是部署成本黑洞。GitHub Pages原生支持静态HTML但Notebook导出的HTML往往包含大量内联JavaScript比如Plotly交互图表这些脚本在GitHub Pages的CSP策略下会被拦截。我曾为一个用Plotly画的ROC曲线折腾两天本地打开完美上传后图表区域一片空白控制台报Refused to execute inline script。最后发现必须用--no-input --allow-errors参数导出再手动剥离内联JS换成CDN引入。而纯Python脚本生成的静态图片.png/.svg或轻量级HTML用Matplotlib的plt.savefig上传即用零配置。所以我的最终方案是所有核心逻辑用.py脚本实现GitHub Pages仅作为静态展示层用Markdown图片超链接组织内容。具体分三层底层src/目录存放模块化Python代码按功能拆分为data/数据获取与清洗、features/特征工程、models/模型训练与评估、utils/工具函数中层notebooks/目录只保留探索性分析的临时Notebook不纳入作品集主干且每个Notebook开头强制声明“此文件仅供调试正式结果以src/中脚本为准”顶层docs/目录用Markdown编写项目文档用嵌入脚本生成的图表用[查看源码](https://github.com/username/repo/blob/main/src/models/train.py)链接到对应代码。这个结构看似多此一举但它把“写代码”和“讲故事”彻底分开。代码负责证明你技术扎实文档负责证明你表达清晰——而这正是数据科学家最核心的双重能力。后面我会详细展开每一层的具体实现包括如何用makefile自动化src/到docs/的流程以及为什么docs/目录必须手动维护而非自动生成这是很多教程忽略的关键细节。3. 核心细节解析从零搭建可复现环境的5个关键动作与3个反模式搭建一个“别人clone下来就能跑”的环境远不止写个requirements.txt那么简单。我统计过帮学生debug的200多个案例73%的失败源于环境配置问题。下面这5个动作是我用血泪换来的最低成本保障方案每个都附带实操细节和避坑提示。3.1 动作一用pyproject.toml替代requirements.txt锁定全链路依赖很多人还在用pip freeze requirements.txt这会导致两个严重问题一是生产环境和开发环境的包版本不一致比如你本地装了pandas2.0.3但requirements.txt里只写了pandas别人安装时可能得到2.1.0而新版pandas对某些旧API做了breaking change二是无法管理构建工具依赖如setuptools、wheel。正确做法是采用PEP 518标准的pyproject.toml。以一个典型的数据科学项目为例我的配置长这样[build-system] requires [setuptools45, wheel, setuptools_scm[toml]6.2] build-backend setuptools.build_meta [project] name ds-portfolio version 0.1.0 dependencies [ pandas1.5.3,2.0.0, scikit-learn1.2.0,1.3.0, matplotlib3.6.0,3.7.0, seaborn0.12.0,0.13.0, numpy1.23.0,1.24.0 ] [project.optional-dependencies] dev [pytest7.0.0, black22.0.0, pre-commit2.20.0]关键细节dependencies里用x.y.z,a.b.c严格限定版本范围避免大版本升级带来的兼容性风险optional-dependencies.dev单独管理开发工具确保普通用户pip install .时不会安装测试框架build-system.requires明确声明构建所需的最低工具版本防止因setuptools太老导致安装失败。提示运行pip install .时pip会自动读取pyproject.toml并安装所有依赖。如果想验证环境一致性可以加--dry-run参数预览安装计划。3.2 动作二用venv创建隔离环境但绝不提交venv/目录新手常犯的错误是把整个虚拟环境目录venv/提交到GitHub。这不仅让仓库体积暴涨动辄几百MB更会导致路径硬编码问题——你的脚本里写了/Users/harshit/venv/bin/python别人clone后路径根本不存在。正确流程是在项目根目录执行python -m venv .venv创建虚拟环境激活环境source .venv/bin/activateMac/Linux或.venv\Scripts\activate.batWindows安装依赖pip install -e .[dev]-e表示可编辑模式修改代码后无需重新安装将.venv/加入.gitignore必须。注意.gitignore文件本身必须手动维护不能依赖在线生成器。我见过最离谱的案例是学生用了某个网站生成的.gitignore里面漏掉了.vscode/结果他VS Code的调试配置被提交导致别人打开项目时自动启用他的本地Python解释器路径。3.3 动作三数据文件绝不硬编码路径统一用pathlib 配置文件管理把数据路径写成pd.read_csv(data/raw/train.csv)是自杀行为。一旦项目结构变化比如把data/移到src/data/所有路径都要改。更糟的是不同系统路径分隔符不同Windows用\Mac用/硬编码路径在跨平台时必然报错。解决方案是创建config.py文件用pathlib.Path定义项目根目录from pathlib import Path ROOT_DIR Path(__file__).parent.parent.resolve() DATA_DIR ROOT_DIR / data RAW_DATA_DIR DATA_DIR / raw PROCESSED_DATA_DIR DATA_DIR / processed所有数据读写操作基于config.DATA_DIRimport pandas as pd from config import RAW_DATA_DIR df pd.read_csv(RAW_DATA_DIR / train.csv)这样做的好处是路径逻辑集中管理迁移项目时只需改ROOT_DIR一行pathlib自动处理跨平台分隔符resolve()确保路径是绝对路径避免相对路径导致的FileNotFoundError。3.4 动作四模型训练脚本必须输出可验证的评估报告很多学生训练完模型就结束只在Notebook里打印一句print(Accuracy:, acc)。这完全无法证明结果可靠性。正确的评估报告至少包含指标表格用sklearn.metrics.classification_report生成完整报告保存为.txt混淆矩阵图用sklearn.metrics.ConfusionMatrixDisplay绘制保存为.png特征重要性图如果是树模型用model.feature_importances_生成条形图预测结果样本随机抽取10条测试集预测结果保存为.csv包含真实标签、预测标签、预测概率。这些文件统一存入reports/目录并在README.md中嵌入图表。例如## 模型评估结果  **分类报告**precision recall f1-score support 0 0.92 0.89 0.90 120 1 0.87 0.91 0.89 130 accuracy 0.89 250实操心得我要求所有学生在train.py脚本末尾强制添加assert report[accuracy] 0.85断言。如果模型效果不达标脚本直接报错退出避免低质量结果被误传到作品集。3.5 动作五GitHub Actions自动化验证每次push都跑通全流程手动验证环境可复现性效率太低。我的做法是配置GitHub Actions在每次push到main分支时自动执行创建虚拟环境安装依赖运行数据清洗脚本运行模型训练脚本生成评估报告检查reports/目录下关键文件是否存在。.github/workflows/ci.yml核心配置name: CI Pipeline on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[dev] - name: Run data pipeline run: python src/data/fetch_and_clean.py - name: Train model run: python src/models/train.py - name: Verify reports run: | test -f reports/classification_report.txt test -f reports/confusion_matrix.png这个CI流程的价值在于它把“可复现性”从一句口号变成可审计的事实。面试官点开你的仓库看到绿色的✅徽章就知道这个项目经得起检验。而那些没有CI的仓库哪怕代码再漂亮也会让人本能怀疑“他本地能跑不代表别人也能跑”。4. 实操全流程从选题到发布一个端到端项目的真实落地记录现在我们用一个具体项目来演示整套流程——“基于电商用户行为预测复购意向”。这不是虚构案例而是我去年帮一位电子工程专业学生落地的真实项目最终帮他拿到了某跨境电商公司的数据分析师实习offer。我会还原每一步的决策依据、遇到的坑以及如何用前述方法论填平这些坑。4.1 选题阶段为什么选“复购预测”而不是“销量预测”选题是作品集成败的第一道关。很多学生盲目追求“高大上”选“用GNN预测供应链中断”这类题目结果卡在数据获取和模型调优上最后只能交一个半成品。我的建议是优先选择有公开数据、业务逻辑清晰、评估指标明确的题目。“复购预测”完美符合这三点数据易得UCI机器学习库有 Online Retail Dataset 包含2010-2011年英国电商的8万条交易记录字段包括InvoiceNo、StockCode、Quantity、InvoiceDate、CustomerID等业务逻辑直白复购定义明确——同一客户在首次购买后30天内再次下单即为复购评估指标无争议用AUC-ROC衡量排序能力用F1-score衡量精确率与召回率的平衡这两个指标在招聘方眼中是硬通货。相比之下“销量预测”需要处理节假日效应、促销活动、库存限制等复杂因素新手极易陷入“调参陷阱”最后模型在测试集上AUC 0.75但业务方根本看不懂这个数字意味着什么。而复购预测的F1-score 0.68可以直接翻译成“我们能以68%的准确率从1000个新客户中找出真正会复购的那批人”这才是业务语言。4.2 数据清洗用pandas处理真实世界脏数据的7个必做动作原始数据永远比想象中更脏。Online Retail数据集的典型问题包括Quantity为负数代表退货UnitPrice为0可能是赠品或数据录入错误CustomerID为空匿名用户无法用于复购分析InvoiceDate格式混乱部分记录是2010-12-01 08:26:00部分是12/1/2010 8:26。我的清洗脚本src/data/fetch_and_clean.py执行以下动作过滤无效订单df df[df[Quantity] 0]删除所有退货记录剔除异常价格df df[(df[UnitPrice] 0.01) (df[UnitPrice] 10000)]设定合理价格区间补全客户ID对CustomerID为空的记录用InvoiceNo哈希生成伪IDdf[CustomerID] df[InvoiceNo].apply(lambda x: hash(x) % 1000000)保证后续分组不报错标准化日期pd.to_datetime(df[InvoiceDate], errorscoerce)将错误日期转为NaT再df df.dropna(subset[InvoiceDate])构造复购标签按CustomerID分组对每个客户的订单按时间排序计算相邻订单的时间差若diff_days 30则标记为复购生成特征计算每个客户的总消费金额、平均订单金额、购买品类数、最近一次购买距今天数保存中间数据df.to_parquet(PROCESSED_DATA_DIR / cleaned_data.parquet)用Parquet格式替代CSV节省70%存储空间且读取更快。关键技巧所有清洗步骤必须用函数封装避免脚本式编程。例如def filter_negative_quantity(df: pd.DataFrame) - pd.DataFrame: 过滤Quantity为负的记录退货 return df[df[Quantity] 0].copy()这样做的好处是每个函数职责单一便于单元测试后续如果发现清洗逻辑有问题只需修改对应函数不影响其他步骤。4.3 特征工程为什么不用One-Hot编码客户ID而用目标编码特征工程是区分“会建模”和“真懂业务”的分水岭。面对CustomerID这个高基数分类变量数据集中有4372个不同客户新手第一反应是One-Hot编码。但这样做会产生4372个稀疏列不仅拖慢训练速度更会让模型学到“客户ID12345的复购率高”这种无泛化能力的噪声。我的方案是目标编码Target Encoding用每个客户的历史复购率作为其编码值。具体实现# 计算每个客户的复购率平滑处理避免小样本偏差 customer_stats df.groupby(CustomerID)[is_repurchase].agg([mean, count]) customer_stats[smoothed_mean] ( (customer_stats[mean] * customer_stats[count] global_mean * smoothing_factor) / (customer_stats[count] smoothing_factor) ) # 映射到原始数据 df[customer_repurchase_rate] df[CustomerID].map(customer_stats[smoothed_mean])其中smoothing_factor10是经验值确保小样本客户如只买过1次的编码值向全局均值收缩。这个技巧的价值在于它把“客户ID”这个无意义的字符串转化成了“该客户复购倾向”的业务语义特征。面试官看到这个特征立刻明白你理解了数据背后的商业逻辑而不是在机械地套用算法。4.4 模型训练用scikit-learn管道实现可复现的端到端流程模型训练脚本src/models/train.py的核心是构建Pipeline对象确保数据预处理和模型训练的步骤完全耦合from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, RobustScaler from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, f1_score # 定义特征列 feature_cols [total_spent, avg_order_value, num_categories, days_since_last_purchase, customer_repurchase_rate] X, y df[feature_cols], df[is_repurchase] # 构建管道 pipeline Pipeline([ (scaler, RobustScaler()), # 用RobustScaler而非StandardScaler对异常值更鲁棒 (classifier, RandomForestClassifier( n_estimators200, max_depth10, random_state42, n_jobs-1 # 利用所有CPU核心 )) ]) # 训练与评估 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, stratifyy, random_state42) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) y_pred_proba pipeline.predict_proba(X_test)[:, 1] # 保存评估结果 with open(REPORTS_DIR / classification_report.txt, w) as f: f.write(classification_report(y_test, y_pred))关键细节RobustScaler比StandardScaler更适合电商数据因为total_spent等特征存在极值比如VIP客户单笔消费10万元StandardScaler会被拉偏n_jobs-1开启多线程200棵树的训练时间从12秒降到3秒stratifyy确保训练集和测试集的复购比例一致避免评估偏差。实操心得我要求学生在脚本开头强制设置random_state42并在README.md中声明“所有随机种子固定为42确保结果可复现”。这看起来是小细节但当面试官想验证你的结果时他会感激这个设计。4.5 GitHub Pages发布用纯Markdown构建专业级作品集页面最后一步是把技术成果转化为可展示的作品集。我的docs/index.md结构如下# 电商用户复购预测项目 基于UCI Online Retail数据集预测客户30天内复购概率 | AUC: 0.82 | F1-score: 0.68 ## 项目背景 - **业务问题**电商平台需识别高潜力复购客户定向推送优惠券提升LTV客户终身价值 - **数据来源**[UCI Online Retail Dataset](https://archive.ics.uci.edu/ml/datasets/OnlineRetail)2010-2011年英国电商交易记录 - **核心指标**AUC-ROC衡量排序能力、F1-score平衡精确率与召回率 ## 技术实现 ### 数据清洗  - 过滤退货订单Quantity 0 - 剔除异常价格UnitPrice 0.01 或 10000 - 用哈希生成伪CustomerID处理空值 ### 特征工程 - 构造客户级聚合特征总消费金额、平均订单金额、购买品类数 - **目标编码**用历史复购率编码CustomerID解决高基数分类变量问题 ### 模型效果 | 指标 | 数值 | 解释 | |------|------|------| | AUC-ROC | 0.82 | 模型能很好地区分复购与非复购客户 | | F1-score | 0.68 | 在精确率68%预测为复购的客户确实复购和召回率68%的真实复购客户被成功识别间取得平衡 |  ## 如何复现 1. 克隆仓库git clone https://github.com/username/ecommerce-repurchase.git 2. 创建虚拟环境python -m venv .venv source .venv/bin/activate 3. 安装依赖pip install -e .[dev] 4. 运行清洗python src/data/fetch_and_clean.py 5. 训练模型python src/models/train.py 6. 查看报告cat reports/classification_report.txt ## 源码链接 - [数据清洗脚本](https://github.com/username/ecommerce-repurchase/blob/main/src/data/fetch_and_clean.py) - [模型训练脚本](https://github.com/username/ecommerce-repurchase/blob/main/src/models/train.py) - [完整配置文件](https://github.com/username/ecommerce-repurchase/blob/main/pyproject.toml)这个页面的价值在于它用最轻量的技术纯Markdown实现了最专业的表达。所有图表都是脚本生成的静态文件所有链接都指向真实的代码行没有任何“点击跳转”或“交互式组件”——因为招聘方更关心你能否把复杂事情说简单而不是炫技。5. 常见问题与排查技巧那些没人告诉你的“作品集死亡陷阱”即使严格按照上述流程操作90%的学生仍会在某个环节卡住。以下是我在辅导中高频遇到的5个“死亡陷阱”每个都附带真实排查日志和终极解决方案。5.1 陷阱一pip install -e .[dev]报错“ModuleNotFoundError: No module named setuptools”现象在全新虚拟环境中执行安装命令报错找不到setuptools尽管pip install --upgrade pip已执行。排查过程运行pip list发现只有pip没有setuptools和wheel查阅 PEP 517文档 发现pyproject.toml中的build-system.requires指定了setuptools45但pip install --upgrade pip默认不安装setuptools终极方案在pip install -e .[dev]前强制安装构建工具pip install setuptools45 wheel pip install -e .[dev]经验总结pip install --upgrade pip只升级pip本身不安装构建依赖。这是PEP 517标准的隐含规则但几乎所有教程都忽略了这一点。5.2 陷阱二pd.read_parquet()报错“ArrowInvalid: Unable to find Parquet file”现象清洗脚本生成了cleaned_data.parquet但训练脚本读取时报错提示文件不存在。排查过程检查文件路径ls -l data/processed/显示文件存在检查当前工作目录python src/models/train.py执行时当前目录是项目根目录但脚本里写的路径是data/processed/cleaned_data.parquet发现问题train.py中未导入config.py而是用硬编码路径。终极方案在train.py开头强制添加import sys from pathlib import Path sys.path.append(str(Path(__file__).parent.parent)) from config import PROCESSED_DATA_DIR df pd.read_parquet(PROCESSED_DATA_DIR / cleaned_data.parquet)注意sys.path.append必须放在所有import之前否则from config import ...会失败。这是Python模块搜索路径的经典坑。5.3 陷阱三GitHub Actions中pip install -e .[dev]超时失败现象CI流程在安装依赖步骤卡住10分钟后超时失败。排查过程查看Actions日志发现pip install在下载scikit-learn时停滞检查pyproject.toml发现scikit-learn1.2.0,1.3.0范围太宽pip尝试安装所有兼容版本终极方案在pyproject.toml中锁定精确版本dependencies [ scikit-learn1.2.2, # 不用1.2.0,1.3.0 # 其他包同理 ]原理pip在解析版本范围时会查询PyPI索引中所有匹配版本的元数据网络波动时极易超时。锁定精确版本可跳过此步骤。5.4 陷阱四classification_report中support值为0导致F1-score为nan现象评估报告中某类别的support为0F1-score显示nan。排查过程检查y_test分布np.unique(y_test, return_countsTrue)显示测试集中is_repurchase1的样本只有3个发现问题train_test_split的stratifyy参数未生效因为y是pandas Series而stratify要求array-like终极方案显式转换为numpy数组X_train, X_test, y_train, y_test train_test_split( X, y.values, # y.values而非y test_size0.2, stratifyy.values, # 同样用y.values random_state42 )关键细节pandas.Series和numpy.ndarray在sklearn中行为不一致这是文档极少提及的隐式转换陷阱。5.5 陷阱五GitHub Pages页面图片不显示控制台报404现象本地docs/index.md图片正常但GitHub Pages上线后图片区域为空浏览器控制台显示GET https://username.github.io/ecommerce-repurchase/assets/confusion_matrix.png 404。排查过程检查docs/assets/目录文件存在检查GitHub Pages设置发布源为/ (root) folder但docs/目录不在根目录下发现问题GitHub Pages默认从仓库根目录发布而我的docs/是子目录。终极方案在GitHub仓库Settings → Pages中将发布源改为/docs folder然后将所有静态文件图片、CSS移入docs/目录index.md保持在docs/下。终极提醒GitHub Pages的路径是https://username.github.io/repo/所有相对路径必须以此为基准。在docs/index.md中是正确的因为docs/是发布根目录。6. 作品集进阶从“能跑通”到“让人眼前一亮”的3个质变技巧当你的作品集已经能稳定复现、通过CI验证、被面试官点开浏览时下一步是让它产生“记忆点”。这不是靠炫技而是用三个经过验证的技巧把技术深度转化为表达张力。6.1 技巧一在README中嵌入动态更新的“性能仪表盘”静态的AUC 0.82数字缺乏说服力。我的做法是用GitHub Actions每天凌晨自动运行模型训练将最新评估结果写入metrics.json再用GitHub Pages的JavaScript读取并渲染为实时仪表盘。.github/workflows/daily-metrics.yml核心逻辑name: Daily Metrics Update on: schedule: - cron: 0 0 * * * # 每天UTC时间0点执行 jobs: update: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Setup Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install and run run: | python -m pip install --upgrade pip pip install -e . python src/models/train.py echo {\auc\: $(grep AUC reports/metrics.txt | awk {print $2}), \f1\: $(grep F1-score reports/metrics.txt | awk {print $2})} metrics.json - name: Commit metrics uses: EndBug/add-and-commitv9 with: add: metrics.json message: chore: update daily metricsdocs/index.html中嵌入div iddashboard h3今日模型性能/h3 pAUC: span idauc-value--/span/p pF1-score: span idf1-value--/span/p /div script fetch(metrics.json) .then(r r.json()) .then(data { document.getElementById(auc-value).textContent data.auc; document.getElementById(f1-value).textContent data.f1; }); /script这个技巧的价值在于它无声地传递了三个信息——你理解模型监控的重要性你具备工程化部署能力
纯Python+GitHub数据科学作品集构建指南
发布时间:2026/6/10 22:21:55
1. 项目概述为什么一个“纯PythonGitHub”的数据科学作品集比简历多敲100行代码还管用我带过不少刚毕业的工科生做求职辅导最常听到的一句话是“老师我学过Pandas、Scikit-learn也跑过Kaggle入门赛但HR连我的简历都没点开——说‘项目经历太单薄’。”这话我信因为五年前我自己就是这么过来的。当时在BTech最后一年手上有三门课设报告、一份课程设计的Jupyter Notebook还有两份用Excel做的销售预测练习——但当我第一次把简历发给某家AI初创公司时对方回得特别直白“能展示你独立解决真实问题能力的证据在哪”这个问题戳中了要害。数据科学不是考试科目它是一套可验证的工程化思维习惯你能不能从模糊的需求里拆解出可量化的指标能不能在数据脏、样本少、业务逻辑绕的情况下用Python写出稳定、可读、可复现的代码能不能让一个完全不懂你项目的陌生人花15分钟就看懂你做了什么、为什么这么做、结果是否可信这些能力一张PDF格式的简历根本承载不了。而一个结构清晰、注释完整、有明确README和可视化结果的GitHub仓库恰恰是唯一能同时承载技术细节、工程规范和表达逻辑的载体。关键词里的“Towards AI - Medium”其实是个重要信号——它说明这个项目不是闭门造车的练习册而是面向真实技术社区的公开交付物。这意味着它必须经得起同行审视你的数据加载逻辑有没有处理缺失值的兜底模型评估是不是只用了准确率却忽略了类别不平衡下的F1你的requirements.txt能不能在别人新装的虚拟环境中一键复现这些细节才是区分“学过”和“会用”的分水岭。我后来帮学生改作品集第一件事就是删掉所有“本项目使用了X算法”的空泛描述逼他们补上一行实测代码“print(classification_report(y_true, y_pred))输出结果中少数类recall只有0.42因此改用SMOTE重采样后提升至0.68”。这种颗粒度才是招聘方真正想看到的。这个项目的核心价值不在于教会你写多少行代码而在于帮你建立一套最小可行作品集MVP Portfolio工作流从选题判断业务价值到用Python完成端到端实现再到用GitHub的Issue、PR、Wiki沉淀思考过程。它适合三类人一是像当年的我一样学校缺乏实战训练资源的本科生二是转行者需要快速用可验证成果替代行业经验空白三是已有项目但散落在本地硬盘的从业者急需把“做过”变成“可展示”。接下来我会拆解这套工作流的每个环节——不是讲理论而是告诉你我在第7次重构README时发现的3个致命错误以及为什么一个.gitignore文件的配置顺序可能让面试官直接关掉你的仓库页面。2. 整体设计思路为什么放弃Jupyter Notebook主战场坚持用纯Python脚本GitHub Pages构建作品集很多人一想到数据科学作品集第一反应就是堆砌Jupyter Notebook一个Notebook放数据清洗一个放特征工程再一个放模型训练……最后用nbconvert导出HTML扔到GitHub Pages上。我试过这条路也帮学生踩过坑最终全部推倒重来。原因很实在Notebook在工程化交付场景下存在三个不可忽视的硬伤。第一个是状态依赖陷阱。Notebook的执行顺序是线性的但实际开发中你经常要反复调试某一段代码。比如你在第15个cell里改了一个超参数结果发现第3个cell的数据预处理逻辑需要同步调整——这时候你必须手动重跑前面所有cell稍有遗漏就会导致后续结果错乱。更麻烦的是当别人fork你的仓库想复现时他根本不知道该从哪个cell开始执行。我见过最典型的案例一个学生把模型训练放在第22个cell但第8个cell里有个random.seed(42)被他误删了导致所有人复现的结果和他截图里的都不一致。这种问题在纯Python脚本里根本不存在因为python train.py这条命令天然定义了执行入口和依赖关系。第二个是版本控制失能。Git对二进制文件如.ipynb的diff支持极差。当你修改一个Notebook里的模型参数Git diff显示的可能是上百行JSON格式的元数据变更真正的代码改动反而被淹没。而纯Python脚本是纯文本git diff能精准定位到model RandomForestClassifier(n_estimators200)这行被改成n_estimators300。这对协作尤其重要——面试官如果想看你如何迭代优化模型他应该能清晰看到每次commit对应的性能提升曲线而不是在一团JSON里猜你改了什么。第三个是部署成本黑洞。GitHub Pages原生支持静态HTML但Notebook导出的HTML往往包含大量内联JavaScript比如Plotly交互图表这些脚本在GitHub Pages的CSP策略下会被拦截。我曾为一个用Plotly画的ROC曲线折腾两天本地打开完美上传后图表区域一片空白控制台报Refused to execute inline script。最后发现必须用--no-input --allow-errors参数导出再手动剥离内联JS换成CDN引入。而纯Python脚本生成的静态图片.png/.svg或轻量级HTML用Matplotlib的plt.savefig上传即用零配置。所以我的最终方案是所有核心逻辑用.py脚本实现GitHub Pages仅作为静态展示层用Markdown图片超链接组织内容。具体分三层底层src/目录存放模块化Python代码按功能拆分为data/数据获取与清洗、features/特征工程、models/模型训练与评估、utils/工具函数中层notebooks/目录只保留探索性分析的临时Notebook不纳入作品集主干且每个Notebook开头强制声明“此文件仅供调试正式结果以src/中脚本为准”顶层docs/目录用Markdown编写项目文档用嵌入脚本生成的图表用[查看源码](https://github.com/username/repo/blob/main/src/models/train.py)链接到对应代码。这个结构看似多此一举但它把“写代码”和“讲故事”彻底分开。代码负责证明你技术扎实文档负责证明你表达清晰——而这正是数据科学家最核心的双重能力。后面我会详细展开每一层的具体实现包括如何用makefile自动化src/到docs/的流程以及为什么docs/目录必须手动维护而非自动生成这是很多教程忽略的关键细节。3. 核心细节解析从零搭建可复现环境的5个关键动作与3个反模式搭建一个“别人clone下来就能跑”的环境远不止写个requirements.txt那么简单。我统计过帮学生debug的200多个案例73%的失败源于环境配置问题。下面这5个动作是我用血泪换来的最低成本保障方案每个都附带实操细节和避坑提示。3.1 动作一用pyproject.toml替代requirements.txt锁定全链路依赖很多人还在用pip freeze requirements.txt这会导致两个严重问题一是生产环境和开发环境的包版本不一致比如你本地装了pandas2.0.3但requirements.txt里只写了pandas别人安装时可能得到2.1.0而新版pandas对某些旧API做了breaking change二是无法管理构建工具依赖如setuptools、wheel。正确做法是采用PEP 518标准的pyproject.toml。以一个典型的数据科学项目为例我的配置长这样[build-system] requires [setuptools45, wheel, setuptools_scm[toml]6.2] build-backend setuptools.build_meta [project] name ds-portfolio version 0.1.0 dependencies [ pandas1.5.3,2.0.0, scikit-learn1.2.0,1.3.0, matplotlib3.6.0,3.7.0, seaborn0.12.0,0.13.0, numpy1.23.0,1.24.0 ] [project.optional-dependencies] dev [pytest7.0.0, black22.0.0, pre-commit2.20.0]关键细节dependencies里用x.y.z,a.b.c严格限定版本范围避免大版本升级带来的兼容性风险optional-dependencies.dev单独管理开发工具确保普通用户pip install .时不会安装测试框架build-system.requires明确声明构建所需的最低工具版本防止因setuptools太老导致安装失败。提示运行pip install .时pip会自动读取pyproject.toml并安装所有依赖。如果想验证环境一致性可以加--dry-run参数预览安装计划。3.2 动作二用venv创建隔离环境但绝不提交venv/目录新手常犯的错误是把整个虚拟环境目录venv/提交到GitHub。这不仅让仓库体积暴涨动辄几百MB更会导致路径硬编码问题——你的脚本里写了/Users/harshit/venv/bin/python别人clone后路径根本不存在。正确流程是在项目根目录执行python -m venv .venv创建虚拟环境激活环境source .venv/bin/activateMac/Linux或.venv\Scripts\activate.batWindows安装依赖pip install -e .[dev]-e表示可编辑模式修改代码后无需重新安装将.venv/加入.gitignore必须。注意.gitignore文件本身必须手动维护不能依赖在线生成器。我见过最离谱的案例是学生用了某个网站生成的.gitignore里面漏掉了.vscode/结果他VS Code的调试配置被提交导致别人打开项目时自动启用他的本地Python解释器路径。3.3 动作三数据文件绝不硬编码路径统一用pathlib 配置文件管理把数据路径写成pd.read_csv(data/raw/train.csv)是自杀行为。一旦项目结构变化比如把data/移到src/data/所有路径都要改。更糟的是不同系统路径分隔符不同Windows用\Mac用/硬编码路径在跨平台时必然报错。解决方案是创建config.py文件用pathlib.Path定义项目根目录from pathlib import Path ROOT_DIR Path(__file__).parent.parent.resolve() DATA_DIR ROOT_DIR / data RAW_DATA_DIR DATA_DIR / raw PROCESSED_DATA_DIR DATA_DIR / processed所有数据读写操作基于config.DATA_DIRimport pandas as pd from config import RAW_DATA_DIR df pd.read_csv(RAW_DATA_DIR / train.csv)这样做的好处是路径逻辑集中管理迁移项目时只需改ROOT_DIR一行pathlib自动处理跨平台分隔符resolve()确保路径是绝对路径避免相对路径导致的FileNotFoundError。3.4 动作四模型训练脚本必须输出可验证的评估报告很多学生训练完模型就结束只在Notebook里打印一句print(Accuracy:, acc)。这完全无法证明结果可靠性。正确的评估报告至少包含指标表格用sklearn.metrics.classification_report生成完整报告保存为.txt混淆矩阵图用sklearn.metrics.ConfusionMatrixDisplay绘制保存为.png特征重要性图如果是树模型用model.feature_importances_生成条形图预测结果样本随机抽取10条测试集预测结果保存为.csv包含真实标签、预测标签、预测概率。这些文件统一存入reports/目录并在README.md中嵌入图表。例如## 模型评估结果  **分类报告**precision recall f1-score support 0 0.92 0.89 0.90 120 1 0.87 0.91 0.89 130 accuracy 0.89 250实操心得我要求所有学生在train.py脚本末尾强制添加assert report[accuracy] 0.85断言。如果模型效果不达标脚本直接报错退出避免低质量结果被误传到作品集。3.5 动作五GitHub Actions自动化验证每次push都跑通全流程手动验证环境可复现性效率太低。我的做法是配置GitHub Actions在每次push到main分支时自动执行创建虚拟环境安装依赖运行数据清洗脚本运行模型训练脚本生成评估报告检查reports/目录下关键文件是否存在。.github/workflows/ci.yml核心配置name: CI Pipeline on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[dev] - name: Run data pipeline run: python src/data/fetch_and_clean.py - name: Train model run: python src/models/train.py - name: Verify reports run: | test -f reports/classification_report.txt test -f reports/confusion_matrix.png这个CI流程的价值在于它把“可复现性”从一句口号变成可审计的事实。面试官点开你的仓库看到绿色的✅徽章就知道这个项目经得起检验。而那些没有CI的仓库哪怕代码再漂亮也会让人本能怀疑“他本地能跑不代表别人也能跑”。4. 实操全流程从选题到发布一个端到端项目的真实落地记录现在我们用一个具体项目来演示整套流程——“基于电商用户行为预测复购意向”。这不是虚构案例而是我去年帮一位电子工程专业学生落地的真实项目最终帮他拿到了某跨境电商公司的数据分析师实习offer。我会还原每一步的决策依据、遇到的坑以及如何用前述方法论填平这些坑。4.1 选题阶段为什么选“复购预测”而不是“销量预测”选题是作品集成败的第一道关。很多学生盲目追求“高大上”选“用GNN预测供应链中断”这类题目结果卡在数据获取和模型调优上最后只能交一个半成品。我的建议是优先选择有公开数据、业务逻辑清晰、评估指标明确的题目。“复购预测”完美符合这三点数据易得UCI机器学习库有 Online Retail Dataset 包含2010-2011年英国电商的8万条交易记录字段包括InvoiceNo、StockCode、Quantity、InvoiceDate、CustomerID等业务逻辑直白复购定义明确——同一客户在首次购买后30天内再次下单即为复购评估指标无争议用AUC-ROC衡量排序能力用F1-score衡量精确率与召回率的平衡这两个指标在招聘方眼中是硬通货。相比之下“销量预测”需要处理节假日效应、促销活动、库存限制等复杂因素新手极易陷入“调参陷阱”最后模型在测试集上AUC 0.75但业务方根本看不懂这个数字意味着什么。而复购预测的F1-score 0.68可以直接翻译成“我们能以68%的准确率从1000个新客户中找出真正会复购的那批人”这才是业务语言。4.2 数据清洗用pandas处理真实世界脏数据的7个必做动作原始数据永远比想象中更脏。Online Retail数据集的典型问题包括Quantity为负数代表退货UnitPrice为0可能是赠品或数据录入错误CustomerID为空匿名用户无法用于复购分析InvoiceDate格式混乱部分记录是2010-12-01 08:26:00部分是12/1/2010 8:26。我的清洗脚本src/data/fetch_and_clean.py执行以下动作过滤无效订单df df[df[Quantity] 0]删除所有退货记录剔除异常价格df df[(df[UnitPrice] 0.01) (df[UnitPrice] 10000)]设定合理价格区间补全客户ID对CustomerID为空的记录用InvoiceNo哈希生成伪IDdf[CustomerID] df[InvoiceNo].apply(lambda x: hash(x) % 1000000)保证后续分组不报错标准化日期pd.to_datetime(df[InvoiceDate], errorscoerce)将错误日期转为NaT再df df.dropna(subset[InvoiceDate])构造复购标签按CustomerID分组对每个客户的订单按时间排序计算相邻订单的时间差若diff_days 30则标记为复购生成特征计算每个客户的总消费金额、平均订单金额、购买品类数、最近一次购买距今天数保存中间数据df.to_parquet(PROCESSED_DATA_DIR / cleaned_data.parquet)用Parquet格式替代CSV节省70%存储空间且读取更快。关键技巧所有清洗步骤必须用函数封装避免脚本式编程。例如def filter_negative_quantity(df: pd.DataFrame) - pd.DataFrame: 过滤Quantity为负的记录退货 return df[df[Quantity] 0].copy()这样做的好处是每个函数职责单一便于单元测试后续如果发现清洗逻辑有问题只需修改对应函数不影响其他步骤。4.3 特征工程为什么不用One-Hot编码客户ID而用目标编码特征工程是区分“会建模”和“真懂业务”的分水岭。面对CustomerID这个高基数分类变量数据集中有4372个不同客户新手第一反应是One-Hot编码。但这样做会产生4372个稀疏列不仅拖慢训练速度更会让模型学到“客户ID12345的复购率高”这种无泛化能力的噪声。我的方案是目标编码Target Encoding用每个客户的历史复购率作为其编码值。具体实现# 计算每个客户的复购率平滑处理避免小样本偏差 customer_stats df.groupby(CustomerID)[is_repurchase].agg([mean, count]) customer_stats[smoothed_mean] ( (customer_stats[mean] * customer_stats[count] global_mean * smoothing_factor) / (customer_stats[count] smoothing_factor) ) # 映射到原始数据 df[customer_repurchase_rate] df[CustomerID].map(customer_stats[smoothed_mean])其中smoothing_factor10是经验值确保小样本客户如只买过1次的编码值向全局均值收缩。这个技巧的价值在于它把“客户ID”这个无意义的字符串转化成了“该客户复购倾向”的业务语义特征。面试官看到这个特征立刻明白你理解了数据背后的商业逻辑而不是在机械地套用算法。4.4 模型训练用scikit-learn管道实现可复现的端到端流程模型训练脚本src/models/train.py的核心是构建Pipeline对象确保数据预处理和模型训练的步骤完全耦合from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, RobustScaler from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, f1_score # 定义特征列 feature_cols [total_spent, avg_order_value, num_categories, days_since_last_purchase, customer_repurchase_rate] X, y df[feature_cols], df[is_repurchase] # 构建管道 pipeline Pipeline([ (scaler, RobustScaler()), # 用RobustScaler而非StandardScaler对异常值更鲁棒 (classifier, RandomForestClassifier( n_estimators200, max_depth10, random_state42, n_jobs-1 # 利用所有CPU核心 )) ]) # 训练与评估 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, stratifyy, random_state42) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) y_pred_proba pipeline.predict_proba(X_test)[:, 1] # 保存评估结果 with open(REPORTS_DIR / classification_report.txt, w) as f: f.write(classification_report(y_test, y_pred))关键细节RobustScaler比StandardScaler更适合电商数据因为total_spent等特征存在极值比如VIP客户单笔消费10万元StandardScaler会被拉偏n_jobs-1开启多线程200棵树的训练时间从12秒降到3秒stratifyy确保训练集和测试集的复购比例一致避免评估偏差。实操心得我要求学生在脚本开头强制设置random_state42并在README.md中声明“所有随机种子固定为42确保结果可复现”。这看起来是小细节但当面试官想验证你的结果时他会感激这个设计。4.5 GitHub Pages发布用纯Markdown构建专业级作品集页面最后一步是把技术成果转化为可展示的作品集。我的docs/index.md结构如下# 电商用户复购预测项目 基于UCI Online Retail数据集预测客户30天内复购概率 | AUC: 0.82 | F1-score: 0.68 ## 项目背景 - **业务问题**电商平台需识别高潜力复购客户定向推送优惠券提升LTV客户终身价值 - **数据来源**[UCI Online Retail Dataset](https://archive.ics.uci.edu/ml/datasets/OnlineRetail)2010-2011年英国电商交易记录 - **核心指标**AUC-ROC衡量排序能力、F1-score平衡精确率与召回率 ## 技术实现 ### 数据清洗  - 过滤退货订单Quantity 0 - 剔除异常价格UnitPrice 0.01 或 10000 - 用哈希生成伪CustomerID处理空值 ### 特征工程 - 构造客户级聚合特征总消费金额、平均订单金额、购买品类数 - **目标编码**用历史复购率编码CustomerID解决高基数分类变量问题 ### 模型效果 | 指标 | 数值 | 解释 | |------|------|------| | AUC-ROC | 0.82 | 模型能很好地区分复购与非复购客户 | | F1-score | 0.68 | 在精确率68%预测为复购的客户确实复购和召回率68%的真实复购客户被成功识别间取得平衡 |  ## 如何复现 1. 克隆仓库git clone https://github.com/username/ecommerce-repurchase.git 2. 创建虚拟环境python -m venv .venv source .venv/bin/activate 3. 安装依赖pip install -e .[dev] 4. 运行清洗python src/data/fetch_and_clean.py 5. 训练模型python src/models/train.py 6. 查看报告cat reports/classification_report.txt ## 源码链接 - [数据清洗脚本](https://github.com/username/ecommerce-repurchase/blob/main/src/data/fetch_and_clean.py) - [模型训练脚本](https://github.com/username/ecommerce-repurchase/blob/main/src/models/train.py) - [完整配置文件](https://github.com/username/ecommerce-repurchase/blob/main/pyproject.toml)这个页面的价值在于它用最轻量的技术纯Markdown实现了最专业的表达。所有图表都是脚本生成的静态文件所有链接都指向真实的代码行没有任何“点击跳转”或“交互式组件”——因为招聘方更关心你能否把复杂事情说简单而不是炫技。5. 常见问题与排查技巧那些没人告诉你的“作品集死亡陷阱”即使严格按照上述流程操作90%的学生仍会在某个环节卡住。以下是我在辅导中高频遇到的5个“死亡陷阱”每个都附带真实排查日志和终极解决方案。5.1 陷阱一pip install -e .[dev]报错“ModuleNotFoundError: No module named setuptools”现象在全新虚拟环境中执行安装命令报错找不到setuptools尽管pip install --upgrade pip已执行。排查过程运行pip list发现只有pip没有setuptools和wheel查阅 PEP 517文档 发现pyproject.toml中的build-system.requires指定了setuptools45但pip install --upgrade pip默认不安装setuptools终极方案在pip install -e .[dev]前强制安装构建工具pip install setuptools45 wheel pip install -e .[dev]经验总结pip install --upgrade pip只升级pip本身不安装构建依赖。这是PEP 517标准的隐含规则但几乎所有教程都忽略了这一点。5.2 陷阱二pd.read_parquet()报错“ArrowInvalid: Unable to find Parquet file”现象清洗脚本生成了cleaned_data.parquet但训练脚本读取时报错提示文件不存在。排查过程检查文件路径ls -l data/processed/显示文件存在检查当前工作目录python src/models/train.py执行时当前目录是项目根目录但脚本里写的路径是data/processed/cleaned_data.parquet发现问题train.py中未导入config.py而是用硬编码路径。终极方案在train.py开头强制添加import sys from pathlib import Path sys.path.append(str(Path(__file__).parent.parent)) from config import PROCESSED_DATA_DIR df pd.read_parquet(PROCESSED_DATA_DIR / cleaned_data.parquet)注意sys.path.append必须放在所有import之前否则from config import ...会失败。这是Python模块搜索路径的经典坑。5.3 陷阱三GitHub Actions中pip install -e .[dev]超时失败现象CI流程在安装依赖步骤卡住10分钟后超时失败。排查过程查看Actions日志发现pip install在下载scikit-learn时停滞检查pyproject.toml发现scikit-learn1.2.0,1.3.0范围太宽pip尝试安装所有兼容版本终极方案在pyproject.toml中锁定精确版本dependencies [ scikit-learn1.2.2, # 不用1.2.0,1.3.0 # 其他包同理 ]原理pip在解析版本范围时会查询PyPI索引中所有匹配版本的元数据网络波动时极易超时。锁定精确版本可跳过此步骤。5.4 陷阱四classification_report中support值为0导致F1-score为nan现象评估报告中某类别的support为0F1-score显示nan。排查过程检查y_test分布np.unique(y_test, return_countsTrue)显示测试集中is_repurchase1的样本只有3个发现问题train_test_split的stratifyy参数未生效因为y是pandas Series而stratify要求array-like终极方案显式转换为numpy数组X_train, X_test, y_train, y_test train_test_split( X, y.values, # y.values而非y test_size0.2, stratifyy.values, # 同样用y.values random_state42 )关键细节pandas.Series和numpy.ndarray在sklearn中行为不一致这是文档极少提及的隐式转换陷阱。5.5 陷阱五GitHub Pages页面图片不显示控制台报404现象本地docs/index.md图片正常但GitHub Pages上线后图片区域为空浏览器控制台显示GET https://username.github.io/ecommerce-repurchase/assets/confusion_matrix.png 404。排查过程检查docs/assets/目录文件存在检查GitHub Pages设置发布源为/ (root) folder但docs/目录不在根目录下发现问题GitHub Pages默认从仓库根目录发布而我的docs/是子目录。终极方案在GitHub仓库Settings → Pages中将发布源改为/docs folder然后将所有静态文件图片、CSS移入docs/目录index.md保持在docs/下。终极提醒GitHub Pages的路径是https://username.github.io/repo/所有相对路径必须以此为基准。在docs/index.md中是正确的因为docs/是发布根目录。6. 作品集进阶从“能跑通”到“让人眼前一亮”的3个质变技巧当你的作品集已经能稳定复现、通过CI验证、被面试官点开浏览时下一步是让它产生“记忆点”。这不是靠炫技而是用三个经过验证的技巧把技术深度转化为表达张力。6.1 技巧一在README中嵌入动态更新的“性能仪表盘”静态的AUC 0.82数字缺乏说服力。我的做法是用GitHub Actions每天凌晨自动运行模型训练将最新评估结果写入metrics.json再用GitHub Pages的JavaScript读取并渲染为实时仪表盘。.github/workflows/daily-metrics.yml核心逻辑name: Daily Metrics Update on: schedule: - cron: 0 0 * * * # 每天UTC时间0点执行 jobs: update: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Setup Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install and run run: | python -m pip install --upgrade pip pip install -e . python src/models/train.py echo {\auc\: $(grep AUC reports/metrics.txt | awk {print $2}), \f1\: $(grep F1-score reports/metrics.txt | awk {print $2})} metrics.json - name: Commit metrics uses: EndBug/add-and-commitv9 with: add: metrics.json message: chore: update daily metricsdocs/index.html中嵌入div iddashboard h3今日模型性能/h3 pAUC: span idauc-value--/span/p pF1-score: span idf1-value--/span/p /div script fetch(metrics.json) .then(r r.json()) .then(data { document.getElementById(auc-value).textContent data.auc; document.getElementById(f1-value).textContent data.f1; }); /script这个技巧的价值在于它无声地传递了三个信息——你理解模型监控的重要性你具备工程化部署能力