Price index

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 t vs t-12
  • You can compute p_it = sum(value)/sum(qty) at product-month

Step-by-step

  1. Aggregate transactions to product-month:
    • value_it = SUM(sales_value)
    • qty_it = SUM(quantity)
    • p_it = value_it / qty_it
  2. Join month t with month t-12 on product_id.
  3. Compute weights and index:
    • For Laspeyres: weight = value_i0 / SUM(value_i0) within category
    • Index = SUM(weight * (p_it/p_i0))
  4. 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:

pi,t=SalesValuei,tQuantityi,tp_{i,t}=\frac{\sum \text{SalesValue}_{i,t}}{\sum \text{Quantity}_{i,t}}

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):

ri=pi,tpi,t12r_i=\frac{p_{i,t}}{p_{i,t-12}}

Price index on category level

Let the category contain products iCi \in C. You need a single number ICI_C​ such that:

%ΔC=(IC1)×100%\%\Delta_C = (I_C – 1)\times 100\%

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

Price index on category level
Price index on category level

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:

  1. zmianę cen produktów
  2. 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ć:

  1. unit_value_yoy_pct jako „co widać w średniej cenie sprzedaży”
  2. 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=1nj=1np1,jp0,jI_{\text{Carli}}=\frac{1}{n}\sum_{j=1}^n \frac{p_{1,j}}{p_{0,j}}ICarli​=n1​j=1∑n​p0,j​p1,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=1nj=1np1,j1nj=1np0,jI_{\text{Dutot}}=\frac{\frac{1}{n}\sum_{j=1}^n p_{1,j}}{\frac{1}{n}\sum_{j=1}^n p_{0,j}}IDutot​=n1​∑j=1n​p0,j​n1​∑j=1n​p1,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=10+1002=55\bar p_0=\frac{10+100}{2}=55pˉ​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=20+902=55\bar p_1=\frac{20+90}{2}=55pˉ​1​=220+90​=55


✅ Metoda 2: „średnia cena → relacja” (Dutot)

IDutot=5555=1.000%I_{\text{Dutot}}=\frac{55}{55}=1.00 \Rightarrow 0\%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.0020/10 = 2.0020/10=2.00
  • B: 90/100=0.9090/100 = 0.9090/100=0.90

Średnia relacji:

ICarli=2.00+0.902=1.45+45%I_{\text{Carli}}=\frac{2.00+0.90}{2}=1.45 \Rightarrow +45\%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\bar p_0 = 15pˉ​0​=15

Okres 1

  • A: 11 zł (+10%)
  • B: 18 zł (-10%)
    pˉ1=14.5\bar p_1 = 14.5pˉ​1​=14.5

Dutot:

IDutot=14.515=0.96673.33%I_{\text{Dutot}}=\frac{14.5}{15}=0.9667 \Rightarrow -3.33\%IDutot​=1514.5​=0.9667⇒−3.33%

Carli: Relacje: 11/10=1.10 i 18/20=0.90

ICarli=1.10+0.902=1.000%I_{\text{Carli}}=\frac{1.10+0.90}{2}=1.00 \Rightarrow 0\%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.

Similar Posts