
简介《小白学SAS基础教程》是一份面向零基础读者与初学者的SAS入门PDF资料旨在用清晰路径降低SAS功能庞杂、命令难以上手的门槛适合需要掌握数据整理、分析与报表呈现基本技能的学习者。压缩包内共1个PDF文件整体约1.76MB单文件便于在电脑或平板上阅读与检索。目前已有709人学习说明其在SAS入门群体中具有一定参考价值。内容以对话体展开借主人公“小白”的提问串联建立数据集的基础与高级议题、常用函数思路、缺失值异常值与重复值等数据清洗方法并讲解SAS绘图和规范制表的实现方式附录还补充了新手常见错误提示、高效与低效写法比较等实用内容侧重帮助读者理解操作背后的实际用途而非简单堆砌命令适合边读边练、逐步建立分析思路。1. 从一张知识地图开始SAS 基础入门该按什么顺序走SAS 基础入门最容易走岔的地方是把学习顺序交给软件菜单先装软件、再背命令、最后才碰数据。真正能跑通的顺序反着来——数据先进得来变量再造得出脏数据洗得净最后才是图和表。这四层就是整份笔记的知识地图第一层是数据步包括 data、input、cards 三件套和数值、字符、日期三类输入输出格式第二层是新变量与函数五条造变量的路加六族常用函数第三层是数据清洗合并、对比、去重、缺失、异常、取子集六件事第四层是呈现菜单绘图、proc sgplot、proc template、proc tabulate、proc report 加 ODS 输出。前三层是地基第四层是门面地基不牢图画得再花也站不住。这份地图适合两类人零基础但工作里天天被 SAS 数据集卡住的人以及学过一阵、代码能跑却说不清为什么这么写的人。2. 数据步的骨架data、input、cards 与三类输入格式数据步是整个 SAS 体系里最不该跳过的部分。原因很实在后面所有过程步都建立在数据集之上数据集建错了统计、绘图、制表全是错上加错。这一章把数据步拆成三层——先把骨架立起来再处理输入格式最后处理输出格式。2.1 三件套与命名规则data、input、cards 各自的职责一个最小的数据步就是三句话data 起名字input 列变量cards 宣布“下面开始是数据”。三者分工明确缺一句程序就跑不起来。看一段能直接提交的代码data first; /* data 语句给数据集起名规则字母开头只用字母数字下划线≤32字符 */ input gender age; /* input 语句列出变量默认以空格作为变量间的分隔符 */ cards; /* cards 宣布数据行开始旧版本常用新版等价写法是 datalines */ 1 30 0 24 ; /* 数据结束的分号必须另起一行 */ run;这段代码里有三个高频翻车点。第一数据集名和变量名不能用中文、空格、特殊字符第一个字符不能是数字长度别超过 32 个字符不确定哪些字符算特殊字符时老老实实只用英文字母最保险。第二每一句 SAS 语句后面必须跟英文分号分号是语句结束的标志少一个整段程序都读不下去。第三数据全部写完之后的分号必须单独占一行如果偷懒跟在最后一个数据后面SAS 会把它当成数据的一部分读进去报错信息还未必指向真正的问题。input 后面写多少变量由数据决定不必担心数量上万个变量 SAS 一样处理。变量之间的分隔默认是空格所以数据行里多个值之间用空格隔开即可。至于 cards 和 datalines 选哪个功能完全等价纯属个人习惯写成哪个都不影响结果。数据集建好之后去了哪里是新手第二个懵的点。SAS 左侧的资源管理器里有一排逻辑库默认建立的数据集落在 Work 库。Work 是 SAS 留给用户放数据的地方其余几个库各有用途Sashelp 放帮助与示例数据Sasuser 放练习数据Maps 系列放地图相关数据。Work 库的特性是关闭 SAS 就自动清空所以里面的数据集叫临时数据集。想让数据留下来得先 libname 指定一个永久库再往里写。2.2 输入格式数值、字符、日期以及 : 和 两个修饰符不加任何修饰时SAS 按空格切分再自行判断类型简单但脆弱。一旦遇到“字符里带空格”“日期写成 2024-03-15”“编号必须保留前导零”这类情况就必须显式指定输入格式也就是 informat。类型常见 informat作用典型场景数值型默认、8.、comma9.读入数字可处理千分位金额、计数字符型$8.、$char20.固定宽度读字符保留前导空格姓名、编码日期型yymmdd10.、date9.、mmddyy10.把文本日期读成 SAS 日期值记录日期、出生日期data demo; input id $ name $12. birth yymmdd10. salary comma9.; format birth yymmdd10.; /* 输出时按 2024-03-15 显示 */ cards; 001 张 三 1990-05-12 12,000 002 李 四 1988-11-03 9,800 ; run;日期型是重点也是难点。SAS 内部把日期存成一个整数起点是 1960 年 1 月 1 日存的是“距离那天多少天”。所以日期变量可以直接做加减法、可以比较大小但如果你不做 format输出窗口里看到的是一串莫名其妙的数字很多人第一次见到会以为数据读错了。两个修饰符值得单独记。冒号:用在列表输入里表示“读到分隔符为止”最典型的用法是input name :$20.;遇到空格就停不必写死宽度。允许一个字符值内部包含单个空格适合“张 三”这种把姓和名分开写的场景。这两个符号不常用但一旦碰上“字符被截断”“一个变量被拆成两个”的怪现象先回头看是不是漏了它们。还有一个行保持符默认情况下一行数据读完就进入下一个观测加上可以让 SAS 在同一行里连续读多个观测适合数据被紧凑写在一行里的情况。它和冒号一样属于“平时不用、用上一次省半天”的工具。2.3 输出格式与自定义格式format、informat、picture 的分工informat 管“怎么读进来”format 管“怎么显示出去”两个词只差两个字母作用方向完全相反这是初学者第一周最容易搞混的一对概念。data demo2; set demo; format salary dollar12.2; /* 输出显示为 $12,000.00但存储值仍是 12000 */ run; proc format; /* 自定义格式把 1/0 翻译成中文标签 */ value genderfmt 1男 0女; value agefmt low-30青年 30-high中年; run; data demo3; set demo2; format gender genderfmt.; /* 显示为“男”“女”底层仍是 1 和 0 */ run;自定义格式的写法是value 格式名 值标签区间写法支持low-30、30-high这种开闭区间组合做分组统计时非常顺手。除了 value还能用 picture 定义一个“输出模板”专门处理“在数字中间插字符”的需求比如把 202403 显示成 2024 年 03 月或者给编号统一加前缀。这里有一个必须提前说清的边界format 只改显示不改存储。数据集里存的还是原始数值排序、比较、计算全部按原始值走。所以“我明明格式化成字符了为什么还能参与加减”这类困惑根源就在这里。真正改变类型要用 put 和 input 函数那属于下一章的内容。3. 新变量与函数五条造变量的路、六族常用函数数据步能建数据集只是及格线真正的分水岭是“能不能在数据步里把需要的东西造出来”。原始数据往往只有最朴素的几列分析要用的分组变量、累计值、标记位、转换后的类型全得自己生成。3.1 五条产生新变量的路径表达式、if-then、retain、do 循环、length第一条最直接用表达式或函数当场算出来bmi weight/(height/100)**2;一行搞定。第二条用 if-then 做条件赋值适合“按阈值打标签”。第三条是 retain 配合累加语句处理跨行累积。proc sort datademo; by id; run; data cum; set demo; by id; retain total 0; /* retain 让变量跨行保留否则每次迭代会被重置为缺失 */ total total salary; /* 累加语句效果等同于 retain 加法 */ if first.id then total salary; /* 每个 id 的第一行重新起算 */ run;retain 的关键在于理解 data 步的执行方式SAS 逐行读取每一行走一遍 data 步默认在进入新一行时把非数据集来源的变量重置为缺失。retain 就是告诉 SAS“这个变量别清留着”。忘了写 retain累加结果会变成一行一个值看起来像“只算了最后一行”实际是每行都从头开始。累加语句total salary;是个语法糖它默认自带 retain写成这样就不必再单独写 retain只不过初始值按 0 处理遇到缺失值会自动跳过。第四条 do 循环适合在单个观测内部做多次迭代比如为一行生成多个衍生指标或者配合数组批量处理。第五条 length 语句用来显式指定新变量的类型和长度。data typed; set demo; length level $8; /* 字符变量显式声明长度避免被第一次赋值截断 */ if salary 10000 then level 高; else level 低; run;length 放在 set 之后、赋值之前位置不能乱。如果不声明SAS 会按第一次出现的值决定长度后面再赋值更长的字符串就会被无声截断——这个错误不报只是结果不对属于典型的“黑匣子”式踩坑。3.2 六族函数速查与各自的坑常用函数按用途可以归成六族每一族挑几个代表记比背完整手册划算得多。族代表函数典型用途容易忽略的点数值计算round、ceil、floor、abs、max四舍五入、取整、极值round 的第二参数可指定舍入单位字符处理substr、scan、index、upcase、strip截取、分词、查找、大小写、去空格substr 越界不报错返回空串日期时间mdy、yrdif、intck、intnx、today构造日期、算年龄、算间隔、日期推进日期常量要加引号再跟 d类型转换put、input数值转字符、字符转数值put 后接格式input 后接 informat概率分布probnorm、probt、quantile算 p 值、分位数单双尾方向容易写反特殊行为dif、lag与上一行做差、取上一行值条件分支里使用会错位dif 和 lag 是这一族里最容易被误用的两个。很多人以为 lag 是“取数据集里的上一行”其实它是“取上一次执行到这条语句时的值”本质是一个队列。如果 lag 写在 if 分支内部某些行不进入分支队列就不推进取到的可能是好几行之前的值。data lagtest; set demo; prev lag(salary); /* 正确在 data 步顶层无条件执行 */ if salary 5000 then flag 1; else flag 0; run;正确写法是把 lag 放在 data 步顶层让它对每一行都执行一次需要条件判断时先用 lag 取值再用 if 决定要不要用这个值。理解了这一点lag 就从“玄学函数”变回普通工具。3.3 函数的用法之外round 分组与 count 词频的解题思路命令谁都能查帮助文档函数真正的价值在于“拿它解决什么问题”。两个典型例子可以说明这种思路差别。第一个是 round。多数人知道它能四舍五入但加上第二个参数它就成了分组工具grp round(age, 5);会把年龄按 5 岁一档归组20 到 24 全落在 2025 到 29 全落在 25。比一堆 if-then 区间判断简洁得多改档距只改一个数字。第二个是 count。默认它统计一个字符串里某个子串出现的次数但配合 scan 逐个取词就能干出“词频统计”这种事把一段评价文本按分隔符切成若干词对每个词 count 它在全文出现的次数再排序输出。命令本身是现成的难的是想到可以这么用。data wordfreq; text 好 很好 非常好 好; do i 1 to countw(text, ); /* countw 数一共几个词 */ w scan(text, i, ); /* scan 取第 i 个词 */ freq count(text, strip(w)); /* count 数该词在全文出现次数 */ output; end; drop i; run;这段代码跑完能直接看出“好”出现了三次。把它换成任意文本字段就是一套简易的文本频次统计流程。函数的用法一页 Help 就够函数的用法边界和组合方式才是需要反复练的部分。4. 数据清洗六件事合并、对比、去重、缺失、异常、取子集真实数据几乎没有一拿来就能用的。数据清洗是整条流程里最耗时间、也最考验基本功的一段可以拆成六件事先把分散的数据合到一起再做对比然后去重、查缺失、查异常最后填补和取子集。4.1 纵向 set 与横向 merge合并前必须先排序set 是纵向合并把结构相同的多个数据集一行行接起来merge 是横向合并按某个共同变量把两张表并排拼在一起。data all; set jan feb mar; /* 纵向三张结构相同的表接成一长表 */ run; proc sort dataall; by id; run; /* merge 之前两个数据集都必须按 by 变量排好序 */ proc sort datainfo; by id; run; data merged; merge all(ina) info(inb); /* in 生成临时指示变量标记该行来自哪张表 */ by id; if a and b; /* 只保留两边都有的行等价于内连接 */ run;merge 有两个硬性前提两边都按 by 变量升序排好且变量名一致。没排序直接 mergeSAS 会在日志里丢一句提示然后继续跑结果可能完全错位这是最典型的“不报错但全错”场景。in 选项是用来区分行来源的开关if a and b实现内连接if a保留左表全部行实现左连接写法不难但语义必须先想清楚。4.2 去重与 first./last.排序和分组变量是一对去重有两条路。简单场景直接用 proc sort 的 nodupkey按哪几个变量判重就在 by 里写哪几个需要保留“第几条”或者把重复的挑出来单独看时就要动用 first. 和 last.。proc sort dataraw nodupkey outclean; by id; run; /* 直接去重保留第一条 */ data dup; set raw; by id; if first.id and last.id then output; /* 首末是同一行 只出现一次 */ run; data onlydup; set raw; by id; if not (first.id and last.id); /* 反过来就是把重复行挑出来 */ run;first.id 和 last.id 是 SAS 自动生成的分组指示变量只有在 data 步里出现 by 语句时才可用且数据必须已经按该变量排好序。它们判断的是“当前行是不是这个 by 组的第一行/最后一行”原理简单但组合出的判断条件能覆盖绝大多数去重需求。最容易忽略的是没排序就用 first./last.SAS 直接报错停机排序了但 by 变量写错不报错结果全乱。4.3 缺失值与异常值数组、自动变量、if 与 where 的区别查缺失值的朴素写法是一列一列写 if变量一多就崩溃。真正的解法是数组配合自动变量。data checkmiss; set clean; array num[*] _numeric_; /* 自动变量把所有数值型变量收进数组 */ array chr[*] $ _character_; /* 自动变量把所有字符型变量收进数组 */ miss 0; do i 1 to dim(num); if num[i] . then miss 1; /* 数值缺失用 . 判断 */ end; do i 1 to dim(chr); if chr[i] then miss 1; /* 字符缺失用空串判断 */ end; drop i; run;_numeric_和_character_是 SAS 的自动变量分别代表数据集里全部数值变量和全部字符变量不用手写变量名。dim() 返回数组长度。这段代码换任何数据集都能直接跑是标准工具只要把判断条件从“计数”改成“输出行号”或“打标记”就能得到查缺失和查异常两套程序。异常值检查也是同一套路区别在判断条件可以用均值加减三倍标准差可以用上下四分位加 1.5 倍四分位距也可以直接用业务阈值。把阈值写成宏变量一套程序就能反复用。这里必须分清 if 和 where。where 在读取数据时就筛选被排除的行根本不进 data 步效率高但不能引用本步新生成的变量if 在读取之后、在行上判断可以引用新变量但所有行都得先读进来。data sub1; set clean; where salary 0; /* 读入时就筛掉非正数不能引用下面新造的变量 */ bonus salary * 0.1; if bonus 1000; /* 读入后判断可以引用 bonus */ run;混用的后果很隐蔽把该用 where 的地方写成 if大数据集上慢得莫名其妙把 where 用在新变量上直接报变量不存在。4.4 填补、更新与子集三种收尾动作查出来之后要处理。缺失值填补常见做法是用均值、中位数或上一个非缺失值填充用 retain 保留下一个可用值即可实现“向前填充”。数据更新用 update 语句它和 merge 的区别在于update 遇到缺失值时不会覆盖原值适合“用新表修补旧表”的场景。data filled; set clean; retain lastgood; if not missing(score) then lastgood score; /* 记住最近一个非缺失值 */ if missing(score) then score lastgood; /* 用最近值填补 */ drop lastgood; run; data final; update master newdata; /* 新表里的缺失值不会覆盖主表已有值 */ by id; run;取子集分两种按行取用 where 或 if 加 output按列取用 keep 和 drop。keep 和 drop 既可以写在 data 语句后作为数据集选项也可以写在 data 步里作为语句前者作用在读取阶段效率更高。data subset; set clean(keepid salary age where(age 18)); /* 数据集选项读的时候就筛列筛行 */ run;数据集选项的好处是把筛选前置读进来的就是想要的那部分省内存也省时间。养成这个习惯之后处理大表会顺畅很多。5. 易错点五处最容易翻车的地方SAS 有个特点很多错误不报错只是结果不对。等到出报告时才发现数字对不上回头排查的成本远比当初多写一行高。下面五处是从数据步到输出环节最容易漏的每一条都按“错在哪、为什么容易漏、怎么补”的顺序说。5.1 语法与库分号位置、Work 临时库第一处数据结束的分号跟数据写在同一行。这是新手第一天就会犯的错最后一条数据写完之后顺手打个分号看起来干净利落实际 SAS 会把这个分号当成数据的一部分读进去。为什么容易漏报错信息往往指向后面的语句说变量类型不对或者观测数不符视线被引到别处去了。补法是养成习惯——数据行写完回车分号单独占一行再写 run。可以在提交前扫一眼 cards 之后的区域最后一行只有分号就对了。第二处在 Work 库里攒了一下午的数据集关掉 SAS 全没了。Work 是临时库关闭即清空这是设计如此不是故障。为什么容易漏程序能跑、结果能看一切正常直到第二天打开发现数据集不见了才意识到问题。补法是在做任何需要跨天的工作之前先用 libname 指向一个本地目录再data 库名.数据集名;写入。代价是多写几个字符收益是不用重跑。5.2 逻辑与类型format 不改存储、lag 错位、if 与 where第三处以为 format 把变量变成了另一种类型。给数值加了 dollar 格式显示确实带美元符号了但排序还是按数值、加减还是能算因为底层根本没变。为什么容易漏显示结果看起来“已经是字符了”视觉上没有任何提示。补法是记住一句format 只管显示层改类型必须用 put 和 input。写代码时如果下一步要拼接字符串先显式 put 一次别指望格式。data conv; set clean; id_char put(id, 8.); /* 数值转字符put 格式 */ score_num input(score_char, 8.); /* 字符转数值input informat */ run;put 后面跟的是输出格式input 后面跟的是输入格式两个方向别搞反。反了不一定报错可能得到一堆缺失值。第四处在 if 分支里用 lag。前面已经说过原理这里补一句后果错位之后取值不会报错只是取到了不知哪一行的值往往是整列数据集体偏移极难从结果反推原因。补法只有一条lag 和 dif 一律放在 data 步顶层先取值再判断。第五处if 和 where 随手混用。where 在读取阶段生效速度快但不能引用新变量if 在读入之后生效灵活但全表都要读进来。为什么容易漏小数据集上两者结果一模一样等到数据量上来才发现慢得离谱或者报“变量不存在”却找不到变量在哪。补法是先问自己一句这个条件用的变量是本步新造的吗是就用 if不是就优先 where。养成这个判断顺序两类问题都能避开。6. 复习清单五个模块串成一条能跑通的路线复习这件事光看笔记没用得有一条能自己跑通的路线。下面这张清单按章节顺序排每一行都给了“必须能独立写出来”的东西和“怎么判断自己会了”的标准。能不看笔记把代码敲出来才算过能跑出结果但说不清每一行的作用等于没过。模块必须能独立写出的代码自测标准数据步骨架data input cards 建一个数据集并定义变量不查笔记能写出命名合法的数据集名输入输出格式带 informat 的 input、带 format 的 data 步说清 format 和 informat 的方向差异新变量retain 累加、if-then 打标签、length 声明长度解释不写 retain 时累加为什么会错函数用 round 分组、用 scan count 做词频说出 lag 为什么不能在 if 分支里用合并merge 前先 sort用 in 控制连接类型能写出内连接和左连接两种写法去重proc sort nodupkey 与 first./last. 两套写法说清 first. 依赖排序和 by 语句缺失与异常数组配numeric遍历全表换一个数据集代码照样能跑制表与输出proc tabulate 出三线表、ODS 输出到文件输出能直接用不用手工再排版清单里最后一行值得单独说一句。制表这块proc tabulate 能做出规范的三线表proc report 适合按条件列明细和分组统计最后统一用 ODS 把结果直接落到 Word、Excel 或 PDF省掉手工复制粘贴那一步。ods rtf filereport.rtf stylejournal; /* 打开输出目标指定样式 */ proc tabulate datafinal; class gender agegrp; var salary; table gender*agegrp, salary*(mean std); /* 行维度、列维度用逗号隔开 */ run; ods rtf close; /* 一定要关否则文件会被占用 */这段里最容易忘的是最后那行 close。忘了关文件会一直被占用下次覆盖写不进去报的错还跟制表本身毫无关系。我自己的血泪经验是第一次系统过 SAS 的时候图省事把中间数据集全留在 Work 库代码也没存第二天开电脑一切归零只能凭记忆重敲。从那以后我每次开工前都强制走一遍“先 libname、再存代码”的动作跑通一段存一段。这份检查清单和配套的示例程序、讲解视频一起打包在下载资源里对着敲一遍比只读要快得多。希望帮到你。本文还有配套的精品资源点击获取