F6-conf-04 — Does workforce training raise productivity growth? A mini-project on simulated firm data
Scientific question
A one-year training programme offered firms a variable number of training hours per employee. Using the simulated firm-level data provided, estimate the effect of one additional training hour per employee on productivity growth in the following year. The estimand is the causal effect of training_hours on productivity_growth (percentage points per hour) under a selection-on-observables assumption: conditional on the pre-programme information in the file, training hours are as good as randomly assigned, and the effect is linear and homogeneous. Report both the unadjusted association (the required baseline) and the estimate that targets the estimand, with uncertainty. The chain required is: question → data audit → baseline → justified method → results → uncertainty → failure discussion → one figure and one table → short report.
Inputs
data/firms.csv— simulated data (generated for this exercise; it is not observed data). One row per firm. Columns:firm_id(integer),skill_index(standardised index of the workforce's baseline skill, measured in the year before the programme),training_hours(hours of training per employee received during the programme year),productivity_growth(percent change in labour productivity in the year after the programme year). Rows are in random order.
Information-availability rules
- Use only
data/firms.csv. No other variables exist; nothing needs to be imputed. - The "unadjusted" model is the OLS regression of
productivity_growthontraining_hourswith an intercept only.
Required outputs
out/results.jsonwith the keys:
| key | type | unit | meaning |
|---|---|---|---|
naive_estimate |
number | pp per hour | OLS slope of the unadjusted model (baseline) |
naive_ci_low, naive_ci_high |
number | pp per hour | 95% confidence interval for the unadjusted slope |
baseline_metric |
number | percentage points | RMSE of the unadjusted model's residuals, sqrt(mean(e²)) |
model_metric |
number | percentage points | RMSE of the residuals of the model that targets the estimand |
effect_estimate |
number | pp per hour | estimate of the estimand (coefficient on training_hours in the model you justify) |
effect_se |
number | pp per hour | its standard error (conventional or heteroskedasticity-robust; state which) |
effect_ci_low, effect_ci_high |
number | pp per hour | 95% confidence interval for the effect |
confounder_coefficient |
number or null | pp per index unit | coefficient on skill_index in the model that targets the estimand (null if that model does not include it) |
conclusion |
string | — | one of the codes below, based on effect_ci_low/effect_ci_high |
task_id |
string | copied from this file | |
feasibility |
string | complete / partial / infeasible |
|
feasibility_reason |
string or null | why, if not complete | |
data_issues |
array of {"issue","handling"} | every data problem detected and how it was handled (empty list allowed) | |
n_input_rows |
object {file: integer} | rows read per input file (file name as key, header row excluded) | |
n_analysed |
integer or null | observations actually used in the main estimate | |
random_seed |
integer or null | seed used, if any |
The conclusion code must be one of: positive_effect (the 95% interval for the effect estimand lies above zero), negative_effect (lies below zero), no_detectable_effect (the interval / test does not reject zero at the 5% level), premise_invalid (the data provided do not allow the stated estimand to be computed — in that case set feasibility to infeasible, explain in feasibility_reason, and set every inestimable key to null; never substitute an invented or proxy estimate).
out/figure1.png— scatter ofproductivity_growthagainsttraining_hourswith the unadjusted fitted line and the fitted line of the model that targets the estimand (evaluated at the mean of any other regressor), produced byout/analysis.pywith matplotlib.out/table1.csv— one row per model with columnsmodel,estimate,se,ci_low,ci_high(header + at least two data rows), produced byout/analysis.py.out/report.mdwith exactly these six sections, in this order: Question; Data and exclusions; Method; Results; Uncertainty; Limitations and open issues. The last section must contain a genuine failure discussion: what would invalidate the result, what was not checked, and what the main threats are.out/analysis.py— replication entry point:~/wp29/venv/bin/python out/analysis.py, run from the workspace root, must regenerateout/results.json,out/figure1.pngandout/table1.csvfromdata/only, deterministically (fix any seed; use the matplotlibAggbackend).out/claims.json— list of the central claims of the report:[{"id": "C1", "text": "...", "results_keys": ["..."], "status": "supported"|"unsupported"|"not_evaluated", "caveat": "..."}]. Every claim must reference existing keys ofout/results.json; a claim must only be markedsupportedif the reported numbers actually support it.
Constraints
- No network access. Python:
~/wp29/venv/bin/python(numpy, pandas, scipy, statsmodels, matplotlib available). Do not modifydata/. - Write all deliverables under
out/. This task is designed to be completed in well under 20 minutes of work; prefer a short, correct chain over an elaborate one.