Part IV · Integration Week 5 Published

Temporal PLR DML for Time Series

Extending DML to time series: why random K-fold leaks future data, time-series cross-validation (expanding/sliding/purged windows with gaps), HAC/Newey-West standard errors with kernel and bandwidth choice, the TemporalPLRDML scalar estimator (lagged controls + temporal CV + HAC), and a Monte Carlo coverage check. Not recursive dynamic g-estimation.

On this page
  1. Introduction
  2. The time series challenge
  3. Chapter roadmap
  4. Time series cross-validation
  5. The data leakage problem
  6. Time series cross-validation strategies
  7. Python implementation
  8. HAC standard errors
  9. The autocorrelation problem
  10. Newey–West estimation
  11. Kernel functions
  12. Bandwidth selection
  13. Python implementation
  14. The TemporalPLRDML framework
  15. Algorithm overview
  16. Implementation
  17. Output interpretation
  18. Monte Carlo validation
  19. Data generating process
  20. Simulation results
  21. Practical recommendations
  22. When to use TemporalPLRDML
  23. Configuration guidelines
  24. Diagnostic checks
  25. Exercises
  26. Exercise 5.1: time series cross-validation
  27. Exercise 5.2: HAC bandwidth selection
  28. Exercise 5.3: comparing standard errors
  29. Summary
  30. Roadmap to Chapter 6

Temporal PLR DML for Time Series

Introduction

Chapters 1–4 developed DML for cross-sectional data where observations are independent. However, many economic questions involve time series data where observations exhibit temporal dependence. This chapter develops the companion repo’s current time-series implementation: scalar partially linear DML with lagged treatment controls, temporal cross-fitting, and HAC inference.

The time series challenge

Consider estimating the effect of a policy intervention TtT_t on an outcome YtY_t:

Yt=τTt+g(Xt,Tt1,,TtL)+εtY_t = \tau \cdot T_t + g(X_t, T_{t-1}, \ldots, T_{t-L}) + \varepsilon_t

Two challenges arise that standard DML cannot handle:

  1. Data leakage in cross-validation: random K-fold splits allow future information to “leak” into the training set, creating unrealistic advantage.
  2. Invalid standard errors: influence function–based SEs assume i.i.d. observations; autocorrelation in εt\varepsilon_t invalidates this assumption.

Chapter roadmap

  • Section 5.2: time series cross-validation — purging, blocking, and embargo.
  • Section 5.3: HAC standard errors — Newey–West estimation for autocorrelation.
  • Section 5.4: TemporalPLRDML framework — combining the components.
  • Section 5.5: Monte Carlo validation — verifying coverage and unbiasedness.
  • Section 5.6: practical recommendations.
  • Section 5.7: exercises.

Time series cross-validation

The data leakage problem

Standard K-fold cross-validation randomly assigns observations to folds. With time series data, this means the model predicting Y100Y_{100} might train on {Y99,Y101,Y102}\{Y_{99}, Y_{101}, Y_{102}\} — data that would be unavailable in a real forecasting scenario.

Example 5.1 (Data Leakage Illustration).

Consider a 10-observation time series with 2-fold CV.

Random splitting (wrong for time series):

Fold 1: Train on {t=2,3,5,7,9}, Test on {t=1,4,6,8,10}
Fold 2: Train on {t=1,4,6,8,10}, Test on {t=2,3,5,7,9}

When testing t=4t=4, the model trains on t=5,7,9t=5,7,9future data.

Temporal splitting (correct):

Fold 1: Train on {t=1,2,3,4,5}, Test on {t=6,7,8,9,10}

Now testing never uses future training data.

Time series cross-validation strategies

Remark (Companion v1.1.0).

As of companion release 1.1.0, the temporal-validation infrastructure in this chapter — cross-validation splitters, HAC/Newey–West inference, and stationarity diagnostics — is consumed from the temporalcv library rather than maintained inside the companion. Each migration was gated on golden snapshots of the estimators’ numerical output; the one deliberate behavioral change (the forward-only purged walk replacing a bidirectional purged K-fold) is a leakage correction, documented where it appears below.

Three strategies are available (implemented upstream in temporalcv; the companion’s estimators select among them via cv_strategy):

Definition 5.2 (Expanding Window CV).

For KK folds with gap gg and test size mm:

Fold k:Train={1,,nk}Test={nk+g+1,,nk+g+m}\begin{aligned} \text{Fold } k: \quad &\text{Train} = \{1, \ldots, n_k\} \\ &\text{Test} = \{n_k + g + 1, \ldots, n_k + g + m\} \end{aligned}

where nk=nmin+kΔn_k = n_{\min} + k \cdot \Delta grows with each fold. The training window expands over time, using all available historical data.

Definition 5.3 (Sliding Window CV).

Like expanding window, but with fixed training size ww:

Fold k:Train={nkw+1,,nk}Test={nk+g+1,,nk+g+m}\begin{aligned} \text{Fold } k: \quad &\text{Train} = \{n_k - w + 1, \ldots, n_k\} \\ &\text{Test} = \{n_k + g + 1, \ldots, n_k + g + m\} \end{aligned}

The window “slides” forward, discarding old data to maintain adaptivity to regime changes.

Definition 5.4 (Purged Walk-Forward CV).

Following de Prado ( Prado (2018) ), for data with overlapping labels or leakage concerns, a forward-only walk with purging:

  1. Walk test windows forward through time, training only on observations before each test window.
  2. Purge the last gg training observations adjacent to the test boundary (the purge_gap), so labels that overlap the test window never enter training.
Remark.

De Prado’s original formulation is a bidirectional purged K-fold (training on both sides of each test fold, with a post-test embargo). For nuisance estimation in temporal DML that bidirectionality is itself lookahead: an earlier revision of this companion’s purged_cv strategy trained nuisances on observations after the test window, and was replaced by the forward-only walk above.

Python implementation

Our TimeSeriesCrossValidator handles all three strategies:

from temporalcv import TimeSeriesCrossValidator

# Expanding window with 5 folds and 2-period gap
cv = TimeSeriesCrossValidator(
    n_splits=5,
    gap=2,          # Periods between train end and test start
    expanding=True, # True for expanding, False for sliding
    min_train_size=50
)

# Generate train/test indices
for train_idx, test_idx in cv.split(X):
    print(f"Train: {train_idx[:5]}...{train_idx[-5:]}")
    print(f"Test:  {test_idx[:5]}...{test_idx[-5:]}")
Remark.

The gap parameter is critical for applications where predictions take time to materialize (e.g., quarterly economic forecasts, insurance claims with reporting lag). Setting gap=1 prevents any overlap between training and testing periods.

HAC standard errors

The autocorrelation problem

Even with correct point estimates, standard errors require adjustment for serial correlation. The DML influence function

ψt=(Y~tτ^T~t)T~tE[T~2]\psi_t = \frac{(\tilde{Y}_t - \hat{\tau} \tilde{T}_t) \tilde{T}_t}{\mathbb{E}[\tilde{T}^2]}

has E[ψt]=0\mathbb{E}[\psi_t] = 0 but Cov(ψt,ψtk)0\mathrm{Cov}(\psi_t, \psi_{t-k}) \neq 0 when outcomes are autocorrelated.

Theorem 5.5 (HAC Variance of DML Estimator).

Under regularity conditions, the DML estimator satisfies:

n(τ^τ0)dN(0,Ω)\sqrt{n}(\hat{\tau} - \tau_0) \xrightarrow{d} N(0, \Omega)

where Ω\Omega is the long-run variance:

Ω=k=Cov(ψ0,ψk)=γ0+2k=1γk\Omega = \sum_{k=-\infty}^{\infty} \mathrm{Cov}(\psi_0, \psi_k) = \gamma_0 + 2\sum_{k=1}^{\infty} \gamma_k

with γk=Cov(ψt,ψtk)\gamma_k = \mathrm{Cov}(\psi_t, \psi_{t-k}) the autocovariance at lag kk.

Newey–West estimation

We cannot sum to infinity, so we truncate with a kernel weighting scheme:

Definition 5.6 (Newey-West HAC Estimator).

Following Newey and West ( Newey & West (1987) ), the HAC variance estimator is:

Ω^NW=γ^0+2k=1Bwkγ^k\hat{\Omega}_{NW} = \hat{\gamma}_0 + 2\sum_{k=1}^{B} w_k \hat{\gamma}_k

where:

  • γ^k=1nt=k+1nψtψtk\hat{\gamma}_k = \frac{1}{n}\sum_{t=k+1}^{n} \psi_t \psi_{t-k} is the sample autocovariance,
  • wk=K(k/B)w_k = K(k/B) is a kernel weight,
  • BB is the bandwidth (truncation parameter).

Kernel functions

Three commonly used kernels (implemented upstream in temporalcv):

  1. Bartlett (triangular):
KBartlett(x)={1xx10x>1K_{\text{Bartlett}}(x) = \begin{cases} 1 - |x| & |x| \leq 1 \\ 0 & |x| > 1 \end{cases}

Linear downweighting; guaranteed positive semi-definite.

  1. Parzen:
KParzen(x)={16x2+6x3x0.52(1x)30.5<x10x>1K_{\text{Parzen}}(x) = \begin{cases} 1 - 6x^2 + 6|x|^3 & |x| \leq 0.5 \\ 2(1-|x|)^3 & 0.5 < |x| \leq 1 \\ 0 & |x| > 1 \end{cases}

Smoother than Bartlett; used for higher-order accuracy.

  1. Quadratic spectral:
KQS(x)=2512π2x2(sin(6πx/5)6πx/5cos(6πx/5))K_{\text{QS}}(x) = \frac{25}{12\pi^2 x^2}\left(\frac{\sin(6\pi x/5)}{6\pi x/5} - \cos(6\pi x/5)\right)

Optimal for minimizing MSE; non-truncating.

Comparison of HAC kernel functions — all decay to zero as lag increases, but with different rates and smoothness:

BartlettParzenQuadratic Spectral
Linear decaySmooth decayOptimal MSE

Bandwidth selection

The bandwidth BB controls the bias–variance tradeoff:

  • BB too small: misses important autocovariances (bias).
  • BB too large: includes noise from distant lags (variance).
Definition 5.7 (Optimal Bandwidth Rule).

The rule-of-thumb bandwidth for the Bartlett kernel, as implemented in temporalcv:

B=n1/3B^* = \left\lfloor n^{1/3} \right\rfloor

For n=500n=500: B=7B^* = 7 lags. Constant factors vary across texts and implementations (Newey & West’s (1994) own rule of thumb is 4(n/100)2/94(n/100)^{2/9}); the growth rate n1/3n^{1/3} is what matters for the Bartlett kernel’s consistency.

Data-driven selection (Andrews, 1991) adapts based on estimated autocorrelation:

BAndrews=1.1447(ρ^2n(1ρ^)4)1/3B_{\text{Andrews}} = 1.1447 \left( \frac{\hat{\rho}^2 n}{(1-\hat{\rho})^4} \right)^{1/3}

where ρ^\hat{\rho} is the AR(1) coefficient of the influence scores.

Python implementation

from temporalcv import newey_west_se

result = newey_west_se(
    influence_scores,
    bandwidth="auto",   # floor(n^(1/3)) Newey-West rule
    kernel="bartlett",
)

# HACResult separates the three scales explicitly:
print(f"Long-run variance (Omega):      {result.long_run_variance:.4f}")
print(f"Variance of the mean (Omega/n): {result.variance:.4f}")
print(f"HAC SE = sqrt(Omega/n):         {result.se:.4f}")
print(f"Bandwidth used:                 {result.bandwidth}")
Remark.

The explicit long_run_variance / variance / se split exists precisely because conflating Ω\Omega with Ω/n\Omega/n produces silent n\sqrt{n}-scale errors in downstream standard errors; an earlier revision of this companion’s estimator divided by nn twice at exactly this seam.

The TemporalPLRDML framework

Algorithm overview

TemporalPLRDML combines time series cross-validation with HAC inference:

Algorithm: TemporalPLRDML Estimation
Require: Outcome Y, Treatment T, Confounders X, number of lags L
Ensure: Treatment effect τ̂ with HAC standard error

 1. Create lagged features:  X̃_t = (X_t, T_{t-1}, ..., T_{t-L})
 2. Initialize: time series cross-validator with gap g
 3. for each fold k = 1, ..., K:
 4.     Train outcome model   m̂^(-k) on training data
 5.     Train treatment model ℓ̂^(-k) on training data
 6.     Predict on test fold:  Ŷ^(-k)_t, T̂^(-k)_t
 7. Exclude uncovered early rows: drop observations without valid
    temporal out-of-fold predictions
 8. Compute residuals:  Ỹ_t = Y_t - Ŷ^(-t)_t,  T̃_t = T_t - T̂^(-t)_t
 9. Estimate τ:  τ̂ = (Σ_t Ỹ_t T̃_t) / (Σ_t T̃_t²)
10. Compute influence scores:  ψ_t = (Ỹ_t - τ̂ T̃_t) T̃_t / mean(T̃²)
11. HAC variance:  Ω̂ = γ̂_0 + 2 Σ_{k=1}^B w_k γ̂_k
12. Standard error:  SE(τ̂) = sqrt(Ω̂ / n)
return τ̂, SE(τ̂)

Implementation

from dml_ts import TemporalPLRDML
import numpy as np

# Generate time series data with autocorrelation
np.random.seed(42)
n = 500
time = np.arange(n)

# Confounders with temporal structure
X = np.column_stack([
    np.random.randn(n),
    np.sin(2 * np.pi * time / 100)  # Seasonal
])

# Autocorrelated treatment
T = np.zeros(n)
T[0] = np.random.randn()
for t in range(1, n):
    T[t] = 0.3 * T[t-1] + 0.5 * X[t, 0] + np.random.randn()

# Outcome with true effect tau = 2.0
true_tau = 2.0
Y = true_tau * T + X[:, 0]**2 + np.random.randn(n)

# Fit TemporalPLRDML
model = TemporalPLRDML(
    n_lags=3,
    model_y="random_forest",
    model_t="random_forest",
    cv_strategy="time_series_split",
    hac_kernel="bartlett",
    random_state=42,
)

result = model.fit(Y, T, X, time_index=time)
print(result.summary())

Output interpretation

Temporal PLR DML Results
========================
Treatment Effect (θ):    2.1831
HAC Standard Error:      0.1060
t-statistic:             20.60
p-value:                 0.0000
95% Confidence Interval: [1.9754, 2.3907]

Sample Information:
  Observations:          500
  Used observations:     410
  Lag rows dropped:      3
  CV rows dropped:       87

Nuisance Model Diagnostics:
  Outcome R² (CV):       0.211
  Treatment R² (CV):     0.081

HAC Inference:
  Bandwidth:             7
  CV Strategy:           time_series_split
Remark.

The HAC bandwidth of 7 was automatically selected by the nused1/3\lfloor n_{\text{used}}^{1/3} \rfloor rule implemented in temporalcv: for nused=410n_{\text{used}}=410, 4101/3=7.43=7\lfloor 410^{1/3} \rfloor = \lfloor 7.43 \rfloor = 7.

Monte Carlo validation

We validate TemporalPLRDML through simulation studies examining:

  1. Unbiasedness: E[τ^]τ0\mathbb{E}[\hat{\tau}] \approx \tau_0.
  2. Coverage: the 95% CI covers τ0\tau_0 in 95%\approx 95\% of simulations.
  3. SE calibration: the reported SE matches the empirical standard deviation.

Data generating process

def generate_ts_dgp(n, tau=2.0, rho_T=0.3, rho_eps=0.2, seed=None):
    """
    Generate time series DGP with autocorrelated treatment and errors.

    Parameters
    ----------
    n : int
        Sample size
    tau : float
        True treatment effect
    rho_T : float
        AR(1) coefficient for treatment
    rho_eps : float
        AR(1) coefficient for outcome error

    Returns
    -------
    Y, T, X, time : arrays
    """
    rng = np.random.default_rng(seed)
    time = np.arange(n)

    # Confounders
    X = rng.standard_normal((n, 3))

    # AR(1) treatment
    T = np.zeros(n)
    T[0] = rng.standard_normal()
    for t in range(1, n):
        T[t] = rho_T * T[t-1] + 0.5 * X[t, 0] + rng.standard_normal()

    # AR(1) errors
    eps = np.zeros(n)
    eps[0] = rng.standard_normal()
    for t in range(1, n):
        eps[t] = rho_eps * eps[t-1] + rng.standard_normal()

    # Outcome
    Y = tau * T + X[:, 0]**2 + 0.5 * X[:, 1] + eps

    return Y, T, X, time

Simulation results

TemporalPLRDML Monte Carlo results (n=500n=500, 200 simulations):

MethodBiasEmp. SEAvg. SECoverage
DML (i.i.d. SE)0.020.120.0878%
TemporalPLRDML (HAC SE)0.020.120.1193%

Practical recommendations

When to use TemporalPLRDML

  1. Time series data: any setting with temporal ordering.
  2. Autocorrelated outcomes: check with Durbin–Watson or Ljung–Box tests.
  3. Lagged treatment controls: when prior treatments are confounders or state variables for the current scalar effect.
  4. Macro/finance applications: interest rates, prices, policies over time.

Configuration guidelines

TemporalPLRDML configuration guide

ParameterDefaultGuidance
n_lags1Increase for longer-memory effects
cv_strategytime_series_splitUse purged_cv for financial data
gap0Set to forecast horizon
hac_bandwidthautoManual for specific autocorrelation patterns
hac_kernelbartlettparzen for smoother estimates

Diagnostic checks

Before reporting results, verify:

  1. Nuisance model fit: check cross-validated R2R^2 for both outcome and treatment models. Poor fit (R2<0.1R^2 < 0.1) suggests missing confounders or model misspecification.
  2. Residual autocorrelation: plot the autocorrelation function of influence scores. Significant spikes beyond the HAC bandwidth indicate potential issues.
  3. Bandwidth sensitivity: re-estimate with bandwidth ±50%\pm 50\%. Large SE changes suggest autocorrelation structure uncertainty.

Exercises

Exercise 5.1: time series cross-validation

Consider a dataset with n=100n=100 observations indexed t=1,,100t=1,\ldots,100. You want to use 5-fold time series CV with a gap of 2 periods.

(a) What are the train and test indices for each fold using expanding window CV? (b) How many observations are in each test fold? (c) Why is the gap parameter important for forecasting applications?

Solution.

(a) With 5 folds and gap=2:

  • Fold 1: Train {1,,16}\{1,\ldots,16\}, Test {19,,36}\{19,\ldots,36\} (after gap).
  • Fold 2: Train {1,,34}\{1,\ldots,34\}, Test {37,,54}\{37,\ldots,54\}.
  • …continuing the pattern.

(b) Each test fold contains approximately n/K20n/K \approx 20 observations (exact depends on rounding).

(c) The gap prevents data leakage when there’s a delay between when information is available and when predictions are needed (e.g., quarterly economic data released with lag).

Exercise 5.2: HAC bandwidth selection

You estimate a treatment effect with n=1000n=1000 observations and observe AR(1) autocorrelation ρ^=0.4\hat{\rho} = 0.4 in the influence scores.

(a) Calculate the rule-of-thumb bandwidth B=n1/3B^* = \lfloor n^{1/3} \rfloor. (b) Calculate the Andrews data-driven bandwidth. (c) Which would you recommend and why?

Solution.

(a) B=10001/3=10=10B^* = \lfloor 1000^{1/3} \rfloor = \lfloor 10 \rfloor = 10.

(b) BAndrews=1.1447(0.42×1000(10.4)4)1/3=1.1447×(1600.1296)1/312B_{\text{Andrews}} = 1.1447 \left( \frac{0.4^2 \times 1000}{(1-0.4)^4} \right)^{1/3} = 1.1447 \times \left( \frac{160}{0.1296} \right)^{1/3} \approx 12.

(c) Both suggest similar bandwidths (12\approx 121313). The Andrews method is preferred when you have a reliable AR(1) estimate, as it adapts to the actual autocorrelation structure.

Exercise 5.3: comparing standard errors

A researcher estimates τ^=1.5\hat{\tau} = 1.5 using DML and reports two standard errors: an i.i.d. influence function SE of 0.100.10 and a HAC (Newey–West) SE of 0.180.18.

(a) Compute the 95% confidence intervals under each assumption. (b) What does the difference in SEs tell you about the data? (c) Which CI should be reported in a time series setting?

Solution.

(a) i.i.d.: 1.5±1.96×0.10=[1.30,1.70]1.5 \pm 1.96 \times 0.10 = [1.30, 1.70]. HAC: 1.5±1.96×0.18=[1.15,1.85]1.5 \pm 1.96 \times 0.18 = [1.15, 1.85].

(b) The HAC SE being nearly twice the i.i.d. SE indicates substantial positive autocorrelation in the influence scores. The i.i.d. assumption severely understates uncertainty.

(c) The HAC confidence interval [1.15,1.85][1.15, 1.85] should be reported. Using the narrower i.i.d. interval would claim false precision.

Summary

This chapter extended DML to time series settings through three key innovations:

  1. Time series cross-validation: expanding/sliding windows with purging prevent data leakage while maintaining temporal ordering.
  2. HAC standard errors: Newey–West estimation accounts for autocorrelation in influence scores, providing correctly sized confidence intervals.
  3. TemporalPLRDML framework: integrates lagged controls, temporal CV, and HAC inference into a scalar PLR estimation procedure.

Key takeaways:

  • Standard random-fold DML can create future-data leakage and misleading uncertainty for ordered data.
  • Time series CV with gaps prevents future information leakage.
  • HAC SEs with bandwidth Bn1/3B \approx n^{1/3} correct for autocorrelation.
  • TemporalPLRDML reports lag rows and temporal-CV rows dropped before the final residual regression.

Roadmap to Chapter 6

Chapter 6 extends our time series framework in two directions:

  1. Panel DML: combining fixed effects with DML for panel data (repeated cross-sections over time).
  2. Rolling Window DML: estimating time-varying treatment effects when the causal relationship is non-stationary.

These methods are essential for applications where treatment effects may differ across individuals or change over time — common in insurance pricing, macroeconomic policy, and financial markets.