---
title: "Vertical Integration, Supplier Behavior and Quality Upgrading"
format:
  html:
    theme: cosmo
    toc: true
    embed-resources: true
    self-contained-math: true
    html-math-method: katex
    code-copy: true
    include-in-header:
      text: |
        <style>
        body{font-family:system-ui,sans-serif;line-height:1.6}
        main,.cell,.cell-output-display{min-width:0;max-width:100%}
        .cell-output-display,table{overflow-x:auto}
        table{display:block;font-size:.85rem}
        img{max-width:100%;height:auto!important}
        .katex-display{overflow-x:auto;overflow-y:hidden;max-width:100%}
        p code,li code{white-space:normal;overflow-wrap:anywhere}
        </style>
jupyter: python3
execute:
  warning: false
  error: false
  cache: false
  fig-dpi: 140
---

```{python}
#| tags: [parameters]
study_file = "templates/configs/vertical-integration.json"
config = {}  # Overrides: outcomes, treatments, IDs, controls, weights, horizons.
reuse_results = False
```

```{python}
#| label: setup
import json
import sys
from pathlib import Path

ROOT = next(
    p
    for p in [Path.cwd(), *Path.cwd().parents]
    if (p / "TestingInEventStudies/code/saturated.py").exists()
)
sys.path.insert(0, str(ROOT / "templates"))
from replication_engine import Replication, table, text  # noqa: E402

settings = json.loads((ROOT / study_file).read_text()) | config
run = Replication(settings, ROOT)
```

## Source data and variables

```{python}
run.describe()
df = run.df
df.head()
```

### Outcome scaling and sample

Every model uses $Y_{it}/s_Y$, where $s_Y$ is the **unweighted sample standard deviation of the prepared source outcome, before exclusions** (`ddof=1`). This is Peter's scaling convention; outcomes are not centered. The static, dynamic, imputation and saturated models use the same eligible observations.

Adoption is the within-unit minimum period with treatment equal to one. Relative time is the current period index minus the adoption index. Units treated at their first observed period are removed. Treated units must have an observed period −1. Fixed-effect singleton rows are removed recursively. The counts below show the resulting sample.

```{python}
#| label: construct-panel
sample = run.prepare()
table(sample.T.reset_index().rename(columns={"index": "Quantity", 0: "Value"}))
```

## Adoption timing

```{python}
#| label: adoption-plot
run.panelview()
```

## Estimation

1. **Static TWFE:** treatment, configured controls, and unit and period fixed effects.
2. **Dynamic TWFE:** event-time indicators, omitting period −1, plus the same controls and fixed effects.
3. **BJS / did2s:** fit the fixed effects and configured controls on untreated observations; regress imputation residuals on event-time indicators. Never-treated observations have zero in every second-stage indicator.
4. **Sun–Abraham:** cohort-by-event-time regression, aggregated using treated-observation shares over estimable cohort cells at each event time.

The implementation uses `pf.feols`, `pf.did2s`, and `pf.event_study(estimator="saturated")`. The helper retains singleton unit effects in the did2s first stage: one pre-treatment observation identifies a unit intercept. There is no binning of event-time tails. Post-treatment averages use the same event-time weights across the three dynamic estimators.

```{python}
#| label: estimation
if reuse_results:
    run.load_results()
else:
    run.estimate()
    run.f_tests()
    run.export()
```

```{python}
#| label: estimates
run.estimate_table()
```

### Event-study figures

Intervals are pointwise 95%, clustered as specified above. The did2s covariance accounts for both estimation stages. The static TWFE estimate and interval are overlaid on the dynamic TWFE figure. TWFE and Sun–Abraham normalize period −1 to zero; did2s reports untreated-outcome residual means without this normalization.

```{python}
#| label: event-studies
run.event_plots()
```

## Cohort-deviation F-test

The imported `TestingInEventStudies` design parameterizes the saturated regression as a common event-time path plus cohort deviations. The earliest treated cohort supplies the reference path. Unit and period fixed effects and the configured covariates are retained.

The null sets all identified cohort deviations at **positive event times** to zero. Common event-time effects, leads, and period-zero deviations remain unrestricted. The reported test is the **homoskedastic nested-model F-test**, computed from restricted and unrestricted residual sums of squares; it is separate from the clustered intervals above.

```{python}
#| label: deviations-test
run.f_report()
```

## Results and reproducibility

```{python}
#| label: downloads
#| echo: false
slug = settings["slug"]
base = "https://lalten.org/pages/"
text(
    f"[Estimates JSON]({base}{slug}-estimates.json) · "
    f"[Study configuration]({base}{slug}-config.json) · "
    f"[Quarto template]({base}replication-template.qmd) · "
    f"[Code and configurations]({base}did-replication-template.zip)"
)
```

JSON contains scalar estimates, standard errors, p-values, intervals, event-study paths, F-test restrictions and results, sample counts, SD denominators, package versions, and source/code hashes. The numerical checks compare did2s with direct imputation and the saturated fit with its deviations reparameterization.

The common-sample, unbinned event studies here are not Peter's original estimator-specific samples or horizon rules. The outcome scaling is the same. Each page identifies the one archive specification being analyzed.

### Edit and rerun

The [shared Jot template](https://lalten.org/jot/s/svqqvfc15sdryq) is editable by anyone with its link. Edit the JSON configuration to change input paths, outcomes, treatments, unit/time/cluster IDs, covariates, baseline-by-period interactions (`baseline_by_time`), weights, display window, post-treatment horizon and aggregation weights. `period_values` supplies a complete ordered grid for nonnumeric or irregularly encoded periods. All outcome–treatment pairs run separately.

```bash
uv sync --locked
uv sync --locked --project TestingInEventStudies
QUARTO_PYTHON="$PWD/.venv/bin/python" OPENBLAS_NUM_THREADS=1 \
  quarto render templates/replication-template.qmd \
  -P study_file:templates/configs/credit-markets.json
```

Inputs are prepared unit-period panels with binary treatment, never-treated controls, and at least two treated cohorts. Treatment must be absorbing unless `reversal_policy: drop_post_untreated` is explicitly selected; that option follows Peter’s rule of removing untreated rows after first adoption. All such exclusions are counted above. The main fits use the parent project environment; the imported design uses the pinned `TestingInEventStudies` environment. Code cells call the downloadable helper modules, keeping the report source short.

### References

- [PyFixest: difference-in-differences](https://pyfixest.org/tutorials/difference-in-differences.html)
- [TestingInEventStudies: original design and restrictions](https://github.com/apoorvalal/TestingInEventStudies/blob/67fcce76736e78863a53da9696b405ab31037672/code/saturated.py)
- [Sun and Abraham (2021)](https://doi.org/10.1016/j.jeconom.2020.09.006)
- [Borusyak, Jaravel and Spiess (2024)](https://doi.org/10.1093/restud/rdae007)
