简介《小白学SAS基础教程》面向零基础或曾对SAS望而却步的初学者针对命令繁杂、教材式讲解难以上手的问题以对话体跟随主人公“小白”的学习路径展开强调以解决实际问题为导向而非简单罗列语法。全书围绕数据整理、作图和制表三部分涵盖数据集建立的基础与高级议题、常用函数的使用思路、缺失值与异常值及重复值清洗、常规与非常规图形绘制、规范报表和三线表输出等并延伸至常见SAS错误提示、高效与低效写法比较以及SAS调用R等附录内容。资源包为1个PDF文件大小约1.76MB便于随身查阅和按章节检索。目前已有709人学习适合希望用通俗案例建立SAS入门框架、提升数据管理效率并理解操作背后逻辑的读者。1. 从只会打开 SAS到能跑通第一段程序入职第一周最尴尬的场面不是不会写代码而是简历上写着熟悉 SAS打开软件却只认得那个小人图标。SAS 基础入门这道坎真正的障碍从来不是语法深度而是多数教材按软件目录组织内容先讲安装再逐条过命令和函数看完还是不知道手上那堆业务数据该从哪儿下手。这份《小白学SAS基础教程》换了个路子用对话体带一个零基础的小白走完数据整理、作图、制表三段讲 count 想到的是拆解商品评价讲 round 想到的是按区间自动分组讲数组想到的是写一个查缺失值的通用程序。它适合真正没碰过 SAS、又急着在项目里交货的人已经会写 data 步、但一到清洗和出表就卡壳的也能从里面直接抄到现成的打包程序。2. 数据步与程序步三行代码把一张表立起来SAS 和 Python、R 最不一样的地方是它把取数和加工拆成两个语法块各自有固定的开头词和结束词。这个结构不理解后面所有报错都会变成玄学。2.1 编辑器、日志、输出三个窗口的固定动线打开 SAS 9.4 之后界面上真正要盯的只有三块左边资源管理器、中间编辑器、下方日志和输出。资源管理器是逻辑库的入口所有数据集都挂在某个逻辑库下面编辑器是唯一写代码的地方日志和输出是结果的两个不同侧面。固定动线是这样的在编辑器里写程序点工具栏那个小人图标或者按 F3提交程序跑完先看日志再看输出。日志窗口记录的是发生了什么——读了多少行、建了什么数据集、哪条语句报 ERROR、哪条只是 WARNING。输出窗口放的是过程步生成的结果比如一张打印表、一个频数统计。新手最容易犯的错是跳过日志直接看输出。输出是空的可能是筛选条件真的没命中也可能是程序压根没跑完。日志才是黑匣子报错时从最上面那条 ERROR 往下读因为后面一大片红字经常只是第一条错误的连锁反应。另外提醒一句SAS 默认提交编辑器里的全部内容只想跑某一段就先用鼠标选中再提交这个习惯能省下大量重跑时间。注意日志里NOTE:是正常的运行记录不是错误真正要处理的是ERROR:WARNING:排第二优先级通常和字符长度、数值转字符的精度丢失有关。2.2 data、input、cards三行代码建起第一张表建立数据集的最小骨架就三句话分别负责叫什么名字有哪些变量数据从哪来。这段代码在所有 SAS 教材里都是第一个例子但值得逐字拆开看。/* 最简数据集两个人gender 用 1/0 表示性别 */ data first; /* 数据步开始first 是数据集名 */ input gender age; /* 声明两个数值变量顺序要和数据行一致 */ datalines; /* 也可以用 cards两者等价 */ 1 30 0 24 ; /* 分号必须另起一行顶格写 */ run; /* 告诉 SAS 这段程序到此结束 */ proc print datafirst; run;逻辑说明data语句是数据步的入口等号后面那个词就是数据集名它决定了这份数据在逻辑库里叫什么。input语句按位置顺序声明变量SAS 默认以空格作分隔符读到空格就认为当前变量结束。datalines老版本写作cards之后是纯数据区SAS 会一直读到单独一行的分号为止。参数说明数据集名和变量名只能由英文字母、数字、下划线组成首字符不能是数字长度不超过 32 个字符不能含空格、中文标点和键盘上方的特殊符号。用options validvarnameany;理论上能放开中文变量名但后面写宏、做 merge 时容易出问题不建议开。这里有个血泪经验最后那个分号如果跟在数据行末尾写成0 24;SAS 会把这一整行当成数据内容结果要么变量数对不上报错要么最后一条记录被吞掉。分号必须独占一行。带字符和日期变量的写法稍微复杂一点长度要提前声明data staff; length name $12; /* 字符变量必须先声明长度单位是字节 */ input id name $ age hiredate yymmdd10.; format hiredate yymmdd10.; /* 只决定显示样式不改变存储值 */ datalines; 1001 empA 30 2019-07-01 1002 empB 26 2021-03-15 ; run;length name $12;这一行必须写在input之前而且同一个 data 步里只有第一次生效。UTF-8 编码下一个汉字占 3 字节$12大概能放 4 个汉字。这个长度如果给少了中文会被硬生生截断成半个字日志里只给一条不痛不痒的 WARNING。2.3 Work 与 libname临时库和永久库的分界线数据集建完之后放在哪是新手第二个高频困惑。打开资源管理器能看到几个逻辑库文件夹其中Work是 SAS 留给用户的那一个。所有不带库名前缀的数据集都默认落在Work里。问题在于Work是内存里的临时库关掉 SAS 就清空。想留下来就得用libname把一个物理目录注册成逻辑库然后用库名.数据集名的两级名字引用。/* 把物理目录注册成逻辑库库名自己起最长 8 个字符 */ libname mylib D:\sasdata; /* 把刚才的临时数据集落盘set 负责读、data 负责写 */ data mylib.first; set work.first; run; /* 核对该库下有哪些成员quit 结束过程步 */ proc datasets libmylib; run; quit; /* 用完释放避免目录被句柄占着 */ libname mylib clear;逻辑说明libname建立的是库名到物理路径的映射本身不复制任何文件。data mylib.first; set work.first;是最典型的落盘写法读一侧写一侧。proc datasets相当于目录列表比去文件夹里翻更可靠。参数说明库名不超过 8 个字符只能是字母数字下划线路径必须是已经存在的目录SAS 不会替你创建路径里有中文或空格时要整体加双引号。libname只在当前会话有效重启 SAS 后必须重新执行一次很多人第二天回来发现数据集找不到原因就在这里。外部数据导入走proc import不用手写 inputproc import datafileD:\sasdata\sales_2023.xlsx outmylib.sales dbmsxlsx /* 按文件类型选xlsx / xls / csv / access */ replace; /* 目标数据集已存在时覆盖 */ getnamesyes; /* 首行作为变量名 */ rangeSheet1$A1:G5000; /* 限定有效区域避免整表扫描拖慢速度 */ run;dbms选错会直接报文件格式不匹配range不写的话 SAS 会自己猜区域遇到合并单元格或者表头下面有空行就容易读串。3. 输入输出格式与函数让读进来的值真的算得对数据集能建起来只是第一步。原始数据里日期是2023-05-12、金额带千分位逗号、姓名前后有空格这些东西不处理后面所有计算都是错的。SAS 把这件事拆成两个独立概念informat 管怎么读进来format 管怎么显示出来。3.1 informat 与 format把存什么和显示什么拆开这是 SAS 里最容易混淆的一对。informat 是输入格式作用在input读数据的瞬间决定字符串怎么被解析成数值或日期format 是输出格式作用在结果显示和导出的时候完全不改变存储值。一个日期变量在 SAS 内部永远是一个整数yymmdd10.只是让它在屏幕上长成2023-05-12的样子。数据类型常见 informat常见 format说明数值8.28.2总宽 8 位小数点后 2 位宽度不够时自动转科学计数字符$30.$30.单位是字节UTF-8 下一个汉字 3 字节日期yymmdd10.yymmdd10.2023-05-12 这种写法其他还有 date9.、mmddyy10.日期时间ymdttm.datetime20.带时分秒存储值是从 1960-01-01 00:00 起的秒数format在结果导出时特别好用比如把金额统一成两位小数、把日期统一成 ISO 写法都是在data步或者proc步里加一句format就能覆盖全表。写自定义格式用proc format把一列数字映射成业务标签这在出报表时很省事/* 把 1/2/3 映射成业务上的渠道名称 */ proc format; value chfmt 1 线上 2 门店 3 分销 other 未知; run; /* 应用这个格式只影响显示 */ proc freq datamylib.sales; tables channel; format channel chfmt.; run;逻辑说明value定义的是值域到标签的映射other兜住所有没列出来的取值不加这一行未匹配的值会原样显示。format语句应用在过程步里不改变底层数据所以同一个变量在不同报表里可以套不同格式。参数说明格式名不能超过 32 个字符引用时必须带点号chfmt.漏掉点号会被当成变量名。格式定义只在当前会话有效要跨会话使用得加library选项把格式存进某个逻辑库。3.2 冒号与 修饰符字符里带空格怎么办靠空格分隔字段的读法遇到北京市 朝阳区这种一个字段里本身带空格的数据就会散架。SAS 给了两个修饰符处理它。冒号:的作用是按格式宽度读但遇到分隔符就提前停也就是松绑定。它能避免字符变量把后面的空格一起吞进去也能让input name $20.这种写法在遇到短值时自动收手。data addr; infile datalines truncover; /* : 表示按分隔符截断 表示该字符值内部允许出现一个空格 */ input id :$8. city :$20. addr :$40.; datalines; 1 beijing chaoyang 建国路 88 号 2 shanghai pudong 世纪大道 1 号 ; run;逻辑说明id :$8.表示最多读 8 字节的字符遇到空格就停addr :$40.中的让 SAS 一直读到连续两个空格或者行尾中间单个空格被当作字段内容保留。infile datalines truncover;里的truncover是防止最后一行长度不足时被判定为截断。参数说明只能用在字符变量上且必须和informat一起写如果字段里可能出现多个连续空格会提前结束这种情况得改用dlm|之类指定分隔符的读法把空格从字段里彻底解放出来。3.3 三类函数的分工与 round 的分组用法书里反复强调的一个观点函数本身看 SAS Help 就够值钱的是它背后的使用思路。按用途分日常最常打交道的是这么几类。数值类round、int、ceil、floor、abs、sum、mean、nmiss。nmiss特别好用一行就能数出一条记录里有几个缺失值。 字符类substr、scan、strip、compress、index、upcase、tranwrd。scan按分隔符取第 n 段比substr更抗变长。 日期类year、month、day、mdy、intck、datdif。intck算两个日期之间隔了几个单位写同比环比指标离不开。 类型转换put把数值转字符input把字符转数值这一对是拼接和解析的主力。 概率分布probnorm、probt、cinv做假设检验或者手算置信区间时用得上。 时序dif求相邻两条记录的差值lag取上一条记录的值两者都强依赖排序。data func_demo; set mylib.sales; /* 日期拆解从完整日期里取年、月 */ y year(order_date); m month(order_date); /* round 的第二参数是倍数这里把金额归到最近的 50 元档 */ band round(amount, 50); /* 字符清洗strip 去首尾空格length 统计清洗后长度 */ nlen length(strip(cust_name)); /* 数值转字符再拼接put 之后一定要 strip否则带前导空格 */ lbl A || strip(put(amount, best12.)); /* 一条记录里数一下有几个缺失 */ miss nmiss(amount, qty); /* 相邻记录差分前提是已经按目标变量排过序 */ d_amt dif(amount); run;逻辑说明round(amount, 50)是最值钱的一行。多数人只知道round(x)等于四舍五入到整数加上第二个参数之后它就变成一个分箱器把连续金额压缩成有限的几个档位接着proc freq就能直接出分布表比手写if-then分段干净得多。/* 归档之后直接看分布不用再写一长串 if-then */ proc freq datafunc_demo; tables band / nocum; run;参数说明best12.是 SAS 自动挑宽度的数值格式适合拼接场景put出来的字符串默认带前导空格用||拼接时两边长度不一致会报 WARNING 并可能截断所以必须外套一层strip。dif和lag不会管你有没有排序排错了就是一堆错位数据这类错误在日志里完全静默属于典型的翻车点。4. 数据清洗三件事重复值、缺失值、异常值各一套程序数据整理是这份教程占篇幅最大的一块也是实际工作里最耗时间的环节。三段清洗各有固定的套路把套路记熟之后换一份新数据也只是改几个变量名。4.1 set 与 merge纵向拼接和横向拼接的按键set是纵向堆叠前提是两份数据的变量结构一致行数相加。merge是横向拼接按某个键把两边的列并到一起等价于 SQL 里的 join。/* 纵向三个季度的数据堆成一张表用 in 标记来源 */ data all_q; set q1(inin1) q2(inin2) q3(inin3); if in1 then quarter Q1; else if in2 then quarter Q2; else quarter Q3; run; /* 横向先各自按 by 变量排序这一步不能省 */ proc sort datamylib.orders; by cust_id; run; proc sort datamylib.cust; by cust_id; run; data ord_cust; merge mylib.orders(ino) mylib.cust(inc); by cust_id; if o; /* 只保留订单侧存在的记录相当于左连接 */ src o*1 c*10; /* 调试技巧1 表示只在订单侧10 表示只在客户侧 */ run;逻辑说明in生成的是一个只在 data 步内有效的临时布尔变量不写进输出数据集用来判断当前记录来自哪一边。if o;这一句就是左连接的写法if o and c;是内连接if o or c;是全外连接。src o*1 c*10是我常用的排查手法跑完之后proc freq一下src就能看出有没有哪一侧的键对不上。参数说明merge之前两侧必须都按by变量升序排过序否则日志会报BY variables are not properly sorted而且结果顺序是错的。merge不做去重如果某一侧的 by 变量有重复值SAS 会做笛卡尔展开行数暴涨这是后面要单独讲的一个坑。4.2 first. 与 last.排序之后才有的自动变量去重最稳的写法不是nodupkey而是用by组内的first.和last.自动变量。前者标记组内第一条后者标记组内最后一条取值是 0/1。/* 先按客户 下单时间排序保证每个客户内部按时间有序 */ proc sort datamylib.orders outwork.ord_sorted; by cust_id order_date; run; /* 每个客户只留时间最晚的那一条 */ data work.last_order; set work.ord_sorted; by cust_id; if last.cust_id; run;逻辑说明by cust_id;声明分组SAS 会在内部维护当前组的边界于是last.cust_id在每条记录上都被赋值为是不是本组最后一条。if last.cust_id;是一个子集化语句只保留条件为真的记录。想留最早的一条就换成first.cust_id。参数说明first.和last.依赖排序没有排序直接写会报错。当by有多个变量时比如by cust_id order_date;last.cust_id表示这个客户最后一条而last.order_date表示同一客户同一时间里的最后一条两者含义完全不同选错了结果会安静地偏离预期。如果只是想粗暴去重、不在乎保留哪一条proc sort加nodupkey更快/* nodupkey 会把 by 变量完全相同的记录只留一条不保证留的是哪条 */ proc sort datamylib.orders outwork.uniq nodupkey; by cust_id order_date; run;4.3 数组 numeric/character一个查缺失值的万能程序字段少的时候逐个写if amount . then ...还行字段上百个就只能靠数组。SAS 有两个系统自动维护的特殊变量列表_numeric_代表当前数据集里所有数值变量_character_代表所有字符变量。把它们丢进数组循环一遍就能扫全表。data work.miss_scan; set mylib.sales; array num[*] _numeric_; /* 自动展开为所有数值变量 */ array chr[*] _character_; /* 自动展开为所有字符变量 */ miss_n 0; do i 1 to dim(num); if missing(num[i]) then miss_n 1; end; do j 1 to dim(chr); if missing(chr[j]) then miss_n 1; end; keep order_id miss_n; drop i j; run; /* 看看有多少条记录至少缺一个字段 */ proc freq datawork.miss_scan; tables miss_n / nocum; run;逻辑说明array num[*] _numeric_;声明了一个不指定长度的数组[*]让 SAS 自己去数元素个数dim(num)返回这个个数所以不管数据集有多少列这段代码都不用改。missing()函数对数值和字符都适用数值缺失是.字符缺失是空串一个函数统一处理。参数说明_numeric_会把计数变量miss_n自己也纳进来所以要先给它赋 0 再进循环否则第一轮循环拿到的初值是缺失计数会多算一次。i和j是循环索引最后必须drop掉不然会变成数据集里的两列垃圾数据。这套写法在字段几十上百个的表上收益最大字段只有三五个时反而不如手写if。4.4 异常值的 1.5 倍 IQR 判定与手算分位数异常值判定没有绝对标准工程上最常用的还是箱线图那套 1.5 倍四分位距规则。思路是先算出 Q1 和 Q3落在[Q1 - 1.5*IQR, Q3 1.5*IQR]之外的记为疑似异常。/* 先只算分位数noprint 抑制屏幕输出 */ proc means datamylib.orders noprint; var amount; output outwork.q q1q1 q3q3; run; /* 把单行的分位数结果并到明细表上逐条判定 */ data work.outlier; if _n_ 1 then set work.q; /* 只读第一条把 q1/q3 带进 PDV */ set mylib.orders; iqr q3 - q1; if amount q1 - 1.5*iqr or amount q3 1.5*iqr; run;逻辑说明proc means的output out会把统计量写成一行数据集q1q1前半是统计量关键字后半是输出变量名。if _n_ 1 then set work.q;是 SAS 里做单值广播的标准写法只在第一条记录时读取那个单行数据集之后q1、q3会一直保留在 PDV 里供所有记录使用。参数说明1.5这个系数可以调业务上对异常更敏感就调到 1.0更宽松就调到 3.0。不建议用mean ± 3*std因为均值和标准差本身就被极端值污染了一份有离群点的数据算出来的阈值会失真。顺带记一下if和where的区别这在生成子集时经常被搞混data sub1; set mylib.orders; where amount 1000; /* 读入前过滤参与读的只有满足条件的行更快 */ run; data sub2; set mylib.orders; if amount 1000; /* 读入后过滤全部行先进入 PDV 再判断 */ run;where在set阶段就生效效率更高还能直接用在proc步里if是在数据步里逐条判断好处是可以用first.、last.这类自动变量做条件。5. 五个常见问题排查从日志红字到 merge 翻车下面这五条基本覆盖了新手前两周会撞上的墙每一条都按现象—原因—解决走一遍。5.1 现象日志说有 0 个观测输出一张空表现象很典型proc print出来的是一张只有表头的空表日志里一行NOTE: 数据集 WORK.FIRST 有 0 个观测。原因基本都在数据块边界上。第一种是把datalines;之后的第一个数据行写在了同一行SAS 把它当成了语句的一部分。第二种是数据块里出现了空行SAS 认为数据到此结束后面的内容全部被忽略。第三种是最后那个结束分号跟数据写在了同一行把最后一条记录吞掉。解决方法是把datalines;单独占一行数据从下一行开始结束分号独占一行且顶格。改完再看日志正确的提示应该是NOTE: 数据集 WORK.FIRST 有 2 个观测。养成一个习惯每次建完数据集先看一眼日志里的观测数和自己数的对不上就说明读错了不要等到后面算平均值的时候才发现少了一半数据。5.2 现象中文被截断或者字符值后面跟着一堆空格现象是姓名列显示成半截字或者拼出来的字符串总是带一长串尾随空格。原因有两个。一是字符变量的长度由length决定没声明length时 SAS 会按第一次遇到的值自动定长后续更长的值会被静默截断。UTF-8 下每个汉字占 3 字节$8最多放两个半汉字第三个字必然被切。二是input用了$但没配长度SAS 默认取 8 字节。解决方法是在 data 步开头显式写length name $30;或者在input里用name :$30.松绑定读法。已经建好的数据集要改长度得用proc sql的alter table或者重建一次。另外拼接字符串时记得给put的结果套一层strip否则长度不匹配会触发WARNING: 变量长度不一致日志不显眼但结果里全是空格。5.3 现象merge 之后行数暴涨金额合计翻倍现象是明明左表只有 5 万条merge 完变成 12 万条一算总金额比原来多了一倍多。原因就是 by 变量在某一侧有重复值SAS 对重复键做的是笛卡尔展开左表某键出现 3 次、右表同键出现 4 次结果就是 12 行。这类错误不会报 ERROR日志里只有一行轻描淡写的NOTE: 数据集 WORK.ORD_CUST 有 120000 个观测很容易滑过去。解决分三步。merge 之前先对两侧分别跑一遍proc freq或者proc sort nodupkey确认 by 变量在预期侧是唯一的merge 时带上in变量跑完立刻proc freq一下来源标记看有没有意外的组合最后把 merge 前后的记录数和关键金额合计对一遍这是最省事的兜底检查。/* merge 前后各算一次合计数字对不上就说明键有重复 */ proc means datamylib.orders sum; var amount; run; proc means datawork.ord_cust sum; var amount; run;5.4 现象日期算出来的年份是 1960现象是用year()取年份结果全是 1960或者intck算出来的天数是个离谱的负数。原因是 SAS 的日期本质是一个从 1960-01-01 开始的天数计数字符2023-05-12如果不经过yymmdd10.解析被强行转成数值时会得到缺失值或 0而 0 对应的就是 1960-01-01。把format当成转换工具也是同一个误区——format只改显示不改存储一个字符变量加一百遍format也变不成日期。解决办法是在读入的时候就配好 informatinput d yymmdd10.;或者在数据步里显式转换d input(2023-05-12, yymmdd10.);。反过来要把日期输出成指定样式用put(d, yymmdd10.)。判断一个变量到底是字符还是日期最直接的方法是proc contents看它的类型和格式比盯着输出窗口猜靠谱。5.5 现象libname 报错或者数据集物理文件不存在现象是代码看着没问题但一跑就报ERROR: Libref MYLIB is not assigned或者ERROR: 物理文件不存在。原因有几种按出现频率排物理目录根本没建SAS 不会替你创建库名超过 8 个字符被截断后指向了另一个库路径写的是相对路径而 SAS 的当前目录和你想的不一样上一次会话的libname早就在 SAS 重启时失效了。解决方法是先用libname mylib D:\sasdata;确认目录真实存在再用下面这句回显一下 SAS 眼里这个库指向哪里/* 回显逻辑库实际指向的物理路径排查路径问题最直接的一招 */ %put %sysfunc(pathname(mylib));如果输出路径是对的还报错就去查权限和文件占用如果是重启后失效就把libname语句放到程序最开头每次运行都带上。多人协作时还会遇到别人正开着同一个数据集的情况SAS 会锁文件日志提示文件被占用只能等或者另存一份。6. 出图与出表sgplot 到三线表的交稿验收数据清洗完最后一步是把结果变成能交出去的东西。书里对作图和制表给的篇幅很重实际工作里也确实是这样——分析做得再细图和表不好看汇报效果会打很大折扣。先说作图。proc sgplot是 9.3 之后的主力比老的proc gplot好写太多一张图基本就是几个语句叠加。proc sgplot datamylib.orders; vbar category / responseamount statsum datalabel; /* 柱状图柱顶标数值 */ vline month / responseamount statsum y2axis; /* 折线走次坐标轴 */ xaxis label品类; yaxis label销售额; run;vbar的statsum表示先按category汇总再画不写的话默认画频数y2axis是双坐标轴的关键柱子和折线的量纲差一个数量级时必用否则折线会被压成一条直线贴在底部。出图之前一定要先proc sort按 x 轴变量排好序vline不排序会画出诡异的折返线这是最常见的翻车点之一。制表用proc tabulate三维列联表和三线表都能一次成型。ods rtf fileD:\sasdata\report.rtf stylejournal; /* journal 样式自带三线表外观 */ proc tabulate datamylib.orders; class region channel; var amount; table region区域, (channel渠道 all合计)*(nN mean均值 sum总额); run; ods rtf close; /* 必须成对关闭否则文件锁死 */table语句里逗号前是行维、逗号后是列维all合计自动生成汇总行n、mean、sum后面跟的是表头显示名。ods rtf close;漏写的后果是文件一直被占用下次导出直接报错。交稿前我会固定跑一遍下面这张清单任何一项过不了就不发出去。检查项合格标准失败时先看什么记录数与源表核对一致过滤条件有记录日志里的观测数 NOTE关键指标merge 前后合计金额一致by 变量是否有重复值日期年份落在业务区间内无 1960变量类型是否为字符缺失关键字段缺失率在可接受范围miss_n的频数分布图形x 轴有序双轴量纲不打架是否漏了proc sort表格表头对齐合计行落在最后ods是否成对关闭这套流程我踩过最疼的一次是一份月度报表里 merge 把订单数放大了三倍图和表都好看数字全是错的交上去两天后才被业务侧发现。从那以后我每次出报表前都强制走一遍记录数和金额的双向核对宁可多花五分钟也不吃这个后悔药。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?