TL;DR — A weighted average lets some numbers "count more" than others. You multiply each value by a weight, add those up, and divide by the sum of the weights:
x̄_w = (Σ wᵢxᵢ) / (Σ wᵢ). The plain (simple) mean is just the special case where every weight is equal. Weights usually represent shares (how big or how important each item is). This one idea powers GPAs, portfolio returns, class-imbalanced F1 scores, and the carbon intensity of an electricity grid.
1. Simple explanation
A simple average (mean) treats every number the same. Add them, divide by how many.
A weighted average says: some numbers matter more than others. You give each number a weight — a "how much does this count" number — and let the big weights pull the answer toward them.
Analogy — a jar of coins. Imagine you want the average value of the coins in a jar.
- The simple way: list the coin types you have (penny, dime, quarter) and average those three face values. But that ignores that you have 400 pennies and only 3 quarters.
- The weighted way: weight each coin type by how many of that coin you have. Now the average is dragged down toward the penny, because pennies dominate. This weighted number is the true average value of a coin pulled at random from the jar.
The weight is the count (or the share). Ignoring it gives you a plausible-looking but wrong answer. That mistake — averaging group summaries while ignoring group sizes — is the single most common weighted-average bug in real data work.
Everyday examples of "the weight":
| Setting | The value xᵢ | The weight wᵢ |
|---|---|---|
| GPA | grade in each course | credit hours of the course |
| Portfolio return | return of each asset | dollars invested in that asset |
| Grid carbon intensity | emission factor of each fuel | MWh that fuel generated |
| Class-weighted F1 | F1 on each class | number of examples in that class |
| Customer satisfaction | score per region | number of customers per region |
2. Diagram
WEIGHTED AVERAGE
================
values x1 x2 x3 x4
| | | |
weights w1 w2 w3 w4
| | | |
v v v v
w1*x1 w2*x2 w3*x3 w4*x4 <- multiply each pair
\ | | /
\ | | /
+------+----+----+------+
|
Σ wi*xi (weighted sum, the "top")
|
/
/ divide by
\
Σ wi (total weight, the "bottom")
|
v
x̄_w = (Σ wi*xi) / (Σ wi)
Special case: all wi equal -> x̄_w = (Σ xi)/n = simple mean
Normalized weights (pi = wi / Σ wj, so Σ pi = 1):
x̄_w = Σ pi * xi <- a weighted average IS an expected value
3. How it works
3.1 The recipe (four steps)
| Step | Action | Why |
|---|---|---|
| 1 | Line up each value xᵢ with its weight wᵢ | Every value needs its own "importance" |
| 2 | Multiply: wᵢ · xᵢ | Big weights make big contributions |
| 3 | Sum the products (top) and sum the weights (bottom) | Σ wᵢxᵢ and Σ wᵢ |
| 4 | Divide top by bottom | x̄_w = (Σ wᵢxᵢ)/(Σ wᵢ) |
3.2 Normalizing the weights
Raw weights can be any positive numbers — counts, dollars, MWh. You often normalize them so they sum to 1:
pᵢ = wᵢ / Σ wⱼ then Σ pᵢ = 1
After normalizing, each pᵢ is a share (a proportion, or probability). The weighted average becomes a plain sum: x̄_w = Σ pᵢ·xᵢ.
Why normalize?
- Comparability — shares are unit-free. "Coal is 40% of the mix" travels across grids; "1500 MWh of coal" does not.
- It's the same answer — dividing by
Σ wᵢat the end is normalizing. Doing it up front just makes the interpretation (expected value) obvious. - Stability — you scale the weights however you like (counts, percentages, fractions) and the result is identical.
Key fact: scaling all weights by the same constant does not change the weighted average. wᵢ and 1000·wᵢ give the same x̄_w. Only the ratios between weights matter.
3.3 Micro-averaging vs macro-averaging
When you have several groups and a metric per group, there are two honest ways to combine them.
| Macro-average | Micro-average | |
|---|---|---|
| Formula | simple mean of per-group metrics | pool all items, then compute once (equivalently, weight per-group metric by group size) |
| Each group counts... | equally | proportional to its size |
| Good when | every group is equally important (e.g. per-class fairness) | overall population performance matters |
| Danger | a tiny group can swing the number | a huge group can hide small-group failures |
Macro says "every group is one vote." Micro says "every item is one vote." They only agree when all groups are the same size. Neither is universally correct — you pick based on what the number is for.
3.4 Divide-by-zero guard
If Σ wᵢ = 0 (all weights zero, or the list is empty), the formula is undefined — you cannot divide by zero. Real code must check for this before dividing and decide on a policy: return NaN, raise an error, or fall back to the simple mean. Silent 0/0 is a classic production bug.
4. The math
General form:
Σ wᵢ xᵢ (i = 1 .. n)
x̄_w = ---------
Σ wᵢ
Normalized form (with pᵢ = wᵢ / Σ wⱼ, Σ pᵢ = 1):
x̄_w = Σ pᵢ xᵢ <- this is exactly E[X], an expected value
Simple mean is the equal-weight special case (wᵢ = 1 for all i):
x̄ = (Σ xᵢ) / n
4.1 Worked example A — weighted GPA
Three courses. Grade points on a 4.0 scale, weighted by credit hours.
| Course | Grade points xᵢ | Credits wᵢ | wᵢ·xᵢ |
|---|---|---|---|
| Calculus | 4.0 | 4 | 16.0 |
| History | 3.0 | 3 | 9.0 |
| Lab (1 credit) | 2.0 | 1 | 2.0 |
| Sum | 8 | 27.0 |
Weighted GPA = 27.0 / 8 = 3.375
Simple mean = (4.0 + 3.0 + 2.0) / 3 = 3.00
The weighted GPA (3.375) is higher because the 4.0 came in the heaviest course (4 credits). The simple mean throws away credit hours and understates the student. Same numbers, different question — and the weighted one is what a registrar actually reports.
The identical arithmetic gives a portfolio return: replace "grade points" with each asset's return and "credits" with dollars invested. A +10% return on $8,000 and −2% on $2,000 gives
(0.10·8000 + (−0.02)·2000)/10000 = (800 − 40)/10000 = 7.6%, not the naive(10% − 2%)/2 = 4%.
4.2 Worked example B — generation-weighted grid carbon intensity
The carbon intensity of an electricity grid is a weighted average of fuel emission factors, weighted by how much each fuel generated that hour:
Σ_f gen_f(t) · EF_f
CI(t) = --------------------------- units: gCO2/kWh
Σ_f gen_f(t)
gen_f(t) = generation (MWh) of fuel f in hour t (the weight). EF_f = lifecycle emission factor (gCO2/kWh, the value).
IPCC AR5 lifecycle emission factors (gCO2/kWh):
| Fuel | COAL | GAS | OIL | NUCLEAR | SOLAR | HYDRO | WIND | OTHER |
|---|---|---|---|---|---|---|---|---|
| EF | 820 | 490 | 650 | 12 | 48 | 24 | 11 | ~230 |
Windy night — NUC 5000, WIND 4000, GAS 1000 MWh:
top = 5000·12 + 4000·11 + 1000·490
= 60,000 + 44,000 + 490,000 = 594,000
bottom = 5000 + 4000 + 1000 = 10,000
CI = 594,000 / 10,000 = 59.4 gCO2/kWh (CLEAN)
Evening peak — GAS 6000, COAL 1500, OIL 500, NUC 5000 MWh:
top = 6000·490 + 1500·820 + 500·650 + 5000·12
= 2,940,000 + 1,230,000 + 325,000 + 60,000 = 4,555,000
bottom = 6000 + 1500 + 500 + 5000 = 13,000
CI = 4,555,000 / 13,000 ≈ 350.4 gCO2/kWh (DIRTY)
Same grid, same physics — ~6× swing (59.4 → 350.4) driven entirely by how the weights shift between clean and dirty fuels. A simple (unweighted) average of the eight emission factors would be a meaningless number that ignores what's actually running.
5. Real code
"""
Weighted averages: a robust numpy version and a from-scratch version.
Both guard against the divide-by-zero (Σw = 0) case.
"""
from __future__ import annotations
import numpy as np
def weighted_mean(values, weights, *, on_zero="nan"):
"""Weighted average (Σ w·x) / (Σ w) with a zero-weight guard.
Parameters
----------
values, weights : array-like of equal length (weights >= 0).
on_zero : what to do when Σ weights == 0:
"nan" -> return float('nan') (safe default)
"raise" -> raise ZeroDivisionError
"mean" -> fall back to the simple, unweighted mean
"""
x = np.asarray(values, dtype=float)
w = np.asarray(weights, dtype=float)
if x.shape != w.shape:
raise ValueError(f"length mismatch: {x.shape} vs {w.shape}")
if np.any(w < 0):
raise ValueError("weights must be non-negative")
total = w.sum()
if total == 0:
if on_zero == "raise":
raise ZeroDivisionError("sum of weights is zero")
if on_zero == "mean":
return float(x.mean()) if x.size else float("nan")
return float("nan")
# np.average already does (Σ w·x)/(Σ w) efficiently.
return float(np.average(x, weights=w))
def weighted_mean_scratch(values, weights):
"""Same math, pure Python, no numpy — shows the mechanics."""
top = 0.0 # Σ w_i * x_i
bottom = 0.0 # Σ w_i
for xi, wi in zip(values, weights):
if wi < 0:
raise ValueError("weights must be non-negative")
top += wi * xi
bottom += wi
if bottom == 0:
return float("nan") # undefined; caller decides policy
return top / bottom
def carbon_intensity(gen_mwh: dict[str, float]) -> float:
"""Generation-weighted grid carbon intensity, gCO2/kWh.
gen_mwh maps fuel -> MWh generated this hour (the weights).
"""
EF = { # IPCC AR5 lifecycle emission factors, gCO2/kWh
"COAL": 820, "GAS": 490, "OIL": 650, "NUCLEAR": 12,
"SOLAR": 48, "HYDRO": 24, "WIND": 11, "OTHER": 230,
}
fuels = list(gen_mwh)
ef = [EF[f] for f in fuels] # values
gen = [gen_mwh[f] for f in fuels] # weights
return weighted_mean(ef, gen, on_zero="nan")
def macro_micro(per_group_metric, group_sizes):
"""Return (macro, micro) averages of a per-group metric.
macro = simple mean of group metrics (each group counts equally).
micro = size-weighted mean (each item counts equally).
"""
macro = weighted_mean(per_group_metric,
[1] * len(per_group_metric), on_zero="nan")
micro = weighted_mean(per_group_metric, group_sizes, on_zero="nan")
return macro, micro
if __name__ == "__main__":
# GPA / portfolio-style example
print(weighted_mean([4.0, 3.0, 2.0], [4, 3, 1])) # 3.375
print(weighted_mean_scratch([4.0, 3.0, 2.0], [4, 3, 1])) # 3.375
# Grid carbon intensity
print(round(carbon_intensity(
{"NUCLEAR": 5000, "WIND": 4000, "GAS": 1000}), 1)) # 59.4
print(round(carbon_intensity(
{"GAS": 6000, "COAL": 1500, "OIL": 500, "NUCLEAR": 5000}), 1)) # 350.4
# Micro vs macro: class A F1=0.95 on 950 items, class B F1=0.50 on 50 items
macro, micro = macro_micro([0.95, 0.50], [950, 50])
print(round(macro, 3), round(micro, 3)) # 0.725 0.927 (micro = 0.9275)
# Zero-weight guard does not explode
print(weighted_mean([1.0, 2.0], [0, 0])) # nan
print(weighted_mean([1.0, 2.0], [0, 0], on_zero="mean")) # 1.5
Expected output:
3.375
3.375
59.4
350.4
0.725 0.927
nan
1.5
6. Real-world example
Scenario: reporting a model's F1 across two customer segments.
A support-ticket classifier runs for two segments in one day:
| Segment | Tickets (size) | F1 on that segment |
|---|---|---|
| Enterprise | 950 | 0.95 |
| SMB | 50 | 0.50 |
Two managers ask "what's our F1?" and get two different-but-correct numbers:
Macro-F1 (each segment equal):
(0.95 + 0.50) / 2 = 0.725
Micro-F1 (each ticket equal = size-weighted):
(0.95·950 + 0.50·50) / (950 + 50)
= (902.5 + 25) / 1000 = 0.9275
Which do you report?
- If leadership asks "how well do we serve the average customer ticket?" → micro (0.9275) — the big segment rightly dominates.
- If the fairness team asks "are we failing any segment?" → macro (0.725) — it refuses to let Enterprise hide the terrible SMB score.
The gap (0.9275 vs 0.725) is the story: the model is quietly failing SMB. A single "F1 = 0.93" headline would bury that. Same data, and the choice of weight is a product decision, not a math one.
Simpson's paradox, in one line: a trend that holds inside every group can reverse once groups are pooled with different sizes — so always check whether an aggregate hides an opposite within-group story before trusting it.
7. Interview questions companies actually ask
Q1. What's the difference between a simple mean and a weighted mean?
A simple mean gives every value equal weight (Σxᵢ/n). A weighted mean multiplies each value by a weight and divides by the total weight (Σwᵢxᵢ/Σwᵢ), so values with larger weights pull the result toward them. The simple mean is the special case where all weights are equal.
Q2. (Amazon / Meta) Explain micro vs macro F1. When do you use each? Macro-F1 averages the per-class F1 scores equally — it treats every class as equally important, so it exposes poor performance on rare classes. Micro-F1 pools all true/false positives and negatives before computing, so it's dominated by frequent classes and reflects overall population accuracy. Use macro when minority-class performance matters (fraud, disease, fairness). Use micro when you care about aggregate throughput. On balanced classes they coincide.
Q3. You have accuracy per country. How do you report one global number? You weight each country's accuracy by its number of samples (a micro / size-weighted average), because the global figure should reflect how the population of requests is served. Reporting a simple mean over countries would let a tiny country count as much as a huge one. If the goal is instead "no country left behind," report the macro (unweighted) mean too, and flag the spread.
Q4. Quick numeric: returns of +10% on $8,000 and −2% on $2,000. Portfolio return?
Weight by dollars: (0.10·8000 + (−0.02)·2000) / 10000 = (800 − 40)/10000 = 7.6%. The naive average of the two percentages (4%) is wrong because it ignores that far more money was in the winning position.
Q5. Why normalize weights so they sum to 1?
Normalized weights become unit-free shares (proportions/probabilities), which makes the weighted average an expected value Σpᵢxᵢ and makes results comparable across different total scales. It doesn't change the answer — dividing by Σwᵢ at the end already normalizes — but it clarifies interpretation and prevents unit confusion.
Q6. What breaks if the sum of weights is zero?
The formula divides by zero and is undefined. In code you must guard for it: check Σw == 0 before dividing and choose a policy (return NaN, raise, or fall back to the simple mean). Common triggers are empty inputs or a filter that removed every positive-weight item.
Q7. Does multiplying every weight by 1000 change the weighted average? No. Only the ratios between weights matter; scaling them all by a constant cancels in numerator and denominator. That's why you can freely use counts, percentages, or fractions as weights.
Q8. (Data-science screen) What is Simpson's paradox and how does it relate to weighted averages? It's when an aggregate trend contradicts the trend within every subgroup, caused by unequal group sizes acting as hidden weights. It's a warning that pooling (a size-weighted average) can mislead — always inspect group-level numbers and the weights before trusting an aggregate.
Q9. How would you compute a weighted average in a streaming/online setting?
Keep two running totals: S = Σwᵢxᵢ and W = Σwᵢ. On each new (x, w), do S += w*x; W += w. The current estimate is S/W (guarding W==0). This is O(1) memory and gives an exact weighted mean without storing the data.
8. When to use / tradeoffs
Use a weighted average when:
- Items differ in size, importance, confidence, or exposure (dollars, credits, MWh, sample counts).
- You're combining per-group summaries and the groups are unequal sizes.
- You need an expected value under a known distribution of shares.
Be careful / do NOT just weight blindly when:
| Risk | What happens | Guard |
|---|---|---|
Σw = 0 | undefined / crash | check before dividing |
| Wrong weight chosen | plausible but wrong answer (Q3) | ask "one number for what?" |
| Extreme weights | one huge item dominates; others invisible | inspect the weight distribution |
| Averaging pre-averaged data | double aggregation drops group sizes | keep raw counts, weight by them |
| Simpson's paradox | aggregate reverses subgroup truth | always look at group-level numbers |
| Negative weights | not a real average; can leave [min,max] range | require weights ≥ 0 |
Honest limit: a weighted mean is still a mean — it's sensitive to outliers and hides variance. If the distribution is skewed or multimodal, report spread (or a weighted median) alongside it. The single number is a summary, never the whole picture.
9. Summary + related articles
- A weighted average is
x̄_w = (Σwᵢxᵢ)/(Σwᵢ); the simple mean is the equal-weight case. - Weights are shares — normalize them (
pᵢ = wᵢ/Σwⱼ,Σpᵢ = 1) to read the result as an expected value. - Micro (size-weighted) vs macro (unweighted) averaging answer different questions; pick by intent, and report both when a small group might be hidden.
- Always guard
Σw = 0, keep raw group sizes, and watch for Simpson's paradox. - Grid carbon intensity
CI(t)is just a generation-weighted average of fuel emission factors — the same math as a GPA.
Related:
- Greedy Scheduling & Interval Selection
- Why Grid Carbon Intensity Varies Hour to Hour
- Probability & Statistics Foundations
Resources
- "The Art of Statistics" — David Spiegelhalter (weighting, Simpson's paradox).
- scikit-learn, Model evaluation: micro vs macro averaging — https://scikit-learn.org/stable/modules/model_evaluation.html
- NumPy
numpy.averagereference — https://numpy.org/doc/stable/reference/generated/numpy.average.html - IPCC AR5, Annex III: technology-specific lifecycle emission factors — https://www.ipcc.ch/report/ar5/wg3/
- Wikipedia, Simpson's paradox — https://en.wikipedia.org/wiki/Simpson%27s_paradox