from datetime import datetime
print(f'Päivitetty {datetime.now().date()} / Aki Taanila')
Päivitetty 2024-03-22 / 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 Holtin menetelmää. Holtin menetelmässä aikasarjan tason L (level) hetkellä t määrittää lauseke
Lt = alfa * Yt + (1 - alfa) * (Lt-1 + Tt-1)
Yllä Yt on viimeisin havainto ja Tt-1 on edellinen trendi. Trendille hetkellä t saadaan arvio lausekkeesta
Tt = beta * (Lt - Lt-1) + (1 - beta) * Tt-1
Ennuste hetkelle t+p saadaan
Lt + pTt
Mallin parametrit alfa ja beta pyritään määrittämään siten että ennustevirheiden neliöiden summa saadaan mahdollisimman pieneksi.
Huomaa, että tässä esimerkissä kaksinkertainen eksoponentiaalinen tasoitus ei ole hyvä malli, koska aikasarjassa on selkeä kausivaihtelu, joka toistuu neljän vuosineljänneksen jaksoissa. Tämän kaksinkertainen eksponentiaalinen tasoitus jättää huomiotta!
Trendin ja kausivaihtelun huomioivan mallin löydät muistiosta https://nbviewer.org/github/taanila/aikasarjat/blob/main/forecast3.ipynb
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
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 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'>
Ennustemalli sovitetaan (fit) dataan. Tuloksena saadaan olio (tässä olen antanut oliolle nimeksi malli), joka sisältää monenlaista tietoa mallista.
freq-parametrille käytän arvoa 'Q', 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', freq='Q').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 | 358.298470 |
2014-03-31 | 350 | 383.620020 |
2014-06-30 | 250 | 366.399862 |
2014-09-30 | 400 | 323.669796 |
2014-12-31 | 450 | 329.829738 |
2015-03-31 | 350 | 352.657136 |
2015-06-30 | 200 | 346.807887 |
2015-09-30 | 300 | 299.551497 |
2015-12-31 | 350 | 286.527550 |
2016-03-31 | 200 | 291.568682 |
2016-06-30 | 150 | 255.652973 |
2016-09-30 | 400 | 210.765405 |
2016-12-31 | 550 | 244.590678 |
2017-03-31 | 350 | 321.880765 |
2017-06-30 | 250 | 336.272305 |
2017-09-30 | 550 | 319.435808 |
2017-12-31 | 550 | 388.640726 |
2018-03-31 | 400 | 450.474466 |
2018-06-30 | 350 | 460.375267 |
2018-09-30 | 600 | 450.406081 |
2018-12-31 | 750 | 508.899427 |
2019-03-31 | 500 | 601.657610 |
2019-06-30 | 400 | 609.307232 |
2019-09-30 | 650 | 580.657205 |
2019-12-31 | 850 | 620.479762 |
# 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 | 358.298470 | 141.701530 |
2014-03-31 | 350 | 383.620020 | -33.620020 |
2014-06-30 | 250 | 366.399862 | -116.399862 |
2014-09-30 | 400 | 323.669796 | 76.330204 |
2014-12-31 | 450 | 329.829738 | 120.170262 |
2015-03-31 | 350 | 352.657136 | -2.657136 |
2015-06-30 | 200 | 346.807887 | -146.807887 |
2015-09-30 | 300 | 299.551497 | 0.448503 |
2015-12-31 | 350 | 286.527550 | 63.472450 |
2016-03-31 | 200 | 291.568682 | -91.568682 |
2016-06-30 | 150 | 255.652973 | -105.652973 |
2016-09-30 | 400 | 210.765405 | 189.234595 |
2016-12-31 | 550 | 244.590678 | 305.409322 |
2017-03-31 | 350 | 321.880765 | 28.119235 |
2017-06-30 | 250 | 336.272305 | -86.272305 |
2017-09-30 | 550 | 319.435808 | 230.564192 |
2017-12-31 | 550 | 388.640726 | 161.359274 |
2018-03-31 | 400 | 450.474466 | -50.474466 |
2018-06-30 | 350 | 460.375267 | -110.375267 |
2018-09-30 | 600 | 450.406081 | 149.593919 |
2018-12-31 | 750 | 508.899427 | 241.100573 |
2019-03-31 | 500 | 601.657610 | -101.657610 |
2019-06-30 | 400 | 609.307232 | -209.307232 |
2019-09-30 | 650 | 580.657205 | 69.342795 |
2019-12-31 | 850 | 620.479762 | 229.520238 |
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 | 523546.491 |
Optimized: | True | AIC | 256.738 |
Trend: | Additive | BIC | 261.613 |
Seasonal: | None | AICC | 261.404 |
Seasonal Periods: | None | Date: | Fri, 22 Mar 2024 |
Box-Cox: | False | Time: | 08:51:00 |
Box-Cox Coeff.: | None |
coeff | code | optimized | |
---|---|---|---|
smoothing_level | 0.2323040 | alpha | True |
smoothing_trend | 0.2322406 | beta | True |
initial_level | 373.53961 | l.0 | True |
initial_trend | -15.241136 | b.0 | 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 | 508.899427 | 241.100573 |
2019-03-31 | 500 | 601.657610 | -101.657610 |
2019-06-30 | 400 | 609.307232 | -209.307232 |
2019-09-30 | 650 | 580.657205 | 69.342795 |
2019-12-31 | 850 | 620.479762 | 229.520238 |
# Ennustettavien ajankohtien aikaleimat (alkupäivänä aikasarjan viimeistä aikaleimaa seuraava aikaleima)
index = pd.date_range('2020-03-31', periods=8, freq='Q')
# 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 | 709.894889 |
2020-06-30 | 745.991552 |
2020-09-30 | 782.088215 |
2020-12-31 | 818.184877 |
2021-03-31 | 854.281540 |
2021-06-30 | 890.378203 |
2021-09-30 | 926.474865 |
2021-12-31 | 962.571528 |
# Viivakaavio havainnoista
df['Kysyntä'].plot()
# Ennusteet kaavioon
df_ennuste['Ennuste'].plot()
<Axes: xlabel='Vuosineljännes'>
Data-analytiikka Pythonilla: https://tilastoapu.wordpress.com/python/