Key Performance Index: Sarah, Bob, Jack és Emma története

Meszaros Zoltan Gabor
9 min readApr 14, 2024

--

Korábbi bejegyzésemben időjárási adatokon, egy elméleti problémát közelítettünk Egyszerű Mozgó Átlag segítségével. Vegyünk most egy gyakorlati, üzleti környezetből vett példát, fókuszálva eredményeink mérésére.

🚀Ha értékesnek találod a leírtakat, és szükségét látod egy hasonló elemzés elkészítésének, vagy adatelemzői munkakörbe keresel szakembert, kérlek keress meg alábbi Linkedin profilon (24) Zoltán Mészáros | LinkedIn vagy a mzg.datateam[kukac]gmail.com címen.

📨Kedves Adat Csapat!

Budapesti üzletünk értékesítési előrejelzésében kérünk tőletek segítséget. A BestDeal_01 kódjelű boltunk számára készítsetek egy gyors értékesítési kalkulációt az elkövetkező hétre. Mutassátok be az eredményt, külön kiemelve a kalkuláció bizonytalanságát.

Key Objectives:
✅Bolti előrejelzés elkészítése
✅Bizonytalansági mérőszámok (KPI)
✅Következő lépések

01. Elemzési csomagok importálása

A szokásos adatmanipulációs eszközökön felül, az sklearn gépi tanulást támogató csomagjából importáljuk a leggyakoribb metrikákat. Bár statisztika tudománykörében mozgunk, kiválóan használható a library visszamérésre. A metrikák “kézi számítása” jó gyakorlat, akár Pythonban akár Excelben legalább egyszer érdemes összehasonlító elemzést végezni egy kisebb adathalmazon így ellenőrizve a metodikát.

# Adatmanipuláció, ábrázolás
import pandas as pd
import numpy as np
import seaborn as sns
import plotly.matplotlylib as plt
import math

# Metrikák betöltése
from sklearn.metrics import mean_absolute_error as mae
from sklearn.metrics import mean_absolute_percentage_error as mape
from sklearn.metrics import mean_squared_error as mqe
from sklearn.metrics import median_absolute_error as medae

01. Adatok betöltése

Feladatunk bolti értékesítés predikciója, amit Kaggle-n elérhető Walmart adathalmazból készítünk egy véletlen választott boltkódra. Ebben heti értékesítési adatok képeznek egy sort. Pandas csomag segítségével importálunk, a dátum változót konvertáljuk.

https://www.kaggle.com/datasets/yasserh/walmart-dataset

df = pd.read_excel("raw_data_store_01.xlsx")
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
01. ábra: BestDeal_01 boltunk heti értékesítési adatai

02. Előrejelzés periódus eltolással

Elkészítjük a mozgóátlagot, a korábbi bejegyzéstől eltérő módon felhasználva Pandas adatmanipulációs csomagját.

# Mozgóátlag ablak
period = 3

# Mozgóátlag kalkulálás paraméterben rögzített időablakban
df['forecast'] = df['demand'].rolling(window=period).mean()

Fontos: a mozgóátlag pandas könyvtárból történő meghívásával még predikciót nem készítettünk, hanem végeztünk egy simítást az adatokon, ami fontos lépés az idősor komponenseinek (trend, szezonalitás, ciklikus tulajdonság, outlierek, véletlen ingadozások) megértésében, de ebben az esetben egy héttel későbbi időpontról szeretnénk elmondani valamit, ezért egy egységgel elcsúsztatjuk a forecast oszlopot a demand oszlophoz képest.

# Forecast elcsúsztatása egy dátummal előbbre
df['forecast'] = df['forecast'].shift(1)
03. ábra: idősor forecast oszlopának elcsúsztatása egységgel

A mozgóátlag “rövidtávú memória” jellemzőjére vagyunk kíváncsiak, ezért a demand tényadat oszlop utolsó értékeinek átlagát kell vennünk. Annyi darabot, amennyi periódust választottunk időablaknak. Ezt korábban a period változóban kódoltuk.

# Átlag generálása időablak nagyságban az adatsor végén
last_period_demand_avg = df['demand'].tail(period).mean()
last_period_demand_avg

Megvan az első “Key Objective”, képesek vagyunk egy héttel előre értékesítési mennyiséget jelezni. Fejezzük be a gondolatmenetet, és készítsük el a teljes forecast oszlopot. A 143-as indexű előrejelezéssel: 690.596,67 egység.

# DataFrame készítése forecast oszloppal
x = pd.DataFrame({'forecast': df['forecast']})

# Átlagérték beszúrása új sorként
x.loc[len(x)] = [last_period_demand_avg]
x

Előrejelzett érték kiszűrése egy külön DataFrame-be:

# Utolsó érték szűrése, hogy csak a predikált értéket kapjam vissza
last_row_df = x.tail(1)

# kerekítése két tizedesjegyre
last_row_df.round(2)

03. Adatok ábrázolása

Vizsgáljuk meg egy ábrán, az előrejelzésünk hogyan működik, ehhez vázoljuk fel a demand és forecast vonaldiagramokat. Szükséges előkészítő lépések:

# Demand oszlopok és shifted forecast összekapcsolása
x = pd.DataFrame({'demand': df['demand'], 'forecast': df['forecast']})

# Új sor beszúrása (demand utolsó értéke NaN értéket kap)
x.loc[len(x)] = [np.nan, last_period_demand_avg]
x

Rajzoljuk fel a diagramot, melyben az utolsó érték, mint forecast ábrázolásra kerül:

# Adatok felkészítése
x_reset_index = x.reset_index()

# Ábra létrehozása
plt.figure(figsize=(10, 6))

# Vonaldiagramok kirajzolása
plt.plot(x_reset_index['index'], x_reset_index['demand'], label='Kereslet')
plt.plot(x_reset_index['index'], x_reset_index['forecast'], label='Forecast')

# Y tengely formázása valós értékekre
formatter = EngFormatter(unit='', places=2)
plt.gca().yaxis.set_major_formatter(formatter)

# Ábra címe
plt.title('Kereslet vs Forecast')

# X tengely címe
plt.xlabel('Index')

# Y tengely címe
plt.ylabel('Kereslet')

# Vonaljelzések hozzáadása
plt.legend()

# Ábra megjelenítése
plt.show()

Az ábrán azonnal feltűnnek az idősor komponensei, látható egy erősebb szezonalitás, kiugró értékek, ugyanakkor erősebb lineáris trend nem figyelhető meg. Ezeket a tulajdonságokat egy komplexebb modellel leszünk képesek megragadni a későbbiekben, de jó lehetőség, hogy hibametrikákkal azonosítsuk egyszerűbb modellünk előnyeit/hátrányait.

08. ábra: idősorok ábrázolása

03. Teljesítmény mérőszámok

Saját alapvetésem idősorok kapcsán: Csak annyiban bízzunk modellünkben, amennyire nem bízunk a jövőben.

Miért mérünk? Predikciós modellünk megalkotása során számolunk valamilyen pontosságot. Egyszerű kérdésre keressük a választ: mennyiben bízhatunk modellünkben.
Ennek a bizonytalanságnak két fő formája van.

a, Hiba (error, risk): adhatunk egy becslést, mely a nyers adatok természetéből és az elemzés során alkalmazott technikai eszközök együtteséből eredő számszerűsíthető hibatényező.
Adatvezérelt megközelítés, melynek során azt vizsgáljuk a múltban a modell mennyire működött megfelelően.

b, Bizonytalanság: Másrészt tudatában kell lennünk annak, hogy modellünk a valóság egyszerűsítése, ezért lehetnek nem várt, nem kalkulált tényezők, melyek modellvilágunkon kívülről érkeznek. Nehezen kvantifikálhatók.
Ez egy feltételezés-vezérelt megközelítés: modellünk működését alakító változók a jövőben is fennállnak-e?

A témában ajánlom:

(668) How do mathematical models help predict the future? — with Erica Thompson — YouTube

Ezen a ponton kvantifikálható mérőszámokat keresünk. Egy-egy aggregált értéket, ami kifejezi a modell pontosságát. Sajnos nem lehet azt mondani egy érték tökéletesen, minden szituációban meg fog felelni, így ha KPI mérőszámokról beszélünk, általában több index együtt fog megfelelő betekintést nyújtani.
Minden mérőszámnak vannak előnyei és hátrányai.

A kiindulási pont: vizsgálni fogjuk minden mérési ponton a hibát, ami egyenlő az előrejelzett érték mínusz a valós értékkel.

Képzeljünk el egy adatelemzői csapatot, ahol érvek és ellenérvek ütköznek a teljesítmény mérőszámok használata mellett/ellen.

Sarah: vizsgáljuk meg a hibáink átlagát és az aggregátum irányát!

Sarah: Pontosság (Accuracy) és Bias (Torzítás) fontossága

“Először is tegyünk különbséget a hibáink két jellemzője között, melyek együtt érvényesülnek. Az első típusba tartozó mérőszámok mutatják meg a pontosságot (accuracy), ezeket az sklearn csomag metrics funkcióival érjük el. A mért és valós értékek közötti különbségek nagyságáról van szó. Képet kapunk arról milyen nagyon hibázunk, de nem látjuk annak irányát.

Itt jön képbe a torzítás (bias), mivel ez megmutatja a történeti adatsorokon keletkező átlagos hibatényező irányát.
Képzeljünk el egy íjászmérkőzést.
- A bal felső ábrán a játékos pontosan lő, de konzisztensen rosszul céloz. A találatok szórása kicsi, de a célpont közepétől távol esnek a találatok.
- A jobb felső eredménylap a legproblémásabb: A játékos pontatlan abban, hogy a célközepet elvéti, ahová lő, ott pedig szintén pontatlan. Ezt a legnehezebb a jó irányba kalibrálni.
- A bal alsó játékos az, akire tennénk fogadásunkat: pontos és torzítatlan eredményt teljesít, ezt keressünk elemzéseinkben.
- Végül marad a jobb alsó eredménylap: Az íjász nagyjából belőtte a céltábla közepét, de ezt elég nagy szórással hajtotta végre.”

09. ábra: A pontos és pontatlan íjász esete az adatelemzővel

Sarah álláspontja a következő: nézzük meg a torzítás mértékét, oly módon, hogy a hibák átlagait vesszük, illetve ennek relatív változatát, melynek segítségével a torzítás nagyságát meg tudjuk ítélni az alaphalmaz nagyságrendjének ismerete nélkül.
Egy kiegészítés: bias vizsgálatakor érdemes a teljes időszakot kisebb egységekre bontani, a torzítás mely időszakokban milyen nagyságú és irányú.

Dobjuk el a null értékeket, ami a mozgóátlag számolásból fakad, hogy csak teljes tény/becsült adatpárral rendelkezünk.

metrics_raw = x.dropna()

Mentsük el oszlopainkat egy változóba:

forecast = metrics_raw['forecast']
demand = metrics_raw['demand']
mean_demand = np.mean(demand)
# Sarah: vizsgáljuk meg a hibáink átlagát és azok irányát!
bias = np.mean(forecast - demand)
rel_bias = bias/mean_demand

Bob: Nézzük meg a hibaszázalékaink átlagát!

Bob: Mean Absolute Percentage Error

“Sarah megközelítése kiváló a torzítás fontossága kapcsán. Amire figyelni kell hogy a torzítás (bias) mint mérőszám alkalmazása esetén sok negatív és pozitív eredmény értéke együttesen nulla lehet, amit megtévesztő eredményt ad. Elkerülendő, a hibatényezők eloszlását ábrázoljuk egy reziduális diagramon. Kis torzítást mutató érték adhat fals eredményt ezért ez tovább vizsgálandó, ugyanakkor nagy torzítás egyértelműen valamilyen problémára utal.
Számoljuk ki a hibatényezőket, majd ábrázoljunk:

metrics_raw['error'] = metrics_raw['forecast'] - metrics_raw['demand']

plt.grid(False)
sns.scatterplot(data=metrics_raw, x="demand", y="error", color="white",marker = 'o', edgecolor = 'black')
plt.axhline(y=0, linestyle='--', color='red')

# Címkék hozzáadása
plt.xlabel('Kereslet')
plt.ylabel('Hiba')
plt.title('Reziduálisok megoszlása')

# X tengely skála beállítása nullától
plt.xlim(0, max(metrics_raw['demand']))

# Diagram kirajzolása
plt.show()
10. ábra: Reziduális diagram

Emlékezzünk vissza az íjász példára. Adataink jelentős része 0 hibatényező körül szóródik. Feltűnnek az outlierek melyek a kiugró értékek hibatényezői a vonaldiagramon. Az ábra megmutatja, hogy a torzítás csekély, enyhén negatív irányú. Különbség a szemléltetésre szolgáló céltábla ábrától, hogy a jó eredmény az ha a piros vonal körül egyenletesen szóródnak az eredmények annak hosszában, nem csoportosulnak, vagy mutatnak bármilyen trendszerű összefüggést. Ezt szemléltetve távolítsuk el az outliereket, majd egy trendfüggvényt felrajzolva azonnal feltűnik a pontok jellemző vonulási iránya.”

# query_data
metrics_raw['error_abs'] = metrics_raw['error'].abs()
metrics_raw_query = metrics_raw.query('error_abs < 200000')

# query_data
metrics_raw['error_abs'] = metrics_raw['error'].abs()
metrics_raw_query = metrics_raw.query('error_abs < 200000')
metrics_raw_query
plt.grid(False)
sns.scatterplot(data=metrics_raw_query, x="demand", y="error", color="white",marker = 'o', edgecolor = 'black')
plt.axhline(y=0, linestyle='--', color='red')

# Címkék hozzáadása
plt.xlabel('Kereslet')
plt.ylabel('Hiba')
plt.title('Reziduálisok megoszlása')

# X tengely skála beállítása nullától
plt.xlim(0, max(metrics_raw_query['demand']))

# Diagram kirajzolása
plt.show()
11. ábra: Reziduálisok megoszlása outlierek nélkül

Bob javaslata egy, egy üzleti szempontból is könnyen értelmezhető mérőszám: vegyük hibáink abszolút értékeit, nézzük meg ez hány százaléka a valós értéknek egyesével, majd vonjunk közös átlagot.”

# Bob: Nézzük meg a hibaszázalékaink átlagát!
mape = mean_absolute_percentage_error(demand,forecast)

Az eredmény egy könnyen értelmezhető mérőszám, mely megadja hány százalékot hibáztunk összességében.

Jack: Vizsgáljuk meg hibáink abszolút értékeinek átlagát.

Jack: Mean Absolut Error

Bob által javasolt mérőszám üzleti kommunikációban értelmezhető és hasznos, de a modell teljesítményelemzésekor rejthet hibákat.
Ha 0 értéket felvehet a célváltozó akkor ott nem tudjuk értelmezni, nullával kellene osztanunk a százalék számítás során. Másrészt a hiba mértéke függ az értékkészlet nagyságától: ha az értékkészlet szórása nagy a MAPE nem értelmezhető általánosan, mivel ezeknek a kiugró értékeknek túlságosan nagy súlyt ad, akkor is ha ez csak egy-egy adatpont hibájából ered. Ettől még a modellünk működhet általánosságban jól. Összefoglalva a modell hangoláshoz a MAPE számolás nem tanácsos, kivéve ha ez az iparági standard.
Javaslatom KPI-ra a hibaértékek abszolút értékeinek az átlaga, illetve ennek a relatív %-os formája.

# Jack: Vizsgáljuk a hibáink abszolutértékének átlagát!
mae = mean_absolute_error(demand,forecast)
rel_mae = mae/mean_demand

Emma: Számoljuk ki hibáink négyzeteinek átlagát (MSE) majd gyökvonással hozzuk értelmezhetőbb formába (RMSE), illetve ennek relatív formáját számoljuk.

Emma: Root Mean Squared Error

Vegyük a négyzetre emelt hibáink átlagát (MSE) majd vonjunk belőle gyököt (RMSE) illetve ha ezt osztjuk a tényadatok átlagértékeivel megkapjuk ennek relatív formáját. Értelmezhetősége nehéz, viszont sok ML algoritmus használja optimalizálásra. Sok ezek közül az MSE-t preferálja az RMSE helyett, mivel az MSE gyorsabban kiszámítható és könnyebben kezelhető. Probléma, hogy nem az eredeti hibához van skálázva (mivel a hiba négyzetre van emelve), ami egy olyan KPI-t eredményez, amelyet nem tudunk az eredeti skálához viszonyítani. Ezért kevésbé fogjuk használni statisztikai modelljeink értékelésére.

# Natasa: Nézzük meg a hibáink négyzeteinek átlagát.
mse = mean_squared_error(demand,forecast)
rmse = math.sqrt(mse)
rel_rmse = rmse / mean_demand

04. KPI kiválasztása

Összefoglalva az eredményeket nézzük meg melyik KPI vagy KPI család lesz az a mérőszám amit használni fogunk értékelésre:

# KPI
metrics_table = pd.DataFrame({
'bias': [bias],
'rel_bias': [rel_bias],
'mape': [mape],
'mae': [mae],
'rel_mae': [rel_mae],
'mse': [mse],
'rmse': [rmse],
'rel_rmse': [rel_rmse]})

metrics_table.round(3)
12. ábra: KPI eredménytábla

Mint látható van miből választani, és nincs egyértelmű recept arra melyik értékelési módszer a legjobb. Ezen a ponton már kapcsolódunk optimalizálási kérdésekhez, ezért csak a fő különbségeket szeretném kiemelni, a mögöttes matekot pedig akkor amikor megkísérelünk egy KPI-ra a gyakorlatban is optimalizálni.
Kezdésnek vizsgáljuk meg a tényadatok eloszlását, rajzoljuk fel a medián és átlag értékeket.

metrics_raw["demand"].hist(alpha = 0.5)
mean_demand = metrics_raw["demand"].mean()
median_demand = metrics_raw["demand"].median()
plt.axvline(mean_demand, color='green', linestyle='dashed', linewidth=3, alpha=1, label=f'Átlag: {mean_demand:.2f}')
plt.axvline(median_demand, color='grey', linestyle='dashed', linewidth=3, alpha=1, label=f'Medián: {median_demand:.2f}')

# Címkék hozzáadása
plt.xlabel('Kereslet')
plt.ylabel('Gyakoriság')
plt.title('Kereslet hisztogramja')
plt.legend()

# Háttér vonalak kikapcsolása
plt.grid(False)

# Hisztogram kirajzolása
plt.show()
12. ábra: tényadat hisztogram

Főleg az értékesítési/kereslet előrejelzések esetén gyakori hogy az adatok eloszlása eltér a normálistól, valamilyen irányba. A mi mintapéldánkban tapasztalható magasabb kiugró értékek miatt a MAE értéke csaknem fele az RMSE értékének [7,7 % vs. 15%]. Elmondható, a MAE a mediánt preferálja, a “tömeg” re optimalizál, míg az RMSE érzékeny a kiugró értékre, tehát egy számtani átlag szemléletű mérőszám. Éppen ezért az RMSE érték nagyobb súlyt fog adni a kiugró értékeknek, kevésbé torzít (unbiased), viszont per definíció nem nyújt “védelmet” az outlierekkel szemben.

Az alábbi ábra kiválóan megmutatja a MAPE (forecast 1), MAE (forecast 2), és RMSE (forecast 3) esetére való optimalizáció eredményét.
MAPE optimalizáció esetében nagyon alacsony előrejelzést adnánk, MAE használatakor egy medián értéket, MAPE esetében pedig egy átlagos értéket keresnénk forecast értéknek, mely próbál jobban “tekintettel lenni” a kiugró adatokra.

13. ábra: Nicolas Vandeput Data Science for Supply Chain Forecasting könyvéből

Esetünkben a reportálási szakaszban jelenítsük meg a torzítottságot, MAE, alternatív rel_MAE és rel_RMSE értéket, kiemelve pár mondatban jelentőségüket.

MAE = 54 462
rel_MAE = 7,7%
rel_RMSE = 15%

05. Key Objectives megválaszolása:

Bolti előrejelzés elkészítése: 690 596 egység

Bizonytalansági mérőszámok (KPI)
Átlagosan 54 462 egységgel tévedünk a forecast elkészítésekor, mely 7,7%-os hibázási arányt jelez. Mivel adatsorunk tartalmaz kiugró értékeket, ezt a tulajdonságot kiemelten kezelve a pontatlanságunk 15%-ra ugorhat.

Következő lépések
Hibaarány minimalizálása, különös tekintettel a szezonalitás és kiugró értékek kezelésére, komplexebb modellek alkalmazásának vizsgálata.

06. Elérhetőség

Zoltán Mészáros | LinkedIn
mzg.datateam[kukac]gmail.com

--

--