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 (
/* 1) Product-level aggregation (within Channel + Category)
- unit prices p1, p0 as unit values (value/qty)
- price relative r = p1/p0
- base-valued current quantity: p0*q1 (needed for Paasche weights in Σ(w*r) form)
*/
SELECT
a.ProdIdx,
ISNULL(a.ORDERBYCUST,0) AS Channel,
p.Category_top,
SUM(a.QUANTITY) AS qty_akt, -- q1
SUM(b.QUANTITY) AS qty_py, -- q0
SUM(a.LINEVALUE) AS sales_val_akt, -- p1*q1
SUM(b.LINEVALUE) AS sales_val_py, -- p0*q0
SUM(a.LINEVALUE) / SUM(a.QUANTITY) AS price_akt, -- p1
SUM(b.LINEVALUE) / SUM(b.QUANTITY) AS price_py, -- p0
(SUM(a.LINEVALUE) / SUM(a.QUANTITY))
/ (SUM(b.LINEVALUE) / SUM(b.QUANTITY)) AS price_rel, -- r = p1/p0
-- p0*q1 : base price times current quantity (for Paasche weights in Σ(w*r) form)
(SUM(b.LINEVALUE) / SUM(b.QUANTITY)) * SUM(a.QUANTITY) AS base_val_q1p0
FROM DB_sales_stat a WITH (NOLOCK)
JOIN DB_sales_stat b WITH (NOLOCK)
ON a.PRODIDX = b.PRODIDX
AND ISNULL(a.ORDERBYCUST,0) = ISNULL(b.ORDERBYCUST,0)
AND b.ORDERDATE = DATEADD(year,-1,a.ORDERDATE)
JOIN Products p WITH (NOLOCK)
ON p.ProdIdx = a.ProdIdx
WHERE
a.ORDERDATE >= CAST(DATEADD(day,-10,GETDATE()) AS date)
AND (a.ORDERBYCUST IS NULL OR a.ORDERBYCUST = 1) -- manual + ecom
AND p.Category_top IS NOT NULL
AND a.QUANTITY > 0 AND b.QUANTITY > 0
AND a.LINEVALUE > 0 AND b.LINEVALUE > 0
GROUP BY
a.ProdIdx, ISNULL(a.ORDERBYCUST,0), p.Category_top
),
chcat AS (
/* 2) Totals per Channel + Category
- needed to compute shares/weights
*/
SELECT
Channel,
Category_top,
SUM(sales_val_py) AS total_sales_py, -- Σ(p0*q0)
SUM(base_val_q1p0) AS total_p0q1 -- Σ(p0*q1)
FROM t1
GROUP BY Channel, Category_top
),
weights AS (
/* 3) Weights (structures)
w_L : Laspeyres weights = base period value shares (p0*q0 share)
w_P : Paasche weights = base-valued current shares (p0*q1 share)
Both sets sum to 1 within Channel + Category
*/
SELECT
t1.Channel,
t1.Category_top,
t1.ProdIdx,
t1.sales_val_py / chcat.total_sales_py AS w_L, -- w0 = (p0*q0)/Σ(p0*q0)
t1.base_val_q1p0 / chcat.total_p0q1 AS w_P -- wP = (p0*q1)/Σ(p0*q1)
FROM t1
JOIN chcat
ON chcat.Channel = t1.Channel
AND chcat.Category_top = t1.Category_top
),
idx AS (
/* 4) Indices per Channel + Category
L = Σ(w_L * r)
P = Σ(w_P * r) (this equals Paasche when weights are (p0*q1) shares)
*/
SELECT
t1.Channel,
t1.Category_top,
SUM(weights.w_L * t1.price_rel) AS idx_Laspeyres,
SUM(weights.w_P * t1.price_rel) AS idx_Paasche
FROM t1
JOIN weights
ON weights.Channel = t1.Channel
AND weights.Category_top = t1.Category_top
AND weights.ProdIdx = t1.ProdIdx
GROUP BY
t1.Channel, t1.Category_top
)
SELECT
Channel,
Category_top,
idx_Laspeyres,
idx_Paasche,
/* Fisher = geometric mean of L and P */
SQRT(idx_Laspeyres * idx_Paasche) AS idx_Fisher,
/* Mix / structure-change influence:
- ratio form: how much changing weights (structure) moves index
- diff form: absolute difference (can be easier to read)
*/
(idx_Paasche / idx_Laspeyres) AS mix_effect_ratio,
(idx_Paasche / idx_Laspeyres) - 1 AS mix_effect_pct,
(idx_Paasche - idx_Laspeyres) AS mix_effect_diff
FROM idx
ORDER BY Channel, Category_top;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
Onciązenia Price Index
Obciążenie wynikające z substytucji dóbr (commodity substitution bias). Ob‑ciążenie to wynika ze zmian relatywnych cen poszczególnych dóbr wchodzą‑cych w skład koszyka CPI. Efekt substytucji polega na tym, że konsumenci reagują na zmiany cen przez zamianę tych dóbr lub usług konsumpcyjnych, które są relatywnie droższe, na dobra relatywnie tańsze [Hałka i Leszczyń‑ska 2011].
Obciążenie wynikające z substytucji rynku zbytu (outlet substitution bias).Obciążenie to wynika z migracji konsumentów w kierunku atrakcyjniejszych, często właśnie się pojawiających rynków dla zakupów. Takim nowym rynkiem może być np. hurtownia internetowa czy punkt sprzedaży wysyłkowej. Formuła Laspeyresa z wagami z okresu bazowego nie jest w stanie nadążać za tego typu zmianami preferencji konsumentów i nowymi kanałami dystrybucji.
Obciążenie wynikające z pojawiania się nowych dóbr (new goods bias). Źródłem tego rodzaju obciążenia są nowe dobra, z jakich w okresie objętym badaniem inflacji zaczęli korzystać konsumenci. Najczęściej są to produkty dotąd innowacyjne, powstałe na skutek wprowadzenia nowej technologii wyrobu, które weszły właśnie do powszechnego użycia. Z oczywistych przyczyn produkty te mogą w ogóle nie być uwzględnione w koszyku dóbr służących oszacowaniu CPI (przykładowo w Polsce opłaty za telefon ko‑mórkowy zaczęto uwzględniać dopiero w 2006 r.). Może też się zdarzyć, że spadek cen takich produktów znajduje odzwierciedlenie w CPI dużo później, niż faktycznie nastąpił. Co ciekawe, ocenia się, że ich liczba wy‑nosi od kilku do nawet kilkuset tysięcy w skali roku [Diewert 1996].
Obciążenie wynikające ze zmian jakości produktów (quality adjustment bias).Jest to ten rodzaj obciążenia szacunków CPI, który wynika ze zmieniającej się (np. wraz z rosnącymi oczekiwaniami klientów‑konsumentów) jakości oferowanych przez rynek dóbr. Abraham [1995] podaje tu przykład samochodów, których jakość, komfort jazdy, bezpieczeństwo, cena, a także chęć ich posiadania są dziś zupełnie inne niż tych z lat 70. Szacowanie więc inflacji dla długich odcinków czasu musi uwzględniać fakt, że udział tych dóbr w koszyku jest zupełnie inny na początku i na końcu badanego przedziału czasowego. Z definicji wskaźnik CPI powinien mierzyć zmiany cen towarów i usług przy założeniu, że ich cechy nie uległy zmianie w stosunku do okresu bazowego. W rzeczywistości jednak produkty z koszyka dóbr ulegają zmianom – są ulepszane, modyfikowane, a często po prostu wycofywane [Hałka i Leszczyńska 2011].
Obciążenie wynikające z metody kalkulacji (formula bias). Obciążenie to, nazywane także elementarnym obciążeniem indeksu (elementary index bias[White 1999]) może powstać jako efekt zastosowania danej metody obliczeń na najniższym poziomie agregacji.
W przypadku gdy do kalkulacji wskaźnika ceny danego produktu używana jest średnia arytmetyczna ze wszystkich wskaźników cen danego dobra w kolejnych punktach notowań, wskaźnik cen będzie przeszacowany [Hałka i Leszczyńska 2011]. Jeśli natomiast najpierw najpierw liczona jest średnia cena dobra dla danego okresu ze wszystkich punktów notowań, a następnie jest ona odnoszona do średniej ceny tego
dobra w poprzednim okresie, wskaźnik cen nie powinien wykazywać tego rodzaju obciążenia [Ducharme 2000].
https://gnpje.sgh.waw.pl/pdf-100882-33049?filename=Consumer-Price-Index-Meas.pdf
Przykład: Masz dwa sposoby liczenia „indeksu ceny” dla tego samego dobra obserwowanego w kilku punktach notowań (np. sklepy A, B, C):
Metoda 1 (która daje przeszacowanie): średnia arytmetyczna ze wskaźników cen
Czyli najpierw liczysz dla każdego sklepu „procentową zmianę ceny”, a potem uśredniasz:
ICarli=n1j=1∑np0,jp1,j
To jest klasyczny „Carli index” (arytmetyczna średnia relacji cen).
Metoda 2 (która nie ma tego obciążenia w tym sensie): najpierw średnia cena, potem relacja
Czyli najpierw uśredniasz ceny w okresie 0 i 1, a potem robisz iloraz:
IDutot=n1∑j=1np0,jn1∑j=1np1,j
To jest „Dutot index” (relacja średnich cen).
Klucz: Carli uśrednia procenty, a Dutot uśrednia poziomy cen.
Przykład 1 (najbardziej obrazowy): “tani sklep drożeje mocno, drogi tanieje trochę”
Mamy 2 sklepy sprzedające ten sam produkt.
Okres 0 (rok temu)
- Sklep A: 10 zł
- Sklep B: 100 zł
Średnia cena w okresie 0:
pˉ0=210+100=55
Okres 1 (teraz)
- Sklep A: 20 zł (wzrost o 100%)
- Sklep B: 90 zł (spadek o 10%)
Średnia cena w okresie 1:
pˉ1=220+90=55
✅ Metoda 2: „średnia cena → relacja” (Dutot)
IDutot=5555=1.00⇒0%
Wniosek: średnia cena produktu się nie zmieniła.
❌ Metoda 1: „średnia relacji cen” (Carli)
Najpierw relacje cen w sklepach:
- A: 20/10=2.00
- B: 90/100=0.90
Średnia relacji:
ICarli=22.00+0.90=1.45⇒+45%
Wniosek: Carli mówi „+45%”, mimo że średnia cena w złotych się nie zmieniła.
➡️ To jest dokładnie ten “formula bias / przeszacowanie”: duży wzrost procentowy w tanim punkcie dostaje taką samą wagę jak mała zmiana w drogim punkcie, mimo że drogi punkt „ciąży” dużo mocniej w poziomie cen.
Dlaczego to jest „przeszacowanie” intuicyjnie?
Carli daje każdemu punktowi notowań taką samą wagę w %, niezależnie czy sklep miał cenę 10 czy 100.
- +100% na 10 zł to +10 zł
- -10% na 100 zł to -10 zł
W złotych te zmiany się znoszą, dlatego Dutot wychodzi 0%.
Carli patrzy na procenty i „widzi” 100% i -10%, więc wychodzi mu dodatnio.
Przykład 2 (mniej ekstremalny, ale nadal pokazuje mechanizm)
Okres 0
- A: 10 zł
- B: 20 zł
pˉ0=15
Okres 1
- A: 11 zł (+10%)
- B: 18 zł (-10%)
pˉ1=14.5
Dutot:
IDutot=1514.5=0.9667⇒−3.33%
Carli: Relacje: 11/10=1.10 i 18/20=0.90
ICarli=21.10+0.90=1.00⇒0%
➡️ Znowu: Carli jest „wyżej” niż Dutot (tu zawyża o 3.33 p.p. względem tej interpretacji).
Jak to się ma do Twoich danych (sprzedaż, ilości, wartości)?
W Twoim świecie „punkty notowań” mogą być np.:
- różne kanały (manual vs ecom),
- różne sklepy/oddziały,
- różne warunki transakcji.
Jeśli liczysz indeks na najniższym poziomie jako:
- średnią % zmian (Carli) → ryzyko przeszacowania,
- relację średnich cen (Dutot) → mniejsze ryzyko tego konkretnego obciążenia.
A w transakcjach sprzedażowych często jeszcze lepsze jest ważenie (np. ilościami lub wartościami), ale to już kolejny poziom metodologii.
p0 = [10, 100]
p1 = [20, 90]
carli = sum([p1[i]/p0[i] for i in range(len(p0))]) / len(p0)
dutot = (sum(p1)/len(p1)) / (sum(p0)/len(p0))
print("Carli index:", carli, "=>", (carli-1)*100, "%")
print("Dutot index:", dutot, "=>", (dutot-1)*100, "%")
Mini‑podsumowanie „w jednym zdaniu”
- Carli (średnia relacji) = średnia procentów → potrafi zawyżać, gdy poziomy cen w punktach są różne.
- Dutot (relacja średnich) = relacja średnich cen → nie ma tego typu zawyżenia w opisanym sensie.
