🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
NEWSLIVESearch All Content
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Time Series Analysis in PySpark

🟢 Free Lesson

Advertisement

📈 Time Series Analysis in PySpark

Time Series Decomposition Architecture

Sponsored
Time Series Decomposition PipelineRaw SeriesY(t)DecomposeTrend + Season + ResidStationarityADF TestModel FitARIMA/ProphetForecastPredictionsWindow Functions for Temporal AnalyticsMoving AverageExponential WeightedLag FeaturesSeasonal DiffSQL: AVG(val) OVER (ORDER BY ts ROWS BETWEEN 7 PRECEDING AND CURRENT ROW)DataFrame: window(val, orderBy="ts", rowsBetween(-7, 0))All operations are parallelizable across partitions for petabyte-scale time series

Architecture Diagram

Detailed Explanation

What is Time Series Analysis?

Time series analysis involves analyzing time-ordered data points to extract meaningful statistics, identify patterns, and forecast future values. In PySpark, time series processing is parallelized across partitions, enabling analysis of petabyte-scale temporal datasets.

Key Takeaway: PySpark's distributed processing enables time series analysis at scales impossible with single-machine tools like pandas or R, while maintaining the same analytical rigor.


Temporal Windowing Operations

PySpark supports several windowing strategies for temporal aggregation:

Window TypeDescriptionUse Case
TumblingFixed-size, non-overlappingHourly/daily aggregations
HoppingFixed-size, periodic startRolling averages, overlapping aggregations
SessionDynamic, activity-basedUser session analytics
SlidingRow-count or time-basedMoving averages, lag features

Stationarity and Differencing

Most time series models require stationary data (constant mean and variance):

TestNull Hypothesisp-value Threshold
ADF (Augmented Dickey-Fuller)Unit root existsp < 0.05 = stationary
KPSSSeries is stationaryp > 0.05 = stationary
Phillips-PerronUnit root existsp < 0.05 = stationary

Differencing Techniques:

  1. First-order differencing:
  2. Seasonal differencing: where is the seasonal period
  3. Log transformation: Stabilizes variance for exponential growth patterns

Time Series Decomposition

Decomposition separates a time series into components:

ComponentDescriptionDetection Method
TrendLong-term directionMoving average, LOESS
SeasonalPeriodic patternsAutocorrelation, spectral analysis
ResidualUnexplained noiseAfter removing trend and seasonality

Model Selection:

  • Additive: When seasonal variation is constant ()
  • Multiplicative: When seasonal variation increases with level ()

Feature Engineering for Time Series

Feature TypeDescriptionPySpark Function
Lag FeaturesPrevious time step valueslag(col, n)
Rolling StatisticsMoving averages, std devavg(col).over(Window)
Calendar FeaturesDay of week, month, quarterdayofweek(), month()
Exponential WeightedRecency-weighted averagesewm()
Fourier FeaturesSeasonal periodicitysin(2πft), cos(2πft)

Forecasting Methods

MethodComplexityAccuracyParallelizable
ARIMAO(n²)HighLimited
ProphetO(n log n)HighYes
Random ForestO(n log n)Medium-HighYes
Gradient BoostingO(n log n)HighYes
Deep LearningO(n × d)VariableYes

Best Practice: For distributed time series forecasting, prefer Prophet or ML-based approaches over ARIMA, which requires sequential processing.


Seasonal Patterns Detection

Autocorrelation Function (ACF):

  • Measures correlation between a time series and its lagged values
  • Significant spikes at lag indicate seasonality with period

Partial Autocorrelation Function (PACF):

  • Measures direct correlation at each lag, removing intermediate effects
  • Helps determine ARIMA order parameters (p, d, q)

Mathematical Foundations

Sponsored

Definition: Time Series

A time series is a sequence of observations indexed by time, where each represents the value at time .

ARIMA Model

The ARIMA(p, d, q) model combines autoregressive (AR), differencing (I), and moving average (MA) components:

where is the differenced series, are AR parameters, are MA parameters, and is white noise.

Stationarity Condition

A time series is weakly stationary if:

The Augmented Dickey-Fuller test statistic is:

Exponential Weighted Moving Average

EWMA gives exponentially decreasing weight to older observations:

where is the smoothing parameter.

Forecast Accuracy Metrics

MetricFormulaRangeBest For
MAE[0, ∞)Interpretable scale
RMSE[0, ∞)Penalizes large errors
MAPE[0, 100%]Scale-independent
SMAPE[0, 100%]Symmetric errors

Key Insight

Time series decomposition assumes the observed series is a combination of interpretable components. STL (Seasonal and Trend decomposition using Loess) is robust to outliers and handles any seasonal period, making it preferred over classical decomposition for production forecasting.

Summary

Time series analysis in PySpark combines distributed windowing operations for feature engineering with statistical models for forecasting. Stationarity testing (ADF/KPSS) determines differencing requirements. Decomposition separates trend, seasonality, and residuals for interpretable analysis. Forecast accuracy metrics (MAE, RMSE, MAPE) guide model selection.

Key Concepts Table

ConceptDescriptionPerformance Impact
Tumbling WindowNon-overlapping fixed-size intervalsO(n) processing per partition
Hopping WindowPeriodic start with fixed sizeO(n × window_size/step)
Lag FeaturesPrevious time step valuesO(n) with Window function
Rolling AggregationMoving statistics over windowO(n × window_size)
ADF TestAugmented Dickey-Fuller stationarity testO(n) with O(p) parameters
Seasonal DecompositionSplit into T+S+E componentsO(n) with STL
AutocorrelationCorrelation at different lagsO(n × max_lag)
ProphetAdditive model with trend + seasonalityO(n log n) training
Calendar FeaturesDay/month/quarter encodingO(n) computation
Fourier FeaturesSinusoidal seasonality encodingO(n × k) for k harmonics

Code Examples

Temporal Windowing and Feature Engineering

# Save as script.py, then run:
spark-submit \
  --master yarn \
  --deploy-mode client \
  --driver-memory 4g \
  --executor-memory 8g \
  --executor-cores 4 \
  script.py
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.window import Window
from pyspark.sql.types import *

spark = SparkSession.builder \
    .appName("TimeSeriesAnalysis") \
    .getOrCreate()

# Create sample time series data (sensor readings)
import random
random.seed(42)

timestamps = [f"2024-01-{d:02d} {h:02d}:00:00" 
              for d in range(1, 32) for h in range(24)]
n = len(timestamps)

data = [
    (timestamps[i], 
     20 + 5 * (i / 24) + 3 * ((i % 24) - 12) / 12 + random.gauss(0, 1),
     50 + 10 * (i / 24) + random.gauss(0, 2))
    for i in range(n)
]

df = spark.createDataFrame(data, ["timestamp", "temperature", "humidity"])
df = df.withColumn("ts", to_timestamp("timestamp"))

# Define windows for various temporal aggregations
# 24-hour moving average
window_24h = Window.orderBy("ts").rowsBetween(-23, 0)

# 7-day rolling statistics
window_7d = Window.orderBy("ts").rowsBetween(-167, 0)

# Add temporal features
ts_features = df \
    .withColumn("temp_24h_avg", avg("temperature").over(window_24h)) \
    .withColumn("temp_24h_std", stddev("temperature").over(window_24h)) \
    .withColumn("humidity_7d_avg", avg("humidity").over(window_7d)) \
    .withColumn("temp_lag_1h", lag("temperature", 1).over(Window.orderBy("ts"))) \
    .withColumn("temp_lag_24h", lag("temperature", 24).over(Window.orderBy("ts"))) \
    .withColumn("temp_diff_1h", col("temperature") - lag("temperature", 1).over(Window.orderBy("ts"))) \
    .withColumn("temp_ewm", avg("temperature").over(
        Window.orderBy("ts").rowsBetween(-23, 0))) \
    .withColumn("hour_of_day", hour("ts")) \
    .withColumn("day_of_week", dayofweek("ts")) \
    .withColumn("day_of_month", dayofmonth("ts")) \
    .withColumn("month", month("ts"))

ts_features.show(50, truncate=False)

Seasonal Decomposition and Differencing

# Save as script.py, then run:
spark-submit \
  --master yarn \
  --deploy-mode client \
  --driver-memory 4g \
  --executor-memory 8g \
  --executor-cores 4 \
  script.py
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.window import Window

spark = SparkSession.builder \
    .appName("SeasonalDecomposition") \
    .getOrCreate()

# Create daily sales data with trend and seasonality
import math
import random

random.seed(42)
dates = [f"2024-{m:02d}-{d:02d}" 
         for m in range(1, 13) for d in range(1, 29)]

data = []
for i, date in enumerate(dates):
    trend = 100 + 0.5 * i
    seasonality = 20 * math.sin(2 * math.pi * i / 7)  # Weekly pattern
    monthly_seasonality = 15 * math.sin(2 * math.pi * i / 30)  # Monthly pattern
    noise = random.gauss(0, 5)
    value = trend + seasonality + monthly_seasonality + noise
    data.append((date, value))

df = spark.createDataFrame(data, ["date_str", "value"])
df = df.withColumn("date", to_date("date_str"))

# Decomposition using moving averages
# Step 1: Trend (7-day moving average)
window_7 = Window.orderBy("date").rowsBetween(-3, 3)
df_with_trend = df.withColumn("trend", avg("value").over(window_7))

# Step 2: Deseasonalize (value - trend)
df_deseasonalized = df_with_trend.withColumn(
    "deseasonalized", col("value") - col("trend"))

# Step 3: Seasonal component (average of deseasonalized by day of week)
seasonal_pattern = df_deseasonalized \
    .withColumn("dow", dayofweek("date")) \
    .groupBy("dow") \
    .agg(avg("deseasonalized").alias("seasonal_factor"))

# Step 4: Join seasonal pattern back
df_seasonal = df_deseasonalized \
    .withColumn("dow", dayofweek("date")) \
    .join(seasonal_pattern, "dow") \
    .withColumn("residual", col("deseasonalized") - col("seasonal_factor"))

print("=== Decomposition Results ===")
df_seasonal.select("date", "value", "trend", "seasonal_factor", "residual") \
    .orderBy("date").show(20, truncate=False)

# Seasonal differencing
window_seasonal = Window.orderBy("date")
df_seasonal_diff = df.withColumn(
    "seasonal_diff", 
    col("value") - lag("value", 7).over(window_seasonal)
)

print("=== Seasonal Differencing (period=7) ===")
df_seasonal_diff.show(20, truncate=False)

ARIMA Model Fitting

# Save as script.py, then run:
spark-submit \
  --master yarn \
  --deploy-mode client \
  --driver-memory 4g \
  --executor-memory 8g \
  --executor-cores 4 \
  script.py
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.types import *
import numpy as np

spark = SparkSession.builder \
    .appName("ARIMA_Forecast") \
    .getOrCreate()

# Create monthly time series with trend and seasonality
import math
import random

random.seed(42)
months = 36
data = []
for i in range(months):
    trend = 100 + 2 * i
    season = 10 * math.sin(2 * math.pi * i / 12)
    noise = random.gauss(0, 3)
    data.append((f"2024-{(i%12)+1:02d}-01", trend + season + noise))

df = spark.createDataFrame(data, ["date_str", "value"])
df = df.withColumn("date", to_date("date_str"))

# Collect for local ARIMA fitting (ARIMA requires sequential processing)
values = [row.value for row in df.collect()]

# Simple AR(1) model estimation using least squares
def fit_ar1(series):
    """Fit AR(1) model: Y_t = c + phi * Y_{t-1} + epsilon"""
    n = len(series)
    y = np.array(series[1:])
    x = np.array(series[:-1])
    
    # Least squares estimation
    x_mean = np.mean(x)
    y_mean = np.mean(y)
    phi = np.sum((x - x_mean) * (y - y_mean)) / np.sum((x - x_mean) ** 2)
    c = y_mean - phi * x_mean
    
    # Residuals and forecast
    residuals = y - (c + phi * x)
    sigma = np.std(residuals)
    
    return c, phi, sigma, residuals

c, phi, sigma, residuals = fit_ar1(values)

print(f"AR(1) Model Parameters:")
print(f"  Intercept (c): {c:.4f}")
print(f"  AR coefficient (phi): {phi:.4f}")
print(f"  Residual std: {sigma:.4f}")

# Forecast next 6 months
forecast_values = []
last_value = values[-1]
for i in range(6):
    forecast = c + phi * last_value
    forecast_values.append(forecast)
    last_value = forecast

# Create forecast DataFrame
forecast_dates = [f"2025-{m:02d}-01" for m in range(1, 7)]
forecast_df = spark.createDataFrame(
    list(zip(forecast_dates, forecast_values)),
    ["date_str", "forecast_value"]
).withColumn("date", to_date("date_str"))

print("\n=== 6-Month Forecast ===")
forecast_df.show(truncate=False)

Prophet-like Distributed Forecasting

# Save as script.py, then run:
spark-submit \
  --master yarn \
  --deploy-mode client \
  --driver-memory 4g \
  --executor-memory 8g \
  --executor-cores 4 \
  script.py
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.window import Window
import numpy as np

spark = SparkSession.builder \
    .appName("ProphetStyleForecast") \
    .getOrCreate()

# Simulate Prophet's additive model: Y = T + S + E
# with distributed computation of components

# Create daily sales data
import math
import random

random.seed(42)
dates = [f"2024-{m:02d}-{d:02d}" 
         for m in range(1, 13) for d in range(1, 29)]

data = []
for i, date in enumerate(dates):
    # Trend: linear growth with change points
    if i < 100:
        trend = 100 + 0.5 * i
    else:
        trend = 150 + 0.8 * (i - 100)  # Change point at i=100
    
    # Weekly seasonality
    weekly = 15 * math.sin(2 * math.pi * i / 7)
    
    # Annual seasonality
    annual = 25 * math.sin(2 * math.pi * i / 365)
    
    noise = random.gauss(0, 5)
    value = trend + weekly + annual + noise
    data.append((date, value))

df = spark.createDataFrame(data, ["date_str", "y"])
df = df.withColumn("ds", to_date("date_str"))

# Step 1: Trend estimation using piecewise linear fit
# In production, this uses gradient descent optimization
window_trend = Window.orderBy("ds").rowsBetween(-14, 14)
df_trend = df.withColumn("trend", avg("y").over(window_trend))

# Step 2: Seasonal decomposition
# Weekly seasonality (period = 7)
df_with_dow = df_trend.withColumn("dow", dayofweek("ds"))

weekly_pattern = df_with_dow \
    .withColumn("detrended", col("y") - col("trend")) \
    .groupBy("dow") \
    .agg(avg("detrended").alias("weekly_effect"))

# Monthly seasonality (period = 30)
df_with_dom = df_trend.withColumn("dom", dayofmonth("ds"))

monthly_pattern = df_with_dom \
    .withColumn("detrended", col("y") - col("trend")) \
    .groupBy("dom") \
    .agg(avg("detrended").alias("monthly_effect"))

# Step 3: Join patterns and compute residuals
df_components = df_trend \
    .withColumn("dow", dayofweek("ds")) \
    .withColumn("dom", dayofmonth("ds")) \
    .join(weekly_pattern, "dow") \
    .join(monthly_pattern, "dom") \
    .withColumn("seasonal", col("weekly_effect") + col("monthly_effect")) \
    .withColumn("residual", col("y") - col("trend") - col("seasonal"))

print("=== Prophet-Style Decomposition ===")
df_components.select("ds", "y", "trend", "weekly_effect", "monthly_effect", "residual") \
    .show(30, truncate=False)

# Step 4: Residual-based uncertainty estimation
residual_stats = df_components.agg(
    stddev("residual").alias("residual_std"),
    avg("residual").alias("residual_mean")
).collect()[0]

print(f"\nResidual Statistics:")
print(f"  Mean: {residual_stats['residual_mean']:.4f}")
print(f"  Std: {residual_stats['residual_std']:.4f}")

# Step 5: Forecast with uncertainty intervals
forecast_days = 30
last_date = df.select(max("ds")).collect()[0][0]

future_dates = [f"2025-01-{d:02d}" for d in range(1, forecast_days + 1)]
future_df = spark.createDataFrame(
    [(d,) for d in future_dates], ["date_str"]
).withColumn("ds", to_date("date_str"))

# Simple linear trend extrapolation
trend_slope = 0.5  # From trend estimation
last_trend = df_components.orderBy(col("ds").desc()).select("trend").first()[0]

future_trend = future_df.withColumn(
    "trend", 
    lit(last_trend) + lit(trend_slope) * (datediff(col("ds"), lit(last_date))))
).withColumn("dow", dayofweek("ds")) \
 .join(weekly_pattern, "dow") \
 .withColumn("forecast", col("trend") + col("weekly_effect"))

# Add uncertainty intervals (95% CI = ±1.96σ)
z_score = 1.96
future_with_ci = future_trend \
    .withColumn("lower_ci", col("forecast") - lit(z_score * residual_stats['residual_std'])) \
    .withColumn("upper_ci", col("forecast") + lit(z_score * residual_stats['residual_std']))

print("\n=== 30-Day Forecast with 95% CI ===")
future_with_ci.select("ds", "forecast", "lower_ci", "upper_ci") \
    .orderBy("ds").show(30, truncate=False)

Anomaly Detection in Time Series

# Save as script.py, then run:
spark-submit \
  --master yarn \
  --deploy-mode client \
  --driver-memory 4g \
  --executor-memory 8g \
  --executor-cores 4 \
  script.py
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.window import Window

spark = SparkSession.builder \
    .appName("TimeSeriesAnomalyDetection") \
    .getOrCreate()

# Create time series with anomalies
import random
import math

random.seed(42)
n_points = 1000
data = []
for i in range(n_points):
    value = 50 + 10 * math.sin(2 * math.pi * i / 100) + random.gauss(0, 2)
    # Inject anomalies
    if i in [150, 300, 500, 750]:
        value += 30  # Point anomaly
    if 400 <= i <= 420:
        value += 15  # Contextual anomaly
    data.append((f"2024-01-01 {i//24:02d}:{i%24:02d}:00", value))

df = spark.createDataFrame(data, ["timestamp_str", "value"])
df = df.withColumn("ts", to_timestamp("timestamp_str"))

# Method 1: Z-Score based anomaly detection
window_stats = Window.orderBy("ts").rowsBetween(-50, 50)

df_zscore = df \
    .withColumn("rolling_mean", avg("value").over(window_stats)) \
    .withColumn("rolling_std", stddev("value").over(window_stats)) \
    .withColumn("z_score", 
        (col("value") - col("rolling_mean")) / col("rolling_std")) \
    .withColumn("is_anomaly_zscore", 
        when(abs(col("z_score")) > 3, True).otherwise(False))

# Method 2: IQR-based anomaly detection
window_iqr = Window.orderBy("ts").rowsBetween(-50, 50)

df_iqr = df \
    .withColumn("q1", expr("percentile_approx(value, 0.25)").over(window_iqr)) \
    .withColumn("q3", expr("percentile_approx(value, 0.75)").over(window_iqr)) \
    .withColumn("iqr", col("q3") - col("q1")) \
    .withColumn("lower_bound", col("q1") - 1.5 * col("iqr")) \
    .withColumn("upper_bound", col("q3") + 1.5 * col("iqr")) \
    .withColumn("is_anomaly_iqr",
        when((col("value") < col("lower_bound")) | 
             (col("value") > col("upper_bound")), True).otherwise(False))

# Method 3: Moving average deviation
window_ma = Window.orderBy("ts").rowsBetween(-24, 0)

df_ma = df \
    .withColumn("ma_24h", avg("value").over(window_ma)) \
    .withColumn("deviation", abs(col("value") - col("ma_24h"))) \
    .withColumn("dev_std", stddev(col("deviation")).over(window_ma)) \
    .withColumn("is_anomaly_ma",
        when(col("deviation") > 3 * col("dev_std"), True).otherwise(False))

# Combine methods (ensemble approach)
anomalies = df_zscore.select("ts", "value", "is_anomaly_zscore") \
    .join(df_iqr.select("ts", "is_anomaly_iqr"), "ts") \
    .join(df_ma.select("ts", "is_anomaly_ma"), "ts") \
    .withColumn("anomaly_score",
        (col("is_anomaly_zscore").cast("int") + 
         col("is_anomaly_iqr").cast("int") + 
         col("is_anomaly_ma").cast("int"))) \
    .withColumn("is_anomaly",
        when(col("anomaly_score") >= 2, True).otherwise(False))

print("=== Detected Anomalies ===")
anomalies.filter(col("is_anomaly") == True) \
    .select("ts", "value", "anomaly_score") \
    .orderBy("ts").show(20, truncate=False)

# Summary statistics
total_anomalies = anomalies.filter(col("is_anomaly") == True).count()
print(f"\nTotal anomalies detected: {total_anomalies}")
print(f"Anomaly rate: {total_anomalies / n_points * 100:.2f}%")

Performance Metrics

Operation1K Points100K Points10M Points100M Points
Moving Average (window=24)< 1 sec1-3 sec10-20 sec2-5 min
Lag Feature Generation< 1 sec1-2 sec5-15 sec1-3 min
Seasonal Decomposition< 1 sec2-5 sec30-60 sec5-10 min
ADF Stationarity Test< 1 sec< 1 sec1-3 sec5-10 sec
Autocorrelation (100 lags)< 1 sec1-2 sec10-20 sec2-5 min
Prophet-style Decomposition1-3 sec10-20 sec2-5 min30-60 min
ARIMA Fitting (local)1-5 sec5-15 sec30-60 sec5-10 min
Anomaly Detection (ensemble)< 1 sec2-5 sec20-40 sec3-8 min
Calendar Feature Extraction< 1 sec< 1 sec1-3 sec5-10 sec
Fourier Feature Generation< 1 sec1-2 sec5-10 sec1-2 min

Best Practices

Advertisement
  1. Always sort by timestamp before applying window functions to ensure correct temporal ordering across partitions
  2. Use repartition(num_partitions, timestamp_column) to co-locate temporal data and reduce shuffle during window operations
  3. Pre-filter time ranges before feature engineering to avoid computing lag/rolling features on unnecessary data
  4. Handle missing timestamps explicitly with fillna() or interpolation—gaps in time series break window calculations
  5. Use pandas_udf for complex time series models (ARIMA, Prophet) that cannot be vectorized across partitions
  6. Partition by time period (day, week, month) to enable partition pruning for time-range queries
  7. Cache intermediate DataFrames when performing multiple temporal analyses on the same dataset
  8. Avoid collecting large time series to the driver—use distributed algorithms for fitting models
  9. Use approximate functions for real-time anomaly detection when exact quantiles are not required
  10. Implement watermarking for streaming time series to handle late-arriving data gracefully
  11. Validate temporal ordering with assert df.filter(col("ts") < lag("ts")).count() == 0 before analysis
  12. Use calendar table joins for complex date logic instead of chaining multiple date functions

See also: ML Pipeline Integration (24), ML Feature Engineering (32), Advanced Aggregations (31), Geospatial Data (27)

See Also

—
☆☆☆☆☆
0 ratings

Rate & Feedback

Need Expert PySpark Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement