尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

SAS基础入门全流程:数据步、清洗、函数与ODS报表交付

SAS基础入门全流程:数据步、清洗、函数与ODS报表交付 简介《小白学SAS基础教程》是一份面向零基础读者的SAS入门读物适合被命令语法劝退、希望从实际数据处理任务入手的学习者。全书采用对话体以“小白”的提问推进围绕数据集建立、常用函数、数据清洗、SAS绘图与制表展开强调每个操作背后的实际用途而非简单罗列命令。包内为1个PDF文件压缩包约1.76MB篇幅集中便于在电脑或平板上按章节阅读并对照练习。书中涉及缺失值、异常值、重复值处理以及常规图形绘制和规范报表输出并给出可直接套用的处理思路与程序示例。已有709人学习适合作为SAS入门的系统起点也可供需要提升数据整理与报表效率的职场人士查阅。1. 一份把 SAS 当“工具箱”而不是“命令手册”的讲义多数人学 SAS 基础入门的路径是这样的打开 Help把 data、input、proc 逐条抄一遍三天后连自己建的数据集到底存哪了都说不清。这份《小白学 SAS 基础教程》走的是另一条路全书用师徒对话推进主角是个零基础的新人从编辑器窗口里写三句话开始一路做到数据合并、去重、缺失值与异常值定位、函数派生变量、sgplot 出图、tabulate 制表最后用 ODS 把结果导成能直接交上去的报表。配套给的是全书程序标注了对应页码、9 段视频以及两份附录一份面向新手列常见错误提示和高效与低效写法对比一份面向熟手讲怎么在 SAS 里调用 R。适合刚入职被 LOG 窗口和“数据集去哪了”劝退的人也适合要带新人、需要一套能照着讲的顺序的人。2. 数据步与逻辑库三句话建数据集为什么关掉 SAS 就没了这一章解决的是最基础也最容易埋雷的两件事数据集怎么建出来、建完放在哪。很多人第一次用 SAS 时程序跑通了、输出也看到了第二天打开软件发现数据集消失于是怀疑软件有问题。问题不在软件在于从头到尾没人讲清楚 Work 这个逻辑库的定位。2.1 data、input、cards 三句话与语句边界SAS 的数据步有固定骨架三句话加一个收尾缺一句都跑不动。下面这段是最小可用版本建议先原样敲一遍看 LOG 窗口的输出再改。/* 最小数据步data 起名input 声明变量cards 引数据 */ data first; input gender age; cards; 1 30 0 24 ; run;逻辑说明data first;给数据集起名这个名字遵循三条硬规则——只能由英文字母、数字和下划线组成首字符不能是数字长度不超过 32 个字符。input gender age;声明两个数值变量SAS 默认以空格作为字段分隔符读到空格就认为当前变量结束。cards;之后是数据区数据全部写完必须另起一行写一个单独的分号这一行分号是数据结束的标志跟在前一条数据后面会被当成数据内容。run;告诉 SAS 这一步可以执行了。参数怎么改变量数量随意增减上万列 SAS 也吃得下如果某个变量是字符型必须在变量名后加$否则 SAS 按数值读遇到字母就报Invalid numeric data想让 SAS 接受中文变量名可以加options validvarnameany;但不建议在生产代码里这么干迁移和排错都会变麻烦。数据集名和变量名之外还有一类东西叫“语句边界”SAS 里分号就是句号中文分号、全角分号一律不认这一条踩过的人不在少数。cards和datalines完全等价前者是老写法后者语义更清楚。选哪个纯属习惯同一份代码里保持一致就行。2.2 逻辑库与 libname临时数据集和永久数据集的分水岭SAS 资源管理器里默认能看到几个库Work 是留给用户建数据集的其余几个放的是软件自带的地图数据、帮助数据和练习数据。Work 的特点是一次性的关掉 SAS库里所有数据集清空。这就是“昨天还在、今天没了”的全部原因。要留住数据得先给一个物理目录挂上逻辑库名这个动作由libname完成。/* 把物理目录挂成逻辑库 mylib之后的 mylib.xxx 就是永久数据集 */ libname mylib /data/sasproj; data mylib.class_clean; set sashelp.class; where age 12; run; /* 用完可以解除挂载避免下次同名冲突 */ libname mylib clear;逻辑说明libname 库名 路径;建立逻辑库与磁盘目录的映射之后所有写成库名.数据集名的形式都会落到那个目录里格式是 SAS 自己的 sas7bdat 文件。set sashelp.class;读取软件自带的示例数据where在读取阶段就完成过滤。最后libname mylib clear;解除映射这一步在切换项目目录时尤其重要同一个会话里重复给 mylib 赋不同路径而不 clear很容易读到旧目录的文件。参数怎么改路径用双引号包住Windows 下写D:\sasproj也能识别如果目录不存在SAS 不会自动创建LOG 里会给出明确提示先去把目录建好再回来跑。多人协作时库名尽量带项目前缀避免同一台机器上不同项目的库名撞车。注意work.raw和raw是同一个东西SAS 默认库就是 Work。凡是没写库名的数据集一律是临时的。2.3 informat 与 format读进来和显示出来是两件事新手最容易混的一对概念。informat 管的是“这串字符按什么规则解析成值”format 管的是“这个值显示成什么样子”。两者完全独立一个日期变量可以读进来是数值、显示成 2024-03-15也可以读进来就错、显示成一串数字。/* informat 决定怎么读format 决定怎么显示 */ data fmt_demo; informat dt yymmdd10. name $20. score 8.2; format dt yymmddd10. score 8.2; input dt name score; cards; 2024-03-15 A 88.5 ; run;逻辑说明informat dt yymmdd10.告诉 SAS 输入的2024-03-15是带横线的年月日解析后存成 SAS 日期值1960 年 1 月 1 日起的天数。format dt yymmddd10.决定输出窗口里怎么显示换成date9.就显示成15MAR2024底层数值一个字节都没变。name $20.里的 20 是字符长度这个长度是硬约束超长部分直接截断且不报错属于典型的静默失败。两个特殊输入修饰符值得单独记冒号:用于不定长字符读到分隔符就停避免为每个值预留固定长度用于值内部本身含空格的字符变量需要配合infile指定分隔符。/* 冒号读不定长 读含空格的字段配合 dlm 指定逗号分隔 */ data addr; infile datalines dlm,; input id : $8. name $20.; datalines; 1001,A同学,某地址一号楼 ; run;dlm,把分隔符从空格改成逗号这样地址里的空格就不会被误当成分隔符修饰符要求后面跟一个分隔符才能结束读取所以必须配 dlm 使用只写不写 dlm读出来的结果会让人怀疑人生。2.4 从 Excel 到 SASproc import 的几个必设参数实际工作里数据多半在 Excel 或 CSV 里手工敲 cards 不现实。常见做法是用proc import一把导进来但默认参数踩坑率很高。/* 导入 Excel重点在 guessingrows 和 getnames */ proc import datafile/data/raw.xlsx outwork.raw dbmsxlsx replace; getnamesyes; guessingrowsmax; run;逻辑说明dbmsxlsx指定文件类型out指定落地数据集replace表示同名数据集直接覆盖不加这个选项第二次运行会报错。getnamesyes把首行当变量名不加的话首行会变成一条观测后面所有类型判断全乱。guessingrowsmax让 SAS 扫描全部行来推断变量类型和字符长度默认只扫前若干行前几行都是短值、后面出现长文本时就会被截断而且 LOG 里只有一句轻描淡写的提示。参数怎么改CSV 文件用dbmscsv同时加encodingutf-8处理中文如果源文件是 GBK 编码改成encodinggbk编码不匹配的典型表现是中文变问号或变乱码而数值列不受影响很容易被误判成“文件坏了”。数据量特别大时proc import的性能不理想稳妥做法是先另存为 CSV再用infile加dsd和truncover手工读控制力强得多。/* 大数据量场景手工读 CSV比 proc import 可控 */ data work.raw; infile /data/raw.csv dsd firstobs2 encodingutf-8 truncover; input id name : $30. score; run;dsd让 SAS 正确处理引号包裹的字段和连续分隔符firstobs2跳过表头truncover允许最后一列长度不足时不报错。这三个选项组合起来基本能覆盖绝大多数脏 CSV。3. 数据清洗三段式合并、去重、缺失与异常值定位数据进到 SAS 只是开始真正占时间的是清洗。这一章按“拼起来、去重复、找问题”三段推进每一段都给可直接改参数复用的代码骨架。讲义里反复强调的一点是清洗不是把报错消掉而是让每一行的来路和去向都可追溯。3.1 set 与 merge先分清纵向拼和横向对齐两个语句长得像语义完全不同。set是纵向堆叠行数相加merge是横向对齐按 by 变量把两张表的列拼到一起。搞混的后果是行数莫名其妙翻倍或者减半。/* 纵向合并结构一致的多张表摞起来in 记录来源 */ data work.all; set q1 q2 indssrc; source src; /* 1 表示来自 q12 表示来自 q2 */ run;inds生成一个临时变量记录当前观测来自 set 列表里的第几个数据集这个变量只在数据步里有效赋给source才留得下来。用它可以快速核对各表贡献了多少行proc freq datawork.all; tables source; run;一跑就清楚。/* 横向合并先排序再按 by 对齐 */ proc sort datawork.stu; by id; run; proc sort datawork.score; by id; run; data work.merged; merge work.stu(ina) work.score(inb); by id; if a; /* 只保留主表里存在的记录左连接语义 */ if a and b then match_flag 1; else match_flag 0; run;in后面跟的变量名是临时的 0/1 标志if a;就是只保留主表有的行。if a and b则能标出匹配情况清洗阶段非常有用能一眼看出有多少记录在副表里找不到对应。最常见的报错是BY variables are not properly sorted原因就是 merge 前没排序或者两张表排序方向不一致一个升序一个降序。注意merge 时若 by 变量在某张表里有重复值结果会按笛卡尔方式展开行数暴涨。合并前先确认 by 变量唯一或者先用 3.2 的方法去重。3.2 排序之后才有 first./last.去重的两种取舍去重的核心是proc sort的nodupkey和first./last.这两套写法前者简单后者可控。/* 写法一按主键去重保留排序后的第一条 */ proc sort datawork.raw outwork.raw_dedup nodupkey; by id; run;nodupkey按 by 变量组合去重每组只留第一条。缺点是“第一条”由排序决定你不指定排序规则留下的就是物理顺序里的第一条结果不可复现。/* 写法二显式控制保留哪一条按更新时间倒序后取 first */ proc sort datawork.raw; by id descending updated; run; data work.raw_dedup; set work.raw; by id; if first.id; /* 每个 id 只保留排序后的第一条即最新记录 */ run;first.id和last.id是 SAS 自动生成的分组边界标志前提是数据已按 id 排好序且数据步里有by id;。想保留最早一条就把descending去掉。这两个变量的本质是“当前观测是否为该 by 组的首/末条”在组内做累计、做组内序号都靠它。一个实用扩展是组内序号if first.id then seq 0; seq 1;这样每行都带上了自己在该 id 下的第几条后续做“取最新两条”之类的需求直接筛seq 2就行。3.3 数组加自动变量一份查缺失值的通用骨架缺失值检查如果一个个变量写if x . then几十列的表根本写不完。SAS 提供了两个自动变量列表_numeric_和_character_配合数组和dim()能写出一份对任何数据集都通用的骨架。/* 通用缺失值骨架逐行统计缺失个数 */ data work.miss_flag; set work.raw; array nums[*] _numeric_; /* 所有数值变量自动入组 */ array chars[*] _character_; /* 所有字符变量自动入组 */ miss_num 0; miss_chr 0; do i 1 to dim(nums); miss_num (nums[i] .); /* 条件为真返回 1累加即计数 */ end; do i 1 to dim(chars); miss_chr (chars[i] ); /* 字符缺失是空串不是点 */ end; drop i; run; proc freq datawork.miss_flag; tables miss_num miss_chr; run;逻辑说明array nums[*] _numeric_;把数据集中所有数值变量一次性装进数组不用手写变量清单表结构变了代码也不用改。dim(nums)返回数组元素个数。miss_num (条件)是 SUM 语句的写法条件成立取 1、不成立取 0逐行累加就得到该行的缺失变量个数。字符型缺失要判断空串写成.永远为假这是新手最常犯的一个错误。参数怎么改如果只想检查某几列把_numeric_换成显式列表array nums[*] score age income;_temporary_数组可以用来存跨行累计值写array cnt[10] _temporary_;。要注意 SUM 语句自带 retain 效果且把缺失当 0 处理好处是不会因为某行缺失导致整列累计变空代价是它会悄悄吞掉缺失信息需要严格传播缺失时得换成x x y;的写法。3.4 异常值定位分位数筛选与 if / where 的边界异常值没有统一定义工程上常用分位数做初筛把 1% 和 99% 之外的点先捞出来人工看。/* 先算分位点再把统计量带进数据步做筛选 */ proc means datawork.raw noprint; var score; output outwork.stat p1p1 p99p99 meanmean stdstd; run; data work.outlier; if _n_ 1 then set work.stat; /* 只把单行统计量读进来一次 */ set work.raw; if score p1 or score p99; run;if _n_ 1 then set work.stat;是把汇总表的单行结果广播到每一行的标准写法_n_是数据步的迭代计数器。这一句必须写在set work.raw;之前顺序反了统计量只会在第一行有值。这种写法隐含 retain 效果p1、p99 会一直保留到数据步结束。if和where的区别值得单独说清where在读取阶段过滤不进入 PDV效率高但不能引用_n_和本步刚生成的新变量if在 PDV 里执行能引用所有变量但整张表已经读进来了。大数据集上把能用where的条件都写成where性能差距很明显。4. 函数与派生变量round 分箱、retain 累计、lag 取上一行函数部分最忌讳按字母顺序背语法。讲义里给的思路是按“我要解决什么问题”去找函数这里挑四组在真实清洗和报表里出现频率最高的展开。4.1 数值、字符、日期、类型转换四类函数的取用顺序先把函数按用途分四类数值计算、字符处理、日期时间、类型转换。实际写代码时的顺序通常是先转换类型再处理字符再算数值最后处理日期。data work.func_demo; set work.raw; /* 字符类去空格再转大写顺序不能反 */ name_up upcase(strip(name)); name_len length(strip(name)); /* 类型转换put 数值转字符input 字符转数值 */ score_c put(score, 8.2); score_n input(score_c, 8.); /* 日期类datdif 算两个日期之间的天数 */ days datdif(birth, today(), act/act); run;strip()去掉首尾空格upcase()转大写。如果先转大写再去空格中间的空格仍然在比较时照样不匹配。put(变量, 格式)把数值按指定格式转成字符input(变量, 格式)反过来。这两个函数在做“字符型编号排序”时特别有用——字符串10会排在9前面转成数值就正常了。datdif的第三个参数是计算基准act/act表示按实际天数做年龄和账龄时用它比手工减日期稳妥。参数怎么改put的格式要和目标长度匹配put(score, 8.2)输出宽度 8、保留 2 位小数宽度不够会变成科学计数法或者星号input的格式要和源字符串结构匹配源里带千分位逗号就得用commaw.。4.2 round 不只是四舍五入等距分箱为什么该用 floorround(x, 10)的含义是“四舍五入到最接近的 10 的倍数”不是“向下取整到 10 的倍数”。这两个语义在分箱场景下差别巨大。data work.bin; set work.raw; bin_round round(score, 10); /* 15 会变成 20跨档 */ bin_floor floor(score / 10) * 10; /* 15 落在 10左闭右开 */ bin_label put(floor(score / 10) * 10, best. -l) || - || put(floor(score / 10) * 10 9, best. -l); run;score 15时round给 20floor给 10。做等距分箱、做年龄分段、做金额区间统计一律用floor(x/宽度)*宽度保证区间是左闭右开、不重不漏。round的正确用途是控制小数位比如round(x, 0.01)保留两位。分箱标签用put(...) || - || put(...)拼出来best. -l里的-l表示左对齐拼出来的标签不会带着一堆空格。这一步做完proc freq出来的表直接就是分档统计不用再手工整理。4.3 retain 与累加语句组内累计和组内序号retain的作用是让变量跨行保留值默认情况下数据步每读一行所有变量都会重置。做累计、做序号、做“上一行带过来的状态”都靠它。proc sort datawork.raw; by dept id; run; data work.cum; set work.raw; by dept id; retain dept_total 0; if first.dept then dept_total 0; /* 每个部门开头清零 */ dept_total dept_total amount; /* 显式加法缺失会传播 */ if first.dept then seq 0; seq 1; /* SUM 语句自动 retain */ run;retain dept_total 0;声明变量并在数据步开始时初始化为 0。if first.dept then dept_total 0;保证每换一个部门重新开始累计。这里的dept_total dept_total amount;是显式加法遇到缺失值结果就是缺失语义严格。而seq 1;是 SUM 语句自动 retain 且把缺失当 0永远不会产生缺失值。这两种写法不是风格差异是语义差异。需要“缺失就暴露出来”的场景用显式加法需要“计数永远有值”的场景用 SUM 语句。混用的后果是累计列里偶尔冒出几个缺失排查半天找不到源头。4.4 dif 与 lag队列语义和最常见的错位lag(x)取的不是“上一行的 x”而是“这条语句上一次被执行时的 x”。理解这句话关于 lag 的绝大部分困惑就消失了。/* 错误示范lag 写在 if 里面只有满足条件才入队 */ data work.wrong; set work.raw; if dept A then prev lag(score); /* 队列被条件打乱 */ run; /* 正确写法先无条件取再按条件使用 */ data work.right; set work.raw; prev lag(score); if dept A then diff score - prev; run;第一种写法里只有 dept 为 A 的行才执行 lag队列里存的都是 A 组的值prev变成“上一个 A 的 score”而不是上一行。第二种写法每行都入队prev才是真正的上一行值之后再决定用不用。dif(x)等价于x - lag(x)同样受这个规则约束。做同比、环比、相邻记录差值时另一条路是先proc sort再用retain手工保存上一行逻辑更直白出错时也更好定位。两条路都行团队里统一一种即可混着写最容易出问题。5. 五个高频翻车点从 LOG 报错到 ODS 样式丢失的排查顺序清洗和派生写完程序能跑通不代表结果可信。这一章把实际使用中最常遇到的五处翻车按“现象、原因、解决”记下来都是血泪经验换的。5.1 数据步与导入环节的三处静默失败第一处字符变量忘了写$。现象LOG 里出现NOTE: Invalid numeric data, nameA同学那一行的字符变量变成缺失数值变量也一起变缺失但程序照常跑完输出窗口看起来一切正常。原因input语句里没加$的变量一律按数值读SAS 尝试把文本转成数字失败把整个 PDV 里该行的值置空。解决input id name $;或者更稳妥地写input name : $20.;同时养成看 LOG 里Invalid numeric data关键字的习惯这类提示不会中断运行只会静默污染数据。第二处proc import导入 Excel 后长文本被截断。现象前几十行正常后面某些行的备注列只剩半截没有任何报错。原因SAS 推断字符变量长度时默认只扫描前面若干行后面出现更长的值就按已推断的长度截断。解决加guessingrowsmax;让它扫全表数据量大时先另存为 CSV 再用infile手工读长度在input里写死可控性最好。第三处程序提交后既没输出也没报错光标一直闪。现象点了运行LOG 空白输出窗口空白SAS 像卡住了。原因cards;后面的数据区忘了写单独一行的分号SAS 认为数据还没输完一直在等。解决翻到程序末尾补一个独立成行的;然后重新提交。这个坑几乎每个新手都会踩一次判断依据很简单——LOG 里连NOTE: The data set ... has ... observations都没出现就说明数据步根本没结束。5.2 合并、累计与输出环节的两处偏差第四处merge 之后行数暴涨。现象两张表各一万行merge 完变成三万行甚至更多且找不到明显重复。原因by 变量在其中一张表里不唯一SAS 按笛卡尔方式把每个匹配组合都输出一行。解决合并前先对两张表分别跑proc sort nodupkey或者用first./last.确认唯一性合并后用if a and b标志位统计匹配率匹配数明显超过主表行数就说明有重复。养成“merge 前后各看一次行数”的习惯比事后查错省事得多。第五处ODS 输出到 Word 后表格样式全丢。现象SAS 里看着是规整的三线表导出 rtf 或 HTML 打开后边框粗细不一、表头没有加粗、列宽乱掉。原因ODS 默认套用系统样式模板没有显式指定 style不同目标格式的默认样式不一致。解决在ods rtf或ods html语句里加styleJournal或者在proc template里定义自己的表模板再引用。/* 导出 rtf 时显式指定样式三线表效果才稳定 */ ods rtf file/data/report.rtf styleJournal; proc tabulate datawork.summary; class dept; var amount; table dept, amount*(n mean std); run; ods rtf close;styleJournal是一套偏学术排版的样式表头带底色、边框克制导出后基本不用再手工调。ods rtf close;千万别漏漏了文件会一直处于占用状态内容不完整。注意任何 ODS 目标用完都要 close包括 html、pdf、excel。多个目标同时打开时按打开的逆序关闭顺序错了后面的文件会内容错乱。6. 交付前的三段校验把行数、缺失率、值域做成一次宏调用清洗和制表跑完我一般不会直接交。交之前固定跑三段校验行数守恒、主键唯一、关键字段的值域和缺失率。这三件事写成宏每次换数据集只改参数比肉眼扫输出靠谱得多。/* 交付前自检宏行数、主键基数、数值变量缺失率 */ %macro check(ds, key); proc sql noprint; select count(*) into :nobs from ds.; select count(distinct key.) into :nkey from ds.; select count(*) into :nmiss from ds. where key. is missing; quit; %put NOTE: 观测数nobs. 主键基数nkey. 主键缺失nmiss.; proc means datads. nmiss n mean min max maxdec2; var _numeric_; run; %mend check; %check(dswork.final, keyid);nobs和nkey相等说明主键唯一不等就回到第 3 章去重。nmiss不为 0 说明主键有空值通常意味着上游合并出了问题。proc means里的nmiss直接给出每个数值变量的缺失个数min/max用来对值域做一次目视——金额出现负数、年龄出现 200一眼就能看出来。进阶一点的做法是把阈值也参数化超过就自动报警而不是靠人看 LOG/* 缺失率超阈值时在 LOG 里打醒目提示 */ %macro check2(ds, var, maxmiss0.05); proc sql noprint; select sum(var. is missing)/count(*) into :rate from ds.; quit; %if rate. maxmiss. %then %put ERROR: var. 缺失率 rate. 超过阈值 maxmiss.; %else %put NOTE: var. 缺失率 rate. 在阈值内; %mend check2; %check2(dswork.final, varscore, maxmiss0.05);sum(条件)/count(*)在 SQL 里直接算出缺失比例%if做阈值判断%put ERROR:打出来的行在 LOG 里是红色高亮的扫一眼就能定位。阈值按业务定问卷类数据 5% 已经算高交易流水里金额字段缺失率超过 0.1% 就该回去查采集环节了。从那以后我每次交报表前都强制走一遍这三段行数对不对、主键唯一不唯一、关键字段的缺失和极值合不合理。这三步加起来不到一分钟但拦下过的错误比任何一次代码评审都多。希望帮到你。本文还有配套的精品资源点击获取
返回列表