第13讲:重复值处理——duplicated()与distinct()

数据挖掘与R语言

2026年10月14日

本讲导览

  • 一、前课测验——四道小题(8 min)
  • 二、问题导入——锦城科技的营收为什么被算了两次(7 min)
  • 三、识别——duplicated() 与 count()(16 min)
  • 四、删除——unique() 与 distinct()(14 min)
  • 五、部分字段重复——业务主键去重(15 min)
  • 六、综合实战——识别→处理→验证(20 min)
  • 七、课堂总结——成果自查与 Exit Ticket(10 min)

重要

前两节课把缺失补齐了、异常修正了——但同一条记录还是可能出现不止一次。今天是项目三"数据清洗"的第三课,解决重复值问题:

任务 工具 什么时候用
① 识别 duplicated()·count() 先看有没有重复、重复的是哪几行
② 删除完全重复 distinct()·unique() 整行一模一样的记录
③ 业务主键去重 duplicated(df[, 主键列])·group_by() 整行不同,但某几列组合本该唯一

一、前课测验

缺失值与异常值——识别与处理

1.1 选择题(第 11、12 讲回顾)

1. 第 11 讲的 company_data(16 家公司,revenue 缺 4 个、cost 缺 3 个,散落在 7 个不同的行上),跑 na.omit(df) 和 df %>% drop_na(revenue) 之后各剩几行?

A. na.omit() 剩 9 行,drop_na(revenue) 剩 12 行
B. na.omit() 剩 12 行,drop_na(revenue) 剩 9 行
C. 两者都剩 9 行——本质上是等价的
D. 两者都剩 12 行——cost 缺失的行不该删

2. 一列营收数据 Q1 = 800、Q3 = 1400,按 IQR 法(1.5 倍系数)判断,下面哪个值会被判为异常?

A. 1500
B. 2000
C. 2250
D. 2500

1.2 填空题(第 11、12 讲回顾)

3. 补缺失分数:全部缺的都按及格线 60 填,写 if_else(______(score), 60, score);要优等生填 90、良等生填 80 分档来填,则改用 ______() 函数。

4. 想一行代码直接抓出一列数据里所有超出须外的异常值,写 ________(x)$out——它的界线用的就是箱线图那套 1.5×IQR 规则;返回列表里的 $stats 则给出须线两端、中位数等位置。

二、问题导入——锦城科技的营收为什么被算了两次

先不谈怎么识别,先看看"重复记录"的后果

2.1 承接上节课:一份"最干净"的财务表

经过第 11 讲补缺、第 12 讲修正,company_data 已经没有 NA、没有错录值了。可数据文员复核时,把上月备份文件里的锦城科技记录又导入了一次:

▶️ 查看代码
company_data <- data.frame(
  company = c("锦城科技", "蜀汉制造", "天府物流", "青羊金融", "锦江生物", "浣花实业",
              "锦西商贸", "蜀都餐饮", "青城建材", "天府冷链",
              "锦江农产", "浣花纺织", "锦西物流", "蜀汉电子", "青羊置业", "天府食品"),
  region  = c("华东", "西南", "西南", "华东", "西南", "华东",
              "西南", "西南", "华东", "西南",
              "华东", "华东", "西南", "西南", "华东", "西南"),
  revenue = c(1200, 1463, 1560,  760, 1500,  892, 1340, 680, 807, 2100,
               450,  980, 1463, 1720, 560, 1380),
  debt_ratio = c(0.45, 0.62, 0.38, 0.71, 0.55, 0.48, 0.66, 0.52, 0.44, 0.58,
                 0.75, 0.41, 0.63, 0.49, 0.68, 0.57),
  rating  = c("A", "B", "A", "C", "A", "B", "B", "C", "A", "A",
              "C", "B", "C", "A", "C", "B")
)

# 数据文员把上月备份文件里的锦城科技记录重新导入了一次——
# 一模一样的一行,被追加到了表尾(第 17 行 = 第 1 行)。
company_data <- company_data %>%
  bind_rows(head(company_data, 1))

company_data    # 17 行:第 17 行和第 1 行一模一样——锦城科技被记了两次
    company region revenue debt_ratio rating
1  锦城科技   华东    1200       0.45      A
2  蜀汉制造   西南    1463       0.62      B
3  天府物流   西南    1560       0.38      A
4  青羊金融   华东     760       0.71      C
5  锦江生物   西南    1500       0.55      A
6  浣花实业   华东     892       0.48      B
7  锦西商贸   西南    1340       0.66      B
8  蜀都餐饮   西南     680       0.52      C
9  青城建材   华东     807       0.44      A
10 天府冷链   西南    2100       0.58      A
11 锦江农产   华东     450       0.75      C
12 浣花纺织   华东     980       0.41      B
13 锦西物流   西南    1463       0.63      C
14 蜀汉电子   西南    1720       0.49      A
15 青羊置业   华东     560       0.68      C
16 天府食品   西南    1380       0.57      B
17 锦城科技   华东    1200       0.45      A

2.2 合计被一笔重复抬高

▶️ 查看代码
sum(company_data$revenue)                # 20055 —— 报表上的"集团总营收"
[1] 20055
▶️ 查看代码
sum(head(company_data, 16)$revenue)      # 18855 —— 16 家公司的真实总和
[1] 18855
▶️ 查看代码
20055 - 18855                            # 恰好高出锦城科技一家的 1200
[1] 1200

重要

数据没有缺失、也没有异常值,但同一条记录出现了不止一次——锦城科技的营收被算了两次,总营收凭空多出 1200(虚增约 6.4%)。这在财务上就是"一笔业务重复入账",是数据"脏"的第三种常见形态。

2.3 重复记录是从哪来的?

想一想,现实里同一条记录会出现两次的可能原因:数据导出时重复抓取、多个部门各自上报同一条记录、Excel 复制粘贴出错、备份文件恢复时把旧记录导了回来……

提问:重复值不像 15000 那样扎眼,整行和别的行一模一样,不逐行核对根本发现不了——该怎么系统地查?这就是今天的内容。

三、重复值的识别——duplicated()与它的伙伴们

先看清楚哪些行重复、重复了几次

3.1 duplicated()——逐行判断

▶️ 查看代码
duplicated(company_data)         # 从第二次出现开始标记为TRUE
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[13] FALSE FALSE FALSE FALSE  TRUE
▶️ 查看代码
sum(duplicated(company_data))    # 统计重复行数量
[1] 1

提示

duplicated() 的判断逻辑是"和前面已经出现过的行完全一样":第一次出现的记录标记为FALSE,第二次及以后出现的相同记录才标记为TRUE。

3.2 找出重复的是哪些行

▶️ 查看代码
company_data %>%
  filter(duplicated(company_data))    # 只看被标记为重复的那些行
   company region revenue debt_ratio rating
1 锦城科技   华东    1200       0.45      A
▶️ 查看代码
company_data %>%
  filter(duplicated(company_data) |
           duplicated(company_data, fromLast = TRUE))   # 把涉及重复的所有行都列出来
   company region revenue debt_ratio rating
1 锦城科技   华东    1200       0.45      A
2 锦城科技   华东    1200       0.45      A

提示

fromLast = TRUE 可以反过来从最后一行往前找,两次结合能把所有涉及重复的行(包括第一次出现的那条)都找出来,方便逐条核查。

3.3 统计每条记录重复了几次——count()

▶️ 查看代码
company_data %>%
  count(company, revenue, sort = TRUE) %>%
  filter(n > 1)    # 按关键字段分组计数,一眼看出谁重复了几次
   company revenue n
1 锦城科技    1200 2
▶️ 查看代码
company_data %>%
  add_count(company) %>%
  filter(n > 1)
   company region revenue debt_ratio rating n
1 锦城科技   华东    1200       0.45      A 2
2 锦城科技   华东    1200       0.45      A 2

重要

财务场景:如果一份几千行的报表,逐行看 duplicated() 的TRUE/FALSE不直观,用 count() 配合 sort = TRUE 能直接把重复次数最多的记录排在最前面。

3.4 【课堂练习 1】

给 company_data 再追加一行完全相同的"天府物流"记录(提示:bind_rows() 把第 3 行原样复制一份接到表尾),用 duplicated() 和 count() 分别验证能否识别出来。

四、重复值的删除——distinct()

最常用的去重工具,比unique()更灵活

4.1 unique()——base R的去重方式

▶️ 查看代码
unique(company_data$company)      # 对向量去重,只保留不同的公司名
 [1] "锦城科技" "蜀汉制造" "天府物流" "青羊金融" "锦江生物" "浣花实业"
 [7] "锦西商贸" "蜀都餐饮" "青城建材" "天府冷链" "锦江农产" "浣花纺织"
[13] "锦西物流" "蜀汉电子" "青羊置业" "天府食品"
▶️ 查看代码
unique(company_data)              # 对整个数据框去重,重复行只保留第一次出现的
    company region revenue debt_ratio rating
1  锦城科技   华东    1200       0.45      A
2  蜀汉制造   西南    1463       0.62      B
3  天府物流   西南    1560       0.38      A
4  青羊金融   华东     760       0.71      C
5  锦江生物   西南    1500       0.55      A
6  浣花实业   华东     892       0.48      B
7  锦西商贸   西南    1340       0.66      B
8  蜀都餐饮   西南     680       0.52      C
9  青城建材   华东     807       0.44      A
10 天府冷链   西南    2100       0.58      A
11 锦江农产   华东     450       0.75      C
12 浣花纺织   华东     980       0.41      B
13 锦西物流   西南    1463       0.63      C
14 蜀汉电子   西南    1720       0.49      A
15 青羊置业   华东     560       0.68      C
16 天府食品   西南    1380       0.57      B

提示

unique() 是base R最基础的去重函数,用于向量或整个数据框都可以,规则同样是"只保留第一次出现"。

4.2 dplyr::distinct()——更常用的去重方式

▶️ 查看代码
company_data %>%
  distinct()                          # 对所有列去重,效果同unique(company_data)
    company region revenue debt_ratio rating
1  锦城科技   华东    1200       0.45      A
2  蜀汉制造   西南    1463       0.62      B
3  天府物流   西南    1560       0.38      A
4  青羊金融   华东     760       0.71      C
5  锦江生物   西南    1500       0.55      A
6  浣花实业   华东     892       0.48      B
7  锦西商贸   西南    1340       0.66      B
8  蜀都餐饮   西南     680       0.52      C
9  青城建材   华东     807       0.44      A
10 天府冷链   西南    2100       0.58      A
11 锦江农产   华东     450       0.75      C
12 浣花纺织   华东     980       0.41      B
13 锦西物流   西南    1463       0.63      C
14 蜀汉电子   西南    1720       0.49      A
15 青羊置业   华东     560       0.68      C
16 天府食品   西南    1380       0.57      B
▶️ 查看代码
company_data %>%
  distinct(company)                   # 只看company列的去重结果
    company
1  锦城科技
2  蜀汉制造
3  天府物流
4  青羊金融
5  锦江生物
6  浣花实业
7  锦西商贸
8  蜀都餐饮
9  青城建材
10 天府冷链
11 锦江农产
12 浣花纺织
13 锦西物流
14 蜀汉电子
15 青羊置业
16 天府食品
▶️ 查看代码
company_data %>%
  distinct(company, .keep_all = TRUE) # 只按company去重,但保留其他列信息
    company region revenue debt_ratio rating
1  锦城科技   华东    1200       0.45      A
2  蜀汉制造   西南    1463       0.62      B
3  天府物流   西南    1560       0.38      A
4  青羊金融   华东     760       0.71      C
5  锦江生物   西南    1500       0.55      A
6  浣花实业   华东     892       0.48      B
7  锦西商贸   西南    1340       0.66      B
8  蜀都餐饮   西南     680       0.52      C
9  青城建材   华东     807       0.44      A
10 天府冷链   西南    2100       0.58      A
11 锦江农产   华东     450       0.75      C
12 浣花纺织   华东     980       0.41      B
13 锦西物流   西南    1463       0.63      C
14 蜀汉电子   西南    1720       0.49      A
15 青羊置业   华东     560       0.68      C
16 天府食品   西南    1380       0.57      B

重要

distinct() 比 unique() 更灵活:可以指定只按某几列判断重复,加上 .keep_all = TRUE 还能保留其他列——财务场景里如果只想保留每家公司最早的一条记录,但仍需要营收等其他字段,这个参数非常实用。

4.3 去重前后的行数对比

▶️ 查看代码
nrow(company_data)                                  # 去重前:17 行
[1] 18
▶️ 查看代码
nrow(distinct(company_data))                        # 去重后:16 行
[1] 16
▶️ 查看代码
nrow(company_data) - nrow(distinct(company_data))   # 一共删除了几条重复记录
[1] 2

提示

去重前后对比行数,是验证去重是否生效、以及量化"重复问题有多严重"的简单有效的方法。

4.4 【课堂练习 2】

用 distinct() 对 company_data 按 company 列去重,同时保留 revenue 列,观察结果和只用 distinct(company) 的区别。

五、部分字段重复——业务主键去重

本课最容易踩坑的地方

5.1 完全重复 vs 部分字段重复

业务上"国家 + 年份"这个组合应该唯一,但合并两套数据库导出时出了岔子(数值为教学虚构,单位:万亿美元):

▶️ 查看代码
gdp_data <- data.frame(
  country = c("中国", "美国", "日本", "德国", "印度",
              "中国", "美国", "日本", "德国", "印度"),
  year    = rep(c(2021, 2022), each = 5),
  gdp     = c(17.6, 23.0, 4.9, 4.3, 3.1,
              18.1, 25.4, 4.2, 4.1, 3.4)
)

# 岔子一:美国 2022 那行被整套导出原样重复了一次(完全重复)
gdp_data <- gdp_data %>%
  bind_rows(gdp_data[7, ])

# 岔子二:日本 2022 两套数据库口径不同,各报了一条(4.2 和 4.4)——
#         整行不一样,但"国家+年份"撞车了(部分字段重复)
gdp_data <- gdp_data %>%
  bind_rows(data.frame(country = "日本", year = 2022, gdp = 4.4))

gdp_data    # 12 行:第 11 行与第 7 行完全相同;第 12 行与第 8 行国家年份相同、GDP 不同
   country year  gdp
1     中国 2021 17.6
2     美国 2021 23.0
3     日本 2021  4.9
4     德国 2021  4.3
5     印度 2021  3.1
6     中国 2022 18.1
7     美国 2022 25.4
8     日本 2022  4.2
9     德国 2022  4.1
10    印度 2022  3.4
11    美国 2022 25.4
12    日本 2022  4.4

5.2 一把尺子量不着:duplicated() 的两种用法

▶️ 查看代码
duplicated(gdp_data)                        # 全部列一起比较:只有第 11 行是 TRUE!
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE
▶️ 查看代码
sum(duplicated(gdp_data))                   # 完全重复仅 1 条
[1] 1
▶️ 查看代码
duplicated(gdp_data[, c("country", "year")]) # 只比较 country 和 year 两列:第 11、12 行都是 TRUE
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE

警告

这是本课最容易踩坑的地方:duplicated(gdp_data) 对全部列判断,日本 2022 因为 GDP 一个是 4.2、一个是 4.4,不会被认为"重复"——但从业务角度看,"日本 + 2022"这个组合本来就该只有一条记录,出现两条说明两套数据库至少有一套是错的(哪怕数值不同)。

5.3 明确"业务主键"再判断重复

▶️ 查看代码
gdp_data %>%
  distinct(country, year)   # 按国家+年份去重,只保留第一条:12 → 10 行
   country year
1     中国 2021
2     美国 2021
3     日本 2021
4     德国 2021
5     印度 2021
6     中国 2022
7     美国 2022
8     日本 2022
9     德国 2022
10    印度 2022
▶️ 查看代码
gdp_data %>%
  count(country, year) %>%
  filter(n > 1)             # 找出哪些"国家+年份"组合出现了不止一次
  country year n
1    日本 2022 2
2    美国 2022 2

重要

经济面板数据中,"国家 + 年份"(或"公司 + 季度")这样的组合通常应该是唯一的(业务主键)——判断重复不能只看"整行是否一样",更要弄清楚哪几列组合起来应该唯一,再针对这几列判断。注意上面 distinct(country, year) 已经是按主键删行,日本那条 4.4 被悄悄丢掉了——主键去重前必须先核查(见 5.4),不能直接当清理手段用。

5.4 发现部分字段重复后怎么办

▶️ 查看代码
gdp_data %>%
  group_by(country, year) %>%
  filter(n() > 1)           # 把涉及重复的组合的**所有**记录都列出来,方便人工核查
# A tibble: 4 × 3
# Groups:   country, year [2]
  country  year   gdp
  <chr>   <dbl> <dbl>
1 美国     2022  25.4
2 日本     2022   4.2
3 美国     2022  25.4
4 日本     2022   4.4
▶️ 查看代码
gdp_data %>%
  distinct(country, year, .keep_all = TRUE)   # 简单处理:只保留第一条(需先核实再用)
   country year  gdp
1     中国 2021 17.6
2     美国 2021 23.0
3     日本 2021  4.9
4     德国 2021  4.3
5     印度 2021  3.1
6     中国 2022 18.1
7     美国 2022 25.4
8     日本 2022  4.2
9     德国 2022  4.1
10    印度 2022  3.4

警告

发现"日本 + 2022"重复后,不能想当然地直接删掉一条了事——GDP 不同(4.2 vs 4.4)说明这两条记录里至少有一条是错的,或者只是两套数据库的换算口径不同,需要先核查数据来源,确认后再决定保留哪一条、还是需要人工修正后合并。这和第 12 讲"先判断是错还是真,再选处理方法"是同一个思路。

5.5 【课堂练习 3】

再给 gdp_data 添加一条"德国+2021"、GDP 为 4.35 的记录(与已有的 4.3 不同),用 count() 找出所有重复的"国家+年份"组合——现在应该有几组?

六、综合实战——识别→处理→验证

区分完全重复与部分字段重复,是本课的核心能力

6.1 动手实战

对本讲的两个示例数据(2.1 的 company_data、5.1 现造的 gdp_data),走一遍完整流程:

  1. 识别——在 company_data(17 行)里找出完全重复的记录,说出重复的是哪一行;
  2. 区分——在 gdp_data(12 行)里找出"国家+年份"重复但 GDP 不同的记录,判断它属于哪种重复;
  3. 处理——完全重复用 distinct() 直接去重;部分字段重复先列出来核查,再决定如何处理并写清理由;
  4. 验证——用 nrow() 对比处理前后的行数,再用 sum(revenue) 确认总营收回到真实值、用 count() 确认业务主键不再有重复。

6.2 参考框架

6.3 课堂展示

邀请1-2名同学展示自己的处理流程,并说明"为什么这样选择"——尤其是遇到部分字段重复时是怎么核查、怎么决定的。

七、课堂总结、学习成果确认与Exit Ticket

整行相同只是重复的一种形态

7.1 知识回顾

识别(duplicated()/count()) → 区分完全重复与部分字段(业务主键)重复 → 处理(distinct()) → 验证(对比行数)

重要

这是重复值处理的标准流程,其中"区分"这一步是本课新增的关键环节——它提醒我们,"重复"不只是"整行完全一样"这一种情况。三节课连起来看,项目三的流水线是:补缺失 → 修异常 → 去重复,每一步处理完都要再验证一遍。

7.2 学习成果自查

对照一下,今天这几件事你都做到了吗?

  • ✓ 能够用 duplicated() 识别重复行,用 sum() 统计重复行数量
  • ✓ 能够用 distinct()(含 .keep_all 参数)删除重复记录
  • ✓ 能够区分"完全重复"和"部分字段(业务主键)重复"
  • ✓ 能够用 count() 找出发生部分字段重复的记录组合
  • ✓ 能够对处理前后的行数进行对比验证

提示

如果有没打勾的,下课后一定要找同学或老师补上——下节课"字符串规范化"是项目三的最后一课。

7.3 Exit Ticket —— 写给自己的三个问题

请花1分钟,在纸条或在线问卷上简要回答:

  1. duplicated() 和 distinct() 分别解决什么问题?
  2. 为什么"整行是否完全一样"不能作为判断重复的唯一标准?
  3. 发现"国家+年份"这样的组合重复后,为什么不能直接删掉一条了事?

课后作业

重复值处理综合练习

建议使用Quarto(.qmd)完成并Render生成Word文档,保留代码与结果:

  1. 用R代码生成一份至少6家公司的虚构财务数据,人为构造1-2条完全重复记录,以及1-2条部分字段(如公司+季度)重复但其他字段不同的记录
  2. 用 duplicated() 和 count() 识别重复情况,并区分"完全重复"与"部分字段重复"
  3. 针对不同类型的重复,分别选择合适的方法处理,并说明选择理由
  4. 处理完成后,用 nrow() 对比处理前后的行数,验证重复值是否已处理干净

提交要求

提交.qmd源文件以及Render生成的Word文档,命名为"学号_姓名_第13课作业"。本次作业计入平时成绩中的"实验报告"部分考核,请确保代码可运行、结果完整。

下讲预告

字符串规范化——stringr包实战(项目三收官课)

  • 空格与大小写规范化——str_trim()/str_squish()/str_to_upper/lower()
  • 字符串匹配与替换——str_detect()/str_replace()
  • 字符串拆分与拼接——str_split()/separate()/str_c()/str_glue()

提示

课前准备:保留好今天处理重复值后的数据,下节课是项目三"数据清洗"的最后一课,完成后就要进入项目四"数据可视化"了。

谢谢!

第13讲:重复值处理

"One transaction, one record — never twice." 一笔业务不能重复入账、不能重复报销;发现重复不能"想当然地随手删",需要先核实数据来源,这与财务工作中"先核实、后处理"的审慎原则是一致的。

朱 奇 | 锦城大学 · 财会学院