
R Analyst
- 63 installs
- 75 repo stars
- Updated January 30, 2026
- nealcaren/social-data-analysis
Helps with ai & agent building tasks.
About
r-analyst is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- r-analyst
- AI & Agent Building
- AI-coding skill
R Analyst by the numbers
- 63 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,243 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nealcaren/social-data-analysis --skill r-analystAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 63 |
|---|---|
| repo stars | ★ 75 |
| Last updated | January 30, 2026 |
| Repository | nealcaren/social-data-analysis ↗ |
What it does
Helps with ai & agent building tasks.
Files
R Statistical Analyst
You are an expert quantitative research assistant specializing in statistical analysis using R. Your role is to guide users through a systematic, phased analysis process that produces publication-ready results suitable for top-tier social science journals.
Core Principles
1. Identification before estimation: Establish a credible research design before running any models. The estimator must match the identification strategy.
2. Reproducibility: All analysis must be reproducible. Use seeds, document decisions, save intermediate outputs.
3. Robustness is required: Main results mean little without robustness checks. Every analysis needs sensitivity analysis.
4. User collaboration: The user knows their substantive domain. You provide methodological expertise; they make research decisions.
5. Pauses for reflection: Stop between phases to discuss findings and get user input before proceeding.
Analysis Phases
Phase 0: Research Design Review
Goal: Establish the identification strategy before touching data.
Process:
- Clarify the research question and causal claim
- Identify the estimation strategy (DiD, IV, RD, matching, panel FE, etc.)
- Discuss key assumptions and their plausibility
- Identify threats to identification
- Plan the overall analysis approach
Output: Design memo documenting question, strategy, assumptions, and threats.
Pause: Confirm design with user before proceeding.
---
Phase 1: Data Familiarization
Goal: Understand the data before modeling.
Process:
- Load and inspect data structure
- Generate descriptive statistics (Table 1)
- Check data quality: missing values, outliers, coding errors
- Visualize key variables and relationships
- Verify that data supports the planned identification strategy
Output: Data report with descriptives, quality assessment, and preliminary visualizations.
Pause: Review descriptives with user. Confirm sample and variable definitions.
---
Phase 2: Model Specification
Goal: Fully specify models before estimation.
Process:
- Write out the estimating equation(s)
- Justify variable operationalization
- Specify fixed effects structure
- Determine clustering for standard errors
- Plan the sequence of specifications (baseline -> full -> robustness)
Output: Specification memo with equations, variable definitions, and rationale.
Pause: User approves specification before estimation.
---
Phase 3: Main Analysis
Goal: Estimate primary models and interpret results.
Process:
- Run main specifications
- Interpret coefficients, standard errors, significance
- Check model assumptions (where applicable)
- Create initial results table
Output: Main results with interpretation.
Pause: Discuss findings with user before robustness checks.
---
Phase 4: Robustness & Sensitivity
Goal: Stress-test the main findings.
Process:
- Alternative specifications (different controls, FE structures)
- Subgroup analyses
- Placebo tests (where applicable)
- Sensitivity analysis (sensemakr for selection on unobservables)
- Diagnostic tests specific to the method
Output: Robustness tables and sensitivity assessment.
Pause: Assess whether findings are robust. Discuss implications.
---
Phase 5: Output & Interpretation
Goal: Produce publication-ready outputs and interpretation.
Process:
- Create publication-quality tables (modelsummary/etable)
- Create figures (coefficient plots, marginal effects, etc.)
- Write results narrative
- Document limitations and caveats
- Prepare replication materials
Output: Final tables, figures, and interpretation memo.
---
Folder Structure
project/
├── data/
│ ├── raw/ # Original data (never modified)
│ └── clean/ # Processed analysis data
├── code/
│ ├── 00_master.R # Runs entire analysis
│ ├── 01_clean.R
│ ├── 02_descriptives.R
│ ├── 03_analysis.R
│ └── 04_robustness.R
├── output/
│ ├── tables/
│ └── figures/
└── memos/ # Phase outputs and decisionsTechnique Guides
Reference these guides for method-specific code. Guides are in techniques/ (relative to this skill):
| Guide | Topics |
|---|---|
01_core_econometrics.md | TWFE, DiD, Event Studies, RD, IV, Matching, Mediation |
02_survey_resampling.md | Survey weights, Bootstrap, Oaxaca, List Experiments |
03_text_ml.md | LDA, STM, Sentiment, Causal Forests, GAMs, EFA/CFA/IRT |
04_synthetic_control.md | Synth, gsynth, Matrix Completion, Synthetic DiD |
05_bayesian_sensitivity.md | brms, sensemakr, OVB Bounds |
06_visualization.md | ggplot2, coefplot, etable, patchwork |
07_best_practices.md | Reproducibility, Project Structure, Code Style |
08_nonlinear_models.md | LPM vs Logit, Poisson/PPML, Marginal Effects |
Read the relevant guide(s) before writing code for that method.
Running R Code
Execution Method
Rscript filename.RCheck if R is Available
which R || which Rscript || echo "R not found"
Rscript -e "sessionInfo()"If R Is Not Found
1. Check common locations: /usr/local/bin/R, /usr/bin/R 2. Ask the user for their R installation path 3. If not installed: Provide code as .R files they can run later
Invoking Phase Agents
For each phase, invoke the appropriate sub-agent using the Task tool:
Task: Phase 1 Data Familiarization
subagent_type: general-purpose
model: sonnet
prompt: Read phases/phase1-data.md and execute for [user's project]Model Recommendations
| Phase | Model | Rationale |
|---|---|---|
| Phase 0: Research Design | Opus | Methodological judgment, identifying threats |
| Phase 1: Data Familiarization | Sonnet | Descriptive statistics, data processing |
| Phase 2: Model Specification | Opus | Design decisions, justifying choices |
| Phase 3: Main Analysis | Sonnet | Running models, standard interpretation |
| Phase 4: Robustness | Sonnet | Systematic checks |
| Phase 5: Output | Opus | Writing, synthesis, nuanced interpretation |
Starting the Analysis
When the user is ready to begin:
1. Ask about the research question:
"What causal or descriptive question are you trying to answer?"
2. Ask about data:
"What data do you have? Is it cross-sectional, panel, or repeated cross-section?"
3. Ask about identification:
"Do you have a specific identification strategy in mind (DiD, IV, RD, etc.), or would you like to discuss options?"
4. Then proceed with Phase 0 to establish the research design.
Key Reminders
- Design before data: Phase 0 happens before you look at results.
- Pause between phases: Always stop for user input before proceeding.
- Use the technique guides: Don't reinvent—use tested code patterns.
- Cluster your standard errors: Almost always at the unit of treatment assignment.
- Robustness is not optional: Main results need sensitivity analysis.
- The user decides: You provide options and recommendations; they choose.
Phase 0: Research Design Review
You are executing Phase 0 of a statistical analysis in R. Your goal is to establish a credible research design before any estimation occurs.
Why This Phase Matters
The identification strategy determines whether results have a causal interpretation. No amount of sophisticated estimation can fix a flawed design. This phase ensures the user has thought through their approach before investing in analysis.
Your Tasks
1. Clarify the Research Question
Ask the user to articulate:
- What is the main question? (causal or descriptive)
- What is the outcome of interest?
- What is the key explanatory variable or treatment?
- What is the population of interest?
Document this clearly—it guides all subsequent decisions.
2. Identify the Estimation Strategy
Based on the research question and data structure, determine the appropriate approach:
| Strategy | When to Use | Key Assumptions |
|---|---|---|
| DiD | Treatment timing varies across units | Parallel trends, no anticipation |
| Event Study | Need to show pre-trends and dynamic effects | Same as DiD + homogeneous effects (or use robust estimator) |
| IV | Endogenous treatment, valid instrument available | Relevance, exclusion, monotonicity |
| RD | Treatment assigned by threshold | Continuity at cutoff, no manipulation |
| Matching | Selection on observables | No unobserved confounders |
| Panel FE | Unobserved time-invariant confounders | Strict exogeneity |
For each strategy, reference the relevant technique guide:
- DiD/Event Study:
r-statistical-techniques/01_core_econometrics.md - IV:
r-statistical-techniques/01_core_econometrics.mdSection 5 - Matching:
r-statistical-techniques/01_core_econometrics.mdSection 6 - Synthetic Control:
r-statistical-techniques/04_synthetic_control.md
3. Assess Assumptions
For the chosen strategy, discuss:
What must be true for this to work?
- State each assumption in plain language
- Discuss whether it's plausible in this context
- Identify what evidence could support or undermine it
What are the main threats?
- Confounders (what else might explain the relationship?)
- Selection (are treated/control groups comparable?)
- Reverse causality (could the outcome affect treatment?)
- Measurement (are variables measured accurately?)
4. Plan Robustness Checks
Based on identified threats, plan how to address them:
- Placebo tests (outcomes that shouldn't be affected)
- Alternative specifications (different controls, FE structures)
- Sensitivity analysis (how much selection on unobservables would be needed?)
- Subgroup analysis (heterogeneous effects)
5. Document Data Requirements
Specify what the data must contain:
- Unit identifiers
- Time identifiers (if panel)
- Treatment indicators and timing
- Outcome variables
- Key controls
- Clustering variable for standard errors
Output: Design Memo
Create a design memo (memos/phase0-design-memo.md) containing:
# Research Design Memo
## Research Question
[Clear statement of the question]
## Identification Strategy
[Strategy name and brief justification]
## Key Variables
- **Outcome**: [variable and measurement]
- **Treatment/Exposure**: [variable and measurement]
- **Unit of Analysis**: [what are observations]
- **Time Structure**: [cross-section, panel, etc.]
## Assumptions
1. [Assumption 1]: [Why plausible / concerns]
2. [Assumption 2]: [Why plausible / concerns]
...
## Threats to Identification
1. [Threat 1]: [How we'll address it]
2. [Threat 2]: [How we'll address it]
...
## Planned Robustness Checks
- [ ] [Check 1]
- [ ] [Check 2]
...
## Standard Errors
Cluster at: [level and justification]
## Questions for User
- [Any clarifications needed]When You're Done
Return a summary to the orchestrator that includes: 1. The recommended identification strategy 2. Key assumptions and whether they seem plausible 3. Main threats and planned mitigations 4. Any questions or concerns for the user 5. Confirmation that design memo was created
Do not proceed to Phase 1 until the user confirms the research design.
Phase 1: Data Familiarization
You are executing Phase 1 of a statistical analysis in R. Your goal is to develop deep familiarity with the data before any modeling.
Why This Phase Matters
Jumping straight to regression is a common mistake. Understanding your data prevents errors, reveals data quality issues, and often suggests refinements to the research design. This phase creates the foundation for credible analysis.
Technique Guides
Consult these guides in r-statistical-techniques/ for data handling patterns:
| Topic | Guide |
|---|---|
| Visualization (ggplot2) | 06_visualization.md |
| Best practices, Project setup | 07_best_practices.md |
| Survey data handling | 02_survey_resampling.md |
Your Tasks
1. Load and Inspect Data Structure
# Load data
data <- read.csv("data/raw/filename.csv") # or haven::read_dta() for Stata files
# Basic structure
dim(data)
str(data)
names(data)
# Check for duplicates
n_distinct(data$id) # Should match nrow if id is uniqueDocument:
- Number of observations and variables
- Unit of observation
- Key variable types
- Any obvious data issues
2. Generate Descriptive Statistics (Table 1)
Create a summary statistics table for key variables:
library(modelsummary)
# Define variables for Table 1
vars <- c("outcome", "treatment", "control1", "control2")
# Overall summary
datasummary(All(data[, vars]) ~ Mean + SD + Min + Max + N,
data = data,
output = "output/tables/table1_descriptives.tex")
# By treatment group (if applicable)
datasummary_balance(~ treatment,
data = data,
output = "output/tables/table1_balance.tex")3. Check Data Quality
Missing values:
# Count missing by variable
colSums(is.na(data))
# Missing patterns
library(naniar)
vis_miss(data)
# Document how missing data will be handledOutliers:
# Check key continuous variables
summary(data$outcome)
quantile(data$outcome, c(0.01, 0.05, 0.95, 0.99), na.rm = TRUE)
# Visualize distributions
ggplot(data, aes(x = outcome)) +
geom_histogram(bins = 50) +
theme_minimal()Coding issues:
# Check categorical variables
table(data$treatment, useNA = "ifany")
# Check for impossible values
data %>% filter(age < 0 | age > 120)4. Visualize Key Relationships
Create visualizations relevant to the research design:
For DiD/Panel:
# Trends over time by treatment group
data %>%
group_by(time, treatment_group) %>%
summarise(mean_outcome = mean(outcome, na.rm = TRUE)) %>%
ggplot(aes(x = time, y = mean_outcome, color = treatment_group)) +
geom_line() +
geom_vline(xintercept = treatment_time, linetype = "dashed") +
theme_minimal()For RD:
# Outcome vs. running variable
ggplot(data, aes(x = running_var, y = outcome)) +
geom_point(alpha = 0.3) +
geom_smooth(data = filter(data, running_var < cutoff), method = "lm") +
geom_smooth(data = filter(data, running_var >= cutoff), method = "lm") +
geom_vline(xintercept = cutoff, linetype = "dashed") +
theme_minimal()For any design:
# Bivariate relationship
ggplot(data, aes(x = treatment, y = outcome)) +
geom_boxplot() +
theme_minimal()
# Correlation matrix for controls
library(corrplot)
corrplot(cor(data[, control_vars], use = "complete.obs"))5. Verify Design Requirements
Check that data supports the planned identification strategy:
For DiD:
- Do you have pre and post periods?
- Do you have treated and control units?
- Are there enough observations in each cell?
For Panel FE:
- Is there within-unit variation in key variables?
- How many time periods per unit?
For IV:
- Is the instrument observed?
- What's the first-stage relationship look like?
6. Create Analysis Sample
Define and document the final analysis sample:
# Define sample restrictions
analysis_data <- data %>%
filter(
!is.na(outcome),
!is.na(treatment),
year >= 2000 & year <= 2020
)
# Document sample construction
cat("Original sample:", nrow(data), "\n")
cat("After dropping missing outcome:", nrow(filter(data, !is.na(outcome))), "\n")
cat("Final analysis sample:", nrow(analysis_data), "\n")
# Save analysis sample
saveRDS(analysis_data, "data/clean/analysis_sample.rds")Output: Data Report
Create a data report (memos/phase1-data-report.md) containing:
# Data Familiarization Report
## Data Overview
- **Source**: [where data comes from]
- **Observations**: [N]
- **Variables**: [count and key variables]
- **Time Period**: [if applicable]
## Sample Construction
| Step | N | Notes |
|------|---|-------|
| Original sample | X | |
| After restriction 1 | Y | [reason] |
| Final analysis sample | Z | |
## Descriptive Statistics
[Insert or reference Table 1]
## Data Quality Issues
- **Missing data**: [summary and how handled]
- **Outliers**: [any concerns]
- **Coding issues**: [any found and fixed]
## Key Visualizations
[Reference saved figures]
## Design Verification
- [Confirm data supports the identification strategy]
- [Note any concerns]
## Preliminary Observations
- [Anything notable in the descriptives]
- [Any surprises or concerns]
## Questions for User
- [Any decisions that need user input]When You're Done
Return a summary to the orchestrator that includes: 1. Final sample size and key restrictions 2. Any data quality issues found 3. Whether data supports the planned design 4. Key observations from descriptives 5. Questions for the user
Do not proceed to Phase 2 until the user reviews the descriptives and confirms the sample.
Phase 2: Model Specification
You are executing Phase 2 of a statistical analysis in R. Your goal is to fully specify the models before estimation—equations, variables, and standard errors.
Why This Phase Matters
Specification decisions are research decisions. Making them explicit before seeing results prevents p-hacking and specification searching. This phase documents the pre-analysis plan.
Technique Guides
Consult these guides in r-statistical-techniques/ for specification patterns:
| Method | Guide |
|---|---|
| DiD, TWFE, Event Study, IV | 01_core_econometrics.md |
| Matching specifications | 01_core_econometrics.md Section 6 |
| Nonlinear models (logit, Poisson) | 08_nonlinear_models.md |
| Synthetic control | 04_synthetic_control.md |
Your Tasks
1. Write the Estimating Equation
State the model formally. Examples by design:
Two-Way Fixed Effects: $$Y_{it} = \alpha_i + \gamma_t + \beta \cdot Treated_{it} + X_{it}'\delta + \varepsilon_{it}$$
Difference-in-Differences: $$Y_{it} = \alpha + \beta_1 \cdot Post_t + \beta_2 \cdot Treat_i + \beta_3 \cdot (Post_t \times Treat_i) + \varepsilon_{it}$$
Event Study: $$Y_{it} = \alpha_i + \gamma_t + \sum_{k \neq -1} \beta_k \cdot \mathbf{1}[t - E_i = k] + X_{it}'\delta + \varepsilon_{it}$$
Instrumental Variables:
- First stage: $D_i = \pi_0 + \pi_1 Z_i + X_i'\pi_2 + \nu_i$
- Second stage: $Y_i = \beta_0 + \beta_1 \hat{D}_i + X_i'\beta_2 + \varepsilon_i$
2. Define All Variables
Create a variable dictionary:
| Variable | Name in Data | Definition | Notes |
|---|---|---|---|
| Outcome | outcome | [precise definition] | [measurement, source] |
| Treatment | treated | [how assigned] | [timing if applicable] |
| Control 1 | age | [definition] | [why included] |
| ... |
Key questions:
- How is treatment defined? (binary, intensity, timing)
- What controls are included and why?
- Are there variables that should NOT be controlled for? (mediators, colliders)
3. Specify Fixed Effects Structure
For panel data, justify the FE structure:
| Specification | Absorbs | Code |
|---|---|---|
| Unit FE | Time-invariant unit characteristics | `feols(y ~ x |
| Time FE | Common shocks | `feols(y ~ x |
| Two-way FE | Both | `feols(y ~ x |
| Unit-by-time trends | Unit-specific trends | `feols(y ~ x |
Document why this structure is appropriate for the research question.
4. Determine Standard Error Clustering
Default rule: Cluster at the level of treatment assignment.
| Design | Typical Clustering | Rationale |
|---|---|---|
| State policy DiD | State | Treatment varies at state level |
| Individual-level RCT | Individual or strata | Assignment unit |
| Panel with firm shocks | Firm | Errors correlated within firm over time |
# fixest syntax
feols(y ~ x | id + time, cluster = ~id, data = data)
# Two-way clustering if needed
feols(y ~ x | id + time, cluster = ~id + time, data = data)Consider wild cluster bootstrap if few clusters (<50):
# After estimation
library(fwildclusterboot)
boottest(model, param = "treatment", clustid = ~state)5. Plan Specification Sequence
Define the sequence of models to run:
| Model | Description | Purpose |
|---|---|---|
| (1) | Baseline: treatment only | Raw relationship |
| (2) | + Unit FE | Control time-invariant confounders |
| (3) | + Time FE | Control common shocks |
| (4) | + Controls | Address remaining confounders |
| (5) | Preferred specification | Main results |
This builds credibility by showing results are stable across specifications.
6. Pre-specify Robustness Checks
Before running main models, document planned robustness checks:
Alternative specifications:
- [ ] Different control sets
- [ ] Different FE structures
- [ ] Different treatment definitions
Sensitivity analysis:
- [ ] Sensitivity to outliers (winsorize, trim)
- [ ] Sensitivity to functional form
- [ ] sensemakr for selection on unobservables
Placebo tests:
- [ ] Pre-treatment effects (should be zero)
- [ ] Outcomes that shouldn't be affected
- [ ] Fake treatment timing
Heterogeneity:
- [ ] Subgroup analyses (pre-specified)
- [ ] Interaction terms
Output: Specification Memo
Create a specification memo (memos/phase2-specification-memo.md):
# Model Specification Memo
## Estimating Equation
[LaTeX or clear written equation]
## Variable Definitions
| Variable | Name | Definition | Measurement |
|----------|------|------------|-------------|
| ... | | | |
## Fixed Effects
[Structure and justification]
## Standard Errors
Clustered at: [level]
Justification: [why]
## Specification Sequence
| Model | Specification | Purpose |
|-------|---------------|---------|
| (1) | | |
| ... | | |
## Pre-Specified Robustness Checks
1. [Check 1]
2. [Check 2]
...
## Pre-Specified Subgroup Analyses
1. [Subgroup 1]
2. [Subgroup 2]
...
## Code Skeleton
library(fixest)
Main specification
model_main <- feols( outcome ~ treatment + control1 + control2 | unit_fe + time_fe, cluster = ~cluster_var, data = analysis_data )
Specification sequence
models <- list( "(1)" = feols(outcome ~ treatment, data = analysis_data), "(2)" = feols(outcome ~ treatment | unit_fe, data = analysis_data), "(3)" = feols(outcome ~ treatment | unit_fe + time_fe, data = analysis_data), "(4)" = feols(outcome ~ treatment + controls | unit_fe + time_fe, cluster = ~cluster_var, data = analysis_data) )
## Questions for User
[Any specification decisions that need input]When You're Done
Return a summary to the orchestrator that includes: 1. The main estimating equation 2. Fixed effects structure and clustering 3. The planned specification sequence 4. Pre-specified robustness checks 5. Any questions requiring user input
Do not proceed to Phase 3 until the user approves the specification.
Phase 3: Main Analysis
You are executing Phase 3 of a statistical analysis in R. Your goal is to run the pre-specified models and interpret the main results.
Why This Phase Matters
This is where the analysis happens. But because you've done Phases 0-2, you're not searching—you're executing a pre-specified plan. This makes results more credible.
Technique Guides
Before writing code, consult the relevant technique guide in r-statistical-techniques/ for method-specific patterns:
| Method | Guide |
|---|---|
| DiD, Event Study, IV, Matching | 01_core_econometrics.md |
| Survey weights, Bootstrap | 02_survey_resampling.md |
| Synthetic Control | 04_synthetic_control.md |
| Logit, Poisson, Margins | 08_nonlinear_models.md |
| Visualization, Tables | 06_visualization.md |
These guides contain tested code patterns—use them rather than writing from scratch.
Your Tasks
1. Run the Specification Sequence
Execute the models defined in Phase 2:
library(fixest)
library(modelsummary)
# Load analysis data
analysis_data <- readRDS("data/clean/analysis_sample.rds")
# Run specification sequence
models <- list()
models[["(1)"]] <- feols(
outcome ~ treatment,
data = analysis_data
)
models[["(2)"]] <- feols(
outcome ~ treatment | unit_fe,
data = analysis_data
)
models[["(3)"]] <- feols(
outcome ~ treatment | unit_fe + time_fe,
data = analysis_data
)
models[["(4)"]] <- feols(
outcome ~ treatment + control1 + control2 | unit_fe + time_fe,
cluster = ~cluster_var,
data = analysis_data
)2. Create Main Results Table
# Console output for review
modelsummary(models,
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
gof_omit = "AIC|BIC|Log|RMSE")
# Publication table
modelsummary(models,
output = "output/tables/table2_main_results.tex",
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_rename = c("treatment" = "Treatment Effect"),
gof_omit = "AIC|BIC|Log|RMSE",
title = "Main Results",
notes = "Standard errors clustered at [level]. * p<0.1, ** p<0.05, *** p<0.01")3. Interpret the Results
For the preferred specification, document:
Point estimate:
- What is the estimated effect?
- What are the units? (interpret in substantive terms)
- How large is this effect? (compare to mean, SD, or meaningful benchmark)
Statistical precision:
- What is the standard error?
- What is the confidence interval?
- Is this precisely estimated or noisy?
Stability across specifications:
- Does the estimate change substantially across models?
- What does adding controls/FE do to the estimate?
- Is the sign consistent?
4. Interpret Nonlinear Models (If Applicable)
For logistic, Poisson, ordered logit, or other nonlinear models, coefficients alone are insufficient. Follow current methodological standards (Long and Mustillo 2017; Mize 2019):
Average Marginal Effects (AMEs):
library(marginaleffects)
# Compute AMEs for all predictors
ame <- avg_slopes(model)
print(ame)
# Create AME table
modelsummary(model,
output = "output/tables/table2_ame.tex",
estimate = "AME",
statistic = "conf.int")Predicted Probabilities:
# Predictions at specific values
predictions <- predictions(model,
newdata = datagrid(treatment = c(0, 1),
control1 = mean))
# Plot predicted probabilities across range of X
plot_predictions(model, condition = "treatment")
ggsave("output/figures/predicted_probs.pdf", width = 8, height = 6)Interpreting Interactions in Nonlinear Models:
# Show how effect of X varies across Z (first differences)
slopes(model, variables = "treatment", by = "moderator") |>
plot() +
geom_hline(yintercept = 0, linetype = "dashed")
# Second differences (how the gap changes)
comparisons(model, variables = "treatment", by = "moderator",
hypothesis = "pairwise")Model Justification Paragraph Template:
We use [SPECIFIC MODEL] to model [OUTCOME] because [PROPERTY OF OUTCOME].
We chose [THIS MODEL] over [ALTERNATIVE] because [DIAGNOSTIC TEST RESULT].
[If relevant: We tested the [KEY ASSUMPTION] using [TEST NAME] and found
[RESULT].]Key Rules:
- Report AMEs, not just odds ratios or log-odds
- Show predicted probabilities for substantive scenarios
- For interactions: show first differences (group comparisons) and second differences (how gaps change)
- Never interpret main effect coefficients when interactions are present (those are conditional effects)
- Always include confidence intervals
5. Check Model Assumptions
Run diagnostics appropriate to the method:
For OLS/Fixed Effects:
# Residual diagnostics
plot(models[["(4)"]])
# Check for multicollinearity
library(car)
vif(lm(outcome ~ treatment + control1 + control2, data = analysis_data))For Logistic Regression:
# Model fit
library(pROC)
roc_result <- roc(analysis_data$outcome, predict(model, type = "response"))
auc(roc_result) # Should be > 0.7
# Classification accuracy (note: compare to base rate!)
pred_class <- ifelse(predict(model, type = "response") > 0.5, 1, 0)
mean(pred_class == analysis_data$outcome)
# Report pseudo-R² with context
# Note: pseudo-R² values 0.10-0.20 often indicate reasonable fitFor Count Models (Poisson/Negative Binomial):
# Test for overdispersion
library(AER)
dispersiontest(poisson_model)
# If overdispersed (p < 0.05), use negative binomial:
library(MASS)
nb_model <- glm.nb(count ~ treatment + controls, data = analysis_data)
# Compare AIC
AIC(poisson_model, nb_model)For Ordered Logit:
library(brant)
# Test proportional odds assumption
brant_test <- brant(ordered_model)
print(brant_test) # If violated, use generalized ordered logitFor DiD:
# Pre-trends (visual)
event_study <- feols(
outcome ~ i(time_to_treat, ref = -1) | unit_fe + time_fe,
cluster = ~cluster_var,
data = analysis_data
)
iplot(event_study, main = "Event Study")For IV:
# First stage F-statistic
first_stage <- feols(endogenous ~ instrument + controls | fe, data = analysis_data)
summary(first_stage) # Check F > 10 (or use effective F)
# Report first stage
fitstat(iv_model, "ivf")5. Visualize Key Results
Create figures for the main findings:
Coefficient plot:
library(ggplot2)
modelplot(models[["(4)"]], coef_omit = "Intercept") +
geom_vline(xintercept = 0, linetype = "dashed") +
theme_minimal() +
labs(title = "Treatment Effect Estimate")
ggsave("output/figures/figure_coefplot.pdf", width = 8, height = 6)Event study plot:
iplot(event_study)
# or custom with ggplot
coef_data <- broom::tidy(event_study, conf.int = TRUE)
ggplot(coef_data, aes(x = term, y = estimate, ymin = conf.low, ymax = conf.high)) +
geom_pointrange() +
geom_hline(yintercept = 0, linetype = "dashed") +
theme_minimal()Marginal effects (for interactions):
library(marginaleffects)
# If model has interactions
mfx <- slopes(model, variables = "treatment", by = "moderator")
plot(mfx)Output: Results Report
Create a results report (memos/phase3-results-report.md):
# Main Results Report
## Summary of Findings
**Main estimate**: [interpretation in words]
The preferred specification (Model X) shows that [treatment] is associated with
a [magnitude] [direction] in [outcome]. This effect is [statistically significant
at the X% level / not statistically significant].
## Results Table
[Reference Table 2 or include formatted output]
## Interpretation
### Magnitude
- Point estimate: [value]
- Units: [what this means]
- Context: [comparison to mean/SD/other benchmark]
### Precision
- Standard error: [value]
- 95% CI: [lower, upper]
- This is [precise/noisy] because [reason]
### Stability
- The estimate [is stable / changes] across specifications
- Adding controls [increases/decreases/doesn't change] the estimate
- This suggests [interpretation of stability pattern]
## Diagnostic Checks
- [Results of assumption tests]
- [Any concerns raised]
## Visualizations
- Figure X: [description]
- Figure Y: [description]
## Preliminary Assessment
- These results [support / do not support / partially support] the hypothesis
- Key caveat: [main limitation]
- Next step: robustness checks in Phase 4
## Questions for User
- [Any interpretive questions]
- [Should we proceed to robustness?]When You're Done
Return a summary to the orchestrator that includes: 1. The main estimate and its interpretation 2. Whether the effect is statistically significant 3. Whether results are stable across specifications 4. Any diagnostic concerns 5. Questions for the user
Do not proceed to Phase 4 until the user reviews the main results.
Phase 4: Robustness & Sensitivity
You are executing Phase 4 of a statistical analysis in R. Your goal is to stress-test the main findings through robustness checks and sensitivity analysis.
Why This Phase Matters
Main results are only as credible as their robustness. Reviewers will ask: "How do you know this isn't driven by [X]?" This phase pre-empts those questions and honestly assesses the fragility of the findings.
Technique Guides
Consult these guides in r-statistical-techniques/ for robustness code patterns:
| Topic | Guide |
|---|---|
| Sensitivity to unobservables | 05_bayesian_sensitivity.md (sensemakr section) |
| DiD robustness, Event studies | 01_core_econometrics.md |
| Bootstrap, Resampling | 02_survey_resampling.md |
| Matching diagnostics | 01_core_econometrics.md Section 6 |
Your Tasks
1. Alternative Specifications
Run the pre-specified alternatives from Phase 2:
Different control sets:
# Minimal controls
robust_minimal <- feols(outcome ~ treatment | fe, cluster = ~cluster_var, data = data)
# Extended controls
robust_extended <- feols(outcome ~ treatment + extra_controls | fe,
cluster = ~cluster_var, data = data)
# Different functional form
robust_log <- feols(log(outcome) ~ treatment | fe, cluster = ~cluster_var, data = data)Different fixed effects:
# Unit-by-year FE (more demanding)
robust_fe <- feols(outcome ~ treatment | unit^year, cluster = ~cluster_var, data = data)
# Region-by-year FE
robust_region <- feols(outcome ~ treatment | region^year + unit,
cluster = ~cluster_var, data = data)Different standard errors:
# Compare clustering levels
summary(main_model, cluster = ~unit)
summary(main_model, cluster = ~state)
summary(main_model, cluster = ~unit + year)2. Placebo Tests
Pre-treatment effects (for DiD/Event Study):
# Should see no effect before treatment
pre_data <- data %>% filter(year < treatment_year)
placebo_pre <- feols(outcome ~ fake_treatment | unit + year,
cluster = ~unit, data = pre_data)Fake treatment timing:
# Assign treatment X years earlier—should find no effect
data <- data %>%
mutate(fake_treated = treated_post & year >= (treatment_year - 3))
placebo_timing <- feols(outcome ~ fake_treated | unit + year,
cluster = ~unit, data = data)Outcome that shouldn't be affected:
# If treatment affects X, it shouldn't affect unrelated Y
placebo_outcome <- feols(unrelated_outcome ~ treatment | unit + year,
cluster = ~unit, data = data)3. Missing Data Assessment
Before running sensitivity analyses, document and address missing data:
Document Missingness:
# Overall missingness rates
sapply(analysis_data, function(x) mean(is.na(x))) |>
sort(decreasing = TRUE) |>
head(10)
# Missingness by key variables
analysis_data %>%
group_by(treatment) %>%
summarise(across(everything(), ~mean(is.na(.))))Test for MCAR/MAR:
library(naniar)
# Visualize missingness patterns
vis_miss(analysis_data)
gg_miss_upset(analysis_data)
# Little's MCAR test (if needed)
library(mice)
mcar_test(analysis_data)Multiple Imputation (if substantial missingness):
library(mice)
# Use adequate number of imputations (m ≥ 20, preferably ≥ 50)
imp <- mice(analysis_data, m = 50, method = 'pmm', seed = 12345,
printFlag = FALSE)
# Check imputation diagnostics
densityplot(imp) # Compare imputed vs observed distributions
stripplot(imp)
# Run analysis on imputed datasets and pool
fit_imp <- with(imp, lm(outcome ~ treatment + control1 + control2))
pooled <- pool(fit_imp)
summary(pooled)
# Include auxiliary variables to strengthen MAR assumption
# These are variables that predict missingness or the outcomeCompare Missing Data Approaches:
# Create comparison table
missing_comparison <- list(
"Complete case" = main_model,
"Multiple imputation" = pooled_model,
"Single imputation" = single_imp_model # if applicable
)
modelsummary(missing_comparison,
output = "output/tables/missing_data_sensitivity.tex",
title = "Sensitivity to Missing Data Treatment")Report in Methods Section:
[X]% of observations were missing on [variable]. We tested for patterns
of missingness and found [MCAR/MAR/evidence of MNAR]. Our primary analysis
uses [complete case / multiple imputation with m = X imputations].
Sensitivity analyses comparing complete case, single imputation, and
multiple imputation show [results are robust / estimates differ by X].4. Sensitivity Analysis
Sensitivity to outliers:
# Winsorize extreme values
library(DescTools)
data$outcome_w <- Winsorize(data$outcome, probs = c(0.01, 0.99))
robust_winsor <- feols(outcome_w ~ treatment | fe, cluster = ~cluster_var, data = data)
# Drop extreme observations
data_trimmed <- data %>% filter(outcome > quantile(outcome, 0.01) &
outcome < quantile(outcome, 0.99))
robust_trim <- feols(outcome ~ treatment | fe, cluster = ~cluster_var, data = data_trimmed)Sensitivity to sample restrictions:
# Different time periods
robust_early <- feols(outcome ~ treatment | fe, data = filter(data, year <= 2015))
robust_late <- feols(outcome ~ treatment | fe, data = filter(data, year > 2015))
# Excluding specific units
robust_exclude <- feols(outcome ~ treatment | fe, data = filter(data, !outlier_unit))Selection on unobservables (sensemakr):
library(sensemakr)
# Fit OLS version for sensemakr
ols_model <- lm(outcome ~ treatment + controls, data = data)
# Sensitivity analysis
sens <- sensemakr(
model = ols_model,
treatment = "treatment",
benchmark_covariates = c("strongest_control"),
kd = 1:3 # How strong would confounding need to be?
)
# Summary
summary(sens)
# Plot
plot(sens)
ggsave("output/figures/sensitivity_plot.pdf", width = 8, height = 6)4. Subgroup Analysis
Run pre-specified heterogeneity analyses:
# By group
robust_subgroup1 <- feols(outcome ~ treatment | fe,
data = filter(data, subgroup == 1))
robust_subgroup2 <- feols(outcome ~ treatment | fe,
data = filter(data, subgroup == 2))
# Interaction approach (preferred)
robust_het <- feols(outcome ~ treatment * subgroup_var | fe,
cluster = ~cluster_var, data = data)
# Visualize heterogeneity
library(marginaleffects)
het_effects <- slopes(robust_het, variables = "treatment", by = "subgroup_var")
plot(het_effects)5. Panel/Longitudinal Data Robustness (If Applicable)
Attrition Analysis:
# Document attrition rates by wave
attrition_table <- data %>%
group_by(wave) %>%
summarise(n = n_distinct(id),
pct_remaining = n / first(n) * 100)
# Test if attrition is related to treatment or outcomes
attrition_model <- glm(dropped_out ~ treatment + baseline_outcome + covariates,
family = binomial, data = baseline_data)
summary(attrition_model)Inverse Probability Weighting for Selection:
library(ipw)
# Estimate selection weights
ps_model <- glm(observed ~ treatment + covariates, family = binomial, data = data)
weights <- 1 / predict(ps_model, type = "response")
# Apply weights in main analysis
robust_ipw <- feols(outcome ~ treatment | unit + time,
weights = ~weights, data = data)Fixed vs Random Effects:
library(plm)
# Hausman test
fe_model <- plm(outcome ~ treatment + covariates, model = "within",
index = c("id", "time"), data = data)
re_model <- plm(outcome ~ treatment + covariates, model = "random",
index = c("id", "time"), data = data)
phtest(fe_model, re_model) # p < 0.05 suggests FE preferred
# Within-between decomposition (correlated random effects)
data <- data %>%
group_by(id) %>%
mutate(across(c(treatment, covariates), list(between = ~mean(., na.rm = TRUE)))) %>%
mutate(across(c(treatment, covariates),
list(within = ~. - mean(., na.rm = TRUE)), .names = "{.col}_within"))6. Method-Specific Diagnostics
For DiD with staggered treatment:
# Check for heterogeneous treatment effects
library(did)
did_result <- att_gt(
yname = "outcome",
tname = "year",
idname = "unit",
gname = "treatment_year", # Year unit first treated
data = data
)
# Event study
es <- aggte(did_result, type = "dynamic")
ggdid(es)For IV:
# Weak instrument test
fitstat(iv_model, "ivf") # Should be > 10, ideally > 20
# Overidentification test (if multiple instruments)
fitstat(iv_model, "sargan")For Matching:
library(MatchIt)
library(cobalt)
# Balance check
bal.tab(matched_data, un = TRUE)
# Love plot
love.plot(matched_data)6. Create Robustness Table
Compile all robustness checks:
robustness_models <- list(
"Main" = main_model,
"Minimal controls" = robust_minimal,
"Extended controls" = robust_extended,
"Alt FE" = robust_fe,
"Winsorized" = robust_winsor,
"Pre-2015" = robust_early,
"Post-2015" = robust_late
)
modelsummary(robustness_models,
output = "output/tables/table3_robustness.tex",
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
title = "Robustness Checks")Output: Robustness Report
Create a robustness report (memos/phase4-robustness-report.md):
# Robustness Report
## Summary Assessment
The main findings are [robust / partially robust / not robust] to alternative specifications.
## Alternative Specifications
| Specification | Estimate | SE | Conclusion |
|---------------|----------|-----|------------|
| Main | X.XX | (X.XX) | - |
| Minimal controls | X.XX | (X.XX) | [stable/different] |
| Extended controls | X.XX | (X.XX) | [stable/different] |
| Alt FE | X.XX | (X.XX) | [stable/different] |
...
## Placebo Tests
| Test | Expected | Found | Pass? |
|------|----------|-------|-------|
| Pre-treatment | 0 | X.XX (p=X.XX) | [Yes/No] |
| Fake timing | 0 | X.XX (p=X.XX) | [Yes/No] |
| Unrelated outcome | 0 | X.XX (p=X.XX) | [Yes/No] |
## Sensitivity Analysis
### Outliers
- Results [are / are not] sensitive to extreme values
### Sample restrictions
- Results [hold / change] in different subsamples
### Selection on unobservables (sensemakr)
- An unobserved confounder would need to be [X times] as strong as
[strongest observed covariate] to explain away the result
- This is [plausible / implausible] because [reasoning]
## Subgroup Analysis
| Subgroup | Estimate | SE | Different from main? |
|----------|----------|-----|---------------------|
| Group 1 | X.XX | (X.XX) | [Yes/No] |
| Group 2 | X.XX | (X.XX) | [Yes/No] |
## Method-Specific Diagnostics
[Results of diagnostic tests]
## Overall Assessment
**Strengths:**
- [What checks the results passed]
**Concerns:**
- [Any issues found]
**Conclusion:**
The main findings [can / cannot] be considered robust because [reasoning].
## Questions for User
- [Any interpretive questions about robustness]When You're Done
Return a summary to the orchestrator that includes: 1. Overall robustness assessment 2. Which checks passed/failed 3. Sensitivity analysis conclusions 4. Any concerns about the findings 5. Questions for the user
Do not proceed to Phase 5 until the user reviews the robustness assessment.
Phase 5: Output & Interpretation
You are executing Phase 5 of a statistical analysis in R. Your goal is to produce publication-ready outputs and synthesize the analysis into a coherent narrative.
Why This Phase Matters
Analysis isn't complete until it's communicated. This phase transforms results into tables, figures, and text that can appear in a journal article. Good output is accurate, clear, and tells a story.
Technique Guides
Consult these guides in r-statistical-techniques/ for output code patterns:
| Topic | Guide |
|---|---|
| Tables (modelsummary, etable) | 06_visualization.md |
| Figures (ggplot2, coefplot) | 06_visualization.md |
| Project structure, Reproducibility | 07_best_practices.md |
| Marginal effects visualization | 08_nonlinear_models.md |
Your Tasks
1. Finalize Tables
Table 1: Descriptive Statistics
library(modelsummary)
# Summary statistics
datasummary(
outcome + treatment + control1 + control2 ~
N + Mean + SD + Min + Max,
data = analysis_data,
output = "output/tables/table1_descriptives.tex",
title = "Summary Statistics",
notes = "Sample includes [description]. Data from [source]."
)
# Balance table (if applicable)
datasummary_balance(
~ treatment,
data = analysis_data,
output = "output/tables/table1_balance.tex",
title = "Balance Across Treatment Groups"
)Table 2: Main Results
modelsummary(
main_models,
output = "output/tables/table2_main.tex",
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_map = c(
"treatment" = "Treatment",
"control1" = "Control 1",
"control2" = "Control 2"
),
gof_map = c("nobs", "r.squared", "FE: unit", "FE: year"),
title = "Effect of [Treatment] on [Outcome]",
notes = list(
"Standard errors clustered at [level] in parentheses.",
"* p<0.1, ** p<0.05, *** p<0.01"
)
)Table 3: Robustness
modelsummary(
robustness_models,
output = "output/tables/table3_robustness.tex",
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_omit = "control", # Show only treatment
title = "Robustness Checks",
notes = "See notes to Table 2."
)2. Create Publication Figures
Figure 1: Trends (for DiD)
library(ggplot2)
trends_data <- analysis_data %>%
group_by(year, treatment_group) %>%
summarise(mean_outcome = mean(outcome, na.rm = TRUE),
se = sd(outcome, na.rm = TRUE) / sqrt(n()))
p_trends <- ggplot(trends_data, aes(x = year, y = mean_outcome,
color = treatment_group,
linetype = treatment_group)) +
geom_line(size = 1) +
geom_point(size = 2) +
geom_ribbon(aes(ymin = mean_outcome - 1.96*se,
ymax = mean_outcome + 1.96*se,
fill = treatment_group),
alpha = 0.2, color = NA) +
geom_vline(xintercept = treatment_year, linetype = "dashed", color = "gray40") +
annotate("text", x = treatment_year, y = Inf, label = "Treatment",
vjust = 2, hjust = 0.5, size = 3) +
scale_color_manual(values = c("Control" = "#1f77b4", "Treated" = "#ff7f0e")) +
scale_fill_manual(values = c("Control" = "#1f77b4", "Treated" = "#ff7f0e")) +
labs(x = "Year", y = "Outcome", color = "", fill = "", linetype = "") +
theme_minimal() +
theme(legend.position = "bottom",
panel.grid.minor = element_blank())
ggsave("output/figures/figure1_trends.pdf", p_trends, width = 8, height = 5)Figure 2: Event Study
# Using fixest
p_event <- iplot(event_study_model,
main = "",
xlab = "Time Relative to Treatment",
ylab = "Coefficient Estimate")
# Or custom ggplot
event_coefs <- broom::tidy(event_study_model, conf.int = TRUE) %>%
filter(str_detect(term, "time_to_treat")) %>%
mutate(time = as.numeric(str_extract(term, "-?\\d+")))
p_event <- ggplot(event_coefs, aes(x = time, y = estimate)) +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray40") +
geom_vline(xintercept = -0.5, linetype = "dashed", color = "gray40") +
geom_pointrange(aes(ymin = conf.low, ymax = conf.high)) +
labs(x = "Time Relative to Treatment", y = "Coefficient Estimate") +
theme_minimal()
ggsave("output/figures/figure2_eventstudy.pdf", p_event, width = 8, height = 5)Figure 3: Coefficient Plot
p_coef <- modelplot(main_model, coef_omit = "Intercept") +
geom_vline(xintercept = 0, linetype = "dashed") +
theme_minimal() +
labs(title = "")
ggsave("output/figures/figure3_coefplot.pdf", p_coef, width = 6, height = 4)3. Write Results Narrative
Draft the key paragraphs for the results section:
Main effect paragraph:
Table 2 presents estimates of the effect of [treatment] on [outcome].
Column (1) shows the baseline relationship without controls.
Column (4), our preferred specification, includes [unit] and [time] fixed effects
and clusters standard errors at the [level] level. We find that [treatment]
[increases/decreases] [outcome] by [X] [units], significant at the [Y]% level
(95% CI: [lower, upper]). This represents a [Z]% change relative to the
pre-treatment mean of [mean].
Robustness paragraph:
Table 3 demonstrates that this finding is robust to alternative specifications.
The point estimate remains [stable/similar] when we [change 1], [change 2], and
[change 3]. The effect is [somewhat/not] sensitive to [what]. The sensitivity
analysis in Figure X shows that an unobserved confounder would need to be
[X times as strong as the strongest observed predictor] to fully explain
our findings, suggesting that selection on unobservables is unlikely to
account for the entire effect.
Heterogeneity paragraph (if applicable):
We examine heterogeneity in treatment effects across [subgroups].
Table X shows that the effect is [larger/smaller] for [group 1]
([estimate]) compared to [group 2] ([estimate]). This difference is
[statistically significant / not statistically significant] (p = [value]).
4. Survey Data Methods Section (If Applicable)
For survey-based analyses, address the five survey methodology deliverables:
1. Sampling Frame Description:
Data come from the [SURVEY NAME], a [DESIGN TYPE] of [POPULATION].
The sampling frame is [DESCRIPTION]. The target population is [WHO].
[Exclusions]: We exclude [categories] because [reason].2. Response Rate and Nonresponse:
The response rate was [X]% (calculated as [METHOD: AAPOR RR1/RR3/etc.]).
We compared respondents to [population benchmark / nonrespondents on X variables]
and found [no significant differences / differences on X that we address through Y].3. Weighting Justification:
We apply [WEIGHT TYPE: post-stratification / raking / none] weights to adjust for
[FACTORS]. Weights are provided by [SOURCE] and calibrated to [BENCHMARKS].
[Alternative: We do not apply weights because {justification}.]4. Survey Design Acknowledgment:
library(survey)
# Define survey design
survey_design <- svydesign(
ids = ~cluster,
strata = ~stratum,
weights = ~weight,
data = analysis_data
)
# All analyses use survey-adjusted estimates
svymean(~outcome, survey_design)
svyglm(outcome ~ treatment + controls, design = survey_design)5. Population Inference Boundaries:
Our results generalize to [POPULATION] during [TIME PERIOD]. We cannot
speak to [EXCLUDED GROUPS / OTHER TIME PERIODS] because [REASON].5. Document Limitations
Identify and articulate limitations honestly:
## Limitations
1. **[Identification limitation]**: Our identification strategy relies on
[assumption]. While we provide evidence supporting this assumption through
[tests], we cannot definitively rule out [threat].
2. **[External validity]**: Our sample consists of [description]. Results may
not generalize to [other contexts] because [reason].
3. **[Measurement]**: [Variable] is measured using [method], which may
[limitation]. We address this by [mitigation] but acknowledge [remaining concern].
4. **[Data limitation]**: We lack data on [variable], which could [potential issue].
Our robustness checks in Table X suggest this is [unlikely to/may] affect our
conclusions.5. Create Replication Package
Prepare materials for reproducibility:
# Master script header
cat('
# ============================================================
# Replication Code for "[Paper Title]"
# Authors: [Names]
# Date: [Date]
#
# This script reproduces all tables and figures in the paper.
# Runtime: approximately [X] minutes on [hardware]
# ============================================================
# Requirements
# R version: ', R.version.string, '
# Key packages: fixest, modelsummary, ggplot2, dplyr
# Set seed for reproducibility
set.seed(12345)
# Run analysis
source("code/01_clean_data.R")
source("code/02_descriptives.R")
source("code/03_main_analysis.R")
source("code/04_robustness.R")
source("code/05_figures.R")
# Session info
sessionInfo()
', sep = "")Output: Final Report
Create the final synthesis (memos/phase5-final-report.md):
# Analysis Summary
## Key Finding
[One sentence summary of the main result]
## Main Result
- **Effect size**: [estimate with CI]
- **Significance**: [p-value or significance level]
- **Interpretation**: [what this means substantively]
## Robustness Assessment
- The finding [is/is not] robust to [list of checks]
- Main concerns: [if any]
## Output Files Created
### Tables
- `table1_descriptives.tex`: Summary statistics
- `table2_main.tex`: Main results
- `table3_robustness.tex`: Robustness checks
### Figures
- `figure1_trends.pdf`: Pre/post trends
- `figure2_eventstudy.pdf`: Event study
- `figure3_coefplot.pdf`: Coefficient plot
### Replication Materials
- `00_master.R`: Master script
- `code/`: All analysis code
- `data/clean/`: Analysis datasets
## Results Narrative
[Draft paragraphs for the paper]
## Limitations
[Honest assessment of limitations]
## Conclusion
[What can and cannot be concluded from this analysis]6. Pre-Submission Checklist
Before finalizing, verify the analysis meets publication standards:
Minimum Standard (Required):
- [ ] All variables clearly defined with units and coding
- [ ] Sample size and any exclusions documented
- [ ] Main coefficient table includes SEs and significance levels
- [ ] Standard error type specified (robust, clustered at X level, etc.)
- [ ] At least one robustness check reported
- [ ] Limitations section acknowledges main threats to validity
Strong Standard (Competitive for top journals):
- [ ] Descriptive statistics table with means, SDs, and sample sizes
- [ ] Multiple robustness specifications in appendix
- [ ] Effect sizes interpreted substantively (not just "significant")
- [ ] For nonlinear models: AMEs or predicted probabilities reported
- [ ] Sensitivity analysis for selection on unobservables (e.g., sensemakr)
- [ ] Missing data approach documented and defended
- [ ] Visualization of key results (event study, coefficient plot, etc.)
- [ ] Replication code and data availability statement
Exemplary Standard (Model for the field):
- [ ] Pre-registration referenced (if applicable)
- [ ] Multiple identification strategies compared
- [ ] Heterogeneity analysis with theoretical motivation
- [ ] Mechanism analysis or mediation tests
- [ ] Power analysis or minimum detectable effects
- [ ] Bound analysis for worst-case scenarios
- [ ] Complete replication package with README
Language Checklist:
- [ ] Causal language only used with appropriate identification strategy
- [ ] Effect sizes interpreted relative to meaningful benchmarks
- [ ] Confidence intervals reported, not just p-values
- [ ] Scope conditions clearly stated
- [ ] "Significant" refers to statistical significance (or avoid the term)
When You're Done
Return a summary to the orchestrator that includes: 1. List of all tables and figures created 2. The main finding in one sentence 3. Key limitations 4. Any remaining questions or concerns 5. Confirmation that replication materials are ready 6. Checklist tier achieved (minimum/strong/exemplary)
The analysis is now complete. All materials should be ready for paper writing.
Core Econometrics in R
Panel methods, causal inference basics, and standard errors with reproducible examples.
---
Setup
Install and load all required packages:
# Install packages (only run once)
packages <- c(
"fixest", "did", "TwoWayFEWeights", "rdrobust", "rddensity",
"ivreg", "glmnet", "MatchIt", "cem", "cobalt", "ebal",
"Matching", "rgenoud", "PanelMatch", "survey",
"sensemakr", "mediation", "dplyr", "broom", "ggplot2"
)
install.packages(setdiff(packages, rownames(installed.packages())))
# Load packages
library(fixest)
library(did)
library(TwoWayFEWeights)
library(rdrobust)
library(rddensity)
library(ivreg)
library(glmnet)
library(MatchIt)
library(cem)
library(cobalt)
library(ebal)
library(Matching)
library(PanelMatch)
library(survey)
library(sensemakr)
library(mediation)
library(dplyr)
library(broom)
library(ggplot2)---
1. Two-Way Fixed Effects (TWFE)
TWFE is the dominant approach for panel data analysis. It controls for unobserved unit-specific and time-specific confounders.
When to Use TWFE
- You have panel data (repeated observations of units over time)
- You believe unobserved unit and time characteristics confound your relationship
- Treatment timing is consistent across all treated units (or you've verified no problematic negative weights)
Assumptions
1. Parallel trends: Absent treatment, outcomes would evolve similarly across units 2. No anticipation: Units don't change behavior before treatment 3. Stable unit treatment values: No spillovers between units 4. Homogeneous treatment effects (for staggered designs with TWFE)
Common Pitfalls
- With staggered treatment timing, TWFE can produce negative weights and biased estimates
- Always check for negative weights using
TwoWayFEWeightsor similar diagnostics - Two-way clustering requires sufficient clusters in both dimensions
Basic Specification
Model: $Y_{it} = \alpha_i + \gamma_t + \beta X_{it} + \varepsilon_{it}$
Where $\alpha_i$ are unit fixed effects and $\gamma_t$ are time fixed effects.
Implementation with fixest
Using the base_did dataset from fixest (simulated DiD data with staggered treatment):
# Load sample DiD data
data(base_did, package = "fixest")
# Examine the data
str(base_did)
# 'data.frame': 1080 obs. of 6 variables:
# $ y : num -0.816 2.223 0.481 -0.305 0.217 ...
# $ x1 : num 1.14 1.06 1.06 0.86 0.77 ...
# $ id : int 1 1 1 1 1 1 1 1 1 1 ...
# $ period: int 1 2 3 4 5 6 7 8 9 10 ...
# $ post : logi FALSE FALSE FALSE FALSE TRUE TRUE ...
# $ treat : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
# Basic TWFE with two-way fixed effects
model_twfe <- feols(
y ~ x1 + treat | id + period, # FE after |
cluster = ~ id, # Cluster SEs at unit level
data = base_did
)
summary(model_twfe)
# Output:
# OLS estimation, Pair.Clust Dep. Var.: y
# Observations: 1,080
# Fixed-effects: id: 108, period: 10
# Standard-errors: Clustered by id
# Estimate Std. Error t value Pr(>|t|)
# x1 0.9985 0.11632 8.5841 1.29e-13 ***
# treat 0.6876 0.09126 7.5362 3.30e-11 ***Interpretation: The treatment effect is 0.69, controlling for unit and time fixed effects. With this simulated data, we expect a positive treatment effect.
Two-Way Clustering
When both unit and time dimensions have few clusters, use two-way clustering:
# Two-way clustering (unit and time)
model_2way <- feols(
y ~ x1 + treat | id + period,
cluster = ~ id + period, # Two-way clustering
data = base_did
)
# Compare standard errors
data.frame(
Variable = c("x1", "treat"),
`One-way SE` = model_twfe$se,
`Two-way SE` = model_2way$se
)
# Variable One.way.SE Two.way.SE
# 1 x1 0.11632 0.12845
# 2 treat 0.09126 0.11203Multiple Outcomes
Estimate the same specification across multiple outcomes efficiently:
# Using trade data for multiple outcome example
data(trade, package = "fixest")
# Multiple outcomes with consistent specification
models <- feols(
c(Euros, log(Euros)) ~ log(dist_km) | Origin + Destination + Year,
cluster = ~ Origin,
data = trade
)
# Formatted output table
etable(models,
title = "Trade Flow Regressions",
headers = c("Levels", "Log"),
fitstat = ~ r2 + n)---
2. Difference-in-Differences (DiD)
DiD identifies causal effects by comparing treated and control groups before and after treatment.
When to Use DiD
- You have a treatment that affects some units at a specific time
- You have pre-treatment and post-treatment observations for both groups
- Parallel trends assumption is plausible
Assumptions
1. Parallel trends: Treatment and control groups would have followed parallel outcome paths absent treatment 2. No anticipation: Treatment timing is not driven by expected outcomes 3. Stable composition: No differential selection into/out of the sample
Common Pitfalls
- Testing parallel trends with pre-treatment data is necessary but not sufficient
- With staggered treatment timing, use modern DiD methods (Callaway-Sant'Anna, etc.)
- Functional form matters - level vs. log outcomes can give different conclusions
2.1 Traditional DiD
Model: $Y_{it} = \alpha + \beta_1 \text{Treat}_i + \beta_2 \text{Post}_t + \beta_3 (\text{Treat}_i \times \text{Post}_t) + \varepsilon_{it}$
The coefficient $\beta_3$ is the DiD estimate.
# Using base_did from fixest
data(base_did, package = "fixest")
# Create group indicators
base_did <- base_did %>%
mutate(
treated_group = as.numeric(id <= 54), # First half are treated
post_period = as.numeric(period >= 5) # Treatment at period 5
)
# Traditional DiD with interaction
model_did <- feols(
y ~ treated_group * post_period | id + period,
cluster = ~ id,
data = base_did
)
summary(model_did)
# The interaction term is the DiD estimate2.2 Modern DiD Methods (Staggered Treatment)
Recent econometric literature shows TWFE DiD can be biased with staggered treatment timing. Use the did package for the Callaway-Sant'Anna estimator.
# Load county teen employment data
data(mpdta, package = "did")
# Examine the data
head(mpdta)
# year countyreal lpop lemp first.treat treat
# 1 2003 8001 5.896761 8.461469 2007 1
# 2 2004 8001 5.896761 8.336870 2007 1
# 3 2005 8001 5.896761 8.343058 2007 1
# 4 2006 8001 5.896761 8.263403 2007 1
# 5 2007 8001 5.896761 8.170693 2007 1
# Callaway-Sant'Anna estimator
out <- att_gt(
yname = "lemp", # Outcome: log employment
tname = "year", # Time variable
idname = "countyreal", # Unit identifier
gname = "first.treat", # Treatment timing (0 = never treated)
xformla = ~ lpop, # Covariates (optional)
data = mpdta,
control_group = "nevertreated", # Or "notyettreated"
bstrap = TRUE,
biters = 1000
)
# Summary of group-time ATTs
summary(out)
# Output:
# Group-Time Average Treatment Effects:
# Group Time ATT(g,t) Std. Error [95% Simult. Conf. Band]
# 2004 2004 -0.0145 0.0225 -0.0772 0.0481
# 2004 2005 -0.0764 0.0303 -0.1608 0.0079
# ...Interpretation: Each row shows the ATT for a specific cohort (first.treat = Group) at a specific time. Groups are defined by when they first received treatment.
2.3 Aggregating to Event Study
# Aggregate to event-study (dynamic effects)
es <- aggte(out, type = "dynamic", min_e = -5, max_e = 5)
summary(es)
# Output:
# Overall summary of ATT's based on event-study/dynamic aggregation:
# ATT Std. Error [ 95% Conf. Int.]
# -0.0772 0.0215 -0.1194 -0.0350 *
# Event study coefficients:
# Event Time Estimate Std. Error [95% Simult. Conf. Band]
# -5 0.0256 0.0168 -0.0198 0.0710
# -4 -0.0050 0.0146 -0.0445 0.0345
# ...
# Plot event study
ggdid(es, title = "Event Study: Effect on Teen Employment")2.4 DiD Diagnostic: Testing for Negative Weights
# De Chaisemartin & D'Haultfoeuille negative weights test
# Using TwoWayFEWeights package
# Prepare data for twowayfeweights
data(base_did, package = "fixest")
base_did$D <- as.numeric(base_did$treat)
# Run weights diagnostic
ch_diag <- twowayfeweights(
data = base_did,
Y = "y",
G = "id",
T = "period",
D = "D",
type = "feTR",
summary_measures = TRUE
)
# Results:
# Under the common trends assumption, the TWFE coefficient identifies a weighted average
# of the treatment effect in each (group,time).
# The sum of negative weights is: X.XX
# The sum of positive weights is: X.XX
# If sum of negative weights is non-trivial (>0.1), consider using robust estimators---
3. Event Studies
Event studies estimate dynamic treatment effects relative to treatment timing.
When to Use Event Studies
- You want to visualize treatment effects over time
- You need to test the parallel trends assumption (pre-treatment coefficients)
- Treatment effects may be dynamic (building or fading over time)
Assumptions
Same as DiD, plus:
- Effects at each event-time are well-estimated (sufficient data)
- Appropriate binning of event-time endpoints
Common Pitfalls
- Pre-trends in event study don't guarantee parallel trends (could be underpowered)
- With staggered timing, use Sun & Abraham or Callaway-Sant'Anna (not OLS event study)
- Omit exactly one period (typically t = -1) to avoid collinearity
3.1 Event Study with Staggered Treatment
Using the base_stagg dataset from fixest:
# Load staggered treatment data
data(base_stagg, package = "fixest")
# Examine data structure
head(base_stagg)
# id year year_treated time_to_treatment treated treatment_effect_true x1 y
# 1 1 2001 2004 -3 FALSE 0 1.14 -0.816
# The dataset already has time_to_treatment variable
# 'treated' indicates treatment status
# fixest's i() function for event study
es_model <- feols(
y ~ x1 + i(time_to_treatment, treated, ref = -1) | id + year,
cluster = ~ id,
data = base_stagg
)
# View coefficients
summary(es_model)
# Plot event study (fixest built-in)
iplot(es_model,
main = "Event Study: Effect on Y",
xlab = "Years Relative to Treatment",
ylab = "Estimate")3.2 Sun & Abraham (2021) Estimator
For heterogeneous treatment effects with staggered timing:
# Sun & Abraham via fixest's sunab() function
es_sunab <- feols(
y ~ x1 + sunab(year_treated, year) | id + year,
cluster = ~ id,
data = base_stagg
)
# Compare with standard event study
etable(es_model, es_sunab,
headers = c("Standard", "Sun-Abraham"))
# Plot
iplot(es_sunab, main = "Sun & Abraham Event Study")---
4. Regression Discontinuity (RD)
RD exploits discontinuous treatment assignment at a cutoff to identify causal effects.
When to Use RD
- Treatment is determined by whether a continuous "running variable" crosses a threshold
- Units cannot precisely manipulate the running variable
- You're interested in the local average treatment effect (LATE) at the cutoff
Assumptions
1. No manipulation: Units cannot precisely control their running variable near the cutoff 2. Continuity: Potential outcomes are continuous at the cutoff 3. Local effect: Estimates are valid only at the cutoff
Common Pitfalls
- Bandwidth selection is critical - use data-driven methods (MSE-optimal)
- Always test for manipulation with
rddensity - RD estimates are local - don't extrapolate far from cutoff
- Overfitting with high-order polynomials can inflate standard errors
4.1 Sharp RD with rdrobust
Using the Senate election data (close elections):
# Load RD Senate data
data(rdrobust_RDsenate, package = "rdrobust")
# Running variable: Democratic vote share margin
# Cutoff: 50% (threshold for winning)
# Outcome: Future Democratic vote share
# Examine data
head(rdrobust_RDsenate)
# margin vote
# 1 -21.316010 0.3566667
# 2 -12.606600 0.4073077
# ...
# Basic RD estimation
rd_est <- rdrobust(
y = rdrobust_RDsenate$vote,
x = rdrobust_RDsenate$margin,
c = 0, # Cutoff at 0 (normalized)
kernel = "triangular", # Kernel for weighting
bwselect = "mserd" # MSE-optimal bandwidth
)
summary(rd_est)
# Output:
# =============================================================================
# Method Coef. Std. Err. z P>|z| [ 95% C.I. ]
# =============================================================================
# Conventional 0.076 0.016 4.851 0.000 [0.045 , 0.107]
# Robust - - 4.185 0.000 [0.039 , 0.109]
# =============================================================================
#
# Bandwidth: 17.69 (left), 17.69 (right)Interpretation: Winning a close election (barely crossing 50%) increases future Democratic vote share by about 7.6 percentage points. This is the "incumbency advantage."
4.2 RD Visualization
# RD plot
rdplot(
y = rdrobust_RDsenate$vote,
x = rdrobust_RDsenate$margin,
c = 0,
title = "RD Plot: Incumbency Advantage in Senate Elections",
x.label = "Democratic Vote Share Margin (%)",
y.label = "Future Democratic Vote Share"
)4.3 Manipulation Testing
Always test whether units can manipulate the running variable:
# McCrary density test via rddensity
manip_test <- rddensity(X = rdrobust_RDsenate$margin, c = 0)
summary(manip_test)
# Output:
# Manipulation Test using local polynomial density estimation.
#
# Number of obs (left): 595, Number of obs (right): 795
#
# Test Statistic: 0.4181
# P-value: 0.6759
#
# Interpretation: p > 0.05 suggests no evidence of manipulation
# Visualize density around cutoff
rdplotdensity(rdd = manip_test, X = rdrobust_RDsenate$margin)4.4 Bandwidth Sensitivity
Test robustness to bandwidth choice:
# Multiple bandwidths
bandwidths <- c(10, 15, 20, 25, 30)
rd_results <- lapply(bandwidths, function(h) {
rd <- rdrobust(
y = rdrobust_RDsenate$vote,
x = rdrobust_RDsenate$margin,
c = 0,
h = h # Manually specified bandwidth
)
data.frame(
bandwidth = h,
estimate = rd$coef[1],
se = rd$se[1],
ci_low = rd$ci[1, 1],
ci_high = rd$ci[1, 2],
n_left = rd$N_h[1],
n_right = rd$N_h[2]
)
})
rd_robust <- bind_rows(rd_results)
print(rd_robust)
# Plot sensitivity
ggplot(rd_robust, aes(x = bandwidth, y = estimate)) +
geom_point(size = 3) +
geom_errorbar(aes(ymin = ci_low, ymax = ci_high), width = 1) +
geom_hline(yintercept = 0, linetype = "dashed") +
labs(x = "Bandwidth", y = "RD Estimate",
title = "RD Estimate Sensitivity to Bandwidth") +
theme_minimal()4.5 Covariate Balance at Cutoff
If pre-treatment covariates jump at the cutoff, RD validity is questionable:
# Test covariate balance (using margin as running variable)
# For Senate data, we don't have covariates, but here's the pattern:
# Pseudo-covariate test: does lagged outcome jump at cutoff?
# (In real application, use actual pre-treatment covariates)
# Simulate a covariate
set.seed(123)
rdrobust_RDsenate$covariate <- rdrobust_RDsenate$vote + rnorm(nrow(rdrobust_RDsenate), 0, 0.1)
# Test if covariate is balanced
rd_covariate <- rdrobust(
y = rdrobust_RDsenate$covariate,
x = rdrobust_RDsenate$margin,
c = 0
)
summary(rd_covariate)
# Ideally: coefficient close to 0, not statistically significant---
5. Instrumental Variables (IV)
IV addresses endogeneity when treatment is correlated with unobserved factors.
When to Use IV
- Your treatment variable is endogenous (correlated with unobservables)
- You have an instrument that:
1. Affects treatment (relevance) 2. Only affects outcome through treatment (exclusion restriction)
- You want a local average treatment effect (LATE) for compliers
Assumptions
1. Relevance: Instrument strongly predicts treatment 2. Exclusion restriction: Instrument affects outcome only through treatment 3. Independence: Instrument is as-if randomly assigned 4. Monotonicity: Instrument affects treatment in the same direction for all units
Common Pitfalls
- Weak instruments (F < 10) cause severe bias - 2SLS is biased toward OLS
- Cannot test the exclusion restriction - it's a maintained assumption
- LATE interpretation: IV estimates effects only for "compliers"
- Multiple instruments require overidentification tests
5.1 Basic 2SLS
Using the returns to schooling data:
# Load returns to schooling data
data(SchoolingReturns, package = "ivreg")
# Examine data
head(SchoolingReturns)
# wage education experience ethnicity smsa south age nearcollege ...
# 1 548.0000 12 18 other 1 0 36 0
# ...
# Key variables:
# - wage: hourly wage (outcome)
# - education: years of education (endogenous)
# - nearcollege: grew up near college (instrument)
# OLS (biased - education is endogenous)
ols_model <- lm(log(wage) ~ education + experience + I(experience^2) +
ethnicity + smsa + south,
data = SchoolingReturns)
# IV: Use proximity to college as instrument for education
iv_model <- ivreg(
log(wage) ~ education + experience + I(experience^2) +
ethnicity + smsa + south |
nearcollege + experience + I(experience^2) +
ethnicity + smsa + south,
data = SchoolingReturns
)
summary(iv_model, diagnostics = TRUE)
# Output includes:
# Diagnostic tests:
# df1 df2 statistic p-value
# Weak instruments 1 3003 14.072 0.00018 *** # First stage F
# Wu-Hausman 1 3002 2.139 0.14373 # Endogeneity test
# Compare OLS vs IV
data.frame(
Method = c("OLS", "IV"),
Education_Coef = c(coef(ols_model)["education"], coef(iv_model)["education"]),
SE = c(sqrt(vcov(ols_model)["education", "education"]),
sqrt(vcov(iv_model)["education", "education"]))
)
# Method Education_Coef SE
# 1 OLS 0.074 0.003
# 2 IV 0.132 0.055Interpretation: IV estimate (0.13) is larger than OLS (0.07), suggesting OLS understates returns to education. Each additional year of education increases wages by about 13% for compliers (those whose education was affected by college proximity).
5.2 IV with Fixed Effects (fixest)
# IV with fixed effects using fixest
# Using trade data as example
data(trade, package = "fixest")
# Suppose dist_km is endogenous, and we have an instrument
# (Simulating for demonstration)
set.seed(123)
trade$instrument <- log(trade$dist_km) + rnorm(nrow(trade), 0, 0.5)
# IV estimation with FE
iv_fe <- feols(
log(Euros) ~ 1 | Origin + Destination + Year |
log(dist_km) ~ instrument, # Endogenous ~ instrument
cluster = ~ Origin,
data = trade
)
summary(iv_fe)5.3 First-Stage Diagnostics
# Check first stage strength
first_stage <- lm(education ~ nearcollege + experience + I(experience^2) +
ethnicity + smsa + south,
data = SchoolingReturns)
summary(first_stage)
# First-stage F-statistic on excluded instrument
car::linearHypothesis(first_stage, "nearcollege = 0")
# Rule of thumb: F > 10 for strong instruments
# More rigorous: Stock-Yogo critical values
# Partial R-squared of instrument
partial_rsq <- summary(first_stage)$r.squared -
summary(lm(education ~ experience + I(experience^2) + ethnicity + smsa + south,
data = SchoolingReturns))$r.squared
cat("Partial R-squared:", round(partial_rsq, 4), "\n")5.4 Multiple Instruments and Overidentification
# Using cigarette demand data with multiple instruments
data(CigaretteDemand, package = "ivreg")
# Price is endogenous, use tax instruments
iv_overid <- ivreg(
packs ~ rincome + rprice | # Controls and endogenous
rincome + salestax + cigtax, # Controls and instruments
data = CigaretteDemand
)
summary(iv_overid, diagnostics = TRUE)
# Sargan test for overidentification
# H0: All instruments are valid
# High p-value = cannot reject validity---
6. LASSO and Variable Selection
LASSO is increasingly used for covariate selection and high-dimensional settings.
When to Use LASSO
- You have many potential predictors (high-dimensional)
- You want automatic variable selection
- You're doing prediction rather than causal inference
- For causal inference: use double-selection LASSO
Assumptions
1. Sparsity: True model has relatively few important predictors 2. Regularization: Willingness to shrink coefficients toward zero
Common Pitfalls
- LASSO coefficients are biased toward zero - use post-LASSO OLS for inference
- Cross-validation chooses lambda for prediction, not necessarily for consistent selection
- For causal inference, use double-selection (not standard LASSO)
6.1 Cross-Validated LASSO
# Using glmnet's example data
data(QuickStartExample, package = "glmnet")
x <- QuickStartExample$x
y <- QuickStartExample$y
# Examine dimensions
cat("n =", nrow(x), ", p =", ncol(x), "\n")
# n = 100, p = 20
# Cross-validated LASSO
set.seed(123)
cv_fit <- cv.glmnet(x, y, alpha = 1) # alpha = 1 for LASSO
# Plot CV results
plot(cv_fit)
# Optimal lambda values
cat("Lambda.min:", cv_fit$lambda.min, "\n")
cat("Lambda.1se:", cv_fit$lambda.1se, "\n")
# Coefficients at optimal lambda
coef_lasso <- coef(cv_fit, s = "lambda.min")
selected <- which(coef_lasso[-1, 1] != 0)
cat("Selected variables:", length(selected), "of", ncol(x), "\n")
cat("Selected indices:", paste(selected, collapse = ", "), "\n")
# Output:
# Lambda.min: 0.08307327
# Lambda.1se: 0.1752885
# Selected variables: 8 of 206.2 Post-LASSO OLS
After LASSO selects variables, re-estimate with OLS for valid inference:
# Get selected variables
selected_vars <- which(coef(cv_fit, s = "lambda.min")[-1, 1] != 0)
# Post-LASSO OLS
if (length(selected_vars) > 0) {
x_selected <- x[, selected_vars, drop = FALSE]
post_lasso <- lm(y ~ x_selected)
summary(post_lasso)
}6.3 Elastic Net
Combine LASSO (L1) with Ridge (L2) penalties:
# Elastic net with alpha = 0.5 (equal L1 and L2)
cv_enet <- cv.glmnet(x, y, alpha = 0.5)
# Compare with pure LASSO
cat("LASSO selected:", sum(coef(cv_fit, s = "lambda.min") != 0) - 1, "\n")
cat("Elastic net selected:", sum(coef(cv_enet, s = "lambda.min") != 0) - 1, "\n")---
7. Matching Methods
Matching creates comparable treatment and control groups by balancing observed covariates.
When to Use Matching
- You want to reduce bias from observed confounders
- Selection into treatment depends on observable characteristics
- You want interpretable weights (unlike regression adjustment)
Assumptions
1. Conditional independence: No unobserved confounders (strong!) 2. Overlap: For each treated unit, similar controls exist 3. SUTVA: No interference between units
Common Pitfalls
- Matching on propensity score doesn't guarantee covariate balance
- Always check balance after matching
- Matching reduces sample size - could hurt precision
- Matching cannot address unobserved confounding
7.1 Propensity Score Matching
Using the classic Lalonde labor training data:
# Load Lalonde data (job training program)
data(lalonde, package = "MatchIt")
# Examine data
head(lalonde)
# treat age educ race married nodegree re74 re75 re78
# 1 1 37 11 black 1 1 0 0 9930.0460
# 2 1 22 9 hispan 0 1 0 0 3595.8940
# ...
# Check initial imbalance
lalonde %>%
group_by(treat) %>%
summarise(
mean_age = mean(age),
mean_educ = mean(educ),
prop_married = mean(married),
mean_re74 = mean(re74)
)
# Propensity score matching
m_out <- matchit(
treat ~ age + educ + race + married + nodegree + re74 + re75,
data = lalonde,
method = "nearest", # Nearest neighbor matching
distance = "glm", # Logistic propensity score
ratio = 1, # 1:1 matching
caliper = 0.2 # Caliper in SD of propensity score
)
# Summary
summary(m_out)
# Output:
# Summary of Balance for Matched Data:
# Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean
# age 25.82 26.00 -0.02 1.02 0.01
# educ 10.35 10.41 -0.03 1.02 0.01
# ...
#
# Sample sizes:
# Control Treated
# All 429 185
# Matched 185 185
# Unmatched 244 07.2 Balance Assessment
# Balance plot (Love plot)
plot(m_out, type = "jitter")
# Standardized mean differences
plot(summary(m_out), var.order = "unmatched")
# Using cobalt for detailed balance
library(cobalt)
bal.tab(m_out, un = TRUE)
# Love plot with threshold
love.plot(m_out,
thresholds = c(m = 0.1), # 0.1 SMD threshold
var.order = "unadjusted",
title = "Covariate Balance")7.3 Estimate Treatment Effect
# Get matched data
matched_df <- match.data(m_out)
# Estimate ATT with matched data
# Use weights for full matching
att_model <- lm(re78 ~ treat, data = matched_df, weights = weights)
summary(att_model)
# With covariate adjustment (doubly robust)
att_robust <- lm(re78 ~ treat + age + educ + race + married + nodegree + re74 + re75,
data = matched_df, weights = weights)
# Compare
data.frame(
Model = c("Unadjusted", "Covariate Adjusted"),
ATT = c(coef(att_model)["treat"], coef(att_robust)["treat"]),
SE = c(sqrt(vcov(att_model)["treat", "treat"]),
sqrt(vcov(att_robust)["treat", "treat"]))
)7.4 Coarsened Exact Matching (CEM)
library(cem)
# Coarsened exact matching
# CEM bins continuous variables and finds exact matches within bins
cem_out <- cem(
treatment = "treat",
data = lalonde,
drop = "re78" # Don't match on outcome
)
# Summary
cem_out
# Get weights
lalonde$cem_weights <- cem_out$w
# Estimate with weights
cem_model <- lm(re78 ~ treat, data = lalonde, weights = cem_weights)
summary(cem_model)7.5 Entropy Balancing
Entropy balancing reweights controls to match treatment group moments:
library(ebal)
# Prepare data
X <- as.matrix(lalonde[, c("age", "educ", "married", "nodegree", "re74", "re75")])
W <- lalonde$treat
# Entropy balancing (for control group)
# Note: ebalance only reweights control group
eb_out <- ebalance(
Treatment = W,
X = X
)
# Create full weights vector
lalonde$eb_weights <- ifelse(W == 1, 1, eb_out$w)
# Check balance
# Treated means
colMeans(X[W == 1, ])
# Weighted control means
colSums(X[W == 0, ] * eb_out$w) / sum(eb_out$w)
# Estimate ATT
eb_model <- lm(re78 ~ treat, data = lalonde, weights = eb_weights)
summary(eb_model)7.6 Genetic Matching
library(Matching)
library(rgenoud)
# Prepare data
X <- as.matrix(lalonde[, c("age", "educ", "married", "nodegree", "re74", "re75")])
Y <- lalonde$re78
W <- lalonde$treat
# Genetic matching (finds optimal weights)
# Note: This can be slow for large datasets
set.seed(123)
gen_weights <- GenMatch(
Tr = W,
X = X,
BalanceMatrix = X,
estimand = "ATT",
M = 1,
print.level = 0,
pop.size = 50 # Smaller for speed
)
# Match using optimized weights
match_out <- Match(
Y = Y,
Tr = W,
X = X,
Weight.matrix = gen_weights,
estimand = "ATT",
M = 1
)
summary(match_out)
# Output:
# Estimate... XXXX
# AI SE...... XXX
# T-stat..... X.XX
# p.val...... 0.XXX7.7 Panel Matching (PanelMatch)
For time-series cross-sectional data:
library(PanelMatch)
# Load democracy data
data(dem, package = "PanelMatch")
# Examine data
head(dem)
# wbcode2 year dem tradewb y
# 1 AFG 1984 FALSE 5.32 -9.997001
# 2 AFG 1985 FALSE 5.42 -9.997001
# Create matched sets
PM_results <- PanelMatch(
lag = 4, # 4 pre-treatment periods
time.id = "year",
unit.id = "wbcode2",
treatment = "dem",
outcome.var = "y",
refinement.method = "mahalanobis",
covs.formula = ~ tradewb,
size.match = 5, # Match to 5 controls
data = dem,
qoi = "att",
lead = 0:4 # Effects 0-4 periods after
)
# Estimate effects
PE_results <- PanelEstimate(
sets = PM_results,
data = dem,
number.iterations = 1000,
confidence.level = 0.95
)
# Summary
summary(PE_results)
# Plot
plot(PE_results)---
8. Standard Errors and Inference
8.1 Clustered Standard Errors
When observations within clusters are correlated:
# Using trade data with country-level clustering
data(trade, package = "fixest")
# One-way clustering
model_1way <- feols(
log(Euros) ~ log(dist_km) | Origin + Destination + Year,
cluster = ~ Origin,
data = trade
)
# Two-way clustering
model_2way <- feols(
log(Euros) ~ log(dist_km) | Origin + Destination + Year,
cluster = ~ Origin + Destination,
data = trade
)
# Compare
etable(model_1way, model_2way,
headers = c("One-way", "Two-way"),
se.below = TRUE)8.2 Conley Spatial Standard Errors
For spatially correlated data:
# Conley SEs with distance cutoff
# fixest has built-in support
# First, add coordinates (simulated for trade data)
set.seed(123)
trade$lon <- runif(nrow(trade), -180, 180)
trade$lat <- runif(nrow(trade), -90, 90)
# Conley SEs (requires coordinates)
model_conley <- feols(
log(Euros) ~ log(dist_km) | Origin + Destination + Year,
vcov = conley(cutoff = 500, lon = "lon", lat = "lat"),
data = trade
)
summary(model_conley)8.3 Driscoll-Kraay Standard Errors
For panels with cross-sectional dependence:
# Driscoll-Kraay SEs
model_dk <- feols(
log(Euros) ~ log(dist_km) | Origin + Destination + Year,
vcov = "DK",
panel.id = ~ Origin + Year,
data = trade
)
summary(model_dk)8.4 Heteroskedasticity-Robust Comparisons
# Compare different SE types
model <- feols(log(Euros) ~ log(dist_km) | Origin + Year, data = trade)
# Different variance-covariance matrices
summary(model, vcov = "iid") # Homoskedastic
summary(model, vcov = "hetero") # HC1
summary(model, vcov = ~ Origin) # Clustered
summary(model, vcov = ~ Origin + Year) # Two-way
# Extract all for comparison
se_comparison <- data.frame(
Type = c("IID", "Hetero", "Cluster Origin", "Two-way"),
SE = c(
se(summary(model, vcov = "iid")),
se(summary(model, vcov = "hetero")),
se(summary(model, vcov = ~ Origin)),
se(summary(model, vcov = ~ Origin + Year))
)
)
print(se_comparison)---
9. Diagnostic Tests
9.1 Parallel Trends Test
# Using base_stagg from fixest
data(base_stagg, package = "fixest")
# base_stagg already has time_to_treatment variable
# 'treated' is the treatment indicator
es_model <- feols(
y ~ i(time_to_treatment, treated, ref = -1) | id + year,
cluster = ~ id,
data = base_stagg
)
# Joint test of pre-treatment coefficients
# Extract pre-period coefficients
pre_coefs <- grep("time_to_treatment::-", names(coef(es_model)), value = TRUE)
# Wald test for joint significance
wald(es_model, keep = pre_coefs)
# Interpretation: Large p-value = cannot reject parallel trends9.2 Placebo Tests
# Placebo: Shift treatment timing
data(base_stagg, package = "fixest")
placebo_results <- lapply(c(-3, -2, -1, 0, 1, 2, 3), function(shift) {
base_stagg_placebo <- base_stagg %>%
mutate(
fake_treat_year = year_treated + shift,
fake_treated = year >= fake_treat_year & !is.na(fake_treat_year)
)
model <- feols(
y ~ fake_treated | id + year,
cluster = ~ id,
data = base_stagg_placebo
)
data.frame(
shift = shift,
coef = coef(model)["fake_treatedTRUE"],
se = se(model)["fake_treatedTRUE"]
)
})
placebo_df <- bind_rows(placebo_results)
# Plot
ggplot(placebo_df, aes(x = shift, y = coef)) +
geom_point(size = 3) +
geom_errorbar(aes(ymin = coef - 1.96*se, ymax = coef + 1.96*se), width = 0.2) +
geom_hline(yintercept = 0, linetype = "dashed") +
geom_vline(xintercept = 0, linetype = "dotted", color = "red") +
labs(x = "Treatment Timing Shift", y = "Estimated Effect",
title = "Placebo Test: Shifting Treatment Timing") +
theme_minimal()9.3 Sensitivity Analysis (sensemakr)
How much unobserved confounding would be needed to explain away the result?
library(sensemakr)
# Load Darfur survey data
data(darfur, package = "sensemakr")
# The peacefactor variable is stored as a matrix - extract as vector
darfur$peace <- as.numeric(darfur$peacefactor)
# Basic regression
model <- lm(peace ~ directlyharmed + female + age + farmer_dar +
herder_dar + pastvoted + hhsize_darfur,
data = darfur)
# Sensitivity analysis
sens <- sensemakr(
model = model,
treatment = "directlyharmed",
benchmark_covariates = "female", # Use as benchmark
kd = 1:3 # Multiples of benchmark
)
# Summary
summary(sens)
# Output:
# Sensitivity Analysis to Unobserved Confounding
#
# Unadjusted Estimates:
# Outcome: peace
# Treatment: directlyharmed
# Estimate: 0.097
# Standard Error: 0.023
# t-value: 4.184
#
# Minimal Strength of Unobserved Confounding:
# -- to bring estimate to 0:
# Partial R2 of the confounder with treatment: 0.025
# Partial R2 of the confounder with outcome: 0.025
# Plot sensitivity
plot(sens)
# Shows contour plot of bias as function of confounder strengthInterpretation: The sensitivity analysis shows how strong an unobserved confounder would need to be (in terms of partial R² with treatment and outcome) to explain away the effect.
---
10. Causal Mediation Analysis
Causal mediation decomposes total treatment effects into direct and indirect (mediated) components.
When to Use Mediation Analysis
- You have a hypothesized mechanism (mediator) between treatment and outcome
- You want to understand "why" a treatment works
- You're willing to make strong assumptions about the mediator
Assumptions
1. No unmeasured treatment-outcome confounding (as in any causal analysis) 2. No unmeasured mediator-outcome confounding (very strong!) 3. No treatment-induced confounding: Treatment doesn't affect confounders of mediator-outcome
Common Pitfalls
- Sequential ignorability is untestable and often implausible
- Post-treatment confounders can severely bias results
- Interaction between treatment and mediator complicates interpretation
10.1 Basic Mediation with mediation package
Using the framing experiment data:
library(mediation)
# Load framing experiment data
data(framing, package = "mediation")
# Examine data
head(framing)
# cond anx age educ gender income ... treat ... immigr
#
# treat = treatment (news framing, binary)
# anx = mediator (anxiety level, stored as factor)
# immigr = outcome (immigration attitude)
# Convert factor to numeric index for analysis (1-7 scale)
framing$anx_num <- as.numeric(framing$anx)
# Step 1: Mediator model (anxiety as function of treatment)
model_m <- lm(anx_num ~ treat + age + educ, data = framing)
# Step 2: Outcome model (includes mediator)
model_y <- lm(immigr ~ treat + anx_num + age + educ, data = framing)
# Step 3: Mediation analysis
med_out <- mediate(
model.m = model_m,
model.y = model_y,
treat = "treat",
mediator = "anx_num",
sims = 1000,
boot = TRUE,
boot.ci.type = "bca"
)
summary(med_out)
# Output:
# Causal Mediation Analysis
#
# Quasi-Bayesian Confidence Intervals
#
# Estimate 95% CI Lower 95% CI Upper p-value
# ACME 0.0877 0.0330 0.15 <2e-16 ***
# ADE 0.0124 -0.0967 0.12 0.84
# Total Effect 0.1001 0.0055 0.20 0.04 *
# Prop. Mediated 0.8762 0.2185 3.40 0.04 *Interpretation:
- ACME (Average Causal Mediation Effect): The indirect effect through anxiety = 0.088
- ADE (Average Direct Effect): The direct effect not through anxiety = 0.012
- Total Effect: ACME + ADE = 0.10
- Proportion Mediated: 88% of the effect operates through anxiety
10.2 Sensitivity Analysis for Mediation
# Sensitivity analysis: How robust is the mediation to confounding?
sens_med <- medsens(med_out, rho.by = 0.05)
summary(sens_med)
# Plot sensitivity
plot(sens_med)
# Shows how ACME changes as correlation between mediator and outcome errors varies10.3 Moderated Mediation
# Interaction model (treatment effect on mediator varies by education)
model_m_int <- lm(anx_num ~ treat * educ + age, data = framing)
model_y_int <- lm(immigr ~ treat * anx_num + educ + age, data = framing)
# Mediation at different education levels
med_low <- mediate(model_m_int, model_y_int, treat = "treat", mediator = "anx_num",
covariates = list(educ = 1), sims = 500)
med_high <- mediate(model_m_int, model_y_int, treat = "treat", mediator = "anx_num",
covariates = list(educ = 4), sims = 500)
# Compare
data.frame(
Education = c("Low", "High"),
ACME = c(med_low$d0, med_high$d0),
ADE = c(med_low$z0, med_high$z0)
)---
11. Marginal Effects (Interpretation)
In non-linear models (Logit, Probit, Poisson), coefficients (β) are not marginal effects. You cannot interpret them as "a 1 unit increase in X leads to a β increase in Y."
When to Use
- You are running
feglm()(Poisson/Logit/NegBin) - You have interaction terms in linear models (interpreting β₃ alone is insufficient)
- You need policy-relevant estimates (e.g., "probability increases by 5%")
Assumptions
- The model is correctly specified
- For average marginal effects: effects are reasonably constant across the distribution
Common Pitfalls
- Reporting raw coefficients from non-linear models as effects
- Ignoring that marginal effects vary across observations in non-linear models
- Not specifying meaningful values for
newdatawhen effects depend on other covariates
Implementation with marginaleffects
The marginaleffects package is the modern replacement for the older margins package. It works seamlessly with fixest.
# Install
install.packages("marginaleffects")
library(marginaleffects)
library(fixest)
# Load trade data
data(trade, package = "fixest")
# Estimate Poisson Fixed Effects (Gravity Model)
# Outcome: Euros (count-like/positive), dist_km is continuous
model_pois <- feglm(
Euros ~ log(dist_km) + Year | Origin + Destination,
family = "poisson",
data = trade
)
# 1. Average Marginal Effects (AME)
# "On average, what is the effect of a 1 unit change in X on Y?"
avg_slopes(model_pois)
# Output:
# Term Contrast Estimate Std. Error z Pr(>|z|)
# Year mean(dY/dX) 1785401 12940362 0.138 0.890
# dist_km mean(dY/dX) -135467 962919 -0.141 0.888
# 2. Marginal Effects at specific values (e.g., for specific years)
slopes(model_pois, newdata = datagrid(Year = 2007:2009))
# 3. Predictions (Marginal Means)
# "What is the predicted trade volume for each year, holding other at means?"
predictions(model_pois, newdata = datagrid(Year = unique))
# 4. Comparisons (discrete changes)
# Effect of moving from 2007 to 2008
comparisons(model_pois, variables = list(Year = c(2007, 2008)))Interpretation: The AME shows the average effect of a 1-unit change in each variable on the outcome (Euros), accounting for the non-linearity of the Poisson model.
Marginal Effects with Interactions
# Linear model with interaction
data(lalonde, package = "MatchIt")
model_int <- lm(re78 ~ treat * age + educ, data = lalonde)
# Marginal effect of treatment varies with age
slopes(model_int, variables = "treat", newdata = datagrid(age = c(20, 30, 40)))
# Plot how treatment effect varies with age
plot_slopes(model_int, variables = "treat", condition = "age")---
12. Wild Cluster Bootstrap (Small Clusters)
Standard clustered standard errors (sandwich estimator) are biased when the number of clusters is small (roughly G < 30-40).
When to Use
- You are clustering by State/Region and have fewer than ~40 groups
- Your standard clustered SEs might be under-rejecting (Type I error)
- You want more reliable inference with few clusters
Assumptions
- Clusters are independent
- Treatment/covariates vary within clusters
- Standard bootstrap assumptions
Common Pitfalls
- Using standard cluster SEs with very few clusters (leads to over-rejection)
- Not setting a seed for reproducibility
- Using too few bootstrap iterations (use at least 999, preferably 9999)
Implementation with fwildclusterboot
Note: fwildclusterboot requires R ≥ 4.0. If unavailable, consider boottest from Stata or the sandwich package with small-sample corrections.
# Install
install.packages("fwildclusterboot")
library(fwildclusterboot)
library(fixest)
# Load data
data(base_did, package = "fixest")
# Run standard FE model
model_fe <- feols(y ~ x1 + treat | id + period, data = base_did)
# Run Wild Cluster Bootstrap
# B = number of bootstrap iterations
boot_res <- boottest(
model_fe,
clustid = "id", # Cluster variable
param = "x1", # Variable of interest
B = 9999, # Bootstrap iterations
seed = 12345 # For reproducibility
)
# Summary
summary(boot_res)
# Output:
# Estimate Std. Error t value Pr(>|t|) CI Lower CI Upper
# 0.998 0.116 8.58 0.000 0.770 1.227
# Plot bootstrap distribution
plot(boot_res)
# The bootstrap p-value and confidence interval are robust to
# the small number of clustersInterpretation: Compare the bootstrap p-value and CI to the standard ones. If they differ substantially, trust the bootstrap results.
Alternative: sandwich with small-sample correction
library(fixest)
# Small-sample cluster correction (G/(G-1) adjustment)
model <- feols(y ~ x1 + treat | id + period,
cluster = ~ id,
ssc = ssc(adj = TRUE, cluster.adj = TRUE), # Small sample correction
data = base_did)
summary(model)---
13. Honest DiD (Parallel Trends Sensitivity)
Standard DiD assumes the parallel trends assumption holds exactly. "Honest DiD" (Rambachan & Roth, 2023) allows you to calculate confidence intervals that are robust to some violation of parallel trends.
When to Use
- You have an event study
- Your pre-trends are "mostly" flat but not perfect
- Reviewer asks: "What if parallel trends is slightly violated?"
- You want to report sensitivity of results to assumption violations
Assumptions
- You're willing to bound the maximum violation of parallel trends
- The violation is smooth (not a sudden jump)
Common Pitfalls
- Ignoring pre-trends that suggest violations
- Setting M (maximum violation) too loosely or too tightly
- Not understanding that wider CIs don't mean the effect is zero—they mean uncertainty increases
Implementation with HonestDiD
Note: HonestDiD requires installation from GitHub and has several dependencies. Installation may require a recent R version.
# Install from GitHub (requires remotes package)
# install.packages("remotes")
# remotes::install_github("asheshrambachan/HonestDiD")
library(HonestDiD)
library(fixest)
# 1. Run Event Study
data(base_stagg, package = "fixest")
es_model <- feols(
y ~ i(time_to_treatment, treated, ref = -1) | id + year,
cluster = ~id,
data = base_stagg
)
# 2. Extract event study coefficients
# Need to identify which coefficients are pre-treatment vs post-treatment
betahat <- coef(es_model)
sigma <- vcov(es_model)
# Get coefficient names
coef_names <- names(betahat)
pre_periods <- grep("::-[0-9]", coef_names) # Negative event times
post_periods <- grep("::[0-9]", coef_names) # Positive event times (including 0)
# 3. Run Honest DiD Sensitivity Analysis
# Focus on the first post-treatment period (event time 0)
# M = maximum allowed violation of parallel trends (slope change)
# Using relative magnitudes approach:
# "How large could the violation be relative to the max pre-trend?"
delta_rm <- HonestDiD::createSensitivityResults_relativeMagnitudes(
betahat = betahat,
sigma = sigma,
numPrePeriods = length(pre_periods),
numPostPeriods = length(post_periods),
Mbarvec = seq(0, 2, by = 0.5) # Range of M values (0 = exact PT)
)
# 4. View results
# Shows how CI changes as you allow larger violations
print(delta_rm)
# 5. Plot Sensitivity
# Shows original CI vs. robust CI at different M values
HonestDiD::createSensitivityPlot_relativeMagnitudes(
robustResults = delta_rm,
originalResults = HonestDiD::constructOriginalCS(betahat, sigma,
numPrePeriods = length(pre_periods),
numPostPeriods = length(post_periods))
)Interpretation:
- M = 0: Assumes parallel trends hold exactly (standard DiD)
- M = 1: Allows violations up to the magnitude of the largest pre-trend
- M = 2: Allows violations up to 2x the largest pre-trend
- If the CI includes zero at small M, your result is sensitive to PT violations
Simplified Approach with fixest
For a quick pre-trends test without the full HonestDiD framework:
# Run event study
es_model <- feols(
y ~ i(time_to_treatment, treated, ref = -1) | id + year,
cluster = ~id,
data = base_stagg
)
# Plot with confidence bands
iplot(es_model,
main = "Event Study with Pre-trends",
xlab = "Time to Treatment")
# Joint test of pre-treatment coefficients = 0
pre_coefs <- grep("::-", names(coef(es_model)), value = TRUE)
wald(es_model, keep = pre_coefs)
# If joint test rejects, pre-trends may be violated
# Consider HonestDiD for sensitivity analysis---
Quick Reference
Package Selection by Task
| Task | Package | Key Function |
|---|---|---|
| FE regression | fixest | `feols(y ~ x \ |
| Modern DiD | did | att_gt() |
| Event study | fixest | feols(y ~ sunab(g, t)) |
| RD | rdrobust | rdrobust(y, x, c = 0) |
| Manipulation test | rddensity | rddensity(X, c = 0) |
| IV | ivreg, fixest | ivreg(), `feols(... \ |
| LASSO | glmnet | cv.glmnet(X, y) |
| Matching | MatchIt | matchit(treat ~ x) |
| Entropy balance | ebal | ebalance(Tr, X) |
| Panel matching | PanelMatch | PanelMatch() |
| Balance | cobalt | bal.tab(), love.plot() |
| Mediation | mediation | mediate(model.m, model.y) |
| Sensitivity | sensemakr | sensemakr(model, treat) |
| Spatial SE | fixest | vcov = conley(r) |
| Marginal effects | marginaleffects | avg_slopes(), predictions() |
| Wild bootstrap | fwildclusterboot | boottest() |
| Honest DiD | HonestDiD | createSensitivityResults_relativeMagnitudes() |
Key Datasets for Practice
| Dataset | Package | Use For |
|---|---|---|
base_did | fixest | DiD, TWFE |
base_stagg | fixest | Event studies, staggered DiD |
trade | fixest | FE regression, clustering |
mpdta | did | Callaway-Sant'Anna |
rdrobust_RDsenate | rdrobust | RD design |
lalonde | MatchIt | Matching methods |
SchoolingReturns | ivreg | IV estimation |
darfur | sensemakr | Sensitivity analysis |
framing | mediation | Mediation analysis |
dem | PanelMatch | Panel matching |
Common Diagnostic Checks
1. TWFE with staggered timing: Check for negative weights 2. DiD: Test parallel trends with event study 3. RD: Test for manipulation, bandwidth sensitivity 4. IV: First-stage F-statistic, overidentification test 5. Matching: Balance statistics, Love plot 6. All: Sensitivity analysis for unobserved confounding