How to Implement Simple Exponential Smoothing in Python
Simple Exponential Smoothing (SES) is a time series forecasting technique that generates predictions by calculating weighted averages of past observations, where recent data points receive...
Key Insights
- Simple Exponential Smoothing (SES) excels at short-term forecasting for stable time series without trend or seasonality, using a single smoothing parameter (alpha) that balances responsiveness versus noise reduction.
- The alpha parameter determines forecast behavior: values near 1.0 make forecasts reactive to recent changes, while values near 0.0 create smoother predictions that change slowly over time.
- While SES is computationally cheap and interpretable, it fails on trending or seasonal data—use Double Exponential Smoothing for trends or seasonal methods like Holt-Winters for more complex patterns.
Introduction to Exponential Smoothing
Simple Exponential Smoothing (SES) is a time series forecasting technique that generates predictions by calculating weighted averages of past observations, where recent data points receive exponentially higher weights than older ones. Unlike simple moving averages that treat all observations in the window equally, SES recognizes that yesterday’s data is typically more relevant than data from last month.
SES works best for time series data that fluctuates around a stable level without systematic trends or seasonal patterns. Think inventory levels that oscillate around a mean, daily website traffic for an established product, or temperature readings in a climate-controlled environment. When your data shows consistent upward/downward movement or repeating seasonal cycles, you’ll need more sophisticated methods.
The key advantage of SES is its simplicity: it requires storing only the previous forecast and uses a single parameter to control smoothing intensity. This makes it computationally efficient and easy to interpret—critical factors when you’re implementing forecasting for dozens or hundreds of time series.
The Mathematics Behind Simple Exponential Smoothing
The SES formula is deceptively simple:
S_t = α × Y_t + (1 - α) × S_(t-1)
Where:
S_tis the smoothed value (forecast) at time tY_tis the actual observation at time tS_(t-1)is the previous smoothed valueα(alpha) is the smoothing parameter, ranging from 0 to 1
The alpha parameter controls how much weight you give to recent observations. An alpha of 0.8 means your forecast is 80% based on the latest actual value and only 20% on the previous forecast. An alpha of 0.2 flips this, creating much smoother forecasts that change gradually.
Here’s how different alpha values behave:
import numpy as np
import matplotlib.pyplot as plt
# Generate sample data with noise
np.random.seed(42)
time = np.arange(50)
actual = 100 + np.random.normal(0, 10, 50)
def simple_exp_smoothing(data, alpha):
result = np.zeros(len(data))
result[0] = data[0] # Initialize with first observation
for t in range(1, len(data)):
result[t] = alpha * data[t] + (1 - alpha) * result[t-1]
return result
# Compare different alpha values
alphas = [0.1, 0.5, 0.9]
plt.figure(figsize=(12, 6))
plt.plot(time, actual, 'ko-', label='Actual Data', alpha=0.5)
for alpha in alphas:
smoothed = simple_exp_smoothing(actual, alpha)
plt.plot(time, smoothed, label=f'α = {alpha}')
plt.xlabel('Time')
plt.ylabel('Value')
plt.title('Effect of Alpha on Exponential Smoothing')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Choosing alpha typically involves either optimization (minimizing forecast error on historical data) or domain knowledge. For stable processes, use lower alpha values (0.1-0.3). For rapidly changing environments where you need to detect shifts quickly, use higher values (0.6-0.9).
Manual Implementation from Scratch
Building SES from scratch helps solidify understanding. Here’s a complete implementation with intermediate value tracking:
import numpy as np
def exponential_smoothing_detailed(data, alpha):
"""
Implement Simple Exponential Smoothing with detailed output.
Parameters:
-----------
data : array-like
Time series data
alpha : float
Smoothing parameter (0 < alpha <= 1)
Returns:
--------
smoothed : numpy array
Smoothed values
"""
if not 0 < alpha <= 1:
raise ValueError("Alpha must be between 0 and 1")
data = np.array(data)
smoothed = np.zeros(len(data))
smoothed[0] = data[0] # Initialize with first observation
print(f"Alpha: {alpha}")
print(f"Initial value S_0: {smoothed[0]:.2f}\n")
for t in range(1, len(data)):
# Calculate components
recent_component = alpha * data[t]
historical_component = (1 - alpha) * smoothed[t-1]
smoothed[t] = recent_component + historical_component
# Show calculation for first few steps
if t <= 3:
print(f"Step {t}:")
print(f" Y_{t} = {data[t]:.2f}")
print(f" S_{t} = {alpha} × {data[t]:.2f} + {1-alpha} × {smoothed[t-1]:.2f}")
print(f" S_{t} = {recent_component:.2f} + {historical_component:.2f} = {smoothed[t]:.2f}\n")
return smoothed
# Example usage
sample_data = [112, 118, 132, 129, 121, 135, 148, 148, 136, 119]
result = exponential_smoothing_detailed(sample_data, alpha=0.3)
This implementation shows exactly how each forecast is calculated. The initialization uses the first observation, though you could also use the average of the first few values for more stability.
Practical Implementation with Real Data
Let’s apply SES to monthly product sales data:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Create sample monthly sales data
np.random.seed(42)
dates = pd.date_range('2022-01-01', periods=24, freq='M')
sales = 1000 + np.random.normal(0, 100, 24) # Sales around 1000 units
data = pd.DataFrame({'date': dates, 'sales': sales})
def ses_forecast(data, alpha, periods_ahead=3):
"""Apply SES and generate future forecasts."""
smoothed = np.zeros(len(data) + periods_ahead)
smoothed[0] = data[0]
# Smooth historical data
for t in range(1, len(data)):
smoothed[t] = alpha * data[t] + (1 - alpha) * smoothed[t-1]
# Generate forecasts (flat forecast)
for t in range(len(data), len(data) + periods_ahead):
smoothed[t] = smoothed[t-1] # SES forecast is last smoothed value
return smoothed
# Apply SES
alpha = 0.3
smoothed_values = ses_forecast(data['sales'].values, alpha)
# Visualize
plt.figure(figsize=(12, 6))
plt.plot(data['date'], data['sales'], 'o-', label='Actual Sales', alpha=0.6)
plt.plot(data['date'], smoothed_values[:len(data)], 'r-', label=f'SES (α={alpha})', linewidth=2)
# Add forecast period
forecast_dates = pd.date_range(data['date'].iloc[-1], periods=4, freq='M')[1:]
plt.plot(forecast_dates, smoothed_values[len(data):], 'r--', label='Forecast', linewidth=2)
plt.xlabel('Date')
plt.ylabel('Sales Units')
plt.title('Sales Data with Simple Exponential Smoothing')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Notice that SES produces a flat forecast—the predicted value for all future periods is simply the last smoothed value. This is a fundamental limitation when dealing with trending data.
Using statsmodels Library
For production use, leverage statsmodels’ battle-tested implementation:
from statsmodels.tsa.holtwinters import SimpleExpSmoothing
import numpy as np
import pandas as pd
# Using the same sales data
model = SimpleExpSmoothing(data['sales'])
# Fit model - let it optimize alpha
fitted_model = model.fit(optimized=True)
# Or specify alpha manually
# fitted_model = model.fit(smoothing_level=0.3, optimized=False)
print(f"Optimized Alpha: {fitted_model.params['smoothing_level']:.4f}")
# Get fitted values
fitted_values = fitted_model.fittedvalues
# Generate forecasts
forecast = fitted_model.forecast(steps=3)
print(f"\nForecast for next 3 periods: {forecast.values}")
# Access model parameters and statistics
print(f"AIC: {fitted_model.aic:.2f}")
print(f"BIC: {fitted_model.bic:.2f}")
# Visualize
plt.figure(figsize=(12, 6))
plt.plot(data['sales'], 'o-', label='Actual', alpha=0.6)
plt.plot(fitted_values, 'r-', label='Fitted', linewidth=2)
forecast_index = range(len(data), len(data) + 3)
plt.plot(forecast_index, forecast, 'r--', label='Forecast', linewidth=2)
plt.xlabel('Time Period')
plt.ylabel('Sales')
plt.title('SES with statsmodels')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
The statsmodels implementation handles edge cases, provides automatic parameter optimization, and includes statistical diagnostics like AIC and BIC for model comparison.
Evaluating Model Performance
Always validate forecasting models using proper time series splits:
from sklearn.metrics import mean_absolute_error, mean_squared_error
import numpy as np
def calculate_metrics(actual, predicted):
"""Calculate common forecasting error metrics."""
mae = mean_absolute_error(actual, predicted)
rmse = np.sqrt(mean_squared_error(actual, predicted))
mape = np.mean(np.abs((actual - predicted) / actual)) * 100
return {'MAE': mae, 'RMSE': rmse, 'MAPE': mape}
# Train/test split (use last 6 months for testing)
train_size = len(data) - 6
train_data = data['sales'][:train_size]
test_data = data['sales'][train_size:]
# Test different alpha values
alphas = [0.1, 0.3, 0.5, 0.7, 0.9]
results = []
for alpha in alphas:
model = SimpleExpSmoothing(train_data)
fitted = model.fit(smoothing_level=alpha, optimized=False)
predictions = fitted.forecast(steps=len(test_data))
metrics = calculate_metrics(test_data.values, predictions.values)
metrics['alpha'] = alpha
results.append(metrics)
# Display results
results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))
print(f"\nBest alpha by RMSE: {results_df.loc[results_df['RMSE'].idxmin(), 'alpha']}")
This approach prevents overfitting by evaluating on unseen data. For time series, never use random splits—always preserve temporal order.
Limitations and When to Use Alternatives
SES has clear boundaries. It fails when your data exhibits:
Trend: Sales increasing 5% monthly will be consistently underforecast. Use Double Exponential Smoothing (Holt’s method) which adds a trend component.
Seasonality: Retail sales with December spikes need Triple Exponential Smoothing (Holt-Winters) which handles both trend and seasonal patterns.
Complex patterns: ARIMA, Prophet, or machine learning methods handle autocorrelation structures and external regressors that SES ignores.
Use SES when you need quick, interpretable forecasts for stable processes, when computational resources are limited, or as a baseline to benchmark more complex models against. Its simplicity is a feature, not a bug—sometimes a straightforward weighted average is exactly what you need.