Mean, variance and spread
Expected value, variance and standard deviation.
Create a free account Try part of it here
Why it matters
The expected value is the average you'd get over many tries. AI work is full of them: expected revenue per visitor, or expected reward in reinforcement learning, where a program learns by trial and error. Training a model means making its expected loss as small as possible. The loss is a score for how wrong the model is on one example. The expected loss is its average over many examples. Variance measures how far single results scatter around the average. It's what separates a steady channel from a lottery ticket, and a calm training run from a noisy one.
What you'll be able to do
- Compute an expected value as a probability-weighted average
- Measure spread with variance and standard deviation
- Use the rules for scaling and adding random variables
- Compare options with the same average but different risk
What it covers
- The average you'd expect
- Same average, very different ride
- Worked example: two channels with the same average
- From real data: sample mean and variance
- Shortcuts: shifting and scaling
- Adding random amounts together
The average you'd expect
Roll a die many times and write down each result. Then average them. You'll get close to 3.5.
Here's why. Each face from 1 to 6 comes up about one time in six. So the long-run average is (1 + 2 + 3 + 4 + 5 + 6) ÷ 6, which is 21 ÷ 6 = 3.5. No die ever shows 3.5. The average doesn't have to be a result that can actually happen.
This long-run average is called the expected value, or the mean. It works for any random variable: a number that comes out of a random process. You met these in the last lesson.
When outcomes aren't equally likely, you weight each one by its chance. Here's a shop example. A visitor to your online store buys something with probability 0.03, which is 3%. A buyer spends $80. Everyone else spends nothing. What is one visitor worth?
- 3% of visitors spend $80: 0.03 × 80 = $2.40.
- 97% spend $0: 0.97 × 0 = $0.
- Add the two: $2.40 per visitor.
So 10,000 visitors are worth about $24,000. That also tells you the most you should pay for each visitor. Notice that no single visitor spends $2.40. Each one spends $80 or nothing.
In math, the recipe looks like this:
Here is each piece:
- is the random variable, such as "what one visitor spends".
- means "the expected value of X". The E stands for "expected".
- Small stands for one particular value that X can take.
- is the chance that X comes out as that value.
- The big Greek letter (sigma) means "add up". The under it says "for every possible value".
In plain words: take each possible value, multiply it by its chance, and add up the results.
Here is the visitor example in that notation:
In Python, the becomes a loop that adds as it goes:
values = [80, 0] # what one visitor spends probs = [0.03, 0.97] # how likely each amount is print(sum(v * p for v, p in zip(values, probs)))
It prints 2.4. zip pairs each value with its chance. Then sum adds up the products.
outcomes = [(10, 0.2), (0, 0.8)] print(sum(x * p for x, p in outcomes))
Show the answer
2.0
10 × 0.2 is 2.0 and 0 × 0.8 is 0, so the total is 2.0. Each outcome counts in proportion to its chance. You'd never see exactly 2, but that's the long-run average.
Show the answer
$5
Multiply each prize by its chance and add: 0.1 × 50 + 0.9 × 0 = $5. No ticket ever wins exactly $5. It's the long-run average.
Same average, very different ride
Picture two ways to get to work. Both take 30 minutes on average. The bus takes 28 to 32 minutes every day. The car takes 15 minutes on a clear day and 60 in a jam. The averages match, but only one lets you plan your morning.
The average alone can't tell these apart. You need a second number that measures spread: how far single results tend to land from the average. The recipe has three steps.
- Find how far each value is from the mean. This gap is called the deviation.
- Square each gap, which means multiply it by itself. This makes every gap positive. Without it, gaps above and below the mean would cancel each other out.
- Take the weighted average of those squares, using the same chances as before.
The result is the variance. Because of the squaring, it's in odd units like "squared dollars". So you usually take its square root: the number that, times itself, gives the variance. For example, the square root of 25 is 5. That square root is the standard deviation. It's back in the original units, like dollars or minutes. Think of it as "the typical distance from the average".
Two more Greek letters help here. (mu, said "mew") is the mean. (a small sigma) is the standard deviation. It's a different letter from the big that means "add up".
In plain words: the variance is the average squared distance from the mean. The standard deviation is its square root, so it's in the same units as the data.
A worked example: a coin game pays $0 on tails and $10 on heads. The mean is $5. Both outcomes are $5 away from it. Squared, each gap is 25. The weighted average is 0.5 × 25 + 0.5 × 25 = 25. So the variance is 25, and the standard deviation is the square root of 25, which is $5. That matches your gut: every result is $5 from the average.
values = [0, 10] probs = [0.5, 0.5] mean = sum(v * p for v, p in zip(values, probs)) var = sum((v - mean) ** 2 * p for v, p in zip(values, probs)) print(mean, var, var ** 0.5)
It prints 5.0 25.0 5.0: the mean, the variance and the standard deviation. In Python, ** 0.5 takes a square root.
Now a real choice. You have two campaign options. "Steady" earns $9,000, $10,000 or $11,000, with chances 25%, 50% and 25%. "Moonshot" earns $0 (60% of the time), $10,000 (25%) or $40,000 (15%). The code below does the same three steps with NumPy. NumPy multiplies arrays item by item, so d["values"] * d["probs"] multiplies each value by its chance in one go.
import numpy as np # Two campaign options: revenue outcomes and their probabilities steady = {"values": np.array([9_000, 10_000, 11_000]), "probs": np.array([0.25, 0.5, 0.25])} moonshot = {"values": np.array([0, 10_000, 40_000]), "probs": np.array([0.6, 0.25, 0.15])} for name, d in [("steady", steady), ("moonshot", moonshot)]: mean = np.sum(d["values"] * d["probs"]) var = np.sum((d["values"] - mean) ** 2 * d["probs"]) print(f"{name:<9} expected = {mean:>8,.0f} std = {np.sqrt(var):>8,.0f}")
Steady expects $10,000, with a standard deviation of about $707. Moonshot expects $8,500. That's close enough to be tempting. But its standard deviation is about $13,883, nearly twenty times larger. Most of the time it earns nothing. Whether that's acceptable is a business decision. The numbers make the trade-off plain to see.
Go deeper Why square the gaps?
You could drop the minus signs and average the plain distances instead. That measure exists. It's called the mean absolute deviation. Squaring is the standard choice for two reasons. First, it makes the rules later in this lesson come out neatly: for independent amounts, variances add. Second, it weighs big misses more than small ones. A miss of 10 counts as 100, while a miss of 2 counts as only 4.
Same mean, different spread. Drag the standard deviation (σ) and draw samples. A larger σ means single results land farther from the average.
Show the answer
They have the same mean, and B has the bigger standard deviation
Both average $100. A never moves, so its standard deviation is 0. B always lands $100 from the average, so its standard deviation is $100.
Worked example: two channels with the same average
Two ad channels earn a monthly profit, in thousands of dollars. Channel A earns 4, 5 or 6. Channel B earns −3, 5 or 13. For both, the chances are 25%, 50% and 25%. Work out each one's mean and standard deviation.
def mean_and_sd(values, probs): mean = sum(v * p for v, p in zip(values, probs)) var = sum((v - mean) ** 2 * p for v, p in zip(values, probs)) return mean, var ** 0.5 probs = [0.25, 0.5, 0.25] for name, values in [("A", [4, 5, 6]), ("B", [-3, 5, 13])]: mean, sd = mean_and_sd(values, probs) print(name, "mean", mean, "sd", round(sd, 2))
Both channels have a mean of 5, so both average $5,000 a month. Channel A's standard deviation is about 0.71 (roughly $710). Channel B's is about 5.66 (roughly $5,660), eight times bigger. In a bad month, B loses $3,000. If your business needs predictable months, A is the safer pick. The averages alone would have called it a tie.
From real data: sample mean and variance
So far you knew the chances in advance. Usually you don't. Instead you have a list of results, like last week's daily sales or the reply times of 50 support emails. Then every observation gets equal weight.
Call the number of observations . Write each observation as . The small counts through the list: , , and so on. The mean of the data is written , said "x bar". The sample variance is written .
In plain words: the mean is the ordinary average: add the values and divide by how many there are. The sample variance adds up the squared distances from that mean. Then it divides by one less than the count, , instead of by .
Why one less? Your mean was worked out from the same data. No other number sits closer to those points overall. The true mean, which you don't know, is almost always a bit farther away. So the squared distances from your mean come out a little too small. Dividing by a slightly smaller number nudges the answer back up.
A worked example with three values: 2, 4 and 6. The mean is 4. The distances from it are −2, 0 and 2. Squared, they're 4, 0 and 4, which add to 8. Divide by and the variance is
- The standard deviation is the square root of 4, which is 2. Dividing by instead
would give about 2.667.
In NumPy, you choose with ddof, the number taken off . Use np.var(x, ddof=1) and np.std(x, ddof=1) for the version. The default, ddof=0, divides by .
import numpy as np data = np.array([2, 4, 6]) print(data.mean()) print(np.var(data, ddof=1), np.std(data, ddof=1)) print(round(float(np.var(data)), 3))
It prints 4.0, then 4.0 2.0, then 2.667. With large samples the difference is negligible. Dividing the sum of 1,000 squared distances by 999 or by 1,000 gives almost the same answer.
Go deeper What "unbiased" means
The version is called unbiased. Imagine taking many samples and working out for each one. The average of all those answers would land on the true variance. The divide-by- version would come out slightly low on average. This fix is known as Bessel's correction.
Show the answer
4
ddof=1 takes 1 off n, so it divides by 5 − 1 = 4. The default, ddof=0, would divide by 5.
Shortcuts: shifting and scaling
Picture a class photo where every kid stands on the same 20 cm step. The average height goes up by 20 cm. But the gap between the tallest and shortest kid doesn't change. Adding the same amount to everything moves the average and leaves the spread alone.
Now picture converting prices from dollars to cents: multiply everything by 100. The average becomes 100 times bigger. So does every distance from it. Multiplying stretches the spread too.
You can write both changes as . Here is the number you multiply by. And is the number you add afterwards.
In plain words: the new mean is the old mean, times , plus . Adding doesn't touch the spread. Multiplying by multiplies the variance by squared. So the standard deviation is multiplied by , ignoring any minus sign.
A worked example: two values, 4 and 6. Their mean is 5 and their standard deviation is 1. Double them and add 10, and you get 18 and 22. The new mean is 2 × 5 + 10 = 20. The new standard deviation is 2 × 1 = 2. The variance went from 1 to 4, which is 2 squared times 1.
import numpy as np x = np.array([4.0, 6.0]) y = 2 * x + 10 print(x.mean(), x.std()) print(y.mean(), y.std())
It prints 5.0 1.0 and then 20.0 2.0.
Show the answer
$65 and $20
Adding a constant shifts the mean up by $5 but doesn't change the spread, like everyone standing on the same step.
Adding random amounts together
Now add two random amounts, like the sales of two separate campaigns. Means always add. If one averages $3,000 and the other $5,000, together they average $8,000. This holds even when the two are linked.
Spread works differently. Variances add, but only when the two are independent: one doesn't affect the other. Standard deviations don't add.
Call the first amount . Call the second one , like the second campaign's sales.
In plain words: the average of a total is the total of the averages. For independent amounts, the variance of a total is the total of the variances. Add variances, then take the square root. Never add standard deviations.
A worked example: two independent campaigns each have a standard deviation of $3,000. Work in thousands to keep the numbers small. Each variance is 3 × 3 = 9. Together that's 9 + 9 = 18. The square root of 18 is about 4.24. So the total's standard deviation is about $4,240, not $6,000. It's smaller because good and bad weeks partly cancel. When one campaign has a bad week, the other often doesn't.
sd_one = 3 var_total = sd_one ** 2 + sd_one ** 2 print(var_total, round(var_total ** 0.5, 2))
It prints 18 4.24.
This rule explains why spreading your bets works, which is called diversification. Take independent campaigns with the same spread. The expected total grows times. The standard deviation grows only by the square root of , written . With 4 campaigns, the total is 4 times bigger but only 2 times as spread out. With 100 campaigns, it's 100 times bigger but only 10 times as spread out. Compared with its size, the total gets steadier. The same shows up in the next two lessons. It's the reason bigger samples give more precise estimates.
The code below checks this by simulation. rng.normal(10_000, 4_000, ...) draws random campaign results that average 10,000 with a standard deviation of 4,000. (That bell-shaped "normal" distribution gets its own lesson soon.) It draws 100,000 single campaigns, and 100,000 groups of twenty. The std/mean column compares the spread to the size.
import numpy as np rng = np.random.default_rng(1) one = rng.normal(10_000, 4_000, size=100_000) # one campaign twenty = rng.normal(10_000, 4_000, size=(100_000, 20)).sum(axis=1) # twenty independent ones print("one: mean", round(one.mean()), " std", round(one.std()), " std/mean", round(one.std() / one.mean(), 3)) print("twenty: mean", round(twenty.mean()), " std", round(twenty.std()), " std/mean", round(twenty.std() / twenty.mean(), 3))
For one campaign, the spread is about 40% of the mean (0.399). For twenty, the mean is about 200,000 and the standard deviation about 17,900. That's only about 9% of the mean (0.089). The total is 20 times bigger, but the spread is only about 4.5 times bigger. That matches , which is about 4.47.
Go deeper The square-root rule as a formula
Say each of independent amounts has standard deviation . Each variance is , so the total's variance is . Take the square root:
Read it as: "the spread of the total is one item's spread times the square root of how many there are." Here that's 4,000 × , about 17,889. The simulation gave 17,899, very close.
Show the answer
$5k
Add the variances, not the standard deviations: 9 + 16 = 25. The square root of 25 is 5, so about $5k.
Practice what you just read
- 8 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Every order value is doubled (a price change). What happens to the standard deviation?