ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

R语言基础入门:向量、循环法则与NA/NaN处理

R语言基础入门:向量、循环法则与NA/NaN处理 搞R语言也有几年了每次带新人入门我都会让他们先把向量、循环法则、NA和NaN这几个概念弄明白再往后学才是正路。别小看这些基础多少人在实际处理数据时被循环法则静默回收坑得怀疑人生被NA和NaN的组合判断搞得头皮发麻。这篇我把R语言基础系列的第一部分摊开揉碎讲清楚包含R的基本概念、向量的创建与索引、循环法则的机制与陷阱、NaN和NA的区别与处理以及内置函数dnorm的使用思路。适合刚接触R的初学者也适合用过一阵子但对底层规则一知半解的朋友查漏补缺。1. R语言的核心设计思想与基本概念1.1 R语言到底是个什么东西R本质上是一门为统计计算和数据分析而生的编程语言它的底层实现了大量向量化运算的机制也就是说当你对一个向量做运算时R会在底层高效地批量处理每个元素而不是像很多通用语言那样要你手动写循环一个个来。这也是为什么R在数据清洗、统计建模、可视化领域这么流行。我第一次接触R的时候印象最深的就是它那种交互式的工作方式。打开RStudio在控制台敲一行代码回车就出结果不用编译、不用写完整程序这对数据分析场景来说极其友好。你可以一边探索数据一边写代码逐步把散落的代码整理成脚本甚至R包。从数据结构上看R有几种基础类型向量vector、矩阵matrix、数组array、数据框data.frame和列表list。表面上看类型很多实际上矩阵和数组本质上都是向量的扩展数据框则是由多个等长向量按列拼起来的特殊列表。所以要学好R首要任务就是把向量这个概念彻底吃透因为它是其他一切数据结构的基石。1.2 R的赋值、对象与工作环境R里面最常见的赋值符号是-等号也能用但在某些场景下语义略有区别。日常写脚本推荐统一用-这是R社区的主流约定在函数参数里才用做命名传参。比如x - c(1, 2, 3, 4, 5)这行代码把右侧创建的向量赋给了左边的对象x。在R的世界里万物皆对象向量、函数、数据框、模型结果都作为对象存在于工作环境中。你可以用ls()查看当前环境里有哪些对象用rm(x)删除某个对象。工作环境这个概念初学时不觉得重要等到你跑复杂分析、加载多个R包时就会明白命名空间和环境的管理到底有多关键。一个简单的建议给对象起名字要有意义避免用data、df这种覆盖性太强的名字否则后面很容易出现对象被意外覆盖的惨案。2. 向量R语言的第一公民2.1 向量的创建方法与类型创建向量最常用的方法是c()函数c是combine的缩写作用是组合多个元素成为向量v1 - c(1, 2, 3, 4, 5) # 数值型向量 v2 - c(a, b, c) # 字符型向量 v3 - c(TRUE, FALSE, TRUE) # 逻辑型向量除了c()还有两种特别常用的生成方式冒号运算符和seq()函数。1:10会生成从1到10的整数序列seq(1, 10, by 2)会生成1、3、5、7、9这样的等差数列。rep()函数则用于重复元素rep(c(A, B), times 2)得到A、B、A、Brep(c(A, B), each 2)得到A、A、B、B。这两个参数的区别是初学者的高频易错点实际使用时要想清楚你要的是整组重复还是逐个重复。R的向量有一个重要特征一个向量中所有元素的类型必须一致。如果你尝试用c(1, a)混合创建R会自动把数值1强制转换为字符1这就是隐式类型转换。虽然便利却经常让新手措手不及比如排序结果和你预期的数字排序完全不同。主动用as.numeric()、as.character()做显式转换是更稳妥的做法。2.2 向量化运算让代码既简洁又高效向量化运算是R语言最核心的卖点。当你对向量做四则运算、比较运算、函数运算时R默认把所有元素挨个处理一遍返回同等长度的结果向量x - c(1, 2, 3, 4, 5) y - x * 2 z - x c(10, 20, 30, 40, 50)你在R里几乎不需要写显式的for循环来处理向量代码写起来简洁执行效率也高。因为向量化操作底层调用了经过优化的C或者Fortran代码比R层面的循环快了不止一个量级。我实测过处理100万条数据时向量化写法比显式循环快几十倍甚至上百倍这个差距在数据分析工作中非常可观。这种设计哲学贯穿整个R语言能用向量化的方式解决问题就不要写循环。你写的代码不仅更短还有更强的可读性别人一眼就能看出你想做什么操作而不用钻进for循环里逐行分析逻辑。2.3 向量的索引、切片与修改R的索引方式很灵活方括号[]是访问向量元素的核心语法。整数索引是最直观的x - c(10, 20, 30, 40, 50) x[1] # 10R的索引从1开始 x[3:5] # 30 40 50 x[c(1, 3)] # 10 30注意R的索引从1开始这和Python从0开始的习惯完全不同初学R时容易踩坑。除此之外负整数索引表示剔除某个位置x[-2]返回除第2个元素外的所有元素逻辑索引则根据条件筛选x[x 30]返回40和50字符索引适用于有名字的向量比如你给向量设置过names()属性后就能直接按名字提取。修改向量元素也走索引思路x[1] - 100把第一个元素改成100。有一个细节值得注意对向量做切片时如果索引中存在超出长度的位置R不会报错只会给对应位置返回NA这往往就是数据里莫名出现NA的根源之一。做索引操作时养成先确认长度的习惯能省掉后面很多排查的精力。3. 循环法则R语言里最容易被忽视的规则3.1 循环法则的机制与触发条件循环法则recycling rule说的是当两个长度不同的向量做运算时R会把较短的向量自动循环补齐到长向量的长度然后再逐元素运算。举个例子c(1, 2, 3, 4, 5, 6) c(10, 20)短的向量c(10, 20)会被循环补成10、20、10、20、10、20然后和长向量相加结果就是11、22、13、24、15、26。这个机制在标量与向量运算时特别常见比如x * 2本质上是2这个长度为1的向量被循环补成了和x等长的向量只不过因为长度整除你感知不到循环的过程。循环法则的意义在于它让R的向量化运算更加灵活不需要你手动把短向量扩展成和长向量等长的形式。比如你想给数据框的某一列统一加一个常数直接加就行循环法则帮你无缝完成。3.2 循环法则的常见应用场景最经典的应用场景是分组数据的变换。假设你有一个长度为12的月度数向量想按季度打标签month_value - c(100, 110, 105, 120, 130, 125, 140, 150, 145, 160, 170, 165) quarter_label - rep(c(Q1, Q2, Q3, Q4), each 3)这里rep()手动生成了与月份向量等长的标签序列这是显式做法。循环法则的隐式做法则是如果你想给每3个数一组做某种标志可以让短向量循环作用在分组后的数据上。虽然实际分析中你更多会用rep()来精确控制长度但理解循环法则能让你看懂很多R代码中看似长度不匹配却能正常运行的写法。另一个常见场景是做条件判断后的赋值ifelse(x 5, high, low)内部也依赖了向量的循环机制为结果向量填充符合条件的值。你写ifelse时条件参数、是、否三个参数通过循环法则对齐成相同长度因此这个函数能优雅地逐元素判断并返回结果向量。3.3 循环法则的坑与规避方法循环法则最大的坑在于长向量长度不是短向量的整数倍时R只会给出一个警告但依然继续执行。比如c(1, 2, 3, 4, 5) c(10, 20)短向量会被循环补成10、20、10、20、10然后相加R弹出一个警告说长的对象长度不是短的对象长度的倍数但不会中断计算。这个设计容易让人忽略问题尤其在写复杂数据处理流程时你可能在数据框的两个列之间做运算列长度偶然不一致结果却被悄悄补齐得到完全错误的输出。我在实操中的习惯是在进行向量运算前先对参与运算的对象执行length()检查或者利用比较长度是否一致。另一个实用技巧是用rep()配合length.out参数明确扩展短向量rep(c(10, 20), length.out 5)生成10、20、10、20、10这样你清楚自己在做什么而不是把循环法则当作隐式的默认行为把结果交给R去猜。4. NaN与NA缺失值的两种面孔4.1 NA与NaN的定义和区别这两个概念在R里非常接近却又本质不同也是面试和实战中经常被问到的点。NA是Not Available的缩写表示数据缺失也就是说这个位置理论上应该有值但实际没有被记录或者记录丢失了。NaN是Not a Number的缩写表示数学上未定义的结果比如0除以0、正无穷减正无穷都属于NaN。所以核心区别是语义层面的NA代表这个观测缺失了NaN代表计算过程算出了非数值。用R的输出可以直观感受这种区别 0 / 0 [1] NaN c(1, 2, NA, 4) [1] 1 2 NA 4R里NaN在严格意义上也属于NA的一种因为is.na(NaN)返回TRUE。但反过来is.nan(NA)返回FALSE。这一点非常容易混淆很多人在判断缺失值时只用is.na()结果把NaN和NA都筛掉了有时候这没问题有时候却会掩盖真正的计算异常你需要根据场景决定是否要区分二者。4.2 缺失值在运算中的传播机制NA和NaN都有一个让新手抓狂的传播特性只要运算中有NA结果往往是NA有NaN结果往往是NaN。这不是R在捣乱而是统计软件的一种安全设计。试想你有一组销售数据其中某一天的销售额缺失那么这一周的总销售额就不应该是确定的数值因为你不知道缺失那天到底卖了多少所以R返回NA提醒你这周数据不完整。 sum(c(10, 20, NA)) [1] NA这种传播机制在数据清理之前显得很麻烦但它是统计严谨性的一部分。处理方式有两种一是用na.rm TRUE参数显式忽略缺失值sum(c(10, 20, NA), na.rm TRUE)返回30二是用is.na()筛选出非缺失的位置后再做运算。我建议你在使用na.rm TRUE之前先认真确认缺失值的占比和产生机制。如果缺失比例较高直接删掉可能引入严重偏差这时要考虑插补方法。对缺失值做盲目的消除处理是数据分析新手最容易犯的错误之一。4.3 如何正确判断和区分NA与NaN判断函数的选择很关键。is.na()会同时识别NA和NaN因为NaN被设计为NA的一种。is.nan()则只识别NaN。如果你只想判断真正的缺失值、排除计算异常可以用以下组合逻辑x - c(1, NA, NaN, 2) is.na(x) # FALSE TRUE TRUE FALSE is.nan(x) # FALSE FALSE TRUE FALSE is.na(x) !is.nan(x) # FALSE TRUE FALSE FALSE只识别真正的NA这个组合表达式在实际数据清洗中非常有用。比如你处理基因表达数据时某些数值是0除以0产生的NaN而另外一些是实验过程中样品丢失导致的NA这两种情况的处理策略完全不同。前者可能要从计算逻辑排查后者则涉及样本剔除或者插补。用table(is.na(x), is.nan(x))这种交叉形式可以快速摸清数据中的缺失结构。关于NA还有几个特殊变体NA_integer_、NA_real_、NA_character_等它们分别对应不同类型向量的缺失值。大部分情况下你直接写NAR会自动根据上下文转换类型但如果你在一个字符向量里写NA它在底层是字符型缺失在数值向量里则是数值型缺失。这在你用str()查看数据结构时会看得很清楚。5. 内置函数dnorm正态分布的概率密度5.1 dnorm的数学含义与参数说明dnorm的全称是normal probability density function即正态分布的概率密度函数。给定一个数值xdnorm(x)返回标准正态分布在该点的密度值。完整的调用形式是dnorm(x, mean 0, sd 1, log FALSE)mean指定均值sd指定标准差log设置为TRUE时返回对数密度值。标准的正态分布密度函数公式是f(x) (1 / (sd * sqrt(2 * pi))) * exp(-((x - mean)^2) / (2 * sd^2))dnorm就是这个公式的直接实现你不用自己手写R已经帮你算好了。此外R还提供了与正态分布配套的其他函数pnorm用于计算累积分布概率qnorm用于根据概率反推分位数rnorm用于生成服从指定正态分布的随机数。这四兄弟构成了一套完整的分布函数族后面做假设检验、置信区间、蒙特卡洛模拟都会用到。5.2 dnorm的实操案例配合curve画分布曲线光说公式太抽象实际应用中我经常用dnorm配合curve函数画分布密度曲线。比如我想看看均值为0、标准差分别为1和2的两条正态曲线的形状差异curve(dnorm(x, mean 0, sd 1), from -5, to 5, col blue, ylab 密度) curve(dnorm(x, mean 0, sd 2), from -5, to 5, col red, add TRUE) legend(topright, legend c(sd1, sd2), col c(blue, red), lty 1)这样画出来你能直观看到标准差越大曲线越矮越胖因为密度函数在x轴上的总面积恒为1标准差变大意味着分布变宽峰值自然下降。这种可视化的直觉对后续理解z-score、置信区间非常有帮助。dnorm也不光能画曲线。在极大似然估计中你需要计算样本在特定参数下的联合密度也就是每个观测点的密度值求乘积再取对数。这时dnorm是你实现自定义似然函数的核心工具。配合optim或nlm做参数优化就能从数据中反推最可能的均值与标准差。5.3 内置分布函数的全家桶及扩展思路除了正态分布R还内置了几十个概率分布对应的函数遵循统一的命名规则d表示密度或离散分布的概率质量函数p表示累积分布函数q表示分位数函数r表示随机数生成。比如dbinom、pbinom、qbinom、rbinom对应二项分布dpois、ppois、qpois、rpois对应泊松分布dt、pt、qt、rt对应t分布dchisq、pchisq、qchisq、rchisq对应卡方分布。掌握这套规律后你不需要每次查文档也能快速推断出想要的函数名称。比如做线性回归时要去检验某个回归系数的p值你知道检验统计量服从t分布就直接用pt()算尾部概率做卡方独立性检验后想找临界值就用qchisq()反推。这套分布函数族是R最强大的资产之一遇到概率统计问题先想想能不能用这几个前缀加分布名解决通常会比别人手算快得多。在实际数据分析项目中我几乎每周都会用到dnorm和它的同类函数。有一次做金融收益率建模我假设日收益率服从正态分布用rnorm生成模拟数据再用dnorm计算每个收益率出现的似然最后用optim求出最大似然估计的参数整个过程都建立在这套分布函数族之上。理解单一函数是基础理解整套命名规则你才真正解锁了R的统计计算能力。6. 常见问题与排查技巧实录6.1 高频报错与解决思路速查表这里整理我在带新人和日常使用中经常遇到的一批问题按表格形式呈现方便你直接对照排查。现象可能原因解决思路x[0]返回空向量R的索引从1开始0索引表示空集用x[1]取第一个元素向量的长度不是短对象长度的倍数时出现警告循环法则触发长度不整除用length()检查长度用rep()显式控制sum(x)返回NA向量中存在NA或NaN使用na.rm TRUE或先is.na()筛除is.na(x)把NaN也识别为TRUENaN属于NA的一种用is.na(x) !is.nan(x)区分c(1, a)得到字符向量隐式类型转换用as.numeric()显式转换画出的正态曲线不连续from和to设置太窄或n太小curve函数增加参数n1000代码在RStudio里卡住意外进入了循环或等待输入按Esc中断执行检查控制台状态6.2 关于缺失值处理的两个实操建议第一个建议是在数据分析的探索阶段先用summary()查看每个变量的缺失情况R会返回每列的NA数量。再配合colMeans(is.na(df))算出各列缺失比例超过某个阈值比如30%就要慎重考虑这个变量是否还能用。缺失值处理不是无脑删除更不是无脑填充你要先搞清楚缺失机制是完全随机缺失、随机缺失还是非随机缺失不同机制对应完全不同的处理策略。第二个建议来自我踩过的一次坑不要在不知道NA存在的情况下直接做运算。比如你用mean(x)计算结果发现是NA就说明x中存在缺失但mean(x, na.rm TRUE)的结果又可能因为删除了NA而偏离真实值。我后来养成了习惯在上模型之前先用complete.cases()或na.omit()把数据框处理干净再把处理后的数据存成新对象这样后续的分析就不会被NA渗入。这个习惯帮我避开过很多后续建模阶段的诡异错误。6.3 从基础到实战的进阶路径掌握了向量、循环法则、NA/NaN和dnorm这些基础后下一步该学什么我建议按这个路径推进用向量操作改写你过去写的循环代码体会向量化的性能差异练习data.frame的创建与操作把向量组装成真正的表格数据学习dplyr包的管道操作和分组汇总方法掌握ggplot2的画图逻辑用数据可视化反推数据质量问题最后回到统计学本身结合R的分布函数族做假设检验和置信区间计算这套路径的核心思路是先把R的数据操作搞顺再进入统计建模和可视化。不要把时间浪费在背诵语法上最好的学习方式是带着真实数据分析任务去接触这些概念遇到不懂的再回头查基础知识。你在处理真实数据时遇到的缺失值问题永远比教科书上的示例复杂得多而解决这些问题的过程就是你真正内化R语言的过程。最后再分享一个小技巧学习R的过程中养成打印出来看看的习惯。每创建一个新对象就在控制台输入对象名回车或者用str()查看结构用head()查看前几行。不要凭感觉猜测数据长什么样R的优势就在于你可以随时直观地查看中间结果把每一步都看得清清楚楚。这个习惯会让你排查问题的速度快很多也是我带过的所有上手快的使用者共同具备的特点。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进