简介:面向使用CHARLS数据库开展研究的学者与分析人员,这份项目源码包聚焦数据清洗、拼接与整理环节,以甘油三酯葡萄糖指数与新发糖尿病关系研究为实例,演示从数据下载到初步整理的全流程操作。压缩包共8个文件,大小仅12KB,包含R语言数据清洗与分析脚本、样例数据CSV、Markdown说明文档及HTML预览等,代码量超过100行,可直接运行或按需修改;其中R语言脚本按功能拆分,样例数据便于即时测试,文档则提供操作步骤说明,资源结构简洁,覆盖从原始数据到可分析数据集的完整处理链条。已有349人学习下载,适合希望提升CHARLS数据处理效率、减少重复踩坑的初学者和进阶研究者。通过跟随源码操作,读者不仅能掌握数据清洗的关键步骤与排错思路,还能为后续开展cox回归、分位数回归等多模型比较分析奠定扎实的数据基础。
1. 项目概述:CHARLS数据清洗到底在做什么
1.1 核心需求解析
CHARLS(中国健康与养老追踪调查)是国内社科研究里用得最多的微观数据库之一,覆盖全国150个县、450个社区(村),样本量超过1.9万人。数据维度涵盖个人基本信息、家庭结构、健康状况、医疗服务使用、工作退休、收入消费、资产等多个模块。但用过的人都知道,CHARLS原始数据看起来特别“劝退”——变量命名规则复杂、缺失值处理标准不统一、跨年跨模块匹配困难、样本筛选逻辑繁琐。如果你刚拿到数据不知道从哪下手,或者被一堆Stata代码搞得头皮发麻,那这个基于Python pandas的清洗项目源码就是给你准备的。
这个项目的核心价值在于:用一套清晰的pandas数据清洗流程,把CHARLS原始数据从“能用但难用”变成“直接可分析”的结构化数据。它解决的痛点非常具体——变量重命名、缺失值编码识别、样本筛选、多模块数据合并、异常值处理。适合的人群包括:正在写毕业论文的社科类研究生、做政策评估的研究人员、以及所有想用Python处理CHARLS数据但缺乏现成参考的入门者。
1.2 为什么选择pandas而非Stata
社科领域传统上习惯用Stata处理CHARLS数据,毕竟CHARLS官方提供的代码和问卷编码都以Stata格式为主。但我在实际项目中逐渐转向Python+pandas,原因很实在:
- pandas的数据处理语法更直观,链式操作(method chaining)让清洗逻辑一目了然,调试效率比Stata高不少
- 与机器学习、可视化生态无缝衔接,清洗完可以直接用sklearn建模、用matplotlib出图,省去跨软件转换的麻烦
- pandas对缺失值、重复值、文本型变量的处理能力更强,尤其适合CHARLS这种数据量大、结构复杂的微观调查数据
- 免费开源,不需要授权,团队协作时没有软件版权问题
当然,Stata在计量经济学分析上有不可替代的优势,但清洗阶段用pandas完全够用,甚至更顺手。清洗完成后导出为csv或dta格式,再回到Stata做回归分析,这个组合拳是我目前最推荐的工作流。
2. 数据清洗整体设计思路
2.1 清洗流程的五个核心阶段
拿到CHARLS数据后,我习惯把清洗过程拆成五个阶段,每个阶段有明确的目标和输出物,这样既不会漏掉关键步骤,也方便复查。
第一个阶段是数据加载与初步探查。这步看似简单但最容易翻车。CHARLS提供的原始数据通常是dta格式,需要用pandas的read_stata函数读取,但要注意编码问题和版本兼容性。读取之后先用shape、dtypes、head()快速摸底,确认数据规模和变量类型是否符合预期。
第二个阶段是变量标准化处理。CHARLS的变量命名有其内在规律,比如前缀代表模块(如da代表 demographics 模块,hc代表 health care 模块),但不同年份的同一变量名可能不一致,同一概念在不同模块中可能有多个近似变量。这个阶段要做的是建立变量映射表,把需要使用的变量统一重命名,方便后续分析。
第三个阶段是缺失值与异常值处理。这是清洗工作的重头戏。CHARLS的缺失值编码有特殊规则,比如-1表示不知道、-2表示拒绝回答、-3表示不适用、-8表示其他,这些在官方问卷说明里有明确规定,但在数据分析时必须统一处理,否则会把负值当成有效数值参与计算。
第四个阶段是样本筛选与子集提取。根据研究设计,可能只需要特定年龄段的样本、特定省份的样本、或者特定健康状况的样本。这个阶段涉及复杂的条件筛选逻辑,也是pandas布尔索引发挥最大优势的地方。
第五个阶段是数据合并与输出。CHARLS数据按模块分散在不同文件中,需要根据个体ID(ID变量)进行横向合并,或者根据年份进行纵向拼接。合并时要特别注意一对多、多对一的关系,避免产生笛卡尔积导致数据膨胀。
2.2 方案选型的三个关键考量
在方案设计上,我做了几个关键决策,这里说明原因供大家参考。
第一,全程使用相对路径和配置文件管理数据路径。CHARLS原始数据动辄几个GB,如果代码里写死绝对路径,换台电脑就得改代码。我采用的方式是创建一个config.py文件,统一管理原始数据路径、中间数据路径和输出数据路径,清洗主脚本只负责调逻辑,不关心数据存在哪。
第二,清洗代码按模块化组织,而不是写成一个巨长的脚本。每个模块(如人口学变量清洗、健康变量清洗)单独成一个函数或一个.py文件,主脚本只做调用。这样做的直接好处是,当某个变量的清洗逻辑需要调整时,不用在几百行的脚本里大海捞针。
第三,中间数据格式统一使用parquet。清洗过程中会产生多个中间数据集,第一次我用csv保存,结果发现读取速度慢且占用空间大,后来改用parquet格式,读取速度快了将近10倍,还自动保留了数据类型信息。这里多说一句,如果你的数据量不大,用csv也没问题,但CHARLS这种体量,parquet是更优解。
2.3 变量映射表的构建方法
变量映射是整个清洗流程的灵魂。我从CHARLS 2011、2013、2015、2018四年的问卷中梳理出常用的核心变量,建立了一张映射表,结构如下:
| 新变量名 | 原变量名(2011) | 原变量名(2013) | 原变量名(2015) | 原变量名(2018) | 说明 |
|---|---|---|---|---|---|
| id | ID | ID | ID | ID | 个体唯一标识 |
| age | ba002_1 | ba002_1 | ba002_1 | ba002_1 | 年龄 |
| gender | ba000_2 | ba000_2 | ba000_2 | ba000_2 | 性别 |
| edu | bd001 | bd001 | bd001 | bd001 | 教育程度 |
| hukou | bd004 | bd004 | bd004 | bd004 | 户口类型 |
| marry | be001 | be001 | be001 | be001 | 婚姻状况 |
| chronic | da005 | da005 | da005 | da005 | 慢性病数量 |
这里要特别注意,虽然多数核心变量的编码在不同年份是保持一致的,但有些变量在2015年后做了调整,比如部分健康相关变量的选项编码从1-5改成了0-4。所以在建立映射表时,一定对照各年份的问卷编码手册逐一核实,宁可多花两个小时,也不要凭经验想当然。
3. 核心细节解析与实操要点
3.1 CHARLS数据编码规则详解
CHARLS的缺失值编码是新手最容易踩坑的地方。和其他数据库用NA或NaN表示缺失不同,CHARLS用负数编码了不同类型的“非有效回答”,具体规则如下:
- -1:不知道(Don't Know)
- -2:拒绝回答(Refuse)
- -3:不适用(Not Applicable)
- -8:其他(Other)
- -9:未回答(Missing)
这意味着如果你直接读入数据后不加处理就做描述性统计,会出现大量负值,比如年龄变量的均值可能被这种情况拉低。更麻烦的是,有些负值编码在不同模块间并不完全统一,比如个别模块用-4表示“不适用”,用-5表示“无法回答”。
我的处理策略是:在读入数据后第一时间定义一个函数,把各模块中出现的负值统一替换为pd.NA。这里不推荐直接替换为NaN,因为pandas的NaN在数值运算中会被跳过,而pd.NA在布尔运算中保留“未知”语义,这在后续构建筛选条件时会更安全。
import pandas as pd import numpy as np def clean_missing(df, negative_values=[-1, -2, -3, -8, -9]): """ 将CHARLS数据中的负值缺失编码统一替换为pd.NA """ df_clean = df.replace(negative_values, pd.NA) return df_clean3.2 数据类型转换的细节经验
CHARLS原始数据中很多数值型变量被读成了浮点数或对象类型,这会导致后续分组计算和绘图时的各种问题。我常用的类型转换规则如下:
- 年龄、收入、医疗支出等连续变量:统一转为float
- 性别、婚姻状况、教育程度等分类变量:保留为object或转为category类型
- 日期类变量:转为datetime类型
- ID类变量:保留为object类型(因为有些ID包含数字和字母组合,转成int会报错)
这里有个容易忽略的坑:CHARLS的ID变量在个别年份被读入时会出现精度丢失问题。如果你直接使用read_stata读取,ID被自动转为int64,但CHARLS的ID位数较长,在转换过程中可能会丢失末位精度。我的做法是将ID变量强制转为object类型:
df['ID'] = df['ID'].astype(str).str.zfill(8)用zfill补足位数是为了保证不同年份的ID长度一致,方便后续合并。
3.3 跨年份变量一致性处理
如果你需要合并多年的CHARLS数据做面板分析,变量一致性就是绕不开的难题。我的经验是:不要试图一次性处理所有年份的所有变量,而是先确定分析需要的核心变量清单,然后逐年提取,最后统一合并。
具体步骤如下:
- 确定核心变量清单,列出变量名、所属模块、年份
- 逐年读取数据,按变量清单筛选列
- 对每年数据进行相同的清洗操作(缺失值替换、类型转换、编码映射)
- 用pd.concat进行纵向合并,设置keys参数标记年份
- 检查合并后各变量的缺失率变化,确认没有系统性问题
比如做健康与收入关系研究时,核心变量可能包括:年龄、性别、教育、收入、自评健康、慢性病数量、医疗支出、医保类型。这些变量分布在人口学模块、健康模块、收入模块中,需要分别提取后按ID合并。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
要跑通这套清洗流程,需要一个标准的Python数据科学环境。我的推荐配置如下:
pip install pandas numpy pyarrow statsmodelspandas和numpy是数据处理的基础,pyarrow是parquet格式支持所必需的,statsmodels在清洗完成后做简单的描述统计和回归检查时很有用。如果你是用Anaconda发行版,这些库大部分已经预装,只需额外安装pyarrow。
我在实际项目中用的是Python 3.10 + pandas 2.0.3的组合,运行稳定没有发现兼容性问题。如果你还在用pandas 1.x,建议升级,因为pandas 2.0在性能上有明显提升,尤其是对大数据集的内存占用和运算速度优化明显。
4.2 数据加载与初步探查的现场记录
以CHARLS 2018年数据为例,原始数据文件结构大致如下:
/data /CHARLS2018 Demographics.dta Health_Care.dta Income.dta ...读取一个人口学模块的数据,我做如下处理:
import pandas as pd # 读取数据 df_demo = pd.read_stata('./data/CHARLS2018/Demographics.dta') # 初步探查 print(f'数据维度: {df_demo.shape}') print(f'变量列表: {df_demo.columns.tolist()[:20]}') print(f'数据类型:\n{df_demo.dtypes.value_counts()}') # 缺失值探查 missing_df = df_demo.isna().sum().sort_values(ascending=False) print(f'缺失值最多的前10个变量:\n{missing_df.head(10)}')这一步最重要的产出是确认数据读取没有出现乱码、列数是否正确、是否存在“隐形缺失值”(即负值编码)。我通常会紧接着做一步负值检查:
# 检查是否存在负值缺失编码 negative_counts = (df_demo.select_dtypes(include=[np.number]) < 0).sum() print(f'含负值的变量数量: {(negative_counts > 0).sum()}')如果这个数量很多,说明确实需要统一的缺失值处理步骤;如果数量很少,可能是个别变量的正常负值(比如净资产为负),需要逐一确认。
4.3 核心清洗函数的设计与实现
我在项目中封装了一个清洗类,把常用的操作统一管理起来,这里展示核心部分:
class CHARLSCleaner: """ CHARLS数据清洗类 """ def __init__(self, df, year): self.df = df self.year = year self.numeric_cols = [] self.category_cols = [] def clean_missing(self, negative_values=[-1, -2, -3, -8, -9]): """统一处理负值缺失编码""" self.df = self.df.replace(negative_values, pd.NA) return self def convert_types(self): """类型转换""" for col in self.df.columns: if col.startswith('ID'): self.df[col] = self.df[col].astype(str) elif self.df[col].dtype == 'object': # 尝试转为数值型 try: self.df[col] = pd.to_numeric(self.df[col], errors='ignore') except: pass return self def rename_vars(self, mapping): """变量重命名""" self.df = self.df.rename(columns=mapping) return self def filter_samples(self, conditions): """样本筛选""" mask = pd.Series(True, index=self.df.index) for col, condition in conditions.items(): mask &= condition(self.df[col]) self.df = self.df[mask] return self def save(self, path): """保存为parquet格式""" self.df.to_parquet(path, index=False) return self这个类设计的关键点在于每个方法都返回self,支持链式调用,可以让清洗流程读起来像自然语言:
cleaner = CHARLSCleaner(df_demo, year=2018) cleaner.clean_missing() \ .convert_types() \ .rename_vars(var_mapping) \ .filter_samples({ 'age': lambda x: (x >= 45) & (x <= 95), 'gender': lambda x: x.isin([1, 2]) }) \ .save('./data/processed/demo_2018.parquet')这样一段代码,完整表达了“清洗缺失值→转换类型→重命名变量→筛选45-95岁且有有效性别的样本→保存结果”的所有步骤。相比传统的一长串逐行操作,可读性和可维护性都高了一个档次。
4.4 多模块数据合并的实现
清洗完各个模块后,需要按ID进行横向合并。这里要注意合并方式的选取:
- 如果分析需要的是“所有模块都有完整记录”的样本,用inner join
- 如果分析需要保留某个主模块的所有样本,其他模块信息补充,用left join
- 如果两个模块之间没有明确的主次关系,考虑用outer join后检查缺失情况
以合并人口学模块和健康模块为例:
df_demo = pd.read_parquet('./data/processed/demo_2018.parquet') df_health = pd.read_parquet('./data/processed/health_2018.parquet') # 确保ID类型一致 df_demo['id'] = df_demo['id'].astype(str) df_health['id'] = df_health['id'].astype(str) # 合并 df_merged = pd.merge( df_demo, df_health, on='id', how='left', validate='one_to_one', indicator=True ) # 检查合并结果 print(df_merged['_merge'].value_counts())这里使用了validate='one_to_one'参数,它的作用是强制检查合并关系,如果出现一对多或多对一的情况会直接报错。这个参数在调试时特别好用,能避免因为ID重复导致的数据膨胀问题。而indicator=True会在合并结果中增加一列_merge,标识每条记录是来自左表、右表还是两边都有,方便检查合并的覆盖情况。
4.5 清洗结果的质量检查清单
数据清洗完不能直接用,必须先做质量检查。我总结了一份检查清单,每次清洗后按顺序过一遍:
- 维度检查:清洗后的数据行数是否符合预期,相比原始数据减少了多少
- 缺失率检查:核心变量的缺失率是否在可接受范围内(一般不超过20%)
- 取值范围检查:连续变量是否在合理区间内(如年龄0-120、收入非负等)
- 编码检查:分类变量的取值是否都符合问卷编码手册
- 跨模块一致性检查:同一样本在不同模块中的基本信息(年龄、性别)是否一致
- 重复值检查:ID是否存在重复,重复的原因是什么
- 抽样对比检查:随机抽10条记录,与原始数据逐项核对
def quality_check(df, id_col='id'): """清洗结果质量检查""" results = {} # 1. 维度检查 results['row_count'] = len(df) results['col_count'] = len(df.columns) # 2. 缺失率 missing_rate = df.isna().mean() results['high_missing_vars'] = missing_rate[missing_rate > 0.2].index.tolist() # 3. 重复值检查 results['duplicate_ids'] = df[id_col].duplicated().sum() # 4. 基础统计 numeric_cols = df.select_dtypes(include=[np.number]).columns results['numeric_stats'] = df[numeric_cols].describe().to_dict() return results5. 常见问题与排查技巧实录
5.1 高发问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读取dta文件报编码错误 | Stata版本不一致,文件使用了不同的字符编码 | 指定encoding参数,如encoding='utf-8'或encoding='latin1' |
| ID变量精度丢失,出现重复ID | read_stata将长ID转为int64导致末位精度丢失 | 读取后立即转为str类型,并用zfill补齐位数 |
| 合并后行数暴增 | 存在一对多的ID关系,merge时未指定validate参数 | 先检查ID重复情况,指定validate='one_to_many'或先做去重 |
| 描述性统计中出现大量负值 | 未处理CHARLS负数缺失编码 | 在分析前统一用replace替换为pd.NA |
| 变量全部变成object类型 | 原变量中含有文本型缺失值编码 | 使用pd.to_numeric配合errors='coerce'转换 |
| 多年数据合并后变量对不上 | 不同年份的同一变量编码不一致 | 建立跨年份变量映射表,逐年核实编码手册 |
5.2 debug实录:一个合并导致的数据膨胀案例
有一次我在合并2015年和2018年的数据时,发现清洗结果的行数从预期的1.2万变成了3.6万,足足膨胀了三倍。排查过程是这样的:
第一步,检查ID是否有重复。我用了duplicated()方法检查,结果发现ID确实大量重复。第二步,定位重复的来源。我把两年数据分别单独检查,发现2018年的数据中有一个变量的提取逻辑出了问题——在筛选“家庭成员信息”时,没有按“本人”条件筛选,导致一户家庭中每个成员的信息都被当作一条独立记录保留了下来。第三步,修正筛选条件后重新清洗,行数恢复正常。
这个案例说明,清洗过程中的每一个筛选条件都值得反复确认,尤其是在处理多成员家庭数据时,要清楚id的粒度是“个人”还是“家庭”。如果在筛选前先用groupby('id').size().sort_values()看一眼每户人数的分布,就能提前发现问题。
5.3 独家避坑技巧:从“够用”到“好用”
洗过几次CHARLS数据后,我总结了几条可能不多见于官方文档的经验:
第一,不要只保存最终清洗结果,中间每一步的关键数据也建议保留。有一次我在分析时发现某个变量的处理逻辑有问题,要追溯到底哪一步出了错,如果只有最终数据,就得全部重新跑一遍。后来我养成了习惯,每个清洗阶段都保存一份带标记的中间文件,比如demo_2018_clean_missing.parquet、demo_2018_clean_rename.parquet,发现问题时可以直接定位到对应的清洗步骤。
第二,把变量映射表单独保存成csv文件,用代码自动读取。这样一来,如果合作者想新增一个变量,只需要在csv里加一行,不用修改任何代码。而且,csv文件本身就是变量字典的可视化文档,对写论文时的变量说明也很有帮助。
第三,多利用pandas的.info()方法查看非空值统计,但注意它不会显示负数编码的缺失情况。我建议写一个自定义函数,同时检查NaN和负数编码的数量,这样不会漏掉任何缺失信息。
第四,如果你的研究方向是面板数据,建议在清洗完成后就构建好宽表(每行一个个体,每列是某个变量在某年的取值)和长表(每行是一个个体-年份观测),两个格式分别存好。分析时按需取用,省得每次重新转换。
6. 一点个人经验总结
CHARLS数据清洗这件事,刚上手的时候觉得繁琐枯燥,甚至想过放弃,但真正把一套标准化流程跑通之后,后面做任何实证分析都会非常顺畅。我个人最大的体会是:清洗代码要不要复用,取决于你的研究是不是长期的。
如果你只是做一篇课程作业,一次性把数据清洗完、跑完回归、交差走人,那可以临时写脚本,怎么快怎么来。但如果你打算围绕CHARLS做一系列研究,或者后续要考虑追踪数据,那一定花时间把这套清洗流程做成可复用、可扩展的工程。前期多花两三天把函数封装好、映射表建好、中间结果存好,后面每一次新研究的分析效率都能提升。
最后再分享一个小技巧:清洗完成后,给你的变量生成一份完整的数据字典,包括变量名、变量标签、取值说明、缺失率、来源问卷题目编号。这个数据字典在写论文的数据说明部分时几乎可以直接搬运,能省下大量写作时间。
CHARLS数据清洗并没有想象中那么神秘,本质上就是“理解编码规则 + 标准化处理 + 严格质检”的过程。希望这篇内容能帮你少踩一些坑,把宝贵的时间留给真正的研究问题本身。
本文还有配套的精品资源,点击获取