news 2026/9/1 12:58:54

CHARLS数据清洗实战:基于Python pandas的完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CHARLS数据清洗实战:基于Python pandas的完整流程

简介:面向使用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)说明
idIDIDIDID个体唯一标识
ageba002_1ba002_1ba002_1ba002_1年龄
genderba000_2ba000_2ba000_2ba000_2性别
edubd001bd001bd001bd001教育程度
hukoubd004bd004bd004bd004户口类型
marrybe001be001be001be001婚姻状况
chronicda005da005da005da005慢性病数量

这里要特别注意,虽然多数核心变量的编码在不同年份是保持一致的,但有些变量在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_clean

3.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数据做面板分析,变量一致性就是绕不开的难题。我的经验是:不要试图一次性处理所有年份的所有变量,而是先确定分析需要的核心变量清单,然后逐年提取,最后统一合并。

具体步骤如下:

  1. 确定核心变量清单,列出变量名、所属模块、年份
  2. 逐年读取数据,按变量清单筛选列
  3. 对每年数据进行相同的清洗操作(缺失值替换、类型转换、编码映射)
  4. 用pd.concat进行纵向合并,设置keys参数标记年份
  5. 检查合并后各变量的缺失率变化,确认没有系统性问题

比如做健康与收入关系研究时,核心变量可能包括:年龄、性别、教育、收入、自评健康、慢性病数量、医疗支出、医保类型。这些变量分布在人口学模块、健康模块、收入模块中,需要分别提取后按ID合并。

4. 实操过程与核心环节实现

4.1 环境准备与依赖安装

要跑通这套清洗流程,需要一个标准的Python数据科学环境。我的推荐配置如下:

pip install pandas numpy pyarrow statsmodels

pandas和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 清洗结果的质量检查清单

数据清洗完不能直接用,必须先做质量检查。我总结了一份检查清单,每次清洗后按顺序过一遍:

  1. 维度检查:清洗后的数据行数是否符合预期,相比原始数据减少了多少
  2. 缺失率检查:核心变量的缺失率是否在可接受范围内(一般不超过20%)
  3. 取值范围检查:连续变量是否在合理区间内(如年龄0-120、收入非负等)
  4. 编码检查:分类变量的取值是否都符合问卷编码手册
  5. 跨模块一致性检查:同一样本在不同模块中的基本信息(年龄、性别)是否一致
  6. 重复值检查:ID是否存在重复,重复的原因是什么
  7. 抽样对比检查:随机抽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 results

5. 常见问题与排查技巧实录

5.1 高发问题速查表

问题现象可能原因解决方案
读取dta文件报编码错误Stata版本不一致,文件使用了不同的字符编码指定encoding参数,如encoding='utf-8'或encoding='latin1'
ID变量精度丢失,出现重复IDread_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.parquetdemo_2018_clean_rename.parquet,发现问题时可以直接定位到对应的清洗步骤。

第二,把变量映射表单独保存成csv文件,用代码自动读取。这样一来,如果合作者想新增一个变量,只需要在csv里加一行,不用修改任何代码。而且,csv文件本身就是变量字典的可视化文档,对写论文时的变量说明也很有帮助。

第三,多利用pandas的.info()方法查看非空值统计,但注意它不会显示负数编码的缺失情况。我建议写一个自定义函数,同时检查NaN和负数编码的数量,这样不会漏掉任何缺失信息。

第四,如果你的研究方向是面板数据,建议在清洗完成后就构建好宽表(每行一个个体,每列是某个变量在某年的取值)和长表(每行是一个个体-年份观测),两个格式分别存好。分析时按需取用,省得每次重新转换。

6. 一点个人经验总结

CHARLS数据清洗这件事,刚上手的时候觉得繁琐枯燥,甚至想过放弃,但真正把一套标准化流程跑通之后,后面做任何实证分析都会非常顺畅。我个人最大的体会是:清洗代码要不要复用,取决于你的研究是不是长期的。

如果你只是做一篇课程作业,一次性把数据清洗完、跑完回归、交差走人,那可以临时写脚本,怎么快怎么来。但如果你打算围绕CHARLS做一系列研究,或者后续要考虑追踪数据,那一定花时间把这套清洗流程做成可复用、可扩展的工程。前期多花两三天把函数封装好、映射表建好、中间结果存好,后面每一次新研究的分析效率都能提升。

最后再分享一个小技巧:清洗完成后,给你的变量生成一份完整的数据字典,包括变量名、变量标签、取值说明、缺失率、来源问卷题目编号。这个数据字典在写论文的数据说明部分时几乎可以直接搬运,能省下大量写作时间。

CHARLS数据清洗并没有想象中那么神秘,本质上就是“理解编码规则 + 标准化处理 + 严格质检”的过程。希望这篇内容能帮你少踩一些坑,把宝贵的时间留给真正的研究问题本身。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 12:58:27

多模态情感分析实战:文本、音频、视频融合模型与源码解析

简介&#xff1a;面向深度学习与多模态情感分析研究者和工程师的PyTorch可运行源码包&#xff0c;整合语音与文本双模态&#xff0c;借助预训练多语言BERT与Wav2Vec2分别提取语义和语音深层特征&#xff0c;由注意力机制完成跨模态融合&#xff0c;针对negative、neutral、posi…

作者头像 李华
网站建设 2026/9/1 12:58:02

AI电影制作全流程:从剧本到成片的免费工具链实战指南

“Make AI movie 30 minutes Free”——看到这个标题&#xff0c;第一反应是&#xff1a;现在真的能用免费工具把一部30分钟的AI电影完整做出来吗&#xff1f;先说结论&#xff1a;没有哪个工具能直接输入一句话就吐出一整部成片&#xff0c;但把一部AI短片的生产流程拆成“剧本…

作者头像 李华
网站建设 2026/9/1 12:56:07

STM32+TSW-30低成本浊度检测仪制作教程

简介&#xff1a;TSW-30浊度传感器与STM32F103C8T6组合的Keil5工程资源&#xff0c;主要面向STM32初学者&#xff0c;重点演示ADC转换与DMA传输的配合使用&#xff0c;最终将水体浑浊度实时打印到串口。工程以标准外设库为基础&#xff0c;源码覆盖定时器、Flash、RCC时钟、ADC…

作者头像 李华
网站建设 2026/9/1 12:54:30

燃气灶选型安装与验收:5.2kW热负荷、铝炉头、两用结构解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/1 12:53:20

NS-3+SUMO车联网仿真实战:V2X平台搭建与关键技术解析

简介&#xff1a;面向5G车联网与V2X仿真研究的NS-3环境搭建源码包&#xff0c;适合需要快速上手NS-3并搭建车联网仿真场景的高校学生、科研人员与工程开发者使用&#xff0c;对Linux环境下的网络仿真入门者也具有一定参考价值。压缩包共7个文件&#xff0c;以4个shell脚本为核心…

作者头像 李华
网站建设 2026/9/1 12:53:15

模拟IC设计入门:从运算放大器到DC-DC转换器的完整项目实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华