What is the difference between an SMA and an EMA?

Compute a simple and an exponential moving average, and see how each one weights the past.

A simple moving average of the last n closes gives every one of those n closes the same weight, and gives every close before them a weight of zero. An exponential moving average gives today the most weight, yesterday a bit less, the day before a bit less again, and never drops a close entirely.

In this lesson I compute both on seven prices I can check by hand, reproduce one exponential value from the formula ema_t = a * price_t + (1 - a) * ema_(t-1), and then put a 50-day version of each side by side on AAA.

Step 1. Seven prices

.rolling(3).mean() is the simple moving average over three rows. .ewm(span=3, adjust=False).mean() is the exponential one.

import pandas as pd

price = pd.Series([10.0, 11.0, 12.0, 20.0, 12.0, 12.0, 12.0])   # seven closes, with one spike at row 3

sma = price.rolling(3).mean()                                   # flat mean of the last three closes
ema = price.ewm(span=3, adjust=False).mean()                    # same three day speed, weighted toward today

print(pd.DataFrame({"price": price, "sma3": sma, "ema3": ema}))
# ->    price       sma3       ema3
# -> 0   10.0        NaN  10.000000
# -> 1   11.0        NaN  10.500000
# -> 2   12.0  11.000000  11.250000
# -> 3   20.0  14.333333  15.625000
# -> 4   12.0  14.666667  13.812500
# -> 5   12.0  14.666667  12.906250
# -> 6   12.0  12.000000  12.453125
   price       sma3       ema3
0   10.0        NaN  10.000000
1   11.0        NaN  10.500000
2   12.0  11.000000  11.250000
3   20.0  14.333333  15.625000
4   12.0  14.666667  13.812500
5   12.0  14.666667  12.906250
6   12.0  12.000000  12.453125

The simple average has no value on rows 0 and 1, because there are not yet three prices to average. The exponential one has a value on every row.

Row 3 of the simple average is the mean of rows 1, 2 and 3.

print(sma.iloc[3])                       # -> 14.333333333333334
print((11.0 + 12.0 + 20.0) / 3)          # -> 14.333333333333334
14.333333333333334
14.333333333333334

Step 2. The recursive formula

The exponential average carries yesterday’s value forward and mixes in today’s price. The mixing weight is a = 2 / (span + 1), so a span of 3 gives a = 0.5.

Row 0 has no yesterday, so with adjust=False pandas seeds it with the first price.

alpha = 2 / (3 + 1)                      # mixing weight 2/(span+1) for a span of 3

print(alpha)                             # -> 0.5
print(ema.iloc[0])                       # -> 10.0
print(ema.iloc[1])                       # -> 10.5
print(alpha * 11.0 + (1 - alpha) * 10.0) # -> 10.5
0.5
10.0
10.5
10.5

Row 3 is the same step run on row 2, which is where the jump to 20 shows up.

by_hand = alpha * price.iloc[3] + (1 - alpha) * ema.iloc[2]  # half of today, half of yesterday carried on

print(round(by_hand, 10))                # -> 15.625
print(round(ema.iloc[3], 10))            # -> 15.625
15.625
15.625

Unrolling that recursion gives the weight on each past price: a on today, a * (1 - a) on yesterday, a * (1 - a)^2 on the day before, and so on.

weights = [alpha * (1 - alpha) ** k for k in range(6)]  # weight landing on each of the last six days

print([round(w, 4) for w in weights])
# -> [0.5, 0.25, 0.125, 0.0625, 0.0312, 0.0156]

print(round(sum(weights), 4))            # -> 0.9844
[0.5, 0.25, 0.125, 0.0625, 0.0312, 0.0156]
0.9844

Six days account for 98.44% of the weight. The rest sits on everything older, halving each step back, never reaching zero.

Step 3. When an old price leaves the window

Rows 4, 5 and 6 all have a price of 12. The simple average holds at 14.666667 while the 20 is still inside its three day window, then drops to 12.0 the moment the 20 falls out.

print(pd.DataFrame({"price": price, "sma3": sma, "ema3": ema}).tail(3))
# ->    price       sma3       ema3
# -> 4   12.0  14.666667  13.812500
# -> 5   12.0  14.666667  12.906250
# -> 6   12.0  12.000000  12.453125
   price       sma3       ema3
4   12.0  14.666667  13.812500
5   12.0  14.666667  12.906250
6   12.0  12.000000  12.453125

The exponential average moves on all three rows instead: 13.812500, then 12.906250, then 12.453125, halving the distance to 12 each time.

Step 4. Fifty days on AAA

prices.csv sits next to this lesson and holds simulated data: daily closes for four tickers over six years. I take AAA and index it by date.

prices = pd.read_csv("prices.csv", parse_dates=["Date"])      # Date read as timestamps, not text

aapl  = prices[prices["Ticker"] == "AAA"].set_index("Date")  # one ticker, dates as the index
close = aapl["Close"]                                         # one column, ready for rolling and ewm

print(len(close))            # -> 1566

print(close.head(3))                                          # first three closes, keyed by date
# -> Date
# -> 2020-01-01    75.08
# -> 2020-01-02    76.94
# -> 2020-01-03    77.34
# -> Name: Close, dtype: float64
1566
Date
2020-01-01    75.08
2020-01-02    76.94
2020-01-03    77.34
Name: Close, dtype: float64

Both averages go into one frame next to the close.

ind = pd.DataFrame({"close": close})                    # one frame to hold close and both averages
ind["sma50"] = close.rolling(50).mean()                 # equal weight on the last 50 closes
ind["ema50"] = close.ewm(span=50, adjust=False).mean()  # fades old closes instead of dropping them

print(ind.tail(4).round(2))                             # ema50 tracks the close more closely
# ->              close   sma50   ema50
# -> Date
# -> 2025-12-26  197.62  195.16  192.58
# -> 2025-12-29  196.94  195.27  192.75
# -> 2025-12-30  196.56  195.34  192.90
# -> 2025-12-31  199.30  195.50  193.15
             close   sma50   ema50
Date                              
2025-12-26  197.62  195.16  192.58
2025-12-29  196.94  195.27  192.75
2025-12-30  196.56  195.34  192.90
2025-12-31  199.30  195.50  193.15

The same one line formula produces the last exponential value, with a = 2 / 51.

a    = 2 / (50 + 1)                                                 # mixing weight for a span of 50
last = a * ind["close"].iloc[-1] + (1 - a) * ind["ema50"].iloc[-2]  # one recursion step done by hand

print(round(a, 6))                        # -> 0.039216
print(round(last, 6))                     # -> 193.149569
print(round(ind["ema50"].iloc[-1], 6))    # -> 193.149569
0.039216
193.149569
193.149569

Each new close moves the 50-day exponential average by 3.9216% of the gap between the close and yesterday’s average.

Step 5. How many rows each one needs

The 50-day simple average needs 50 closes, so the first 49 rows are NaN. The exponential average produces a number on row 0.

print(ind["sma50"].isna().sum())          # -> 49
print(ind["ema50"].isna().sum())          # -> 0

print(ind["sma50"].first_valid_index())   # -> 2020-03-10 00:00:00
print(ind["ema50"].first_valid_index())   # -> 2020-01-01 00:00:00
49
0
2020-03-10 00:00:00
2020-01-01 00:00:00

Here are the two ends of that gap.

print(ind.head(3).round(2))      # sma50 still empty this early
# ->             close  sma50  ema50
# -> Date
# -> 2020-01-01  75.08    NaN  75.08
# -> 2020-01-02  76.94    NaN  75.15
# -> 2020-01-03  77.34    NaN  75.24

print(ind.iloc[48:51].round(2))  # row 49 is the first sma50 value
# ->             close  sma50  ema50
# -> Date
# -> 2020-03-09  78.50    NaN  76.42
# -> 2020-03-10  80.98  76.20  76.60
# -> 2020-03-11  79.65  76.29  76.72
            close  sma50  ema50
Date                           
2020-01-01  75.08    NaN  75.08
2020-01-02  76.94    NaN  75.15
2020-01-03  77.34    NaN  75.24
            close  sma50  ema50
Date                           
2020-03-09  78.50    NaN  76.42
2020-03-10  80.98  76.20  76.60
2020-03-11  79.65  76.29  76.72

The early exponential values are not an average of 50 days. On 2020-01-01 the value is just the first close. By 2020-03-10, where the simple average starts, the exponential average reads 76.60 against a close of 80.98.

Over the rows where both exist, the exponential average sits closer to the close.

both = ind.dropna()                                             # rows where both averages exist

print(round((both["close"] - both["sma50"]).abs().mean(), 2))   # -> 5.72
print(round((both["close"] - both["ema50"]).abs().mean(), 2))   # -> 4.91
5.72
4.91

Both of these are indicators. Lesson 29 turns one into a position.

Your turn

Compute a 20-day simple and a 20-day exponential moving average on CCC from prices.csv. How many NaN values does each one start with?

import pandas as pd

prices = pd.read_csv("prices.csv", parse_dates=["Date"])      # Date read as timestamps, not text

msft  = prices[prices["Ticker"] == "CCC"].set_index("Date")  # one ticker, dates as the index
close = msft["Close"]

out = pd.DataFrame({"close": close})
out["sma20"] = close.rolling(20).mean()                       # no value until 20 closes exist
out["ema20"] = close.ewm(span=20, adjust=False).mean()        # a value from the very first row

print(out["sma20"].isna().sum())    # -> 19
print(out["ema20"].isna().sum())    # -> 0

print(out.tail(3).round(2))
# ->              close   sma20   ema20
# -> Date
# -> 2025-12-29  817.75  794.42  796.46
# -> 2025-12-30  826.31  794.59  799.30
# -> 2025-12-31  828.51  795.60  802.08