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.
trend = _hp_filter(gdp_series, lamb=1600) # quarterly
cycle = gdp_series.reindex(trend.index) - trendCore 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
- Endpoint bias (Phillips-Jin 2021): boundary trend is unreliable
- λ frequency dependence: use 14400 for monthly, 100 for annual (caller must know)
- Direct-solve O(n³): use statsmodels sparse for > 10k obs
- Hamilton (2018) critique: suggests OLS alternative; this operator retains classic HP due to industry-standard
- No missing dates handling: only dropna values
- 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-equalagrees_with_statsmodels— cross-validate vs statsmodels hpfilter < 1e-8short_series_returns_empty— len<5 → emptypreserves_index— output index == y.dropna() indexdropna_handling— NaN values are droppeddeterminism— 5 reruns consistentlambda_quarterly_constant— HP_LAMBDA_QUARTERLY == 1600.0lambda_sensitivity— higher λ → smoother → larger cycle amplitudeexact_smooth_data— perfectly linear input → cycle ≈ 0
Changelog
- 1.0.0 (2026-04-21) — First Active (Tier 3 batch 3, HP primitive)

