Seasonal Analysis¶
Many real-world functional datasets exhibit periodic patterns -- daily temperature cycles, weekly traffic flows, annual growth curves. The seasonal analysis module provides tools for detecting, decomposing, and measuring periodicity in functional data.
Period detection¶
fdars offers three period-detection algorithms, each with different strengths:
SAZED¶
SAZED (Seasonal And Zero-crossing Estimation of Periodicity via Distance) combines multiple period estimates from different signal features (zero crossings, peaks, autocorrelation) and returns a consensus period.
import numpy as np
from fdars import Fdata
from fdars.seasonal import sazed
argvals = np.linspace(0, 10, 500)
# Create data with a known period
fd = Fdata(
np.sin(2 * np.pi * argvals / 2.5)[None, :] + np.random.default_rng(1).normal(0, 0.1, (10, 500)),
argvals=argvals,
)
result = sazed(fd.data, fd.argvals, tolerance=0.05)
print(f"Detected period: {result['period']:.3f}")
print(f"Confidence: {result['confidence']:.3f}")
print(f"Agreeing comps: {result['agreeing_components']}")
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
ndarray (n, m) |
-- | Functional observations |
argvals |
ndarray (m,) |
-- | Evaluation points |
tolerance |
float |
0.05 |
Relative tolerance for period matching |
Returns a dictionary:
| Key | Type | Description |
|---|---|---|
period |
float |
Estimated period |
confidence |
float |
Confidence score (fraction of agreeing components) |
agreeing_components |
int |
Number of estimation methods that agree |
Autoperiod¶
Uses FFT peak detection followed by autocorrelation validation. Best for clean, well-defined periodic signals.
from fdars.seasonal import autoperiod
result_ap = autoperiod(fd.data, fd.argvals, n_candidates=5, gradient_steps=10)
print(f"Period: {result_ap['period']:.3f}")
print(f"FFT power: {result_ap['fft_power']:.3f}")
print(f"ACF validation: {result_ap['acf_validation']:.3f}")
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
ndarray (n, m) |
-- | Functional observations |
argvals |
ndarray (m,) |
-- | Evaluation points |
n_candidates |
int |
5 |
Maximum number of FFT peaks to consider |
gradient_steps |
int |
10 |
Gradient ascent refinement steps |
Returns a dictionary:
| Key | Type | Description |
|---|---|---|
period |
float |
Estimated period |
confidence |
float |
Confidence score |
fft_power |
float |
Spectral power at the detected frequency |
acf_validation |
float |
Autocorrelation validation score |
CFD Autoperiod¶
A cluster-based variant of autoperiod that can detect multiple periodicities simultaneously.
from fdars.seasonal import cfd_autoperiod
result_cfd = cfd_autoperiod(fd.data, fd.argvals, cluster_tolerance=0.1, min_cluster_size=1)
print(f"Primary period: {result_cfd['period']:.3f}")
print(f"All periods: {result_cfd['periods']}")
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
ndarray (n, m) |
-- | Functional observations |
argvals |
ndarray (m,) |
-- | Evaluation points |
cluster_tolerance |
float |
0.1 |
Tolerance for clustering candidate periods |
min_cluster_size |
int |
1 |
Minimum cluster size to keep |
Returns a dictionary:
| Key | Type | Description |
|---|---|---|
period |
float |
Primary (strongest) period |
confidence |
float |
Confidence for the primary period |
periods |
ndarray |
All detected periods |
confidences |
ndarray |
Confidence for each detected period |
Peak detection¶
Locate peaks in each functional observation, optionally smoothing the data first. The function also estimates the mean period from inter-peak distances.
from fdars.seasonal import detect_peaks
peaks = detect_peaks(
fd.data, fd.argvals,
min_distance=0.5,
min_prominence=0.1,
smooth_first=True,
smooth_nbasis=20,
)
print(f"Mean period from peaks: {peaks['mean_period']:.3f}")
# Peaks for the first observation: list of (time, value, prominence) tuples
for t, v, p in peaks["peaks"][0]:
print(f" t={t:.2f} value={v:.3f} prominence={p:.3f}")
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
ndarray (n, m) |
-- | Functional observations |
argvals |
ndarray (m,) |
-- | Evaluation grid |
min_distance |
float |
None |
Minimum distance between consecutive peaks |
min_prominence |
float |
None |
Minimum peak prominence |
smooth_first |
bool |
False |
Smooth data before detection |
smooth_nbasis |
int |
None |
Number of basis functions for smoothing |
Returns a dictionary:
| Key | Type | Description |
|---|---|---|
peaks |
list[list[tuple]] |
Per-observation list of (time, value, prominence) tuples |
mean_period |
float |
Mean inter-peak distance across all observations |
STL decomposition¶
Seasonal and Trend decomposition using Loess (STL) splits each functional observation into trend, seasonal, and remainder components.
from fdars.seasonal import stl_decompose
decomp = stl_decompose(fd.data, period=25, robust=False)
# decomp["trend"] shape (n, m)
# decomp["seasonal"] shape (n, m)
# decomp["remainder"] shape (n, m)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
ndarray (n, m) |
-- | Functional observations |
period |
int |
-- | Seasonal period (in grid points) |
s_window |
int |
None |
Seasonal smoothing window (auto if None) |
t_window |
int |
None |
Trend smoothing window (auto if None) |
robust |
bool |
False |
Use robust (re-weighted) fitting |
Returns a dictionary:
| Key | Shape | Description |
|---|---|---|
trend |
(n, m) |
Trend component |
seasonal |
(n, m) |
Seasonal component |
remainder |
(n, m) |
Remainder (residual) |
Seasonal strength¶
Quantify how strongly seasonal a signal is, using either a variance-based or spectral method. The returned value lies in \([0, 1]\), where 0 means no seasonality and 1 means a purely periodic signal.
from fdars.seasonal import seasonal_strength
strength = seasonal_strength(fd.data, fd.argvals, period=2.5, method="variance")
print(f"Seasonal strength (variance): {strength:.3f}")
strength_spec = seasonal_strength(fd.data, fd.argvals, period=2.5, method="spectral")
print(f"Seasonal strength (spectral): {strength_spec:.3f}")
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
ndarray (n, m) |
-- | Functional observations |
argvals |
ndarray (m,) |
-- | Evaluation points |
period |
float |
-- | Estimated period |
method |
str |
"variance" |
"variance" or "spectral" |
Returns a float -- the seasonal strength.
Full example -- detect period, decompose, and measure strength¶
import numpy as np
from fdars import Fdata
from fdars.seasonal import sazed, stl_decompose, seasonal_strength, detect_peaks
# ── 1. Create seasonal data ──────────────────────────────────
rng = np.random.default_rng(42)
argvals = np.linspace(0, 20, 1000)
trend = 0.05 * argvals
seasonal = np.sin(2 * np.pi * argvals / 4.0)
fd = Fdata(
(trend + seasonal)[None, :] + rng.normal(0, 0.15, (15, 1000)),
argvals=argvals,
)
# ── 2. Detect the period ─────────────────────────────────────
detected = sazed(fd.data, fd.argvals)
print(f"Detected period: {detected['period']:.2f} (true = 4.0)")
# ── 3. Decompose ─────────────────────────────────────────────
period_pts = int(round(detected["period"] / (fd.argvals[1] - fd.argvals[0])))
decomp = stl_decompose(fd.data, period=period_pts)
print(f"Trend range: [{decomp['trend'][0].min():.2f}, {decomp['trend'][0].max():.2f}]")
print(f"Seasonal range: [{decomp['seasonal'][0].min():.2f}, {decomp['seasonal'][0].max():.2f}]")
# ── 4. Measure strength ──────────────────────────────────────
s = seasonal_strength(fd.data, fd.argvals, period=detected["period"])
print(f"Seasonal strength: {s:.3f}")
# ── 5. Find peaks ────────────────────────────────────────────
pk = detect_peaks(fd.data, fd.argvals, smooth_first=True, smooth_nbasis=30)
print(f"Mean inter-peak distance: {pk['mean_period']:.2f}")
# ── 6. Visualize (optional) ──────────────────────────────────
try:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(4, 1, figsize=(12, 8), sharex=True)
i = 0 # show first observation
axes[0].plot(fd.argvals, fd.data[i], linewidth=0.7)
axes[0].set_ylabel("Original")
axes[1].plot(fd.argvals, decomp["trend"][i])
axes[1].set_ylabel("Trend")
axes[2].plot(fd.argvals, decomp["seasonal"][i])
axes[2].set_ylabel("Seasonal")
axes[3].plot(fd.argvals, decomp["remainder"][i])
axes[3].set_ylabel("Remainder")
axes[3].set_xlabel("t")
fig.suptitle("STL Decomposition")
plt.tight_layout()
plt.savefig("seasonal_analysis.png", dpi=150)
plt.show()
except ImportError:
pass