Szeregi czasowe i Python Pandas

Szeregi czasowe mogą być wygodnie przetwarzane w języku Python dzięki narzędziom dostępnym w pakiecie Pandas. W artykule przedstawiam przykład ilustrujący wykorzystanie języka Python i biblioteki Pandas do prostych operacji na szeregach czasowych. Artykuł jest przeznaczony dla osób początkujących, które nie mają jeszcze większego doświadczenia z przetwarzaniem szeregów czasowych w języku Python, ale znają podstawy samego języka programowania.

Wprowadzenie

Przed rozpoczęciem pracy zaimportujmy niezbędne pakiety. W omawianym przykładzie będzie to: pakiet NumPy do obliczeń numerycznych, zwłaszcza obliczeń wektorowo-macierzowych, pakiet Matplotlib do tworzenia wykresów oraz pakiet Pandas do przetwarzania danych. Wszystkie niezbędne pakiety są domyślnie instalowane w popularnej dystrybucji Anaconda Python używanej w Data Science, w innych dystrybucjach może okazać się konieczna ich ręczna instalacja.

Poniższy kod importuje niezbędne pakiety.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

Tworzenie losowego szeregu czasowego

Na początku stwórzmy losowy szereg czasowy, wypiszmy go na ekranie i narysujmy jego wykres. Szereg czasowy składa się z dwóch elementów: z indeksu określającego czas rejestracji kolejnych obserwacji oraz z ciągu liczb zawierającego kolejne pomiary.

Poniższy kod tworzy indeks składający się z kolejnych miesięcy od stycznia 2000 do sierpnia 2017, ciąg danych losowych z rozkładu jednostajnego na odcinku [0, 1] o długości odpowiadającej długości utworzonego indeksu oraz szereg czasowy złożony z tych dwóch elementów.

czas = pd.date_range('2000-01', '2017-08', freq='M')
dane = np.random.rand(czas.shape[0])
szereg_czasowy = pd.Series(dane, index=czas)

Utworzony szereg czasowy może być wypisany na ekranie poleceniem

print szereg_czasowy

Wygodniej jednak, zwłaszcza w przypadku bardzo długich szeregów czasowych, zrobić to poleceniem

print szereg_czasowy.head()

które wypisuje jedynie początek szeregu czasowego, a nie jego całość. Polecenie to zwraca poniższy rezultat (ze względu na losowość danych wynik może się różnić)

2000-01-31    0.587368
2000-02-29    0.506749
2000-03-31    0.982686
2000-04-30    0.940802
2000-05-31    0.922867
Freq: M, dtype: float64

Wykres szeregu czasowego można wykonać poleceniem

szereg_czasowy.plot()

które tworzy poniższy wykres (ze względu na losowość danych wynik może się różnić)

wykres losowego szeregu czasowego

Utworzony szereg czasowy wygląda bardzo chaotycznie i nienaturalnie. Bardziej naturalny losowy szereg czasowy można stworzyć inaczej losując jego zawartość, co prezentuje poniższy kod.

czas = pd.date_range('2000-01', '2017-08', freq='M')
przyrost = 0.2 * np.random.rand(czas.shape[0]) - 0.1
dane = (1 + przyrost).cumprod()
szereg_czasowy = pd.Series(dane, index=czas)
print szereg_czasowy.head()
szereg_czasowy.plot()
2000-01-31    1.050916
2000-02-29    1.125902
2000-03-31    1.077655
2000-04-30    1.005373
2000-05-31    0.938185
Freq: M, dtype: float64

wykres losowego szeregu czasowego

Wczytanie szeregu czasowego z pliku tekstowego

Zamiast pracować na losowych szeregach czasowych, wczytajmy szereg czasowy z pliku tekstowego. Przygotowany na potrzeby przedstawianego przykładu plik tekstowy, dostępny tutaj, zawiera notowania wybranej spółki giełdowej. Notowania składają się z 5 szeregów czasowych, ceny otwarcia, ceny najwyższej, ceny najniższej, ceny zamknięcia i wolumenu obrotów, rejestrowanych w kolejnych dniach notowania spółki na giełdzie.

Poniższy kod wczytuje dane ze wskazanego pliku tekstowego, dla wygody zmienia nazwy szeregów czasowych i indeksu, wypisuje na ekranie ich początek oraz rysuje wykres jednego z szeregów czasowych.

notowania = pd.read_csv('PKNORLEN.CSV', index_col=1, parse_dates=True)[['<OPEN>', '<HIGH>', '<LOW>', '<CLOSE>', '<VOL>']]
notowania.columns = ['open', 'high', 'low', 'close', 'volume']
notowania.index.name = 'time'
print notowania.head()
notowania['close'].plot()
            open  high   low  close    volume
time                                         
1999-11-26  22.2  22.4  21.4   22.1  10902704
1999-11-29  22.0  22.1  21.6   21.7   3781050
1999-11-30  21.9  22.3  21.9   22.1   2260083
1999-12-01  22.3  22.8  22.1   22.8   2116148
1999-12-02  23.0  23.7  22.9   23.2   2271722

wykres szeregu czasowego wczytanego z pliku tekstowego

Podstawowe operacje na szeregach czasowych

operacje na zestawach danych zawierających szeregi czasowe

Do wyświetlenia początku zestawu danych służy poniższy kod

print notowania.head()
            open  high   low  close    volume
time                                         
1999-11-26  22.2  22.4  21.4   22.1  10902704
1999-11-29  22.0  22.1  21.6   21.7   3781050
1999-11-30  21.9  22.3  21.9   22.1   2260083
1999-12-01  22.3  22.8  22.1   22.8   2116148
1999-12-02  23.0  23.7  22.9   23.2   2271722

Podobnie, poniższy kod wyświetla koniec zestawu danych

print notowania.tail()
              open   high     low   close  volume
time                                             
2017-08-08  105.55  108.0  105.35  108.00  598530
2017-08-09  108.00  108.1  106.45  107.80  504264
2017-08-10  107.35  107.8  106.50  107.00  537099
2017-08-11  106.45  107.0  106.00  106.25  474341
2017-08-14  106.70  107.4  105.35  106.65  295075

Poniższy kod wyświetla wartości szeregów czasowych dla określonej daty. Warto zauważyć, że polecenie notowania[‚2017-01-02’] zwróci błąd, bo ‚2017-01-02’ nie jest nazwą kolumny. Jeśli podana data nie istnieje w szeregu czasowym, to polecenie notowania.loc[…] zwróci błąd.

print notowania.loc['2017-01-02']
open          85.10
high          85.89
low           84.76
close         85.59
volume    162705.00
Name: 2017-01-02 00:00:00, dtype: float64

Podobnie, do wyświetlenia wartości szeregów czasowych dla daty o określonym indeksie służy poniższy kod. Warto zauważyć, że polecenie notowania[-1] zwróci błąd, bo -1 nie jest nazwą kolumny.

print notowania.iloc[-1]
open         106.70
high         107.40
low          105.35
close        106.65
volume    295075.00
Name: 2017-08-14 00:00:00, dtype: float64

Do wybrania określonego szeregu czasowego z zestawu danych służy poniższy kod

cena = notowania['close']

Na zakończenie, do zestawu danych można dodać nowy szereg czasowy poniższym kodem

notowania['wzrost'] = notowania['close'] / notowania['close'].shift(1)

operacje na pojedynczych szeregach czasowych

Wyświetlenie początku szeregu czasowego

print cena.head()
time
1999-11-26    22.1
1999-11-29    21.7
1999-11-30    22.1
1999-12-01    22.8
1999-12-02    23.2
Name: close, dtype: float64

Wyświetlenie końca szeregu czasowego

print cena.tail()
time
2017-08-08    108.00
2017-08-09    107.80
2017-08-10    107.00
2017-08-11    106.25
2017-08-14    106.65
Name: close, dtype: float64

Wyświetlenie wartości szeregu czasowego dla określonej daty

print cena.loc['2017-01-02']
print cena['2017-01-02']
85.59
85.59

Wyświetlenie wartości szeregu czasowego dla daty o określonym indeksie

print cena.iloc[-1]
print cena[-1]
106.65
106.65

selekcja danych z szeregów czasowych

print notowania[['close', 'volume']].tail()
             close  volume
time                      
2017-08-08  108.00  598530
2017-08-09  107.80  504264
2017-08-10  107.00  537099
2017-08-11  106.25  474341
2017-08-14  106.65  295075
print notowania['2017-01-01':'2017-01-15']
             open   high    low  close   volume    wzrost
time                                                     
2017-01-02  85.10  85.89  84.76  85.59   162705  1.003400
2017-01-03  86.00  86.51  84.35  84.75  1205721  0.990186
2017-01-04  85.20  86.50  83.34  86.50  1162427  1.020649
2017-01-05  86.11  89.69  85.93  89.34  1526983  1.032832
2017-01-09  89.30  89.30  87.74  88.60   957675  0.991717
2017-01-10  88.30  89.50  87.89  88.30   911964  0.996614
2017-01-11  88.30  88.43  85.09  85.46  1286998  0.967837
2017-01-12  85.85  87.81  85.35  86.39  1198237  1.010882
2017-01-13  86.38  87.34  85.51  86.50   657816  1.001273
print notowania[['close', 'volume']]['2017-01-01':'2017-01-15']
            close   volume
time                      
2017-01-02  85.59   162705
2017-01-03  84.75  1205721
2017-01-04  86.50  1162427
2017-01-05  89.34  1526983
2017-01-09  88.60   957675
2017-01-10  88.30   911964
2017-01-11  85.46  1286998
2017-01-12  86.39  1198237
2017-01-13  86.50   657816
print notowania[['close', 'volume']]['2017-08-01':]
             close  volume
time                      
2017-08-01  107.45  547346
2017-08-02  106.55  338122
2017-08-03  106.60  554577
2017-08-04  106.15  302784
2017-08-07  105.50  394322
2017-08-08  108.00  598530
2017-08-09  107.80  504264
2017-08-10  107.00  537099
2017-08-11  106.25  474341
2017-08-14  106.65  295075
print type(notowania)
print type(notowania['volume'])
print type(notowania.loc['2017-08-09'])
<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
print notowania['close']['2017']
time
2017-01-02     85.59
2017-01-03     84.75
2017-01-04     86.50
2017-01-05     89.34
2017-01-09     88.60
2017-01-10     88.30
2017-01-11     85.46
2017-01-12     86.39
2017-01-13     86.50
2017-01-16     85.04
2017-01-17     85.70
2017-01-18     85.75
2017-01-19     86.50
2017-01-20     82.50
2017-01-23     82.30
2017-01-24     82.31
2017-01-25     82.41
2017-01-26     81.50
2017-01-27     83.31
2017-01-30     81.31
2017-01-31     81.18
2017-02-01     81.80
2017-02-02     81.70
2017-02-03     83.29
2017-02-06     84.65
2017-02-07     84.19
2017-02-08     83.80
2017-02-09     86.90
2017-02-10     85.45
2017-02-13     86.88
               ...  
2017-07-04    110.35
2017-07-05    113.25
2017-07-06    111.00
2017-07-07    110.65
2017-07-10    111.50
2017-07-11    110.25
2017-07-12    112.90
2017-07-13    111.85
2017-07-14    113.00
2017-07-17    112.40
2017-07-18    111.95
2017-07-19    111.00
2017-07-20    109.90
2017-07-21    106.35
2017-07-24    105.90
2017-07-25    106.05
2017-07-26    108.20
2017-07-27    105.45
2017-07-28    106.55
2017-07-31    106.35
2017-08-01    107.45
2017-08-02    106.55
2017-08-03    106.60
2017-08-04    106.15
2017-08-07    105.50
2017-08-08    108.00
2017-08-09    107.80
2017-08-10    107.00
2017-08-11    106.25
2017-08-14    106.65
Name: close, dtype: float64
print notowania['close']['2017-01']
time
2017-01-02    85.59
2017-01-03    84.75
2017-01-04    86.50
2017-01-05    89.34
2017-01-09    88.60
2017-01-10    88.30
2017-01-11    85.46
2017-01-12    86.39
2017-01-13    86.50
2017-01-16    85.04
2017-01-17    85.70
2017-01-18    85.75
2017-01-19    86.50
2017-01-20    82.50
2017-01-23    82.30
2017-01-24    82.31
2017-01-25    82.41
2017-01-26    81.50
2017-01-27    83.31
2017-01-30    81.31
2017-01-31    81.18
Name: close, dtype: float64

wykresy szeregów czasowych

Narysowanie wykresu szeregu czasowego

cena.plot()

wykres szeregu czasowego

Narysowanie wykresu fragmentu szeregu czasowego

cena['2014':'2016'].plot()

wykres fragmentu szeregu czasowego

Narysowanie fragmentu wykresu szeregu czasowego

notowania['wzrost']['2016-06':'2016-12'].plot()

wykres fragmentu szeregu czasowego

agregowanie i grupowanie szeregów czasowych

Do popularnych operacji na szeregach czasowych należy ich agregowanie i grupowanie. W języku Python z pakietem Pandas można robić to bardzo wygodnie.

Poniższy kod pokazuje jak liczyć średnią, minimum i maksimum z wartości szeregu czasowego lub jego wybranego fragmentu.

print notowania['close']['2017'].mean()
103.132258065
print notowania['close']['2017'].min()
81.18
print notowania['close']['2017'].max()
121.55

Zbiorcze informacje statystyczne o szeregu czasowym lub całym zestawie danych można otrzymać poleceniem describe, jak pokazuje poniższy kod

print notowania.describe()
              open         high          low        close        volume  \
count  4438.000000  4438.000000  4438.000000  4438.000000  4.438000e+03   
mean     42.123031    42.699813    41.530771    42.120649  1.070267e+06   
std      19.702682    19.969570    19.443928    19.722585  6.965051e+05   
min      14.950000    15.200000    14.750000    15.100000  5.853700e+04   
25%      26.002500    26.500000    25.600000    26.000000  6.169572e+05   
50%      40.265000    40.800000    39.620000    40.170000  9.273625e+05   
75%      52.500000    53.200000    51.800000    52.500000  1.336421e+06   
max     121.000000   122.850000   118.600000   121.550000  1.090270e+07   

            wzrost  
count  4437.000000  
mean      1.000574  
std       0.020936  
min       0.885522  
25%       0.988075  
50%       1.000000  
75%       1.012793  
max       1.137300  

Częstą operacją jest zmiana częstotliwości szeregu czasowego. Służy do tego polecenie resample z odpowiednimi parametrami.

Poniższy kod zmienia częstotliwość szeregu czasowego na roczną, w taki sposób, że dla każdego roku policzona zostanie średnia cena ze wszystkich obserwacji zarejestrowanych w danym roku. Średnia cena zostanie domyślnie przypisana do ostatniego dnia roku (czyli nowy szereg czasowy będzie składał się z ostatnich dni poszczególnych lat).

szereg_czasowy = notowania['close'].resample('A').mean()
print szereg_czasowy.head()
time
1999-12-31    24.026087
2000-12-31    21.543400
2001-12-31    18.425000
2002-12-31    18.586345
2003-12-31    20.777490
Freq: A-DEC, Name: close, dtype: float64

Poniższy kod zmienia częstotliwość szeregu czasowego na roczną, ale w taki sposób, że średnia cena jest przypisywana do ostatniego dnia poprzedniego roku.

szereg_czasowy = notowania['close'].resample('A', label='left').mean()
print szereg_czasowy.head()
time
1998-12-31    24.026087
1999-12-31    21.543400
2000-12-31    18.425000
2001-12-31    18.586345
2002-12-31    20.777490
Freq: A-DEC, Name: close, dtype: float64

Poniższy kod zmienia częstotliwość szeregu czasowego na trzymiesięczną.

szereg_czasowy = notowania['close'].resample('3M').mean()
print szereg_czasowy.head()
time
1999-11-30    21.966667
2000-02-29    24.954839
2000-05-31    22.498361
2000-08-31    20.982812
2000-11-30    18.794531
Freq: 3M, Name: close, dtype: float64

Poniższy kod zmienia częstotliwość szeregu czasowego na trzymiesięczną, ale z przesunięciem początku szeregu o jeden miesiąc, tak aby trzymiesięczne okresy odpowiadały kalendarzowym kwartałom.

szereg_czasowy = notowania['close'].resample('3M', loffset='1M').mean()
print szereg_czasowy.head()
time
1999-12-31    21.966667
2000-03-31    24.954839
2000-06-30    22.498361
2000-09-30    20.982812
2000-12-31    18.794531
Freq: Q-DEC, Name: close, dtype: float64

Przetworzone dane można łatwo prezentować na wykresach.

notowania['volume'].resample('A').sum().plot(kind='bar')

wykres statystyk szeregu czasowego

notowania['volume'].groupby(notowania.index.year).sum().plot(kind='bar')

wykres statystyk szeregu czasowego

notowania['volume'].groupby(notowania.index.weekday).sum().plot(kind='bar')

wykres statystyk szeregu czasowego

Inne operacje na szeregach czasowych

Po poznaniu podstawowych operacji na szeregach czasowych możemy zająć się ich właściwą analizą. W artykule Trend szeregu czasowego opisuję jak w prosty sposób wyznaczyć trend szeregu czasowego w języku Python z pakietem Pandas.