Stationarity Tests
SkillAI & modelsTest whether time-series features are stationary using ADF and KPSS tests. Use when selecting or transforming features to verify stationarity assumptions for predictive models.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Stationarity Tests skill
What this skill tells your AI
The instructions your AI receives, as published by ml4t/skills in validation/stationarity-tests/SKILL.md and read by ahel’s review.
Feeding non-stationary features (raw prices, trending volume) into ML models produces spurious correlations that vanish out of sample. Test stationarity first, transform if needed.
The Problem
Raw asset prices are non-stationary: their mean and variance change over time. A regression of one non-stationary series on another can produce high R-squared and significant t-statistics even when there is no causal relationship (spurious regression). In ML4T, this means a model trained on raw prices will appear to predict well in-sample but fail live. Returns and most financial ratios are stationary. The gap between "looks predictive" and "is predictive" often comes down to stationarity.
The Pattern
WRONG
import numpy as np
from sklearn.linear_model import Ridge
# Model raw prices - non-stationary, spurious correlation
X = price_features # Trending upward
y = forward_prices # Also trending upward
model = Ridge().fit(X, y)
print(f"R-squared: {model.score(X, y):.3f}") # High but meaningless
CORRECT
import numpy as np
from statsmodels.tsa.stattools import adfuller, kpss
def test_stationarity(series):
"""Run ADF and KPSS tests. Both must agree."""
series = series[~np.isnan(series)]
adf_stat, adf_pval, *_ = adfuller(series, autolag='AIC')
kpss_stat, kpss_pval, *_ = kpss(series, regression='c', nlags='auto')
return {
'adf_pval': adf_pval, # H0: unit root (non-stationary)
'kpss_pval': kpss_pval, # H0: stationary
'stationary': adf_pval < 0.05 and kpss_pval > 0.05
}
# Test before modeling
for col in ['price', 'returns', 'momentum_12m', 'volatility_20d']:
result = test_stationarity(features[col].to_numpy())
status = "OK" if result['stationary'] else "TRANSFORM"
print(f"{col:>20}: ADF p={result['adf_pval']:.3f}, "
f"KPSS p={result['kpss_pval']:.3f} -> {status}")
Decision Matrix
| ADF rejects H0? | KPSS rejects H0? | Conclusion | Action |
|---|---|---|---|
| Yes | No | Stationary | Use as-is |
| No | Yes | Non-stationary | Difference or use returns |
| Yes | Yes | Trend-stationary | Remove trend, then use |
| No | No | Inconclusive | Get more data or use returns |
Making Features Stationary
# Method 1: First difference (simplest)
diff = series.diff().dropna()
# Method 2: Log returns (standard for prices)
log_ret = np.log(series / series.shift(1)).dropna()
# Method 3: Fractional differentiation (preserves memory)
# d=1.0 is full differencing; d=0.3-0.5 balances stationarity + memory
from fracdiff import fdiff
frac_diff = fdiff(series.to_numpy(), d=0.4)
Guardrails
- Always run BOTH ADF and KPSS - they have opposite null hypotheses and catch different failures
- KPSS is sensitive to lag selection; use
nlags='auto'to avoid misleading results - Returns are almost always stationary; prices almost never are
- Non-stationary features can still be useful inside tree-based models (which split on rank), but regularized linear models and neural networks require stationarity
- After transformation, verify IC is preserved - aggressive differencing can destroy signal
Production Implementation
ml4t-diagnostic provides batch stationarity testing:
from ml4t.diagnostic.evaluation.stationarity import analyze_stationarity
result = analyze_stationarity(feature_series, include_tests=["adf", "kpss"])
print(result.consensus)
print(result.summary_df)
Checklist
- Both ADF and KPSS tests run on every feature
- Non-stationary features transformed (differencing, returns, or frac-diff)
- Transformation method documented per feature
- IC compared before vs after transformation to verify signal preservation
- Tree models distinguished from linear/DL models in stationarity requirements
Signals
- GitHub stars
- 20
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
ml4t-stationarity-tests- Source
- github.com/ml4t/skills