Recipe Tableau and SQL
How do I calculate a weighted average by group?
Multiply each value by its weight, sum, and divide by the sum of the weights: SUM([price_per_kg] * [kg]) / SUM([kg]). Plain AVG gives a 100-gram order the same say as a 40-kilo one. In this tea company it doesn't just overstate the price: it ranks the regions upside down.
The data
Fernhill Tea, order lines in 2025
A tea merchant selling six teas two ways: small bags online at full price, and bulk sacks to cafés at a trade price, in three regions.
order_id | order_date | region | channel | tea | kg | price_per_kg | revenue |
|---|---|---|---|---|---|---|---|
| FT-10100 | 2025-01-01 | North | Online | Chamomile | 0.1 | 53.01 | 5.3 |
| FT-10951 | 2025-01-01 | South | Café | Darjeeling First Flush | 40 | 65.48 | 2619.2 |
| FT-11097 | 2025-01-01 | West | Online | Assam Breakfast | 0.5 | 61.57 | 30.79 |
| FT-11268 | 2025-01-01 | West | Online | Chamomile | 0.5 | 55.29 | 27.64 |
Step 1
The plain average
One number per order line, averaged. The North looks like the premium region. But a line is a 100-gram online bag or a 40-kilo café sack, and AVG counts them the same.
Tableau
AVG([price_per_kg])The xmetric engine ran this formula and got the SQL’s Price per kg column, row for row.
SQL
SELECT region, AVG(price_per_kg) AS simple_avg
FROM input
GROUP BY region
ORDER BY simple_avg DESCRows
| Region | Price per kg |
|---|---|
| North | $65 |
| West | $61 |
| South | $59 |
Step 2
Weight every line by its kilos
Price times kilos is what each line earned; divided by total kilos it's the price the region really gets per kilo. The order flips: the North is now last.
Tableau
SUM([price_per_kg] * [kg]) / SUM([kg])The xmetric engine ran this formula and got the SQL’s Kg column, row for row.
SQL
SELECT
region,
SUM(price_per_kg * kg) / SUM(kg) AS weighted_avg
FROM input
GROUP BY region
ORDER BY weighted_avg DESCRows
| Region | Kg |
|---|---|
| South | 45.89 |
| West | 42.68 |
| North | 37.23 |
Why does the North go from first to last?
Because AVG counts order lines, not tea. The North's plain average is 64.56 a kilo, the highest of the three regions; weighted by what it actually sold, it gets 37.23, the lowest. The South goes the other way, from 58.84 to 45.89. Nobody changed a price. The North simply has the most small online orders, and each one counts as much as a café's 40-kilo sack.
Step 3
Put the two side by side
The plain average is high everywhere, and by different amounts, which is why it reorders the regions. Switch Group by to channel: inside one channel the two averages almost agree. The gap comes from mixing small and big orders.
Tableau
AVG([price_per_kg]) - SUM([price_per_kg] * [kg]) / SUM([kg])
// the gap, or both measures with Measure Names on ColorSQL
SELECT
region,
'Plain AVG' AS method,
AVG(price_per_kg) AS price
FROM input
GROUP BY region
UNION ALL
SELECT
region,
'Weighted by kg',
SUM(price_per_kg * kg) / SUM(kg)
FROM input
GROUP BY region
ORDER BY 1, 2Rows
| Region | Method | Price per kg |
|---|---|---|
| North | Plain AVG | $65 |
| North | Weighted by kg | $37 |
| South | Plain AVG | $59 |
| South | Weighted by kg | $46 |
| West | Plain AVG | $61 |
| West | Weighted by kg | $43 |
Step 4
Find the cause: the order mix
Online bags are most of the order lines and almost none of the tea. The North has the most online lines, so its plain average is pulled furthest toward the online price, while its kilos go to cafés at the lowest trade price of the three regions.
Tableau
COUNT(IF [channel] = "Online" THEN [order_id] END) / COUNT([order_id])
// and SUM(IF [channel] = "Online" THEN [kg] END) / SUM([kg])The xmetric engine ran this formula and got the SQL’s Online share of lines column, row for row.
SQL
SELECT
region,
COUNT(*) FILTER (WHERE channel = 'Online')
* 1.0
/ COUNT(*) AS online_share_of_lines,
SUM(kg) FILTER (WHERE channel = 'Online')
/ SUM(kg) AS online_share_of_kg
FROM input
GROUP BY region
ORDER BY 1Rows
| Region | Online share of lines | Kg |
|---|---|---|
| North | 70% | 3.8% |
| South | 42.5% | 1.2% |
| West | 55.2% | 1.9% |
How much does it matter overall?
Online bags are 56.7% of the order lines and 2% of the tea sold. So the plain average of every line, 61.67, describes the online shop; the company actually gets 42.72 a kilo. AVG overstates the price by 44.3%. Any pricing, margin or forecast built on it starts from the wrong number.
Common mistake
AVG of a rate stored on each row
Any price, rate, percentage or score stored per row has this problem when rows carry different volumes. Weight by the volume, or divide the two totals.
Tableau
AVG([price_per_kg])SQL
SELECT region, AVG(price_per_kg) AS price
FROM input
GROUP BY regionTableau
SUM([revenue]) / SUM([kg])SQL
SELECT region, SUM(revenue) / SUM(kg) AS price
FROM input
GROUP BY regionCommon mistake
A row-level ratio, then aggregated
A calculated field [revenue] / [kg] with no aggregation is computed per row, and Tableau then sums or averages those per-row results. Put the aggregation inside the field so it divides totals at every level.
Tableau
[revenue] / [kg]
// dropped on Rows as AVGTableau
SUM([revenue]) / SUM([kg])Step 5
The grand total, three ways
Only the last bar is the price the company gets per kilo. Averaging the regions' weighted averages is closer, but still treats a small region like a big one.
Tableau
SUM([price_per_kg] * [kg]) / SUM([kg])
// Total Using: Automatic, not AverageSQL
WITH g AS (
SELECT region, SUM(price_per_kg * kg) / SUM(kg) AS w
FROM input
GROUP BY region
)
SELECT
'Plain AVG of all lines' AS method,
(
SELECT AVG(price_per_kg)
FROM input
) AS price
UNION ALL
SELECT 'Average of the group averages', AVG(w)
FROM g
UNION ALL
SELECT
'Weighted by kg',
(
SELECT SUM(price_per_kg * kg) / SUM(kg)
FROM input
)Rows
| Method | Price |
|---|---|
| Plain AVG of all lines | 61.67 |
| Average of the group averages | 41.93 |
| Weighted by kg | 42.72 |