23 使用xts和zoo操作时间序列数据

23.1 简介

笔记摘抄自Datacamp,仅供学习交流

23.1.1 第一个时间序列对象

xts (eXtensible Time Series)是灵活和强大的对象,旨在使时间序列变得简单。xts的核心是一个zoo对象,一个矩阵对象加上对应于每一行的时间向量。从视觉上看,你可以把它看成是数据加上一个时间数组。我们可以创建一个矩阵和一个时间(真正意义上的时间类型),然后”合并”起来.

x <- matrix(1:4, ncol = 2, nrow = 2)

idx <- as.Date(c("2023-03-01", "2023-03-02"))

X <- xts(x, order.by = idx)

X
##            [,1] [,2]
## 2023-03-01    1    3
## 2023-03-02    2    4

# 查看xts属性
attributes(X)
## $dim
## [1] 2 2
## 
## $index
## [1] 1677628800 1677715200
## attr(,"tzone")
## [1] "UTC"
## attr(,"tclass")
## [1] "Date"
## 
## $class
## [1] "xts" "zoo"

我们可以查看xts函数的参数:

str(xts)
## function (x = NULL, order.by = index(x), frequency = NULL, unique = TRUE, 
##     tzone = Sys.getenv("TZ"), ...)

需要注意的是,时间序列构造的函数”zone”是来设置时区,“unique”强制所有时间都是唯一的,请注意,xts并不强制要求你的索引(例如上面的idx)具有唯一性,但你在自己的实际应用中可能需要这样,还有就是提供的索引应该是 按时间递增的顺序,即早期的观测值在你的对象的顶部,后来更近的观测值则在底部.如果导入了一个没有排序的向量,xts将重新排序索引和数据的相应行,确保有一个正确的时间序列.

如果只需要时间索引或者矩阵数据,有两个函数可以使用,传入对象都是一个 xts对象:

  • coredata():获得原始矩阵.
  • index():获得时间索引.
# 提取前面创建的时间序列的矩阵

coredata(X)
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4

# 提取时间序列的索引

index(X)
## [1] "2023-03-01" "2023-03-02"

23.1.2 基础操作

xts对象可以像矩阵那样提取子列或者某个元素,与矩阵的提取不同的是,它会返回一个带有时间的”标签”.

# 原始矩阵的提取第二行
coredata(X)[2,]
## [1] 2 4

# xts对象提取第二行
X[2,]
##            [,1] [,2]
## 2023-03-02    2    4

xts还允许你将任意的键值属性与你的数据绑定。这让你在你的对象中保留关于你的对象的元数据。要在创建时添加这些属性,你只需向xts()函数传递额外的name = value参数。

data <- rnorm(n = 5)
dates <- seq(as.Date("2023-03-01"), length = 5, by = "days")

notes <- xts(x = data, order.by = dates)

# 想起记笔记的时间
tday <- as.POSIXct("2023-02-20")

myts <- xts(x = data, order.by = dates, dnote = tday)

attributes(myts)
## $dim
## [1] 5 1
## 
## $index
## [1] 1677628800 1677715200 1677801600 1677888000 1677974400
## attr(,"tzone")
## [1] "UTC"
## attr(,"tclass")
## [1] "Date"
## 
## $class
## [1] "xts" "zoo"
## 
## $dnote
## [1] "2023-02-20 CST"

如果我们定义好了想要的属性,需要使用xtsAttributes()函数来索引或提取:

xtsAttributes(myts)
## $dnote
## [1] "2023-02-20 CST"

xtsAttributes(myts)$dnote
## [1] "2023-02-20 CST"

23.1.3 基于时间的简单索引

如果只是矩阵,那么我们提取子集需要指定行列,或者列名之类的.如果是xts对象,我们可以利用时间来索引到想要的数据(留心创建数据的不同方法)

dates <- as.Date("2023-03-01") + 0:4

# 创建一个时间序列ts_a
ts_a <- xts(x = 1:5, order.by = dates)

# 创建一个时间序列ts_b,但是类型是POSIXct
ts_b <- xts(x = 1:5, order.by = as.POSIXct(dates))

# 使用ts_b的索引提取ts_a的行
ts_a[index(ts_b)]
##            [,1]
## 2023-03-01    1
## 2023-03-02    2
## 2023-03-03    3
## 2023-03-04    4
## 2023-03-05    5

# 使用ts_a的索引提取ts_b的行,为什么会出错?
ts_b[index(ts_a)]
##      [,1]

# 使用ts_a的索引提取不到b的数据是因为 
ts_b
##                     [,1]
## 2023-03-01 08:00:00    1
## 2023-03-02 08:00:00    2
## 2023-03-03 08:00:00    3
## 2023-03-04 08:00:00    4
## 2023-03-05 08:00:00    5

23.1.4 导入与转换数据类型

在R中处理时间序列数据时,经常需要在不同的类之间进行转换。转换的原因有很多,但通常你会希望使用一个可能不了解时间序列的函数,或者你可能想用xts的某一方面来处理不一定需要是完整的时间序列的东西。

用R中提供的标准as.*风格的功能(例如,as.POSIXct()as.matrix())来转换是非常容易的。

# 读取一个临时创建的数据
dat <- read.delim(tmp_file, sep = ",")
## Warning in read.table(file = file, header = header, sep = sep, quote = quote, :
## incomplete final line found by readTableHeader on 'datasets/temp_file.txt'

dat
##           a b
## 1/02/2015 1 3
## 2/03/2015 2 4

str(dat)
## 'data.frame':    2 obs. of  2 variables:
##  $ a: int  1 2
##  $ b: int  3 4

# 转变为xts对象
xts(dat, order.by = as.Date(rownames(dat), "%m/%d/%Y"))
##            a b
## 2015-01-02 1 3
## 2015-02-03 2 4

# 使用 read.zoo
dat_zoo <- read.zoo(tmp_file, index.column = 0, sep = ",", format = "%m/%d/%Y")
## Warning in read(file, ...): incomplete final line found by readTableHeader on
## 'datasets/temp_file.txt'

# 转换格式
dat_xts <- as.xts(dat_zoo)

导出xts对象有两个主要用例。首先,你可能需要一个对象在不同的会话中持续存在,以便在以后的分析中使用。在这种情况下,几乎总是最好使用saveRDS()readRDS()来序列化单个R对象。

另外,你可能会发现自己需要与他人分享你的分析结果,通常期望数据被不了解R和xts的进程所消耗。我们中的大多数人都不愿意想到我们的数据会有这种可怕的命运,但现实世界要求我们至少要了解这一点。

从R中写一个xts对象的最好方法之一是使用zoo函数write.zoo()

# 我们先创建一个路径
tmp <- "datasets/writezoo.csv"

# 将之前的xts数据写到磁盘上
write.zoo(dat_xts, sep = ",", file = tmp)

# 然后读取,FUN = as.yearmon 将1749年1月这样的字符串转换为适当的时间类。
# 没有header这个会报错,原因不知道...
extract_r <- read.zoo(tmp, sep = ",", FUN = as.yearmon, header=TRUE)

ex_xts <- as.xts(extract_r)

ex_xts
##          a b
## 1月 2015 1 3
## 2月 2015 2 4

23.2 时间索引

23.2.1 时间段索引

我们先创建一个大的xts对象.

idx <- as.Date("2015-06-01")+0:305

x <- rnorm(306)

xts1 <- xts(x, order.by = idx)

一些索引的规则,这里没有运行,假设A是一个xts对象.

A["20090825"]       ## 选取指定日期
A["201203/201212"]  ## 选取一段时间
A["/201601"]        ## 直到并且包括2016年1月

如果处理一天内的数据,可以如下形式索引.不需要指定明确的日期,而是使用日内重复区间设计的特殊T/T符号.

# 满足这个时间段的所有日期,可以日期不一样
NYSE["T09:30/T16:00"] 

23.2.2 时间索引与更新

我们可以将指定的时间对应的值进行替换.这里使用的数据是之前创建的随机数的时间序列对象.

# 指定时间
dates <- as.Date(c("2016-03-04", "2016-03-08"))

# 将我们指定的时间,所对应的值替换成NA
xts1[dates] <- NA

# 从某一个时间直到最后,替换成0
xts1["20160309/"] <- 0

# 查看其中一行,检查是否被替换
xts1["20160311"]
##            [,1]
## 2016-03-11    0

23.2.3 时间序列的头和尾索引

有时你需要通过相对时间来定位数据。有些东西说起来容易做起来难。这相当于索引一个时间序列的头部或尾部,但不是使用一个绝对的偏移量,而是描述一个相对的时间位置。一个简单的例子,比如一个时间序列的最后3周,或当前月的第一天。我们使用下面的函数:

  • first(): 相当于head()函数,但是它使用起来更加灵活,你可以传递像”3 weeks”这样的参数. 文档

  • last():与上面函数正好相反,相当于 tail().

这里我们还是使用之前的xts1数据,注意最后一个命令,是一个嵌套:

# 返回最后一周
lastweek <- last(xts1, "1 week")
lastweek
##            [,1]
## 2016-03-28    0
## 2016-03-29    0
## 2016-03-30    0
## 2016-03-31    0
## 2016-04-01    0

# 返回最后两周
last(xts1, "2 weeks")
##            [,1]
## 2016-03-21    0
## 2016-03-22    0
## 2016-03-23    0
## 2016-03-24    0
## 2016-03-25    0
## 2016-03-26    0
## 2016-03-27    0
## 2016-03-28    0
## 2016-03-29    0
## 2016-03-30    0
## 2016-03-31    0
## 2016-04-01    0

# 返回最后两个观测值
last(xts1, n = 2)
##            [,1]
## 2016-03-31    0
## 2016-04-01    0

# 返回最后一周除了前两天其他日期的观测值
first(lastweek, "-2 day")
##            [,1]
## 2016-03-30    0
## 2016-03-31    0
## 2016-04-01    0

对于处理更复杂的需求,我们可以将两个函数结合起来使用,再翻译代码的时候,应该是从内向外去翻译和理解:

#提取时间序列 前两周 的后一周 的前三天
first(last(first(xts1, "2 weeks"), "1 week"), "3 days")
##                   [,1]
## 2015-06-08 -0.05746899
## 2015-06-09 -3.02281260
## 2015-06-10  1.29372453

23.2.4 窗口函数

选取指定时间段

  • window.xts()
  • window():作用同上
x.date <- as.Date(paste(2003, rep(1:4, 4:1), seq(1,19,2), sep = "-"))
x <- xts(matrix(rnorm(20), ncol = 2), x.date)
x
##                  [,1]       [,2]
## 2003-01-01 -1.2048405  0.3735142
## 2003-01-03 -0.6292939 -1.2019446
## 2003-01-05  0.6139091  0.3457839
## 2003-01-07 -1.0707324 -0.6190318
## 2003-02-09 -0.6704893  1.1487506
## 2003-02-11 -1.8119186  0.4495418
## 2003-02-13  0.4229121 -1.3511595
## 2003-03-15  0.2473825 -0.8682160
## 2003-03-17  0.2433945  0.1511438
## 2003-04-19 -0.4025846  0.5398422

window(x, start = "2003-02-01", end = "2003-03-01")
##                  [,1]       [,2]
## 2003-02-09 -0.6704893  1.1487506
## 2003-02-11 -1.8119186  0.4495418
## 2003-02-13  0.4229121 -1.3511595
window(x, start = as.Date("2003-02-01"), end = as.Date("2003-03-01"))
##                  [,1]       [,2]
## 2003-02-09 -0.6704893  1.1487506
## 2003-02-11 -1.8119186  0.4495418
## 2003-02-13  0.4229121 -1.3511595
window(x, index = x.date[1:6], start = as.Date("2003-02-01"))
##                  [,1]      [,2]
## 2003-02-09 -0.6704893 1.1487506
## 2003-02-11 -1.8119186 0.4495418
window(x, index = x.date[c(4, 8, 10)])
##                  [,1]       [,2]
## 2003-01-07 -1.0707324 -0.6190318
## 2003-03-15  0.2473825 -0.8682160
## 2003-04-19 -0.4025846  0.5398422

23.2.5 时间序列其他频率

如果需要按月按年生成索引,可以使用seq.Date()函数,我们使用按月生成来演示.

date <- seq.Date(as.Date("1969-01-01"), by="month",
                 length.out = 70)
head(date)
## [1] "1969-01-01" "1969-02-01" "1969-03-01" "1969-04-01" "1969-05-01"
## [6] "1969-06-01"

23.2.6 时间序列的矩阵运算

xts对象注重时间。根据设计,当你使用两个xts对象执行任何二进制操作时,这些对象首先会使用时间索引的交集来对齐。这在第一次遇到时可能会令人惊讶。

这样做的原因是你想保留数据的时间点方面,确保你不会在计算中引入意外的超前(或滞后!)偏差。

我们先创建两个时间序列ab.

dates <- as.Date("2015-01-24")+0:2

a <- xts(rep(1, 3), order.by = dates)

b <- xts(2, order.by = dates[1])

# 两个时间序列相加
a+b
##            e1
## 2015-01-24  3
# a加上b的数值
a+as.numeric(b)
##            [,1]
## 2015-01-24    3
## 2015-01-25    3
## 2015-01-26    3

如果想要a的数据维度,我们可以使用merge函数

# 使用a的数据维度填充b
merge(b, index(a))
##             b
## 2015-01-24  2
## 2015-01-25 NA
## 2015-01-26 NA

# 填充为0
a + merge(b, index(a), fill = 0)
##            b
## 2015-01-24 3
## 2015-01-25 1
## 2015-01-26 1

# 最后的观察值填充NA
a + merge(b, index(a), fill = na.locf)
##            b
## 2015-01-24 3
## 2015-01-25 3
## 2015-01-26 3

23.3 融合和修正时间序列

23.3.1 按行或列的融合

向时间序列添加数值的最常用方法之一是按列组合。按列组合是通过xts函数merge()cbind()完成的,它们是同一个调用。这些函数就像数据库的连接,但它们不是使用数据的值,而是使用时间

xts支持四种不同的连接,内连接(inner),即时间的交集,外连接(outer),即时间的联合,以及左右连接(left or right joins),分别只使用左边或右边系列的时间。 merge()接收任意数量的对象来连接,一个 “连接”(join)参数和一个 “填充”(fill)参数来处理合并后产生的缺失问题。我们首先创建两个时间序列,为了展示几种连接,使用了不同的时间。

# 时间序列 x
d_x <- as.Date("2016-08-09")+0:2
x <- xts(x = rep(1, 3), order.by = d_x)

# 时间序列 y
d_y <- as.Date("2016-08-09")+c(0, 1, 3)
y <- xts(x = rep(2, 3), order.by = d_y)

默认设置,外连接(outer)

merge(x, y)
##             x  y
## 2016-08-09  1  2
## 2016-08-10  1  2
## 2016-08-11  1 NA
## 2016-08-12 NA  2

内连接(inner)

merge(x, y, join = "inner")
##            x y
## 2016-08-09 1 2
## 2016-08-10 1 2

右连接(right),只使用y的日期,然后x缺失值变成了1,是使用了fill = na.locf

merge(x, y, join = "right", fill = na.locf)
##            x y
## 2016-08-09 1 2
## 2016-08-10 1 2
## 2016-08-12 1 2

同时,我们也可以将时间序列与普通的向量进行组合

merge(x, c(2, 5, 6))
##            x c.2..5..6.
## 2016-08-09 1          2
## 2016-08-10 1          5
## 2016-08-11 1          6

merge(x, 3)
##            x X3
## 2016-08-09 1  3
## 2016-08-10 1  3
## 2016-08-11 1  3

如果要按照行组合可以使用rbind(),我们仍然可以传入任何数量的对象进行绑定,但是这些对象需要更具体一些。他们首先需要有时间,否则就会出错,或者应该给他们分配什么索引。除此之外,你确实需要确保所绑定的对象有相同数量的列。一旦你得到了这两点,新行或多行将按照正确的时间顺序插入(会有重复)。

rbind(x, y)
##            [,1]
## 2016-08-09    1
## 2016-08-09    2
## 2016-08-10    1
## 2016-08-10    2
## 2016-08-11    1
## 2016-08-12    2

23.3.2 处理缺失值

l.o.c.f 是”last observation carried forward”的缩写

处理缺失值,我们使用na.locf()函数,文档,它有几个重要的参数:

  • na.rm :默认值是TRUE,移除NA
  • fromLast:默认值是FALSE,结果是从后往前更新NA值还是从前往后.
  • maxgap:默认值是Inf,

我们新建一个时间序列

d_z <- as.Date("2016-08-09")+0:3
z <- xts(x = c(1, NA, NA, 4), order = d_z)

# 按行融合,这里也可以用merge()
cbind(z = z, 
      z1 = na.locf(z), 
      z2 = na.locf(z, fromLast = TRUE))
##             z z1 z2
## 2016-08-09  1  1  1
## 2016-08-10 NA  1  4
## 2016-08-11 NA  1  4
## 2016-08-12  4  4  4

其他处理缺失值的函数

  • 替换缺失值
  • na.fill(object, fill, ...):所有NA值填入一个替换值.
  • 移除缺失值
  • na.trim(object, ...):只删除开头或结尾的有NA值的行
  • na.omit(object, ...):删除所有的NA行
  • 插值补充缺失值
    • na.approx(object, ...):它根据时间之间的距离在观测值之间进行线性插值。虽然这对预测的实际用途有限,因为它需要用未来的观测值来计算过去的观测值,但在某些情况下,它可能是有用的。

23.3.3 滞后和差分

时间序列课本上滞后算子用的是B,这里用L,但是课本上的滞后算子好像放在右边的? \[ X_{t-k}=L^{k}X_{t} \]

d_x <- as.Date("2023-2-26")+0:4
x <- xts(x = 5:9, order.by = d_x)

# 向前
lead_x <- lag(x, k = -1)

# 向后 
lag_x  <- lag(x, k = 1)

# 合并
merge(lead_x, x, lag_x)
##            lead_x x lag_x
## 2023-02-26      6 5    NA
## 2023-02-27      7 6     5
## 2023-02-28      8 7     6
## 2023-03-01      9 8     7
## 2023-03-02     NA 9     8

查看单一(或 “一阶”)差分的简单方法如下面的式子,其中k是回溯的滞后数。高阶差分只是将差分重新应用于每个先前的结果: \[ x_t-x_{t-k} = x_t-L^{k}x_t \] 所以我们可以用lag()函数手动的计算k阶差分,或者使用差分函数diff(),文档.

23.4 按时间应用和聚合

23.4.1 时间上的应用

时间序列的一个非常常见的使用模式是计算不连续的时间段的值,或者从高频率到低频率的集合值。对于大多数系列,你经常想看到价格或测量的每周平均值。你甚至会发现自己看到的数据有不同的频率,你需要将其归一化到最低的频率。

使用时间序列对象的好处之一是很容易按时间应用函数。按照年月日周期或者时间段找到最后一个时间点,使用endpoints()函数,文档

第一个调用将生成你数据中每一周的最后一天。请注意,最后返回的值将总是你的输入数据的长度,即使它不对应于一个跳过的区间。

temps <- read.csv("datasets/Temps.csv")
temps <- as.xts(temps)

# 周期为一周
endpoints(temps, on = "weeks")
## [1]  0  4 11 16

# 周期为两周
endpoints(temps, on = "weeks", k = 2)
## [1]  0 11 16

此时,有了endpoints()来定位周期的结尾。下面就是如何处理这些值

在最简单的情况下,可以对时间序列进行取子集以获得最后的数值。在某些情况下,这可能是有用的。例如,确定一个传感器在一小时内的最后已知值,或者在一天的开始时获得美元/日元汇率的值。对于大多数系列,你会想对端点之间的值应用一个函数。实质上,使用基础函数apply(),但是在一个时间窗口上使用。

要做到这一点,xts提供了period.apply()命令,它需要一个时间序列,一个端点的索引和一个函数。文档

# 计算每周的末尾的索引
ep <- endpoints(temps, on = "weeks")

# 现在计算每周平均数并显示结果
period.apply(temps[, "Temp.Mean"], INDEX = ep, FUN = mean)
##            Temp.Mean
## 2016-07-04  69.75000
## 2016-07-11  77.42857
## 2016-07-16  76.20000

通常情况下,将数据按时间分割成互不相干的小块,并对这些时间段进行一些计算是很有用的。

xts提供的split()函数讲数据按时间划分数据块。split()函数创建一个列表,其中包含每个分割的元素。split()中的f参数是一个描述要分割的时期的字符串(例如,“月”、“年”等)。最后我们可以使用lapply()函数对每一块进行计算.

# 按周分割
temps_weekly <- split(temps, f = "weeks")

# 按周计算均值
temps_avg <- lapply(X = temps_weekly, FUN = mean)
temps_avg
## $`2016-07-01`
## [1] 69.5
## 
## $`2016-07-05`
## [1] 77.90476
## 
## $`2016-07-12`
## [1] 75.46667

按周期分割数据的方法,可以对比一下,关于do.call函数的文档(do.call相当于可以调用函数,然后函数参数可以列表传递)

  • split()-lapply()-rbind()范式
  • endpoints()-[index]
# 使用split、lapply和rbind的适当组合
temps_1 <- do.call(rbind, lapply(split(temps, "weeks"), function(w) last(w, n = "1 day")))

# 使用endpoints()
last_day_of_weeks <- endpoints(temps, on = "weeks")

# 取子集
temps_2 <- temps[last_day_of_weeks]

23.4.2 时间上的聚合

在时间序列中,最常见的汇总方式之一是将单变量序列转换为传统意义上的区间条,在金融领域的特殊情况下被称为开盘-高点-低点-收盘条,或OHLC。这种聚合方式给我们提供了一个在特定时期观察到的数值的摘要。要做到这一点,对于每一个时期,我们要计算起始值,最大值,最小值,和最终值。xts提供的to.period()函数

文档

下面所需要的数据由Datacamp提供的,数据链接

usd_eur_weekly <- to.period(usd_eur, period = "weeks")
head(usd_eur_weekly)
##            usd_eur.Open usd_eur.High usd_eur.Low usd_eur.Close
## 1999-01-04       1.1812       1.1812      1.1812        1.1812
## 1999-01-11       1.1760       1.1760      1.1534        1.1534
## 1999-01-15       1.1548       1.1698      1.1548        1.1591
## 1999-01-25       1.1610       1.1610      1.1566        1.1566
## 1999-02-01       1.1577       1.1577      1.1303        1.1303
## 1999-02-08       1.1328       1.1339      1.1283        1.1296

usd_eur_monthly <- to.period(usd_eur, period = "months")
head(usd_eur_monthly)
##            usd_eur.Open usd_eur.High usd_eur.Low usd_eur.Close
## 1999-01-29       1.1812       1.1812      1.1371        1.1371
## 1999-02-26       1.1303       1.1339      1.0972        1.0995
## 1999-03-31       1.0891       1.1015      1.0716        1.0808
## 1999-04-30       1.0782       1.0842      1.0564        1.0564
## 1999-05-28       1.0571       1.0787      1.0422        1.0422
## 1999-06-30       1.0449       1.0516      1.0296        1.0310

# 修改名字
usd_eur_monthly_name <- to.period(usd_eur, period = "months",name = "USDEUR")
head(usd_eur_monthly_name)
##            USDEUR.Open USDEUR.High USDEUR.Low USDEUR.Close
## 1999-01-29      1.1812      1.1812     1.1371       1.1371
## 1999-02-26      1.1303      1.1339     1.0972       1.0995
## 1999-03-31      1.0891      1.1015     1.0716       1.0808
## 1999-04-30      1.0782      1.0842     1.0564       1.0564
## 1999-05-28      1.0571      1.0787     1.0422       1.0422
## 1999-06-30      1.0449      1.0516     1.0296       1.0310

# 从开始观察
usd_eur_monthly_index <- to.period(usd_eur, period = "months", indexAt = "firstof")
head(usd_eur_monthly_index)
##            usd_eur.Open usd_eur.High usd_eur.Low usd_eur.Close
## 1999-01-01       1.1812       1.1812      1.1371        1.1371
## 1999-02-01       1.1303       1.1339      1.0972        1.0995
## 1999-03-01       1.0891       1.1015      1.0716        1.0808
## 1999-04-01       1.0782       1.0842      1.0564        1.0564
## 1999-05-01       1.0571       1.0787      1.0422        1.0422
## 1999-06-01       1.0449       1.0516      1.0296        1.0310

usd_eur_yearly <- to.period(usd_eur, period = "years", OHLC = FALSE)
head(usd_eur_yearly)
##            DEXUSEU
## 1999-12-31  1.0070
## 2000-12-29  0.9388
## 2001-12-31  0.8901
## 2002-12-31  1.0485
## 2003-12-31  1.2597
## 2004-12-31  1.3538

23.4.3 窗口滚动函数

你可能想应用的一个常见的聚合涉及到在一个时期的范围内进行计算,但要返回该时期的每个观察点的中期结果。

例如,你可能想计算一个系列的月度至今的累积总和。在查看你有兴趣投资的共同基金的每月业绩时,这将是相关的。使用数据edhec(链接)来自PerformanceAnalytics

最基础的方法是分割,然后求累加和:

# 按年分割
edhec_years <- split(edhec , f = "years")

# 使用lapply计算累加和
edhec_ytd <- lapply(edhec_years, FUN = cumsum)

# 合并
edhec_xts <- do.call(rbind, edhec_ytd)

xts通过直观的命名zoo函数rollapply()提供了这种便利。这个函数的内部结构是

rollapply(x, width = 10, FUN = max, na.rm = TRUE)
  • x:时间序列对象;
  • width:一个窗口大小的宽度(可能很把握);
  • FUN:应用于每个滚动期的函数.
head(rollapply(temps, 3, FUN = sd))
##            Temp.Max Temp.Mean Temp.Min
## 2016-07-01       NA        NA       NA
## 2016-07-02       NA        NA       NA
## 2016-07-03 2.645751  1.527525 2.081666
## 2016-07-04 1.000000  5.291503 6.806859
## 2016-07-05 6.082763  5.686241 5.507571
## 2016-07-06 5.507571  1.732051 1.000000

23.5 xts包的其他特点

23.5.1 时区的设置(重要)

什么是时区呢?时区是一种跟踪地理上定义的与UTC的偏移量的方法,历史上也被称为格林威治标准时间。当你向西移动时,你的时钟向后移动。向东移动,时间前进。时区还跟踪日光节约时间,它因地点不同而不同,甚至随时间变化而变化! 幸运的是,xts和R为你追踪这个令人困惑的问题,你所需要做的就是指定所需的时区。将此卸载到你的时间序列对象的力量在于,你现在可以自由地在各种时区同时工作,而且在内部,一切都将简单地 “表现得适当”。时区是如此重要,你应该始终确保设置一个TZ环境变量,否则你可能会惊讶地发现操作系统为你选择了时区 为了帮助你选择时区,R有一个关于管理所有细节的zoneinfo数据库的很好的参考页,你可以用help(OlsonNames)访问它。它非常值得你花时间。

当涉及到时间时,xts对象就有些棘手了。在内部,我们现在已经看到,index属性实际上是一个数值向量,对应于UNIX纪元(1970-01-01)以来的秒数。

这些值如何在打印时显示,以及在使用index()函数时如何返回给用户,都取决于一些关键的内部属性。

  • tformat:内部时间的表现形式(原来是tformat())
  • tclass:时间格式(原来的是indexClass())
  • tzone:时区(原来是indexTZ())
index(temps)[1:3]
## [1] "2016-07-01 CST" "2016-07-02 CST" "2016-07-03 CST"

tclass(temps)
## [1] "POSIXct" "POSIXt"

tzone(temps)
## [1] ""

tformat(temps) <- "%b-%d-%Y"

head(temps)
##             Temp.Max Temp.Mean Temp.Min
## 7月-01-2016       74        69       60
## 7月-02-2016       78        66       56
## 7月-03-2016       79        68       59
## 7月-04-2016       80        76       69
## 7月-05-2016       90        79       68
## 7月-06-2016       89        79       70

在一般情况下,处理时间序列最棘手的部分之一是处理时区问题。xts提供了一个简单的方法来在每个序列的基础上利用时区。虽然R在本地类POSIXct和POSIXlt中提供了时区支持,但xts将这种能力扩展到整个对象,允许你在各种对象中拥有多个时区。

一些内部操作系统函数需要一个时区来进行日期计算。如果没有明确设置时区,就会为你选择一个时区! 要注意在你的环境中总是设置一个时区,以防止在处理日期和时间时出现错误。

tzone(temps) <- "Asia/Hong_Kong"
tzone(temps)
## [1] "Asia/Hong_Kong"

23.5.2 周期与时间标记

周期性的概念是非常简单的。你的数据是以何种规律性重复出现的?对于股市数据,你可能有每小时的价格,或者是每天的开盘价-最高价-最低价-收盘价。对于宏观经济系列,它可能是每月或每周的调查数字。

xts提供了一个方便的工具,通过使用periodicity()估计观测值的频率–也就是我们所说的周期性–来发现数据中的这种规律性。

periodicity(temps)
## Daily periodicity from 2016-07-01 to 2016-07-16

periodicity(edhec)
## Monthly periodicity from 1997-01-31 to 2019-11-30

# 转换成每年的,有什么不同?
edhec_yearly <- to.yearly(edhec)

periodicity(edhec_yearly)
## Yearly periodicity from 1997-12-31 to 2019-11-30

通常情况下,不仅要知道你的时间序列索引的范围,还要知道你的时间序列数据涵盖多少个离散的不规则周期,这是很方便的。当你知道xts提供了一组函数来做这件事的时候,你不应该感到惊讶!这就是xts文档

nseconds(x)
nminutes(x)
nhours(x)
ndays(x)
nweeks(x)
nmonths(x)
nquarters(x)
nyears(x)
# 数月数
nmonths(edhec)
## [1] 275

# 数季节数
nquarters(edhec)
## [1] 92

# 数年数
nyears(edhec)
## [1] 23

xts使用一个非常特殊的属性,叫做index,来为你的对象提供时间支持。由于性能和设计的原因,索引是以一种特殊的方式存储的。这意味着无论你的索引是什么类别(如Date或yearmon),对xts来说,一切内部看起来都一样。原始索引实际上是一个自UNIX纪元以来的小数秒的简单向量。

index()通过使用你的indexClass神奇地帮你做到了这一点。要获得索引的原始向量,你可以使用.index()。注意函数名称前的关键点。

比提取原始秒数更有用的是提取类似于POSIXlt类的时间成分的能力,它密切反映了POSIX内部编译的底层结构tm。这个功能是由一些命令提供的,如.indexday().indexmon().indexyear()等等。

# Explore underlying units of temps in two commands: .index() and .indexwday()
.index(temps)
##  [1] 1467302400 1467388800 1467475200 1467561600 1467648000 1467734400
##  [7] 1467820800 1467907200 1467993600 1468080000 1468166400 1468252800
## [13] 1468339200 1468425600 1468512000 1468598400
## attr(,"tzone")
## [1] "Asia/Hong_Kong"
## attr(,"tclass")
## [1] "POSIXct" "POSIXt" 
## attr(,"tformat")
## [1] "%b-%d-%Y"
.indexwday(temps)
##  [1] 5 6 0 1 2 3 4 5 6 0 1 2 3 4 5 6


# Create an index of weekend days using which()
index <- which(.indexwday(temps) == 6 | .indexwday(temps) == 0)

# Select the index
temps[index]
## Warning: object timezone (Asia/Hong_Kong) is different from system timezone ()
##   NOTE: set 'options(xts_check_TZ = FALSE)'to disable this warning
##     This note is displayed once per session
##             Temp.Max Temp.Mean Temp.Min
## 7月-02-2016       78        66       56
## 7月-03-2016       79        68       59
## 7月-09-2016       81        73       67
## 7月-10-2016       83        72       64
## 7月-16-2016       79        69       60

23.5.3 高频时间数据

我们看到的大多数时间序列都是每日或较低频率的。根据你的领域,你可能会遇到更高频率的数据–想想日内交易间隔,或医疗设备的传感器数据。

在这些情况下,xts中的两个函数是很方便了解的。

如果你发现你的观测结果有相同的时间标记,扰动或删除这些时间以允许唯一性可能是有用的。xts提供的函数make.index.unique()就是为了这个目的。eps参数是epsilon或small change的缩写,它控制了相同时间应该被扰动的程度,而drop = TRUE让你完全删除重复的观测值。文档

在其他场合,你可能会发现你的时间标记有点太精确了。在这些情况下,最好是四舍五入到某个固定的时间间隔,例如,一个观察可能发生在一小时内的任何时间点,但你想记录下一个小时开始的最新时间。对于这种情况,align.time()命令可以满足你的需要,将n参数设置为你想四舍五入的秒数。文档

make.index.unique(x, eps = 1e-4)  # Perturb
make.index.unique(x, drop = TRUE) # Drop duplicates
align.time(x, n = 60) # Round to the minute