《利用python进行数据分析》读书笔记之时间序列(一)


本文中可能使用的数据集来自: 《利用python进行数据分析》数据集

日期和时间数据的类型和工具

python标准库中包含了时间和日期数据的类型比如datetime.datetime类:

from datetime import datetime

now = datetime.now()

print(now)
# 2020-04-02 19:39:38.819068
print(type(now))
# <class 'datetime.datetime'>
print(now.year,now.month,now.day)
# 2020 4 2

我们可以一用两个datetime对象相减来计算时间差:

res = datetime(2011,1,7)-datetime(2008,6,24,8,15)
print(type(res))
# <class 'datetime.timedelta'>
print(res)
# 926 days, 15:45:00
print(res.seconds)
# 56700

注意res和now两个对象的类型。
我们可以为一个datetime对象加上或者减去一个datedelta的整数倍来得到一个人新的datetime对象。在此不再演示。
以下是datetime模块中的一些数据类型:

类型描述
date使用公历日历存储日历日期(年,月,日)
time将时间存储为小时,分钟,秒和微秒
datetime存储日期和时间
timedelta表示两个datetime值之间的差(如日,秒或者微秒)
tzinfo用于存储时区信息的基本类型

字符串与datetime互相转换

可以使用str方法或传递一个格式给datetime的strftime或者pandas中的Timestanp对象来将其转化为字符串的格式:

stamp = datetime(2011,1,3)
print(str(stamp))
# 2011-01-03 00:00:00
print(stamp.strftime('%Y-%m-%d'))
# 2011-01-03

日期格式代码的完整列表:

类型描述
%Y四位的年份
%y两位的年份
%m两位的月份【01,12】
%d两位的日期号【01,31】
%H24小时制【0,23】
%I12小时制【01,12】
%M两位的分钟【00,59】
%S秒【00,61】(60,61是闰秒)
%w星期日期【0(星期天),6】
%U一年中的星期数【00,53】,以星期天为每周的第一天,一年中第一个星期天前的日期作为第0周
%W一年中的星期数【00,53】,以星期一为每周的第一天
%z格式为+HHMM或者-HHMM的UTC时区偏移;如果没有时区则为空
%F%Y-%m-%d的简写
%D%m/%d/%y的简写

可以使用datetime.strptime方法将字符串转化为日期:

value = "2011-01-03"
time = datetime.strptime(value,"%Y-%m-%d")
print(time)
# 2011-01-03 00:00:00
datestrs = ["7/6/2011",'8/6/2011']
times = [datetime.strptime(x,"%m/%d/%Y") for x in datestrs]
print(times)
# [datetime.datetime(2011, 7, 6, 0, 0), datetime.datetime(2011, 8, 6, 0, 0)]

使用strptime可以将字符串类型的日期转化为datetime类型,但是每次都需要输入日期的格式,非常的麻烦,为此我们可以使用dateuil包中 的parser.parse方法(这个包在安装pandas时也会自动安装),该方法可以将各种类型的字符串类型的时间转换为datetime类型:

from dateutil.parser import parse

print(parse("'2011-01-03"))
# 2011-01-03 00:00:00
print(parse("Jan 31,1997 10:45 PM"))
# 2020-01-31 22:45:00

在一些情况下,日期会出现在月份前面,可以设置参数dayfrist=True来处理这种情况:

print(parse("6/12/2012",dayfirst = True))
# 2012-12-06 00:00:00

在pandas中也有用于将字符串转化为datetime类型的函数(主要用于处理日期数组),即to_datetime函数:

datestrs = ['2011-07-06 12:00:00','2011-08-06 00:00:00']
res = pd.to_datetime(datestrs)
print(res)
# DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='datetime64[ns]', freq=None)

假设日期数组中含有None或者空的字符串等,则to_datetime方法将会把他转化为NaT(Not a Time):

datestrs = ['2011-07-06 12:00:00','2011-08-06 00:00:00']+[None]
res = pd.to_datetime(datestrs)
print(res)
# DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)

时间序列基础

我们可以使用datetime组成的数字来作为Series或者DataFrame的时间戳索引:

import pandas as pd
from datetime import datetime
import numpy as np

dates = [datetime(2011,1,2),datetime(2011,1,3),
         datetime(2011,1,4),datetime(2011,1,5),
         datetime(2011,1,6),datetime(2011,1,7)]
np.random.seed(1234)
ts = pd.Series(np.random.randn(6),index=dates)
print(ts)
# 2011-01-02    0.471435
# 2011-01-03   -1.190976
# 2011-01-04    1.432707
# 2011-01-05   -0.312652
# 2011-01-06   -0.720589
# 2011-01-07    0.887163
# dtype: float64

此时我们可以使用ts.index来查看索引,其数据类型被转换为了DatetimeIndex,而其中的标量值是pandas中的Timestamp对象:

print(ts.index)
# DatetimeIndex(['2011-01-02', '2011-01-03', '2011-01-04', '2011-01-05',
#                '2011-01-06', '2011-01-07'],
#               dtype='datetime64[ns]', freq=None)
print(ts.index[0])
# 2011-01-02 00:00:00

索引、选择、子集

当我们选择想要通过索引选择Series对象中的某一条数据时,可以有以下几种方法:

print(ts[2])
print(ts[ts.index[2]])
#使用Timestamp对象访问
print(ts['1/4/2011'])
print(ts['20110104'])

# 1.4327069684260973

对于切片操作,可以通过传入一个年份或者一个年份和一个月份来完成选择当年或者当月的数据:

print(longer_ts.head())
# 2000-01-01    0.471435
# 2000-01-02   -1.190976
# 2000-01-03    1.432707
# 2000-01-04   -0.312652
# 2000-01-05   -0.720589
print(longer_ts['2001'].head())
# 2001-01-01   -1.154601
# 2001-01-02   -1.268069
# 2001-01-03    0.607862
# 2001-01-04   -1.080096
# 2001-01-05   -0.611282
print(longer_ts['2001-05'].head())
# 2001-05-01   -0.766040
# 2001-05-02    0.099882
# 2001-05-03   -0.713024
# 2001-05-04   -2.226118
# 2001-05-05    0.725714
# Freq: D, dtype: float64

也可以使用datetime的类型来进行切片。另外由于时间戳往往是连续的,也允许使用不包含在索引内的时间戳来进行切片:

print(longer_ts[datetime(2001,7,28):].head())
# 2001-07-28   -1.530892
# 2001-07-29    0.801888
# 2001-07-30   -0.424467
# 2001-07-31    1.118855
# 2001-08-01    1.569548
# Freq: D, dtype: float64
print(longer_ts[datetime(2002,1,1):'2004-1-1'][-5:])
# 2002-09-22    0.272164
# 2002-09-23    0.931546
# 2002-09-24    0.327532
# 2002-09-25    0.740814
# 2002-09-26   -0.801905
# Freq: D, dtype: float64

注意,通过切片得到的是原数据的视图,这就意味着当切片得到的数据被更改的时候原数据内容也会被更改
对于Series的切片方法对于DataFrame同样适用,在此不再赘述。

含有重复索引的时间序列

在有些情况下,数据中可能会含有具有数个相同的时间索引,考虑一下例子:

np.random.seed(1234)

dates = pd.DatetimeIndex(['2011-1-1','2011-1-2','2011-1-2','2011-1-2','2011-1-3'])
dup_ts = pd.Series(np.arange(5),index=dates)
print(dup_ts)
# 2011-01-01    0
# 2011-01-02    1
# 2011-01-02    2
# 2011-01-02    3
# 2011-01-03    4
# dtype: int32

我们可以查看dup_ts.index的is_unique属性来查看是否出现了索引重复的问题,选取如果通过重复的索引来选择数据,则会返回一个Series:

print(dup_ts.index.is_unique)
# False
print(dup_ts['2011-1-1'])
# 0
print(dup_ts['2011-1-2'])
# 2011-01-02    1
# 2011-01-02    2
# 2011-01-02    3
# dtype: int32

对于含有重复的索引的数据,一种处理方法是通过groupby方法来聚合,传递参数level = 0,groupby的聚合具体用法见:《利用python进行数据分析》读书笔记之数据聚合,这里我们通过取平均来完成聚合的操作:

print(dup_ts.groupby(level=0).mean())
# 2011-01-01    0
# 2011-01-02    2
# 2011-01-03    4
# dtype: int32

日期范围、频率和位移

我们可以使用含有时间序列的对象的resample方法将数据从时域转换为时域,以之前的ts对象为例:

print(ts)
# 2011-01-02    0.471435
# 2011-01-03   -1.190976
# 2011-01-04    1.432707
# 2011-01-05   -0.312652
# 2011-01-06   -0.720589
# 2011-01-07    0.887163
# dtype: float64
ts.resample("D")
# "D"代表转换为每日的频率

生成日期范围

首先重新介绍一下pd.date_range函数,该函数可以用来生成指定长度的DatatimeIndex:

index = pd.date_range('2012-04-01','2014-06-01')
print(index)
# DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
#                '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
#                '2012-04-09', '2012-04-10',
#                ...
#                '2014-05-23', '2014-05-24', '2014-05-25', '2014-05-26',
#                '2014-05-27', '2014-05-28', '2014-05-29', '2014-05-30',
#                '2014-05-31', '2014-06-01'],
#               dtype='datetime64[ns]', length=792, freq='D')

默认情况下,生成的是以日为单位的连续时间戳,我们也可以只指定一个起始或者结尾日期,但是必须传递一个数字,这样pandas才能确定出时间戳的范围:

index1 = pd.date_range(start = '2012-04-01',periods=20)
print(index1)
# DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
#                '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
#                '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
#                '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
#                '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'],
#               dtype='datetime64[ns]', freq='D')
index2 = pd.date_range(end = '2012-04-01',periods=20)
print(index2)
# DatetimeIndex(['2012-03-13', '2012-03-14', '2012-03-15', '2012-03-16',
#                '2012-03-17', '2012-03-18', '2012-03-19', '2012-03-20',
#                '2012-03-21', '2012-03-22', '2012-03-23', '2012-03-24',
#                '2012-03-25', '2012-03-26', '2012-03-27', '2012-03-28',
#                '2012-03-29', '2012-03-30', '2012-03-31', '2012-04-01'],
#               dtype='datetime64[ns]', freq='D')

当然我们还可以指定生成时间戳的单位(即频率),比如将“BM”作为频率参数传入时,得到的时间戳是每一个月的最后一天(只有落在范围内的日期被包含在内):

index1 = pd.date_range(start = '2012-04-01',end="2012-12-01",freq="BM")
print(index1)
# DatetimeIndex(['2012-04-30', '2012-05-31', '2012-06-29', '2012-07-31',
#                '2012-08-31', '2012-09-28', '2012-10-31', '2012-11-30'],
#               dtype='datetime64[ns]', freq='BM')

下表是部分可选时间序列的频率:

频率描述
D日历日的每天
B工作日的每天
H每小时
T或者min每分钟
S每秒
L或ms每毫秒
U每微秒
M日历日的月底日期
BM工作日的月底日期
MS日历日的月初日期
BMS工作日的月初日期
W-MON,W-TUE…按照给定的星期日期按照每周取日期(MON,TUE,WED,THU,FRI,SAT,SUN)

现在假设我们使用date_range生成一个系列日为频率的时间戳,如果给定的起始时间不为0点,则之后生成的时间也不为0点:

index = pd.date_range(start = '2012-04-01 12:56:31',periods=5)
print(index)
# DatetimeIndex(['2012-04-01 12:56:31', '2012-04-02 12:56:31',
#                '2012-04-03 12:56:31', '2012-04-04 12:56:31',
#                '2012-04-05 12:56:31'],
#               dtype='datetime64[ns]', freq='D')

如果我们想要将生成的时间戳全部设为0点,可以使用normalize选项进行实现:

index = pd.date_range(start = '2012-04-01 12:56:31',periods=5,normalize=True)
print(index)
# DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
#                '2012-04-05'],
#               dtype='datetime64[ns]', freq='D')

频率和日期偏置

学会了date_range函数的基本用法后,又出现了一个问题,假设我们想设置一系列以若干小时为频率的时间戳序列,应该怎么办呢?有两种方法(以1小时30位置为例子):

  • 将“90min”或者“1.5H”传给freq
  • 将“1H30min”传给freq
index1 = pd.date_range(start = '2012-04-01 12:0000',periods=5,freq="90min")
print(index1)
index2 = pd.date_range(start = '2012-04-01 12:0000',periods=5,freq="1.5H")
print(index2)
index3 = pd.date_range(start = '2012-04-01 12:0000',periods=5,freq="1H30min")
print(index3)
# DatetimeIndex(['2012-04-01 12:00:00', '2012-04-01 13:30:00',
#                '2012-04-01 15:00:00', '2012-04-01 16:30:00',
#                '2012-04-01 18:00:00'],
#               dtype='datetime64[ns]', freq='90T')

移位(向前或者向后)日期

移位是指将日期按照时间向前或者向后移动,类似于几何学上的平移。Series和DataFrame对象都有一个shift方法来完成日期的移位,当传入参数为正表示数据整体向前(日期增加)移动,负整则相反(索引不会变):

np.random.seed(1234)
ts = pd.Series(np.random.randn(5),index=pd.date_range("2011-11-30",periods=5))
print(ts)
# 2011-11-30    0.471435
# 2011-12-01   -1.190976
# 2011-12-02    1.432707
# 2011-12-03   -0.312652
# 2011-12-04   -0.720589
# Freq: D, dtype: float64
print(ts.shift(2))
# 2011-11-30         NaN
# 2011-12-01         NaN
# 2011-12-02    0.471435
# 2011-12-03   -1.190976
# 2011-12-04    1.432707
# Freq: D, dtype: float64
print(ts.shift(-2))
# 2011-11-30    1.432707
# 2011-12-01   -0.312652
# 2011-12-02   -0.720589
# 2011-12-03         NaN
# 2011-12-04         NaN
# Freq: D, dtype: float64

我们可以看到,当进行时间的移位时,会产生缺失值,这是因为原始的数据移走后,后续的位置没有来对其进行顶替导致的。如果我们给freq选项传入相应的频率,则时间序列也会进行移位,此时则不会发生数据的丢失:

print(ts.shift(2,freq="D"))
# 2011-12-02    0.471435
# 2011-12-03   -1.190976
# 2011-12-04    1.432707
# 2011-12-05   -0.312652
# 2011-12-06   -0.720589
# Freq: D, dtype: float64

时区处理

时间的本地化和转换

一般情况下,世界上各个地区的时间都可以表示为UTC(世界协调时)的一个偏执,比如中国的背景就是UTC+8,默认情况下作为索引的时间序列是没有时区的,我们可以使用ts.index的tz属性来查看:

np.random.seed(1234)
rng = pd.date_range("2012-3-27 9:30",periods=6,freq="D")
ts = pd.Series(np.random.randn(6),index=rng)
print(ts)
# 2012-03-27 09:30:00    0.471435
# 2012-03-28 09:30:00   -1.190976
# 2012-03-29 09:30:00    1.432707
# 2012-03-30 09:30:00   -0.312652
# 2012-03-31 09:30:00   -0.720589
# 2012-04-01 09:30:00    0.887163
# Freq: D, dtype: float64
print(ts.index.tz)
# None

此时我们可以使用tz_localize方法设定该时间序列所在的时区:

ts_utc = ts.tz_localize("UTC")
# 将时区设置为UTC
print(ts_utc)
# 2012-03-27 09:30:00+00:00    0.471435
# 2012-03-28 09:30:00+00:00   -1.190976
# 2012-03-29 09:30:00+00:00    1.432707
# 2012-03-30 09:30:00+00:00   -0.312652
# 2012-03-31 09:30:00+00:00   -0.720589
# 2012-04-01 09:30:00+00:00    0.887163
# Freq: D, dtype: float64
print(ts_utc.index.tz)
# UTC

设定了本地时区后,我们就可以使用tz_convert方法轻松的切换时区,比如切换到上海(北京)时间(UTC+8):

print(ts_utc.tz_convert("Asia/Shanghai"))
# 2012-03-27 17:30:00+08:00    0.471435
# 2012-03-28 17:30:00+08:00   -1.190976
# 2012-03-29 17:30:00+08:00    1.432707
# 2012-03-30 17:30:00+08:00   -0.312652
# 2012-03-31 17:30:00+08:00   -0.720589
# 2012-04-01 17:30:00+08:00    0.887163
# Freq: D, dtype: float64

不同时区之前的操作

当两个拥有不同时区的时间戳序列的Series或者DataFrame对象进行联合时,结果时间序列的本地市区会自动转换为UTC:

rng = pd.date_range("2012-3-27 9:30",periods=6,freq="D")
ts = pd.Series(np.random.randn(6),index=rng)
print(ts)
# 2012-03-27 09:30:00    0.471435
# 2012-03-28 09:30:00   -1.190976
# 2012-03-29 09:30:00    1.432707
# 2012-03-30 09:30:00   -0.312652
# 2012-03-31 09:30:00   -0.720589
# 2012-04-01 09:30:00    0.887163
# Freq: D, dtype: float64
ts1 = ts[:7].tz_localize("Europe/London")
ts2 = ts1[2:].tz_convert("Asia/Shanghai")
res = ts1 + ts2
print(res.index)
# DatetimeIndex(['2012-03-27 08:30:00+00:00', '2012-03-28 08:30:00+00:00',
#                '2012-03-29 08:30:00+00:00', '2012-03-30 08:30:00+00:00',
#                '2012-03-31 08:30:00+00:00', '2012-04-01 08:30:00+00:00'],
#               dtype='datetime64[ns, UTC]', freq='D')

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值