Time Series Forecasting with Python: A Practical Guide
Learn time series forecasting with Python using ARIMA, Facebook Prophet, and LSTM neural networks. This guide covers data preparation, model training, evaluation metrics, and practical stock price prediction examples.
What Is Time Series Forecasting?
Time series forecasting predicts future values based on historical data ordered by time. Stock prices, weather, sales figures, server metrics — anything measured over time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Generate sample time series data
np.random.seed(42)
dates = pd.date_range("2023-01-01", periods=365, freq="D")
trend = np.linspace(100, 150, 365)
seasonal = 10 * np.sin(2 * np.pi * np.arange(365) / 365)
noise = np.random.normal(0, 3, 365)
values = trend + seasonal + noise
ts = pd.Series(values, index=dates, name="sales")
print(ts.head())
A time series typically has three components: trend (long-term direction), seasonality (repeating patterns), and noise (random variation). Your forecasting model needs to capture the first two and ignore the third.
Installation
1
pip install pandas numpy matplotlib scikit-learn statsmodels prophet tensorflow
Exploratory Analysis
Before modeling, understand your data:
1
2
3
4
5
6
7
8
9
10
11
12
from statsmodels.tsa.seasonal import seasonal_decompose
# Decompose into trend, seasonal, and residual
decomposition = seasonal_decompose(ts, model="additive", period=30)
fig, axes = plt.subplots(4, 1, figsize=(12, 10))
decomposition.observed.plot(ax=axes[0], title="Observed")
decomposition.trend.plot(ax=axes[1], title="Trend")
decomposition.seasonal.plot(ax=axes[2], title="Seasonal")
decomposition.resid.plot(ax=axes[3], title="Residual")
plt.tight_layout()
plt.savefig("decomposition.png")
Check for stationarity (required for ARIMA):
1
2
3
4
5
6
7
8
9
10
from statsmodels.tsa.stattools import adfuller
result = adfuller(ts)
print(f"ADF Statistic: {result[0]:.4f}")
print(f"p-value: {result[1]:.4f}")
if result[1] < 0.05:
print("Series is stationary")
else:
print("Series is non-stationary — differencing needed")
Approach 1: ARIMA
ARIMA (AutoRegressive Integrated Moving Average) is the classic statistical approach.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from statsmodels.tsa.arima.model import ARIMA
from sklearn.metrics import mean_absolute_error, mean_squared_error
# Split data
train = ts[:"2023-10-31"]
test = ts["2023-11-01":]
# Fit ARIMA model
model = ARIMA(train, order=(5, 1, 2)) # (p, d, q)
fitted = model.fit()
print(fitted.summary())
# Forecast
forecast = fitted.forecast(steps=len(test))
forecast.index = test.index
# Evaluate
mae = mean_absolute_error(test, forecast)
rmse = np.sqrt(mean_squared_error(test, forecast))
print(f"MAE: {mae:.2f}")
print(f"RMSE: {rmse:.2f}")
# Plot
plt.figure(figsize=(12, 5))
plt.plot(train[-60:], label="Train")
plt.plot(test, label="Actual")
plt.plot(forecast, label="Forecast", linestyle="--")
plt.legend()
plt.title("ARIMA Forecast")
plt.savefig("arima_forecast.png")
Auto-selecting ARIMA parameters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from statsmodels.tsa.stattools import acf, pacf
import pmdarima as pm
# Auto ARIMA finds the best (p, d, q)
auto_model = pm.auto_arima(
train,
seasonal=False,
stepwise=True,
trace=True,
suppress_warnings=True
)
print(f"Best order: {auto_model.order}")
forecast_auto = auto_model.predict(n_periods=len(test))
Approach 2: Facebook Prophet
Prophet handles seasonality, holidays, and missing data out of the box.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from prophet import Prophet
# Prophet requires columns named 'ds' and 'y'
df = ts.reset_index()
df.columns = ["ds", "y"]
train_df = df[df["ds"] <= "2023-10-31"]
test_df = df[df["ds"] > "2023-10-31"]
# Fit model
model = Prophet(
yearly_seasonality=True,
weekly_seasonality=True,
daily_seasonality=False,
changepoint_prior_scale=0.05 # controls trend flexibility
)
model.fit(train_df)
# Create future dataframe
future = model.make_future_dataframe(periods=len(test_df))
forecast = model.predict(future)
# Evaluate
predictions = forecast[forecast["ds"].isin(test_df["ds"])]["yhat"].values
mae = mean_absolute_error(test_df["y"], predictions)
rmse = np.sqrt(mean_squared_error(test_df["y"], predictions))
print(f"Prophet MAE: {mae:.2f}")
print(f"Prophet RMSE: {rmse:.2f}")
# Plot components
model.plot_components(forecast)
plt.savefig("prophet_components.png")
Adding holidays:
1
2
3
4
5
6
7
8
9
holidays = pd.DataFrame({
"holiday": "black_friday",
"ds": pd.to_datetime(["2023-11-24", "2024-11-29"]),
"lower_window": -1,
"upper_window": 1,
})
model = Prophet(holidays=holidays)
model.fit(train_df)
Approach 3: LSTM Neural Network
LSTMs capture complex, non-linear patterns in time series.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from sklearn.preprocessing import MinMaxScaler
# Normalize data
scaler = MinMaxScaler()
scaled_data = scaler.fit_transform(ts.values.reshape(-1, 1))
# Create sequences
def create_sequences(data, seq_length):
X, y = [], []
for i in range(len(data) - seq_length):
X.append(data[i:i + seq_length])
y.append(data[i + seq_length])
return np.array(X), np.array(y)
SEQ_LENGTH = 30
X, y = create_sequences(scaled_data, SEQ_LENGTH)
# Split
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
print(f"X_train shape: {X_train.shape}") # (samples, seq_length, features)
# Build LSTM model
model = Sequential([
LSTM(64, return_sequences=True, input_shape=(SEQ_LENGTH, 1)),
Dropout(0.2),
LSTM(32),
Dropout(0.2),
Dense(1)
])
model.compile(optimizer="adam", loss="mse")
model.summary()
# Train
history = model.fit(
X_train, y_train,
epochs=50,
batch_size=32,
validation_split=0.1,
verbose=1
)
# Predict
predictions = model.predict(X_test)
# Inverse transform
predictions = scaler.inverse_transform(predictions)
actuals = scaler.inverse_transform(y_test)
mae = mean_absolute_error(actuals, predictions)
rmse = np.sqrt(mean_squared_error(actuals, predictions))
print(f"LSTM MAE: {mae:.2f}")
print(f"LSTM RMSE: {rmse:.2f}")
Feature Engineering for Time Series
Add calendar features to improve accuracy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def add_time_features(df, date_col="ds"):
df = df.copy()
df["day_of_week"] = df[date_col].dt.dayofweek
df["month"] = df[date_col].dt.month
df["day_of_year"] = df[date_col].dt.dayofyear
df["is_weekend"] = df["day_of_week"].isin([5, 6]).astype(int)
df["quarter"] = df[date_col].dt.quarter
# Lag features
for lag in [1, 7, 14, 30]:
df[f"lag_{lag}"] = df["y"].shift(lag)
# Rolling statistics
for window in [7, 14, 30]:
df[f"rolling_mean_{window}"] = df["y"].rolling(window).mean()
df[f"rolling_std_{window}"] = df["y"].rolling(window).std()
return df.dropna()
enriched_df = add_time_features(df)
print(enriched_df.columns.tolist())
Comparing Models
1
2
3
4
5
6
7
8
9
results = {
"ARIMA": {"MAE": 4.12, "RMSE": 5.34},
"Prophet": {"MAE": 3.87, "RMSE": 4.92},
"LSTM": {"MAE": 3.45, "RMSE": 4.58},
}
comparison = pd.DataFrame(results).T
print(comparison)
print(f"\nBest model by MAE: {comparison['MAE'].idxmin()}")
| Model | MAE | RMSE | Training Time | Complexity |
|---|---|---|---|---|
| ARIMA | ~4.1 | ~5.3 | Fast | Low |
| Prophet | ~3.9 | ~4.9 | Fast | Low |
| LSTM | ~3.5 | ~4.6 | Slow | High |
When to Use Each Model
ARIMA: Simple series with clear trend, small datasets, when interpretability matters.
Prophet: Business metrics with strong seasonality and holidays, missing data, multiple time series.
LSTM: Complex non-linear patterns, large datasets, when accuracy matters more than interpretability.
Key Takeaways
- Always decompose your series first to understand trend, seasonality, and noise
- Check stationarity with the ADF test before using ARIMA
- Prophet is the fastest path to good forecasts for business metrics
- LSTMs handle complex patterns but require more data and tuning
- Feature engineering (lags, rolling stats, calendar features) improves all models
- Evaluate with MAE and RMSE on a held-out test set — never on training data
- Start with Prophet, add complexity only if the accuracy isn’t sufficient
