使用 Tidyverse 清洗 CFPS 数据(二)

R
SPSS
数据处理
一些尝试
tidyverse
Author

Rui

Published

September 24, 2022

在上一节中,我们从 CFPS 数据繁多的变量中剔除了大量无关变量,初步筛选出了对经济研究相关的变量。这一节主要根据一篇论文选出特定的一些变量(具体内容请见上一节),并按照家庭为主,个人为辅的思路,将同一年份的家庭表与个人表配对并合并。在此之前,我们先做 3 个符合逻辑与常识的假设:

2 个任务:

目标变量:

变量名称 含义
\(exp_i\) 第 i 个家庭的家庭总支出
\(lev_i\) 第 i 个家庭的杠杆率,杠杆率为家庭总负债除以家庭总资产
\(inc_i\) 第 i 个家庭的家庭纯收入
\(asset_i\) 第 i 个家庭的家庭总资产
\(X_i^a\) 第 i 个家庭的家庭人口数量
\(X_i^b\) 第 i 个家庭的家庭特征,包括户主年龄、年龄的平方、性别、婚姻状况、健康状况等
\(\mu_p\) 省份固定效应,是一系列虚拟变量, p 表示不同城市

具体说明请见前一节内容。

导入数据以及提取目标变量

Code
setwd("F:/RuiBlog/posts/R/使用tidyverse清洗CFPS数据")
library(tidyverse)

file_path_famecon_2018 <- "./data/operated_data/2018famecon.sav"
file_path_person_2018 <- "./data/operated_data/2018person.sav"

以 2018 年的数据为例,先提取家庭表中的目标变量:

Code
## 处理家庭表
selected_cols_famecon_2018 <- c("fid16", "expense", 
                                "house_debts", "nonhousing_debts",
                                "durables_asset", "finance_asset", 
                                "fixed_asset", "houseasset_gross", "land_asset",
                                "fincome1", "resp4pid", "fml_count", 
                                "urban18", "provcd18")
# 家庭编号2018年、家庭总支出、家庭总负债、家庭总资产、家庭纯收入、户主编号、家庭人口、省编号
# 生活支出方面做出实际回答人的为户主

df_famecon_2018 <- haven::read_sav(file_path_famecon_2018, col_select = selected_cols_famecon_2018) %>%
  as_tibble() %>%
  mutate(family_debts = house_debts + nonhousing_debts,
         family_asset = durables_asset + finance_asset + fixed_asset + houseasset_gross + land_asset) %>%
  select(-c("house_debts", "nonhousing_debts", "durables_asset", 
            "finance_asset", "fixed_asset", "houseasset_gross", "land_asset")) %>%
  rename(expense16 = expense, 
         family_asset16 = family_asset,
         family_debts16 = family_debts,
         fincome16 = fincome1,
         pid = resp4pid,
         fml18 = fml_count) %>% # 重命名列名
  select(fid16, pid, everything()) # 对列排序

再提取个人表中的目标变量:

Code
## 处理个人表
selected_cols_person_2018 <- c("pid", "fid16", "age", "qa002", "qea0", "kz202")
df_person_2018 <- haven::read_sav(file_path_person_2018, col_select = selected_cols_person_2018) %>%
  as_tibble() %>%
  rename(gender = qa002,
         marriage = qea0,
         health = kz202) %>%
  mutate(age2 = age^2) %>%
  select(fid16, pid, age, age2, everything())

以上家庭表和个人表中的目标变量提取完毕。但数据仍然分散在两张表中,接下来需要将两张表中的信息按照一定规则匹配并整合。

Note

2016 年的数据也是按照以上方法处理。但是请注意:同一变量在不同年份的表中的名称不一定相同,可以结合 SPSS 的变量视图进行比对。

探索主键

“键”用于连接每对数据表的变量,是能够唯一标识观测的变量(或变量的集合)。

键的类型有两种:

  • 主键: 唯一标识其所在数据表中的观测(记录)。例如,fid 是一个主键,因为其可以唯一标识家庭表中的每个家庭(理想情况下是这样,不排除异常情况的出现)。

  • 外键: 唯一标识另外一个数据表中的观测(记录)。例如,家庭表中的 pid 就是一个外键,因为他是个人表的一个主键,可以与个人表中的每个个人唯一匹配。

Note

一个变量既可以是主键,也可以是外键。

按照如上定义,我们假设:对于家庭表来说,fid16 是主键,pid 是外键;对于个人表来说,pid 是主键,fid16 是外键。但真实情况真的如此吗?我们需要验证:

Code
df_famecon_2018 %>%
  count(fid16) %>%
  filter(n > 1) # fid16不是df_famecon_2018的主键
## # A tibble: 1,085 × 2
##        fid16     n
##    <dbl+lbl> <int>
##  1    106561     2
##  2    106945     2
##  3    109121     2
##  4    109216     2
##  5    110020     2
##  6    120008     2
##  7    120046     2
##  8    120051     2
##  9    120056     2
## 10    120114     2
## # … with 1,075 more rows

df_famecon_2018 %>%
  count(pid) %>%
  filter(n > 1) # pid不是df_famecon_2018的主键(但是df_person_2018的外键)
## # A tibble: 25 × 2
##                   pid     n
##             <dbl+lbl> <int>
##  1        -8 [不适用]  1335
##  2 130379105              2
##  3 130830101              2
##  4 130900101              2
##  5 140093103              2
##  6 140209431              2
##  7 210751101              2
##  8 232975551              2
##  9 360310101              2
## 10 411338102              2
## # … with 15 more rows

df_person_2018 %>%
  count(fid16) %>%
  filter(n > 1) # fid16不是df_famecon_2018的主键
## # A tibble: 10,635 × 2
##              fid16     n
##          <dbl+lbl> <int>
##  1     -8 [不适用]   707
##  2 100051              3
##  3 100376              2
##  4 100453              3
##  5 100551              2
##  6 100569              2
##  7 101023              3
##  8 101129              4
##  9 101274              2
## 10 102571              2
## # … with 10,625 more rows

df_person_2018 %>%
  count(pid) %>%
  filter(n > 1) # pid是df_famecon_2018的主键
## # A tibble: 0 × 2
## # … with 2 variables: pid <dbl+lbl>, n <int>

好像找不到主键?再试试将 fid16 和 pid 这两个变量联合起来能形成主键吗?

Code
## fid16与pid联合起来可以成为主键吗?
df_famecon_2018 %>%
  count(fid16, pid) %>%
  filter(n > 1) # 不是主键
## # A tibble: 34 × 3
##        fid16                pid     n
##    <dbl+lbl>          <dbl+lbl> <int>
##  1    106561        -8 [不适用]     2
##  2    130379 130379105              2
##  3    130727        -8 [不适用]     2
##  4    140229        -8 [不适用]     2
##  5    140573        -8 [不适用]     2
##  6    169532        -8 [不适用]     2
##  7    199193        -8 [不适用]     2
##  8    205873        -8 [不适用]     2
##  9    211525        -8 [不适用]     2
## 10    220022        -8 [不适用]     2
## # … with 24 more rows

df_person_2018 %>%
  count(fid16, pid) %>%
  filter(n > 1) # 是主键
## # A tibble: 0 × 3
## # … with 3 variables: fid16 <dbl+lbl>, pid <dbl+lbl>, n <int>

可以发现这种办法只对个人表有效。难道是我们的假设不对吗?仔细观察发现:与假设相违背的情况都只是个例。 所以有一个很自然的想法,那就是:将违背假设的个例作为异常予以剔除,那么 fid16 和 pid 这两个变量联合起来就可以形成主键。进一步,可以将 fid16 和 pid 转换为字符串并拼接,从而取代 fid16 和 pid 成为新的主键。不过在此之前,为了避免无用功,需要将在 fid16 或 pid 变量上缺失或者异常的观测(记录)剔除。

构造代理键

为了同时考虑 fid16 和 pid,需要将两者拼接。新的变量名称为 id。

Code
# 在拼接前,应该剔除在fid16或pid上缺失或异常的记录
library(magrittr) # 为了使用 %<>%

# 过滤数据
df_famecon_2018 %<>% filter(fid16 > 0 & pid > 0) # 剔除缺失和异常的观测
df_person_2018 %<>% filter(fid16 > 0 & pid > 0) # 剔除缺失和异常的观测

# 转换为字符串
fid <- df_famecon_2018[["fid16"]] %>% as.character()
pid <- df_famecon_2018[["pid"]] %>% as.character()
id <- paste(fid, pid, sep = "_")
df_famecon_2018 %<>% mutate(id = id) %>% 
  select(-pid) %>% # 删除pid,但应该保留fid16以为之后合并2016、2018数据使用
  select(id, everything()) # 家庭数据

fid <- df_person_2018[["fid16"]] %>% as.character()
pid <- df_person_2018[["pid"]] %>% as.character()
id <- paste(fid, pid, sep = "_")
df_person_2018 %<>% mutate(id = id) %>% 
  select(-pid) %>% # 删除pid,但应该保留fid16以为之后合并2016、2018数据使用
  select(id, everything()) # 个人数据

检验 id 是否为主键:

Code
# 检查是否是主键
df_famecon_2018 %>%
  count(id) %>%
  filter(n > 1) # 不是主键
## # A tibble: 2 × 2
##   id                   n
##   <chr>            <int>
## 1 130379_130379105     2
## 2 510528_510528105     2

剔除违反假设的个例:

Code
temp <- df_famecon_2018 %>% 
  count(id) %>% 
  filter(n == 1) %>%
  magrittr::extract2(1) # 获得正常记录的id
df_famecon_2018 <- df_famecon_2018[df_famecon_2018$id %in% temp, ] # 提取出了正常的观测

再次检验 id 是否为主键:

Code
df_person_2018 %>%
  count(id) %>%
  filter(n > 1) # 是主键
## # A tibble: 0 × 2
## # … with 2 variables: id <chr>, n <int>

检验通过!2016 年数据也可以按以上方法处理。

在没有主键的情况下,通过一些方法构造出来的、起到主键功能的键成为代理键。本例中,id 变量就是构造出来的代理键。

个人表与家庭表合并

采取内连接的方式,使用代理键将个人表和家庭表匹配并合并。

Code
df_2018 <- df_famecon_2018 %>% 
  inner_join(df_person_2018, by = "id") # 相当于取交集

2016 年数据也可以按以上方法处理。

除了内连接外,还有左连接、右连接和外连接。更多关于数据连接的学习资料参见 R for Data Science

保存数据

Code
haven::write_sav(df_2018, "./data/operated_data/data_2018.sav")

#df_2018 %>% knitr::kable(head(10))