介绍
社区工作坊有 A、B 两个版本的报名页面。比较之前,分析者必须先确定什么算一次观察。同一位访客可能访问三次或点击报名两次,另一位访客则可能完全没有产生事件。把这些事件行当成不同的人,会使比较失真。
你将先阅读一个六人示例,再在一个更大的固定实验中,为每位已分组访客建立一个七天内的二元结果。这些记录是原创合成教学数据,表示随机分配的情形,并非实时访客追踪,也不是关于真实业务的证据。独立的 WebIDE 环境已准备好 Python、pandas、NumPy、SciPy 和 Matplotlib。你需要掌握基本终端导航、Python 函数和入门级 pandas 表格筛选;本实验会讲解所需的关联与时间操作。不要求事先了解统计推断。55 分钟是作者估计,包含阅读和普通错误的修正时间。
比较人数,而不是点击次数
本步骤中,你将阅读六张访客卡片,并以已分组的人数为分母计算报名率。
从 WebIDE 开始,打开 Terminal → New Terminal。终端与文件浏览器共享同一组文件。cd 改变当前终端的工作目录;打开另一个终端时,需要再次执行。
cd /home/labex/project/signup-experiment
cat 显示文本文件而不修改它。阅读这个小示例:
cat warmup-visitors.csv
每行代表一位已分组访客。前三位属于 A,后三位属于 B。在这个示例中,qualifying_signup_events 已按每个人的七天窗口完成分类。signed_up 是对应的二元结果:至少发生一次符合条件的报名时为 1,否则为 0。
P-8 访问三次、产生两次报名事件,但只贡献一个人和一个正结果。Q-3 没有访问或报名事件,仍是一位已分组的人,结果为 0。计算前先读完六张卡片:A 的三人中有一人报名,B 的三人中有两人报名。
A 的报名率为 1 / 3,约 33.33%;B 为 2 / 3,约 66.67%。两者之差按 B 减 A 计算,约为 +33.33 个百分点。百分点是两个百分比直接相减的差,不是相对百分比增幅。增加访问次数不会扩大任何一组的分母。
编写可复用的比较函数。运行器会传入名为 people 的 pandas DataFrame,其中包含 visitor_id、group 和 signed_up 列。people["group"] == "A" 这样的布尔掩码会选出 A 组的行。len(a) 统计已分组人数;对二元列求和统计已报名人数。int 和 float 将结果转换为可用于 JSON 文件的普通 Python 数值。
Shell 重定向符 > 会替换指定文件。带引号的 here-document <<'PY' 会把内容写入该文件,直到遇到仅含 PY 的一行,期间不进行 Shell 展开。请粘贴整个代码块,包括最后的 PY。也可以在 WebIDE 中编辑同一文件,再使用 File → Save 保存。
cat > compare.py <<'PY'
def summarize(people):
result = {}
for group in ["A", "B"]:
selected = people[people["group"] == group]
assigned = len(selected)
signed_up = int(selected["signed_up"].sum())
result[group] = {
"assigned": assigned,
"signed_up": signed_up,
"rate_pct": 100 * signed_up / assigned,
}
result["difference_pp"] = result["B"]["rate_pct"] - result["A"]["rate_pct"]
return result
PY
python3 运行 Python。准备好的 run.py 会调用你的函数并写入它实际返回的值;参数 warmup 选择这个输入。运行器不会补齐缺失的答案。
python3 run.py warmup
该命令保存 output/warmup.json。JSON 保存具名对象和数值。准备好的读取器仅显示已经存在的文件,既不重新运行程序,也不修补缺失结果。
python3 show_results.py warmup
确认两组已分组人数均为 3,正结果人数分别为 1 和 2,差值接近 +33.33 个百分点。保存的结果应保留未舍入数值;本说明中的舍入只便于阅读。仅凭观察到的报名率较高,不能认定 B 改善了页面效果。
本实验只需修改 compare.py,以及下一步的 outcomes.py。可以在它们旁边添加普通 Python 辅助文件。保留 assignments.csv、events.csv、warmup-visitors.csv、config.json、data-dictionary.md、run.py 和 show_results.py。其他单元拥有独立副本,不依赖本 VM 的文件。
若文件名或目录有误,用 pwd 查看当前目录,用 ls 列出文件,再执行上面的 cd 返回工作目录。重新复制完整函数块即可修复普通编辑错误,无需重新运行 setup 或重置环境。
建立完整的七天结果
本步骤中,你将把原始分配与事件记录转换为比较所需的完整个人级表格。
继续使用同一个 WebIDE 终端,工作目录为 /home/labex/project/signup-experiment。先阅读字段定义,再查看原始表格:
cat data-dictionary.md
cat assignments.csv
cat events.csv
完整表格有 12 位已分组访客,每组六人。visitor_id 是稳定的文本身份标识,不是行位置。事件表包含同一人的重复记录,还包含无关访客 OUTSIDE,因此不能按行号把两张表配对。本教学示例的日志是完整的:没有符合条件的事件表示没有报名,而非结果未知。
每个人都有一个带时区的分配时刻 assigned_at。窗口为**[分配时刻,分配时刻 + 七天)**:起点计入,精确上边界不计入。UTC 是统一的时间参照;12:00:00+02:00 和 10:00:00Z 表示同一时刻。pd.to_datetime(..., utc=True, format="ISO8601") 把这些明确格式的字符串转换为可比较的 UTC 时间戳。Timedelta(days=7) 表示经过七天的时间长度。
截至给定的分析时刻,所有随访都必须已经结束:
cat config.json
本单元每个窗口都在 analysis_at 之前结束。其余设置将在后续实验中使用。不要修改分析时刻,让尚未结束随访的人看起来已经完成。所有提供的课程表格均有 4–14 人,每组至少两人。重复分配 ID,包括组别冲突的 ID,均无效;挑选能让结果更好看的那一行并不能修复问题。
左关联(left merge)从每条分配记录出发,附加匹配的事件。how="left" 保留没有事件的人。validate="one_to_many" 要求每个 ID 在分配表中至多出现一次,同时允许它有多条事件记录。无关的事件 ID 不会向已分配总体增加人员。
比较操作会生成布尔掩码。在 pandas 中,& 组合多个条件,| 表示满足任一条件;每个比较式都应放在括号内。随后,groupby(...).any() 判断每个人是否至少有一个符合条件的事件。再按 ID 把标记映射回去,就能为每条分配保留一行,包括有效的零结果。ValueError 会明确拒绝无效分配或未完成随访,而不是假装这些数据可以比较。
cat > outcomes.py <<'PY'
import pandas as pd
def build_people(assignments, events, analysis_at):
a = assignments.copy()
if (a["visitor_id"].isna().any()
or (a["visitor_id"] == "").any()
or a["visitor_id"].duplicated().any()):
raise ValueError("Each visitor needs one unique, nonempty assignment ID.")
if not a["group"].isin(["A", "B"]).all():
raise ValueError("Only groups A and B are valid.")
a["assigned_at"] = pd.to_datetime(a["assigned_at"], utc=True, format="ISO8601")
a["window_end"] = a["assigned_at"] + pd.Timedelta(days=7)
if not (a["window_end"] <= pd.Timestamp(analysis_at)).all():
raise ValueError("Every seven-day follow-up must be complete.")
e = events.copy()
e["occurred_at"] = pd.to_datetime(e["occurred_at"], utc=True, format="ISO8601")
joined = a.merge(e, on="visitor_id", how="left", validate="one_to_many")
eligible = ((joined["kind"] == "signup")
& (joined["occurred_at"] >= joined["assigned_at"])
& (joined["occurred_at"] < joined["window_end"]))
flags = eligible.groupby(joined["visitor_id"]).any()
a["signed_up"] = a["visitor_id"].map(flags).astype(int)
return a[["visitor_id", "group", "signed_up"]]
PY
左关联中未匹配到事件的记录,其事件字段为空。它的资格比较结果为 false,因此该人的结果为 0。这并不是把未知分配或未完成的随访填成零。
下一条命令故意拼错阶段名称。预期会显示 invalid choice 消息,并以状态 2 退出。这是命令参数错误,没有使数据失效,也没有修改数据。
python3 run.py pepole
改正阶段名称,保存个人级表格:
python3 run.py people
python3 show_results.py people
结果应有 12 个不同 ID。检查 P-8(两次符合条件的事件,但结果为 1)、Q-3(无事件,结果为 0)、M-90(报名恰好发生在被排除的上边界,结果为 0),以及 K-4(在上边界前一秒报名,结果为 1)。X-02 在分配前报名,仍为 0。结果中不得出现 OUTSIDE。
现在在这个真实保存的表格上复用比较函数:
python3 run.py summary
python3 show_results.py summary
完整实验中,A 的六人有一人报名,B 的六人有五人报名,差值约为 +66.67 个百分点。现在你已经有明确的分母与窗口,但还不能声称新页面造成了改善。接下来的实验会考察随机分配如何改变观察到的差值。
检查会读取已保存的作品,并仅在符合相同公开契约的隔离副本上调用原样函数。它们覆盖两张输入表各自独立的行顺序、其他 ID、无事件输入,以及无效分配和随访。等效 pandas 或 Python 实现均可,只要 build_people 返回具有相同列名的 DataFrame,且 summarize 返回规定的对象。重复检查不会重建现场结果,也不会改变进度。
总结
你比较了六张访客卡片,编写了可复用的报名率比较函数,并从原始事件建立了完整七天结果。稳定 ID、明确的 UTC 窗口和已分组人数分母,使重复点击与缺失事件不会扭曲比较。保存的个人表与汇总现在可以支持对随机波动的诚实分析。



