Price Index
There are established, “official” ways to aggregate product‑level price changes into a category‑level average price increase, and they come straight from the price index literature used by statistical offices (CPI/HICP/PPI). The key point is: an “average % change” at category level is not usually a simple mean of product % changes; instead you compute a price index (typically a Laspeyres‑type, often chain‑linked), because the index answers a specific economic question and behaves well under aggregation. [imf.org], [ec.europa.eu]
Below I’ll (1) summarize what manuals say, (2) show the best formulas for your exact data (date, product_id, sales value, quantity), (3) explain what to avoid, and (4) give ready-to-use SQL/Python patterns.
Why this matters
With your data you can compute transaction unit prices (value/quantity) and then construct a category price index. The “right” average depends on what you mean:
- “How much would last year’s basket cost at this year’s prices?” → Laspeyres (base-period weights).
- “How much would this year’s basket have cost at last year’s prices?” → Paasche (current weights).
- Best symmetric compromise (often recommended in theory) → Fisher Ideal = √(Laspeyres × Paasche).
Practical recipe
Assumptions:
- You have a mapping
product_id -> category_id - You want YoY for a given month
tvst-12 - You can compute
p_it = sum(value)/sum(qty)at product-month
Step-by-step
- Aggregate transactions to product-month:
value_it = SUM(sales_value)qty_it = SUM(quantity)p_it = value_it / qty_it
- Join month
twith montht-12on product_id. - Compute weights and index:
- For Laspeyres: weight =
value_i0 / SUM(value_i0)within category - Index =
SUM(weight * (p_it/p_i0))
- For Laspeyres: weight =
- Convert to percent:
%Δ = (Index - 1) * 100
This matches the weighted aggregation logic used in official price indices. Price index.
Calculation
For each product i, month t, you can compute a unit value price:
This “unit value” approach is standard when you have value + quantity, and it is widely used as a proxy for price movement in official contexts (e.g., trade statistics). Then define the product price relative (YoY for the same month):
Price index on category level
Let the category contain products . You need a single number such that:
This index will show price changes on category level, as just a one figure, ex 10% – easy to interpret.
Price index calculation example of 3 products is demonstrated here

and a SQL code for Price index calculation
WITH t1 AS (
SELECT
a.ProdIdx,
SUM(a.[QUANTITY]) AS qty_akt,
SUM(b.[QUANTITY]) AS qty_py,
SUM(a.[LINEVALUE]) AS sales_val_akt,
SUM(b.[LINEVALUE]) AS sales_val_py
FROM sales_stat a WITH (NOLOCK)
INNER JOIN sales_stat b WITH (NOLOCK)
ON a.PRODIDX = b.PRODIDX
AND b.ORDERDATE = DATEADD(year,-1,a.ORDERDATE)
WHERE a.ORDERDATE >= CAST(DATEADD(day,-10,GETDATE()) AS date)
GROUP BY a.ProdIdx
),
t2 AS (
SELECT
t1.*,
sales_val_akt / NULLIF(qty_akt,0) AS price_act,
sales_val_py / NULLIF(qty_py,0) AS price_py,
( (sales_val_akt / NULLIF(qty_akt,0)) / NULLIF((sales_val_py / NULLIF(qty_py,0)),0) ) AS price_rel
FROM t1
WHERE ISNULL(qty_akt,0) > 0
AND ISNULL(qty_py,0) > 0
AND ISNULL(sales_val_py,0) > 0
AND ISNULL(sales_val_akt,0) > 0
),
t3 AS (
SELECT
p.Category,
/* 1) Twoja metryka: unit value (miks + cena) */
SUM(t2.sales_val_akt) / NULLIF(SUM(t2.qty_akt),0) AS unit_price_act,
SUM(t2.sales_val_py) / NULLIF(SUM(t2.qty_py),0) AS unit_price_py,
100.0 * (
(SUM(t2.sales_val_akt) / NULLIF(SUM(t2.qty_akt),0)) /
NULLIF((SUM(t2.sales_val_py) / NULLIF(SUM(t2.qty_py),0)),0)
- 1
) AS unit_value_yoy_pct,
/* 2) Laspeyres: “czysty” indeks cen z wagami z PY (sales_val_py) */
100.0 * (
SUM( (t2.sales_val_py) * t2.price_rel ) / NULLIF(SUM(t2.sales_val_py),0)
- 1
) AS laspeyres_yoy_pct,
/* 3) Paasche: wagi z AKT (sales_val_akt) */
100.0 * (
SUM( (t2.sales_val_akt) * t2.price_rel ) / NULLIF(SUM(t2.sales_val_akt),0)
- 1
) AS paasche_yoy_pct,
/* 4) Fisher = sqrt(L*P) */
100.0 * (
SQRT(
(SUM( (t2.sales_val_py) * t2.price_rel ) / NULLIF(SUM(t2.sales_val_py),0)) *
(SUM( (t2.sales_val_akt) * t2.price_rel ) / NULLIF(SUM(t2.sales_val_akt),0))
)
- 1
) AS fisher_yoy_pct
FROM t2
INNER JOIN Products p WITH (NOLOCK)
ON p.prodidx = t2.prodidx
GROUP BY p.Category_top
)
SELECT *
FROM t3
ORDER BY Category;about CTE, you can read here: How Works Recursive CTE in SQL Server?
O co chodzi z „co widzimy na kasie” vs „czy to realna podwyżka cen”?
„Co widzimy na kasie” = unit_value_yoy_pct
To jest zmiana średniej ceny jednostkowej, liczona z Twoich danych jako:
- cena_akt_kat = SUMA sprzedaży / SUMA ilości
- cena_py_kat = SUMA sprzedaży rok temu / SUMA ilości rok temu
- różnica YoY = (cena_akt_kat / cena_py_kat − 1)
Czyli to odpowiada na pytanie:
„Ile średnio (ważone ilościami) płaci klient za sztukę w tej kategorii w tym miesiącu vs rok temu?”
To jest to, co „widać na kasie” / w raportach finansowych, bo bierze to, co faktycznie się sprzedało.
Problem: ta miara miesza dwa efekty:
- zmianę cen produktów
- zmianę miksu sprzedaży (czyli tego, które produkty sprzedawały się bardziej)
„Czy to realna podwyżka cen?” = laspeyres_yoy_pct (oraz fisher)
Laspeyres odpowiada na pytanie:
„Gdyby klienci kupowali w tym miesiącu dokładnie taki sam koszyk produktów jak rok temu (te same udziały), to o ile zmieniłyby się koszty tego koszyka z powodu cen?”
Czyli: izolujesz efekt cen, trzymając „miks” stały (taki jak w poprzednim roku).
Dlatego to jest częściej KPI dla „inflacji cen” w kategorii.
Mini-przykład, który pokazuje różnicę
Masz kategorię z 2 produktami:
- Produkt A: tani
- Produkt B: drogi
Rok temu (PY)
- A: cena 10, sprzedano 90 szt. → wartość 900
- B: cena 100, sprzedano 10 szt. → wartość 1000
Razem: 1900 zł, 100 szt.
Średnia cena = 1900 / 100 = 19 zł
Teraz (AKT) – ceny się nie zmieniły, ale miks tak
- A: cena 10, sprzedano 50 szt. → 500
- B: cena 100, sprzedano 50 szt. → 5000
Razem: 5500 zł, 100 szt.
Średnia cena = 5500 / 100 = 55 zł
Co pokaże Twoje unit_value_yoy_pct?
(55 / 19 − 1) = +189%
Czyli „na kasie” wygląda, jakby ceny w kategorii eksplodowały…
ale ceny A i B się nie zmieniły ani o grosz.
To wzrost jest tylko dlatego, że klienci kupili dużo więcej produktu droższego (B).
To jest właśnie efekt miksu.
Co pokaże Laspeyres w tym samym przykładzie?
Laspeyres trzyma wagi z poprzedniego roku (PY). Skoro ceny się nie zmieniły:
- relacja cen A: 10/10 = 1
- relacja cen B: 100/100 = 1
Zatem indeks Laspeyresa = 1 → 0% zmiany cen.
I to jest „realna podwyżka cen” (w sensie: zmiana cenników / stawek), a nie zmiana struktury sprzedaży. Price index
Price index
Co robić w praktyce?
Jeżeli raport jest „dla biznesu” (zarząd / sprzedaż / controlling), to zwykle najlepiej pokazywać:
- unit_value_yoy_pct jako „co widać w średniej cenie sprzedaży”
- laspeyres_yoy_pct jako „prawdziwy KPI podwyżek cen”
A obok można dopisać interpretację:
Różnica między unit_value a Laspeyres to efekt miksu
(np. więcej droższych produktów / więcej premium / mniej promek / inne pack size)
mix_effect
Możemy policzyć prostą miarę:
- Mix effect (w p.p.) = unit_value_yoy_pct − laspeyres_yoy_pct
To nie jest „idealna dekompozycja ekonomiczna”, ale w praktyce działa świetnie jako sygnał:
- dodatnie → miks poszedł w stronę droższych produktów
- ujemne → miks poszedł w stronę tańszych/promocyjnych
