Exponentially Weighted Moving Average
Apply exponential smoothing to give recent values more weight with ewm(span=), a standard technique in financial analysis.
Exponentially Weighted Moving Average is a free Pandas & NumPy Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Pandas & NumPy Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Problem with Simple Moving Averages
A simple moving average treats all n observations in the window equally — a price from 30 days ago contributes the same as yesterday's price. But for predicting near-term values, recent observations are more informative than old ones. An Exponentially Weighted Moving Average (EWMA) solves this by assigning exponentially decreasing weights to older observations: yesterday counts the most, the day before counts slightly less, and so on back to the beginning of the series.
import pandas as pd
import numpy as np
np.random.seed(42)
prices = pd.Series(
100 + np.cumsum(np.random.normal(0.5, 2, 30)),
index=pd.date_range('2024-01-01', periods=30)
)
df = pd.DataFrame({'price': prices})
df['SMA_7'] = df['price'].rolling(7).mean()
df['EWM_span7'] = df['price'].ewm(span=7).mean()
print(df.tail(5).round(2))How EWMA Weighting Works
EWMA computes each value as a weighted combination of the current observation and the previous EWMA: EWM_t = alpha * x_t + (1 - alpha) * EWM_{t-1}. The alpha (smoothing factor) determines how quickly weights decay — a large alpha (close to 1) responds quickly to recent changes (less smoothing), a small alpha (close to 0) is slow to respond (more smoothing). The weights form a geometric series: the most recent value has weight alpha, the previous has alpha*(1-alpha), and so on.
import numpy as np
# Manually show EWMA weights
alpha = 0.3
n_points = 8
weights = [alpha * (1 - alpha)**i for i in range(n_points)]
print(f'Alpha = {alpha}')
print(f'Weights (most recent first):')
for i, w in enumerate(weights):
print(f' t-{i}: {w:.4f}')
print(f'Sum of shown weights: {sum(weights):.4f} (converges to 1.0)')The ewm() Method and span Parameter
Pandas implements EWMA through Series.ewm(). The most intuitive parameter is span, which is analogous to a simple moving average window: alpha = 2 / (span + 1). A span of 10 approximately corresponds to a 10-period SMA but with exponentially decaying weights instead of equal weights. Use ewm(span=n).mean() to get the EWMA, and ewm(span=n).std() for exponentially weighted volatility.
import pandas as pd
import numpy as np
np.random.seed(0)
data = pd.Series(
np.random.normal(100, 10, 50),
index=pd.date_range('2024-01-01', periods=50)
)
df = pd.DataFrame({'value': data})
df['EWM_3'] = df['value'].ewm(span=3).mean()
df['EWM_10'] = df['value'].ewm(span=10).mean()
df['EWM_20'] = df['value'].ewm(span=20).mean()
print('Alpha values:')
for span in [3, 10, 20]:
alpha = 2 / (span + 1)
print(f' span={span} → alpha={alpha:.3f}')
print(df.tail(5).round(2))The halflife Parameter
The halflife parameter defines how many periods it takes for the weight to decrease by half. If halflife=5, a value from 5 periods ago has half the weight of the current value. The relationship is: alpha = 1 - exp(log(0.5) / halflife). Halflife is often more intuitive than span or alpha because it directly expresses the 'memory' of the average in business terms — for example, 'prices from 2 weeks ago should have half the influence of today's price'.
import pandas as pd
import numpy as np
np.random.seed(5)
prices = pd.Series(
100 + np.cumsum(np.random.normal(0, 2, 30)),
index=pd.date_range('2024-01-01', periods=30)
)
df = pd.DataFrame({'price': prices})
# halflife=5: weight halves every 5 days
df['EWM_hl5'] = df['price'].ewm(halflife=5).mean()
# halflife=14: weight halves every 14 days (longer memory)
df['EWM_hl14'] = df['price'].ewm(halflife=14).mean()
print(df.tail(5).round(2))The alpha Parameter Directly
You can also set the smoothing factor directly with ewm(alpha=). Alpha must be between 0 and 1 (exclusive). A high alpha like 0.8 gives very little smoothing — the EWMA tracks the raw signal closely. A low alpha like 0.1 gives heavy smoothing — the EWMA changes slowly and lags behind sudden shifts. Choosing the right alpha depends on the trade-off between responsiveness (detecting real trend changes quickly) and stability (ignoring random noise).
import pandas as pd
import numpy as np
np.random.seed(3)
data = pd.Series(
[100]*20 + [120]*20, # sudden level shift at row 20
dtype=float
) + np.random.normal(0, 2, 40)
df = pd.DataFrame({'signal': data})
df['alpha_0.1'] = df['signal'].ewm(alpha=0.1).mean() # slow
df['alpha_0.5'] = df['signal'].ewm(alpha=0.5).mean() # medium
df['alpha_0.9'] = df['signal'].ewm(alpha=0.9).mean() # fast
print('After the level shift (rows 20-25):')
print(df.iloc[18:26].round(2))EWMA for Volatility: ewm().std()
ewm(span=).std() computes an exponentially weighted standard deviation. This is the basis of the EWMA volatility model used in financial risk management (also known as the RiskMetrics model with decay factor lambda=0.94). Exponentially weighted volatility responds faster to volatility spikes than a simple rolling std because it assigns more weight to recent high-variance observations.
import pandas as pd
import numpy as np
np.random.seed(42)
# Simulate returns: quiet period then volatile period
returns = pd.Series(
np.concatenate([
np.random.normal(0, 0.01, 60),
np.random.normal(0, 0.04, 60) # 4x more volatile
]),
index=pd.date_range('2024-01-01', periods=120)
)
df = pd.DataFrame({'returns': returns})
df['rolling_vol'] = df['returns'].rolling(20).std()
df['ewm_vol'] = df['returns'].ewm(span=20).std()
print('Volatility comparison (transition at row 60):')
print(df.iloc[58:65].round(5))MACD: Combining Two EWMAs
The MACD (Moving Average Convergence Divergence) is a classic financial indicator built from two EWMAs. It is the difference between a fast (short-span) EWMA and a slow (long-span) EWMA of the price. When the MACD crosses zero from below, it signals potential upward momentum (a buy signal); crossing from above signals downward momentum. MACD is a perfect example of applying two EWMAs to derive a derived indicator.
import pandas as pd
import numpy as np
np.random.seed(7)
prices = pd.Series(
100 + np.cumsum(np.random.normal(0.3, 2, 100)),
index=pd.date_range('2024-01-01', periods=100)
)
df = pd.DataFrame({'price': prices})
df['EMA_12'] = df['price'].ewm(span=12).mean() # fast
df['EMA_26'] = df['price'].ewm(span=26).mean() # slow
df['MACD'] = df['EMA_12'] - df['EMA_26']
df['Signal'] = df['MACD'].ewm(span=9).mean() # signal line
print('MACD values (last 8 rows):')
print(df[['price', 'MACD', 'Signal']].tail(8).round(2))EWMA with Time-Based Halflife
Pandas 1.1+ supports specifying halflife, span, and com as time offsets (e.g. halflife='7D') when working with a DatetimeIndex. This enables proper EWMA on non-uniform time series — for example, if you have hourly data with gaps on weekends, a halflife of '7D' means the weight truly halves every 7 calendar days regardless of the number of rows in that period.
import pandas as pd
import numpy as np
np.random.seed(0)
# Business-day data (no weekends)
biz_dates = pd.bdate_range('2024-01-01', periods=20)
prices = pd.Series(100 + np.cumsum(np.random.randn(20)), index=biz_dates)
# halflife as a time offset (7 calendar days)
ewm_7d = prices.ewm(halflife='7D', times=biz_dates).mean()
# halflife as row count (7 rows)
ewm_7rows = prices.ewm(halflife=7).mean()
df = pd.DataFrame({'price': prices, 'hl_7D': ewm_7d, 'hl_7rows': ewm_7rows})
print(df.round(2))Choosing Between EWMA, Rolling, and Expanding
Summary of when to use each:
- Rolling(n).mean(): all n recent observations equally weighted — good for simple trailing metrics and when all recent periods matter equally.
- ewm(span=n).mean(): exponentially weighted — best when you want smoothing that responds more to recent changes, commonly used in finance and signal processing.
- expanding().mean(): all historical data with equal weight — best for YTD or all-time statistics where forgetting old data is not acceptable.
Visualising EWMA vs SMA
Plotting EWMA and SMA together on a time series chart makes the difference in their responsiveness visible. The EWMA reacts more quickly to sudden level changes because it has not 'forgotten' all previous observations — the short-span EWMA will turn faster than an SMA with the same nominal window. This makes EWMA preferable in forecasting applications where capturing trend direction quickly is more valuable than maximum noise reduction.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
prices = pd.Series(
np.concatenate([
100 + np.cumsum(np.random.normal(0.3, 1.5, 50)),
130 + np.cumsum(np.random.normal(-0.3, 1.5, 50))
]),
index=pd.date_range('2024-01-01', periods=100)
)
df = pd.DataFrame({'price': prices})
df['SMA_10'] = df['price'].rolling(10).mean()
df['EWMA_10'] = df['price'].ewm(span=10).mean()
df.plot(figsize=(12, 5), title='SMA vs EWMA — EWMA reacts faster to trend reversals')
plt.ylabel('Price')
plt.axvline(x=df.index[50], color='red', linestyle='--', label='Trend change')
plt.legend()
plt.show()EWMA in Business Applications
Beyond finance, EWMA is widely used in product analytics and operations: customer lifetime value smoothing (recent behaviour predicts future spend better than old behaviour), server latency monitoring (an exponentially smoothed latency metric sounds false alarms faster than a rolling mean), and inventory demand forecasting (recent sales weeks should outweigh data from six months ago). The universal principle is: wherever recency matters, EWMA outperforms simple averaging.
Quick Check
Test your understanding of exponentially weighted moving averages from this lesson.
Lesson Recap
In this lesson you learned: ewm(span=) applies exponentially decaying weights so recent observations matter more, alpha controls responsiveness (high = fast, low = smooth), and the halflife parameter expresses memory in business-meaningful terms. EWMA outperforms SMA when detecting trend reversals quickly is important. Next up we explore rank and percentile calculations within groups using groupby().rank() and pd.qcut.
Frequently asked questions
Is the “Exponentially Weighted Moving Average” lesson free?
Yes — the full text of “Exponentially Weighted Moving Average” is free to read here on the web, and the Pandas & NumPy Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Pandas & NumPy Academy course, upgrade to CoddyKit PRO.
What will I learn in “Exponentially Weighted Moving Average”?
Apply exponential smoothing to give recent values more weight with ewm(span=), a standard technique in financial analysis. You practise Pandas & NumPy Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Pandas & NumPy Academy?
No prior experience is required. Pandas & NumPy Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Exponentially Weighted Moving Average” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Pandas & NumPy Academy lesson?
Yes. Every Pandas & NumPy Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Rolling Windows
- Expanding Windows
- Exponentially Weighted Moving Average
- Rank and Percentile within Groups