这份文档不调用原项目中的任何 .R 或
.py
脚本。每个模型都在下面的代码块中重新写公式、重新读取处理后样本、重新估计。
项目并非全部由 R 完成:
本 Rmd 默认重估所有非模拟型 R 回归。两类耗时模拟默认关闭:
run_contest_permutation = FALSE:旧版场次内均匀置换,只是辅助诊断,不是正式制度
RI。run_design_ri = FALSE:正式按选举日期重抽 26 字母表的
RI。先把 ri_reps 设为 20 做冒烟测试;正式稿设为 1000。coverage <- data.table(
module = c(
"主一阶段、ITT、IV", "随机化平衡", "性别/种族异质性",
"额外因果诊断", "关键子样本稳健性", "姓名和时间窗口",
"作者当届效应复现", "近胜 RDD", "下一次定期选举",
"制度分类分层", "1—8 年动态路径", "第一位/第二位排位形状",
"正式制度异质性", "处理后机制诊断", "设计型 RI",
"贝叶斯分层模型", "姓名及州级案例人工核验"
),
engine = c(rep("R", 15), "Python", "人工 + R"),
this_document = c(rep("直接重估", 14), "参数开启后直接重估", "另附 Python 单元格文件", "暂缓")
)
show_result(coverage)
| module | engine | this_document |
|---|---|---|
| 主一阶段、ITT、IV | R | 直接重估 |
| 随机化平衡 | R | 直接重估 |
| 性别/种族异质性 | R | 直接重估 |
| 额外因果诊断 | R | 直接重估 |
| 关键子样本稳健性 | R | 直接重估 |
| 姓名和时间窗口 | R | 直接重估 |
| 作者当届效应复现 | R | 直接重估 |
| 近胜 RDD | R | 直接重估 |
| 下一次定期选举 | R | 直接重估 |
| 制度分类分层 | R | 直接重估 |
| 1—8 年动态路径 | R | 直接重估 |
| 第一位/第二位排位形状 | R | 直接重估 |
| 正式制度异质性 | R | 直接重估 |
| 处理后机制诊断 | R | 直接重估 |
| 设计型 RI | R | 参数开启后直接重估 |
| 贝叶斯分层模型 | Python | 另附 Python 单元格文件 |
| 姓名及州级案例人工核验 | 人工 + R | 暂缓 |
主分析样本是一行一个“首次出现、非现任、随机排位竞选中的候选人”。核心变量如下。
variable_dictionary <- data.table(
variable = c(
"first_position", "current_win", "run_again_4yr", "same_local_4yr",
"same_office_4yr", "future_win_4yr", "new_voteshare", "ballot_order",
"n_cands", "year", "co", "office2", "random_contest_id", "exact_date",
"female", "white", "name_length"
),
role = c(
"处理变量/工具变量 Z", "一阶段因变量、IV 内生变量 D",
rep("长期结果 Y", 4), "当届得票结果/处理后机制变量", "实际选票位次",
rep("固定效应", 4), "聚类层级/竞选场次", "共同随机字母表层级",
rep("预定候选人特征", 3)
),
definition = c(
"候选人实际排在第一位=1,否则=0",
"候选人在当届竞选中胜出=1,否则=0",
"基准选举后 48 个月内在同县再次出现=1",
"48 个月内在同一地方辖区再次参选=1",
"48 个月内竞选同一职位=1",
"48 个月内至少有一次未来胜选=1",
"候选人的当届合并得票份额",
"1=第一位、2=第二位,以此类推",
"该场候选人数", "基准选举年份", "县代码", "职位大类",
"完整竞选场次编号", "精确选举日期",
"女性姓名分类=1", "白人姓名分类=1", "标准化姓名字符串长度"
)
)
show_result(variable_dictionary)
| variable | role | definition |
|---|---|---|
| first_position | 处理变量/工具变量 Z | 候选人实际排在第一位=1,否则=0 |
| current_win | 一阶段因变量、IV 内生变量 D | 候选人在当届竞选中胜出=1,否则=0 |
| run_again_4yr | 长期结果 Y | 基准选举后 48 个月内在同县再次出现=1 |
| same_local_4yr | 长期结果 Y | 48 个月内在同一地方辖区再次参选=1 |
| same_office_4yr | 长期结果 Y | 48 个月内竞选同一职位=1 |
| future_win_4yr | 长期结果 Y | 48 个月内至少有一次未来胜选=1 |
| new_voteshare | 当届得票结果/处理后机制变量 | 候选人的当届合并得票份额 |
| ballot_order | 实际选票位次 | 1=第一位、2=第二位,以此类推 |
| n_cands | 固定效应 | 该场候选人数 |
| year | 固定效应 | 基准选举年份 |
| co | 固定效应 | 县代码 |
| office2 | 固定效应 | 职位大类 |
| random_contest_id | 聚类层级/竞选场次 | 完整竞选场次编号 |
| exact_date | 共同随机字母表层级 | 精确选举日期 |
| female | 预定候选人特征 | 女性姓名分类=1 |
| white | 预定候选人特征 | 白人姓名分类=1 |
| name_length | 预定候选人特征 | 标准化姓名字符串长度 |
sample_audit <- data.table(
item = c("候选人数", "竞选场次数", "选举日期数", "年份起点", "年份终点", "第一位置比例"),
value = c(
nrow(main), uniqueN(main$random_contest_id), uniqueN(main$exact_date),
min(main$year), max(main$year), mean(main$first_position)
)
)
show_result(sample_audit)
| item | value |
|---|---|
| 候选人数 | 37870.00000 |
| 竞选场次数 | 16213.00000 |
| 选举日期数 | 62.00000 |
| 年份起点 | 1999.00000 |
| 年份终点 | 2020.00000 |
| 第一位置比例 | 0.25004 |
令候选人为 \(i\),竞选场次为 \(c\)。所有主模型都吸收候选人数、年份、县和职位大类固定效应,并把标准误聚类到竞选场次:
一阶段:
\[D_{ic}=\pi Z_{ic}+\alpha_{n(c)}+\lambda_{year(c)}+\gamma_{county(c)}+\delta_{office(c)}+u_{ic}.\]
这里 \(Z\) 是
first_position,\(D\) 是
current_win。\(\pi\)
回答“随机排第一使当届胜选概率提高多少”。
约化式 ITT:
\[Y_{ic}=\beta Z_{ic}+\alpha_{n(c)}+\lambda_{year(c)}+\gamma_{county(c)}+\delta_{office(c)}+\varepsilon_{ic}.\]
\(\beta\) 是被随机分到第一位对长期结果的总效应,单位是概率点。例如 0.022 等于 2.2 个百分点。
IV/LATE:
\[Y_{ic}=\tau \widehat D_{ic}+\text{同一组固定效应}+e_{ic},\qquad D_{ic}\leftarrow Z_{ic}.\]
\(\tau\) 是“当选状态被随机第一位置改变的候选人”中的局部平均处理效应。它比 ITT 依赖更多假设,尤其是排除限制。
outcomes <- c(
run_again_4yr = "4年内再次参选",
same_local_4yr = "4年内在同一辖区参选",
same_office_4yr = "4年内竞选同一职位",
future_win_4yr = "4年内未来获胜"
)
first_stage <- feols(
as.formula(paste0("current_win ~ first_position | ", fe_rhs)),
data = main, cluster = ~random_contest_id, notes = FALSE
)
main_results <- list(extract_fixest(
first_stage, "first_position", "当前当选", "一阶段:随机第一→当前当选"
))
itt_models <- list(); iv_models <- list()
for (y in names(outcomes)) {
itt_models[[y]] <- feols(
as.formula(paste0(y, " ~ first_position | ", fe_rhs)),
data = main, cluster = ~random_contest_id, notes = FALSE
)
main_results[[length(main_results) + 1L]] <- extract_fixest(
itt_models[[y]], "first_position", outcomes[[y]], "ITT:随机第一→长期结果"
)
iv_models[[y]] <- feols(
as.formula(paste0(y, " ~ 1 | ", fe_rhs, " | current_win ~ first_position")),
data = main, cluster = ~random_contest_id, notes = FALSE
)
iv_term <- names(coef(iv_models[[y]]))[1]
main_results[[length(main_results) + 1L]] <- extract_fixest(
iv_models[[y]], iv_term, outcomes[[y]], "IV/LATE:被随机排位改变的当选→长期结果"
)
}
main_table <- rbindlist(main_results, fill = TRUE)
first_stage_F <- (coef(first_stage)[["first_position"]] / se(first_stage)[["first_position"]])^2
main_table[, first_stage_F := first_stage_F]
show_result(main_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | first_stage_F |
|---|---|---|---|---|---|---|---|---|---|
| 一阶段:随机第一→当前当选 | 当前当选 | first_position | 0.04900 | 0.00616 | 0.03693 | 0.06107 | 0.00000 | 37870 | 63.30993 |
| ITT:随机第一→长期结果 | 4年内再次参选 | first_position | 0.02216 | 0.00514 | 0.01209 | 0.03223 | 0.00002 | 37870 | 63.30993 |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内再次参选 | fit_current_win | 0.45226 | 0.10506 | 0.24634 | 0.65819 | 0.00002 | 37870 | 63.30993 |
| ITT:随机第一→长期结果 | 4年内在同一辖区参选 | first_position | 0.02059 | 0.00487 | 0.01104 | 0.03013 | 0.00002 | 37870 | 63.30993 |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内在同一辖区参选 | fit_current_win | 0.42014 | 0.09883 | 0.22643 | 0.61386 | 0.00002 | 37870 | 63.30993 |
| ITT:随机第一→长期结果 | 4年内竞选同一职位 | first_position | 0.01206 | 0.00427 | 0.00368 | 0.02043 | 0.00480 | 37870 | 63.30993 |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内竞选同一职位 | fit_current_win | 0.24604 | 0.08589 | 0.07769 | 0.41439 | 0.00418 | 37870 | 63.30993 |
| ITT:随机第一→长期结果 | 4年内未来获胜 | first_position | 0.01464 | 0.00421 | 0.00639 | 0.02288 | 0.00051 | 37870 | 63.30993 |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内未来获胜 | fit_current_win | 0.29867 | 0.08181 | 0.13833 | 0.45902 | 0.00026 | 37870 | 63.30993 |
headline <- coef(itt_models[["run_again_4yr"]])[["first_position"]]
cat(sprintf(
"\n**读法:** `run_again_4yr` 的 ITT 为 %.6f,即随机排在第一位使四年内再次参选概率提高 **%.3f 个百分点**。一阶段 F=%.2f。\n",
headline, 100 * headline, first_stage_F
))
读法: run_again_4yr 的 ITT 为
0.022162,即随机排在第一位使四年内再次参选概率提高 2.216
个百分点。一阶段 F=63.31。
这一步只把刚才“现场重估”的数值与旧输出对照;模型估计本身不依赖旧表。
saved_main <- fread(file.path(saved_table_dir, "07_随机排位_一阶段_ITT_IV结果.csv"))
compare_main <- merge(
main_table[, .(estimand, outcome, new_estimate = estimate)],
saved_main[, .(estimand, outcome, saved_estimate = estimate)],
by = c("estimand", "outcome"), all = TRUE
)
compare_main[, difference := new_estimate - saved_estimate]
compare_main[, status := fifelse(abs(difference) < 1e-10, "PASS", "CHECK")]
show_result(compare_main)
| estimand | outcome | new_estimate | saved_estimate | difference | status |
|---|---|---|---|---|---|
| ITT:随机第一→长期结果 | 4年内再次参选 | 0.02216 | 0.02216 | 0 | PASS |
| ITT:随机第一→长期结果 | 4年内在同一辖区参选 | 0.02059 | 0.02059 | 0 | PASS |
| ITT:随机第一→长期结果 | 4年内未来获胜 | 0.01464 | 0.01464 | 0 | PASS |
| ITT:随机第一→长期结果 | 4年内竞选同一职位 | 0.01206 | 0.01206 | 0 | PASS |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内再次参选 | 0.45226 | 0.45226 | 0 | PASS |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内在同一辖区参选 | 0.42014 | 0.42014 | 0 | PASS |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内未来获胜 | 0.29867 | 0.29867 | 0 | PASS |
| IV/LATE:被随机排位改变的当选→长期结果 | 4年内竞选同一职位 | 0.24604 | 0.24604 | 0 | PASS |
| 一阶段:随机第一→当前当选 | 当前当选 | 0.04900 | 0.04900 | 0 | PASS |
平衡检验用与 ITT
相同的固定效应和聚类方法,但把预定特征当作因变量。如果排位近似随机,first_position
不应系统预测这些变量。
balance_vars <- c(female = "女性", white = "白人", name_length = "标准化姓名长度")
balance_table <- rbindlist(lapply(names(balance_vars), function(v) {
d <- main[!is.na(get(v))]
fit <- feols(
as.formula(paste0(v, " ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
extract_fixest(fit, "first_position", balance_vars[[v]], "随机化平衡")
}))
show_result(balance_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n |
|---|---|---|---|---|---|---|---|---|
| 随机化平衡 | 女性 | first_position | -0.00402 | 0.00584 | -0.01547 | 0.00742 | 0.49115 | 37459 |
| 随机化平衡 | 白人 | first_position | -0.00328 | 0.00496 | -0.01300 | 0.00645 | 0.50915 | 36773 |
| 随机化平衡 | 标准化姓名长度 | first_position | -0.00134 | 0.03754 | -0.07492 | 0.07225 | 0.97157 | 37870 |
分组点估计只能描述不同组结果;正式差异检验使用交互项:
\[Y=\beta_0 Z+\beta_1 G+\beta_2(Z\times G)+FE+\varepsilon.\]
其中 \(\beta_2\) 才是两组处理效应差的检验。
subgroups <- list(
"全部首次候选人" = rep(TRUE, nrow(main)),
"白人候选人" = main$white == 1,
"非白人候选人" = main$white == 0,
"女性候选人" = main$female == 1,
"男性候选人" = main$female == 0
)
heterogeneity <- list()
for (g in names(subgroups)) {
d <- main[which(subgroups[[g]] %in% TRUE)]
if (nrow(d) < 500) next
for (y in c("current_win", "run_again_4yr")) {
fit <- feols(
as.formula(paste0(y, " ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
row <- extract_fixest(
fit, "first_position",
ifelse(y == "current_win", "当前当选", "4年内再次参选"),
ifelse(y == "current_win", "一阶段", "ITT")
)
row[, subgroup := g]
heterogeneity[[length(heterogeneity) + 1L]] <- row
}
}
for (moderator in c("white", "female")) {
d <- main[!is.na(get(moderator))]
for (y in c("current_win", "run_again_4yr")) {
fit <- feols(
as.formula(paste0(y, " ~ first_position * ", moderator, " | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
term <- paste0("first_position:", moderator)
row <- extract_fixest(
fit, term, ifelse(y == "current_win", "当前当选", "4年内再次参选"),
"组间效应差"
)
row[, subgroup := ifelse(
moderator == "white", "白人效应减非白人效应", "女性效应减男性效应"
)]
heterogeneity[[length(heterogeneity) + 1L]] <- row
}
}
heterogeneity_table <- rbindlist(heterogeneity, fill = TRUE)
show_result(heterogeneity_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | subgroup |
|---|---|---|---|---|---|---|---|---|---|
| 一阶段 | 当前当选 | first_position | 0.04900 | 0.00616 | 0.03693 | 0.06107 | 0.00000 | 37870 | 全部首次候选人 |
| ITT | 4年内再次参选 | first_position | 0.02216 | 0.00514 | 0.01209 | 0.03223 | 0.00002 | 37870 | 全部首次候选人 |
| 一阶段 | 当前当选 | first_position | 0.04155 | 0.00702 | 0.02779 | 0.05532 | 0.00000 | 28370 | 白人候选人 |
| ITT | 4年内再次参选 | first_position | 0.01999 | 0.00589 | 0.00845 | 0.03153 | 0.00069 | 28370 | 白人候选人 |
| 一阶段 | 当前当选 | first_position | 0.07521 | 0.01273 | 0.05026 | 0.10016 | 0.00000 | 8402 | 非白人候选人 |
| ITT | 4年内再次参选 | first_position | 0.02401 | 0.01140 | 0.00166 | 0.04635 | 0.03529 | 8402 | 非白人候选人 |
| 一阶段 | 当前当选 | first_position | 0.05740 | 0.01037 | 0.03708 | 0.07772 | 0.00000 | 13150 | 女性候选人 |
| ITT | 4年内再次参选 | first_position | 0.01836 | 0.00862 | 0.00146 | 0.03526 | 0.03328 | 13150 | 女性候选人 |
| 一阶段 | 当前当选 | first_position | 0.04287 | 0.00732 | 0.02853 | 0.05722 | 0.00000 | 24309 | 男性候选人 |
| ITT | 4年内再次参选 | first_position | 0.02418 | 0.00650 | 0.01144 | 0.03692 | 0.00020 | 24309 | 男性候选人 |
| 组间效应差 | 当前当选 | first_position:white | -0.02571 | 0.01355 | -0.05227 | 0.00085 | 0.05782 | 36773 | 白人效应减非白人效应 |
| 组间效应差 | 4年内再次参选 | first_position:white | -0.00468 | 0.01213 | -0.02845 | 0.01910 | 0.69992 | 36773 | 白人效应减非白人效应 |
| 组间效应差 | 当前当选 | first_position:female | 0.01997 | 0.01176 | -0.00308 | 0.04302 | 0.08956 | 37459 | 女性效应减男性效应 |
| 组间效应差 | 4年内再次参选 | first_position:female | -0.00575 | 0.01026 | -0.02586 | 0.01435 | 0.57477 | 37459 | 女性效应减男性效应 |
同场固定效应模型只比较同一竞选内部的候选人。落选者诊断会按处理后的
current_win
选样,因此只能作为排除限制的描述性检查,不能当作新的因果估计。
diagnostics <- list()
for (y in c("current_win", "run_again_4yr")) {
fit <- feols(
as.formula(paste0(y, " ~ first_position | random_contest_id")),
data = main, cluster = ~random_contest_id, notes = FALSE
)
diagnostics[[length(diagnostics) + 1L]] <- extract_fixest(
fit, "first_position", ifelse(y == "current_win", "当前当选", "4年内再次参选"),
"同场竞选固定效应"
)
}
loser_fit <- feols(
as.formula(paste0("run_again_4yr ~ first_position | ", fe_rhs)),
data = main[current_win == 0], cluster = ~random_contest_id, notes = FALSE
)
diagnostics[[length(diagnostics) + 1L]] <- extract_fixest(
loser_fit, "first_position", "4年内再次参选", "仅当前落选者(处理后选样;非正式)"
)
date_fit <- feols(
as.formula(paste0("run_again_4yr ~ first_position | ", fe_rhs)),
data = main, cluster = ~exact_date, notes = FALSE
)
diagnostics[[length(diagnostics) + 1L]] <- extract_fixest(
date_fit, "first_position", "4年内再次参选", "仅按精确日期聚类"
)
twoway_fit <- feols(
as.formula(paste0("run_again_4yr ~ first_position | ", fe_rhs)),
data = main, cluster = ~random_contest_id + exact_date, notes = FALSE
)
diagnostics[[length(diagnostics) + 1L]] <- extract_fixest(
twoway_fit, "first_position", "4年内再次参选", "竞选场次+精确日期双向聚类"
)
diagnostic_table <- rbindlist(diagnostics, fill = TRUE)
show_result(diagnostic_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n |
|---|---|---|---|---|---|---|---|---|
| 同场竞选固定效应 | 当前当选 | first_position | 0.04775 | 0.00777 | 0.03251 | 0.06298 | 0.00000 | 31687 |
| 同场竞选固定效应 | 4年内再次参选 | first_position | 0.02376 | 0.00620 | 0.01160 | 0.03592 | 0.00013 | 31687 |
| 仅当前落选者(处理后选样;非正式) | 4年内再次参选 | first_position | 0.00792 | 0.00569 | -0.00323 | 0.01907 | 0.16388 | 26136 |
| 仅按精确日期聚类 | 4年内再次参选 | first_position | 0.02216 | 0.00425 | 0.01383 | 0.03050 | 0.00000 | 37870 |
| 竞选场次+精确日期双向聚类 | 4年内再次参选 | first_position | 0.02216 | 0.00425 | 0.01383 | 0.03050 | 0.00000 | 37870 |
robustness_samples <- list(
"标准主样本" = rep(TRUE, nrow(main)),
"仅两候选人竞选" = main$n_cands == 2,
"三名以上候选人" = main$n_cands >= 3,
"总统大选同期" = as.character(main$electiming3) == "Presidential",
"中期选举同期" = as.character(main$electiming3) == "Midterm",
"错峰地方选举" = as.character(main$electiming3) == "Off-cycle",
"1999—2010" = main$year <= 2010,
"2011—2020" = main$year >= 2011,
"至少8年观察前史(2003—2020)" = main$year >= 2003,
"剔除同票匹配风险" = main$tied_share_in_contest == 0,
"剔除可观察同名碰撞" = main$possible_name_collision == 0
)
robustness <- rbindlist(lapply(names(robustness_samples), function(label) {
d <- main[which(robustness_samples[[label]] %in% TRUE)]
fit <- feols(
as.formula(paste0("run_again_4yr ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
row <- extract_fixest(fit, "first_position", "4年内再次参选", "ITT 稳健性")
row[, `:=`(sample = label, contests = uniqueN(d$random_contest_id))]
row
}), fill = TRUE)
show_result(robustness)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | sample | contests |
|---|---|---|---|---|---|---|---|---|---|---|
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02216 | 0.00514 | 0.01209 | 0.03223 | 0.00002 | 37870 | 标准主样本 | 16213 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.01013 | 0.01050 | -0.01045 | 0.03072 | 0.33471 | 5560 | 仅两候选人竞选 | 4524 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02438 | 0.00588 | 0.01285 | 0.03591 | 0.00003 | 32310 | 三名以上候选人 | 11689 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02434 | 0.00871 | 0.00726 | 0.04142 | 0.00524 | 13251 | 总统大选同期 | 5752 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.01784 | 0.00946 | -0.00071 | 0.03638 | 0.05951 | 11167 | 中期选举同期 | 4814 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02178 | 0.00863 | 0.00486 | 0.03870 | 0.01168 | 13450 | 错峰地方选举 | 5647 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02105 | 0.00693 | 0.00747 | 0.03463 | 0.00239 | 21299 | 1999—2010 | 8755 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02326 | 0.00769 | 0.00820 | 0.03833 | 0.00249 | 16571 | 2011—2020 | 7458 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02253 | 0.00572 | 0.01132 | 0.03375 | 0.00008 | 30235 | 至少8年观察前史(2003—2020) | 13243 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02238 | 0.00515 | 0.01229 | 0.03247 | 0.00001 | 37752 | 剔除同票匹配风险 | 16191 |
| ITT 稳健性 | 4年内再次参选 | first_position | 0.02217 | 0.00513 | 0.01211 | 0.03222 | 0.00002 | 37776 | 剔除可观察同名碰撞 | 16180 |
这组模型的回归式与主 ITT 完全相同,改变的是结果变量的姓名匹配规则或随访窗口。
name_specs <- list(
list("主结果:完整姓名+县,4年", extended, "run_again_4yr"),
list("宽松:姓+首个名字+县,4年;剔除歧义键", extended[core_key_ambiguous == 0], "run_again_4yr_relaxed_county"),
list("完整姓名全州,4年;剔除跨县同名", extended[state_name_ambiguous == 0], "run_again_4yr_exact_state"),
list("完整姓名+县,2年", extended, "run_again_2yr_exact_county"),
list("完整姓名+县,8年;基线不晚于2016", extended[year <= 2016], "run_again_8yr_exact_county")
)
name_sensitivity <- rbindlist(lapply(name_specs, function(spec) {
label <- spec[[1]]; d <- spec[[2]]; y <- spec[[3]]
fit <- feols(
as.formula(paste0(y, " ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
extract_fixest(fit, "first_position", y, label)
}), fill = TRUE)
show_result(name_sensitivity)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n |
|---|---|---|---|---|---|---|---|---|
| 主结果:完整姓名+县,4年 | run_again_4yr | first_position | 0.02216 | 0.00514 | 0.01209 | 0.03223 | 0.00002 | 37870 |
| 宽松:姓+首个名字+县,4年;剔除歧义键 | run_again_4yr_relaxed_county | first_position | 0.01980 | 0.00547 | 0.00908 | 0.03052 | 0.00030 | 37665 |
| 完整姓名全州,4年;剔除跨县同名 | run_again_4yr_exact_state | first_position | 0.02356 | 0.00559 | 0.01259 | 0.03452 | 0.00003 | 33816 |
| 完整姓名+县,2年 | run_again_2yr_exact_county | first_position | -0.00226 | 0.00354 | -0.00919 | 0.00468 | 0.52394 | 37870 |
| 完整姓名+县,8年;基线不晚于2016 | run_again_8yr_exact_county | first_position | 0.01793 | 0.00615 | 0.00588 | 0.02997 | 0.00354 | 30945 |
这是用作者复制数据先复现“第一位置对当届得票份额/胜选概率”的效果。公式仍吸收候选人数、年份、县和职位类别固定效应,标准误聚类到作者的竞选编号
new_raceid。
replication <- as.data.table(readRDS(file.path(raw_dir, "ceda_replication_ballot_order.rds")))
published <- replication[n_cands > 1 & incumb == "N" & !is.na(first)]
published_models <- list(
new_voteshare = feols(
new_voteshare ~ first | n_cands + year + co + office2,
data = published, cluster = ~new_raceid, notes = FALSE
),
win = feols(
win ~ first | n_cands + year + co + office2,
data = published, cluster = ~new_raceid, notes = FALSE
)
)
published_table <- rbindlist(list(
extract_fixest(published_models$new_voteshare, "first", "当届得票份额", "作者模型复现"),
extract_fixest(published_models$win, "first", "当届胜选概率", "作者模型复现")
))
show_result(published_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n |
|---|---|---|---|---|---|---|---|---|
| 作者模型复现 | 当届得票份额 | first | 0.01341 | 0.00119 | 0.01107 | 0.01574 | 0 | 60330 |
| 作者模型复现 | 当届胜选概率 | first | 0.04667 | 0.00509 | 0.03670 | 0.05664 | 0 | 58917 |
RDD 样本限定为首次参选、两候选人、单席位、非现任。margin
是两候选人得票份额减 0.5;won=1
表示跨过胜选断点。每个带宽分别估计:
\[Y=\alpha+\tau won+\beta_1 margin+\beta_2(won\times margin)+\varepsilon.\]
\(\tau\)
是断点处由落选跨到当选的跳跃。标准误按 contest_id
聚类并使用 HC1
修正。它是“当选效果”的辅助设计,不是随机选票顺序主设计。
cluster_lm <- function(model, cluster, term = "won") {
ct <- lmtest::coeftest(model, vcov. = sandwich::vcovCL(model, cluster = cluster, type = "HC1"))
data.table(
estimate = unname(ct[term, 1]), std_error = unname(ct[term, 2]),
p_value = unname(ct[term, 4]), n = nobs(model)
)
}
rdd_outcomes <- c(
run_again_4yr = "4年内再次参选", same_local_4yr = "4年内同一辖区参选",
future_win_4yr = "4年内未来获胜", run_again_8yr = "8年内再次参选"
)
bandwidths <- c(0.02, 0.05, 0.10)
rdd_results <- list()
for (y in names(rdd_outcomes)) for (bw in bandwidths) {
max_year <- if (y == "run_again_8yr") 2016 else 2020
d <- rd[year <= max_year & abs(margin) <= bw & !is.na(get(y))]
fit <- lm(as.formula(paste0(y, " ~ won + margin + won:margin")), data = d)
row <- cluster_lm(fit, d$contest_id)
row[, `:=`(
outcome = rdd_outcomes[[y]], bandwidth = bw,
lower_95 = estimate - 1.96 * std_error,
upper_95 = estimate + 1.96 * std_error,
control_mean = mean(d[won == 0, get(y)]), contests = uniqueN(d$contest_id)
)]
rdd_results[[length(rdd_results) + 1L]] <- row
}
rdd_table <- rbindlist(rdd_results, fill = TRUE)
show_result(rdd_table)
| estimate | std_error | p_value | n | outcome | bandwidth | lower_95 | upper_95 | control_mean | contests |
|---|---|---|---|---|---|---|---|---|---|
| 0.13481 | 0.07412 | 0.06954 | 524 | 4年内再次参选 | 0.02 | -0.01048 | 0.28009 | 0.16842 | 421 |
| 0.14816 | 0.04724 | 0.00175 | 1248 | 4年内再次参选 | 0.05 | 0.05556 | 0.24075 | 0.17647 | 998 |
| 0.17472 | 0.03410 | 0.00000 | 2501 | 4年内再次参选 | 0.10 | 0.10789 | 0.24156 | 0.14585 | 2003 |
| 0.12406 | 0.07209 | 0.08588 | 524 | 4年内同一辖区参选 | 0.02 | -0.01724 | 0.26536 | 0.12982 | 421 |
| 0.14032 | 0.04494 | 0.00184 | 1248 | 4年内同一辖区参选 | 0.05 | 0.05223 | 0.22840 | 0.13575 | 998 |
| 0.16742 | 0.03241 | 0.00000 | 2501 | 4年内同一辖区参选 | 0.10 | 0.10390 | 0.23094 | 0.10975 | 2003 |
| 0.19764 | 0.06509 | 0.00251 | 524 | 4年内未来获胜 | 0.02 | 0.07006 | 0.32522 | 0.09474 | 421 |
| 0.15818 | 0.04214 | 0.00018 | 1248 | 4年内未来获胜 | 0.05 | 0.07558 | 0.24078 | 0.08446 | 998 |
| 0.16544 | 0.02968 | 0.00000 | 2501 | 4年内未来获胜 | 0.10 | 0.10727 | 0.22362 | 0.06065 | 2003 |
| 0.24907 | 0.09548 | 0.00947 | 367 | 8年内再次参选 | 0.02 | 0.06192 | 0.43621 | 0.19289 | 296 |
| 0.20066 | 0.05968 | 0.00081 | 913 | 8年内再次参选 | 0.05 | 0.08368 | 0.31764 | 0.20534 | 722 |
| 0.20223 | 0.04257 | 0.00000 | 1845 | 8年内再次参选 | 0.10 | 0.11880 | 0.28565 | 0.18375 | 1467 |
RDD 平衡变量不在最小 RDD 文件中,因此下面用 row_id
回接原始 CEDA,只重构原脚本定义的 name_length 和
designation_missing。
raw <- as.data.table(readRDS(file.path(raw_dir, "ceda_allcandidates_1995-2024_fixed.rds")))
raw[, row_id := .I]
raw[, name_norm_rdd := normalize_letters(paste(first, last))]
raw[, `:=`(
name_length = nchar(name_norm_rdd),
designation_missing = as.integer(is.na(baldesig) | trimws(baldesig) == "")
)]
rd_balance <- copy(rd)
rd_balance[raw[, .(row_id, name_length, designation_missing)], on = "row_id",
`:=`(name_length = i.name_length, designation_missing = i.designation_missing)]
rdd_balance_results <- list()
for (v in c("name_length", "designation_missing")) for (bw in bandwidths) {
d <- rd_balance[abs(margin) <= bw & !is.na(get(v))]
fit <- lm(as.formula(paste0(v, " ~ won + margin + won:margin")), data = d)
row <- cluster_lm(fit, d$contest_id)
row[, `:=`(variable = v, bandwidth = bw, contests = uniqueN(d$contest_id))]
rdd_balance_results[[length(rdd_balance_results) + 1L]] <- row
}
show_result(rbindlist(rdd_balance_results, fill = TRUE))
| estimate | std_error | p_value | n | variable | bandwidth | contests |
|---|---|---|---|---|---|---|
| -0.36433 | 0.58885 | 0.53637 | 524 | name_length | 0.02 | 421 |
| 0.05134 | 0.36899 | 0.88936 | 1248 | name_length | 0.05 | 998 |
| 0.10789 | 0.24827 | 0.66393 | 2501 | name_length | 0.10 | 2003 |
| 0.00000 | 0.00000 | NaN | 524 | designation_missing | 0.02 | 421 |
| 0.00000 | 0.00000 | NaN | 1248 | designation_missing | 0.05 | 998 |
| 0.00000 | 0.00000 | NaN | 2501 | designation_missing | 0.10 | 2003 |
这些模型避免把“四年”误解成事后挑选窗口。严格职位键要求县、辖区、职位和席位/选区一致;宽职位键允许席位编号变化。因变量定义已经保存在处理后样本中,回归仍为同一主 ITT 公式。
strict <- next_sample[next_strict_observed == 1L]
broad <- next_sample[next_broad_observed == 1L]
strict_2 <- strict[cycle_type_strict == "约2年"]
strict_4 <- strict[cycle_type_strict == "约4年"]
next_specs <- list(
list(strict, "run_same_office_next_strict", "下一次定期选举竞选同一职位(严格键)"),
list(broad, "run_same_office_next_broad", "下一次定期选举竞选同类职位(宽键)"),
list(strict, "run_any_local_at_next_cycle", "下一次选举同月参加本县任一地方选举"),
list(strict, "run_by_next_scheduled_any_local", "截至下一次选举前已参加本县任一地方选举"),
list(strict_2, "run_by_next_scheduled_any_local", "约2年周期:截至下一次选举前再次参选"),
list(strict_4, "run_by_next_scheduled_any_local", "约4年周期:截至下一次选举前再次参选"),
list(strict_2, "run_again_4yr", "约2年周期:原四年结果"),
list(strict_4, "run_again_4yr", "约4年周期:原四年结果")
)
next_results <- rbindlist(lapply(next_specs, function(spec) {
d <- spec[[1]]; y <- spec[[2]]; label <- spec[[3]]
fit <- feols(
as.formula(paste0(y, " ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE, warn = FALSE
)
row <- extract_fixest(fit, "first_position", y, label)
row[, outcome_mean := mean(d[[y]], na.rm = TRUE)]
row
}), fill = TRUE)
show_result(next_results)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | outcome_mean |
|---|---|---|---|---|---|---|---|---|---|
| 下一次定期选举竞选同一职位(严格键) | run_same_office_next_strict | first_position | 0.00057 | 0.00532 | -0.00985 | 0.01100 | 0.91400 | 24359 | 0.11753 |
| 下一次定期选举竞选同类职位(宽键) | run_same_office_next_broad | first_position | -0.00234 | 0.00393 | -0.01004 | 0.00537 | 0.55234 | 28281 | 0.08345 |
| 下一次选举同月参加本县任一地方选举 | run_any_local_at_next_cycle | first_position | 0.00007 | 0.00554 | -0.01079 | 0.01093 | 0.99003 | 24359 | 0.13100 |
| 截至下一次选举前已参加本县任一地方选举 | run_by_next_scheduled_any_local | first_position | 0.00347 | 0.00595 | -0.00819 | 0.01513 | 0.55968 | 24359 | 0.16154 |
| 约2年周期:截至下一次选举前再次参选 | run_by_next_scheduled_any_local | first_position | -0.00920 | 0.00627 | -0.02149 | 0.00309 | 0.14235 | 16875 | 0.11644 |
| 约4年周期:截至下一次选举前再次参选 | run_by_next_scheduled_any_local | first_position | 0.02768 | 0.01211 | 0.00393 | 0.05142 | 0.02238 | 7484 | 0.26323 |
| 约2年周期:原四年结果 | run_again_4yr | first_position | 0.02712 | 0.00873 | 0.01001 | 0.04423 | 0.00190 | 16875 | 0.24948 |
| 约4年周期:原四年结果 | run_again_4yr | first_position | 0.02682 | 0.01187 | 0.00356 | 0.05009 | 0.02389 | 7484 | 0.25481 |
这些是分层点估计,不等于正式的组间差异检验。只有候选人不少于 300 且竞选不少于 100 的类别才估计。
families <- c(
"seat_structure", "effective_competition", "term_class", "special_proxy",
"election_month_class", "runoff_class", "writein_class", "office_group"
)
institution_results <- list()
for (family in families) for (category in unique(na.omit(institution[[family]]))) {
d <- institution[get(family) == category]
for (y in c("current_win", "run_again_4yr")) {
if (nrow(d) < 300 || uniqueN(d$random_contest_id) < 100) next
fit <- feols(
as.formula(paste0(y, " ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE, warn = FALSE
)
row <- extract_fixest(fit, "first_position", y, "制度分层 ITT")
row[, `:=`(classification_family = family, category = as.character(category),
contests = uniqueN(d$random_contest_id))]
institution_results[[length(institution_results) + 1L]] <- row
}
}
institution_table <- rbindlist(institution_results, fill = TRUE)
show_result(institution_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | classification_family | category | contests |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 制度分层 ITT | current_win | first_position | 0.07054 | 0.00854 | 0.05380 | 0.08728 | 0.00000 | 23490 | seat_structure | 多席位 | 8310 |
| 制度分层 ITT | run_again_4yr | first_position | 0.03072 | 0.00705 | 0.01689 | 0.04455 | 0.00001 | 23490 | seat_structure | 多席位 | 8310 |
| 制度分层 ITT | current_win | first_position | 0.02135 | 0.00875 | 0.00421 | 0.03850 | 0.01465 | 14380 | seat_structure | 单席位 | 7903 |
| 制度分层 ITT | run_again_4yr | first_position | 0.01196 | 0.00751 | -0.00277 | 0.02669 | 0.11155 | 14380 | seat_structure | 单席位 | 7903 |
| 制度分层 ITT | current_win | first_position | 0.04982 | 0.00618 | 0.03771 | 0.06193 | 0.00000 | 37702 | effective_competition | 候选人数多于席位 | 16094 |
| 制度分层 ITT | run_again_4yr | first_position | 0.02259 | 0.00515 | 0.01249 | 0.03269 | 0.00001 | 37702 | effective_competition | 候选人数多于席位 | 16094 |
| 制度分层 ITT | current_win | first_position | 0.04796 | 0.00626 | 0.03568 | 0.06023 | 0.00000 | 36448 | term_class | 完整任期 | 15526 |
| 制度分层 ITT | run_again_4yr | first_position | 0.02107 | 0.00525 | 0.01078 | 0.03136 | 0.00006 | 36448 | term_class | 完整任期 | 15526 |
| 制度分层 ITT | current_win | first_position | 0.06553 | 0.03391 | -0.00093 | 0.13200 | 0.05371 | 1413 | term_class | 短任期 | 686 |
| 制度分层 ITT | run_again_4yr | first_position | 0.04633 | 0.02570 | -0.00403 | 0.09669 | 0.07182 | 1413 | term_class | 短任期 | 686 |
| 制度分层 ITT | current_win | first_position | 0.04790 | 0.00626 | 0.03563 | 0.06017 | 0.00000 | 36454 | special_proxy | 普通完整任期代理 | 15527 |
| 制度分层 ITT | run_again_4yr | first_position | 0.02102 | 0.00525 | 0.01073 | 0.03131 | 0.00006 | 36454 | special_proxy | 普通完整任期代理 | 15527 |
| 制度分层 ITT | current_win | first_position | 0.06553 | 0.03391 | -0.00093 | 0.13200 | 0.05371 | 1413 | special_proxy | 短任期/特别选举代理 | 686 |
| 制度分层 ITT | run_again_4yr | first_position | 0.04633 | 0.02570 | -0.00403 | 0.09669 | 0.07182 | 1413 | special_proxy | 短任期/特别选举代理 | 686 |
| 制度分层 ITT | current_win | first_position | 0.05669 | 0.00705 | 0.04287 | 0.07050 | 0.00000 | 31476 | election_month_class | 11月选举 | 13360 |
| 制度分层 ITT | run_again_4yr | first_position | 0.02373 | 0.00557 | 0.01280 | 0.03465 | 0.00002 | 31476 | election_month_class | 11月选举 | 13360 |
| 制度分层 ITT | current_win | first_position | 0.01768 | 0.01191 | -0.00566 | 0.04102 | 0.13766 | 6394 | election_month_class | 非11月选举 | 2853 |
| 制度分层 ITT | run_again_4yr | first_position | 0.01382 | 0.01301 | -0.01167 | 0.03932 | 0.28792 | 6394 | election_month_class | 非11月选举 | 2853 |
| 制度分层 ITT | current_win | first_position | 0.05845 | 0.00694 | 0.04485 | 0.07204 | 0.00000 | 32307 | runoff_class | 当轮直接决定 | 14241 |
| 制度分层 ITT | run_again_4yr | first_position | 0.02396 | 0.00543 | 0.01331 | 0.03460 | 0.00001 | 32307 | runoff_class | 当轮直接决定 | 14241 |
| 制度分层 ITT | current_win | first_position | -0.00001 | 0.01174 | -0.02302 | 0.02300 | 0.99926 | 5563 | runoff_class | 存在进入下一轮机制 | 1972 |
| 制度分层 ITT | run_again_4yr | first_position | 0.00686 | 0.01491 | -0.02238 | 0.03609 | 0.64584 | 5563 | runoff_class | 存在进入下一轮机制 | 1972 |
| 制度分层 ITT | current_win | first_position | 0.05687 | 0.00774 | 0.04171 | 0.07203 | 0.00000 | 24093 | writein_class | 无写入票记录 | 10026 |
| 制度分层 ITT | run_again_4yr | first_position | 0.02567 | 0.00663 | 0.01268 | 0.03866 | 0.00011 | 24093 | writein_class | 无写入票记录 | 10026 |
| 制度分层 ITT | current_win | first_position | 0.03582 | 0.01021 | 0.01582 | 0.05582 | 0.00045 | 13777 | writein_class | 有写入票记录 | 6187 |
| 制度分层 ITT | run_again_4yr | first_position | 0.01486 | 0.00815 | -0.00111 | 0.03082 | 0.06822 | 13777 | writein_class | 有写入票记录 | 6187 |
| 制度分层 ITT | current_win | first_position | 0.03832 | 0.01039 | 0.01795 | 0.05869 | 0.00023 | 14476 | office_group | City Council | 5066 |
| 制度分层 ITT | run_again_4yr | first_position | 0.03167 | 0.00981 | 0.01244 | 0.05090 | 0.00126 | 14476 | office_group | City Council | 5066 |
| 制度分层 ITT | current_win | first_position | 0.06374 | 0.00917 | 0.04577 | 0.08171 | 0.00000 | 18347 | office_group | School Board | 8506 |
| 制度分层 ITT | run_again_4yr | first_position | 0.01539 | 0.00647 | 0.00270 | 0.02807 | 0.01746 | 18347 | office_group | School Board | 8506 |
| 制度分层 ITT | current_win | first_position | 0.00560 | 0.01661 | -0.02695 | 0.03816 | 0.73593 | 1990 | office_group | County Legislator | 1012 |
| 制度分层 ITT | run_again_4yr | first_position | 0.02557 | 0.02397 | -0.02140 | 0.07254 | 0.28621 | 1990 | office_group | County Legislator | 1012 |
| 制度分层 ITT | current_win | first_position | 0.05557 | 0.03634 | -0.01565 | 0.12679 | 0.12698 | 899 | office_group | Judge | 419 |
| 制度分层 ITT | run_again_4yr | first_position | -0.01481 | 0.02888 | -0.07141 | 0.04179 | 0.60831 | 899 | office_group | Judge | 419 |
| 制度分层 ITT | current_win | first_position | -0.00948 | 0.02634 | -0.06111 | 0.04214 | 0.71890 | 1106 | office_group | Mayor | 597 |
| 制度分层 ITT | run_again_4yr | first_position | 0.04126 | 0.03355 | -0.02450 | 0.10702 | 0.21926 | 1106 | office_group | Mayor | 597 |
| 制度分层 ITT | current_win | first_position | 0.10678 | 0.07259 | -0.03550 | 0.24905 | 0.14296 | 308 | office_group | Clerk | 195 |
| 制度分层 ITT | run_again_4yr | first_position | 0.08358 | 0.06020 | -0.03440 | 0.20157 | 0.16661 | 308 | office_group | Clerk | 195 |
first_return_gap_months_8yr
是基准选举到首次再次参选之间的月数。第 \(h\) 年累计结果定义为首次回归间隔不超过
\(12h\)
个月。固定八年完整随访样本避免不同年份使用不同基线队列;各期最大完整样本提高每一时点的样本量。
raw_end_index <- max(raw$year * 12L + fifelse(is.na(raw$month), 6L, as.integer(raw$month)), na.rm = TRUE)
balanced_8yr <- dynamic[base_index_dynamic + 96L <= raw_end_index]
dynamic_results <- list()
for (h in 1:8) for (design in c("固定八年完整随访样本", "各期最大完整随访样本")) {
d <- if (design == "固定八年完整随访样本") balanced_8yr else
dynamic[base_index_dynamic + 12L * h <= raw_end_index]
d <- copy(d)
d[, dynamic_outcome := as.integer(
!is.na(first_return_gap_months_8yr) & first_return_gap_months_8yr <= 12L * h
)]
fit <- feols(
as.formula(paste0("dynamic_outcome ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
row <- extract_fixest(fit, "first_position", paste0(h, "年内再次参选"), "累计 ITT")
row[, `:=`(horizon_year = h, sample_design = design,
control_mean = mean(d[first_position == 0, dynamic_outcome]),
contests = uniqueN(d$random_contest_id))]
dynamic_results[[length(dynamic_results) + 1L]] <- row
}
dynamic_table <- rbindlist(dynamic_results, fill = TRUE)
dynamic_table[, p_value_bh := p.adjust(p_value, method = "BH"), by = sample_design]
show_result(dynamic_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | horizon_year | sample_design | control_mean | contests | p_value_bh |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 累计 ITT | 1年内再次参选 | first_position | 0.00130 | 0.00233 | -0.00328 | 0.00587 | 0.57829 | 30945 | 1 | 固定八年完整随访样本 | 0.03224 | 13002 | 0.74334 |
| 累计 ITT | 1年内再次参选 | first_position | 0.00108 | 0.00206 | -0.00295 | 0.00512 | 0.59862 | 37870 | 1 | 各期最大完整随访样本 | 0.03169 | 16213 | 0.59862 |
| 累计 ITT | 2年内再次参选 | first_position | -0.00131 | 0.00401 | -0.00917 | 0.00654 | 0.74334 | 30945 | 2 | 固定八年完整随访样本 | 0.10164 | 13002 | 0.74334 |
| 累计 ITT | 2年内再次参选 | first_position | -0.00226 | 0.00354 | -0.00919 | 0.00468 | 0.52394 | 37870 | 2 | 各期最大完整随访样本 | 0.09979 | 16213 | 0.59862 |
| 累计 ITT | 3年内再次参选 | first_position | -0.00145 | 0.00411 | -0.00950 | 0.00660 | 0.72420 | 30945 | 3 | 固定八年完整随访样本 | 0.10799 | 13002 | 0.74334 |
| 累计 ITT | 3年内再次参选 | first_position | -0.00203 | 0.00364 | -0.00916 | 0.00510 | 0.57694 | 37870 | 3 | 各期最大完整随访样本 | 0.10626 | 16213 | 0.59862 |
| 累计 ITT | 4年内再次参选 | first_position | 0.01910 | 0.00570 | 0.00793 | 0.03026 | 0.00080 | 30945 | 4 | 固定八年完整随访样本 | 0.21240 | 13002 | 0.00320 |
| 累计 ITT | 4年内再次参选 | first_position | 0.02216 | 0.00514 | 0.01209 | 0.03223 | 0.00002 | 37870 | 4 | 各期最大完整随访样本 | 0.21355 | 16213 | 0.00013 |
| 累计 ITT | 5年内再次参选 | first_position | 0.02080 | 0.00584 | 0.00934 | 0.03225 | 0.00038 | 30945 | 5 | 固定八年完整随访样本 | 0.22443 | 13002 | 0.00300 |
| 累计 ITT | 5年内再次参选 | first_position | 0.02144 | 0.00553 | 0.01060 | 0.03228 | 0.00011 | 34364 | 5 | 各期最大完整随访样本 | 0.22504 | 14584 | 0.00042 |
| 累计 ITT | 6年内再次参选 | first_position | 0.01794 | 0.00595 | 0.00629 | 0.02960 | 0.00256 | 30945 | 6 | 固定八年完整随访样本 | 0.23821 | 13002 | 0.00511 |
| 累计 ITT | 6年内再次参选 | first_position | 0.01838 | 0.00563 | 0.00734 | 0.02941 | 0.00110 | 34221 | 6 | 各期最大完整随访样本 | 0.23895 | 14532 | 0.00294 |
| 累计 ITT | 7年内再次参选 | first_position | 0.01854 | 0.00597 | 0.00683 | 0.03024 | 0.00191 | 30945 | 7 | 固定八年完整随访样本 | 0.24111 | 13002 | 0.00510 |
| 累计 ITT | 7年内再次参选 | first_position | 0.01823 | 0.00595 | 0.00656 | 0.02989 | 0.00220 | 31184 | 7 | 各期最大完整随访样本 | 0.24099 | 13083 | 0.00440 |
| 累计 ITT | 8年内再次参选 | first_position | 0.01793 | 0.00615 | 0.00588 | 0.02997 | 0.00354 | 30945 | 8 | 固定八年完整随访样本 | 0.26073 | 13002 | 0.00567 |
| 累计 ITT | 8年内再次参选 | first_position | 0.01793 | 0.00615 | 0.00588 | 0.02997 | 0.00354 | 30945 | 8 | 各期最大完整随访样本 | 0.26073 | 13002 | 0.00567 |
ggplot(dynamic_table, aes(horizon_year, 100 * estimate, color = sample_design)) +
geom_hline(yintercept = 0, linetype = 2, color = "grey60") +
geom_line() + geom_point() +
geom_errorbar(aes(ymin = 100 * lower_95, ymax = 100 * upper_95), width = 0.12) +
scale_x_continuous(breaks = 1:8) +
labs(x = "随访年数", y = "随机第一位置的累计 ITT(百分点)", color = "样本") +
theme_minimal()
互斥的“首次回归年份”把累计曲线拆成第 1 年、……、第 8 年首次回归。
interval_results <- list()
for (h in 1:8) {
d <- copy(balanced_8yr)
d[, first_return_interval := as.integer(
!is.na(first_return_gap_months_8yr) &
first_return_gap_months_8yr > 12L * (h - 1L) &
first_return_gap_months_8yr <= 12L * h
)]
fit <- feols(
as.formula(paste0("first_return_interval ~ first_position | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
row <- extract_fixest(fit, "first_position", paste0("第", h, "年首次再次参选"), "区间 ITT")
row[, interval_year := h]
interval_results[[h]] <- row
}
interval_table <- rbindlist(interval_results)
interval_table[, p_value_bh := p.adjust(p_value, method = "BH")]
show_result(interval_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | interval_year | p_value_bh |
|---|---|---|---|---|---|---|---|---|---|---|
| 区间 ITT | 第1年首次再次参选 | first_position | 0.00130 | 0.00233 | -0.00328 | 0.00587 | 0.57829 | 30945 | 1 | 0.77105 |
| 区间 ITT | 第2年首次再次参选 | first_position | -0.00261 | 0.00333 | -0.00913 | 0.00391 | 0.43270 | 30945 | 2 | 0.70411 |
| 区间 ITT | 第3年首次再次参选 | first_position | -0.00014 | 0.00102 | -0.00214 | 0.00187 | 0.89420 | 30945 | 3 | 0.89420 |
| 区间 ITT | 第4年首次再次参选 | first_position | 0.02055 | 0.00445 | 0.01181 | 0.02928 | 0.00000 | 30945 | 4 | 0.00003 |
| 区间 ITT | 第5年首次再次参选 | first_position | 0.00170 | 0.00158 | -0.00140 | 0.00480 | 0.28330 | 30945 | 5 | 0.70411 |
| 区间 ITT | 第6年首次再次参选 | first_position | -0.00285 | 0.00148 | -0.00575 | 0.00005 | 0.05376 | 30945 | 6 | 0.21503 |
| 区间 ITT | 第7年首次再次参选 | first_position | 0.00060 | 0.00077 | -0.00092 | 0.00211 | 0.44007 | 30945 | 7 | 0.70411 |
| 区间 ITT | 第8年首次再次参选 | first_position | -0.00061 | 0.00191 | -0.00435 | 0.00312 | 0.74806 | 30945 | 8 | 0.85493 |
expanded_outcomes <- c(
future_election_dates_4yr = "四年内后续参选日期次数",
future_winning_dates_4yr = "四年内后续胜选日期次数",
first_return_same_office_4yr = "首次回归为同一职位",
first_return_other_office_same_place_4yr = "首次回归为同一辖区其他职位",
first_return_other_place_same_county_4yr = "首次回归为同县其他辖区"
)
expanded_table <- rbindlist(lapply(names(expanded_outcomes), function(y) {
fit <- feols(
as.formula(paste0(y, " ~ first_position | ", fe_rhs)),
data = dynamic, cluster = ~random_contest_id, notes = FALSE
)
extract_fixest(fit, "first_position", expanded_outcomes[[y]], "随机第一位置的 ITT")
}))
expanded_table[, p_value_bh := p.adjust(p_value, method = "BH")]
show_result(expanded_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | p_value_bh |
|---|---|---|---|---|---|---|---|---|---|
| 随机第一位置的 ITT | 四年内后续参选日期次数 | first_position | 0.01944 | 0.00598 | 0.00772 | 0.03116 | 0.00115 | 37870 | 0.00385 |
| 随机第一位置的 ITT | 四年内后续胜选日期次数 | first_position | 0.01415 | 0.00453 | 0.00527 | 0.02303 | 0.00179 | 37870 | 0.00385 |
| 随机第一位置的 ITT | 首次回归为同一职位 | first_position | 0.01186 | 0.00423 | 0.00357 | 0.02016 | 0.00506 | 37870 | 0.00633 |
| 随机第一位置的 ITT | 首次回归为同一辖区其他职位 | first_position | 0.00792 | 0.00260 | 0.00283 | 0.01301 | 0.00231 | 37870 | 0.00385 |
| 随机第一位置的 ITT | 首次回归为同县其他辖区 | first_position | 0.00238 | 0.00216 | -0.00186 | 0.00662 | 0.27111 | 37870 | 0.27111 |
在至少三名候选人的竞选中,基准组是第三位及以后:
\[Y=\beta_1 1[position=1]+\beta_2 1[position=2]+FE+\varepsilon.\]
第一位减第二位 是线性组合 \(\beta_1-\beta_2\),不是另跑一条二元回归。
position_sample <- copy(dynamic[n_cands >= 3])
position_sample[, `:=`(
position_first = as.integer(ballot_order == 1L),
position_second = as.integer(ballot_order == 2L)
)]
position_outcomes <- c(
current_win = "当前当选", new_voteshare = "当前得票份额",
run_again_4yr = "四年内再次参选", same_local_4yr = "四年内同一辖区参选",
same_office_4yr = "四年内同一职位参选", future_win_4yr = "四年内未来获胜"
)
position_results <- list()
for (y in names(position_outcomes)) {
fit <- feols(
as.formula(paste0(y, " ~ position_first + position_second | ", fe_rhs)),
data = position_sample, cluster = ~random_contest_id, notes = FALSE
)
position_results[[length(position_results) + 1L]] <- rbindlist(list(
extract_fixest(fit, "position_first", position_outcomes[[y]], "第一位相对第三位及以后"),
extract_fixest(fit, "position_second", position_outcomes[[y]], "第二位相对第三位及以后"),
linear_combination(fit, c(position_first = 1, position_second = -1),
position_outcomes[[y]], "第一位减第二位")
))
}
position_table <- rbindlist(position_results)
position_table[estimand == "第一位减第二位", p_value_bh := p.adjust(p_value, method = "BH")]
show_result(position_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | p_value_bh |
|---|---|---|---|---|---|---|---|---|---|
| 第一位相对第三位及以后 | 当前当选 | position_first | 0.05891 | 0.00698 | 0.04524 | 0.07258 | 0.00000 | 32310 | NA |
| 第二位相对第三位及以后 | 当前当选 | position_second | 0.01654 | 0.00682 | 0.00317 | 0.02990 | 0.01534 | 32310 | NA |
| 第一位减第二位 | 当前当选 | linear combination | 0.04237 | 0.00819 | 0.02633 | 0.05842 | 0.00000 | 32310 | 0.00000 |
| 第一位相对第三位及以后 | 当前得票份额 | position_first | 0.01332 | 0.00144 | 0.01049 | 0.01615 | 0.00000 | 32310 | NA |
| 第二位相对第三位及以后 | 当前得票份额 | position_second | 0.00294 | 0.00140 | 0.00019 | 0.00569 | 0.03646 | 32310 | NA |
| 第一位减第二位 | 当前得票份额 | linear combination | 0.01038 | 0.00177 | 0.00691 | 0.01385 | 0.00000 | 32310 | 0.00000 |
| 第一位相对第三位及以后 | 四年内再次参选 | position_first | 0.02603 | 0.00617 | 0.01394 | 0.03813 | 0.00002 | 32310 | NA |
| 第二位相对第三位及以后 | 四年内再次参选 | position_second | 0.00532 | 0.00601 | -0.00646 | 0.01711 | 0.37586 | 32310 | NA |
| 第一位减第二位 | 四年内再次参选 | linear combination | 0.02071 | 0.00719 | 0.00661 | 0.03481 | 0.00400 | 32310 | 0.00600 |
| 第一位相对第三位及以后 | 四年内同一辖区参选 | position_first | 0.02255 | 0.00588 | 0.01102 | 0.03408 | 0.00013 | 32310 | NA |
| 第二位相对第三位及以后 | 四年内同一辖区参选 | position_second | -0.00097 | 0.00567 | -0.01209 | 0.01015 | 0.86440 | 32310 | NA |
| 第一位减第二位 | 四年内同一辖区参选 | linear combination | 0.02352 | 0.00681 | 0.01018 | 0.03686 | 0.00055 | 32310 | 0.00110 |
| 第一位相对第三位及以后 | 四年内同一职位参选 | position_first | 0.01360 | 0.00524 | 0.00333 | 0.02387 | 0.00949 | 32310 | NA |
| 第二位相对第三位及以后 | 四年内同一职位参选 | position_second | -0.00162 | 0.00511 | -0.01163 | 0.00839 | 0.75093 | 32310 | NA |
| 第一位减第二位 | 四年内同一职位参选 | linear combination | 0.01522 | 0.00606 | 0.00334 | 0.02710 | 0.01207 | 32310 | 0.01448 |
| 第一位相对第三位及以后 | 四年内未来获胜 | position_first | 0.01630 | 0.00504 | 0.00641 | 0.02618 | 0.00123 | 32310 | NA |
| 第二位相对第三位及以后 | 四年内未来获胜 | position_second | 0.00682 | 0.00491 | -0.00280 | 0.01644 | 0.16453 | 32310 | NA |
| 第一位减第二位 | 四年内未来获胜 | linear combination | 0.00947 | 0.00593 | -0.00215 | 0.02109 | 0.11014 | 32310 | 0.11014 |
这里让
n_cands、year、co、office2
的固定效应在两组中完全自由,再估计
first_position × moderator。交互项才是正式组间差异。
dynamic[, multi_seat_dynamic := fifelse(
seat_structure == "多席位", 1L, fifelse(seat_structure == "单席位", 0L, NA_integer_)
)]
dynamic[, crowded_dynamic := as.integer(n_cands >= 4L)]
dynamic[, on_cycle_dynamic := fifelse(
as.character(electiming3) %in% c("Presidential", "Midterm"), 1L,
fifelse(as.character(electiming3) == "Off-cycle", 0L, NA_integer_)
)]
moderator_specs <- list(
multi_seat = list("multi_seat_dynamic", !is.na(dynamic$multi_seat_dynamic), "单席位", "多席位"),
crowded = list("crowded_dynamic", rep(TRUE, nrow(dynamic)), "2—3名候选人", "4名及以上候选人"),
on_cycle = list("on_cycle_dynamic", !is.na(dynamic$on_cycle_dynamic), "错峰地方选举", "总统或中期选举同期"),
council_vs_school = list("council_dynamic", dynamic$office_group %in% c("City Council", "School Board"), "School Board", "City Council")
)
formal_het <- list()
for (mod_name in names(moderator_specs)) {
spec <- moderator_specs[[mod_name]]
moderator <- spec[[1]]; d <- copy(dynamic[which(spec[[2]] %in% TRUE)])
if (mod_name == "council_vs_school") d[, council_dynamic := as.integer(office_group == "City Council")]
for (v in c("n_cands", "year", "co", "office2")) {
d[, paste0(v, "_by_group") := interaction(get(v), get(moderator), drop = TRUE)]
}
saturated_fe <- "n_cands_by_group + year_by_group + co_by_group + office2_by_group"
for (y in c("current_win", "run_again_4yr")) {
fit <- feols(
as.formula(paste0(y, " ~ first_position * ", moderator, " | ", saturated_fe)),
data = d, cluster = ~random_contest_id, notes = FALSE
)
interaction_term <- paste0("first_position:", moderator)
rows <- rbindlist(list(
extract_fixest(fit, "first_position", y, paste0(spec[[3]], "效应")),
linear_combination(fit, setNames(c(1, 1), c("first_position", interaction_term)),
y, paste0(spec[[4]], "效应")),
extract_fixest(fit, interaction_term, y, paste0(spec[[4]], "减去", spec[[3]]))
))
rows[, moderator := mod_name]
formal_het[[length(formal_het) + 1L]] <- rows
}
}
formal_het_table <- rbindlist(formal_het)
formal_het_table[grepl("减去", estimand), p_value_bh := p.adjust(p_value, method = "BH")]
show_result(formal_het_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | moderator | p_value_bh |
|---|---|---|---|---|---|---|---|---|---|---|
| 单席位效应 | current_win | first_position | 0.02135 | 0.00874 | 0.00423 | 0.03848 | 0.01455 | 37870 | multi_seat | NA |
| 多席位效应 | current_win | linear combination | 0.07054 | 0.00855 | 0.05379 | 0.08729 | 0.00000 | 37870 | multi_seat | NA |
| 多席位减去单席位 | current_win | first_position:multi_seat_dynamic | 0.04919 | 0.01222 | 0.02523 | 0.07314 | 0.00006 | 37870 | multi_seat | 0.00046 |
| 单席位效应 | run_again_4yr | first_position | 0.01196 | 0.00751 | -0.00276 | 0.02667 | 0.11121 | 37870 | multi_seat | NA |
| 多席位效应 | run_again_4yr | linear combination | 0.03072 | 0.00706 | 0.01689 | 0.04455 | 0.00001 | 37870 | multi_seat | NA |
| 多席位减去单席位 | run_again_4yr | first_position:multi_seat_dynamic | 0.01876 | 0.01030 | -0.00144 | 0.03896 | 0.06867 | 37870 | multi_seat | 0.12379 |
| 2—3名候选人效应 | current_win | first_position | 0.03296 | 0.01002 | 0.01331 | 0.05260 | 0.00101 | 37870 | crowded | NA |
| 4名及以上候选人效应 | current_win | linear combination | 0.06042 | 0.00773 | 0.04527 | 0.07557 | 0.00000 | 37870 | crowded | NA |
| 4名及以上候选人减去2—3名候选人 | current_win | first_position:crowded_dynamic | 0.02746 | 0.01266 | 0.00266 | 0.05227 | 0.03003 | 37870 | crowded | 0.12012 |
| 2—3名候选人效应 | run_again_4yr | first_position | 0.01162 | 0.00754 | -0.00316 | 0.02639 | 0.12338 | 37870 | crowded | NA |
| 4名及以上候选人效应 | run_again_4yr | linear combination | 0.02978 | 0.00699 | 0.01607 | 0.04349 | 0.00002 | 37870 | crowded | NA |
| 4名及以上候选人减去2—3名候选人 | run_again_4yr | first_position:crowded_dynamic | 0.01816 | 0.01028 | -0.00199 | 0.03832 | 0.07737 | 37870 | crowded | 0.12379 |
| 错峰地方选举效应 | current_win | first_position | 0.03767 | 0.00974 | 0.01859 | 0.05675 | 0.00011 | 37869 | on_cycle | NA |
| 总统或中期选举同期效应 | current_win | linear combination | 0.05594 | 0.00792 | 0.04042 | 0.07146 | 0.00000 | 37869 | on_cycle | NA |
| 总统或中期选举同期减去错峰地方选举 | current_win | first_position:on_cycle_dynamic | 0.01827 | 0.01255 | -0.00632 | 0.04287 | 0.14540 | 37869 | on_cycle | 0.18968 |
| 错峰地方选举效应 | run_again_4yr | first_position | 0.02178 | 0.00862 | 0.00488 | 0.03868 | 0.01155 | 37869 | on_cycle | NA |
| 总统或中期选举同期效应 | run_again_4yr | linear combination | 0.02118 | 0.00641 | 0.00863 | 0.03374 | 0.00094 | 37869 | on_cycle | NA |
| 总统或中期选举同期减去错峰地方选举 | run_again_4yr | first_position:on_cycle_dynamic | -0.00060 | 0.01074 | -0.02165 | 0.02046 | 0.95578 | 37869 | on_cycle | 0.95578 |
| School Board效应 | current_win | first_position | 0.06374 | 0.00917 | 0.04577 | 0.08172 | 0.00000 | 32823 | council_vs_school | NA |
| City Council效应 | current_win | linear combination | 0.03832 | 0.01039 | 0.01796 | 0.05868 | 0.00023 | 32823 | council_vs_school | NA |
| City Council减去School Board | current_win | first_position:council_dynamic | -0.02542 | 0.01386 | -0.05258 | 0.00174 | 0.06658 | 32823 | council_vs_school | 0.12379 |
| School Board效应 | run_again_4yr | first_position | 0.01539 | 0.00648 | 0.00270 | 0.02808 | 0.01749 | 32823 | council_vs_school | NA |
| City Council效应 | run_again_4yr | linear combination | 0.03167 | 0.00981 | 0.01245 | 0.05089 | 0.00124 | 32823 | council_vs_school | NA |
| City Council减去School Board | run_again_4yr | first_position:council_dynamic | 0.01628 | 0.01175 | -0.00675 | 0.03931 | 0.16597 | 32823 | council_vs_school | 0.18968 |
current_win 和 new_voteshare
都会受到随机第一位置影响,属于处理后变量。把它们加入回归会破坏原随机化解释,所以这里只看系数如何变化,不能把剩余
first_position 系数解释为正式直接效应。
mechanism_specs <- list(
"总ITT:不控制处理后变量" = paste0("run_again_4yr ~ first_position | ", fe_rhs),
"加入当届胜选" = paste0("run_again_4yr ~ first_position + current_win | ", fe_rhs),
"加入当届得票份额" = paste0("run_again_4yr ~ first_position + new_voteshare | ", fe_rhs),
"同时加入胜选和得票份额" = paste0("run_again_4yr ~ first_position + current_win + new_voteshare | ", fe_rhs)
)
mechanism_table <- rbindlist(lapply(names(mechanism_specs), function(label) {
fit <- feols(
as.formula(mechanism_specs[[label]]), data = dynamic,
cluster = ~random_contest_id, notes = FALSE
)
row <- extract_fixest(fit, "first_position", "4年内再次参选", label)
row[, causal_status := ifelse(
label == "总ITT:不控制处理后变量", "主因果估计", "处理后调整:仅描述"
)]
row
}))
show_result(mechanism_table)
| estimand | outcome | term | estimate | std_error | lower_95 | upper_95 | p_value | n | causal_status |
|---|---|---|---|---|---|---|---|---|---|
| 总ITT:不控制处理后变量 | 4年内再次参选 | first_position | 0.02216 | 0.00514 | 0.01209 | 0.03223 | 0.00002 | 37870 | 主因果估计 |
| 加入当届胜选 | 4年内再次参选 | first_position | 0.01198 | 0.00496 | 0.00226 | 0.02169 | 0.01566 | 37870 | 处理后调整:仅描述 |
| 加入当届得票份额 | 4年内再次参选 | first_position | 0.00899 | 0.00494 | -0.00070 | 0.01867 | 0.06899 | 37870 | 处理后调整:仅描述 |
| 同时加入胜选和得票份额 | 4年内再次参选 | first_position | 0.00743 | 0.00491 | -0.00219 | 0.01705 | 0.13015 | 37870 | 处理后调整:仅描述 |
旧项目把“第一位”在每场竞选内随机放给一人,忽略了同一日期共用一套字母表,因此只可作为辅助安慰剂。要运行,把
YAML 中 run_contest_permutation 改为
true。
if (isTRUE(params$run_contest_permutation)) {
set.seed(1965)
B <- as.integer(params$contest_permutation_reps)
pool_simple <- main[, .(row_id, random_contest_id)]
perm_est <- numeric(B)
for (b in seq_len(B)) {
pool_simple[, perm_first := as.integer(seq_len(.N) == sample.int(.N, 1L)), by = random_contest_id]
d <- copy(main)
d[pool_simple, on = "row_id", perm_first := i.perm_first]
fit <- feols(
as.formula(paste0("run_again_4yr ~ perm_first | ", fe_rhs)),
data = d, notes = FALSE, warn = FALSE
)
perm_est[b] <- coef(fit)[["perm_first"]]
}
p_perm <- (1 + sum(abs(perm_est) >= abs(headline))) / (B + 1)
show_result(data.table(B = B, observed = headline, null_mean = mean(perm_est),
null_sd = sd(perm_est), p_two_sided_plus1 = p_perm))
} else {
cat("本次 Knit 未运行辅助场次内置换。\n")
}
## 本次 Knit 未运行辅助场次内置换。
正式 RI 每次在每个精确选举日期重抽一套完整 26 字母顺序,先给完整竞选候选人重新排位,再把伪第一位回接主样本。这一过程保留了真实制度中的日期层级相关性。
if (isTRUE(params$run_design_ri)) {
ri_pool <- as.data.table(readRDS(file.path(
processed_dir, "随机排位_完整竞选候选人_设计型RI分配池.rds"
)))
ri_pool[, `:=`(
surname_clean = normalize_letters(last),
given_clean = normalize_letters(first)
)]
ri_pool[, sort_name_clean := paste0(surname_clean, given_clean)]
setorder(ri_pool, exact_date, random_contest_id, row_id)
date_index <- split(seq_len(nrow(ri_pool)), as.character(ri_pool$exact_date))
alphabet <- paste(LETTERS, collapse = "")
B <- as.integer(params$ri_reps)
ri_est <- numeric(B)
for (b in seq_len(B)) {
set.seed(20260726L + b)
translated <- character(nrow(ri_pool))
for (date_now in names(date_index)) {
idx <- date_index[[date_now]]
random_alphabet <- sample(LETTERS, 26L, replace = FALSE)
rank_letters <- paste(LETTERS[match(LETTERS, random_alphabet)], collapse = "")
translated[idx] <- chartr(alphabet, rank_letters, ri_pool$sort_name_clean[idx])
}
sim <- data.table(
row_id = ri_pool$row_id, random_contest_id = ri_pool$random_contest_id,
translated = translated
)
setorder(sim, random_contest_id, translated, row_id)
sim[, pseudo_first := as.integer(seq_len(.N) == 1L), by = random_contest_id]
d <- copy(main)
d[sim, on = "row_id", pseudo_first := i.pseudo_first]
fit <- feols(
as.formula(paste0("run_again_4yr ~ pseudo_first | ", fe_rhs)),
data = d, cluster = ~random_contest_id, notes = FALSE, warn = FALSE
)
ri_est[b] <- coef(fit)[["pseudo_first"]]
}
ri_p <- (1 + sum(abs(ri_est) >= abs(headline))) / (B + 1)
show_result(data.table(
reps = B, real_itt = headline, null_mean = mean(ri_est), null_sd = sd(ri_est),
p_two_sided_plus1 = ri_p,
note = ifelse(B < 1000, "冒烟测试;不是正式 p 值", "正式 1000 次 RI")
))
} else {
cat("本次 Knit 未运行正式设计型 RI。把 run_design_ri 改为 true;先用 20 次检查,再用 1000 次正式运行。\n")
}
## 本次 Knit 未运行正式设计型 RI。把 run_design_ri 改为 true;先用 20 次检查,再用 1000 次正式运行。
R 不能把 NumPyro 的 MCMC 自动变成“R 的结果”。本项目贝叶斯模型是
Python/JAX/NumPyro 模型。配套文件
加州项目_Python贝叶斯交互复现.py 使用 # %%
单元格,可在 VS Code 或支持 Python Cell
的编辑器中逐格运行;它直接写出数据编码、先验、分层逻辑回归、NUTS
采样和诊断,不调用原 11B 脚本。
贝叶斯因变量仍是 run_again_4yr,处理变量仍是
first_position,但链接函数为
logit,并允许日期、县、职位的截距和处理斜率随机变化:
\[logit\{P(Y_i=1)\}=\eta_i+Z_i\Delta_i,\]
\[\eta_i=\alpha+X_i\beta+a_{date[i]}+a_{county[i]}+a_{office[i]},\]
\[\Delta_i=\beta_Z+X_i\theta+b_{date[i]}+b_{county[i]}+b_{office[i]}.\]
主报告不直接解释 log-odds,而把每个后验抽样转换为概率尺度平均边际效应(AME)。
checks <- data.table(
check = c(
"主样本行数为 37,870", "主样本竞选为 16,213",
"四年再参选 ITT 复现 0.02216217", "一阶段复现 0.04900263",
"一阶段 F 大于 10", "现场重估主表与保存主表一致"
),
pass = c(
nrow(main) == 37870,
uniqueN(main$random_contest_id) == 16213,
abs(headline - 0.02216217) < 0.0000001,
abs(coef(first_stage)[["first_position"]] - 0.04900263) < 0.0000001,
first_stage_F > 10,
all(compare_main$status == "PASS")
)
)
checks[, status := ifelse(pass, "PASS", "FAIL")]
show_result(checks[, .(check, status)])
| check | status |
|---|---|
| 主样本行数为 37,870 | PASS |
| 主样本竞选为 16,213 | PASS |
| 四年再参选 ITT 复现 0.02216217 | PASS |
| 一阶段复现 0.04900263 | PASS |
| 一阶段 F 大于 10 | PASS |
| 现场重估主表与保存主表一致 | PASS |
stopifnot(all(checks$pass))
如果这一章全部显示 PASS,说明当前电脑上的数据、R
包和上述直接回归能复现核心数值。它不自动替代人工姓名/案例核验,也不代表
Python 贝叶斯链已经重新采样。
sessionInfo()
## R version 4.5.1 (2025-06-13)
## Platform: aarch64-apple-darwin20
## Running under: macOS Tahoe 26.2
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: Asia/Shanghai
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] ggplot2_4.0.3 sandwich_3.1-1 lmtest_0.9-40 zoo_1.8-15 fixest_0.14.0 data.table_1.18.0
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 jsonlite_2.0.0 dplyr_1.2.1 compiler_4.5.1 tidyselect_1.2.1
## [6] Rcpp_1.1.1 jquerylib_0.1.4 scales_1.4.0 yaml_2.3.12 fastmap_1.2.0
## [11] lattice_0.22-7 R6_2.6.1 labeling_0.4.3 generics_0.1.4 Formula_1.2-5
## [16] knitr_1.51 tibble_3.3.1 pillar_1.11.1 bslib_0.9.0 RColorBrewer_1.1-3
## [21] rlang_1.2.0 cachem_1.1.0 xfun_0.56 sass_0.4.10 S7_0.2.0
## [26] stringmagic_1.2.0 otel_0.2.0 cli_3.6.5 withr_3.0.2 magrittr_2.0.4
## [31] digest_0.6.39 grid_4.5.1 rstudioapi_0.18.0 lifecycle_1.0.5 nlme_3.1-168
## [36] vctrs_0.7.3 evaluate_1.0.5 glue_1.8.0 farver_2.1.2 numDeriv_2016.8-1.1
## [41] dreamerr_1.5.0 rmarkdown_2.30 pkgconfig_2.0.3 tools_4.5.1 htmltools_0.5.9