3 数据读取与写入:readr、readxl、haven 包实战

好,咱们进入第三章。前两章我们把 R 和 RStudio 环境搭好了,也把基本的变量类型摸了个透。但做质控分析,数据是根本。你手里可能有 Excel 表格、CSV 日志、SPSS 的问卷结果,甚至 SAS 的临床试验数据。怎么把它们弄进 R 里?

说实话,我在工厂里见过太多人用 read.csv() 碰到乱码,气得砸键盘。其实 R 的基础函数能用,但不够快、不够稳。咱们今天要讲的三个包——readr、readxl、haven——才是工业级的数据搬运工。我个人习惯,接手任何项目的第一件事就是把这几个包装上。

3.1 三大包的定位和选择

你想想看,数据文件格式五花八门,如果一个包试图支持所有格式,反而容易出问题。专业的事交给专业的包:

包名 主要处理格式 核心理念
readr CSV、TSV、固定宽度文件 快、准、不猜类型(或猜得更聪明)
readxl .xls、.xlsx 无需安装 Java/Perl,原生读取 Excel
haven .sav (SPSS)、.dta (Stata)、.sas7bdat (SAS) 保留标签、值标签和缺失值定义

readr 属于 tidyverse 的基石,它用 C++ 重写了底层解析引擎。我以前用 utils::read.csv() 读 500MB 的制造日志,等得都能泡杯咖啡;换成 readr::read_csv() 后,30 秒搞定。

经验之谈: 如果你所在的企业还在用 SPSS 或 SAS 存储历史质控数据,千万别手动转换成 CSV。一是容易丢失变量标签,二是精度可能被破坏。“haven 包直接读 .sav 文件,连问卷的“非常满意=5”这种值标签都帮你保留下来。

下面我们把三种数据读取方式逐个拆开讲。

3.2 使用 readr 读取 CSV 和文本数据

咱们先处理最普遍的 CSV。read_csv() 语法很直观,但有几个关键参数我得重点说一下:

library(readr)

# 最简单的调用
df <- read_csv("data/生产批次记录.csv")

# 指定列类型,避免自动猜测犯错
df <- read_csv("data/批次数据.csv",
                col_types = cols(
                  批次号  = col_character(),
                  生产日期 = col_date(format = "%Y-%m-%d"),
                  合格率   = col_double(),
                  缺陷数   = col_integer()
                ),
                locale = locale(encoding = "GBK")
)

注意第二个例子里的 col_types。为什么要手动指定?因为 readr 虽然猜类型很聪明,但它有极限。比如批次号是 “001” 开头,默认会猜成数字,变成 1。这在质控溯源时会出大问题。

还有 locale,这是处理中文乱码的关键。国内很多工厂的 CSV 是从老旧系统导出来的,编码是 GB2312 或 GBK。不指定 encoding = "GBK",读进来全是乱码。碰到这种情况怎么办?试试 locale = locale(encoding = "GB18030"),兼容性更好。

readr 还支持 TSV(制表符分隔) 和 固定宽度文件。我检查一个案例时遇到过供应商提供的 .prn 日志,就是固定宽度的。 read_fwf() 这时候就派上用场了:

# 固定宽度文件,需要指定每列的起始和结束位置
df <- read_fwf("设备日志.prn",
                fwf_positions(start = c(1, 11, 21, 31),
                              end   = c(10, 20, 30, 40)),
                col_names = c("时间", "温度", "压力", "状态"))

如果文件里本身有分隔线,也可以用 fwf_empty() 自动猜测列宽,不过稳妥起见,我通常手动指定。

3.3 使用 readxl 读取 Excel 文件

Excel 在质控部门简直是“准系统”。很多人用它记录巡检数据、校准记录、不合格品台账。但 readxl 有一个限制:它只读数据,不读公式和宏。换句话说,如果某个单元格是 =SUM(A1:A10),它读到的会是计算结果,不是公式本身。这其实正是我们需要的——我们只关心最终数值。

基本的读取方法:

library(readxl)

# 读取第一个 sheet
df <- read_excel("质检日报.xlsx")

# 指定 sheet 名称或索引
df <- read_excel("质检日报.xlsx", sheet = "3月第一周")

# 跳过开头几行(很多报表有表头或合并标题)
df <- read_excel("质检日报.xlsx", sheet = 1, skip = 2)

read_excel() 使用 Sheet 1 的第一行作为列名。但现实中的报表往往花里胡哨:前几行是公司 logo、报告编号、审批签字。这时候 skip 参数就非常重要了。

我记得有个项目,对方发来的 Excel 模板前 3 行是合并单元格的标题,第 4 行才是真正的字段名。如果我不加 skip = 3,读进来全 NAN,浪费了半天排查。

注意: readxl 默认会猜测每一列的类型。如果你的列里既有数字又有少量文本(比如备注列),它可能把整列变成字符。建议在读取后,用 mutate_at() 或在后续分析中做类型转换,不要在参数里过度限制 col_types。

另外,Excel 文件里面经常存放多个工作表。你可以用 excel_sheets() 查看所有 sheet 名称:

excel_sheets("过程能力报告.xlsx")

3.4 使用 haven 读取 SPSS 和 SAS 数据

大型制药企业、医疗器械公司和临床研究机构,仍然大量使用 SPSS 和 SAS。这些文件的二进制结构很不透明,haven 包把它们包装成了 R 能识别的 tibble,并且保留了“变量标签”、“值标签”这些元数据。

3.4.1 读取 SPSS 数据 (.sav)

library(haven)

df <- read_sav("客户满意度调查.sav")

# 查看变量标签
attributes(df$满意程度)

# 值标签通常被保留为 labelled 类
# 可以用 as_factor() 转换为因子
df <- df %>% mutate(满意程度 = as_factor(满意程度))

你可能会发现 df$满意程度 显示的是 “5”、“4” 这类数字,而不是 “非常满意”、“满意”。这是因为 haven 默认保留了原始数值。要想显示标签,用 as_factor() 转一下。

3.4.2 读取 SAS 数据 (.sas7bdat 和 .sas7bcat)

SAS 的问题稍微复杂点。它通常有两个关键文件:数据文件 (.sas7bdat) 和格式目录 (.sas7bcat)。格式目录里定义了变量标签和值标签。只读数据文件,你会得到和 SPSS 类似的 labelled 对象,但缺少标签信息。

# 读取 SAS 数据文件
df <- read_sas("临床试验结果.sas7bdat")

# 如果格式目录在另一路径
df <- read_sas("临床试验结果.sas7bdat",
                catalog_file = "格式目录.sas7bcat")

3.4.3 读取 Stata 数据 (.dta)

中国质控圈用 Stata 的少,但高校经管院系和一些咨询公司用得不少。如果你碰到了,用 read_dta():

df <- read_dta("审计抽样数据.dta")

# Stata 也常用 labelled 变量,处理方式与 SPSS 类似

3.5 写入数据

数据读进来处理完了,肯定要写回去或者存成中间结果。这三个包都提供了对应的写入函数:

包 写入函数 典型用法
readr write_csv() / write_tsv() write_csv(df, "处理结果.csv")
readxl write_xlsx() write_xlsx(list(Sheet1=df1, Sheet2=df2), "多工作表输出.xlsx")
haven write_sav() / write_dta() write_sav(df, "回传数据.sav")

我个人有个习惯:用 write_csv() 时总是加上 na = "" 参数,把缺失值写成空白而不是 “NA” 字符串。因为很多老旧 ERP 系统只认空格不认 “NA”。

write_csv(df, "上报数据.csv", na = "")

另外,如果要导出一个带多个工作表的 Excel 文件,write_xlsx() 支持将 命名列表 作为输入,名称就是工作表名,非常实用。

小心浮点数精度: 写入 CSV 时,默认会保留 15 位有效数字。对于质控数据,这通常太多了反而显得文件丑陋。可以用 write_csv(..., write_options = list(digits = 4)) 控制小数位数。不过目前 write_csv 没有直接参数,我的做法是先 mutate(across(where(is.double), ~ round(., 2))) 再写。

3.6 知识结构图:数据读取流程

我把今天的内容整理成了一张流程图。你可以按照这个思路,根据文件扩展名和来源,选择正确的包和参数。

数据读取流程与包选择 原始数据文件 判断格式 readr 包 .csv / .tsv / 固定宽度 readxl 包 .xls / .xlsx haven 包 .sav / .sas7bdat / .dta R 中 tibble / data.frame 关键:col_types / 编码 / 标签

牢记: 读进来的数据,先做三件事——glimpse() 看结构、summary() 看分布、head() 看前几行。确认列名、类型、缺失值都没问题,再进行后续的质控计算。

3.7 日常避坑指南

最后分享几条我自己踩过的坑,希望能帮你节省时间:

  1. 文件路径不要用中文名称。R 在不同操作系统上对中文路径支持不稳定,尽量用英文或数字命名。
  2. 遇到编码问题先检查文件。用记事本打开 CSV,另存为时可以看到当前编码。如果全是乱码,试试 encoding = "UTF-8-BOM" 或 encoding = "GB18030"。
  3. Excel 里的合并单元格要小心。合并区域中只有左上角有值,其余单元格为空。读取前尽量在 Excel 里取消合并,或者用 tidyr::fill() 填充。
  4. haven 读出来的 labelled 列不要直接运算。比如你拿到 “缺陷等级” 列,显示 1、2、3,但 1 可能代表 “严重”。直接求均值没有意义,先用 as_factor() 转换,或者用 zap_labels() 剥离标签再运算。
  5. 大文件处理推荐使用 readr。它使用了内存映射技术,可以读入比内存还大的文件,前提是操作系统支持。如果遇到 10GB 以上的 CSV,考虑 read_csv_chunked() 分块读取。

好了,第三章就到这儿。数据读取是质控分析的起点,磨刀不误砍柴工,把这一步做稳了,后面写任何统计代码都不会因为数据格式问题卡壳。