本篇讲时间序列的基础知识:用于构建时间索引(时间戳)的日期和时间对象。这部分内容有点枯燥,建议作为函数参考来用,直接看看最后一部分的例子就好了。
时间序列数据的主要属性是它的时间索引(时间戳),时间索引可以是日期对象、日期时间对象或者依赖于序列频率的其他对象。载入原始数据的时候,往往并没有伴随着恰当的日期/时间对象。因此,在将数据转换为时间序列之前,往往要进行格式上的重新处理。处理时间和日期对象的能力是时间序列数据预处理过程的一部分,往往也是相当麻烦和繁琐的一部分。
1、日期和时间
日期和时间对象的处理是复杂的,主要的挑战在于格式的多样性。比如,常用的计算机日历系统使用字母顺序的形式表示年月日这三种日期成分:
- Y:年
- yy:20
- yyyy:2020
- M:月
- m:1(1月)
- mm:01(1月)
- mmm:Jan(1月)英文月名的三位缩写
- mmmm:January 英文的完整月名
- D:天。
- d:1
- dd:01
- ddd:Mon 星期的缩写
- dddd:Monday 完整的星期名称
除此之外,还有表示顺序的差异,不同国家/地区的惯用表示法是不一样的:
中国:YMD 2020/05/27
美国:MDY 05/27/2020 May 27th,2020
2、R内置的日期和日期时间类
作为基础,我们首先来介绍R中基本的日期和日期时间类。
R的 base
包提供了两种基本的日期和时间类:
1、Date
类:日期类,服从ISO8601标准的日历日期表示,格式是yyyy-mm-dd。默认的起点时间是1970-01-01,每个日期对象都有一个数值取值。序列频率为天或低于天(月、年等)。
2、POSIXct
类/POSIXlt
类:日期时间类(DateTime),标准格式是yyyy-mm-dd h : m : s 。两者的差异在于各自取值内部存储的方式不同。POSIXct类与Date类相似,以数值向量表示从起点时间开始的秒数,POSIXlt类则将每个日期时间的各种时间成分按列表保存。序列频率高于天。
看一下这两种类型(Sys.Date()
取系统的日期,Sys.time()
取系统的日期时间):
>date> date[1] "2020-05-28"> class(date)[1] "Date"> date_time> date_time[1] "2020-05-28 15:07:41 CST"> class(date_time)[1] "POSIXct" "POSIXt"
2.1 日期和时间对象的创建和转换
在R中,对一个特定的类指派取值有一个通用的函数模版:as.[the class name]
。
所以,可以使用as.Date()``as.POSIXct()``as.POSIXct()
这样的函数将字符形式的时间表示转换为日期/日期时间类:
> as.Date("2020-05-31")[1] "2020-05-31"> as.Date("31-05-2020")###wrong![1] "0031-05-20"
由于时间表示形式的复杂性,这种转换是很容易出错误的(如上面的as.Date("31-05-2020")
)。因此,这种格式转换,特别是从外部文件读入数据时,往往需要指定待转换时间的表示格式。常用的时间格式表示如下表所示(完整的格式参考strptime函数的帮助文件:??striptime
):
表1
看一些例子:
> as.Date("2020-05-31")[1] "2020-05-31"> as.Date("31-05-2020")[1] "0031-05-20"> as.Date("31-05-2020",format='%d-%m-%Y')[1] "2020-05-31"> as.Date("01/05/2020",format="%m/%d/%Y")[1] "2020-01-05"> as.Date("May 28,2020",format="%B %d,%Y")[1] "2020-05-28"> strptime("31-05-2020",format="%d-%m-%Y")[1] "2020-05-31 CST"> strptime("31/05/2020",format="%d/%m/%Y")[1] "2020-05-31 CST"> strptime("20200531",format="%Y%m%d")[1] "2020-05-31 CST"> strptime("31/5/2020",format="%d/%m/%Y")[1] "2020-05-31 CST"> as.POSIXct("20200531",format="%Y%m%d")[1] "2020-05-31 CST"
日期时间也可以和数值类型进行互相转换:
> as.numeric(as.Date("2020-01-01"))[1] 18262> as.numeric(as.POSIXlt("2020-01-01 00:00:00"))[1] 1577808000> as.POSIXlt(1588000000,origin="1970-01-01")[1] "2020-04-27 23:06:40 CST"
2.2 创建日期/日期时间格式的索引(时间戳)
seq.Date()
:
参数:"day"、"week"、"month"、"quarter"、"year"seq.POSIXt()
:
参数:"sec"、"min"、"hour"、"day"、"DSTday"、"week"、"month"、"quarter"、"year"
> daily_2from= to=as.Date("2020-01-11"), by="2 days")> daily_2[1] "2020-01-01" "2020-01-03" "2020-01-05" "2020-01-07" "2020-01-09"[6] "2020-01-11"
2.3 时区
"2020-04-27 23:06:40 CST"
中的CST指的是时区。时区既是一个时间概念,也是一种行政治理的概念。指定日期时间对象时,使用tz参数可以设置时区。OlsonNames()给出了全部592个时区。
3、 lubridate包
与日期和日期时间对象有关的整洁处理,可以通过lubridate
包实现。lubridate
包是tidyverse系列R包之一(尽管并非tidyverse的核心)。
lubridate主要由这样几个方面的功能:创建、解析或转换日期时间对象,获取或设置日期时间成分,创建表示时间间隔的类、对日期进行数学运算。
3.1 创建日期时间对象
在tidyverse的数据框tibble中, 表示日期和时间的数据有3种类型:
- 日期:日历时间,在tibble中显示为。可以对应于上面讨论的
Date
类 - 时间:一天中的某个时刻,在tibble中显示为。(在R中没有保存时间的原生类,对时间数据的处理,可以参考
hms
包) - 日期时间:可以唯一 标识某个时刻的日期加时间,在tibble中显示为。可以对应于上面讨论的
POSIXct
类。
创建日期或日期时间,有三种方式:
- 通过字符串创建
- 通过日期时间的各个成分创建
- 通过现有的时间对象创建,也就是进行对象类的转换
涉及的函数很多,可以参考lubridate的帮助文件。下面就通过一些例子来说明:
#####当前系统的日期时间> today()[1] "2020-06-01"> now()[1] "2020-06-01 19:59:31 CST"#####1、通过字符串创建日期/日期时间###y-m-d:年月日> ymd(20200601)[1] "2020-06-01"> ymd("2020/06/01")[1] "2020-06-01"###注意要解析的字符串中年月日的顺序,如m-d-y:月日年> mdy("Jan 01,2020")[1] "2020-01-01"###h-m-s:小时-分-秒> ymd_hms("2020/06/01 20:01:02")[1] "2020-06-01 20:01:02 UTC"#####2、通过日期时间的各个成分创建> t1> t1# A tibble: 1 x 4 year month day hour 1 2000 6 1 20###make_date()创建日期,make_datetime()创建日期时间> t2% mutate(date=make_date(year,month,day))> t2# A tibble: 1 x 5 year month day hour date 1 2000 6 1 20 2000-06-01#####3、通过现有的时间对象创建> as_date(today())[1] "2020-06-01"> as_datetime(today())[1] "2020-06-01 UTC"
3.2获取或设置日期成分
我们可以获取日期和日期时间中的独立成分。获取时间成分,主要有这样一些函数:year()
、month()
、mday()
(一个月中的第几天)、yday()
(一年中的第几天)、wday()
(一周中第几天)、hour()
、minute()
、second()
等。也可以使用这些函数对时间成分进行设置。
#####1、获取时间成分> year(today())[1] 2020> mday(today())[1] 1> wday(today())###第一天从周日开始[1] 2> today()-1[1] "2020-05-31"> wday(today()-1)[1] 1#####2、设置时间成分> t1"2020-01-01")> t1[1] "2020-01-01"> year(t1)> t1[1] "2019-01-01"
3.3 时间间隔
我们经常需要获取某个事件的持续时间或者两个事件之间的间隔时间,lubridate给出了3种表示时间间隔(timespan)的类。通过它们,我们也可以对日期进行数学计算(加法、减法和除法)。
- 阶段(duration)
在R中,我们可以让两个日期相加减:
> today()-ymd(20200101)Time difference of 152 days
这样得到的时间间隔,其单位或者取值是不统一的。对此,lubridate提供了一种以秒为单位的时间间隔计时对象:阶段(duration):
> as.duration(today()-ymd(20200101))[1] "13132800s (~21.71 weeks)"
除了as.duration()
函数,还有dseconds()
、dminutes()
等函数用于创建阶段对象:
> dseconds(1)[1] "1s"> dminutes(1)[1] "60s (~1 minutes)"> ddays(1)[1] "86400s (~1 days)"
- 时期(period)
时期是使用人工时间(而不仅是秒)表示的时间间隔,与阶段相比,时期要更加直观:
###比较阶段(dyears)和时期(years)> dyears(1)[1] "31536000s (~52.14 weeks)"> years(1)[1] "1y 0m 0d 0H 0M 0S"> ymd(20200101)+dyears(1)###注意2020年是闰年[1] "2020-12-31"> ymd(20200101)+years(1)[1] "2021-01-01"
(注意,创建时期的函数如seconds()
、hours()
、years()
等与获取日期的函数如second()
、hour()
、year()
等的区别,前者多了一个“s”。)
- 区间(interval)
区间是带有起点的阶段,它可以非常精确地表示一段时间间隔:
> last_year> last_year%--%today()[1] 2019-06-02 UTC--2020-06-02 UTC
4、lubridate实例:湖人队的2008-2009赛季
看一个例子。lubridate
包提供了一个数据集lakers
,这个数据集包含了NBA2008-2009赛季(常规赛)洛杉矶湖人队每场篮球赛的实况(play-by-play)统计数据。数据包括每场球赛(game)的日期、对手和类型(主场home/客场away)。球赛中的每种比赛情况(play)都由比赛时钟上的时间、比赛进行的节、比赛的类型、比赛的球员和球队、比赛结果以及每次比赛的场地位置来描述:
> data(lakers)> str(lakers)'data.frame': 34624 obs. of 13 variables: $ date : int 20081028 20081028 20081028 20081028 20081028 20081028 20081028 20081028 20081028 20081028 ... $ opponent : chr "POR" "POR" "POR" "POR" ... $ game_type: chr "home" "home" "home" "home" ... $ time : chr "12:00" "11:39" "11:37" "11:25" ... $ period : int 1 1 1 1 1 1 1 1 1 1 ... $ etype : chr "jump ball" "shot" "rebound" "shot" ... $ team : chr "OFF" "LAL" "LAL" "LAL" ... $ player : chr "" "Pau Gasol" "Vladimir Radmanovic" "Derek Fisher" ... $ result : chr "" "missed" "" "missed" ... $ points : int 0 0 0 0 0 2 0 1 0 2 ... $ type : chr "" "hook" "off" "layup" ... $ x : int NA 23 NA 25 NA 25 NA NA NA 36 ... $ y : int NA 13 NA 6 NA 10 NA NA NA 21 ...
我们讨论与时间联系在一起的一些问题,希望以可视化的方式对这些问题进行探索。
简述一下篮球比赛的时间规则:NBA的一场篮球赛(game)可能会持续2-3小时,其中包括了净比赛时间以及不计时的犯规、暂停、中场休息的时间。净比赛时间分为4节(period),每节12分钟共48分钟(若四节结束后比分平局,会有5分钟的加时赛)。比赛时间由比赛时钟进行记录,计时规则是停表计时:比赛开始后,犯规、罚球、暂停都要停表,(倒计时)累计到12分钟,本节次结束。
4.1 球赛的时间分布
首先,我们来看一下球赛场次的时间分布,目标是通过日历图进行展示。首先,将数据集中给出的日期date解析成日期对象(使用ymd()
),并在此基础上提取各种时间成分:wday()
给出了日期是星期几,mday()
给出了日期是每月的第几天,month()
提取了日期的月份,isoweek()
根据ISO8601标准返回了日期位于该年份的第几周(isoweek()
返回的周数是基于日历时间的,week()
也可以返回周数,但它是以每年的第一天作为第一周的起始)。最后,由于这个数据集是从10月28日开始的,对于每月第几周的计算mweek
要稍微复杂一点。有了这些时间成分,就可以绘制日历图。展示时间序列的日历图在本质上就是热图。我们绘制的日历图以mweek
(每月第几周)为横坐标,以weekday
(星期几)为纵坐标,然后按年和月进行分片:
lakers1% mutate(date=ymd(date), weekday=wday(date,label=T), monthday=mday(date), month=month(date,label=T), week_y=isoweek(date), year=year(date))###计算每个月内有几周lakers1% group_by(month)%>% mutate(mweek=if_else(month=="Oct",5,1+as.integer(week_y)-min(as.integer(week_y)))) ###为图形美观,将weekday和mweek转换为因子,并指定因子水平lakers1%mutate(weekday1=factor(weekday,levels=c("Mon","Tue","Wed","Thu","Fri","Sat","Sun")), mweek1=factor(mweek,levels=c("6","5","4","3","2","1"))) ###日历图lakers1%>%ggplot(aes(x=weekday1,y=mweek1,fill=factor(game_type)))+ geom_tile(color="black")+ geom_text(aes(label=monthday))+facet_wrap(~year+month,nrow=2,strip.position='top')+ theme_bw()
图1
这个日历图也可以用其他的日期成分进行绘制。
4.2 比赛情况的时间规律
lakers
数据集中变量time
给出了每种比赛情况发生时刻的计时,这是按倒计时记录的时刻时间。我们想要把它转换为正常时间顺序的计时。首先,使用ms()
对time
变量进行解析,得到的结果是一个时期变量。然后,用as.duration()
将它转换为用秒表示的一个阶段对象。注意,此时它表示的是一个时间间隔(从本节比赛开始为起点),我们只要用累积时间(dminutes(12) * lakers1$period
)减去这个时间间隔就可以得到正常时间顺序下比赛情况发生的时刻。最后,用一个直方图将常规赛中各个时刻发生的净比赛次数汇总展示:
###时期lakers1$time $time)###阶段lakers1$time $time)###正常时间顺序下的发生时刻lakers1$time $period - lakers1###去掉加时赛的情况lakers1% filter(period != 5)###直方图 lakers1%>%ggplot(aes(as.integer(time)))+geom_histogram(binwidth=60,colour="black",fill="steelblue")+theme_bw()
图2
图2中的时间是以秒为单位展示的。我们还是希望能把时间转换成更自然地以分钟为单位的形式,也就是转变成区间对象。虽然我们并不知道这些球赛具体在什么时间开始的,作为区间,只要给一个时间起点就可以了:
lakers1%>% mutate(minutes=ymd("2008-10-28") + time)%>% ggplot(aes(minutes))+geom_histogram(binwidth=60,colour="black",fill="steelblue")+theme_bw()
图3
图2和图3反映的都是每种比赛情况完成时刻的汇总情况,它们的区别只在于横轴的时间表示不同。从图中我们可以推测比赛的激烈程度。一般第一节比赛的节奏都是比较慢的,比赛完成的次数较少,越到比赛中后阶段越激烈。在每一节中,比赛在接近本节结束的时段要比本节刚开始的时段要激烈的多。
4.3 一场球赛中的投篮间隔时间
现在,我们来考察一场具体的比赛。2008年10月28日,湖人队主场迎战开拓者队(POR)。我们来汇总这场球赛中两次投篮的时间间隔。我们已经有了每种比赛情况与本节开始时刻的时间间隔time
,只要针对shot
这种类型对time
进行差分,就可以得到两次投篮出手之间的间隔时间wait
:
###取2008-10-28这场比赛作为子集game1% filter(date=="2008-10-28")### 针对shot这种类型计算两次投篮间隔时间wait attempts% filter(etype=="shot")%>% mutate(wait=c(time[1],diff(time)))###绘制等待时间的汇总直方图 attempts%>%ggplot(aes(as.integer(wait)))+geom_histogram(binwidth=2,colour="black",fill="steelblue")+theme_bw()
图4
由图4可见,由于篮球比赛规定拥有球权的球队必须在24秒内投篮,所以两次投篮出手之间的间隔时间大多数都是在24秒内,说明两队的进攻欲望和进攻效率还是很强的。也有少数的投篮间隔时间超过了60秒,这应该就是防守发挥作用的时候吧。
4.4 一场球赛的得分
继续看2008年10月28日的这一场球赛。我们来汇总展示两队的得分。只要按球队分组(group_by()
)进行累积求和计算(cumsum()
)就可以两队的累积得分。要展示两队得分随时间的变化情况,还要借助上面使用过的区间方法(图5):
game1_scores% group_by(team)%>% mutate(score=cumsum(points))%>% filter(team!="OFF")game1_scores%>% mutate(minutes=ymd("2008-01-01") + time)%>%ggplot(aes(x=minutes,y=score,colour=team))+geom_line(size=2)+theme_bw()
图5
可以看到,湖人队占据了绝对的优势,比分一直领先于客队。