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 AAPL.

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])

sma = price.rolling(3).mean()
ema = price.ewm(span=3, adjust=False).mean()

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)

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]

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)]

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.8125, then 12.906250, then 12.453125, halving the distance to 12 each time.

Step 4. Fifty days on AAPL

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

prices = pd.read_csv("prices.csv", parse_dates=["Date"])

aapl  = prices[prices["Ticker"] == "AAPL"].set_index("Date")
close = aapl["Close"]

print(len(close))            # -> 1566

print(close.head(3))
# -> 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})
ind["sma50"] = close.rolling(50).mean()
ind["ema50"] = close.ewm(span=50, adjust=False).mean()

print(ind.tail(4).round(2))
# ->              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)
last = a * ind["close"].iloc[-1] + (1 - a) * ind["ema50"].iloc[-2]

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))
# ->             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))
# ->             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, and it takes several dozen rows before the seed stops mattering.

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

both = ind.dropna()

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 MSFT 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"])

msft  = prices[prices["Ticker"] == "MSFT"].set_index("Date")
close = msft["Close"]

out = pd.DataFrame({"close": close})
out["sma20"] = close.rolling(20).mean()
out["ema20"] = close.ewm(span=20, adjust=False).mean()

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