{"cells":[{"cell_type":"markdown","metadata":{},"source":["# 从零做 DID：手算、回归与独立迁移\n","\n","本文件把原Python脚本按完整语法块分段呈现；计算语句来自同一份脚本。请从头顺序运行，先保留原始材料，再在副本改做。非Python主课中的本笔记本仅作可选独立复核，不能代替原生软件操作。\n","\n","启动前阅读同目录《五步学习说明.txt》。使用本课配置的Python内核；不要另装一套Python。"],"id":"cell-0"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pathlib import Path\n","import sys\n","课程目录 = Path.cwd().resolve()\n","assert (课程目录 / '课堂练习.py').is_file(), \"请在完整解压的本课目录打开笔记本\"\n","__file__ = str(课程目录 / '课堂练习.py')\n","__name__ = \"__main__\"\n","sys.argv = [__file__]\n","sys.path.insert(0, str(课程目录))\n","print(\"本课目录：\", 课程目录)\n","print(\"Python：\", sys.executable)\n"],"id":"cell-1"},{"cell_type":"markdown","metadata":{},"source":["## 原脚本第 1–6 行\n","\n","阅读下面这段初始化、函数定义或计算代码，运行后再继续。完整语法块不会拆开。"],"id":"cell-2"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pathlib import Path\n","import csv\n","import json\n","from collections import defaultdict\n","import numpy as np\n","\n"],"id":"cell-3"},{"cell_type":"markdown","metadata":{},"source":["## 原脚本第 7–21 行\n","\n","**核对四格输入与分母**\n","\n","读取四个组别与时期组合，先验证唯一键和正分母，再把总量除以该格门店数。\n","\n","核对：四格齐全；新泽西政策前均值为20.43940810。\n","\n","**手算两次差与反事实**\n","\n","按同一方向算政策后减政策前，再算处理变化减对照变化；同时计算新泽西的反事实后期均值。\n","\n","核对：处理变化约0.58802137，对照变化约-2.16558442，DID约2.75360578。"],"id":"cell-4"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["目录 = Path(__file__).resolve().parent\n","\n","def 读表(名称):\n","    with (目录/名称).open(encoding=\"utf-8-sig\",newline=\"\") as 文件:\n","        return list(csv.DictReader(文件))\n","\n","def 写表(名称,列名,行):\n","    with (目录/名称).open(\"w\",encoding=\"utf-8-sig\",newline=\"\") as 文件:\n","        写入=csv.writer(文件);写入.writerow(列名);写入.writerows(行)\n","\n","# 步骤一：四行是组别与时期的汇总，不是四家门店\n","原表=读表(\"两州就业汇总.csv\")\n","assert len(原表)==4\n","assert {(r[\"州别\"],r[\"调查时期\"]) for r in 原表}=={\n","    (州,期) for 州 in [\"新泽西州\",\"宾夕法尼亚州东部\"] for 期 in [\"政策前\",\"政策后\"]}\n"],"id":"cell-5"},{"cell_type":"markdown","metadata":{},"source":["## 原脚本第 22–39 行\n","\n","**手算两次差与反事实**\n","\n","按同一方向算政策后减政策前，再算处理变化减对照变化；同时计算新泽西的反事实后期均值。\n","\n","核对：处理变化约0.58802137，对照变化约-2.16558442，DID约2.75360578。\n","\n","**用交互回归核对**\n","\n","模型包含截距、组别、后期和交互项；以对照组政策前为基准。只读取点估计，检查四格的零残差自由度。\n","\n","核对：交互项与手算DID相同，残差自由度为0。"],"id":"cell-6"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["四格={}\n","汇总=[]\n","for r in 原表:\n","    人数=int(r[\"有效门店数\"]);总量=float(r[\"全职当量合计\"])\n","    assert 人数>0 and np.isfinite(总量)\n","    t=int(r[\"州别\"]==\"新泽西州\");p=int(r[\"调查时期\"]==\"政策后\")\n","    四格[t,p]=总量/人数\n","    汇总.append([r[\"州别\"],r[\"调查时期\"],人数,总量,四格[t,p]])\n","\n","# 步骤二：两次相减与反事实\n","处理变化=四格[1,1]-四格[1,0]\n","对照变化=四格[0,1]-四格[0,0]\n","反事实=四格[1,0]+对照变化\n","DID=处理变化-对照变化\n","assert abs(DID-2.75360578298058)<1e-10\n","assert abs(四格[1,1]-反事实-DID)<1e-12\n","\n","# 步骤三：四格回归仅核对点估计\n"],"id":"cell-7"},{"cell_type":"markdown","metadata":{},"source":["## 原脚本第 40–48 行\n","\n","**用交互回归核对**\n","\n","模型包含截距、组别、后期和交互项；以对照组政策前为基准。只读取点估计，检查四格的零残差自由度。\n","\n","核对：交互项与手算DID相同，残差自由度为0。"],"id":"cell-8"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["组合=sorted(四格)\n","X=np.array([[1,t,p,t*p] for t,p in 组合],dtype=float)\n","y=np.array([四格[k] for k in 组合])\n","系数=np.linalg.solve(X,y)\n","自由度=len(y)-np.linalg.matrix_rank(X)\n","assert abs(系数[3]-DID)<1e-12 and 自由度==0\n","\n","# 步骤四：只改变假设的对照组趋势，原始调查文件保持不变\n","情景=读表(\"反事实情景.csv\")\n"],"id":"cell-9"},{"cell_type":"markdown","metadata":{},"source":["## 原脚本第 49–58 行\n","\n","**改变反事实情景**\n","\n","从独立情景CSV读取假设的对照组变化。它只改变课堂反事实，原真实调查汇总没有被覆盖。\n","\n","核对：假设变化为-1时，情景DID约1.58802137，反事实后期均值约19.43940810。"],"id":"cell-10"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["assert len(情景)==1\n","假设变化=float(情景[0][\"假设对照组变化\"])\n","情景DID=处理变化-假设变化\n","情景反事实=四格[1,0]+假设变化\n","assert np.isfinite(假设变化) and abs(四格[1,1]-情景反事实-情景DID)<1e-12\n","if 假设变化 == -1:\n","    assert abs(情景DID-1.58802136739616)<1e-10\n","\n","# 步骤五：用另一份明确模拟的门店面板独立迁移\n","面板=读表(\"独立练习_模拟门店.csv\")\n"],"id":"cell-11"},{"cell_type":"markdown","metadata":{},"source":["## 原脚本第 59–71 行\n","\n","**换数据独立迁移**\n","\n","读取新的模拟门店面板，检查门店与时期唯一、组别不变、两期齐全，再形成四格均值。\n","\n","核对：16条门店-时期记录、8家门店、每格4家，四格均值为13、18、11、13。"],"id":"cell-12"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["assert len(面板)==16 and len({(r[\"门店编号\"],r[\"时期\"]) for r in 面板})==16\n","门店=defaultdict(set);分格=defaultdict(list)\n","for r in 面板:\n","    assert r[\"组别\"] in {\"试点组\",\"参照组\"} and r[\"时期\"] in {\"政策前\",\"政策后\"}\n","    门店[r[\"门店编号\"]].add((r[\"组别\"],r[\"时期\"]))\n","    分格[r[\"组别\"],r[\"时期\"]].append(float(r[\"全职当量\"]))\n","assert len(门店)==8 and all(len(v)==2 and len({x[0] for x in v})==1 for v in 门店.values())\n","assert all(len(v)==4 for v in 分格.values())\n","均值={k:sum(v)/len(v) for k,v in 分格.items()}\n","模拟处理变化=均值[\"试点组\",\"政策后\"]-均值[\"试点组\",\"政策前\"]\n","模拟对照变化=均值[\"参照组\",\"政策后\"]-均值[\"参照组\",\"政策前\"]\n","模拟DID=模拟处理变化-模拟对照变化\n","assert [均值[\"试点组\",\"政策前\"],均值[\"试点组\",\"政策后\"],均值[\"参照组\",\"政策前\"],均值[\"参照组\",\"政策后\"]]==[13,18,11,13]\n"],"id":"cell-13"},{"cell_type":"markdown","metadata":{},"source":["## 原脚本第 72–112 行\n","\n","**保存可核对成果**\n","\n","将计算结果写成CSV；Python同时绘制趋势与反事实图。Stata输出两份对应软件结果表及文本日志。\n","\n","核对：模拟DID为3；查看导出的四格结果和记录性质，图的纵轴单位是每店平均全职当量。"],"id":"cell-14"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["assert 模拟DID==3 and 模拟处理变化==5 and 模拟对照变化==2\n","\n","# 步骤六：导出核对表和有单位、刻度的趋势图\n","写表(\"四格均值结果.csv\",[\"州别\",\"调查时期\",\"有效门店数\",\"全职当量合计\",\"每店平均全职当量\"],汇总)\n","写表(\"独立练习四格结果.csv\",[\"组别\",\"时期\",\"记录数\",\"每店平均全职当量\"],[[*k,len(分格[k]),v] for k,v in sorted(均值.items())])\n","核对={\"follow\":{\"treated_change\":处理变化,\"control_change\":对照变化,\"did\":DID},\n","      \"change\":{\"counterfactual\":情景反事实,\"did\":情景DID},\n","      \"transfer\":{\"observations\":len(面板),\"treated_change\":模拟处理变化,\"did\":模拟DID}}\n","(目录/\"作业数值核对.json\").write_text(json.dumps(核对,ensure_ascii=False,indent=2),\"utf-8\")\n","写表(\"DID核对结果.csv\",[\"案例\",\"处理组变化\",\"对照组变化\",\"DID\",\"性质\"],\n","     [[\"公开调查四格\",处理变化,对照变化,DID,\"真实调查汇总\"],\n","      [\"改变反事实趋势\",处理变化,假设变化,情景DID,\"课堂假设\"],\n","      [\"另一组门店\",模拟处理变化,模拟对照变化,模拟DID,\"原创模拟\"]])\n","def Y(v):return 330-(v-16)*24\n","图=['<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 800 390\">',\n","    '<rect width=\"800\" height=\"390\" fill=\"white\"/>',\n","    '<g font-family=\"Microsoft YaHei, sans-serif\" font-size=\"13\" fill=\"#333\">',\n","    '<text x=\"35\" y=\"28\">每店平均全职当量</text>']\n","for v in [16,18,20,22,24,26]:\n","    yy=Y(v);图.append(f'<path d=\"M100 {yy}H585\" stroke=\"#ddd\"/><text x=\"83\" y=\"{yy+4}\" text-anchor=\"end\">{v}</text>')\n","for group,color,dash in [(1,\"#222\",\"\"),(0,\"#777\",'stroke-dasharray=\"7 5\"')]:\n","    a,b=四格[group,0],四格[group,1]\n","    图.append(f'<path d=\"M200 {Y(a)}L550 {Y(b)}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"2.5\" {dash}/>')\n","    for x,v in [(200,a),(550,b)]:图.append(f'<circle cx=\"{x}\" cy=\"{Y(v)}\" r=\"4\" fill=\"{color}\"/>')\n","图 += [f'<path d=\"M200 {Y(四格[1,0])}L550 {Y(反事实)}\" fill=\"none\" stroke=\"#999\" stroke-width=\"2\" stroke-dasharray=\"2 5\"/>',\n","    f'<text x=\"110\" y=\"{Y(四格[0,0])-12}\">宾州东部 23.3312</text>',\n","    f'<text x=\"110\" y=\"{Y(四格[1,0])+22}\">新泽西州 20.4394</text>',\n","    f'<path d=\"M555 {Y(四格[0,1])}L595 179\" stroke=\"#777\"/><text x=\"600\" y=\"183\">宾州东部 21.1656</text>',\n","    f'<path d=\"M555 {Y(四格[1,1])}L595 231\" stroke=\"#222\"/><text x=\"600\" y=\"235\">新泽西州 21.0274</text>',\n","    f'<text x=\"560\" y=\"{Y(反事实)+18}\">假设下的反事实 {反事实:.4f}</text>',\n","    '<text x=\"200\" y=\"356\" text-anchor=\"middle\">政策前</text><text x=\"550\" y=\"356\" text-anchor=\"middle\">政策后</text>',\n","    '<text x=\"100\" y=\"382\">点线为平行趋势假设下的反事实；只有一个前期，不能检验平行趋势。</text></g></svg>']\n","(目录/\"两组趋势与反事实.svg\").write_text(\"\".join(图),\"utf-8\")\n","结果={\"metrics\":[{\"label\":\"真实汇总DID\",\"value\":round(DID,8)},{\"label\":\"处理组前后变化\",\"value\":round(处理变化,8)},{\"label\":\"模拟迁移DID\",\"value\":模拟DID}],\n","      \"columns\":[\"案例\",\"处理组变化\",\"对照组变化\",\"DID\"],\n","      \"rows\":[[\"公开调查四格\",处理变化,对照变化,DID],[\"改变反事实趋势\",处理变化,假设变化,情景DID],[\"原创模拟门店\",模拟处理变化,模拟对照变化,模拟DID]],\n","      \"note\":\"真实调查按本课已有完整就业分项口径汇总，DID为2.75360578，保留与论文表3的2.76之差。四格回归残差自由度为0，不提供门店层面的标准误或平行趋势检验。情景和迁移数据均非新实证证据。\",\n","      \"checks\":[{\"name\":\"四格键与分母有效\",\"ok\":True},{\"name\":\"两次差、反事实差和回归系数一致\",\"ok\":True},{\"name\":\"四格回归零残差自由度\",\"ok\":bool(自由度==0)},{\"name\":\"新模拟面板16行、8家门店且DID为3\",\"ok\":True}],\n","      \"outputs\":[\"四格均值结果.csv\",\"独立练习四格结果.csv\",\"作业数值核对.json\",\"DID核对结果.csv\",\"两组趋势与反事实.svg\"]}\n","(目录/\"运行结果.json\").write_text(json.dumps(结果,ensure_ascii=False,indent=2),\"utf-8\")\n","print(json.dumps(核对,ensure_ascii=False,indent=2))\n"],"id":"cell-15"},{"cell_type":"markdown","metadata":{},"source":["## 保存、核对与改做\n","\n","1. 保存本笔记本。\n","2. 在左侧文件列表打开本次生成的结果文件，并与课程网页列出的基准核对。\n","3. 重启内核并从头运行全部单元，确认结果不依赖之前的隐藏变量。\n","4. 复制课程目录，再按原课“改做”和“独立完成”要求修改；保留原始输入与输出。\n","\n","本笔记本的分段代码与原脚本必须同步修改；正式复算仍可在已选定的Python环境运行原脚本。"],"id":"cell-16"}],"metadata":{"kernelspec":{"display_name":"Python（请选择本课已准备的环境）","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.11"}},"nbformat":4,"nbformat_minor":5}