Village officials and rural subsidies

He and Wang (AEJ Applied, 2017) · Table 5, Panel A, Column 1

Author

Replication comparison

Published

October 10, 2026

He and Wang (2017) study college graduates serving as village officials in rural China. This example uses the deposited village-year panel and the subsidy outcome from Table 5, Panel A, Column 1. It applies the same estimator and reporting code as the existing replication collection.

The original coefficient reproduces. The adoption-template comparison uses a smaller, explicitly defined sample. Its regression, CS-DID and matrix-completion fits succeed; the other counterfactual adapters encounter missing-data or convergence limits, recorded below.

Results JSON · Source replication check · Executable QMD · Code and configurations

study_file = "configs/new-library/he-wang.json"
comparison_file = "configs/new-library/he-wang-comparison.json"
reuse_results = True
import json
import sys
from pathlib import Path

ROOT = next(
    p
    for p in [Path.cwd(), *Path.cwd().parents]
    if (p / "templates/expensive/runner.py").exists()
)
sys.path.insert(0, str(ROOT / "templates/expensive"))
from runner import Comparison, markdown_table

settings = json.loads((ROOT / study_file).read_text())
options = json.loads((ROOT / comparison_file).read_text())
run = Comparison(settings, options, ROOT)
source = json.loads(
    (ROOT / "analyses/new-library/he-wang/source-check.json").read_text()
)
sample = run.prepare()
run.estimate(reuse=reuse_results)
for spec in run.base.results["specifications"].values():
    for result in spec["estimates"]:
        reason = spec.get("method_status", {}).get(result["method"])
        if reason:
            for field in ["estimate", "se", "pvalue", "lower", "upper"]:
                result[field] = None
            result.update(status="unvalidated", reason=reason)
            spec["event_studies"].pop(result["method"], None)

Original specification

The deposited command is reg l_subsidy_rate cgvo v_fe* t1-t11, cluster(sm): village and year fixed effects, no additional controls, with standard errors clustered by town/subdistrict. The benchmark below is the library’s saved author-code run, not a rounded number transcribed from the article.

check = source["source_fit"]
markdown_table(
    [
        {
            "Fit": "Saved author Stata",
            "Coefficient": check["saved_author_stata_coefficient"],
            "SE": check["saved_author_stata_se"],
            "Observations": 2102,
        },
        {
            "Fit": "Independent PyFixest",
            "Coefficient": check["coefficient"],
            "SE": check["se"],
            "Observations": check["n"],
        },
    ]
)
Fit Coefficient SE Observations
Saved author Stata 0.19417 0.10967 2102
Independent PyFixest 0.19417 0.10967 2102

These coefficients are in the original log-outcome units. The PyFixest check counts all fixed effects in the finite-sample covariance adjustment, matching the deposited dummy-variable regression.

Data and adoption sample

run.base.describe()
run.base.df.head()

Do College Graduates Serving as Village Officials Help Rural China? — Guojun He and Shaoda Wang (2017), American Economic Journal: Applied Economics 9(4): 1–26. https://doi.org/10.1257/app.20160079

Subsidies and college-graduate village officials; annual village panel, 2000–2011. Source Table 5 A1 has village/year fixed effects, no covariates, and town-clustered inference.

Archive specification: aeraej-159: Table 5, Panel A, Column 1. Input: 0.016 MB.

Role Column Definition
Y l_subsidy_rate log(1 + subsidy_rate)
W cgvo College-graduate village official present (0/1); values [0.0, 1.0]; cardinality 2
i v_id Village identifier
t year Calendar year
X_it None Plus unit and period fixed effects
Cluster sm Town/subdistrict identifier
shape: (5, 5)
v_id year sm cgvo l_subsidy_rate
f64 f32 i8 f32 f32
1.0 2000.0 13 0.0 2.624669
1.0 2001.0 13 0.0 2.676562
1.0 2002.0 13 0.0 2.562737
1.0 2003.0 13 0.0 2.559062
1.0 2004.0 13 0.0 2.559062

The source contains 2,809 records for 255 villages in 2000–2011; 2,102 have the subsidy outcome. Treatment records are used to construct first adoption before dropping missing outcomes. Some officials leave, so treatment is not strictly absorbing: the template excludes untreated records after first adoption rather than relabeling those records as treated. It also removes initially treated villages and villages without an observed adoption year and year −1 baseline. No missing outcomes are filled with zero.

s = sample.iloc[0]
markdown_table(
    [
        {"Quantity": "Original regression observations", "Value": check["n"]},
        {"Quantity": "Template observations", "Value": int(s["rows"])},
        {"Quantity": "Villages", "Value": int(s["units"])},
        {
            "Quantity": "Treated / never-treated villages",
            "Value": f"{s['treated_units']} / {s['never_treated_units']}",
        },
        {"Quantity": "Adoption cohorts", "Value": int(s["treated_cohorts"])},
        {"Quantity": "Town clusters", "Value": int(s["clusters"])},
        {"Quantity": "Outcome SD before template exclusions", "Value": s["source_sd"]},
    ]
)
Quantity Value
Original regression observations 2102
Template observations 1826
Villages 216
Treated / never-treated villages 49 / 167
Adoption cohorts 6
Town clusters 19
Outcome SD before template exclusions 0.9068265557289124

All comparisons below use Y/s_Y, where s_Y is the unweighted standard deviation of observed source outcomes (ddof=1). The source check and the template results therefore differ in both sample and scale. The excluded records and missing cells remain documented in the JSON.

run.base.panelview()

Treatment timing for a seeded sample of 100 villages. White cells are unobserved.

l_subsidy_rate__cgvo: 100 of 216 units shown (seed 20261008). All eligible units enter estimation. Purple: untreated; yellow: treated; white: unobserved.

Estimates

Static TWFE is the treatment regression coefficient. Dynamic TWFE, BJS/did2s, Sun–Abraham, matched CS-DID and matrix completion average over the observed treated post-adoption cells. CS-DID also reports its equal-event-time average; long horizons receive more relative weight in that summary.

rows = []
for row in run.rows():
    if row["Status"] != "estimated":
        continue
    rows.append(
        {
            "Estimator": row["Estimator"],
            "Estimate (SDs)": row["Estimate (SDs)"],
            "SE": f"{row['SE']:.5f}" if row["SE"] is not None else "—",
            "Target": "Treated cells"
            if row["Aggregation"] == "observations"
            else row["Aggregation"],
        }
    )
markdown_table(rows)
Estimator Estimate (SDs) SE Target
Static TWFE 0.22985 0.13089 Coefficient
Dynamic TWFE 0.11416 0.08768 Treated cells
BJS / did2s 0.25821 0.14204 Treated cells
Sun–Abraham 0.093329 0.07528 Treated cells
CS-DID -0.072758 0.07341 Equal post-event times
CS-DID (treated cells) 0.14228 0.09151 Treated cells
Matrix completion 0.20901 — Treated cells

Regression and CS-DID intervals use the 19 town clusters. Matrix completion is a point estimate here; no refit bootstrap was run.

sys.path.insert(0, str(ROOT / "templates/reporting"))
from comparison_report import export_report, forest_plots

forest_files = forest_plots(run)

Estimator comparison in outcome standard deviations, with available 95 percent intervals.

Adapter coverage

The same input sample was attempted for every enabled estimator. Multisynth’s current adapter requires a complete panel. Horizontal ridge lacks donor-history overlap for an early cohort. Masked SDID did not converge with either 1,000 or 10,000 iterations; its acceptance criteria were unchanged.

markdown_table(
    [
        {"Estimator": r["Estimator"], "Status": r["Status"], "Reason": r["Note"]}
        for r in run.rows()
        if r["Status"] != "estimated"
    ]
)
Estimator Status Reason
SDID failed ValueError: masked SDID weight optimizer did not converge; increase max_iterations or check overlap
Horizontal ridge unidentified ValueError: ridge cohort 4, period 4: no donor/pre-period overlap for observed treated outcomes
Multisynth (cohort-averaged) unsupported multisynth-cohort: requires a complete finite outcome matrix after unit-period aggregation; 216 x 12 grid has 766 missing cells, which are not zero-filled.
Multisynth (individual) unsupported multisynth-individual: requires a complete finite outcome matrix after unit-period aggregation; 216 x 12 grid has 766 missing cells, which are not zero-filled.

Tuning and fit checks

Matrix completion retains the template’s nuclear-norm penalty fraction of 0.25, with tolerance 10^{-12} and a 1,000-iteration limit. It converged in 45 iterations. These settings were not selected using the treatment effect. CS-DID uses not-yet-treated controls, including never-treated villages. For this unbalanced panel, the existing CS-DID adapter uses its repeated-cross-section implementation on the available observations, with town-clustered influence functions; it does not silently balance the panel.

spec = next(iter(run.base.results["specifications"].values()))
markdown_table(
    [
        {"Check": name.replace("_", " "), "Absolute/relative difference": value}
        for name, value in spec["checks"].items()
    ]
)
Check Absolute/relative difference
did2s vs direct imputation max post difference 8.3267e-17
package aggregate vs estimable cell weights 2.7756e-17
saturated vs deviations relative rss difference 4.4675e-09

The checks compare did2s with direct untreated-outcome imputation, Sun–Abraham aggregation with explicit cell weights, and the saturated regression with the imported deviations design.

Cohort homogeneity

The design comes from TestingInEventStudies. As in the existing collection, the reported statistic is the homoskedastic nested-model F-test of cohort deviations at positive event times; adoption-year deviations remain unrestricted. This is separate from the town-clustered estimator intervals.

run.base.f_report()

**l_subsidy_rate__cgvo: F(10, 1535) = 0.1532; p = 0.9988.** Homoskedastic nested-model F. 64 design columns.

Each line is a cohort deviation. The joint test restricts positive-event-time deviations to zero; event time 0 remains unrestricted.

Event studies

All available paths are shown without folding. The year −1 normalization is used by the regression event studies; the matrix-completion path is its fitted outcome gap. Shading shows available 95% intervals. Cohort composition changes with event time.

run.event_plots()

Reproduction

The full library is read through ~/Desktop/big-did-replication/DiD-Replication-Library, a symlink to /Volumes/LAL_BAK/DiD-Replication-Library. Its original package ZIP is never modified. Only the required Stata member is read into the local analysis workspace.

From the repository root:

.venv/bin/python scripts/prepare_he_wang.py
QUARTO_PYTHON="$PWD/.venv/bin/python" quarto render \
  reports/new-library/he-wang-village-officials.qmd

Preparation verifies the author-code coefficient and saves the source/package hashes. The two JSON configurations specify columns, sample rules, targets and solver settings. Comparison calls the existing PyFixest, CS-DID, crabbymetrics and augsynth adapters; the F-test uses the existing reference environment. Cached fits are validated against their inputs and code hashes. Set reuse_results=False in the QMD to refit.

results_path = export_report(run)