Detecting Seasonality & Patterns
Discover hidden patterns in your time series data to improve forecasts.
Why Pattern Detection Matters
Correct pattern identification → Better model choice → More accurate forecasts
Data with weekly seasonality
↓
Detected with TS_DETECT_SEASONALITY
↓
Use AutoETS with seasonal_period=7
↓
Accurate forecasts ✓
Detecting Seasonality
Method 1: Visual Inspection
Plot data and look for repeating patterns:
-- Export data for visualization
SELECT
date,
sales,
DAYNAME(date) as day_name,
WEEK(date) as week_num,
MONTH(date) as month_num
FROM sales_data
ORDER BY date
LIMIT 100;
Signs of seasonality:
- Repeating peaks and valleys
- Regular spikes every N days/weeks/months
- Waves that recur at fixed intervals
Method 2: Autocorrelation Function (ACF)
Correlation between values at different time lags:
-- High correlation at lag 7 = weekly seasonality
-- High correlation at lag 365 = yearly seasonality
Method 3: Automatic Detection
Let AnoFox detect seasonality:
-- Simple detection
SELECT * FROM TS_DETECT_SEASONALITY(
'sales_data',
'date',
'sales',
{'method': 'auto'}
);
Output:
is_seasonal detected_period strength p_value
true 7 0.85 0.001
Interpretation:
is_seasonal=true: Seasonality existsdetected_period=7: Pattern repeats every 7 daysstrength=0.85: Strong seasonality (scale 0-1)p_value=0.001: Statistically significant (p < 0.05)
Method 4: Multi-Period Detection
Find all seasonal periods:
-- Detect multiple seasonality (hourly + daily + yearly)
SELECT * FROM TS_DETECT_SEASONALITY_ALL(
'traffic_data',
'timestamp',
'visitors',
{'max_periods': 5}
);
-- Output: 24 (hourly), 168 (weekly), 8760 (yearly)
Understanding Seasonality Results
Seasonality Strength
strength = 0.0-0.2 → Very weak (might be noise)
strength = 0.2-0.5 → Moderate seasonality
strength = 0.5-0.8 → Strong seasonality
strength = 0.8-1.0 → Very strong seasonality
Action based on strength:
- < 0.3: Can ignore, use non-seasonal model
- 0.3-0.7: Include in model with seasonal_period
-
0.7: Critical to include seasonality
Statistical Significance (p-value)
p_value < 0.05: Seasonality is real, not just noisep_value > 0.05: Could be random fluctuation
Rule: Only use detected seasonality if p_value < 0.05
Decomposing Your Series
Break down into components:
STL Decomposition (Single Seasonality)
-- Separate trend, seasonal, and noise
CREATE TABLE decomposed AS
SELECT
date,
sales,
trend,
seasonal,
remainder
FROM TS_STL_DECOMPOSITION(
'sales_data',
'date',
'sales',
{'seasonal_period': 7}
);
-- View components
SELECT
date,
ROUND(sales, 2) as original,
ROUND(trend, 2) as trend,
ROUND(seasonal, 2) as seasonal,
ROUND(remainder, 2) as noise
FROM decomposed
LIMIT 20;
Interpretation:
- trend: Long-term direction (increasing/decreasing)
- seasonal: Repeating pattern effect
- remainder: Random variation/noise
Example output:
date | original | trend | seasonal | noise
-----------|----------|-------|----------|------
2024-01-01 | 100.5 | 98.2 | 2.1 | 0.2
2024-01-02 | 95.3 | 98.5 | -3.5 | 0.3
2024-01-03 | 108.7 | 98.8 | 9.2 | 0.7
What this tells us:
- Sales trending up slightly
- Strong weekly pattern (±2-9 units around trend)
- Low noise level
MSTL Decomposition (Multiple Seasonality)
For complex data with hourly + daily + yearly patterns:
-- Multiple seasonality decomposition
CREATE TABLE mstl_decomposed AS
SELECT
timestamp,
visitors,
trend,
seasonal_hourly,
seasonal_daily,
remainder
FROM TS_MSTL_DECOMPOSITION(
'traffic_data',
'timestamp',
'visitors',
{'seasonal_periods': [24, 168]}
);
-- View multiple seasonal effects
SELECT
timestamp,
ROUND(visitors, 0) as actual,
ROUND(trend, 1) as trend,
ROUND(seasonal_hourly, 1) as hour_effect,
ROUND(seasonal_daily, 1) as day_effect,
ROUND(remainder, 1) as noise
FROM mstl_decomposed
LIMIT 48;
Detecting Changepoints
Find structural breaks where data pattern changes:
-- Detect changepoints (regime shifts)
SELECT
date,
is_changepoint,
probability
FROM TS_DETECT_CHANGEPOINTS(
'sales_data',
'date',
'sales',
{'hazard_lambda': 250} -- Expected spacing between changepoints
);
-- View only confirmed changepoints
SELECT date
FROM TS_DETECT_CHANGEPOINTS(...)
WHERE is_changepoint = true
ORDER BY date;
Output example:
date | is_changepoint | probability
-----------|---------------|-------------
2023-06-15 | true | 0.92
2023-12-01 | true | 0.87
Interpretation:
is_changepoint=true: Structural break detectedprobability=0.92: 92% confident this is a real changepoint
Parameter Tuning
-- Adjust sensitivity
SELECT * FROM TS_DETECT_CHANGEPOINTS(
'sales_data',
'date',
'sales',
{'hazard_lambda': 50} -- More sensitive (more changepoints)
);
SELECT * FROM TS_DETECT_CHANGEPOINTS(
'sales_data',
'date',
'sales',
{'hazard_lambda': 500} -- Less sensitive (fewer changepoints)
);
Analyzing Detected Patterns
Summary Statistics
-- Seasonality summary
WITH seasonality_analysis AS (
SELECT * FROM TS_DETECT_SEASONALITY(
'sales_data',
'date',
'sales',
{}
)
)
SELECT
CASE is_seasonal
WHEN true THEN 'Seasonal'
ELSE 'Non-seasonal'
END as data_type,
detected_period as period_days,
ROUND(strength, 2) as strength,
CASE
WHEN strength > 0.7 THEN 'Strong'
WHEN strength > 0.5 THEN 'Moderate'
ELSE 'Weak'
END as seasonality_level,
CASE
WHEN p_value < 0.05 THEN 'Significant'
ELSE 'Not significant'
END as statistical_significance
FROM seasonality_analysis;
Trend Analysis
-- Analyze trend component
SELECT
MIN(date) as period_start,
MAX(date) as period_end,
ROUND(MIN(trend), 2) as trend_min,
ROUND(MAX(trend), 2) as trend_max,
ROUND(MAX(trend) - MIN(trend), 2) as total_trend_change,
ROUND((MAX(trend) - MIN(trend)) / COUNT(*) * 365, 2) as annual_trend
FROM TS_STL_DECOMPOSITION(
'sales_data',
'date',
'sales',
{'seasonal_period': 7}
)
GROUP BY true;
Pattern Detection Examples
Example 1: Weekly Seasonality
-- Detect weekly pattern
SELECT * FROM TS_DETECT_SEASONALITY(
'retail_sales',
'date',
'sales',
{}
);
-- Result: detected_period=7, strength=0.82
-- Action: Use seasonal_period=7 in forecasting
SELECT * FROM TS_FORECAST(
'retail_sales',
'date',
'sales',
'AutoETS',
28,
{'seasonal_period': 7} -- Weekly seasonality
);
Example 2: Multiple Seasonal Periods
-- Website traffic with hourly + daily + weekly patterns
SELECT * FROM TS_DETECT_SEASONALITY_ALL(
'web_traffic',
'timestamp',
'page_views',
{'max_periods': 5}
);
-- Result: [24, 168, 8760]
-- Action: Use TBATS or MSTL with multiple periods
SELECT * FROM TS_FORECAST(
'web_traffic',
'timestamp',
'page_views',
'TBATS',
336, -- 2 weeks
{'seasonal_periods': [24, 168]}
);
Example 3: Trend + Seasonal
-- Decompose to see both components
CREATE TABLE analysis AS
SELECT
date,
sales,
trend,
seasonal,
remainder,
ROUND((trend - LAG(trend) OVER (ORDER BY date)) * 365, 2) as annual_trend_pct
FROM TS_STL_DECOMPOSITION(
'sales_data',
'date',
'sales',
{'seasonal_period': 7}
);
-- View growth rate and seasonal effect
SELECT
ROUND(AVG(annual_trend_pct), 2) as avg_annual_growth,
ROUND(MAX(seasonal) - MIN(seasonal), 2) as seasonal_amplitude
FROM analysis;
Common Pattern Detection Issues
❌ False Positive: Detecting Seasonality That Doesn't Exist
-- WRONG: Accepting low-strength seasonality
WHERE strength < 0.3 AND p_value > 0.05
-- RIGHT: Require both strong AND significant
WHERE strength > 0.5 AND p_value < 0.05
❌ Wrong Seasonal Period
-- WRONG: Detecting daily data as having 30-day seasonality
-- RIGHT: Check detected_period makes sense for your data
-- Daily data likely has 7-day (weekly) or 365-day (yearly)
❌ Missing Multiple Seasonality
-- WRONG: Only detecting one seasonal period
WHERE detected_period = (SELECT MAX(detected_period) FROM ...)
-- RIGHT: Use TS_DETECT_SEASONALITY_ALL to find all
SELECT * FROM TS_DETECT_SEASONALITY_ALL(...)
Pattern Detection Workflow
1. Check for seasonality
↓
2. If seasonal: Detect period(s)
↓
3. Decompose to understand components
↓
4. Check for changepoints
↓
5. Choose model based on patterns found
↓
6. Include detected periods in forecasting
SQL Pattern Detection Scripts
Quick Pattern Check
-- One query to understand your data
SELECT
'Total records' as metric, COUNT(*)::VARCHAR as value
FROM sales_data
UNION ALL
SELECT 'Date range', MIN(date)::VARCHAR || ' to ' || MAX(date)::VARCHAR
FROM sales_data
UNION ALL
SELECT 'Is seasonal?',
CASE WHEN (SELECT is_seasonal FROM TS_DETECT_SEASONALITY(...)) THEN 'Yes' ELSE 'No' END
FROM (SELECT 1)
UNION ALL
SELECT 'Seasonal period',
(SELECT detected_period::VARCHAR FROM TS_DETECT_SEASONALITY(...))
FROM (SELECT 1)
UNION ALL
SELECT 'Seasonality strength',
(SELECT ROUND(strength, 2)::VARCHAR FROM TS_DETECT_SEASONALITY(...))
FROM (SELECT 1)
UNION ALL
SELECT 'Trend direction',
CASE WHEN (SELECT MAX(trend) FROM TS_STL_DECOMPOSITION(...)) > (SELECT MIN(trend) FROM TS_STL_DECOMPOSITION(...))
THEN 'Upward' ELSE 'Downward' END
FROM (SELECT 1);
Next Steps
- Choosing Models — Pick best model based on patterns
- Basic Workflow — Apply pattern detection to complete workflow
- Model Comparison — Compare models that match your patterns
Key Takeaways
- ✅ Detect seasonality automatically with
TS_DETECT_SEASONALITY - ✅ Decompose series to understand trend + seasonal + noise
- ✅ Look for multiple seasonal periods in complex data
- ✅ Detect changepoints to identify structural breaks
- ✅ Require p_value < 0.05 for statistical significance
- ✅ Use detected patterns to inform model selection
- ✅ Match seasonal_period to detected period in forecasting