3.4 日期和时间的处理
在上一节最后,我们留下了一个问题:数据收集过程中经常产生的日期和时间,在SAS中是如何存储的?本节就来讨论日期和时间的存储方式。
首先开宗明义,SAS中的变量类型只有数值型和字符型,日期和时间是以数值型变量存储的。日期存储的数值是从1960年1月1日开始到该日期的天数,时间存储的数值是从1960年1月1日0点0分0秒开始到该时间点的秒数。
使用间隔单位数表示日期,是很多编程语言常用的方法。例如Java语言是以1970年1月1日为时间的起点,VBA以1899年12月30日作为时间起点,C语言以1970年1月1日作为时间起点。这种选定某日期作为起点,以某日期与起点的相距天数的数值作为日期的保存方法具有诸多好处。
(1)方便数据类型的存储。日期作为一种自然存在,本质上既非数字,也非字符,而是人们对间隔的感知方式。如果为日期单独设置一种数据类型,既会增加程序的复杂程度,也不方便学习者记忆和理解。相反使用天数或秒数表示日期,只要起始时间确定,给定某数值,就可以唯一确定该数值所对应的日期。同时给定某日期,也可以找到其对应的唯一数值。
(2)方便计算。针对日期、时间的计算,无非是计算两个日期相距的天数、秒数等,或给定一个日期,计算与其相距特定间隔的日期时间,使用数值型变量可以方便地对日期进行加减运算,而如果按照年月日存储日期,则涉及月份相加进位问题、平年闰年转化问题等,语言的可编程性会大打折扣。
不过以天数或秒数对应的数值作为日期时间也有某些缺点,本书并非编程通识类数据,在这里只讲一个小问题,即32位系统的日期最大值问题。在以前,32位系统是主流,而时间所用的也是32位,它最多可以表示的数字是2147483647,按照Java以1970年1月1日为起点计算,到2038年01月19日03时14分07秒,以Java编写的程序的日期会达到最大值,出现时间回归现象,有可能造成软件异常。不过目前64位系统已经成为主流,以64位存储的日期最大可以表示到292277026596年12月4日15时30分08秒,这个数值是目前宇宙年龄的22倍,可以说直到天荒地老,我们再也不用担心时间不够用的问题了。
SAS采取的是以1960年1月1号为日期的原点,1960年1月1号0点0分0秒为时间的原点,但并不是说在此之前的日期就无法表示。在原点之前的日期依然用与原点的间隔来表示,只是前面需要加一个负号。例如-2如果表示日期,则表示的是1960年1月1日的前两天,即1959年12月30日。
3.4.1 日期和时间变量的数据格式
日期和时间变量的值基本伴随着数据格式,与数值不同,不套用数据格式的日期和时间完全无法理解。请问19283是哪一年?43500是上午还是下午?虽然在计算上使用与起始点的间隔非常方便,但相应的理解成本会非常高。所幸SAS提供了大量日期和时间的数据格式,下面分别来学习。
1.日期变量的数据格式
日期变量的格式是将日期存储的数字转化为完全或部分包含年、月、日的值,其中比较常用的是yymmdd.格式和date.格式,它们的使用方法与数值变量格式使用方法相同,都是格式名+长度的结构。首先尝试给数字22000赋予yymmdd格式。
运行以下代码:
使用format语句对date变量赋予数据格式,让它的格式显示为yymmdd10.格式,查看数据集,我们得到的是2020-03-26,这正是与日期原点相差22000天的日期,而yymmdd10.格式就是我们要介绍的第一个日期的数据格式,它将日期以4位的年、2位的月、2位的日表示,中间以“-”间隔,总长度为10。yymmddw.为一类数据格式的统称,它们都以年为开头,后接月和日,不同长度的显示样式不同,表3-12是不同长度的yymmdd格式的显示样式。
表3-12
其中比较常用的为6位、8位和10位的格式。
按照我国习惯,日期按年月日排列,美国往往采用月日年排列,欧洲更愿意采用日月年排列,它们就需要用到SAS中的mmddyyw.与ddmmyyw.格式,可以看出来,在以年月日表达的方式上,以yy代表年,mm代表月,dd代表日,它们之间的相对位置则代表了年月日出现的位置。
有时,我们不希望仅以数字表达日期,而是希望用月份名称来表示,SAS也提供了相关的数据格式,这就是date.格式,首先看以下代码:
运行后查看结果,得到的是26MAR2020,可以看到date.格式按照日月年的顺序显示日期,但将月份以3位字母表示。这样做的好处是表示年与日的数字分离,中间以字母间隔,每部分内容表示的意义一目了然,方便查看,当然缺点在于直观上理解不便,例如相同年份的05月和06月,我们很容易看出谁在前面,但MAY和JUN则需要反应一下。
date.格式的长度可以在5~11之间取值,它们的区别如表3-13所示。
表3-13
其中date.格式常用的长度为7、9和11。
可以看出,在日期相关的格式上,不同长度不仅影响显示日期的位数,更影响年月日显示的完整性。除此之外,SAS还提供了更多的日期格式,用于满足不同地区不同形式的日期表达方式,读者可以通过如下网站查询:
https://v8doc.sas.com/sashtml/lgref/z1263753.htm
2.时间变量的数据格式
提到时间的时候,我们一般有两种所指,一种是日期与时间的结合,它在SAS中用该日期时间点与1960年1月1日0点0分0秒相距的秒数表示,而另一种时间是指一个单纯的时间,例如05:00,它所对应的数字是其与0点0分0秒相距的秒数。我们首先来看这种单纯的时间。
时间最常用的格式为time.格式,它的显示清晰、明确,将时、分、秒以:分割显示。例如以下代码:
生成的数据集中,time变量的值为3:25:45,表示与0点0分0秒相距12345秒的时间点为凌晨3点25分45秒。我们知道,不同地区的人们对时间描述的方法不同,分为12小时制与24小时制。例如下午5点会被描述为17点,time.格式提供的是24小时制的时间,相对应地,timeampm则是12小时制下的时间。例如以上代码改写为如下代码:
得到的结果为3:25AM。奇怪?为什么使用了timeampm.格式后,时间中的秒就丢失了呢?这是因为我们限定了timeampm.格式的显示长度为8,而显示3:25AM已经用掉了7位长度,再增添一位也无法显示出秒数,所以SAS就只能保留至分钟。如果希望显示完整,则需要扩大该格式容纳的长度:
改为10位长度后,获得的结果为3:25:45AM,时、分、秒均得到了保留。
有时,我们不需要显示完整的时间,例如临床试验的数据测量,一般精确到分钟,保留到秒的话没有意义,所有秒数都会是00,反而对阅读造成不便,此时可以使用一些其他格式,方便数据的转化。
当希望只保留小时的时候,可以使用hour.格式,它只显示数字所对应的小时:
得到的结果为3。
当只希望保留小时与分钟的时候,可以使用hhmm.格式,它表示以分号分割小时和分钟:
得到的结果为3:25。
时间相比起日期,具有更灵活的特点,读者可以在以下网址查找相关的数据格式:
https://v8doc.sas.com/sashtml/lgref/z1263753.htm
3.4.2 IOS8601格式
介绍完日期和时间相关的数据格式,那么是否有一种“日期+时间”的数据格式呢?
答案当然是有的,其中最广泛使用的就是ISO8601日期时间格式。ISO这个词大家可能听说过,它的全称是International Organization of Standardization,中文名为国际标准化组织,它有162个成员,中国是ISO的正式成员,我们熟悉的中国的英文简称CN和CHN就是其3166-1号标准下的国家代码。ISO组织致力于建立全球统一的标准库,制定全球工商业的国际标准。ISO8601就是其关于日期时间交换的标准。ISO的官方语言是英语,所以它的相关标准是英文版本,我们截取ISO8601的英文描述:
Data elements and interchange formats --Information interchange --Representation of dates and times.
可以看出它是关于数据元的交换格式,代表时间与日期。目前这套标准已经广泛地应用于各个生产领域,例如食品的生产日期,如果符合ISO标准,则会按照其日期时间码标准标明生产日期。在临床试验分析领域,ISO8601标准也已成为唯一的选择,无论是受试者副作用的起止时间,还是某项检验结果的时间,都要按照ISO8601标准存储和交换。
说了这么久这项标准,那么它到底长什么样呢?简而言之,符合ISO8601标准的日期和时间如下:
它是一个结合了日期与时间的函数,首先看日期,类似yymmdd10.格式,以4位年+2位月+2位日,中间以-分割的方式表达日期,时间则类似time8.格式,使用2位时+2位分+2位秒,中间以:分隔,日期与时间之间以字母T分隔。
例如以下日期时间:
可以将年月日时分秒表达清晰。
那么,ISO8601格式在SAS中的数据格式是什么样的呢?ISO8601格式在SAS中有多种格式系列,我们着重介绍其中的E系列。E系列的格式包括e8601da.、e8601dn.、e8601dt.、e8601tm.等,其日期时间格式如表3-14所示。
表3-14
例如,我们有某药物实验的副作用记录数据集,如图3-37所示,其中变量subject表示患者ID,aeid表示副作用ID,year、month、day、hour、minute、second分别表示该副作用发生的年月日时分秒,也就是说,在数据收集的过程中,数据处理师没有把日期时间当成一个变量,而是将其各部分数值放到了不同的变量中,在最终的数据集里,我们希望看到的是一个按照标准日期时间格式显示的变量,此时就需要把它们结合到一起,再按照ISO8601格式显示。
图3-37
这里我们使用函数嵌套的方式完成转化,具体语句如下:
①②步语句中我们用到了函数的嵌套。
put函数将数字类型转化为字符类型,除了变量year以外,其他变量均采用z2.格式,即如果数字为2位数,则保留完整数字,如果数字为1位数,则在前方加0,例如2会变成02。
strip函数将put转化的字符型变量去掉首尾的空格,经常和put嵌套使用。
函数||将字符串连起来,这是SAS提供的一种简单的字符串连接方式。cat/catx函数也可以完成类似操作。
完成①②步我们获得了两个字符型变量aedate和aetime,分别存储日期和时间。然后在③步中使用input函数把“日期+时间”的组合按照is8601dt.的格式转化成数值型变量datetime。
最后使用format语句让变量datetime显示为is8601dt.格式。生成is8601dt格式的数据集如表3-15所示。
表3-15
最后的datetime变量就是我们想要的结果,它是一个数值型变量,以is8601dt.格式显示日期时间的数据格式。
3.4.3 日期和时间变量相关函数
我们需要明确,日期和时间变量虽然在SAS中并不是一类特别的变量,但SAS设计了专门的函数来处理它们。日期和时间函数本质上也是数值型变量相关函数,只是它们将数值型变量理解为日期和时间,然后进行操作。日期和时间类函数大体可以分为3类:截取类、组合类与间隔类。
1.截取类函数
当我们获取了一个日期和时间变量后,有时候我们希望将其拆解为年月日时分秒,此时截取类函数就发挥了作用,具体包含如表3-16所示的函数。
表3-16
还是以数字22000为例,它代表的日期是2020年3月26日,现在需要分别截取它的年、季度、月、日,直接生成数值型变量。实现这个操作的方法很多,可以把value转化为字符型,按位数截取年月日,再根据月份计算季度,但最简单的操作方法是直接使用函数day、month、year和qtr。
生成的结果如图3-38所示。
图3-38
善用截取类函数,可以快速地将日期、时间变量拆分成数字,然后进行运算,加快数据分析的速度。
2.组合类函数
组合类函数与截取类函数功能相反,是将组成日期的部分直接组合成一个完整的日期变量,包括如表3-17所示的函数。
表3-17
现在我们可以用以上函数实现之前例子中的需求:
在前两句中,我们分别定义了date和time变量,利用mdy函数和hms函数直接获得日期和时间的数值,然后使用一个简单的计算,因为一天有86400秒,所以用日期数字乘以86400加上时间数字,结果就是日期时间所对应的数字,最后用is8601dt.格式。得到的结果如表3-18所示。
表3-18
获得的date、time的值与之前的结果相同,这样通过合理的函数运用,化简了之前数据类型复杂的转化问题,将字符串的合并变成了数值的加减问题。
3.间隔类函数
最后要介绍的是间隔类函数,最常用的为intck和intnx。
intck函数的功能是给定两个日期,计算它们中间的间隔数量,语法为:
例如我们有date1=2020-05-18,date2=2019-03-17,希望计算它们之间相差多少天,使用intck的方法为:
读者或许发现,计算两个日期间隔的天数更简单的办法是直接相减,为什么要使用intck呢?因为intck的第一个参数,除了day以外,还可以接受week、month、quarter、year等,它们分别可以计算两个日期相距的周、月、季、年的数量,此时使用相减处理的方法获得的结果并不准确,直接应用intck函数才是最好的方法。
需要注意的是,intck的返回值默认设定为整数,即不管是计算月或年,返回结果只会是1个月或0个月,而不会出现1.5个月的情况。
intnx函数的功能是给定某个日期和间隔的数量,获得另一个日期值,它的语法为:
例如我们已知今天的日期,希望得到7个礼拜之后的日期,可以使用以下语句:
这里又使用了函数的嵌套,date函数可以获取当天的日期,这个函数没有参数,我们称为无参函数。intnx计算的就是今天后的7的礼拜后的日期。
对比intck和intnx可以发现,intck是给定日期计算间隔数,而intnx则是给定间隔数计算日期。二者既有相同点,又有不同功能,在使用时需要先考虑自己需要实现的功能再合理选择。
本节详细介绍了日期和时间数据在SAS中的存储和处理方式,SAS提供了大量专门的函数和数据格式,熟悉它们可以让我们更好地进行数据分析工作。希望读者可以在工作中学习和应用。