時間序列

日期和時間數(shù)據(jù)的類型及工具

from datetime import datetime
# datetime
now = datetime.now()
print(now.year,now.month,now.day)

# datetime.timedelta表示兩個datetime對象之間的時間差
delta = datetime(2011,1,7)-datetime(2008,6,24,8,15) # 926 days, 15:45:00
delta.days # 926
delta.seconds # 56700

# 可以給datetime對象加上(或減去)一個或多個timedelta
from datetime import timedelta
start = datetime(2011,1,7)
start+timedelta(12)
start-2*timedelta(12)

# 利用str或strftime方法并傳入指定格式,可以將datetime對象和pandas的Timestamp對象格式化為字符串
stamp = datetime(2011,1,3)
str(stamp) # '2011-01-03 00:00:00'
stamp.strftime("%Y-%m-%d") # '2011-01-03'

# 使用datetime.strptime和這些格式編碼(但不能使用某些編碼,比如%F),可以將字符串轉(zhuǎn)換為日期:
value = "2011-01-03"
datetime.strptime(value,"%Y-%m-%d")

datetime模塊中的數(shù)據(jù)類型:

  • date:以公歷形式存儲日歷日期(年、月、日)
  • time:將時間存儲為時、分、秒和微秒
  • datetime:存儲日期和時間
  • timedelta:兩個datetime值之間的差(日、秒、微秒)
  • tzinfo:存儲時區(qū)信息的基礎類型

datetime格式說明(兼容ISO C89):

  • %Y:四位數(shù)的年

  • %y:兩位數(shù)的年

  • %m:兩位數(shù)的月[01,12]

  • %d:兩位數(shù)的日[01,31]

  • %H:小時(24小時制)[00,23]

  • %I:小時(12小時制)[01,12]

  • %M:兩位數(shù)的分[00,59]

  • %S:秒[00,61](秒60和61用于閏秒)

  • %f:整數(shù)形式的微秒,零填充(從000000到999999)

  • %j:年中的第幾天,為零填充的整數(shù)(從001到366)

  • %w:用整數(shù)表示的星期幾[0(星期天),6]

  • %u:用整數(shù)表示的星期幾,從1開始,1為星期一

  • %U:每年的第幾周[00,53]:星期天作為每周的第一天,每年第一個星期天之前的若干天屬于“第0周”

  • %W:每年的第幾周[00,53]:星期一被認為是每周的第一天,每年第一個星期一之前的若干天屬于“第0周”

  • %z:以+HHMM或-HHMM表示的UTC時區(qū)偏移量,如果沒有時區(qū)則為空

  • %Z:字符串形式的時區(qū)名,如果沒有時區(qū)則為空字符串

  • %F:%Y-%m-%d的快捷簡寫(例如,2012-4-18)

  • %D:%m/%d/%y的快捷簡寫(例如,04/18/12)

pandas通常用于處理日期數(shù)組,不管這些日期是DataFrame的軸索引還是列:

# pandas.to_datetime方法可以解析多種不同的日期表示形式
datestrs = ["2011-07-06 12:00:00","2011-08-06 00:00:00"]
pd.to_datetime(datestrs) # DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='datetime64[ns]', freq=None)

# pandas.to_datetime方法還可以處理應當作為缺失值的值(None、空字符串等)
idx = pd.to_datetime(datestrs+[None]) # NaT(Not a Time)用于表示pandas中時間戳數(shù)據(jù)的空值
# DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)

特定地區(qū)的日期格式化:

  • %a:簡寫的工作日名稱
  • %A:工作日全稱
  • %b:簡寫的月份
  • %B:完整的月份
  • %c:完整的日期和時間(例如,Tue01May201204:20:57PM)
  • %p:表示AM或PM的本地格式
  • %x:適合本地的日期格式(例如,在美國May1,2012會變?yōu)?5/01/2012)
  • %X:適合本地的時間格式(例如,04:24:12PM)

時間序列基礎知識

# 以時間戳為索引的Series
dates = [datetime(2011,1,2),datetime(2011,1,5),
         datetime(2011,1,7),datetime(2011,1,8)
         ,datetime(2011,1,10),datetime(2011,1,12)]
ts = pd.Series(np.random.standard_normal(6),index=dates)
# 不同索引的時間序列之間的算術運算會自動按日期對齊
ts + ts[::2]
# 2011-01-02    2.479692
# 2011-01-05         NaN
# 2011-01-07    3.979818
# 2011-01-08         NaN
# 2011-01-10    0.445173
# 2011-01-12         NaN
# dtype: float64

索引、選取、子集構(gòu)造

# 根據(jù)標簽進行索引和選取數(shù)據(jù)
stamp = ts.index[2]
ts[stamp]
# 傳入一個可以解釋為日期的字符串
ts["2011-01-10"]
# 對于較長的時間序列,只需傳入“年”或“年月”即可輕松選取數(shù)據(jù)的切片
longer_ts = pd.Series(np.random.standard_normal(1000),
                      index=pd.date_range("2000-01-01",periods=1000))
# 2000-01-01    0.149029
# 2000-01-02   -0.436183
# 2000-01-03   -0.285143
# 2000-01-04    1.617606
# 2000-01-05   -0.216020

# 選取2001年5月份的數(shù)據(jù)
longer_ts["2001-05"]
# 時間切片
ts[datetime(2011,1,7):]
ts[datetime(2011,1,7):datetime(2011,1,10)]
# 由于大部分時間序列數(shù)據(jù)都是按照時間先后排序的,因此也可以用不存在于該時間序列中的時間戳對其進行范圍查詢
ts["2011-01-06":"2011-01-11"]

與之前一樣,可以傳入字符串日期、datetime或Timestamp。注意,這樣切片所產(chǎn)生的是原時間序列的視圖,與NumPy數(shù)組的切片運算是一樣的。這意味著不會復制數(shù)據(jù),對切片進行修改會反映到原始數(shù)據(jù)上。

# 還有一個等價的實例方法truncate,也可以截取兩個日期之間的Series
ts.truncate(after="2011-01-09")

# 所有這些操作對DataFrame也有效,并對DataFrame的行進行索引
dates = pd.date_range("2000-01-01",periods=100,freq="W-WED")
long_df = pd.DataFrame(np.random.standard_normal((100,4)),
                       index=dates,
                       columns=["Colorado","Texas","New York","Ohio"])
long_df.loc["2001-05"]

帶有重復索引的時間序列

dates = pd.DatetimeIndex(["2000-01-01","2000-01-02","2000-01-02","2000-01-02","2000-01-03"])
dup_ts = pd.Series(np.arange(5),index=dates)
# 通過檢查索引的is_unique屬性,就可以知道索引是不是唯一的
dup_ts.index.is_unique
# 對這個時間序列進行索引,取決于時間戳是否重復,要么生成標量值,要么生成切片
dup_ts["2000-01-02"]
# 2000-01-02    1
# 2000-01-02    2
# 2000-01-02    3

# 假設你想對具有重復時間戳的數(shù)據(jù)進行聚合,一個辦法是使用groupby,并傳入level=0(存在的唯一層級
grouped = dup_ts.groupby(level=0)
grouped.mean()
grouped.count()

日期的范圍、頻率以及移位

dates = [datetime(2011,1,2),datetime(2011,1,5),
         datetime(2011,1,7),datetime(2011,1,8)
         ,datetime(2011,1,10),datetime(2011,1,12)]
ts = pd.Series(np.random.standard_normal(6),index=dates)
# 通過調(diào)用resample方法,可以將樣本時間序列轉(zhuǎn)換為具有固定日頻率的序列:
# 符串"D"被解釋為每日的頻率
resampler = ts.resample("D")

生成日期范圍

# pandas.date_range還可以用于根據(jù)指定頻率生成特定長度的DatetimeIndex
index = pd.date_range("2012-04-01","2012-06-01")
# 如果只傳入起始日期或結(jié)束日期,還必須傳入要生成的周期個數(shù)
pd.date_range(start="2012-04-01",periods=20)
pd.date_range(end="2012-06-01",periods=20)
# 如果你想生成一個由每月最后一個工作日組成的日期索引,可以傳入頻率"BM"
pd.date_range("2000-01-01","2000-12-01",freq="BM")

基本的時間序列頻率:

別名 偏移量類型 說明
D Day 日歷日的每天
B BusinessDay 工作日的每天
H Hour 每時
T 或 min Minute 每分
S Second 每秒
L 或 ms Milli 每毫秒(即每千分之一秒)
U Micro 每微秒(即每百萬分之一秒)
M MonthEnd 每月最后一個日歷日
BM BusinessMonthEnd 每月最后一個工作日
MS MonthBegin 每月第一個日歷日
BMS BusinessMonthBegin 每月第一個工作日
W-MON,W-TUE,.. Week 從指定的星期幾(MON、TUE、WED、THU、FRI、SAT、SUN)開始算起,每周取日期
WOM-1MON,WOM-2MON,... k0fMonth 產(chǎn)生每月第一、第二、第三或第四周,..的星期幾。例如WOM-3FRI表示每月第3個星期五
Q-JAN, Q-FEB,... QuarterEnd 對于以指定月份(JAN、FEB、MAR、APRMAY、JUN、JUL、AUG、SEP、OCT、NOV、DEC)結(jié)束的年度,每季度最后一月的最后一個日歷日
BQ-JAN,BQ-FEB,.. BusinessQuarterEnd 對于以指定月份結(jié)束的年度,每季度最后一月的最后一個工作日
QS-JAN, QS-FEB,... QuarterBegin 對于以指定月份結(jié)束的年度,每季度最后一月的第一個日歷日
BQS-JAN,BQS-FEB BBusinessQuarterBegin 對于以指定月份結(jié)束的年度,每季度最后一月的第一個工作日
A-JAN,A-FEB,.. YearEnd 每年指定月份(JAN、FEB、MAR、APR、MAY、JUN、JUL、AUG、SEP、OCT、NOV、DEC)的最后一個日歷日
BA-JAN, BA-FEB,... BusinessYearEnd 每年指定月份的最后一個工作日
AS-JAN,AS-FEB,... YearBegin 每年指定月份的第一個日歷日
BAS-JAN,BAS-FEB,... BusinessYearBegin 每年指定月份的第一個工作日
# pandas.date_range默認會保留起始時間戳和結(jié)束時間戳的時間信息(如果有的話)
pd.date_range("2012-05-02 12:56:31",periods=5)
# DatetimeIndex(['2012-05-02 12:56:31', '2012-05-03 12:56:31',
#                '2012-05-04 12:56:31', '2012-05-05 12:56:31',
#                '2012-05-06 12:56:31'],
#               dtype='datetime64[ns]', freq='D')

# 雖然起始日期和結(jié)束日期帶有時間信息,但你希望生成一組標準化到晚間零點的時間戳,normalize選項可以實現(xiàn)該功能
pd.date_range("2012-05-02 12:56:31",periods=5,normalize=True)

頻率和日期偏移量

pandas中的頻率是由基礎頻率和倍數(shù)組成的?;A頻率通常以一個字符串別名表示,比如"M"表示每月,"H"表示每小時。對于每個基礎頻率,都有一個稱為日期偏移量的對象與之對應。

# 每小時的頻率可以用Hour類表示
from pandas.tseries.offsets import Hour,Minute
hour = Hour()
# 傳入一個整數(shù)即可定義偏移量的倍數(shù)
four_hours = Hour(4)
# 對于大多數(shù)應用,無須顯式創(chuàng)建這樣的對象,只需使用諸如"H"或"4H"這樣的字符串別名。在基礎頻率前面加上一個整數(shù)即可創(chuàng)建倍數(shù)
pd.date_range("2000-01-01","2000-01-03 23:59",freq="4H")
# DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00',
#                '2000-01-01 08:00:00', '2000-01-01 12:00:00',
#                '2000-01-01 16:00:00', '2000-01-01 20:00:00',
#                '2000-01-02 00:00:00', '2000-01-02 04:00:00',
#                '2000-01-02 08:00:00', '2000-01-02 12:00:00',
#                '2000-01-02 16:00:00', '2000-01-02 20:00:00',
#                '2000-01-03 00:00:00', '2000-01-03 04:00:00',
#                '2000-01-03 08:00:00', '2000-01-03 12:00:00',
#                '2000-01-03 16:00:00', '2000-01-03 20:00:00'],
#               dtype='datetime64[ns]', freq='4h')

# 大部分偏移量對象都可通過加法進行連接
Hour(2)+Minute(30)
# 也可以傳入頻率字符串,比如"1h30min",它可以被高效地解析為等效的表達式
pd.date_range("2000-01-01",periods=10,freq="1h30min")
list(monthly_dates)
# [Timestamp('2012-01-20 00:00:00'), Timestamp('2012-02-17 00:00:00'), Timestamp('2012-03-16 00:00:00'), Timestamp('2012-04-20 00:00:00'), Timestamp('2012-05-18 00:00:00'), Timestamp('2012-06-15 00:00:00'), Timestamp('2012-07-20 00:00:00'), Timestamp('2012-08-17 00:00:00')]

對超前和滯后數(shù)據(jù)進行移位

移位(shifting)是指沿著時間軸將數(shù)據(jù)前移或后移。Series和DataFrame都有一個shift方法,用于執(zhí)行單純的前移或后移操作,并保持索引不變:

ts = pd.Series(np.random.standard_normal(4),
               index=pd.date_range("2000-01-01",periods=4,freq="M"))
# 2000-01-31    0.677049
# 2000-02-29   -1.828040
# 2000-03-31    0.878885
# 2000-04-30   -1.142629
# Freq: ME, dtype: float64

ts.shift(2)
# 2000-01-31         NaN
# 2000-02-29         NaN
# 2000-03-31   -1.247258
# 2000-04-30   -0.700633
# Freq: ME, dtype: float64

ts.shift(-2)
# 2000-01-31    0.802095
# 2000-02-29    0.736439
# 2000-03-31         NaN
# 2000-04-30         NaN
# Freq: ME, dtype: float64

# shift通常用于計算一個時間序列或多個時間序列(如DataFrame的列)中的連續(xù)百分比變化??梢赃@樣表達:
ts / ts.shift(1) - 1
# 由于單純的移位操作不會修改索引,因此部分數(shù)據(jù)會被丟棄。如果頻率已知,則可以將其傳給shift以便實現(xiàn)對時間戳進行移位,而不是對數(shù)據(jù)進行簡單位移:
ts.shift(2,freq="M")
# 2000-03-31   -1.202354
# 2000-04-30   -2.380137
# 2000-05-31   -1.475926
# 2000-06-30    0.082859
# Freq: ME, dtype: float64

ts.shift(3,freq="D") # 天
# 這里的T代表的是分鐘。注意,參數(shù)freq表明移位是針對時間戳的,但沒有修改底層的數(shù)據(jù)頻率(如果存在的話)。
ts.shift(1,freq="90T")

通過偏移量對日期進行移位:

# pandas的日期偏移量還可以用在datetime或Timestamp對象上
from pandas.tseries.offsets import Day,MonthEnd
now = datetime(2011,11,17)
now + 3 * Day()

# pandas的日期偏移量還可以用在datetime或Timestamp對象上
now+MonthEnd()
now + MonthEnd(2)

# 通過錨定偏移量的rollforward和rollback方法,可顯式地將日期向前或向后“滾動
offset = MonthEnd()
offset.rollback(now) # 向后
offset.rollback(now) # 向前

# 日期偏移量還有一個巧妙的用法,即結(jié)合groupby使用這兩個“滾動”方法
ts = pd.Series(np.random.standard_normal(20),
               index=pd.date_range("2000-01-15",periods=20,freq="4D"))
ts.groupby(MonthEnd().rollforward).mean()
# 更簡單、更快速地實現(xiàn)該功能的辦法是使用resample
ts.resample("M").mean()

時區(qū)處理

在Python中,時區(qū)信息來自第三方庫pytz(使用pip或conda進行安裝),它使Python可以使用Olson數(shù)據(jù)庫(匯編了世界時區(qū)信息)。

pytz.common_timezones[-5:] # ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']
# 要從pytz中獲取時區(qū)對象,使用pytz.timezone即可
tz = pytz.timezone("America/New_York") # America/New_York

時區(qū)本地化和轉(zhuǎn)換

默認情況下,pandas中的時間序列就是簡單的時區(qū)。

dates = pd.date_range("2012-03-09 09:30",periods=6)
ts = pd.Series(np.random.standard_normal(len(dates)),index=dates)
ts.index.tz # None
# 可以用時區(qū)集合生成日期范圍:
pd.date_range("2012-03-09 09:30",periods=10,tz="UTC")
# DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00',
#                '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
#                '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00',
#                '2012-03-15 09:30:00+00:00', '2012-03-16 09:30:00+00:00',
#                '2012-03-17 09:30:00+00:00', '2012-03-18 09:30:00+00:00'],
#               dtype='datetime64[ns, UTC]', freq='D')

# 從簡單時區(qū)到本地化時區(qū)(以特定時區(qū)重新觀測數(shù)據(jù))的轉(zhuǎn)換是通過tz_localize方法實現(xiàn)的:
ts_utc = ts.tz_localize("UTC")
# 一旦時間序列被本地化到某個特定時區(qū),就可以用tz_convert將其轉(zhuǎn)換到其他時區(qū)了
ts_utc.tz_convert("America/New_York")
# tz_localize和tz_convert也是DatetimeIndex的實例方法:
ts_utc.index.tz_localize("Asia/Shanghai")

對時區(qū)型時間戳對象的操作

# 獨立的Timestamp對象也能從簡單型本地化為時區(qū)型,并從一個時區(qū)轉(zhuǎn)換到另一個時區(qū)
stamp = pd.Timestamp("2011-03-12 04:00")
stamp_utc = stamp.tz_localize("utc")
stamp_utc.tz_convert("America/New_York")
# 在創(chuàng)建Timestamp時,還可以傳入時區(qū)信息:
stamp_moscow = pd.Timestamp("2011-03-12 04:00",tz="Europe/Moscow")
# 時區(qū)型Timestamp對象在內(nèi)部保存了UTC時間戳值——一個自UNIX紀元(1970年1月1日)算起的納秒數(shù)。
# 因此轉(zhuǎn)換時區(qū)不會改變內(nèi)部UTC值:
stamp_utc.value
stamp_utc.tz_convert("America/New_York").value

當使用pandas的DateOffset對象執(zhí)行時間算術運算時,運算過程會自動關注是否存在夏令時轉(zhuǎn)變期。這里,我們創(chuàng)建了在DST轉(zhuǎn)變之前的時間戳(通過運算在轉(zhuǎn)變期的前后移動。

# 來看夏令時轉(zhuǎn)變前的30分鐘
stamp = pd.Timestamp("2012-03-11 01:30",tz="US/Eastern") # 2012-03-11 01:30:00-05:00
stamp + Hour() # 2012-03-11 03:30:00-04:00

# 夏令時轉(zhuǎn)變前的90分鐘
stamp = pd.Timestamp("2012-03-11 00:30",tz="US/Eastern") # 2012-03-11 00:30:00-05:00
stamp +2 * Hour() # 2012-03-11 03:30:00-04:00

不同時區(qū)之間的運算

如果兩個時間序列的時區(qū)不同,在將它們合并到一起時,最終結(jié)果就會是UTC。由于時間戳其實是存儲在UTC內(nèi)部的,因此這是個直接運算,不需要做任何轉(zhuǎn)換:

ts1 = ts[:7].tz_localize("Europe/London")
ts2 = ts1[2:].tz_convert("Europe/Moscow")
result = ts1 + ts2
result.index
# DatetimeIndex(['2012-03-07 09:30:00+00:00', '2012-03-08 09:30:00+00:00',
#                '2012-03-09 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
#                '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00',
#                '2012-03-15 09:30:00+00:00'],
#               dtype='datetime64[ns, UTC]', freq=None)

簡單型和時區(qū)型的對象間不支持運算,如果運算將會拋出異常。

周期及其算術運算

周期表示的是時間段,比如數(shù)日、數(shù)月、數(shù)季、數(shù)年等。pandas.Period類所表示的就是這種數(shù)據(jù)類型,它需要用到字符串或整數(shù),以及表中的頻率:

# Period對象表示的是從2011年1月1日到2011年12月31日之間的整段時間
p = pd.Period("2011",freq="A-DEC")
# 只需對周期對象加上或減去一個整數(shù),就可以移動其頻率
p + 5 # 2016
p-2
# 如果兩個周期對象擁有相同的頻率,則二者的差就是它們之間日期偏移量的單位數(shù)量
pd.Period("2014",freq="A-DEC") - p # <3 * YearEnds: month=12>

# period_range函數(shù)可用于創(chuàng)建規(guī)則的周期范圍
periods = pd.period_range("2000-01-01","2000-06-30",freq="M") 
# PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '2000-06'], dtype='period[M]')

# PeriodIndex類保存了一組周期序列,它可以在任何pandas數(shù)據(jù)結(jié)構(gòu)中用作軸索引
pd.Series(np.random.standard_normal(6),index=periods)

# 如果你有一個字符串數(shù)組,也可以使用PeriodIndex類(所有值均為周期對象)
values = ["2001Q3","2002Q2","2003Q1"]
index = pd.PeriodIndex(values,freq="Q-DEC")

周期的頻率轉(zhuǎn)換

周期和PeriodIndex對象都可以通過其asfreq方法轉(zhuǎn)換為別的頻率。

# 假設我們有一個年度周期,希望將其轉(zhuǎn)換為當年年初或年末的一個月度周期,可以如下實現(xiàn):
p = pd.Period("2011",freq="A-DEC")
p.asfreq("M",how="start")
p.asfreq("M",how="end")
p.asfreq("M")

可以將Period('2011','A-DEC')看作一個時間段中的游標,該時間段被劃分為多個月度周期。對于不是以十二月作為結(jié)束月的財政年度,相應的月度的子周期是不同的:

[圖片上傳失敗...(image-86f163-1749773119693)]

# 在將高頻率轉(zhuǎn)換為低頻率時,根據(jù)父周期的歸屬情況,pandas確定了周期
# 在A-JUN頻率中,月份Aug-2011實際上是屬于周期2012的
p = pd.Period("Aug-2011","M")
p.asfreq("A-JUN") # 2012

# 完整的PeriodIndex或時間序列也可以用相同的語法進行轉(zhuǎn)換
periods = pd.period_range("2006","2009",freq="A-DEC")
ts = pd.Series(np.random.standard_normal(len(periods)),index=periods)
# 2006    0.105699
# 2007   -0.309283
# 2008   -0.925747
# 2009   -0.159156
# Freq: Y-DEC, dtype: float64
ts.asfreq("M",how="start")
# 2006-01    0.105699
# 2007-01   -0.309283
# 2008-01   -0.925747
# 2009-01   -0.159156
# Freq: M, dtype: float64
ts.asfreq("B",how="end")
# 2006-12-29    1.278334
# 2007-12-31    0.366932
# 2008-12-31    0.710241
# 2009-12-31   -0.168008
# Freq: B, dtype: float64

季度周期頻率

季度數(shù)據(jù)在會計、金融等領域很常見。許多季度數(shù)據(jù)都會涉及“財年末”的概念,通常是一年12個月中某月的最后一個日歷日或工作日。就這一點來說,周期2012Q4根據(jù)財年末的不同會有不同的含義。

# pandas支持12種可能的季度頻率,即Q-JAN到Q-DEC:
p = pd.Period("2012Q4",freq="Q-JAN") # 在以1月結(jié)束的財年中,2012Q4是從2011年11月到2012年1月。

不同季度頻率之間的轉(zhuǎn)換如下圖:

[圖片上傳失敗...(image-6ee83b-1749773119693)]

# 要獲取該季度倒數(shù)第二個工作日下午4點的時間戳,可以這樣做
p4pm = (p.asfreq("B",how="end") - 1).asfreq("T",how="start") + 16 * 60
# pandas.period_range可用于生成季度時間范圍。季度時間范圍的算術運算跟上面是一樣的:
periods = pd.period_range("2011Q3","2012Q4",freq="Q-JAN")
ts = pd.Series(np.arange(len(periods)),index=periods)
# 2011Q3    0
# 2011Q4    1
# 2012Q1    2
# 2012Q2    3
# 2012Q3    4
# 2012Q4    5
# Freq: Q-JAN, dtype: int64
new_periods = (periods.asfreq("B","end") -1 ).asfreq("H","start") + 1
ts.index = new_periods.to_timestamp()
# 2010-10-28 01:00:00    0
# 2011-01-28 01:00:00    1
# 2011-04-28 01:00:00    2
# 2011-07-28 01:00:00    3
# 2011-10-28 01:00:00    4
# 2012-01-30 01:00:00    5
# dtype: int64

時間戳和周期的相互轉(zhuǎn)換

通過使用to_period方法,可以將由時間戳索引的Series和DataFrame對象轉(zhuǎn)換為以周期作為索引:

dates = pd.date_range("2000-01-01",periods=3,freq="M")
ts = pd.Series(np.random.standard_normal(3),index=dates)
# 2000-01-31    0.409628
# 2000-02-29   -1.218067
# 2000-03-31   -0.473066
# Freq: ME, dtype: float64
pts = ts.to_period()
# 2000-01    0.059742
# 2000-02    1.174967
# 2000-03   -0.645208
# Freq: M, dtype: float64

由于周期指的是非重疊時間段,因此對于給定的頻率,一個時間戳只能屬于一個周期。新PeriodIndex的頻率默認是從時間戳推斷而來的,你也可以指定任何支持的頻率:

dates = pd.date_range("2000-01-29",periods=6)
ts2 = pd.Series(np.random.standard_normal(6),index=dates)
# 2000-01-29    0.467004
# 2000-01-30    0.879966
# 2000-01-31    0.032719
# 2000-02-01   -1.150564
# 2000-02-02    0.601257
# 2000-02-03    0.252213
# Freq: D, dtype: float64
ts2.to_period("M")
# 2000-01    0.467004
# 2000-01    0.879966
# 2000-01    0.032719
# 2000-02   -1.150564
# 2000-02    0.601257
# 2000-02    0.252213
# Freq: M, dtype: float64

# 要轉(zhuǎn)換回時間戳,使用to_timestamp即可
pts.to_timestamp(how="end")

通過數(shù)組創(chuàng)建PeriodIndex

固定頻率的數(shù)據(jù)集通常會將時間信息分開存放在多個列中。

year = pd.Series([1959,1960,1961])
quarter = pd.Series([1,2,3])
# PeriodIndex(['1959Q1', '1960Q2', '1961Q3'], dtype='period[Q-DEC]')
index = pd.PeriodIndex(year=year,quarter=quarter,freq="Q-DEC")

重采樣及頻率轉(zhuǎn)換

重采樣指的是將時間序列從一個頻率轉(zhuǎn)換到另一個頻率的處理過程。將高頻率數(shù)據(jù)連接到低頻率稱為降采樣,而將低頻率數(shù)據(jù)轉(zhuǎn)換到高頻率則稱為升采樣。并不是所有的重采樣都能被劃分到這兩大類中。例如,將W-WED(每周三)轉(zhuǎn)換為W-FRI既不是降采樣也不是升采樣。

# resample有一個類似于groupby的API,調(diào)用resample可以對數(shù)據(jù)進行分組,然后調(diào)用聚合函數(shù):
dates = pd.date_range("2000-01-01",periods=100)
ts = pd.Series(np.random.standard_normal(len(dates)),index=dates)
ts.resample("M").mean()
# 2000-01-31    0.119976
# 2000-02-29   -0.394672
# 2000-03-31    0.157229
# 2000-04-30   -0.143676
# Freq: ME, dtype: float64

ts.resample("M",kind="period").mean()
# 2000-01    0.230413
# 2000-02    0.073806
# 2000-03    0.106564
# 2000-04   -0.121623
# Freq: M, dtype: float64

resample方法的參數(shù):

  • rule:用于指明重采樣頻率的字符串、DateOffset、timedelta對象(例如,M、5min或Second(15))
  • axis:重采樣的軸,默認為axis=0
  • fill_method:升采樣時如何插值,比如,"ffill"或"bfill"。默認不插值
  • closed:在降采樣中各時間段的哪一端是閉合(即包含)的,“right"或“l(fā)eft"
  • label:在降采樣中如何設置聚合值的標簽,分箱邊界為"right"或"left"(例如,對于9:30到9:35之間的五分鐘區(qū)間,標簽為9:30或9:35)
  • limit:在前向填充或后向填充時,允許填充的最大周期數(shù)
  • kind:聚合到周期("period")或時間戳("timestamp"),默認聚合到時間序列的索引類型
  • convention:當對周期進行重采樣時,將低頻周期轉(zhuǎn)換為高頻周期的慣用法("start”或"end"),默認是"start"
  • origin:用于確定重采樣分箱邊界的“基礎”時間戳,可以是"epoch"、"start"、"start_day"、"end"、"end_day"其中之一,完整細節(jié)見resample的文檔字符串
  • offset:添加到origin的偏移時間差,默認為None

降采樣

在用resample對數(shù)據(jù)進行降采樣時,需要考慮兩件事:

  • 各區(qū)間哪邊是閉合的。
  • 如何對各個聚合分箱打標簽,是用區(qū)間的開頭還是末尾。
# 我們來看一些頻率為一分鐘的數(shù)據(jù)
dates = pd.date_range("2000-01-01",periods=12,freq="T")
ts = pd.Series(np.arange(len(dates)),index=dates)
# 通過各組求和的方式將這些數(shù)據(jù)聚合到“五分鐘”的數(shù)據(jù)塊或柱狀圖的柱中
ts.resample("5min").sum() # 傳入的頻率將會以“五分鐘”的增量定義分箱邊界。對于這個頻率,分箱是默認包含左邊界的,因此00:00到00:05的區(qū)間是包含00:00的
# 2000-01-01 00:00:00    10
# 2000-01-01 00:05:00    35
# 2000-01-01 00:10:00    21
# Freq: 5min, dtype: int64

# 最終的時間序列是以各分箱左邊界的時間戳進行標記的
ts.resample("5min",closed="right").sum()
# 1999-12-31 23:55:00     0
# 2000-01-01 00:00:00    15
# 2000-01-01 00:05:00    40
# 2000-01-01 00:10:00    11
# Freq: 5min, dtype: int64

# 傳入label="right",即可用分箱的右邊界對其進行標記
ts.resample("5min",closed="right",label="right").sum()
# 2000-01-01 00:00:00     0
# 2000-01-01 00:05:00    15
# 2000-01-01 00:10:00    40
# 2000-01-01 00:15:00    11
# Freq: 5min, dtype: int64

# 你可能希望對結(jié)果索引做一些位移,比如從右邊界減去一秒,使其更容易分清該時間戳到底表示的是哪個區(qū)間;只需對結(jié)果索引添加一個偏移量即可實現(xiàn)
from pandas.tseries.frequencies import to_offset
result = ts.resample("5min",closed="right",label="right").sum()
result.index = result.index + to_offset("-1s")
# 1999-12-31 23:59:59     0
# 2000-01-01 00:04:59    15
# 2000-01-01 00:09:59    40
# 2000-01-01 00:14:59    11
# Freq: 5min, dtype: int64

開-高-低-收(OHLC)重采樣:

在金融領域,一種常用的時間序列聚合方式是計算各個桶的4個值,即第一個值(open,開盤)、最后一個值(close,收盤)、最大值(high,最高)以及最小值(low,最低)。

# 通過ohlc聚合函數(shù),即可得到一個含有這4種聚合值的DataFrame,整個過程很高效,只需經(jīng)過一次函數(shù)調(diào)用
ts = pd.Series(np.random.permutation(np.arange(len(dates))),index=dates)
ts.resample("5min").ohlc()
#                      open  high  low  close
# 2000-01-01 00:00:00     8    10    0      0
# 2000-01-01 00:05:00     6     9    1      3
# 2000-01-01 00:10:00     4    11    4     11

升采樣和插值

升采樣是將數(shù)據(jù)從低頻率轉(zhuǎn)換為高頻率,不需要做聚合。

frame = pd.DataFrame(np.random.standard_normal((2,4)),
                     index=pd.date_range("2000-01-01",periods=2,freq="W-WED"),
                     columns=["Colorado","Texas","New York","Ohio"])
#             Colorado     Texas  New York      Ohio
# 2000-01-05 -0.194153  0.556775  2.156055 -0.341275
# 2000-01-12  0.214674 -0.064093 -1.073947  1.186780

# 使用asfreq方法將其轉(zhuǎn)換為高頻率數(shù)據(jù)
df_daily = frame.resample("D").asfreq()
#             Colorado     Texas  New York      Ohio
# 2000-01-05  1.241783 -0.911819 -0.998386 -0.877590
# 2000-01-06       NaN       NaN       NaN       NaN
# 2000-01-07       NaN       NaN       NaN       NaN
# 2000-01-08       NaN       NaN       NaN       NaN
# 2000-01-09       NaN       NaN       NaN       NaN
# 2000-01-10       NaN       NaN       NaN       NaN
# 2000-01-11       NaN       NaN       NaN       NaN
# 2000-01-12  0.953335  0.576465  0.822951  0.607682

# 假設你想用前面的每周數(shù)值來填充非星期三的日期。
# fillna和reindex方法中可用的填充和插值方法也可以用于重采樣:
frame.resample("D").ffill()
#             Colorado     Texas  New York      Ohio
# 2000-01-05  0.542464  0.695223 -0.139967  0.744035
# 2000-01-06  0.542464  0.695223 -0.139967  0.744035
# 2000-01-07  0.542464  0.695223 -0.139967  0.744035
# 2000-01-08  0.542464  0.695223 -0.139967  0.744035
# 2000-01-09  0.542464  0.695223 -0.139967  0.744035
# 2000-01-10  0.542464  0.695223 -0.139967  0.744035
# 2000-01-11  0.542464  0.695223 -0.139967  0.744035
# 2000-01-12 -1.067251 -0.281277  0.985272 -0.906820

# 這里也可以只填充指定的周期數(shù),以限制觀測值的持續(xù)范圍:
frame.resample("D").ffill(limit=2)
#             Colorado     Texas  New York      Ohio
# 2000-01-05 -0.515932 -1.563824 -1.344832 -0.353373
# 2000-01-06 -0.515932 -1.563824 -1.344832 -0.353373
# 2000-01-07 -0.515932 -1.563824 -1.344832 -0.353373
# 2000-01-08       NaN       NaN       NaN       NaN
# 2000-01-09       NaN       NaN       NaN       NaN
# 2000-01-10       NaN       NaN       NaN       NaN
# 2000-01-11       NaN       NaN       NaN       NaN
# 2000-01-12 -2.730212  1.302561  0.010783  1.074983

# 新的日期索引不必與舊的索引重疊
frame.resample("W-THU").ffill()
#             Colorado     Texas  New York      Ohio
# 2000-01-06 -1.033289  0.670375 -0.086887  0.903338
# 2000-01-13  0.303904 -1.170826 -0.807921 -0.167185

使用周期進行重采樣

對使用周期作為索引的數(shù)據(jù)進行重采樣,與時間戳的情況類似:

frame = pd.DataFrame(np.random.standard_normal((24,4)),
                     index=pd.period_range("1-2000","12-2001",freq="M"),
                     columns=["Colorado","Texas","New York","Ohio"])
frame.head()
#          Colorado     Texas  New York      Ohio
# 2000-01  0.599385 -1.026146  0.377846 -0.769628
# 2000-02 -0.415738 -1.616806  0.171898  2.030871
# 2000-03  0.230840  0.893198  0.129828  0.970785
# 2000-04  0.560167  1.678211 -0.648175  0.604873
# 2000-05 -0.484082 -1.751307  0.541053 -0.738717
annual_frame = frame.resample("A-DEC").mean()
#       Colorado     Texas  New York      Ohio
# 2000 -0.260185  0.356527 -0.098587  0.189736
# 2001 -0.034842  0.078273 -0.026577  0.041263

升采樣要稍微麻煩一些,在重采樣之前,你必須決定在新頻率時間段的哪一端放置數(shù)值。參數(shù)convention默認為"start",也可設置為"end":

# Q-DEC:每季度,年末為12月
annual_frame.resample("Q-DEC").ffill()
#         Colorado     Texas  New York      Ohio
# 2000Q1  0.620187 -0.141949  0.157756  0.016274
# 2000Q2  0.620187 -0.141949  0.157756  0.016274
# 2000Q3  0.620187 -0.141949  0.157756  0.016274
# 2000Q4  0.620187 -0.141949  0.157756  0.016274
# 2001Q1 -0.089812 -0.184859 -0.495531  0.156181
# 2001Q2 -0.089812 -0.184859 -0.495531  0.156181
# 2001Q3 -0.089812 -0.184859 -0.495531  0.156181
# 2001Q4 -0.089812 -0.184859 -0.495531  0.156181

annual_frame.resample("Q-DEC",convention="end").asfreq()
#         Colorado     Texas  New York      Ohio
# 2000Q4 -0.650872 -0.281441  0.046464  0.029406
# 2001Q1       NaN       NaN       NaN       NaN
# 2001Q2       NaN       NaN       NaN       NaN
# 2001Q3       NaN       NaN       NaN       NaN
# 2001Q4  0.243617  0.253209 -0.287791 -0.081825

由于周期指的是時間段,因此升采樣和降采樣的規(guī)則比較嚴格:

  • 在降采樣中,目標頻率必須是頻率源的子周期。
  • 在升采樣中,目標頻率必須是頻率源的父周期。

如果不滿足這些條件,就會引發(fā)異常。這主要會影響季度、年度、每周的頻率。例如,由Q-MAR定義的時間段只能和A-MAR、A-JUN、A-SEP、A-DEC對齊。

對分組時間進行重采樣

對于時間序列數(shù)據(jù),從語義上講,resample方法實質(zhì)是基于時間間隔的分組運算。

N=15
times = pd.date_range("2017-05-20",freq="1min",periods=N)
df = pd.DataFrame({"time":times,"value":np.arange(N)})
# 可以根據(jù)"time"進行索引,然后進行重采樣:
df.set_index("time").resample("5min").count()
#                      value
# time
# 2017-05-20 00:00:00      5
# 2017-05-20 00:05:00      5
# 2017-05-20 00:10:00      5

df2 = pd.DataFrame({"time":times.repeat(3),"key":np.tile(["a","b","c"],N),
                    "value":np.arange(N * 3)})
df2.head(7)
#                  time key  value
# 0 2017-05-20 00:00:00   a      0
# 1 2017-05-20 00:00:00   b      1
# 2 2017-05-20 00:00:00   c      2
# 3 2017-05-20 00:01:00   a      3
# 4 2017-05-20 00:01:00   b      4
# 5 2017-05-20 00:01:00   c      5
# 6 2017-05-20 00:02:00   a      6

# 為了對各個"key"值做相同的重采樣,引入pandas.Grouper對象:
time_key = pd.Grouper(freq="5min")
# 接下來設置時間索引,以"key"和time_key進行分組,并進行聚合:
resampled = (df2.set_index("time").groupby(["key",time_key]).sum())
#                          value
# key time
# a   2017-05-20 00:00:00     30
#     2017-05-20 00:05:00    105
#     2017-05-20 00:10:00    180
# b   2017-05-20 00:00:00     35
#     2017-05-20 00:05:00    110
#     2017-05-20 00:10:00    185
# c   2017-05-20 00:00:00     40
#     2017-05-20 00:05:00    115
#     2017-05-20 00:10:00    190

resampled.reset_index()
#   key                time  value
# 0   a 2017-05-20 00:00:00     30
# 1   a 2017-05-20 00:05:00    105
# 2   a 2017-05-20 00:10:00    180
# 3   b 2017-05-20 00:00:00     35
# 4   b 2017-05-20 00:05:00    110
# 5   b 2017-05-20 00:10:00    185
# 6   c 2017-05-20 00:00:00     40
# 7   c 2017-05-20 00:05:00    115
# 8   c 2017-05-20 00:10:00    190

使用pandas.Grouper存在一個限制,即必須使用時間作為Series或DataFrame的索引。

移動窗口函數(shù)

在移動窗口或指數(shù)衰減權重上進行統(tǒng)計或運行其他函數(shù),也是一類常見于時間序列的數(shù)組變換。這對于圓滑噪聲數(shù)據(jù)或不連續(xù)數(shù)據(jù)很有幫助。

stock_px.csv文件:https://huihuiteresa.github.io/image/files/stock_px.csv

stock_px.csv文件內(nèi)容如下圖:

20250613001.png
close_px_all = pd.read_csv("stock_px.csv",parse_dates=True,index_col=0)
close_px = close_px_all[["AAPL","MSFT","XOM"]]
close_px = close_px.resample("B").ffill()
close_px["AAPL"].plot()
close_px["AAPL"].rolling(250).mean().plot()

<img src="https://huihuiteresa.github.io/image/youdao/20250613002.png" width="400">

表達式rolling(250)與groupby很像,但它不是進行分組,而是創(chuàng)建一個可以按照250日分組的移動窗口對象。然后,我們就得到了蘋果公司股價的250日移動窗口。

未完待續(xù)

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容