from datetime import datetime
print(f'Päivitetty {datetime.now().date()} / Aki Taanila')
Päivitetty 2024-04-02 / Aki Taanila
Aikasarjaennustamisessa oletan että toteutuneiden havaintojen muodostama aikasarja sisältää informaatiota, joka auttaa tulevien havaintojen ennustamisessa.
Eksponentiaalisen tasoituksen mallit ovat erityisen suosittuja liiketaloudessa kysynnän ennustamisessa. Mallit ovat helppokäyttöisiä, nopeasti laskettavissa ja helposti päivitettävissä uusien havaintojen myötä. Ennustusmenetelmä riippuu siitä, minkälaista systemaattista vaihtelua aikasarjassa esiintyy. Eksponentiaalisia tasoitusmenetelmiä käytettäessä on kolme päävaihtoehtoa:
Tässä muistiossa käytetään Holt-Wintersin menetelmää. Holt-Winterin tulomallissa aikasarjan tason L (level) hetkellä t määrittää lauseke
Lt = alfa * Yt/St-s + (1 - alfa)(Lt-1 + Tt-1)
Yllä Yt on viimeisin havainto, St-s on edellisen vastaavan periodin kausivaihtelu ja Tt-1 on edellinen trendi.
Trendille T hetkellä t saadaan arvio lausekkeesta
Tt = beta * (Lt - Lt-1) + (1 - beta) * Tt-1
Kausivaihtelulle S hetkellä t saadaan arvio lausekkeesta St = gamma * Yt/Lt + (1 - gamma) * St-s
Ennuste hetkelle t + p saadaan (Lt + pTt)St-s
Yllä on kyse Holt-Wintersin tulomallista, jossa kausivaihtelu huomioidaan kausivaihtelukertoimena. Holt-Wintersin mallia voidaan soveltaa myös summamallina, jolloin kausivaihtelu huomioidaan lisättävänä kausivaihteluterminä. Tulomalli soveltuu paremmin tilanteisiin, joissa kausivaihtelukomponentin suuruus vaihtelee aikasarjan tason L mukaan. Summamalli soveltuu tilanteisiin, joissa kausivaihtelukomponentin suuruus ei riipu aikasarjan tasosta L.
Mallin parametrit alfa, beta ja gamma pyritään määrittämään siten että ennustevirheiden neliöiden summa saadaan mahdollisimman pieneksi.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.api import seasonal_decompose
from statsmodels.tsa.api import ExponentialSmoothing
sns.set_style('whitegrid')
df = pd.read_excel('http://taanila.fi/aikasarja.xlsx')
df.head()
Vuosineljännes | Kysyntä | |
---|---|---|
0 | 2013-12-31 | 500 |
1 | 2014-03-31 | 350 |
2 | 2014-06-30 | 250 |
3 | 2014-09-30 | 400 |
4 | 2014-12-31 | 450 |
# Aikaleimat indeksiin
# to_datetime muuntaa merkkijonomuotoisen tiedon aikaleimoiksi
# format-parametri mahdollistaa erilaisten esitysmuotojen tunnistamisen aikaleimoiksi
df.index = pd.to_datetime(df['Vuosineljännes'], format="%Y-%m-%d")
# Pudotetaan tarpeettomaksi käynyt sarake pois
df = df.drop('Vuosineljännes', axis=1)
df
Kysyntä | |
---|---|
Vuosineljännes | |
2013-12-31 | 500 |
2014-03-31 | 350 |
2014-06-30 | 250 |
2014-09-30 | 400 |
2014-12-31 | 450 |
2015-03-31 | 350 |
2015-06-30 | 200 |
2015-09-30 | 300 |
2015-12-31 | 350 |
2016-03-31 | 200 |
2016-06-30 | 150 |
2016-09-30 | 400 |
2016-12-31 | 550 |
2017-03-31 | 350 |
2017-06-30 | 250 |
2017-09-30 | 550 |
2017-12-31 | 550 |
2018-03-31 | 400 |
2018-06-30 | 350 |
2018-09-30 | 600 |
2018-12-31 | 750 |
2019-03-31 | 500 |
2019-06-30 | 400 |
2019-09-30 | 650 |
2019-12-31 | 850 |
df.plot()
<Axes: xlabel='Vuosineljännes'>
# Aikasarjan vaihtelua aiheuttavien komponenttien erottelu
# Observed=alkuperäinen aikasarja, Trend=trendi, Seasonal=kausivaihtelu,
# Residual=muu kuin trendiin ja kausivaihteluun liittyvä vaihtelu
decompose = seasonal_decompose(df['Kysyntä']).plot()
Tässä aikasarjassa on selvästi havaittavat trendi ja kausivaihtelu. Koska kyse on vuosineljänneksittäisestä datasta, niin kausivaihtelu esiintyy neljän havainnon (vuosineljänneksen) jaksoissa.
Ennustemalli sovitetaan (fit) dataan. Tuloksena saadaan olio (tässä olen antanut oliolle nimeksi malli), joka sisältää monenlaista tietoa mallista.
Trendiin (trend) käytän summamallia (add), jossa trendi on aikasarjaan lisättävä termi. Tämä on suositeltavin vaihtoehto.
Kausivaihteluun (seasonal) käytän summamallia (add) tai tulomallia (mul). Tulomallissa kausivaihtelu ilmenee kertoimina. Tässä olen kokeillut myös summamallia, mutta tulomalli osoittautui paremmaksi.
seasonal_periods-parametrille käytän arvoa 4, koska kausivaihtelu esiintyy neljän vuosineljänneksen jaksoissa.
freq-parametrille käytän arvoa 'QE', koska kyseessä ovat vuosineljänneksien viimeiset päivät. Lisätietoa freq-parametrin mahdollisista arvoista https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases
malli = ExponentialSmoothing(df['Kysyntä'], trend='add', seasonal='mul',
seasonal_periods=4, freq='QE').fit()
# malli-olion avulla saan mallin mukaan simuloidut ennusteet (fittedvalues) jo toteutuneille ajankohdille
df['Ennuste'] = malli.fittedvalues
df
Kysyntä | Ennuste | |
---|---|---|
Vuosineljännes | ||
2013-12-31 | 500 | 500.361288 |
2014-03-31 | 350 | 343.996373 |
2014-06-30 | 250 | 265.222508 |
2014-09-30 | 400 | 445.316005 |
2014-12-31 | 450 | 496.221285 |
2015-03-31 | 350 | 313.025004 |
2015-06-30 | 200 | 262.293162 |
2015-09-30 | 300 | 366.159531 |
2015-12-31 | 350 | 377.900949 |
2016-03-31 | 200 | 244.302321 |
2016-06-30 | 150 | 156.073801 |
2016-09-30 | 400 | 268.831974 |
2016-12-31 | 550 | 473.395022 |
2017-03-31 | 350 | 371.342033 |
2017-06-30 | 250 | 265.642242 |
2017-09-30 | 550 | 445.771871 |
2017-12-31 | 550 | 654.725518 |
2018-03-31 | 400 | 382.808599 |
2018-06-30 | 350 | 299.486010 |
2018-09-30 | 600 | 609.324360 |
2018-12-31 | 750 | 725.597453 |
2019-03-31 | 500 | 508.968588 |
2019-06-30 | 400 | 375.853080 |
2019-09-30 | 650 | 699.824318 |
2019-12-31 | 850 | 789.985348 |
# Alkuperäinen aikasarja ja mallin mukaiset ennusteet samaan kaavioon
df.plot()
<Axes: xlabel='Vuosineljännes'>
# Ennustevirheet (residuaalit) löytyvät malli-oliosta
df['Ennustevirhe'] = malli.resid
df
Kysyntä | Ennuste | Ennustevirhe | |
---|---|---|---|
Vuosineljännes | |||
2013-12-31 | 500 | 500.361288 | -0.361288 |
2014-03-31 | 350 | 343.996373 | 6.003627 |
2014-06-30 | 250 | 265.222508 | -15.222508 |
2014-09-30 | 400 | 445.316005 | -45.316005 |
2014-12-31 | 450 | 496.221285 | -46.221285 |
2015-03-31 | 350 | 313.025004 | 36.974996 |
2015-06-30 | 200 | 262.293162 | -62.293162 |
2015-09-30 | 300 | 366.159531 | -66.159531 |
2015-12-31 | 350 | 377.900949 | -27.900949 |
2016-03-31 | 200 | 244.302321 | -44.302321 |
2016-06-30 | 150 | 156.073801 | -6.073801 |
2016-09-30 | 400 | 268.831974 | 131.168026 |
2016-12-31 | 550 | 473.395022 | 76.604978 |
2017-03-31 | 350 | 371.342033 | -21.342033 |
2017-06-30 | 250 | 265.642242 | -15.642242 |
2017-09-30 | 550 | 445.771871 | 104.228129 |
2017-12-31 | 550 | 654.725518 | -104.725518 |
2018-03-31 | 400 | 382.808599 | 17.191401 |
2018-06-30 | 350 | 299.486010 | 50.513990 |
2018-09-30 | 600 | 609.324360 | -9.324360 |
2018-12-31 | 750 | 725.597453 | 24.402547 |
2019-03-31 | 500 | 508.968588 | -8.968588 |
2019-06-30 | 400 | 375.853080 | 24.146920 |
2019-09-30 | 650 | 699.824318 | -49.824318 |
2019-12-31 | 850 | 789.985348 | 60.014652 |
Mallin hyvyyden tarkasteluun on monia tapoja. Tässä käytän
Huomaa erityisesti SSE (sum of squared errors). Mallia laskeva algoritmi yrittää saada SSE:n mahdollisimman pieneksi.
# Ennustevirheet aikasarjana
# On hyvä, jos ennustevirheiden aikasarjan vaihtelu on sattumanvaraista
df['Ennustevirhe'].plot()
plt.ylabel('Ennustevirhe')
Text(0, 0.5, 'Ennustevirhe')
# Ennusteiden ja toteutuneiden kysyntöjen hajontakaavio
# Ennustemalli on sitä parempi, mitä paremmin pisteet seuraavat suoraa viivaa
# vasemmasta alakulmasta oikeaan yläkulmaan
df.plot(kind='scatter', x='Ennuste', y='Kysyntä')
plt.xlabel('Ennuste')
plt.ylabel('Toteutunut kysyntä')
Text(0, 0.5, 'Toteutunut kysyntä')
# Mallin statistiikkaa
malli.summary()
Dep. Variable: | Kysyntä | No. Observations: | 25 |
---|---|---|---|
Model: | ExponentialSmoothing | SSE | 72742.406 |
Optimized: | True | AIC | 215.395 |
Trend: | Additive | BIC | 225.146 |
Seasonal: | Multiplicative | AICC | 231.109 |
Seasonal Periods: | 4 | Date: | Tue, 02 Apr 2024 |
Box-Cox: | False | Time: | 14:29:10 |
Box-Cox Coeff.: | None |
coeff | code | optimized | |
---|---|---|---|
smoothing_level | 0.8965013 | alpha | True |
smoothing_trend | 0.0119557 | beta | True |
smoothing_seasonal | 0.0088302 | gamma | True |
initial_level | 304.31607 | l.0 | True |
initial_trend | 10.241530 | b.0 | True |
initial_seasons.0 | 1.5906826 | s.0 | True |
initial_seasons.1 | 1.0597773 | s.1 | True |
initial_seasons.2 | 0.7801315 | s.2 | True |
initial_seasons.3 | 1.3390183 | s.3 | True |
Ennustettavien ajankohtien aikaleimojen määrittämiseksi:
Lisätietoa freq-parametrin mahdollisista arvoista https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases
#Tarkistan viimeisen aikaleiman
df.tail()
Kysyntä | Ennuste | Ennustevirhe | |
---|---|---|---|
Vuosineljännes | |||
2018-12-31 | 750 | 725.597453 | 24.402547 |
2019-03-31 | 500 | 508.968588 | -8.968588 |
2019-06-30 | 400 | 375.853080 | 24.146920 |
2019-09-30 | 650 | 699.824318 | -49.824318 |
2019-12-31 | 850 | 789.985348 | 60.014652 |
# Ennustettavien ajankohtien aikaleimat (alkupäivänä aikasarjan viimeistä aikaleimaa seuraava aikaleima)
index = pd.date_range('2020-03-31', periods=8, freq='QE')
# Ennusteet kahdeksalle vuosineljännekselle
ennusteet = malli.forecast(8)
# Ennusteet dataframeen
df_ennuste = pd.DataFrame(data=ennusteet, index=index, columns=['Ennuste'])
df_ennuste
Ennuste | |
---|---|
2020-03-31 | 573.205675 |
2020-06-30 | 429.554427 |
2020-09-30 | 754.271537 |
2020-12-31 | 908.356987 |
2021-03-31 | 616.245179 |
2021-06-30 | 461.213502 |
2021-09-30 | 808.857186 |
2021-12-31 | 972.925416 |
# Viivakaavio havainnoista
df['Kysyntä'].plot()
# Ennusteet kaavioon
df_ennuste['Ennuste'].plot()
<Axes: xlabel='Vuosineljännes'>
# Data, jossa alkuperäinen aikasarja ja lasketut ennusteet
df1 = pd.concat([df, df_ennuste])
df1
Kysyntä | Ennuste | Ennustevirhe | |
---|---|---|---|
2013-12-31 | 500.0 | 500.361288 | -0.361288 |
2014-03-31 | 350.0 | 343.996373 | 6.003627 |
2014-06-30 | 250.0 | 265.222508 | -15.222508 |
2014-09-30 | 400.0 | 445.316005 | -45.316005 |
2014-12-31 | 450.0 | 496.221285 | -46.221285 |
2015-03-31 | 350.0 | 313.025004 | 36.974996 |
2015-06-30 | 200.0 | 262.293162 | -62.293162 |
2015-09-30 | 300.0 | 366.159531 | -66.159531 |
2015-12-31 | 350.0 | 377.900949 | -27.900949 |
2016-03-31 | 200.0 | 244.302321 | -44.302321 |
2016-06-30 | 150.0 | 156.073801 | -6.073801 |
2016-09-30 | 400.0 | 268.831974 | 131.168026 |
2016-12-31 | 550.0 | 473.395022 | 76.604978 |
2017-03-31 | 350.0 | 371.342033 | -21.342033 |
2017-06-30 | 250.0 | 265.642242 | -15.642242 |
2017-09-30 | 550.0 | 445.771871 | 104.228129 |
2017-12-31 | 550.0 | 654.725518 | -104.725518 |
2018-03-31 | 400.0 | 382.808599 | 17.191401 |
2018-06-30 | 350.0 | 299.486010 | 50.513990 |
2018-09-30 | 600.0 | 609.324360 | -9.324360 |
2018-12-31 | 750.0 | 725.597453 | 24.402547 |
2019-03-31 | 500.0 | 508.968588 | -8.968588 |
2019-06-30 | 400.0 | 375.853080 | 24.146920 |
2019-09-30 | 650.0 | 699.824318 | -49.824318 |
2019-12-31 | 850.0 | 789.985348 | 60.014652 |
2020-03-31 | NaN | 573.205675 | NaN |
2020-06-30 | NaN | 429.554427 | NaN |
2020-09-30 | NaN | 754.271537 | NaN |
2020-12-31 | NaN | 908.356987 | NaN |
2021-03-31 | NaN | 616.245179 | NaN |
2021-06-30 | NaN | 461.213502 | NaN |
2021-09-30 | NaN | 808.857186 | NaN |
2021-12-31 | NaN | 972.925416 | NaN |
Data-analytiikka Pythonilla: https://tilastoapu.wordpress.com/python/