claude-skills 项目 pandas-pro 数据清洗实战指南:缺失值、重复值与类型转换的完整方案
【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills
本指南系统讲解 claude-skills 项目中pandas-pro技能所依赖的核心参考文档>import pandas as pd import numpy as np df = pd.DataFrame({ 'name': ['Alice', 'Bob', None, 'Diana'], 'age': [25, np.nan, 35, 28], 'salary': [50000, 60000, np.nan, np.nan], 'department': ['Eng', '', 'Eng', 'Sales'] }) # 检查是否存在缺失值 df.isna().any() # 按列判断 df.isna().any().any() # 整个 DataFrame 是否含缺失 # 统计缺失数量 df.isna().sum() # 按列统计 df.isna().sum().sum() # 总缺失数 # 缺失比例(百分比,保留两位小数) (df.isna().sum() / len(df) * 100).round(2) # 含任意缺失的行 df[df.isna().any(axis=1)] # 所有值均完整的行 df[df.notna().all(axis=1)] # 一键汇总缺失信息(数量、占比、dtype) missing_info = pd.DataFrame({ 'missing': df.isna().sum(), 'percent': (df.isna().sum() / len(df) * 100).round(2), 'dtype': df.dtypes })
注意上面的示例数据中department含有一个空字符串'',它不会被isna()识别为缺失(详见 2.4 节)。
2.2 删除缺失:dropna()的精细控制
dropna()支持按行/按列、按子集、按有效值阈值三种删除策略:
# 删除含任意缺失值的行 df_clean = df.dropna() # 仅当指定列存在缺失时才删除该行 df_clean = df.dropna(subset=['name', 'age']) # 仅删除整行全为缺失的行 df_clean = df.dropna(how='all') # 保留至少 3 个非空值的行(thresh 为"非空值数量下限") df_clean = df.dropna(thresh=3) # 删除含缺失值的列 df_clean = df.dropna(axis=1) # 删除缺失比例超过 50% 的列(thresh 传行数阈值) threshold = len(df) * 0.5 df_clean = df.dropna(axis=1, thresh=threshold)thresh是"至少保留的非空值个数",当配合axis=1时,阈值需要用行数len(df)换算,示例中的写法"缺失超过 50% 的列"即基于这一换算。
2.3 填充缺失:从常量到分组填充
填充策略应结合字段语义选择,切勿无脑填 0:
# 用常量填充 df['age'] = df['age'].fillna(0) # 用列均值 / 中位数 / 众数填充 df['age'] = df['age'].fillna(df['age'].mean()) df['salary'] = df['salary'].fillna(df['salary'].median()) df['department'] = df['department'].fillna(df['department'].mode()[0]) # 前向填充(沿用上一个有效值,常用于时序数据) df['salary'] = df['salary'].ffill() # 后向填充(沿用下一个有效值) df['salary'] = df['salary'].bfill() # 每列使用不同的填充值 fill_values = {'age': 0, 'salary': df['salary'].median(), 'name': 'Unknown'} df = df.fillna(fill_values) # 线性插值(仅适用于数值列) df['salary'] = df['salary'].interpolate(method='linear') # 分组填充(按 department 分组,用组内均值填充该组缺失) df['salary'] = df.groupby('department')['salary'].transform( lambda x: x.fillna(x.mean()) )分组填充是工程中最常用的策略之一:它用"同类别样本的统计量"填补缺失,比全局均值更贴合业务上下文。这一模式与技能另一参考文档 aggregation-groupby.md 中的 GroupBy/transform 能力一脉相承。SKILL.md 中的"Missing Value Handling"模式(SKILL.md)也给出了同款思路:对 object 列填众数、对数值列填中位数。
2.4 空字符串与 NaN:最容易踩的坑
空字符串''和纯空白不是 NaN,isna()默认统计不到它们。读取外部数据(CSV、Excel)时这是最高发的"隐形脏数据":
# 空字符串不会被 isna() 统计到 df['department'].isna().sum() # 不会把 '' 计入 # 将空字符串替换为 NaN df['department'] = df['department'].replace('', np.nan) # 或用正则匹配纯空白(含多个空格) df['department'] = df['department'].replace(r'^\s*$', np.nan, regex=True) # 一次性替换多个常见"假缺失"占位符 df = df.replace(['', 'N/A', 'null', 'None', '-'], np.nan) # 更推荐:读取文件时直接用 na_values 声明缺失标记 df = pd.read_csv('file.csv', na_values=['', 'N/A', 'null', 'None', '-'])na_values在读取阶段就统一了缺失语义,避免后续反复replace,这是处理真实 CSV 数据时的最佳实践。
三、重复值处理:识别、去重或聚合
3.1 检测重复
duplicated()返回布尔 Series,subset控制按哪些列判定重复:
df = pd.DataFrame({ 'id': [1, 2, 2, 3, 4, 4], 'name': ['Alice', 'Bob', 'Bob', 'Charlie', 'Diana', 'Diana'], 'email': ['a@x.com', 'b@x.com', 'b@x.com', 'c@x.com', 'd@x.com', 'd2@x.com'] }) # 统计完全重复的行数(所有列一致) df.duplicated().sum() # 按指定列判定重复 df.duplicated(subset=['id']).sum() df.duplicated(subset=['name', 'email']).sum() # 查看全部重复行 df[df.duplicated(keep=False)] # 重复组的所有行 df[df.duplicated(keep='first')] # 除首次出现外的重复行 df[df.duplicated(keep='last')] # 除最后一次出现外的重复行 # 按 key 统计每个重复组的大小 df.groupby('id').size().loc[lambda x: x > 1]注意上面的示例email列中,d@x.com与d2@x.com是不同的字符串,所以name+email组合在 id=4 这两行并不重复——这正是"按业务键判定重复"的意义。
3.2 删除重复:drop_duplicates()
# 删除完全重复行,保留首次出现 df_clean = df.drop_duplicates() # 保留最后一次出现 df_clean = df.drop_duplicates(keep='last') # 删除所有重复行(重复组一行不留) df_clean = df.drop_duplicates(keep=False) # 按指定列去重 df_clean = df.drop_duplicates(subset=['id']) df_clean = df.drop_duplicates(subset=['name', 'email'], keep='last') # 原地修改 df.drop_duplicates(inplace=True)3.3 不去重,改为聚合
"重复"未必等于"错误"——多条记录可能携带不同信息(如历史订单)。此时聚合比删除更合适:
# 按 id 分组聚合:name 取首条,email 合并去重 df_agg = df.groupby('id').agg({ 'name': 'first', 'email': lambda x: ', '.join(x.unique()) }).reset_index() # 每组保留 score 最大的行 df_best = df.loc[df.groupby('id')['score'].idxmax()] # 为每组内重复行编号(1、2、3...) df['rank'] = df.groupby('id').cumcount() + 1这与 aggregation-groupby.md 的核心理念一致:聚合变换(aggregation)是把"记录级"数据变为"汇总级"数据的标准手段,data-cleaning 文档末尾也明确指出"聚合重复而非删除"是相关参考之一。
四、类型转换:安全、显式、可空
4.1 检查与基础转换
# 检查当前类型 df.dtypes df.info() # 转基础类型 df['age'] = df['age'].astype(int) df['salary'] = df['salary'].astype(float) df['name'] = df['name'].astype(str) # 安全转换:errors='coerce' 把非法值变为 NaN df['age'] = pd.to_numeric(df['age'], errors='coerce') # 非法 -> NaN # errors='ignore' 转换失败则保留原值 df['age'] = pd.to_numeric(df['age'], errors='ignore') # 一次转换多列 df = df.astype({'age': 'int64', 'salary': 'float64'}) # 转 pandas 2.0+ 可空字符串类型 StringDtype df['name'] = df['name'].astype('string') # 可空字符串errors='coerce'是清洗流程的关键:它能捕获"混入数字列里的脏值"并把它们统一转成 NaN,从而进入缺失值处理管线。
4.2 日期时间转换
df = pd.DataFrame({ 'date_str': ['2024-01-15', '2024-02-20', 'invalid', '2024-03-10'], 'timestamp': [1705276800, 1708387200, 1710028800, 1710028800] }) # 字符串转 datetime,非法值变 NaT df['date'] = pd.to_datetime(df['date_str'], errors='coerce') # 指定格式解析更快 df['date'] = pd.to_datetime(df['date_str'], format='%Y-%m-%d', errors='coerce') # Unix 时间戳转 datetime(unit='s' 秒 / 'ms' 毫秒) df['datetime'] = pd.to_datetime(df['timestamp'], unit='s') # 提取日期分量 df['year'] = df['date'].dt.year df['month'] = df['date'].dt.month df['day_of_week'] = df['date'].dt.day_name() # 混合格式解析(pandas 2.0+) df['date'] = pd.to_datetime(df['date_str'], format='mixed', dayfirst=False)format='mixed'是 pandas 2.0 起支持的混合格式解析方式,适合同一列里出现多种日期格式的真实数据。
4.3 分类类型(Categorical):低基数字符串的内存利器
对取值有限(低基数)的字符串列,转category既能省内存,又能配合排序与 GroupBy:
# 低基数字符串转 category df['department'] = df['department'].astype('category') # 有序分类(可控制排序顺序) df['size'] = pd.Categorical( df['size'], categories=['Small', 'Medium', 'Large'], ordered=True ) # 验证内存节省 print(f"Object: {df['department'].nbytes}") df['department'] = df['department'].astype('category') print(f"Category: {df['department'].nbytes}")这一技巧在 performance-optimization.md 中被进一步展开:当某列的 unique 值与总行数之比小于 0.5(低基数)时,通常建议转为category,其内存收益在百万级行上非常显著。
4.4 可空类型(Nullable Types,pandas 2.0+)
传统int64不支持 NaN,一旦列中出现缺失,astype(int)就会抛错。可空类型(注意大写的Int64)正是为"带缺失的整数"设计的:
# 标准 int 不支持 NaN,改用可空整数 df['age'] = df['age'].astype('Int64') # 注意大写 I # 全套可空类型 df = df.astype({ 'count': 'Int64', # 可空整数 'price': 'Float64', # 可空浮点 'flag': 'boolean', # 可空布尔 'name': 'string', # 可空字符串 }) # 直接构造带 NA 的可空数组 df['age'] = pd.array([1, 2, None, 4], dtype='Int64')SKILL.md 的"验证结果"环节也强调:变换后必须检查result.isna().sum().sum() == 0、形状与列集合是否符合预期,可空类型让"缺失 + 整数"不再互相排斥。
五、字符串清洗:去空白、规范化与正则提取
5.1 常用字符串操作
通过.str访问器执行向量化字符串操作(不要用apply(lambda x: x.upper())逐行处理):
df = pd.DataFrame({ 'name': [' Alice ', 'BOB', 'charlie', None, 'Diana Smith'], 'email': ['ALICE@EXAMPLE.COM', 'bob@test', 'invalid', None, 'diana@example.com'] }) # 去除首尾空白 df['name'] = df['name'].str.strip() # 大小写规范化 df['name'] = df['name'].str.lower() df['name'] = df['name'].str.upper() df['name'] = df['name'].str.title() # 标题式:每个单词首字母大写 # 正则替换:多个空格压成一个 df['name'] = df['name'].str.replace(r'\s+', ' ', regex=True) # 只保留数字(清洗电话号码) df['phone'] = df['phone'].str.replace(r'[^0-9]', '', regex=True) # 正则提取 df['domain'] = df['email'].str.extract(r'@(.+)$') # 提取邮箱域名 df['first_name'] = df['name'].str.extract(r'^(\w+)') # 提取第一个单词 # 拆分字符串为多列 df[['first', 'last']] = df['name'].str.split(' ', n=1, expand=True)5.2 字符串校验
清洗后需要校验数据是否符合预期格式:
# 正则校验邮箱格式(na=False 保证 NaN 行返回 False 而非报错) df['valid_email'] = df['email'].str.match(r'^[\w.]+@[\w.]+\.\w+$', na=False) # 字符串长度检查 df['name_length'] = df['name'].str.len() df['valid_length'] = df['name'].str.len().between(2, 50) # 包含关系检查 df['has_domain'] = df['email'].str.contains('@', na=False)na=False参数在处理含缺失的字符串列时不可或缺,否则 NaN 会传导为 NaN 布尔值,破坏后续过滤逻辑。
六、数据校验:从断言到 pandera Schema
6.1 内置校验函数
把校验封装为纯函数,便于复用与测试:
def validate_dataframe(df: pd.DataFrame) -> dict: """对 DataFrame 做全面校验,返回统计报告。""" report = { 'rows': len(df), 'columns': len(df.columns), 'duplicates': df.duplicated().sum(), 'missing_by_column': df.isna().sum().to_dict(), 'dtypes': df.dtypes.astype(str).to_dict(), } return report # 范围校验:返回布尔掩码 def validate_range(series: pd.Series, min_val, max_val) -> pd.Series: """判断 series 中的值是否落在 [min_val, max_val] 区间。""" return series.between(min_val, max_val) df['valid_age'] = validate_range(df['age'], 0, 120) # 自定义校验:邮箱格式 def validate_email(series: pd.Series) -> pd.Series: """校验邮箱格式,返回布尔掩码。""" pattern = r'^[\w.+-]+@[\w-]+\.[\w.-]+$' return series.str.match(pattern, na=False) df['valid_email'] = validate_email(df['email'])6.2 用 pandera 做生产级 Schema 校验
当清洗流程需要进入生产环境时,建议引入 [pandera] 这类 Schema 校验库,把"列类型 + 约束规则"声明式地固化下来:
# 使用 pandera 进行 Schema 校验(生产环境推荐) import pandera as pa from pandera import Column, Check schema = pa.DataFrameSchema({ 'name': Column(str, Check.str_length(min_value=1, max_value=100)), 'age': Column(int, Check.in_range(0, 120)), 'email': Column(str, Check.str_matches(r'^[\w.+-]+@[\w-]+\.[\w.-]+$')), 'salary': Column(float, Check.greater_than(0), nullable=True), }) # 校验 DataFrame try: schema.validate(df) except pa.errors.SchemaError as e: print(f"Validation failed: {e}")pandera 的Check提供了str_length、in_range、str_matches、greater_than等内置检查器,配合nullable=True可以精确表达"该列允许缺失"的业务语义。
七、构建可复用的数据清洗管道
7.1 方法链(Method Chaining)模式
技能文档 SKILL.md 强调"使用方法链实现可读、可维护的清洗流程"。一个端到端的清洗管道示例:
def clean_dataframe(df: pd.DataFrame) -> pd.DataFrame: """基于方法链的完整数据清洗管道。""" return ( df # 拷贝,避免修改原始数据(规避 SettingWithCopyWarning) .copy() # 标准化列名:小写、去空白、空格换下划线 .rename(columns=lambda x: x.lower().strip().replace(' ', '_')) # 删除整行全为缺失的行 .dropna(how='all') # 清洗字符串列 .assign( name=lambda x: x['name'].str.strip().str.title(), email=lambda x: x['email'].str.lower().str.strip(), ) # 缺失值填充 .fillna({'department': 'Unknown'}) # 类型转换 .astype({'age': 'Int64', 'department': 'category'}) # 去重(按 email 判定) .drop_duplicates(subset=['email']) # 重置索引 .reset_index(drop=True) ) df_clean = clean_dataframe(df)要点逐条对应前面各节:copy()对应 dataframe-operations.md 中"修改子集前必须 copy"的规范,rename/assign/fillna/astype/drop_duplicates则对应本文第 2~4 节的各项技术。
7.2 管道 + 校验报告:可审计的清洗函数
生产级的清洗函数应当返回"清洗后的数据"与"清洗统计"两部分,让每次清洗可审计、可复现:
def clean_and_validate( df: pd.DataFrame, required_columns: list[str], unique_columns: list[str] | None = None, ) -> tuple[pd.DataFrame, dict]: """清洗 DataFrame 并返回校验报告。""" # 校验必需列是否存在 missing_cols = set(required_columns) - set(df.columns) if missing_cols: raise ValueError(f"Missing required columns: {missing_cols}") # 记录清洗统计 stats = { 'initial_rows': len(df), 'dropped_empty': 0, 'dropped_duplicates': 0, 'filled_missing': {}, } # 拷贝后清洗 df = df.copy() # 删除整行空行 before = len(df) df = df.dropna(how='all') stats['dropped_empty'] = before - len(df) # 按指定键去重 if unique_columns: before = len(df) df = df.drop_duplicates(subset=unique_columns) stats['dropped_duplicates'] = before - len(df) stats['final_rows'] = len(df) return df, stats该函数体现了技能 SKILL.md 中"评估 → 变换 → 验证"的工作流:先对输入做契约校验(必需列),再逐类执行清洗并记录影响行数,最后以(df, stats)形式输出可供下游继续处理与审计。
八、最佳实践总结
清洗代码的工程化要点(与># BAD: 不理解影响就盲目 dropna,可能丢失大量数据 df = df.dropna() # 可能造成严重数据损失 # GOOD: 先调查,再决策 print(f"Missing values: {df.isna().sum()}") print(f"Rows affected: {df.isna().any(axis=1).sum()}") # 基于数据量权衡后,再决定删除还是填充 # BAD: 缺乏领域知识地填 0(age=0 显然不合法) df['age'] = df['age'].fillna(0) # 年龄 0 不合法 # GOOD: 采用合理填充策略 df['age'] = df['age'].fillna(df['age'].median()) # BAD: 类型转换不做错误处理,遇到 NaN 或非法值直接抛错 df['id'] = df['id'].astype(int) # 遇到 NaN 或非法值会失败 # GOOD: 安全转换(先 coerce 再转可空整数) df['id'] = pd.to_numeric(df['id'], errors='coerce').astype('Int64')
十、相关参考与深入阅读
数据清洗不是孤立环节,它与 pandas-pro 技能的其他参考文档共同构成完整的数据操作体系:
- dataframe-operations.md —— 选择与过滤,是清洗前的"定位数据"前置步骤;
- aggregation-groupby.md —— 用聚合替代删除,处理重复数据的高级方案;
- performance-optimization.md —— 大规模数据清洗时的内存与性能优化;
- merging-joining.md —— 多表合并后的联合去重与一致性校验;
- SKILL.md —— 技能总览,定义了"评估 → 变换 → 验证 → 优化"的完整工作流与 MUST DO/MUST NOT DO 约束。
对于数据量达到百万行级别或需要在受限内存环境运行的清洗任务,建议同时阅读 performance-optimization.md 中的分块读取(chunksize)、downcast降位宽、query()/eval()等加速手段,让"清洗正确"与"清洗高效"同时成立。
【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考