我面临着一个常见的任务,即在给定出生日期和任意日期的情况下计算年龄(以年、月或周为单位)。问题是,我经常需要对许多记录(> 3亿)执行此操作,因此性能是这里的一个关键问题。
在SO和Google快速搜索后,我发现了3种替代方案:
- 一种常用的算术运算程序(/365.25)(link)
- 使用软件包
lubridate
(link)中的函数new_interval()
和duration()
- 来自
eeptools
包的函数age_calc()
(link,link,link)
这是我的玩具代码:
# Some toy birthdates
birthdate <- as.Date(c("1978-12-30", "1978-12-31", "1979-01-01",
"1962-12-30", "1962-12-31", "1963-01-01",
"2000-06-16", "2000-06-17", "2000-06-18",
"2007-03-18", "2007-03-19", "2007-03-20",
"1968-02-29", "1968-02-29", "1968-02-29"))
# Given dates to calculate the age
givendate <- as.Date(c("2015-12-31", "2015-12-31", "2015-12-31",
"2015-12-31", "2015-12-31", "2015-12-31",
"2050-06-17", "2050-06-17", "2050-06-17",
"2008-03-19", "2008-03-19", "2008-03-19",
"2015-02-28", "2015-03-01", "2015-03-02"))
# Using a common arithmetic procedure ("Time differences in days"/365.25)
(givendate-birthdate)/365.25
# Use the package lubridate
require(lubridate)
new_interval(start = birthdate, end = givendate) /
duration(num = 1, units = "years")
# Use the package eeptools
library(eeptools)
age_calc(dob = birthdate, enddate = givendate, units = "years")
让我们稍后讨论准确性,首先关注性能。代码如下:
# Now let's compare the performance of the alternatives using microbenchmark
library(microbenchmark)
mbm <- microbenchmark(
arithmetic = (givendate - birthdate) / 365.25,
lubridate = new_interval(start = birthdate, end = givendate) /
duration(num = 1, units = "years"),
eeptools = age_calc(dob = birthdate, enddate = givendate,
units = "years"),
times = 1000
)
# And examine the results
mbm
autoplot(mbm)
这里的结果:
底线是:lubridate
和eeptools
函数的性能比算术方法差得多(/365.25至少快10倍)。不幸的是,算术方法不够准确,我不能承担这种方法会犯的几个错误。
“由于现代格里历的构造方式,没有直接的算术方法来产生一个人的年龄,根据常见的用法-常见的用法意味着一个人的年龄应该总是一个整数,正好在生日上增加。(link)
正如我在一些帖子中所读到的,lubridate
和eeptools
不会犯这样的错误(尽管我没有查看代码/阅读更多关于这些函数的信息以了解它们使用的方法),这就是为什么我想使用它们,但它们的性能并不适用于我的真实的应用程序。
有没有一种有效而准确的方法来计算年龄?
编辑
Ops,似乎lubridate
也会出错。显然,基于这个玩具的例子,它比算术方法犯的错误更多(见第3、6、9、12行)。(我做错了什么吗?)
toy_df <- data.frame(
birthdate = birthdate,
givendate = givendate,
arithmetic = as.numeric((givendate - birthdate) / 365.25),
lubridate = new_interval(start = birthdate, end = givendate) /
duration(num = 1, units = "years"),
eeptools = age_calc(dob = birthdate, enddate = givendate,
units = "years")
)
toy_df[, 3:5] <- floor(toy_df[, 3:5])
toy_df
birthdate givendate arithmetic lubridate eeptools
1 1978-12-30 2015-12-31 37 37 37
2 1978-12-31 2015-12-31 36 37 37
3 1979-01-01 2015-12-31 36 37 36
4 1962-12-30 2015-12-31 53 53 53
5 1962-12-31 2015-12-31 52 53 53
6 1963-01-01 2015-12-31 52 53 52
7 2000-06-16 2050-06-17 50 50 50
8 2000-06-17 2050-06-17 49 50 50
9 2000-06-18 2050-06-17 49 50 49
10 2007-03-18 2008-03-19 1 1 1
11 2007-03-19 2008-03-19 1 1 1
12 2007-03-20 2008-03-19 0 1 0
13 1968-02-29 2015-02-28 46 47 46
14 1968-02-29 2015-03-01 47 47 47
15 1968-02-29 2015-03-02 47 47 47
4条答案
按热度按时间clj7thdc1#
lubridate出现上述错误的原因是您正在计算持续时间(两个瞬间之间发生的确切时间量,其中1年= 31536000 s),而不是周期(两个瞬间之间发生的时钟时间变化)。
要获取时钟时间的变化(以年、月、日等为单位),您需要使用
它给出以下输出
要只提取年份,可以使用以下命令
请注意,可悲的是,似乎比上面的方法更慢!
im9ewurl2#
好了,我在另一个post中找到了这个函数:
它是由@Jim发布的,他说:“下面的函数采用Date对象的向量并计算年龄,正确地考虑了闰年。似乎是一个比任何其他答案更简单的解决方案”。
它确实更简单,它做了我正在寻找的技巧。平均而言,它实际上比算术方法快(大约快75%)。
而且至少在我的例子中它不会犯任何错误(在任何例子中它都不应该;这是一个非常简单的函数,使用
ifelse
s)。我不认为它是一个完整的解决方案,因为我也想有几个月和几个星期的年龄,这个函数是特定的几年。我把它贴在这里,因为它解决了多年的问题。我不会接受,因为:
1.我会等待@Jim把它作为答案发布。
1.我将等待,看看是否有人想出一个完整的解决方案(高效,准确和生产年龄在几年,几个月或几周的期望)。
0g0grzrc3#
我本来打算把这个留在评论里,但我认为这值得单独回答。正如@Molx所指出的,你的“算术”方法并不像看起来那么简单--看看
-.Date
的代码,最重要的是:因此,类
Date
对象上的“算术”方法实际上是difftime
函数的 Package 器。difftime
怎么样?如果你追求的是原始的速度,这也有一堆开销。关键是
Date
对象被存储为自Jan.10起/到Jan.10止的整数天数。1,1970(尽管它们实际上并没有存储为integer
,因此data.table
中的IDate
类诞生了),所以我们可以减去这些并完成它,但为了避免调用-.Date
方法,我们必须unclass
我们的输入:就你的bang for your buck而言,这种方法比@Jim的
age
方法还要快几个数量级。以下是一些放大的测试数据:
(不包括
eeptools
,因为它几乎慢得不可能--看一下age_calc
的代码就知道,代码甚至会 * 为每对日期创建一个日期序列 *(O(n^2)
-ish),更不用说ifelse
s的大量出现了)因此,我们还强调了在小规模数据上进行基准测试的愚蠢之处。
@Jim的方法的最大代价是,随着向量的增长,
as.POSIXlt
的开销越来越大。不准确的问题仍然存在,但除非这种准确性是至关重要的,否则似乎
unclass
方法是无与伦比的。v64noz0r4#
我一直在努力解决这个问题,终于有了一个)* 完美 * 准确 *(与迄今为止提出的 * 所有 * 其他选项相比)和b)相当快(见我的基准在另一个答案)的东西。它依赖于我手工完成的一堆算术运算和来自
data.table
包的精彩foverlaps
函数。该方法的本质是从
Date
s的整数表示开始工作,并认识到所有出生日期都落在四个1461(= 365 * 4 + 1)天周期中的一个周期中,这取决于下一年的时间,即你的生日将花费366天。函数如下:
比较你的主要例子:
这种方法可以很容易地扩展到处理数月/数周。月份会有点冗长(必须指定4年的月份长度),所以我没有打扰;周很简单(周不受闰年因素的影响,所以我们可以除以7)。
我也在
base
功能上取得了很大的进展,但是a)它非常丑陋(需要0-1460的非线性转换来避免嵌套ifelse
语句等),b)最后一个for循环(以apply
的形式在整个日期列表上)是不可避免的,所以我决定这会让事情变得太慢。(转换为x1 = (unclass(birthdays) - 59) %% 1461; x2 = x1 * (729 - x1) / 402232 + x1
,供后人参考)我把这个函数添加到my package中。