參考開源組織datawhale:https://datawhalechina.github.io/joyful-pandas/build/html/%E7%9B%AE%E5%BD%95/ch10.html
第十章 時序數(shù)據(jù)
In [1]: import numpy as np
In [2]: import pandas as pd
一、時序中的基本對象
時間序列的概念在日常生活中十分常見,但對于一個具體的時序事件而言,可以從多個時間對象的角度來描述。例如2020年9月7日周一早上8點整需要到教室上課,這個課會在當天早上10點結束,其中包含了哪些時間概念?
第一,會出現(xiàn)時間戳(Date times)的概念,即’2020-9-7 08:00:00’和’2020-9-7 10:00:00’這兩個時間點分別代表了上課和下課的時刻,在 pandas 中稱為 Timestamp 。同時,一系列的時間戳可以組成 DatetimeIndex ,而將它放到 Series 中后, Series 的類型就變?yōu)榱?datetime64[ns] ,如果有涉及時區(qū)則為 datetime64[ns, tz] ,其中tz是timezone的簡寫。
第二,會出現(xiàn)時間差(Time deltas)的概念,即上課需要的時間,兩個 Timestamp 做差就得到了時間差,pandas中利用 Timedelta 來表示。類似的,一系列的時間差就組成了 TimedeltaIndex , 而將它放到 Series 中后, Series 的類型就變?yōu)榱?timedelta64[ns] 。
第三,會出現(xiàn)時間段(Time spans)的概念,即在8點到10點這個區(qū)間都會持續(xù)地在上課,在 pandas 利用 Period 來表示。類似的,一系列的時間段就組成了 PeriodIndex , 而將它放到 Series 中后, Series 的類型就變?yōu)榱?Period 。
第四,會出現(xiàn)日期偏置(Date offsets)的概念,假設你只知道9月的第一個周一早上8點要去上課,但不知道具體的日期,那么就需要一個類型來處理此類需求。再例如,想要知道2020年9月7日后的第30個工作日是哪一天,那么時間差就解決不了你的問題,從而 pandas 中的 DateOffset 就出現(xiàn)了。同時, pandas 中沒有為一列時間偏置專門設計存儲類型,理由也很簡單,因為需求比較奇怪,一般來說我們只需要對一批時間特征做一個統(tǒng)一的特殊日期偏置。
對于時間戳那里你也可以對它進行一個轉(zhuǎn)換,比如:

注意:這里不是數(shù)據(jù)脫敏,只是一種轉(zhuǎn)換。
通過這個簡單的例子,就能夠容易地總結出官方文檔中的這個 表格 :
我又來白漂了。

由于時間段對象 Period/PeriodIndex 的使用頻率并不高,因此將不進行講解,而只涉及時間戳序列、時間差序列和日期偏置的相關內(nèi)容。
二、時間戳
- Timestamp的構造與屬性
單個時間戳的生成利用 pd.Timestamp 實現(xiàn),一般而言的常見日期格式都能被成功地轉(zhuǎn)換:
In [3]: ts = pd.Timestamp('2020/1/1')
In [4]: ts
Out[4]: Timestamp('2020-01-01 00:00:00')
In [5]: ts = pd.Timestamp('2020-1-1 08:10:30')
#注意兩者的不同
In [6]: ts
Out[6]: Timestamp('2020-01-01 08:10:30')
通過 year, month, day, hour, min, second 可以獲取具體的數(shù)值:
In [7]: ts.year
Out[7]: 2020
In [8]: ts.month
Out[8]: 1
In [9]: ts.day
Out[9]: 1
In [10]: ts.hour
Out[10]: 8
In [11]: ts.minute
Out[11]: 10
In [12]: ts.second
Out[12]: 30
在 pandas 中,時間戳的最小精度為納秒 ns ,由于使用了64位存儲,可以表示的時間范圍大約可以如下計算:

通過 pd.Timestamp.max 和 pd.Timestamp.min 可以獲取時間戳表示的范圍,可以看到確實表示的區(qū)間年數(shù)大小正如上述計算結果:
In [13]: pd.Timestamp.max
Out[13]: Timestamp('2262-04-11 23:47:16.854775807')
In [14]: pd.Timestamp.min
Out[14]: Timestamp('1677-09-21 00:12:43.145225')
In [15]: pd.Timestamp.max.year - pd.Timestamp.min.year
Out[15]: 585
- Datetime序列的生成
一組時間戳可以組成時間序列,可以用 to_datetime 和 date_range 來生成。其中, to_datetime 能夠把一列時間戳格式的對象轉(zhuǎn)換成為 datetime64[ns] 類型的時間序列:
In [16]: pd.to_datetime(['2020-1-1', '2020-1-3', '2020-1-6'])
Out[16]: DatetimeIndex(['2020-01-01', '2020-01-03', '2020-01-06'], dtype='datetime64[ns]', freq=None)
In [17]: df = pd.read_csv('data/learn_pandas.csv')
In [18]: s = pd.to_datetime(df.Test_Date)
In [19]: s.head()
Out[19]:
0 2019-10-05
1 2019-09-04
2 2019-09-12
3 2020-01-03
4 2019-11-06
Name: Test_Date, dtype: datetime64[ns]
在極少數(shù)情況,時間戳的格式不滿足轉(zhuǎn)換時,可以強制使用 format 進行匹配:
In [20]: temp = pd.to_datetime(['2020\\1\\1','2020\\1\\3'],format='%Y\\%m\\%d')
In [21]: temp
Out[21]: DatetimeIndex(['2020-01-01', '2020-01-03'], dtype='datetime64[ns]', freq=None)
注意上面由于傳入的是列表,而非 pandas 內(nèi)部的 Series ,因此返回的是 DatetimeIndex ,如果想要轉(zhuǎn)為 datetime64[ns] 的序列,需要顯式用 Series 轉(zhuǎn)化:
In [22]: pd.Series(temp).head()
Out[22]:
0 2020-01-01
1 2020-01-03
dtype: datetime64[ns]
另外,還存在一種把表的多列時間屬性拼接轉(zhuǎn)為時間序列的 to_datetime 操作,此時的列名必須和以下給定的時間關鍵詞列名一致:
In [23]: df_date_cols = pd.DataFrame({'year': [2020, 2020],
....: 'month': [1, 1],
....: 'day': [1, 2],
....: 'hour': [10, 20],
....: 'minute': [30, 50],
....: 'second': [20, 40]})
....:
In [24]: pd.to_datetime(df_date_cols)
Out[24]:
0 2020-01-01 10:30:20
1 2020-01-02 20:50:40
dtype: datetime64[ns]
date_range 是一種生成連續(xù)間隔時間的一種方法,其重要的參數(shù)為 start, end, freq, periods ,它們分別表示開始時間,結束時間,時間間隔,時間戳個數(shù)。其中,四個中的三個參數(shù)決定了,那么剩下的一個就隨之確定了。這里要注意,開始或結束日期如果作為端點則它會被包含:
In [25]: pd.date_range('2020-1-1','2020-1-21', freq='10D') # 包含
Out[25]: DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21'], dtype='datetime64[ns]', freq='10D')
In [26]: pd.date_range('2020-1-1','2020-2-28', freq='10D')
Out[26]:
DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21', '2020-01-31',
'2020-02-10', '2020-02-20'],
dtype='datetime64[ns]', freq='10D')
In [27]: pd.date_range('2020-1-1',
....: '2020-2-28', periods=6) # 由于結束日期無法取到,freq不為10天
....:
Out[27]:
DatetimeIndex(['2020-01-01 00:00:00', '2020-01-12 14:24:00',
'2020-01-24 04:48:00', '2020-02-04 19:12:00',
'2020-02-16 09:36:00', '2020-02-28 00:00:00'],
dtype='datetime64[ns]', freq=None)
這里的 freq 參數(shù)與 DateOffset 對象緊密相關,將在第四節(jié)介紹其具體的用法。
最后,要介紹一種改變序列采樣頻率的方法 asfreq ,它能夠根據(jù)給定的 freq 對序列進行類似于 reindex 的操作:
In [28]: s = pd.Series(np.random.rand(5),
....: index=pd.to_datetime([
....: '2020-1-%d'%i for i in range(1,10,2)]))
....:
In [29]: s.head()
Out[29]:
2020-01-01 0.836578
2020-01-03 0.678419
2020-01-05 0.711897
2020-01-07 0.487429
2020-01-09 0.604705
dtype: float64
In [30]: s.asfreq('D').head()
Out[30]:
2020-01-01 0.836578
2020-01-02 NaN
2020-01-03 0.678419
2020-01-04 NaN
2020-01-05 0.711897
Freq: D, dtype: float64
In [31]: s.asfreq('12H').head()
Out[31]:
2020-01-01 00:00:00 0.836578
2020-01-01 12:00:00 NaN
2020-01-02 00:00:00 NaN
2020-01-02 12:00:00 NaN
2020-01-03 00:00:00 0.678419
Freq: 12H, dtype: float64

- dt對象
如同 category, string 的序列上定義了 cat, str 來完成分類數(shù)據(jù)和文本數(shù)據(jù)的操作,在時序類型的序列上定義了 dt 對象來完成許多時間序列的相關操作。這里對于 datetime64[ns] 類型而言,可以大致分為三類操作:取出時間相關的屬性、判斷時間戳是否滿足條件、取整操作。
第一類操作的常用屬性包括: date, time, year, month, day, hour, minute, second, microsecond, nanosecond, dayofweek, dayofyear, weekofyear, daysinmonth, quarter ,其中 daysinmonth, quarter 分別表示月中的第幾天和季度。
In [32]: s = pd.Series(pd.date_range('2020-1-1','2020-1-3', freq='D'))
In [33]: s.dt.date
Out[33]:
0 2020-01-01
1 2020-01-02
2 2020-01-03
dtype: object
In [34]: s.dt.time
Out[34]:
0 00:00:00
1 00:00:00
2 00:00:00
dtype: object
In [35]: s.dt.day
Out[35]:
0 1
1 2
2 3
dtype: int64
In [36]: s.dt.daysinmonth
Out[36]:
0 31
1 31
2 31
dtype: int64
在這些屬性中,經(jīng)常使用的是 dayofweek ,它返回了周中的星期情況,周一為0、周二為1,以此類推:
In [37]: s.dt.dayofweek
Out[37]:
0 2
1 3
2 4
dtype: int64
此外,可以通過 month_name, day_name 返回英文的月名和星期名,注意它們是方法而不是屬性:
In [38]: s.dt.month_name()
Out[38]:
0 January
1 January
2 January
dtype: object
In [39]: s.dt.day_name()
Out[39]:
0 Wednesday
1 Thursday
2 Friday
dtype: object
第二類判斷操作主要用于測試是否為月/季/年的第一天或者最后一天:
In [40]: s.dt.is_year_start # 還可選 is_quarter/month_start
Out[40]:
0 True
1 False
2 False
dtype: bool
In [41]: s.dt.is_year_end # 還可選 is_quarter/month_end
Out[41]:
0 False
1 False
2 False
dtype: bool
第三類的取整操作包含 round, ceil, floor ,它們的公共參數(shù)為 freq ,常用的包括 H, min, S (小時、分鐘、秒),所有可選的 freq 可參考 此處 。
In [42]: s = pd.Series(pd.date_range('2020-1-1 20:35:00',
....: '2020-1-1 22:35:00',
....: freq='45min'))
....:
In [43]: s
Out[43]:
0 2020-01-01 20:35:00
1 2020-01-01 21:20:00
2 2020-01-01 22:05:00
dtype: datetime64[ns]
In [44]: s.dt.round('1H')
Out[44]:
0 2020-01-01 21:00:00
1 2020-01-01 21:00:00
2 2020-01-01 22:00:00
dtype: datetime64[ns]
In [45]: s.dt.ceil('1H')
Out[45]:
0 2020-01-01 21:00:00
1 2020-01-01 22:00:00
2 2020-01-01 23:00:00
dtype: datetime64[ns]
In [46]: s.dt.floor('1H')
Out[46]:
0 2020-01-01 20:00:00
1 2020-01-01 21:00:00
2 2020-01-01 22:00:00
dtype: datetime64[ns]
- 時間戳的切片與索引
一般而言,時間戳序列作為索引使用。如果想要選出某個子時間戳序列,第一類方法是利用 dt 對象和布爾條件聯(lián)合使用,另一種方式是利用切片,后者常用于連續(xù)時間戳。下面,舉一些例子說明:
In [47]: s = pd.Series(np.random.randint(2,size=366),
....: index=pd.date_range(
....: '2020-01-01','2020-12-31'))
....:
In [48]: idx = pd.Series(s.index).dt
In [49]: s.head()
Out[49]:
2020-01-01 1
2020-01-02 1
2020-01-03 0
2020-01-04 1
2020-01-05 0
Freq: D, dtype: int32
Example1:每月的第一天或者最后一天
In [50]: s[(idx.is_month_start|idx.is_month_end).values].head()
Out[50]:
2020-01-01 1
2020-01-31 0
2020-02-01 1
2020-02-29 1
2020-03-01 0
dtype: int32
Example2:雙休日
In [51]: s[idx.dayofweek.isin([5,6]).values].head()
Out[51]:
2020-01-04 1
2020-01-05 0
2020-01-11 0
2020-01-12 1
2020-01-18 1
dtype: int32
Example3:取出單日值
In [52]: s['2020-01-01']
Out[52]: 1
In [53]: s['20200101'] # 自動轉(zhuǎn)換標準格式
Out[53]: 1
Example4:取出七月
In [54]: s['2020-07'].head()
Out[54]:
2020-07-01 0
2020-07-02 1
2020-07-03 0
2020-07-04 0
2020-07-05 0
Freq: D, dtype: int32
Example5:取出5月初至7月15日
In [55]: s['2020-05':'2020-7-15'].head()
Out[55]:
2020-05-01 0
2020-05-02 1
2020-05-03 0
2020-05-04 1
2020-05-05 1
Freq: D, dtype: int32
In [56]: s['2020-05':'2020-7-15'].tail()
Out[56]:
2020-07-11 0
2020-07-12 0
2020-07-13 1
2020-07-14 0
2020-07-15 1
Freq: D, dtype: int32
三、時間差
- Timedelta的生成
正如在第一節(jié)中所說,時間差可以理解為兩個時間戳的差,這里也可以通過 pd.Timedelta 來構造:
In [57]: pd.Timestamp('20200102 08:00:00')-pd.Timestamp('20200101 07:35:00')
Out[57]: Timedelta('1 days 00:25:00')
In [58]: pd.Timedelta(days=1, minutes=25) # 需要注意加s
Out[58]: Timedelta('1 days 00:25:00')
In [59]: pd.Timedelta('1 days 25 minutes') # 字符串生成
Out[59]: Timedelta('1 days 00:25:00')
生成時間差序列的主要方式是 pd.to_timedelta ,其類型為 timedelta64[ns] :
In [60]: s = pd.to_timedelta(df.Time_Record)
In [61]: s.head()
Out[61]:
0 0 days 00:04:34
1 0 days 00:04:20
2 0 days 00:05:22
3 0 days 00:04:08
4 0 days 00:05:22
Name: Time_Record, dtype: timedelta64[ns]
與 date_range 一樣,時間差序列也可以用 timedelta_range 來生成,它們兩者具有一致的參數(shù):
In [62]: pd.timedelta_range('0s', '1000s', freq='6min')
Out[62]: TimedeltaIndex(['0 days 00:00:00', '0 days 00:06:00', '0 days 00:12:00'], dtype='timedelta64[ns]', freq='6T')
In [63]: pd.timedelta_range('0s', '1000s', periods=3)
Out[63]: TimedeltaIndex(['0 days 00:00:00', '0 days 00:08:20', '0 days 00:16:40'], dtype='timedelta64[ns]', freq=None)
對于 Timedelta 序列,同樣也定義了 dt 對象,上面主要定義了的屬性包括 days, seconds, mircroseconds, nanoseconds ,它們分別返回了對應的時間差特征。需要注意的是,這里的 seconds 不是指單純的秒,而是對天數(shù)取余后剩余的秒數(shù):
In [64]: s.dt.seconds.head()
Out[64]:
0 274
1 260
2 322
3 248
4 322
Name: Time_Record, dtype: int64
如果不想對天數(shù)取余而直接對應秒數(shù),可以使用 total_seconds
In [65]: s.dt.total_seconds().head()
Out[65]:
0 274.0
1 260.0
2 322.0
3 248.0
4 322.0
Name: Time_Record, dtype: float64
與時間戳序列類似,取整函數(shù)也是可以在 dt 對象上使用的:
In [66]: pd.to_timedelta(df.Time_Record).dt.round('min').head()
Out[66]:
0 0 days 00:05:00
1 0 days 00:04:00
2 0 days 00:05:00
3 0 days 00:04:00
4 0 days 00:05:00
Name: Time_Record, dtype: timedelta64[ns]
- Timedelta的運算
時間差支持的常用運算有三類:與標量的乘法運算、與時間戳的加減法運算、與時間差的加減法與除法運算:
In [67]: td1 = pd.Timedelta(days=1)
In [68]: td2 = pd.Timedelta(days=3)
In [69]: ts = pd.Timestamp('20200101')
In [70]: td1 * 2
Out[70]: Timedelta('2 days 00:00:00')
In [71]: td2 - td1
Out[71]: Timedelta('2 days 00:00:00')
In [72]: ts + td1
Out[72]: Timestamp('2020-01-02 00:00:00')
In [73]: ts - td1
Out[73]: Timestamp('2019-12-31 00:00:00')
這些運算都可以移植到時間差的序列上:
In [74]: td1 = pd.timedelta_range(start='1 days', periods=5)
In [75]: td2 = pd.timedelta_range(start='12 hours',
....: freq='2H',
....: periods=5)
....:
In [76]: ts = pd.date_range('20200101', '20200105')
In [77]: td1 * 5
Out[77]: TimedeltaIndex(['5 days', '10 days', '15 days', '20 days', '25 days'], dtype='timedelta64[ns]', freq='5D')
In [78]: td1 * pd.Series(list(range(5))) # 逐個相乘
Out[78]:
0 0 days
1 2 days
2 6 days
3 12 days
4 20 days
dtype: timedelta64[ns]
In [79]: td1 - td2
Out[79]:
TimedeltaIndex(['0 days 12:00:00', '1 days 10:00:00', '2 days 08:00:00',
'3 days 06:00:00', '4 days 04:00:00'],
dtype='timedelta64[ns]', freq=None)
In [80]: td1 + pd.Timestamp('20200101')
Out[80]:
DatetimeIndex(['2020-01-02', '2020-01-03', '2020-01-04', '2020-01-05',
'2020-01-06'],
dtype='datetime64[ns]', freq='D')
In [81]: td1 + ts # 逐個相加
Out[81]:
DatetimeIndex(['2020-01-02', '2020-01-04', '2020-01-06', '2020-01-08',
'2020-01-10'],
dtype='datetime64[ns]', freq=None)
四、日期偏置
- Offset對象
日期偏置是一種和日歷相關的特殊時間差,例如回到第一節(jié)中的兩個問題:如何求2020年9月第一個周一的日期,以及如何求2020年9月7日后的第30個工作日是哪一天。
In [82]: pd.Timestamp('20200831') + pd.offsets.WeekOfMonth(week=0,weekday=0)
Out[82]: Timestamp('2020-09-07 00:00:00')
In [83]: pd.Timestamp('20200907') + pd.offsets.BDay(30)
Out[83]: Timestamp('2020-10-19 00:00:00')
從上面的例子中可以看到, Offset 對象在 pd.offsets 中被定義。當使用 + 時獲取離其最近的下一個日期,當使用 - 時獲取離其最近的上一個日期:
In [84]: pd.Timestamp('20200831') - pd.offsets.WeekOfMonth(week=0,weekday=0)
Out[84]: Timestamp('2020-08-03 00:00:00')
In [85]: pd.Timestamp('20200907') - pd.offsets.BDay(30)
Out[85]: Timestamp('2020-07-27 00:00:00')
In [86]: pd.Timestamp('20200907') + pd.offsets.MonthEnd()
Out[86]: Timestamp('2020-09-30 00:00:00')
常用的日期偏置如下可以查閱這里的 文檔 描述。在文檔羅列的 Offset 中,需要介紹一個特殊的 Offset 對象 CDay ,其中的 holidays, weekmask 參數(shù)能夠分別對自定義的日期和星期進行過濾,前者傳入了需要過濾的日期列表,后者傳入的是三個字母的星期縮寫構成的星期字符串,其作用是只保留字符串中出現(xiàn)的星期:
In [87]: my_filter = pd.offsets.CDay(n=1,weekmask='Wed Fri',holidays=['20200109'])
In [88]: dr = pd.date_range('20200108', '20200111')
In [89]: dr.to_series().dt.dayofweek
Out[89]:
2020-01-08 2
2020-01-09 3
2020-01-10 4
2020-01-11 5
Freq: D, dtype: int64
In [90]: [i + my_filter for i in dr]
Out[90]:
[Timestamp('2020-01-10 00:00:00'),
Timestamp('2020-01-10 00:00:00'),
Timestamp('2020-01-15 00:00:00'),
Timestamp('2020-01-15 00:00:00')]
上面的例子中, n 表示增加一天 CDay , dr 中的第一天為 20200108 ,但由于下一天 20200109 被排除了,并且 20200110 是合法的周五,因此轉(zhuǎn)為 20200110 ,其他后面的日期處理類似。

- 偏置字符串
前面提到了關于 date_range 的 freq 取值可用 Offset 對象,同時在 pandas 中幾乎每一個 Offset 對象綁定了日期偏置字符串( frequencies strings/offset aliases ),可以指定 Offset 對應的字符串來替代使用。下面舉一些常見的例子。
In [91]: pd.date_range('20200101','20200331', freq='MS') # 月初
Out[91]: DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'], dtype='datetime64[ns]', freq='MS')
In [92]: pd.date_range('20200101','20200331', freq='M') # 月末
Out[92]: DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31'], dtype='datetime64[ns]', freq='M')
In [93]: pd.date_range('20200101','20200110', freq='B') # 工作日
Out[93]:
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-06',
'2020-01-07', '2020-01-08', '2020-01-09', '2020-01-10'],
dtype='datetime64[ns]', freq='B')
In [94]: pd.date_range('20200101','20200201', freq='W-MON') # 周一
Out[94]: DatetimeIndex(['2020-01-06', '2020-01-13', '2020-01-20', '2020-01-27'], dtype='datetime64[ns]', freq='W-MON')
In [95]: pd.date_range('20200101','20200201',
....: freq='WOM-1MON') # 每月第一個周一
....:
Out[95]: DatetimeIndex(['2020-01-06'], dtype='datetime64[ns]', freq='WOM-1MON')
上面的這些字符串,等價于使用如下的 Offset 對象:
In [96]: pd.date_range('20200101','20200331',
....: freq=pd.offsets.MonthBegin())
....:
Out[96]: DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'], dtype='datetime64[ns]', freq='MS')
In [97]: pd.date_range('20200101','20200331',
....: freq=pd.offsets.MonthEnd())
....:
Out[97]: DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31'], dtype='datetime64[ns]', freq='M')
In [98]: pd.date_range('20200101','20200110', freq=pd.offsets.BDay())
Out[98]:
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-06',
'2020-01-07', '2020-01-08', '2020-01-09', '2020-01-10'],
dtype='datetime64[ns]', freq='B')
In [99]: pd.date_range('20200101','20200201',
....: freq=pd.offsets.CDay(weekmask='Mon'))
....:
Out[99]: DatetimeIndex(['2020-01-06', '2020-01-13', '2020-01-20', '2020-01-27'], dtype='datetime64[ns]', freq='C')
In [100]: pd.date_range('20200101','20200201',
.....: freq=pd.offsets.WeekOfMonth(week=0,weekday=0))
.....:
Out[100]: DatetimeIndex(['2020-01-06'], dtype='datetime64[ns]', freq='WOM-1MON')

五、時序中的滑窗與分組
- 滑動窗口
所謂時序的滑窗函數(shù),即把滑動窗口用 freq 關鍵詞代替,下面給出一個具體的應用案例:在股票市場中有一個指標為 BOLL 指標,它由中軌線、上軌線、下軌線這三根線構成,具體的計算方法分別是 N 日均值線、 N 日均值加兩倍 N 日標準差線、 N 日均值減兩倍 N 日標準差線。利用 rolling 對象計算 N=30 的 BOLL 指標可以如下寫出:
In [101]: import matplotlib.pyplot as plt
In [102]: idx = pd.date_range('20200101', '20201231', freq='B')
In [103]: np.random.seed(2020)
In [104]: data = np.random.randint(-1,2,len(idx)).cumsum() # 隨機游動構造模擬序列
In [105]: s = pd.Series(data,index=idx)
In [106]: s.head()
Out[106]:
2020-01-01 -1
2020-01-02 -2
2020-01-03 -1
2020-01-06 -1
2020-01-07 -2
Freq: B, dtype: int32
In [107]: r = s.rolling('30D')
In [108]: plt.plot(s)
Out[108]: [<matplotlib.lines.Line2D at 0x1520d5bb548>]
In [109]: plt.title('BOLL LINES')
Out[109]: Text(0.5, 1.0, 'BOLL LINES')
In [110]: plt.plot(r.mean())
Out[110]: [<matplotlib.lines.Line2D at 0x1520d78fec8>]
In [111]: plt.plot(r.mean()+r.std()*2)
Out[111]: [<matplotlib.lines.Line2D at 0x1520d78f788>]
In [112]: plt.plot(r.mean()-r.std()*2)
Out[112]: [<matplotlib.lines.Line2D at 0x1520d6f3788>]

對于 shift 函數(shù)而言,作用在 datetime64 為索引的序列上時,可以指定 freq 單位進行滑動:
In [113]: s.shift(freq='50D').head()
Out[113]:
2020-02-20 -1
2020-02-21 -2
2020-02-22 -1
2020-02-25 -1
2020-02-26 -2
dtype: int32
另外, datetime64[ns] 的序列進行 diff 后就能夠得到 timedelta64[ns] 的序列,這能夠使用戶方便地觀察有序時間序列的間隔:
In [114]: my_series = pd.Series(s.index)
In [115]: my_series.head()
Out[115]:
0 2020-01-01
1 2020-01-02
2 2020-01-03
3 2020-01-06
4 2020-01-07
dtype: datetime64[ns]
In [116]: my_series.diff(1).head()
Out[116]:
0 NaT
1 1 days
2 1 days
3 3 days
4 1 days
dtype: timedelta64[ns]
- 重采樣
重采樣對象 resample 和第四章中分組對象 groupby 的用法類似,前者是針對時間序列的分組計算而設計的分組對象。
例如,對上面的序列計算每10天的均值:
In [117]: s.resample('10D').mean().head()
Out[117]:
2020-01-01 -2.000000
2020-01-11 -3.166667
2020-01-21 -3.625000
2020-01-31 -4.000000
2020-02-10 -0.375000
Freq: 10D, dtype: float64
同時,如果沒有內(nèi)置定義的處理函數(shù),可以通過 apply 方法自定義:
In [118]: s.resample('10D').apply(lambda x:x.max()-x.min()).head() # 極差
Out[118]:
2020-01-01 3
2020-01-11 4
2020-01-21 4
2020-01-31 2
2020-02-10 4
Freq: 10D, dtype: int32
在 resample 中要特別注意組邊界值的處理情況,默認情況下起始值的計算方法是從最小值時間戳對應日期的午夜 00:00:00 開始增加 freq ,直到不超過該最小時間戳的最大時間戳,由此對應的時間戳為起始值,然后每次累加 freq 參數(shù)作為分割結點進行分組,區(qū)間情況為左閉右開。下面構造一個不均勻的例子:
In [119]: idx = pd.date_range('20200101 8:26:35', '20200101 9:31:58', freq='77s')
In [120]: data = np.random.randint(-1,2,len(idx)).cumsum()
In [121]: s = pd.Series(data,index=idx)
In [122]: s.head()
Out[122]:
2020-01-01 08:26:35 -1
2020-01-01 08:27:52 -1
2020-01-01 08:29:09 -2
2020-01-01 08:30:26 -3
2020-01-01 08:31:43 -4
Freq: 77S, dtype: int32
下面對應的第一個組起始值為 08:24:00 ,其是從當天0點增加72個 freq=7 min 得到的,如果再增加一個 freq 則超出了序列的最小時間戳 08:26:35 :
In [123]: s.resample('7min').mean().head()
Out[123]:
2020-01-01 08:24:00 -1.750000
2020-01-01 08:31:00 -2.600000
2020-01-01 08:38:00 -2.166667
2020-01-01 08:45:00 0.200000
2020-01-01 08:52:00 2.833333
Freq: 7T, dtype: float64
有時候,用戶希望從序列的最小時間戳開始依次增加 freq 進行分組,此時可以指定 origin 參數(shù)為 start :
In [124]: s.resample('7min', origin='start').mean().head()
Out[124]:
2020-01-01 08:26:35 -2.333333
2020-01-01 08:33:35 -2.400000
2020-01-01 08:40:35 -1.333333
2020-01-01 08:47:35 1.200000
2020-01-01 08:54:35 3.166667
Freq: 7T, dtype: float64
在返回值中,要注意索引一般是取組的第一個時間戳,但 M, A, Q, BM, BA, BQ, W 這七個是取對應區(qū)間的最后一個時間戳:
In [125]: s = pd.Series(np.random.randint(2,size=366),
.....: index=pd.date_range('2020-01-01',
.....: '2020-12-31'))
.....:
In [126]: s.resample('M').mean().head()
Out[126]:
2020-01-31 0.451613
2020-02-29 0.448276
2020-03-31 0.516129
2020-04-30 0.566667
2020-05-31 0.451613
Freq: M, dtype: float64
In [127]: s.resample('MS').mean().head() # 結果一樣,但索引不同
Out[127]:
2020-01-01 0.451613
2020-02-01 0.448276
2020-03-01 0.516129
2020-04-01 0.566667
2020-05-01 0.451613
Freq: MS, dtype: float64