介绍
社区工作室需要在每场工作坊开始前七天准备材料。用活动后的扫码记录预测到场人数,结果可能十分漂亮,却无法服务这个截止时间。你将先建立简单基线,再判断真正的提前预测允许使用哪些行和列。
你需要掌握 Python 函数、基本终端输入、pandas 选择与连接,以及回归和训练集/测试集的入门概念。Python 和科学计算库已准备就绪。所有表格都是原创合成教学数据;这个小样本演示工作流程,不保证生产预测效果。55 分钟为作者估计,包含阅读和普通错误修正。
用人数衡量简单基线
本步中,你将用早期中位数预测两场后期工作坊,并理解平均绝对误差。
从 WebIDE 开始,打开 Terminal → New Terminal。编辑器与终端共享文件。cd 改变当前终端的工作目录;新终端需要单独执行 cd。
cd /home/labex/project/workshop-attendance
准备好的 ./python 启动器使用本课的 Python 环境。./ 表示“此目录中的可执行文件”。运行课程文件时请使用它,而非系统 Python。cat 显示文本文件而不修改文件。
cat warmup-early.csv
这个小练习中,三场早期活动的实际到场人数为 12、18、24。中位数是排序后居中的值,即 18 人。基线不使用活动特征,对每场后期活动都预测 18 人。
cat warmup-later.csv
后期实际人数为 L-7 的 20 人和 L-2 的 27 人。绝对误差分别为 abs(20 - 18) = 2 和 abs(27 - 18) = 9。平均绝对误差(MAE)是二者的平均值:(2 + 9) / 2 = 5.5 人。“平均误差 5.5 人”具有明确含义;没有单位的“准确率”会掩盖测量的对象。这个小练习与下一步使用的完整季节数据相互独立。
创建可复用的基线函数。shell 重定向 > 会替换 baseline.py。带引号的 here-document <<'PY' 将后续内容原样写入文件,直到仅含 PY 的一行,期间不会发生 shell 展开。请粘贴完整代码块,包括最后的 PY。也可以在 WebIDE 中编辑同一文件,然后选择 File → Save。
运行器把两张 pandas 表传入 summarize。itertuples() 逐行访问带名称的字段;字典将每场后期工作坊的 ID 与其误差关联。float 将 pandas 数值转为普通 Python 数字,便于输出 JSON。
cat > baseline.py <<'PY'
def summarize(early, later):
baseline = float(early.actual_attendance.median())
errors = {
row.session_id: float(abs(row.actual_attendance - baseline))
for row in later.itertuples()
}
return {
"median_attendance": baseline,
"absolute_errors": errors,
"mae": sum(errors.values()) / len(errors),
}
PY
运行准备好的文件写入包装器。参数 baseline 选择当前阶段;它将函数结果保存到 output/baseline.json。JSON 保存命名的值和对象,而不是 CSV 行。
./python run.py baseline
显示的值应包含中位数 18.0、误差 2.0 和 9.0,以及 MAE 5.5。使用准备好的读取器查看保存的产物;它从不重建缺失的结果。
./python show_results.py baseline
现在你拥有了一个误差单位与规划问题相同的基准。之后的模型必须在相同的评估活动上与基线比较;在另一份样本上优于这个练习的数值,并不构成公平比较。
区分训练时间与预测时间
本步中,你将按工作坊 ID 关联结果,并保留较晚日期用于评估。
继续使用同一 WebIDE 终端,工作目录为 /home/labex/project/workshop-attendance。读取季节边界和三张主表。
cat season.json
cat history.csv
cat labels.csv
cat future.csv
数据字典如下:
| 字段 | 含义与可得时点 | 用途 |
|---|---|---|
session_id |
稳定的工作坊身份;文件行序任意 | 关联行,绝不用作模型特征 |
event_date |
计划活动日期,ISO YYYY-MM-DD 格式 |
决定时间切分 |
booked_7d |
恰好在活动前七天保存的预约快照 | 允许的数值特征 |
duration_minutes |
在七天前截止时点已公布的活动时长 | 允许的数值特征 |
final_bookings |
活动发生时的最终预约总数 | 预测截止时尚不可得 |
scanned_entries |
活动发生时采集的入场扫码记录 | 预测截止时尚不可得 |
actual_attendance |
活动当天结束前已知的实际结果 | 训练目标或留出评估标签 |
CSV 中的数值空单元格表示信息缺失,而非零预约。每个允许特征都至少有一个非空训练观测值。每张表内 ID 唯一;labels.csv 对每个历史 ID 恰有一个结果,且特意采用不同的行序。future.csv 没有结果或活动后字段。无效键、训练特征整列为空和滚动更新模型不在本课输入范围内。
模型只训练一次,并在 as_of_date,即 2026 年 4 月 1 日 00:00 UTC 冻结。只有在此日期之前发生的活动才能提供训练标签。本季节最后一场训练活动是 3 月 8 日,所以这些标签已经可得。留出集从 holdout_start,即 4 月 12 日开始;其最早的七天前预测截止日是 4 月 5 日,晚于训练冻结日。提供的历史数据中没有活动处于这段间隔。
这里存在两个不同的时钟:4 月 1 日冻结拟合后的模型;每场后期活动在自己的预测日提供其 booked_7d 快照。4 月 12 日活动的 4 月 5 日快照是在 4 月 5 日使用,并不声称 4 月 1 日已经知道。这项基于文件的练习回放保存的快照。后期真实标签只为回顾性评估而揭示。future.csv 中的五月活动仍是无标签预测。
替换尚未完成的按位置切分。具有一对一关系的 merge 按 session_id 关联标签,不假定第一行必然匹配第一行。pd.to_datetime 解析日历日期,pd.Timestamp 解析配置边界,布尔掩码选择相应行。先按日期、再按 ID 排序方便阅读;ID 拼写从不决定所属时段。.copy() 创建独立的选中表格。
cat > split.py <<'PY'
import pandas as pd
def split_events(history, labels, season):
events = history.merge(labels, on="session_id", validate="one_to_one")
dates = pd.to_datetime(events["event_date"])
train = events.loc[dates < pd.Timestamp(season["as_of_date"])].copy()
holdout = events.loc[dates >= pd.Timestamp(season["holdout_start"])].copy()
return (
train.sort_values(["event_date", "session_id"]),
holdout.sort_values(["event_date", "session_id"]),
)
PY
./python run.py split
训练顺序应为 W-91, W-04, W-77, W-12, W-63, W-02;留出 ID 为 W-88, W-09。这些不规则 ID 说明为什么不能用字母顺序或文件最后两行定义切分。
./python show_results.py split
检查 W-88:它必须保留其匹配标签中的实际人数 46,而不是同行位置上的标签。保存的 training.csv 和 holdout.csv 仍包含活动后字段供检查。下一步只选择两个允许特征;以数值形式保存,并不意味着某列可以合法用于预测。
如果意外关闭终端,打开 Terminal → New Terminal 并重复上面的 cd。“No such file or directory” 通常表示目录错误或文件名拼写错误。用 pwd 显示当前路径,用 ls 列出文件,然后返回此工作区并重跑正确命令。不必为普通路径错误重启实验。
只从训练集学习缺失值均值
本步中,你将在早期工作坊上拟合一个填补器,并将其用于后期快照,不再从后期数据学习。
继续在 WebIDE 的 /home/labex/project/workshop-attendance 下操作。可以编辑 baseline.py、split.py、prepare.py 和自己的 output/ 文件。保留所有提供的 CSV/JSON 输入、python、run.py、coursekit.py、show_results.py 以及每个 starter-*.py。起始副本可用于恢复模块,也可以重新粘贴所教的完整版本。即使日期、行序和数值改变,主要函数也必须仍可复用。
缺失值填补用学到的值替换缺失特征。SimpleImputer(strategy="mean") 学习每列的算术平均值。这里 booked_7d 的训练观测值为 10、20、40、25、35,均值为 26。六个已知时长的平均值为 80 分钟。即使不使用后期结果标签,混合所有时段也会让后期工作坊影响训练选择。应先切分,再拟合。
coursekit 提供的 FEATURES 恰为 ['booked_7d', 'duration_minutes']。双层方括号按此顺序选择两列。fit_transform 学习均值并填补训练矩阵;transform 将相同均值用于留出和未来矩阵。statistics_ 按特征顺序给出学习的均值。返回数组保留每张输入表的行序,以便包装器保留对应 ID。
cat > prepare.py <<'PY'
from sklearn.impute import SimpleImputer
from coursekit import FEATURES
def prepare_features(train, holdout, future):
imputer = SimpleImputer(strategy="mean")
training_values = imputer.fit_transform(train[FEATURES])
holdout_values = imputer.transform(holdout[FEATURES])
future_values = imputer.transform(future[FEATURES])
return training_values, holdout_values, future_values, imputer.statistics_
PY
./python run.py prepare
学习的均值应为 booked_7d: 26.0 和 duration_minutes: 80.0。
./python show_results.py prepare
在 holdout-features.csv 中,W-88 使用 26 个预约和其观测到的 150 分钟;W-09 保留 90 个预约并使用 80 分钟。在 future-features.csv 中,F-101 使用相同的训练预约均值。最终预约、扫码、日期、ID 和结果均不进入这些数值矩阵。源文件中的原始空单元格保持不变。
你已经建立了真正的从早期到后期的评估边界。这并不能证明未来表现与过去相同。下一独立实验将拟合小模型,在相同的有标签留出集上比较连续预测误差与训练中位数基线。只要 ID 关联及数值含义不变,等效计算和不同输出行序均可接受;数值比较允许 1e-8 的绝对与相对容差。
总结
你建立了可复用的中位数基线,以人数为单位计算 MAE,按 ID 连接活动结果,并按日期分开早期训练与后期评估。你只选择七天前已可获得的信息,并仅在训练行上拟合缺失值均值。保存的表格保留了原始身份关联,源数据也保持不变,可用于下一阶段分析。



