在处理数据分析任务时,日期往往是最棘手的数据类型之一。你是否曾在 SAS 中尝试读取日期,结果却得到一串毫无意义的数字?或者明明导入了日期数据,却无法按时间顺序正确排序?
别担心,在这篇文章中,我们将深入探讨 SAS 编程中至关重要的两个概念:Informats(输入格式) 和 Formats(显示格式)。我们将不仅学习它们是什么,还会通过实战代码示例,掌握如何精准地控制日期数据的读取与展示。通过这篇文章,你将学会如何驯服 SAS 中的日期,让它们乖乖听话。
目录
什么是 Informats 和 Formats?
在开始写代码之前,我们首先需要厘清两个经常被混淆的核心概念。简单来说,它们决定了 SAS 如何"看"数据和"秀"数据。
Informats(输入格式):数据的翻译官
输入格式 用于告诉 SAS 如何读取数据。这就好比当我们阅读一份英文文档时,我们需要先理解单词的含义。
当我们使用 INLINECODEca43bf96/INLINECODEffd8ae28 语句直接在代码中输入数据,或者从外部的 Text、Excel、CSV 文件读取数据时,SAS 看到的只是一串字符(例如 "2023-10-01")。如果没有正确的指示,SAS 可能会把它当成一串普通文本,或者读错。这时候,我们就需要 Informat 来告诉 SAS:“嘿,把这串字符按照‘日-月-年’的顺序转换成内部的日期数值。”
Formats(格式):数据的化妆师
格式 则用于告诉 SAS 如何显示或写入变量的值。这是关于“呈现”的规则。
SAS 内部存储日期时,实际上存储的是从 1960年1月1日 到该日期的天数(这在后文会详细解释)。如果你直接看这个数字,比如 21915,你是看不出它是哪一天的。Format 的作用就是把 21915 这个“素颜”数字,化妆成人类可读的“2020-01-01”或其他你喜欢的样子。
使用范围的区别:
- 输入格式:主要在读取数据时使用,因此几乎只在 Data Step(数据步) 中配合
INPUT函数或语句使用。 - 格式:既可以在 Data Step 中定义显示样式,也可以在 PROC Step(过程步)(如
PROC PRINT)中使用,控制报表输出的样子。
SAS 日期的底层逻辑:神秘的数字
在深入代码之前,我们需要了解一个关键机制:SAS 存储日期的本质是数值。
SAS 将 1960年1月1日 定义为内部坐标的零点(0)。
- 1960年1月2日 存储为 1。
- 1959年12月31日 存储为 -1。
- 今天的日期,比如 2023年10月1日,存储为 23399 左右(具体取决于时区设置,但你可以想象这是一个巨大的整数)。
这意味着,如果你没有指定显示格式,SAS 就会直接把这个巨大的整数扔给你。这就是为什么很多人新手看到结果时会一脸懵:“我要的是日期,怎么给我一堆乱码?”
实战演练 1:读取不同格式的日期字符串
让我们来看一个实际的例子。在这个例子中,我们有两列不同格式的日期数据。我们的任务是让 SAS 正确地识别它们。
场景描述
假设我们有一行数据:
20-07-19 20-07-2019
第一部分是 INLINECODE505f1999 格式(8个字符宽度),第二部分是 INLINECODE4eaf3fa4 格式(10个字符宽度)。注意:这里的“8”和“10”指的是读取宽度,SAS 会从指针所在位置开始数这么多位。
代码示例
/* 创建一个名为 sampledata 的数据集 */
DATA sampledata;
/*
INPUT 语句用于指定读取规则:
@6 : 将指针移动到第 6 列。
date1 : 第一个变量名。
ddmmyy8. : 告诉 SAS 读取接下来的 8 个字符,并将其解析为“日-月-年”格式的日期。
@15 : 将指针移动到第 15 列(跳过中间的空格和已读区域)。
date2 : 第二个变量名。
ddmmyy10. : 告诉 SAS 读取接下来的 10 个字符。
*/
INPUT @6 date1 ddmmyy8. @15 date2 ddmmyy10.;
/* 紧接着是 CARDS/DATALINES 数据块 */
CARDS;
20-07-19 20-07-2019
;
RUN;
结果解析
如果你此时运行 PROC PRINT; RUN;,你会看到令人惊讶的结果:
date1 date2
21750 21750
虽然我们读取的是 2019年7月20日,但显示出来的却是 21750。正如前文所述,这就是 SAS 的内部存储值。如果我们不告诉 SAS 如何“化妆”,它就只会展示素颜。
实战演练 2:使用 FORMAT 语句美化输出
为了让上面的数字变成我们熟悉的日期格式,我们需要引入 FORMAT 语句。这是解决“数字乱码”问题的金钥匙。
DATA sampledata_formatted;
/* 1. 读取数据(这一步和之前一样,把字符变成内部数字) */
INPUT @6 date1 ddmmyy8. @15 date2 ddmmyy10.;
/* 2. 定义显示格式(这一步告诉 SAS 输出时如何把数字变回字符串) */
/* 注意:这里使用的格式名称可以和输入时不一样,非常灵活 */
FORMAT date1 ddmmyy8. date2 ddmmyy10.;
CARDS;
20-07-19 20-07-2019
;
RUN;
/* 打印结果查看效果 */
PROC PRINT DATA=sampledata_formatted;
RUN;
输出结果:
date1 date2
20-07-19 20-07-2019
你看,现在就清晰多了!这里的关键点在于:FORMAT 语句并没有改变存储在数据集中的数值(依然是 21750),它只是改变了它在报表中的显示方式。
实战演练 3:处理带有月份缩写的日期
在很多国际化的数据文件中,日期往往是 INLINECODEfd61fb64 或 INLINECODE88f42ad9 的格式,例如 20-jul-19。这种格式非常常见,因为它不会产生歧义(不像 01-02-03,谁知道是哪年哪月?)。
对于这种格式,我们可以使用 SAS 通用的 INLINECODE4f8548b3 格式(通常使用 INLINECODE35caf413 或 date11.)。注意,月份的缩写(如 Jul, Aug)必须是英文的前三个字母,且大小写通常不敏感。
代码示例
DATA temp;
/*
@6: 指针跳到第6列
dt: 变量名
date11.: 读取11位宽度的日期,适用于 ‘20-jul-2019‘
*/
INPUT @6 dt date11.;
/* 我们也可以直接输出为不同格式,比如将其显示为 worddate (英文日期全拼) */
FORMAT dt date11.;
CARDS;
20-jul-19
;
RUN;
/* 使用 noobs 选项可以隐藏观测号,让输出更简洁 */
PROC PRINT DATA=temp NOOBS;
TITLE "读取 DD-MMM-YY 格式日期示例";
RUN;
输出结果:
dt
20-jul-2019
代码深度解析:
在这个例子中,INLINECODEd9c64f61 输入格式非常智能,它能识别出连字符 INLINECODE719d6ac5 作为分隔符,并正确处理月份的英文缩写 INLINECODE1e573a5c。如果你尝试把 INLINECODE786ae171 改成中文的 INLINECODE9ce6c882 或数字 INLINECODEdc44b71f,SAS 可能会报错或读入缺失值。这就是输入格式严格性的体现。
深入理解:为什么我的数据读入了缺失值?
在实际工作中,最让人头疼的问题莫过于读到一堆点(.),这在 SAS 中代表缺失值。让我们看看几个常见的坑和解决方案。
案例 1:分隔符与格式不匹配
如果你的数据是 INLINECODE4d155dd3(用斜杠分隔),但你使用了 INLINECODE9a834ede 格式。INLINECODEa5b0fb3e 默认期望的是短横线 INLINECODE19573e25 或者无分隔符的紧凑数字(如 20072019)。
解决方案:使用 INLINECODEc41aefe6 格式系列时,SAS 通常能智能处理多种分隔符(如 /, -),但在某些特定格式下,必须明确告知。最安全的方法是确保源数据的一致性,或者使用 INLINECODE00c5c379 这种“智能猜测”格式。
案例 2:年份缺失导致的数据错误
如果你使用 INLINECODEe20d2e9d 读取 INLINECODEe6980b6f,SAS 会将其识别为 INLINECODE5a55b55f。但如果你读取的是 INLINECODEfe786053,SAS 会默认认为这是 INLINECODE1b40b591。这通常符合预期。然而,如果你的数据全是 1920 年代的出生日期(例如 INLINECODE4362d43e),SAS 可能会将其误判为 2029年。
解决方案:SAS 有一个系统选项 INLINECODE7973a3cd,默认值通常是 INLINECODEa9effbfd。这意味着 26-99 被视为 1926-1999,而 00-25 被视为 2000-2025。
OPTIONS YEARCUTOFF=1920; /* 我们可以根据实际情况调整这个截断年份 */
最佳实践与性能优化建议
作为一名专业的 SAS 开发者,我们不仅要写出能跑的代码,还要写出健壮、易读的代码。
1. 永远指定宽度,但要留有余地
在使用 INLINECODE8160e633 或类似的格式时,数字(8, 10, 11)代表读取的列宽。如果你的数据中有不可见的空格,或者某些行是 INLINECODEeb4413d9(单月份),而你的格式是 ddmmyy8.,指针读取可能会错位。建议:尽量让源数据对齐,或者使用带分隔符的列表输入模式。
2. 区分 INPUT 函数与 PUT 函数(重要!)
这是很多新手最容易晕的地方。
- INPUT 函数:把 字符 转 数值/日期。当你有一列字符串 "20230101" 想变成日期变量时,用它。
/* sas_date_val 是数值型 */
sas_date_val = INPUT("20230101", yymmdd8.);
- PUT 函数:把 数值/日期 转 字符。当你想把日期转换成特定的字符串导出到文本文件时,用它。
/* char_date 是字符型 */
char_date = PUT(TODAY(), yymmdd10.);
记住:“I”nput converts Inward (to number), “P”ut converts P outward (to text)。
3. 利用 FORMAT 语句的持久性
一旦你在 Data Step 中使用了 FORMAT 语句,这个格式会跟随该变量传递到所有基于该数据集的 PROC 步骤中。这意味着你不需要在每次打印时都重新定义格式,这大大提高了效率并保持了输出的一致性。
更多实战场景:处理不规范的日期
让我们通过一个稍微复杂的例子来巩固我们的知识。假设我们遇到混杂的日期格式。
DATA messy_data;
INPUT raw_date $20.; /* 先作为一个长字符串读取 */
LENGTH clean_date 8;
/* 使用 INPUTN 和 IF-THEN 逻辑来判断格式 */
/* 如果包含连字符,尝试一种格式,否则尝试另一种 */
IF INDEX(raw_date, ‘-‘) > 0 THEN DO;
clean_date = INPUT(raw_date, ANYDTDTE10.);
END;
ELSE DO;
clean_date = INPUT(raw_date, 8.); /* 尝试作为纯数字(SAS日期值) */
END;
/* 最后统一格式化输出 */
FORMAT clean_date worddate20.; /* 显示为 July 20, 2019 这种样式 */
DATALINES;
2023/10/01
21915
01-Oct-23
;
RUN;
PROC PRINT DATA=messy_data NOOBS;
VAR raw_date clean_date;
RUN;
在这个例子中,我们先不分青红皂白地把日期当成字符读进来,然后再使用智能输入格式 INLINECODE11908c18 进行清洗。INLINECODEc7e09857 是一个非常强大的工具,当你不确定数据源的具体格式时,它能自动识别 INLINECODEe51f4e27、INLINECODE9eeec630、YYMMDD 等多种顺序。
总结与后续步骤
在这篇文章中,我们像工匠一样拆解了 SAS 处理日期的机制。从理解那串“神秘的数字”开始,到掌握 INLINECODEbce1554a 的读取指针,再到利用 INLINECODEf26dc665 为数据穿上外衣。
让我们回顾一下关键点:
- 内部存储:SAS 日期本质上是数字(距离 1960年1月1日的天数)。
- Informat (Input):用于 Data Step,解决“读进来”的问题,把字符变成数字。
- Format (Display):用于任何地方,解决“秀出去”的问题,把数字变回字符显示。
- 常见陷阱:注意年份的截断设置 (
YEARCUTOFF) 以及分隔符的匹配。 - 实战技巧:遇到复杂情况可先用字符读取,再用
INPUT函数转换。
接下来的建议:
既然你已经掌握了基础的格式化技巧,我建议你接下来尝试结合 INLINECODEa110b621 和 INLINECODE9bc3e404 函数 来学习如何进行日期的加减运算(例如:计算两个日期之间相隔多少个月,或者获取“下个月的第一天”)。这才是处理时间序列数据真正强大的地方。
希望这篇指南能帮助你不再畏惧 SAS 日期!试着去修改上面的代码,看看如果格式指定错误,SAS 会给你什么提示。最好的学习方式就是亲手去“破坏”它,再修好它。