Code
setwd("F:/RuiBlog/posts/R/使用tidyverse清洗CFPS数据")
library(tidyverse)
<- "./data/operated_data/2018famecon.sav"
file_path_famecon_2018 <- "./data/operated_data/2018person.sav" file_path_person_2018
Rui
September 24, 2022
在上一节中,我们从 CFPS 数据繁多的变量中剔除了大量无关变量,初步筛选出了对经济研究相关的变量。这一节主要根据一篇论文选出特定的一些变量(具体内容请见上一节),并按照家庭为主,个人为辅的思路,将同一年份的家庭表与个人表配对并合并。在此之前,我们先做 3 个符合逻辑与常识的假设:
假设 1:同一年份中,每个家庭只能被调查一次。
假设 2:同一年份中,每个个人只能被调查一次。
假设 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 表示不同城市 |
具体说明请见前一节内容。
以 2018 年的数据为例,先提取家庭表中的目标变量:
## 处理家庭表
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()) # 对列排序
再提取个人表中的目标变量:
## 处理个人表
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())
以上家庭表和个人表中的目标变量提取完毕。但数据仍然分散在两张表中,接下来需要将两张表中的信息按照一定规则匹配并整合。
“键”用于连接每对数据表的变量,是能够唯一标识观测的变量(或变量的集合)。
键的类型有两种:
主键: 唯一标识其所在数据表中的观测(记录)。例如,fid 是一个主键,因为其可以唯一标识家庭表中的每个家庭(理想情况下是这样,不排除异常情况的出现)。
外键: 唯一标识另外一个数据表中的观测(记录)。例如,家庭表中的 pid 就是一个外键,因为他是个人表的一个主键,可以与个人表中的每个个人唯一匹配。
按照如上定义,我们假设:对于家庭表来说,fid16 是主键,pid 是外键;对于个人表来说,pid 是主键,fid16 是外键。但真实情况真的如此吗?我们需要验证:
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 这两个变量联合起来能形成主键吗?
## 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。
# 在拼接前,应该剔除在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 是否为主键:
剔除违反假设的个例:
再次检验 id 是否为主键:
检验通过!2016 年数据也可以按以上方法处理。
在没有主键的情况下,通过一些方法构造出来的、起到主键功能的键成为代理键。本例中,id 变量就是构造出来的代理键。
采取内连接的方式,使用代理键将个人表和家庭表匹配并合并。
2016 年数据也可以按以上方法处理。
除了内连接外,还有左连接、右连接和外连接。更多关于数据连接的学习资料参见 R for Data Science。
---
title: "使用 Tidyverse 清洗 CFPS 数据(二)"
author: "Rui"
date: "2022-09-24"
categories: [R, SPSS, 数据处理, 一些尝试, tidyverse]
image: "lemonade.jpg"
format:
html:
code-fold: true
code-tools: true
---
```{r setup, include = FALSE}
# 设置默认参数
knitr::opts_chunk$set(
echo = TRUE,
message = FALSE,
warning = FALSE,
collapse = TRUE
)
```
# 合并家庭表与个人表
在上一节中,我们从 CFPS 数据繁多的变量中剔除了大量无关变量,初步筛选出了对经济研究相关的变量。这一节主要根据一篇论文选出特定的一些变量(具体内容请见上一节),并按照**家庭为主,个人为辅**的思路,将同一年份的家庭表与个人表配对并合并。在此之前,我们先做 3 个符合逻辑与常识的假设:
- 假设 1:同一年份中,每个家庭只能被调查一次。
- 假设 2:同一年份中,每个个人只能被调查一次。
- 假设 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 表示不同省份 |
具体说明请见前一节内容。
## 导入数据以及提取目标变量
```{r}
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 年的数据为例,先提取家庭表中的目标变量:
```{r}
## 处理家庭表
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(expense18 = expense,
family_asset18 = family_asset,
family_debts18 = family_debts,
fincome18 = fincome1,
pid = resp4pid,
fml18 = fml_count) %>% # 重命名列名
select(fid16, pid, everything()) # 对列排序
```
再提取个人表中的目标变量:
```{r}
## 处理个人表
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())
```
以上家庭表和个人表中的目标变量提取完毕。但数据仍然分散在两张表中,接下来需要将两张表中的信息按照一定规则匹配并整合。
::: callout-note
2016 年的数据也是按照以上方法处理。但是请注意:同一变量在不同年份的表中的名称不一定相同,可以结合 SPSS 的变量视图进行比对。
:::
## 探索主键
> “键”用于连接每对数据表的变量,是能够唯一标识观测的变量(或变量的集合)。
键的类型有两种:
- **主键:** 唯一标识其所在数据表中的观测(记录)。例如,fid 是一个主键,因为其可以唯一标识家庭表中的每个家庭(理想情况下是这样,不排除异常情况的出现)。
- **外键:** 唯一标识另外一个数据表中的观测(记录)。例如,家庭表中的 pid 就是一个外键,因为他是个人表的一个主键,可以与个人表中的每个个人唯一匹配。
::: callout-note
一个变量既可以是主键,也可以是外键。
:::
按照如上定义,我们假设:对于家庭表来说,fid16 是主键,pid 是外键;对于个人表来说,pid 是主键,fid16 是外键。但真实情况真的如此吗?我们需要验证:
```{r}
df_famecon_2018 %>%
count(fid16) %>%
filter(n > 1) %>%
nrow() # fid16不是df_famecon_2018的主键
df_famecon_2018 %>%
count(pid) %>%
filter(n > 1) %>%
nrow() # pid不是df_famecon_2018的主键(但是df_person_2018的外键)
df_person_2018 %>%
count(fid16) %>%
filter(n > 1) %>%
nrow() # fid16不是df_famecon_2018的主键
df_person_2018 %>%
count(pid) %>%
filter(n > 1) %>%
nrow() # pid是df_famecon_2018的主键
```
好像找不到主键?再试试将 fid16 和 pid 这两个变量联合起来能形成主键吗?
```{r}
## fid16与pid联合起来可以成为主键吗?
df_famecon_2018 %>%
count(fid16, pid) %>%
filter(n > 1) %>%
nrow() # 不是主键
df_person_2018 %>%
count(fid16, pid) %>%
filter(n > 1) %>%
nrow() # 是主键
```
可以发现这种办法只对个人表有效。难道是我们的假设不对吗?仔细观察发现:与假设相违背的情况都只是个例。
所以有一个很自然的想法,那就是:将违背假设的个例作为异常予以剔除,那么 fid16 和 pid 这两个变量联合起来就可以形成主键。进一步,可以将 fid16 和 pid 转换为字符串并拼接,从而取代 fid16 和 pid 成为新的主键。不过在此之前,为了避免无用功,需要将在 fid16 或 pid 变量上缺失或者异常的观测(记录)剔除。
## 构造代理键
为了同时考虑 fid16 和 pid,需要将两者拼接得到新的变量 id。注意在拼接之前需要剔除在 fid16 或 pid 上缺失或是异常的观测(记录),因为两者中任意一个缺失都会导致个人无法被唯一识别。
```{r}
# 在拼接前,应该剔除在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 是否为主键:
```{r}
# 检查是否是主键
df_famecon_2018 %>%
count(id) %>%
filter(n > 1) # 不是主键
```
剔除违反假设的个例:
```{r}
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 是否为主键:
```{r}
df_person_2018 %>%
count(id) %>%
filter(n > 1) # 是主键
```
检验通过!2016 年数据也可以按以上方法处理。
在没有主键的情况下,通过一些方法构造出来的、起到主键功能的键成为代理键。本例中,id 变量就是构造出来的代理键。
## 个人表与家庭表合并
采取内连接的方式,使用代理键将个人表和家庭表匹配并合并。
```{r}
df_2018 <- df_famecon_2018 %>%
inner_join(df_person_2018, by = "id") # 相当于取交集
```
2016 年数据也可以按以上方法处理。
除了内连接外,还有左连接、右连接和外连接。更多关于数据连接的学习资料参见 [*R for Data Science*](https://r4ds.hadley.nz/)。
## 保存数据
```{r}
haven::write_sav(df_2018, "./data/operated_data/data_2018.sav")
#df_2018 %>% knitr::kable(head(10))
```