假设你新接手一个小区的档案室。架子上插着一本本居民册子,每一页住一户人家,每一栏填一类信息:姓名、年龄、楼栋、疫苗接种……这本册子在 R 语言里就叫 data.frame(数据框)。它几乎是所有数据分析的起点——读进来的原始数据是它,清洗后的整洁数据是它,喂给建模函数的还是它。你会需要新建册子(创建)、翻找档案(索引与筛选)、加栏撕栏(增删列)、把两本册子按身份证号钉在一起(合并)、按楼栋拆成小册子(拆分),还会碰到两种烦人的情况:文字和数字混着填(类型转换),以及格子空着没填(NA)。再往后,你会换上更轻便的智能册子 tibble,也会让电脑帮你批量把 Excel 表导成册子。这篇就把这套“档案管理术”从头到尾讲清楚,让你拿到任何数据都不慌。

岗位关联

数据框操作是数据清洗与特征工程的基本功。掌握它,你才能把杂乱原始数据变成可喂给模型的整洁特征表——这是 AI 项目和数据分析绕不开的第一道工序。

目录
  1. 可视化演示
  2. 数据框创建:搭起第一本档案册
  3. 索引与筛选:翻档案的三种手法
  4. 增删改列:给档案册加减栏位
  5. 合并与拆分:档案册的拼与拆
  6. 类型转换:栏位文字的“翻译官”
  7. 因子与NA:标准选项与空白格
  8. tibble衔接:升级版智能档案册
  9. 读取与性能:把外部档案批量搬进系统
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态展示红黑树插入新节点后通过变色和旋转操作恢复红黑性质的平衡过程,直观呈现自平衡二叉搜索树的颜色约束与修复策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

红黑树插入 - 初始状态 红色节点:新插入或需修复 黑色节点:满足红黑性质 |bh| ≤ 2log(n+1) 自平衡BST

数据框创建:搭起第一本档案册

小林是幸福里小区的档案员。他拿来一本崭新的空白册子,先在页面上画好栏位:姓名、年龄、楼栋、疫苗接种情况。每一栏一列,从上到下;每一户一行,从左到右。这本册子,就是 R 里的 data.frame——一个“行 × 列”的二维表格,行是观测、列是变量。

在 R 中创建数据框同样简单:把几个“等长向量”用 data.frame() 包起来,每个向量就是一列。向量就是“成串的数据”,比如 c("张姨","李爷",...) 是一串姓名,c(38,71,...) 是一串年龄。关键要求是:所有向量的长度必须一致,否则 R 会直接报错——就像册子某一栏少印了几格,整本册子没法对齐装订。

创建之后,用 str(df) 可以查看册子的“体检报告”:每一列是什么类型(数值 num、字符 chr、因子 Factor),一共有几行几列。nrow(df) 返回行数(册子有多少页),ncol(df) 返回列数(每页有多少栏)。平时也常写成 data.frame(姓名 = c(...), 年龄 = c(...)) 这种带名字的写法,列名就自动是向量名。

就像小林检查新册子栏位是否对齐,R 也用 str() 来“抽查”。记住这个二维结构的直觉,后面所有操作——筛选、合并、转换——都是围绕“行”和“列”这两个维度展开的。

DFn×p=(c1,c2,…,cp),∀j: length(cj)=n\displaystyle \mathbf{DF}_{n \times p} = \big(\mathbf{c}_1, \mathbf{c}_2, \ldots, \mathbf{c}_p\big), \quad \forall j: \ \text{length}(\mathbf{c}_j) = n
公式通俗说明:一个数据框有 n 行 p 列,它由 p 个向量拼成;每个向量的长度都必须等于 n,这就是“等长铁律”。
  1. 行与列数据框是有 n 行 p 列的二维结构,就像档案册有“页”有“栏”。
    DFn×p\displaystyle \mathbf{DF}_{n \times p}
  2. 列即向量每一列 c_j 都是一个向量,相当于册子的一个栏位。
    c1,c2,…,cp\displaystyle \mathbf{c}_1, \mathbf{c}_2, \ldots, \mathbf{c}_p
  3. 等长铁律所有列的长度都等于 n,否则创建失败。
    ∀j: length(cj)=n\displaystyle \forall j: \ \text{length}(\mathbf{c}_j) = n

索引与筛选:翻档案的三种手法

小林找档案有三种方式:知道页码直接翻到那一页(按行号);知道栏位名扫一眼那一栏(按列名);或者按条件筛——“把 60 岁以上的老人找出来,准备上门慰问”(逻辑条件)。这三种方式,对应 R 里三种取数手段。

R 里 df[行, 列] 是一把万能钥匙。方括号内逗号前管“行”,逗号后管“列”。df[1, ] 取第 1 行且保留全部列;df[, "年龄"] 取年龄列的全部行;而 df$年龄 是取列最快最直接的写法,$ 就像栏位名的书签。

条件筛选是重中之重:df[df$年龄 > 60, ] 的执行过程分两步——先用 df$年龄 > 60 生成一串 TRUE/FALSE 的逻辑向量,标记每一行是否满足条件,再按 TRUE 的位置把对应行抽出来。新手最容易犯的错是漏写逗号,写成 df[df$年龄 > 60],R 会以为你想筛“列”而不是“行”。subset(df, 年龄 > 60) 是更不易出错的写法。

三招翻档案——翻页码、看栏位名、按条件找人,分别对应行号索引、$ 取列和逻辑筛选。初学时最容易丢的是那个逗号,丢了它,就像对管理员说“我要找年龄大于 60 的”却没说是“翻页”还是“抽栏”,结果自然不对。

DF[行, 列],DF[DF$年龄>60, ]\displaystyle \mathbf{DF}[\text{行}, \ \text{列}], \qquad \mathbf{DF}[\mathbf{DF}\$年龄 > 60, \ ]
公式通俗说明:方括号里逗号前选行、逗号后选列;行位置写条件、列位置留空,就能把所有满足条件的行整行拎出来。
  1. 逗号分隔两个维度方括号里逗号前管“行”,逗号后管“列”。
    [行, 列]\displaystyle [\text{行}, \ \text{列}]
  2. 行方向写条件df$年龄 > 60 先生成每行是否符合的逻辑向量,TRUE 的行被保留。
    DF[DF$年龄>60, ]\displaystyle \mathbf{DF}[\mathbf{DF}\$年龄 > 60, \ ]
  3. 列方向留空逗号后什么都不写,表示“所有列都保留”。
    DF[⋅, ]\displaystyle \mathbf{DF}[\cdot, \ ]

增删改列:给档案册加减栏位

上级通知:本周要补登记“疫苗接种情况”。小林在册子右侧新画一栏,一户一户填上“已打/未打”;过段时间又发现“楼栋代码”大家统一用地图 APP 查了,册子里这一栏纯属冗余,就干脆撕掉。加一栏、撕一栏,这就是数据框的增删列。

R 里加列是最自然的操作:df$新列 <- 值。$ 不仅能读列,还能“造”出新列。右边可以是一个等长向量,也可以是由已有列计算出的表达式,比如 df$年龄段 <- ifelse(df$年龄 > 60, "高龄", "普通")。删列同样简单:df$旧列 <- NULL,把整列赋成“空无”,这栏就消失了。

改列名也有讲究:names(df)[2] <- "岁数" 按位置改,或者用 dplyr 的 rename(df, 岁数 = 年龄)。transform(df, 新列 = 表达式) 则会返回一个添加了新列的副本,不改动原册子——适合链式处理,但要注意它不像 df$新列 <- ... 那样直接改动原对象。这就是“改原件”和“做复印件再改”的区别。

加一栏、撕一栏、改栏位名,都是“册子版本升级”。但要时刻分清两种方式:$ 赋值直接改原件,而 transform 只是给你看一份改好的复印件——想留下改动,必须再把复印件装回档案袋,也就是重新赋值给自己。

df$新列←v,df$旧列←NULL\displaystyle \text{df}\$\text{新列} \leftarrow \mathbf{v}, \qquad \text{df}\$\text{旧列} \leftarrow \text{NULL}
公式通俗说明:给数据框加列就是往册子里塞一个新栏位;把某一列赋成 NULL,就是把这栏整个拿掉。
  1. $ 操作符指定列df$列名 既能读列也能写列,是 R 的“栏位书签”。
    df$新列\displaystyle \text{df}\$\text{新列}
  2. 赋值即新增或修改把向量 v 塞进册子的一个新栏,栏位名就是 $ 后面的名字。
    df$新列←v\displaystyle \text{df}\$\text{新列} \leftarrow \mathbf{v}
  3. 赋 NULL 即删除NULL 代表“空无”,把它赋给一列,等于把这栏整体撕掉。
    df$旧列←NULL\displaystyle \text{df}\$\text{旧列} \leftarrow \text{NULL}

合并与拆分:档案册的拼与拆

小区里有两本册子:一本记基本信息,一本记健康档案,两本都有身份证号。小林把两本册子按身份证号对齐,同一户的档案合到一行,钉成一本“总册”。反过来,临近年底要给各楼楼长发分册,他按楼栋把这本总册拆成六本小册子。拼与拆,就是 R 里的合并与拆分。

竖着拼接用 rbind(df1, df2):把第二本册子叠在第一本下面,要求两本的列名和列数完全一致,就像两本册子的栏位必须一模一样才能叠放。横着拼接用 cbind(df1, df2):左右贴在一起,要求行数一致。而 merge(df1, df2, by = "ID") 才是按关键字段(身份证号)横向关联——这是最常用的“按 ID 对档案”操作,相当于 SQL 里的 JOIN。默认是内连接,只保留两边 ID 都出现的行;加 all = TRUE 就变成外连接,保留所有行,没匹配上的格子填 NA。

拆分则用 split(df, f):按分组变量 f(比如楼栋)把册子拆成一个列表,每个元素是一本小分册。拆完之后配 lapply(),可以给每栋楼分别计算平均年龄,这就是“按组处理”的经典套路。

拼册子有三种手法:上下叠(rbind)、左右贴(cbind)、按 ID 对齐(merge);拆分则是一本变多本(split)。理解“纵向追加”和“横向关联”是两回事,后面遇到报错和错位时,就不会一头雾水了。

merge(A,B,by="ID")  ⟺  A⋈IDB\displaystyle \text{merge}(A, B, \text{by} = \text{"ID"}) \iff A \underset{\text{ID}}{\bowtie} B
公式通俗说明:merge 就是把 A 和 B 两本册子按 ID 字段对上,取双方都有的那部分行,很像数学里的交集。
  1. 两张输入表merge 的第一个参数 A、第二个参数 B 是两本“册子”(数据框)。
    A,B\displaystyle A, B
  2. 按关键字匹配by = "ID" 指定以哪一列作为匹配的身份证号。
    by="ID"\displaystyle \text{by} = \text{"ID"}
  3. 内连接取交集默认只保留两边关键字都存在的行,符号上就像按 ID 做连接。
    A⋈IDB\displaystyle A \underset{\text{ID}}{\bowtie} B

类型转换:栏位文字的“翻译官”

小林发现年龄这一栏乱套了:有的人填“六十二”,有的人填 62。文字和数字混在一起,既没法排序也没法求平均。他得先把所有文字翻译成统一数字格式。在 R 里,as.* 系列函数就是这样的“翻译官”。

as.numeric() 把字符或因子翻译成数值,as.character() 把数值翻译成字符,as.factor() 把字符翻译成因子(标准选项),as.data.frame() 把矩阵或列表翻译成数据框。翻译官们有一个共同特点:它们只产出翻译结果,不会自动改掉原档。想让册子真正换一种写法,必须把结果重新赋值回去:df$年龄 <- as.numeric(df$年龄)。忘了这一步,是最普遍的坑。

还有一个隐蔽的陷阱:因子转数值不能一步到位。因子内部存的是“水平的编号”(1、2、3…),直接 as.numeric() 取出来的是编号而不是真实的数字。安全做法是 as.numeric(as.character(f))——先把标签还原成原文,再把原文翻译成数字。就像查“六楼”的编号发现是 2,你得先看标签表还原成“六楼”,再转成 6。

翻译官只负责翻译,不负责誊抄;你拿到译文后,要亲手把它贴回档案册。在 R 里做任何类型转换,都默念一句“重新赋值”,就能避开大半新手报错。

x′=as.numeric(x),x←x′\displaystyle x' = \text{as.numeric}(x), \qquad x \leftarrow x'
公式通俗说明:as.numeric 把 x 翻译成数值得到新结果 x',翻译件不会自动回填,必须手动赋值 x <- x' 才生效。
  1. as.numeric 是翻译官把字符或因子翻译成数值,产出新值。
    x′=as.numeric(x)\displaystyle x' = \text{as.numeric}(x)
  2. 翻译件不会自动回填必须把结果写回原变量,册子上的内容才会更新。
    x←x′\displaystyle x \leftarrow x'

因子与NA:标准选项与空白格

楼栋这一栏只能填 1 到 6 栋,小林用一张“标准选项表”约束大家,防止有人手滑填成“七栋”。在 R 里,这种“只能在有限选项里取值”的列就是因子(factor)。另一种情况是档案上某个格子空着——不知道、没问到、或确实没有,这个空白格就是 NA(缺失值)。

创建因子用 factor(x, levels = c("1F","2F","3F","4F","5F","6F")),levels() 可以查看甚至重排这些标准选项。因子内部其实存的是“选项编号”(整数),显示时再翻译回标签文字,所以排序按 levels 顺序而非字母顺序。处理 NA 有三板斧:is.na(df) 逐格体检,返回一个和原册子形状一样的 TRUE/FALSE 逻辑矩阵;na.omit(df) 把任何含有 NA 的行整行删掉;计算时还可以加 na.rm = TRUE 参数直接跳过 NA,比如 mean(df$年龄, na.rm = TRUE)。

NA 最重要的一条语义是:NA 代表“未知”。所以 df$年龄 == NA 不会返回 TRUE,因为“未知等于未知”在逻辑上无法回答,R 干脆也返回 NA。判断缺失必须用 is.na()。这就像看到档案上一个空格,你问“空等于空吗?”——管理员只能说“不知道,因为空格本身不代表同一个值”。

因子是“标准选项表”,NA 是“未填格子”。处理档案里的空白格,无外乎三条路:删掉那行、计算时跳过、或者回去补访真实值。千万别用 == 去碰 NA,那是 R 新手最容易挂彩的地方。

is.na(DF)↦{TRUE,FALSE}n×p,na.omit(DF)↦去除NA后的 DF′\displaystyle \text{is.na}(\mathbf{DF}) \mapsto \{\text{TRUE}, \text{FALSE}\}^{n \times p}, \qquad \text{na.omit}(\mathbf{DF}) \mapsto \text{去除NA后的} \ \mathbf{DF}'
公式通俗说明:is.na 把整个数据框逐格变成“是不是 NA”的逻辑矩阵;na.omit 则直接删掉任何含有 NA 的行,返回干净版本。
  1. is.na 逐格体检返回和 DF 同尺寸的逻辑矩阵,格子里是 TRUE 就说明原位置是 NA。
    is.na(DF)↦{TRUE,FALSE}n×p\displaystyle \text{is.na}(\mathbf{DF}) \mapsto \{\text{TRUE}, \text{FALSE}\}^{n \times p}
  2. na.omit 整行淘汰只要某行有一个格子是 NA,就把这一整行从册子里删掉。
    na.omit(DF)\displaystyle \text{na.omit}(\mathbf{DF})

tibble衔接:升级版智能档案册

后来小区启用了新的“智能档案系统”tibble。它和旧册子 data.frame 装的是同一批数据,只是用起来更贴心:打印到屏幕上时自动精简——只显示前几行和每列类型,不再一串到底;也不会“好心办坏事”,把文本列自动变成标准选项(因子),让文本老老实实做文本。

tibble 是 tidyverse 生态里的升级版数据框。用 tibble::as_tibble(df) 可以把旧册子 data.frame 一键导成新系统;需要时 as.data.frame(tb) 再转回来。两者核心结构一致,可以随时衔接,所以现有代码和数据完全不用担心作废。

具体差异还有几个:tibble 打印只显示前 10 行并附上列类型;它不会自动把字符串转成因子(R 4.0 之后 data.frame 也不再自动转了,但旧习惯仍是经典坑);支持列名带空格等特殊字符;切片 tb[ , 1] 的结果仍然是 tibble 而不是退化成向量。直接 tibble::tibble(姓名 = ..., 年龄 = ...) 也能创建。在 tidyverse 流水线里,tibble 是默认的数据格式。

新老两套档案系统可以互导、并存。日常分析时选 tibble 更清爽,需要跟老代码对接时再转回 data.frame——理解了这层“无缝衔接”,你就能自由出入 tidyverse 生态,而不必担心被某一种格式绑死。

TB=tibble::as_tibble(DF),DF′=as.data.frame(TB)\displaystyle \mathbf{TB} = \text{tibble::as\_tibble}(\mathbf{DF}), \qquad \mathbf{DF}' = \text{as.data.frame}(\mathbf{TB})
公式通俗说明:as_tibble 把 data.frame 升级成 tibble,as.data.frame 再降级回来,两个方向都能转。
  1. 升级转 tibble把旧档案册 data.frame 导入新智能系统,得到 tb。
    TB=tibble::as_tibble(DF)\displaystyle \mathbf{TB} = \text{tibble::as\_tibble}(\mathbf{DF})
  2. 降级转回 data.frame某些旧函数只认 data.frame 时,再转回去。
    DF′=as.data.frame(TB)\displaystyle \mathbf{DF}' = \text{as.data.frame}(\mathbf{TB})

读取与性能:把外部档案批量搬进系统

街道办发来一个 CSV 电子表《居民登记表》,几百行数据。小林当然不会手工誊写进册子,他直接一键导入。R 里 read.csv() 就是干这个的:把外部表格文件读成一个 data.frame,瞬间搬进系统。

df <- read.csv("居民.csv", stringsAsFactors = FALSE) 是标准姿势。header = TRUE(默认)表示把第一行当作栏位名;stringsAsFactors = FALSE 表示文本列保持文本,不要自动变成因子——在 R 4.0 之前这个参数默认是 TRUE,导致无数人读进来才发现字符列全变成了因子,排序和改值都怪怪的。

册子大了之后,性能问题也随之而来。data.frame 在行数过百万时,逐行操作(比如 for 循环)会慢得让人想砸键盘。此时可以换用 data.table 包的 fread() 读取极快、DT[条件, 计算] 语法高效;dplyr 的链式操作也针对大数据做了优化。还有一个内存习惯要养成:R 里“复制一份再改”很方便,但大数据场景要尽量在原对象上操作,避免反复复制撑爆内存。

数据从外部进来,先用 read.csv 或 fread 批量导入成册;册子大了,就换更趁手的工具(data.table、dplyr)——就像档案多到一定程度,光靠手写册子不够,得上电子检索系统。理解了这一整套“数据搬运学”,数据清洗的日常链路就跑通了。

DF=read.csv(file, header=TRUE, stringsAsFactors=FALSE)\displaystyle \mathbf{DF} = \text{read.csv}(\text{file}, \ \text{header} = \text{TRUE}, \ \text{stringsAsFactors} = \text{FALSE})
公式通俗说明:read.csv 把外部 CSV 文件读成一个数据框;header 决定第一行是否当列名,stringsAsFactors 决定文本列会不会自动变因子。
  1. 读取文件read.csv 负责把外部表格文件读成数据框。
    DF=read.csv(file)\displaystyle \mathbf{DF} = \text{read.csv}(\text{file})
  2. 首行当列名header = TRUE 表示第一行作为栏位名,不当作数据。
    header=TRUE\displaystyle \text{header} = \text{TRUE}
  3. 字符保持文本stringsAsFactors = FALSE 防止文本列被悄悄改成因子。
    stringsAsFactors=FALSE\displaystyle \text{stringsAsFactors} = \text{FALSE}

AI 应用场景

电商用户档案的清洗合并

阶段:数据清洗

解决的问题:原始的用户表、订单表分散在两个系统,字段命名混乱、年龄有缺失、文本列误存为因子。AI 建模的效果上限取决于数据质量,脏数据会让后续模型“学歪”。

具体案例:电商数据分析师小陈拿到 1 月的注册表和 3 月的订单表,先用 merge 按用户 ID 关联成一张宽表;再按“近 90 天有购买”做条件筛选圈出活跃用户;用 is.na 定位缺失年龄并填充均值。整个过程就像档案员把两本册子按身份证号合订,再筛出重点住户。

风控模型特征宽表的构建

阶段:特征工程

解决的问题:原始行为日志是一行一条记录的长表,而机器学习模型需要“一个用户一行”的宽表,且所有特征都必须是数值。这一步在 AI 项目中被称为特征工程。

具体案例:算法工程师用 split 按用户 ID 拆分行为日志,配合 lapply 逐用户聚合出“月均消费”“活跃天数”,再把“所在城市”“设备类型”等类别字段用 as.factor 编码成数值型特征。特征宽表建好后直接喂给 XGBoost 训练。

社区慰问名单的快速圈选

阶段:数据分析支撑决策

解决的问题:运营同事要针对“60 岁以上、手机号未登记”的独居老人做慰问,需要从几千行居民表里快速圈出名单。人工翻 Excel 要一下午,还容易漏人。

具体案例:业务分析师用一行条件筛选 df[df$年龄 > 60 & is.na(df$手机号), ] 直接得到目标名单,再用 na.omit 检查其他关键字段的缺失情况,最终导出 CSV 给社区同事。脚本跑完只需要几秒。

Python 演示

python51 行
import pandas as pdimport numpy as np # 1. 创建数据框:一列一个等长向量,就像档案册的栏位df = pd.DataFrame({    'name': ['zhang_yi', 'li_ye', 'wang_shu', 'liu_shen', 'chen_bo'],    'age': [38, 71, 62, 44, 85],    'bldg': ['3F', '6F', '2F', '5F', '1F'],    'vaccine': ['done', 'done', np.nan, 'done', np.nan],})print('== 创建完成 ==')print(df)print('维度:', df.shape, '\n') # 2. 索引与筛选:找年龄大于60的居民(行筛选)senior = df[df['age'] > 60]print('== age>60 的行 ==')print(senior, '\n') # 3. 增删列:新增体检列,删除楼栋列df['check'] = np.where(df['age'] < 70, 'ok', 'retest')df = df.drop(columns=['bldg'])print('== 增删列之后 ==')print(df, '\n') # 4. 缺失值检查与整行剔除(对应NA处理)print('== 每列NA数量 ==')print(df.isna().sum())df_clean = df.dropna()print('== 剔除含NA行之后 ==')print(df_clean, '\n') # 5. 类型转换:年龄先转字符串,再转回数值(必须重新赋值)df['age'] = df['age'].astype(str)df['age'] = pd.to_numeric(df['age'])print('== 年龄列类型 ==')print(df.dtypes, '\n') # 6. 合并:按姓名把健康档案关联进来(对应merge)health = pd.DataFrame({    'name': ['zhang_yi', 'li_ye', 'wang_shu', 'liu_shen', 'chen_bo'],    'bp': ['normal', 'high', 'normal', 'normal', 'high'],})merged = pd.merge(df, health, on='name')print('== 按name合并 ==')print(merged, '\n') # 7. 分组统计:按体检结果分组求平均年龄group = df.groupby('check')['age'].mean()print('== 按check分组平均年龄 ==')print(group)
目的:用 Python 的 pandas 复刻正文里 R 数据框的全部核心操作——创建、筛选、增删列、NA 处理、类型转换、合并与分组统计,方便对照理解。
  1. df = pd.DataFrame({...})用字典构造数据框,等价于 R 里 data.frame 拼装等长向量。
  2. df['age'] > 60生成布尔(逻辑)向量,标记哪些行满足条件。
  3. df[df['age'] > 60]把布尔向量当作行索引,只保留 TRUE 的行,正是 R 里的条件筛选。
  4. df['check'] = np.where(...)新增一列,像给档案册加栏位。
  5. df.drop(columns=['bldg'])删除列,但需要重新赋值给 df 才生效。
  6. df.isna().sum()统计每列 NA 的数量,对应 R 的 is.na 思路。
  7. df.dropna()删除含 NA 的整行,对应 na.omit。
  8. df['age'].astype(str)把数值列转成字符串,转换后必须重新赋值。
  9. pd.merge(df, health, on='name')按 name 字段横向关联两表,对应 merge。
  10. df.groupby('check')['age'].mean()分组后求均值,对应 split + 聚合。

常见误区

误区“我明明写了 df[df$年龄 > 60],为什么返回的不是筛选后的行?R 怎么这么不听话!”

正解方括号内“逗号前管行、逗号后管列”。不带逗号时 R 默认你在筛“列”,于是它会按逻辑向量去挑列。要筛行必须写成 df[df$年龄 > 60, ],逗号后留空表示“列全要”。就像查档案时你得先说清“翻哪几页”还是“抽哪几栏”,漏了关键词就会拿错东西。

为何常见:Excel 里只有“选行”的概念,新手把记忆负担全放在“条件”上,忽略了 R 方括号的二维约定。

误区“我明明写了 as.numeric(df$年龄),为什么年龄还是字符型?”

正解as.numeric() 是“翻译官”,它只产出翻译结果,不会替你誊回册子。想真正改掉原数据框,必须赋值:df$年龄 <- as.numeric(df$年龄)。就像翻译官把“六十二”翻译成 62 写在便签上,你得自己把便签贴回档案册。

为何常见:很多编程语言的函数会返回新值,初学者混淆“返回值”和“原地修改”,而 R 里大部分函数都不修改原对象。

误区“df$年龄 == NA 为什么一整列都是 NA?我只是想找缺失值啊!”

正解NA 表示“未知”,“未知等于未知”在逻辑上无法回答,所以 R 返回的还是 NA。要找缺失值必须用 is.na(df$年龄)。就像档案上年龄一栏空白,你问“空白等于空白吗?”——管理员只能说“不知道,因为空白不代表同一个值”。

为何常见:其他语言里 null == null 通常得到 true,新手把 NA 类比成 null,忽略了 R 的 NA 是“逻辑上的未知”。

误区“我把因子转数值,怎么 62 变成了 2?数据全乱了!”

正解因子内部存的是“水平的编号”(按 levels 顺序的整数),外面套着标签。直接 as.numeric(f) 取出的是编号而非标签代表的数值。安全做法是 as.numeric(as.character(f)):先还原标签原文,再转数字。就像查“六楼”的编号发现是 2,得先看标签表还原成“六楼”再转成 6。

为何常见:因子打印时显示的是标签而非内部整数,让人误以为内部存的就是原值。

误区“我有两本册子都带 ID 列,用 rbind 一拼怎么全是 NA、还报错?”

正解rbind 是“上下叠放”,要求两本册子的栏位(列名)完全一致,它不关心哪些是同一户;merge 才是“按 ID 横向对齐”,把同一户的档案合并到一行。想关联两表用 merge(df1, df2, by = "ID"),想追加新住户的记录才用 rbind。

为何常见:生活里“合并档案”一词很笼统,新手不知道 R 把“纵向追加”和“横向关联”拆成了两个函数。

面试题

Q1. R 中要创建一个包含“姓名”和“年龄”两列的数据框,正确写法是?

单选题 · easy

  • A.data.frame(姓名=c(“张”,“李”), 年龄=c(38,71))← 正确答案
  • B.data.frame(c(“张”,“李”), c(38,71))
  • C.cbind(姓名=c(“张”,“李”), 年龄=c(38,71))
  • D.list(姓名=c(“张”,“李”), 年龄=c(38,71))

答案:A。A 用 data.frame 把两个等长向量拼成两列并命名;B 虽能建出数据框,但列名是自动的 X1、X2,不符合“姓名、年龄”要求;C 的 cbind 对两个普通向量返回的是矩阵而非数据框;D 得到的是列表,索引方式完全不同。就像档案册要用专门的册子格式,而不是随手抓个活页夹。

Q2. 想要筛选出 df 中“年龄大于 60”的所有行,正确写法是?

单选题 · easy

  • A.df[df$年龄>60, ]← 正确答案
  • B.df[df$年龄>60]
  • C.df[, df$年龄>60]
  • D.df[年龄>60, ]

答案:A。条件筛选行必须保留逗号,A 的逗号后留空表示保留全部列;B 漏写逗号会被当成“筛列”;C 把条件放进了列位置,也是在筛列;D 中的“年龄”不是独立变量,会直接报错。类比翻档案:要先圈出“页”(行条件),再告诉管理员栏位全看。

Q3. 关于数据框中缺失值 NA 的处理,下列说法正确的是?

单选题 · easy

  • A.is.na(df$年龄) 返回逻辑向量← 正确答案
  • B.df$年龄==NA 能判断缺失
  • C.na.omit(df) 删除含NA的列
  • D.NA 只能出现在数值列

答案:A。is.na 逐格检查返回同为 TRUE/FALSE 的逻辑向量;== 与 NA 比较结果还是 NA,无法定位缺失;na.omit 删除的是含 NA 的整行而非整列;NA 可以出现在字符列、数值列等各种类型里。就像档案空白格要靠“逐格扫描仪”找,不能靠“空==空”去猜。

Q4. 把一个因子变量 f 安全地转换为数值,正确的做法是?

单选题 · mid

  • A.as.numeric(as.character(f))← 正确答案
  • B.直接用 as.numeric(f) 就好
  • C.as.factor(as.character(f))
  • D.as.numeric(as.factor(f))

答案:A。因子内部存整数编号,直接 as.numeric(f) 取到的是编号;先 as.character(f) 还原标签文本,再 as.numeric 才得到真实数值。B 正是常见坑;C 在因子之间打转;D 仍然是在转编号,结果和 B 一样错。

Q5. merge(df1, df2, by="ID") 默认进行的是下列哪种操作?

单选题 · mid

  • A.内连接,只保留两边ID都有的行← 正确答案
  • B.左连接,保留 df1 全部行
  • C.笛卡尔积,全部行两两组合配对
  • D.行拼接,把 df2 叠到 df1 下面

答案:A。merge 在不指定 all 参数时默认 all=FALSE,相当于 SQL 内连接,只保留两边 ID 都出现的行;B 是左连接(all.x=TRUE);C 是没有关键字连接的产物;D 是 rbind 的行为,列不匹配还会报错。

Q6. 关于 tibble 与 data.frame 的关系,下列描述不正确的是?

单选题 · mid

  • A.as_tibble 能把数据框转成 tibble
  • B.tibble 打印精简且不自动转因子
  • C.二者之间完全无法互相转换← 正确答案
  • D.as.data.frame 能转回数据框

答案:C。as_tibble 和 as.data.frame 可以双向转换,C 说“完全无法互相转换”是错误的;A、B、D 都是对 tibble 与 data.frame 关系的正确描述。就像新旧两套档案系统可以互导,而不是互不兼容。

Q7. 使用 rbind(df1, df2) 拼接两个数据框,最可能的报错原因是?

单选题 · hard

  • A.两表的列名或列数不一致← 正确答案
  • B.两张表的行数一样多
  • C.表中存在缺失值 NA
  • D.列名的顺序完全相同

答案:A。rbind 按行拼接要求两表的列名与列数完全对齐,不一致时要么报错要么错位;B 行数相同不是问题,反而更容易拼;C 有 NA 不影响拼接;D 列名顺序相同有助于拼接成功。就像两本册子栏位对不齐,就叠不成一本。

Q8. 关于 rbind 和 cbind,以下说法正确的有?

多选题 · mid

  • A.rbind 按行拼接且要求列名一致← 正确答案
  • B.cbind 按列拼接且要求行数一致← 正确答案
  • C.rbind 相当于把两本册子上下叠← 正确答案
  • D.cbind 的结果永远是正确的数据框

答案:ABC。rbind 是上下叠放(列名必须对齐),cbind 是左右拼接(行数必须对齐),二者都不是按关键字关联,所以 A、B、C 正确;D 错误,因为 cbind 拼接两个普通向量时返回的是矩阵而非数据框。

Q9. 关于数据框的索引 df[行,列],下列说法正确的有?

多选题 · hard

  • A.逗号前选行,逗号后选列← 正确答案
  • B.逗号后留空表示保留全部列← 正确答案
  • C.df$列名 是取列的最快写法← 正确答案
  • D.行索引只能写数字不能写逻辑值

答案:ABC。逗号前确定行、逗号后确定列,列位置留空等于“所有列”,$ 取列是常用快捷方式,A、B、C 都正确;行索引既可以写数字也可以写逻辑向量(TRUE/FALSE),所以 D 错误。就像翻档案可以说“第 5 页”,也可以说“凡是高龄的都调出来”。

Q10. 关于因子(factor)的说法,正确的有哪些?

多选题 · hard

  • A.因子内部以整数编码并配有水平标签← 正确答案
  • B.因子适合存储有限数量的分类取值← 正确答案
  • C.levels() 可以查看和重设水平← 正确答案
  • D.普通因子同样可以直接比大小

答案:ABC。因子以整数编码加水平标签存储,适合有限分类;levels() 可查看和修改水平;普通因子没有顺序语义,不能直接比大小(ordered 因子才可以),所以 D 错误。就像“楼栋”枚举值不能当作 1<2 去排序。