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

  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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *