Skip to content

What (One-liner)

Classic Hodrick-Prescott filter: trend = argmin ||y - t||² + λ ||D t||², solved directly via numpy.linalg.solve((I + λ D'D) t = y). Decomposes time series into trend + cycle. Default λ=1600 (HP 1997 quarterly).

How to use

Internal Python primitive — not a Canvas node. Called by mfle_output_gap, also importable in custom graphlets via from services.mfle.output_gap import _hp_filter.

python
trend = _hp_filter(gdp_series, lamb=1600)    # quarterly
cycle = gdp_series.reindex(trend.index) - trend

Core formulas

# Minimization:
trend = argmin_{t} ||y - t||² + λ ||D t||²

# where D is (n-2, n) second-difference matrix:
#   row i = [0...0, 1, -2, 1, 0...0]  at columns (i, i+1, i+2)

# First-order condition:
(I + λ D'D) t = y

# Direct solve:
M = eye(n) + lamb * (D.T @ D)
trend = numpy.linalg.solve(M, y.values)

Assumptions & applicability

Assumptions: Series sorted by time index + no time-gap + correct frequency λ passed + length ≥ 5 + pandas Series (not list / ndarray).

Applicable: Trend extraction for macro series like GDP / IP / CPI; output_gap calculation; custom detrending.

Not applicable: Real-time endpoint estimation (boundary bias), very long series (O(n³)), structural break, tick frequency.

Input / Output contract

(series: pd.Series, lamb=1600)pd.Series (trend, same index as series.dropna()); len<5 → empty Series.

Known limitations

  1. Endpoint bias (Phillips-Jin 2021): boundary trend is unreliable
  2. λ frequency dependence: use 14400 for monthly, 100 for annual (caller must know)
  3. Direct-solve O(n³): use statsmodels sparse for > 10k obs
  4. Hamilton (2018) critique: suggests OLS alternative; this operator retains classic HP due to industry-standard
  5. No missing dates handling: only dropna values
  6. No structural break detection

References

Hodrick-Prescott (1997) original paper + Ravn-Uhlig (2002) frequency-adjusted λ + Phillips-Jin (2021) end-point bias + Hamilton (2018) HP criticism. See frontmatter for details.

Golden Test

tests/golden/fixtures/tier3/hp_filter/, statsmodels cross-validation (agreement 5e-11 at λ=1600). 1e-10 snapshot tolerance. 9 tests:

  • matches_snapshot — 6 scenarios byte-equal
  • agrees_with_statsmodels — cross-validate vs statsmodels hpfilter < 1e-8
  • short_series_returns_empty — len<5 → empty
  • preserves_index — output index == y.dropna() index
  • dropna_handling — NaN values are dropped
  • determinism — 5 reruns consistent
  • lambda_quarterly_constant — HP_LAMBDA_QUARTERLY == 1600.0
  • lambda_sensitivity — higher λ → smoother → larger cycle amplitude
  • exact_smooth_data — perfectly linear input → cycle ≈ 0

Changelog

  • 1.0.0 (2026-04-21) — First Active (Tier 3 batch 3, HP primitive)

Verifiable intelligence for the decisions that demand scrutiny.