Python数据清洗实战:缺失值、重复行、异常值与类型校准
1. 项目概述:为什么数据清洗不是“脏活”,而是数据工作的命脉
在真实的数据科学工作流里,没人会因为模型准确率高了0.3%而开香槟;但只要一次清洗疏漏导致线上报表凌晨三点崩掉,整个团队的晨会就会变成事故复盘现场。我带过七支不同行业的数据分析小队,从电商用户行为分析到制造业设备传感器日志处理,一个铁律始终成立:85%以上的项目时间花在数据清洗上,而90%以上的线上故障根源在清洗逻辑的边界遗漏。这不是夸张——去年帮某连锁药店做会员复购预测时,我们花三周调参优化XGBoost,结果上线后首周预测全部失效。排查三天才发现,原始销售记录里有27%的“负数数量”字段,是系统退货冲正时写入的,而清洗脚本只过滤了空值和非数字字符,却没识别“-12”这种合法字符串格式的异常业务含义。这个案例后来被我写进内部培训手册第一页:数据清洗不是预处理环节,它是把业务逻辑翻译成机器可读语言的第一道编译器。本文聚焦Python生态下高频、真实、带血泪教训的清洗任务,不讲pandas基础语法,不堆砌API文档,只拆解那些你明天打开Jupyter就可能撞上的坑:缺失值不是简单填均值就能完事,重复行背后藏着CRM系统双录bug,时间戳解析错位会让整个趋势分析倒置方向。适合刚转行的数据新人建立清洗直觉,也适合老手对照自查清单——毕竟我踩过的坑,你没必要再用生产环境验证一遍。
2. 核心清洗任务拆解与场景化选型逻辑
2.1 缺失值处理:为什么“fillna(0)”是最高频的自杀式操作
缺失值处理常被简化为“删或填”,但真实业务中,缺失本身携带关键信号。我见过三个典型反例:某银行风控模型将客户职业字段缺失统一填“其他”,结果发现缺失人群的逾期率是平均值的3.2倍;某SaaS公司用户活跃度表中,last_login_time缺失被填为注册时间,导致新用户被误判为高留存;最致命的是医疗数据,血压字段缺失若填0,直接触发错误预警。因此,我的处理流程强制分三步:
缺失模式诊断:先用
df.isnull().sum()/len(df)看全局比例,再用missingno.matrix(df)可视化分布。重点观察是否呈现“列簇缺失”(如某几列同时缺失),这往往指向系统集成断点。例如某ERP导出数据中,purchase_order_id和supplier_name总是一起为空,说明采购单未关联供应商的业务状态。业务语义映射:对每列缺失定义业务含义。以电商订单表为例:
discount_amount缺失 → 该订单无优惠(非异常,填0合理)shipping_address缺失 → 订单未完成地址确认(高风险,需标记为待补录)payment_status缺失 → 支付网关超时未返回(需查日志,不能填默认值)
动态填充策略:拒绝全局fillna。实操中我建立三层填充矩阵:
- 规则层:基于业务规则硬编码,如
df.loc[df['order_status']=='shipped', 'delivery_date'] = df['order_date'] + pd.Timedelta(days=3) - 统计层:同类样本聚合,如
df['age'].fillna(df.groupby('city')['age'].transform('median')) - 模型层:仅对关键字段用KNNImputer,但必须限定邻居范围(
n_neighbors=5),避免用全国用户均值填一线城市白领年龄。
- 规则层:基于业务规则硬编码,如
提示:永远保留原始缺失标识。我在所有清洗后数据集添加
_is_missing布尔列,如df['price_is_missing'] = df['price'].isnull()。这看似冗余,但在模型解释阶段,它能揭示“价格缺失”本身是否是欺诈特征。
2.2 重复数据识别:当“完全重复”只是冰山一角
初学者常以为df.duplicated().sum()就能搞定重复,但真实数据中的重复是分层的。我按严重程度划分为三类:
物理重复:全字段一致的行。这是最易处理的,
df.drop_duplicates()即可。但要注意:某次处理物流轨迹数据时,发现GPS坐标精确到小数点后6位,而设备采样误差达±0.0001度,直接去重会误删有效轨迹点。解决方案是先对经纬度四舍五入到小数点后4位再判断重复。业务重复:关键业务字段组合重复,但其他字段不同。例如用户表中
user_id相同但phone_number不同,这指向账号合并冲突。此时需人工规则:优先保留last_updated_time最新的记录,并将旧手机号存入backup_phones字段(用JSON序列化)。逻辑重复:无唯一键但存在隐含业务关系。最典型的是交易流水表,同一笔支付可能生成多条记录(主单+分账单+手续费单)。我用
df.groupby(['transaction_id', 'payee_id']).size().max()检测,若大于1则需按transaction_type字段分流处理:主单保留,分账单聚合金额,手续费单单独建表。
注意:去重前必做备份。我强制执行
df.to_parquet(f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.parquet"),曾靠此救回因误操作丢失的200万条用户行为日志。
2.3 异常值检测:别让3σ毁掉你的业务洞察
统计学教材教用Z-score或IQR,但实际工作中,异常值必须绑定业务阈值。某次分析外卖骑手配送时长时,用IQR剔除>120分钟的订单,结果发现这些“异常”全是跨城生鲜配送单——业务方明确告知这是正常服务类型。因此我的异常值处理遵循“三线原则”:
- 红线:绝对不可接受的数值。如年龄字段出现-5岁、订单金额为NaN,直接报错中断流程。
- 黄线:需人工复核的区间。如用户月消费额>10万元,自动触发
alert_to_business_team()函数,推送至企业微信并附原始数据截图。 - 绿线:可程序化修正的偏差。如时间戳字段
created_at早于系统上线日(2020-01-01),则按业务规则修正为上线日零点。
技术实现上,我弃用scikit-learn的IsolationForest(训练慢且难解释),改用向量化规则引擎:
# 定义业务规则字典 business_rules = { 'age': {'min': 0, 'max': 120, 'action': 'drop'}, 'order_amount': {'min': 0.01, 'max': 100000, 'action': 'flag'}, 'delivery_time_min': {'min': 5, 'max': 1440, 'action': 'clip'} # clip到[5,1440] } for col, rule in business_rules.items(): mask = (df[col] < rule['min']) | (df[col] > rule['max']) if rule['action'] == 'drop': df = df[~mask].copy() elif rule['action'] == 'flag': df[f'{col}_is_anomaly'] = mask elif rule['action'] == 'clip': df[col] = df[col].clip(lower=rule['min'], upper=rule['max'])2.4 数据类型校准:字符串里的“2023-01-01”不是日期
类型错误是隐形杀手。某次金融客户数据中,interest_rate字段存储为字符串“4.5%”,直接转float会报错;另一案例中,product_code为“ABC-001”却被误设为int,导致前导零丢失。我的校准流程强制四步:
自动探测:用
pandas.api.types.infer_dtype()初筛,但绝不依赖。该函数会将含空格的数字串判为“string”,而实际需转为float。业务强约束:为每列定义类型契约。例如:
user_id: 必须为64位十六进制字符串(校验re.match(r'^[a-f0-9]{64}$', x))transaction_time: 必须能被ISO8601解析且时区为UTC(pd.to_datetime(x, utc=True))is_premium: 必须为布尔值,但接受字符串“true”/“false”、“1”/“0”、“Y”/“N”
安全转换:拒绝
astype()暴力转换。对日期字段,用pd.to_datetime(df['col'], errors='coerce'),将无法解析的转为NaT,再通过df['col'].isna().sum()量化失败率。若失败率>5%,立即终止并输出前10个失败样本。逆向验证:转换后执行
df[col].apply(lambda x: isinstance(x, expected_type))全量校验。曾发现pd.to_datetime()将“2023-02-30”转为“2023-03-02”,表面成功实则错误,必须用df[col].dt.day == df['raw_day']二次验证。
3. 高频清洗任务实操详解与参数精调
3.1 时间序列清洗:时区、频率、不规则间隔的三重绞杀
时间字段清洗是死亡率最高的环节。我整理出最常踩的五个坑及对应解法:
坑1:本地时间未标准化
某跨境电商数据中,order_time字段混杂UTC、CST、PST三种时区。直接pd.to_datetime()会导致时间错位。解法:
# 先用正则提取时区标识 df['timezone'] = df['order_time'].str.extract(r'([A-Z]{3})$') # 按时区分组转换 timezones = {'UTC': 'UTC', 'CST': 'US/Central', 'PST': 'US/Pacific'} for tz_abbr, tz_full in timezones.items(): mask = df['timezone'] == tz_abbr df.loc[mask, 'order_time_parsed'] = pd.to_datetime( df.loc[mask, 'order_time'].str.replace(f' {tz_abbr}$', ''), format='%Y-%m-%d %H:%M:%S' ).dt.tz_localize(tz_full).dt.tz_convert('UTC')坑2:不规则采样频率
IoT设备日志中,传感器本应每5秒上报,但网络抖动导致间隔为[3,12]秒。若强行resample('5S'),会引入大量插值噪声。解法:用asfreq()替代resample(),并设置容差:
# 先排序确保时间递增 df = df.sort_values('timestamp').set_index('timestamp') # 容差内取最近值,超出容差则留空 df_resampled = df.asfreq('5S', method='nearest', tolerance='2S')坑3:夏令时跳变
欧洲客户数据在3月最后一个周日出现2:30重复两次(夏令时开始),10月最后一个周日2:30消失(夏令时结束)。tz_localize()会报错。解法:用ambiguous='infer'参数:
df['local_time'] = pd.to_datetime(df['raw_time']) df['utc_time'] = df['local_time'].dt.tz_localize( 'Europe/Berlin', ambiguous='infer', # 自动推断夏令时状态 nonexistent='shift_forward' # 处理不存在的时间 )坑4:业务日历 vs 自然日历
金融交易需按交易日(排除周末/节假日)计算,而非自然日。解法:构建自定义日历:
from pandas.tseries.holiday import USFederalHolidayCalendar from pandas.tseries.offsets import CustomBusinessDay us_bus_day = CustomBusinessDay(calendar=USFederalHolidayCalendar()) df['trade_date'] = pd.date_range('2023-01-01', periods=len(df), freq=us_bus_day)坑5:时间精度污染
数据库导出的时间戳含微秒,但业务只需到秒级。dt.floor('S')比dt.round('S')更安全,避免23:59:59.999四舍五入到次日。
3.2 文本清洗:从“iPhone 13 Pro Max (256GB) – 玫瑰金”到结构化特征
电商商品标题清洗是典型“非结构化转结构化”任务。我摒弃正则硬编码,采用分层提取策略:
第一层:品牌归一化
维护品牌别名映射表,解决大小写/符号/缩写问题:
brand_mapping = { 'apple': ['Apple', 'APPLE', 'iphone', 'iPad'], 'samsung': ['Samsung', 'SAMSUNG', 'Galaxy', 'Note'], 'xiaomi': ['Xiaomi', 'MI', 'Redmi'] } # 构建正则模式 pattern = '|'.join([f'({ "|".join(names) })' for names in brand_mapping.values()]) df['brand'] = df['title'].str.extract(f'({pattern})', flags=re.I)[0].str.lower()第二层:规格提取
用命名实体识别思想,定义规格模式:
spec_patterns = { 'storage': r'(\d+)(?:GB|TB)', 'color': r'(?:–|—|\-)\s*([^\(\)]+?)(?=\s*\()', 'model': r'(?:iPhone|Galaxy|Mi)\s+([\w\s]+?)(?=\s+\()' } for spec, pattern in spec_patterns.items(): df[spec] = df['title'].str.extract(pattern, flags=re.I)第三层:噪声过滤
移除营销话术但保留关键修饰词:
# 保留:Pro、Max、Ultra(影响价格) # 移除:全新、正品保障、限时抢购(无业务价值) noise_words = ['全新', '正品', '保障', '抢购', '爆款'] df['clean_title'] = df['title'].replace(noise_words, '', regex=True)实操心得:文本清洗必须保留原始字段。我坚持
df['title_raw'] = df['title'].copy(),因为某次发现“翻新机”被误标为“全新”,靠原始字段快速定位清洗规则漏洞。
3.3 数值型字段清洗:单位、精度、业务逻辑的三角验证
数值清洗的核心是“单位一致性”。某次处理全球销售数据时,发现美国用磅(lb)、英国用公斤(kg)、日本用克(g),直接聚合导致结果偏差1000倍。我的单位清洗框架包含:
步骤1:单位自动识别
unit_patterns = { 'weight_kg': [r'(\d+\.?\d*)\s*(?:kg|kilogram)', r'(\d+\.?\d*)\s*(?:g|gram)'], 'length_cm': [r'(\d+\.?\d*)\s*(?:cm|centimeter)', r'(\d+\.?\d*)\s*(?:inch)'], 'price_usd': [r'\$(\d+\.?\d*)', r'¥(\d+\.?\d*)'] } # 匹配所有模式,取最高置信度 for field, patterns in unit_patterns.items(): for i, pattern in enumerate(patterns): match = df['raw_value'].str.extract(pattern) if not match[0].isna().all(): df[f'{field}_value'] = pd.to_numeric(match[0], errors='coerce') df[f'{field}_unit'] = f'pattern_{i}' break步骤2:单位转换
建立转换因子矩阵,避免硬编码:
conversion_factors = { ('weight', 'g', 'kg'): 0.001, ('weight', 'lb', 'kg'): 0.453592, ('length', 'inch', 'cm'): 2.54 } # 动态应用转换 df['weight_kg_final'] = df['weight_g_value'] * conversion_factors[('weight','g','kg')]步骤3:业务逻辑校验
对转换后数值做合理性检查:
# 手机重量应在100-300g之间 df['weight_check'] = df['weight_kg_final'].between(0.1, 0.3) # 若不通过,触发人工审核 if not df['weight_check'].all(): send_alert_to_qc(df[~df['weight_check']])3.4 分类变量清洗:编码陷阱与类别漂移的防御
分类变量清洗常被忽视,但后果严重。某次用户画像项目中,education_level字段有“本科”、“Bachelor”、“B.S.”、“Bachelors”,pd.get_dummies()生成4个独热列,导致模型过拟合。我的处理流程:
1. 类别归一化
构建标准化词典,支持模糊匹配:
edu_mapping = { 'bachelor': ['bachelor', 'bs', 'b.s.', 'bachelors', '本科'], 'master': ['master', 'ms', 'm.s.', 'masters', '硕士'], 'phd': ['phd', 'doctor', 'ph.d.', '博士'] } # 使用difflib加速模糊匹配 from difflib import get_close_matches def normalize_edu(x): if pd.isna(x): return 'unknown' candidates = [k for k, v in edu_mapping.items() for item in v if item.lower() in str(x).lower()] if candidates: return candidates[0] # 模糊匹配兜底 matches = get_close_matches(str(x).lower(), [item for v in edu_mapping.values() for item in v], n=1, cutoff=0.6) return 'unknown' if not matches else [k for k, v in edu_mapping.items() if matches[0] in v][0] df['education_norm'] = df['education_raw'].apply(normalize_edu)2. 类别漂移监控
生产环境中,新数据可能涌入未见过的类别(如新增“Bootcamp”)。我部署实时监控:
# 训练时保存类别集合 train_categories = set(df['education_norm'].unique()) # 推理时检查 new_categories = set(new_df['education_norm'].unique()) - train_categories if new_categories: log_warning(f"New categories detected: {new_categories}") # 自动扩展one-hot编码维度 new_df = pd.get_dummies(new_df, columns=['education_norm'], prefix='edu')3. 高基数变量处理
对product_id等高基数变量,拒绝独热编码。改用目标编码(Target Encoding):
# 计算每个product_id的平均销量 target_mean = df.groupby('product_id')['sales'].mean() # 平滑处理避免过拟合 global_mean = df['sales'].mean() df['product_target_enc'] = df['product_id'].map(target_mean).fillna(global_mean) # 添加噪声防止信息泄露 df['product_target_enc'] += np.random.normal(0, 0.01, len(df))4. 清洗流程工程化:从Jupyter到生产环境的平滑迁移
4.1 可复现清洗管道设计
单次清洗脚本在Jupyter中可行,但生产环境需可复现、可审计、可回滚。我采用三层管道架构:
基础层:原子清洗函数
每个函数只做一件事,输入DataFrame,输出DataFrame,无副作用:
def clean_phone_number(df: pd.DataFrame, col: str) -> pd.DataFrame: """标准化手机号:移除空格/括号,补全国家码""" df[col] = df[col].str.replace(r'[^\d+]', '', regex=True) df[col] = df[col].str.replace(r'^86', '', regex=True) # 去中国区号 df[col] = '86' + df[col].str.zfill(11) # 补足11位 return df def validate_email_format(df: pd.DataFrame, col: str) -> pd.DataFrame: """邮箱格式校验,无效值设为None""" pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' df[col] = df[col].where(df[col].str.match(pattern), None) return df组合层:清洗流水线
用函数式编程组装,支持动态启停:
class DataCleaningPipeline: def __init__(self): self.steps = [] def add_step(self, func, *args, **kwargs): self.steps.append((func, args, kwargs)) return self def run(self, df: pd.DataFrame) -> pd.DataFrame: result = df.copy() for func, args, kwargs in self.steps: result = func(result, *args, **kwargs) return result # 构建管道 pipeline = DataCleaningPipeline() pipeline.add_step(clean_phone_number, 'mobile') pipeline.add_step(validate_email_format, 'email') pipeline.add_step(remove_outliers_iqr, 'income', threshold=1.5) cleaned_df = pipeline.run(raw_df)监控层:清洗质量仪表盘
每步输出清洗报告:
def generate_cleaning_report(df_before: pd.DataFrame, df_after: pd.DataFrame, step_name: str): report = { 'step': step_name, 'rows_dropped': len(df_before) - len(df_after), 'columns_modified': list(set(df_before.columns) & set(df_after.columns)), 'nulls_before': df_before.isnull().sum().sum(), 'nulls_after': df_after.isnull().sum().sum(), 'anomalies_flagged': getattr(df_after, 'anomaly_count', 0) } return pd.Series(report) # 在pipeline.run中嵌入 reports = [] for i, (func, args, kwargs) in enumerate(pipeline.steps): df_before = result.copy() result = func(result, *args, **kwargs) reports.append(generate_cleaning_report(df_before, result, f'step_{i}')) report_df = pd.DataFrame(reports)4.2 生产环境部署要点
Jupyter调试通过不等于生产可用。我总结出四个必检项:
1. 内存爆炸预防df.fillna(method='ffill')在大数据集上会复制整列。解法:分块处理
def safe_ffill(df: pd.DataFrame, chunk_size: int = 10000): result = pd.DataFrame() for i in range(0, len(df), chunk_size): chunk = df.iloc[i:i+chunk_size].copy() chunk = chunk.fillna(method='ffill') result = pd.concat([result, chunk], ignore_index=True) return result2. 并发安全
多进程清洗时,pandas.read_csv()的dtype参数若传入字典,会因进程间内存共享失效。解法:用pd.api.types.pandas_dtype()预编译:
# 错误写法 dtype_dict = {'user_id': 'string', 'amount': 'float64'} # 正确写法 dtype_dict = { 'user_id': pd.api.types.pandas_dtype('string'), 'amount': pd.api.types.pandas_dtype('float64') }3. 版本兼容性
pandas 1.5+的pd.to_datetime()默认infer_datetime_format=True,但某些格式会解析错误。生产环境强制关闭:
pd.to_datetime(df['date_str'], infer_datetime_format=False, errors='coerce')4. 回滚机制
每次清洗生成版本快照:
version = datetime.now().strftime('%Y%m%d_%H%M%S') df.to_parquet(f'data/cleaned_v{version}.parquet') # 同时写入元数据 metadata = { 'version': version, 'pipeline_hash': hashlib.md5(pickle.dumps(pipeline.steps)).hexdigest(), 'source_hash': calculate_file_hash('data/raw.parquet') } with open(f'data/metadata_v{version}.json', 'w') as f: json.dump(metadata, f)4.3 清洗效果验证:不只是“跑通”,而是“可信”
清洗后必须验证业务正确性。我建立三级验证体系:
单元验证:对单列做断言
def assert_column_valid(df: pd.DataFrame, col: str, condition: str, message: str): assert eval(f'df["{col}"].{condition}'), message # 示例 assert_column_valid(df, 'age', 'between(0, 120).all()', 'Age out of valid range') assert_column_valid(df, 'email', 'str.match("^.+@.+\..+$").all()', 'Invalid email format')关联验证:检验列间业务逻辑
# 订单状态为"shipped"时,发货时间必须晚于下单时间 shipped_mask = df['order_status'] == 'shipped' assert (df.loc[shipped_mask, 'shipping_time'] > df.loc[shipped_mask, 'order_time']).all(), \ "Shipped orders have invalid shipping time"业务验证:用已知业务规则校验
# 已知规则:VIP用户月消费应>5000元 vip_mask = df['is_vip'] == True assert df.loc[vip_mask, 'monthly_spend'].mean() > 5000, \ "VIP user average spend below expectation"5. 真实问题排查手册:那些让你凌晨三点爬起来的Bug
5.1 “数据变少了”问题速查表
| 现象 | 最可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
len(df)减少10% | dropna()误删整行 | df.isnull().sum().sort_values(ascending=False) | 改用subset参数指定关键列 |
len(df)减少99% | drop_duplicates()未设subset | df.duplicated().sum() | 明确指定业务主键列 |
len(df)减少但无报错 | pd.merge()默认inner连接 | df_merge = pd.merge(left, right, how='left') | 显式声明how='left' |
len(df)不变但值异常 | fillna()覆盖了有效0值 | df['col'].value_counts(dropna=False) | 用mask()条件填充 |
实战案例:某次ETL任务后数据量骤减,排查发现pd.read_csv()的na_values参数被设为['NULL', 'N/A'],而业务数据中“NULL”是有效字符串(表示未填写),导致整行被标为NaN。解决方案:na_values=[]禁用自动识别,改用df.replace('NULL', np.nan)显式处理。
5.2 “数值全变NaN”问题根因分析
这类问题通常源于类型转换链断裂。典型路径:str → float → int → NaN。例如:
# 原始数据:['1.5', '2', '3.0', 'invalid'] df['col'] = df['col'].astype(float) # 成功:[1.5, 2.0, 3.0, NaN] df['col'] = df['col'].astype(int) # 失败:全部变NaN(int不能存NaN)黄金排查法:
df['col'].apply(type).value_counts()查看原始类型分布df['col'].str.contains(r'[^0-9.]').sum()统计非法字符pd.to_numeric(df['col'], errors='coerce').isna().sum()量化转换失败数
终极解法:永远用pd.to_numeric(..., errors='coerce')替代astype(),并在转换后立即检查:
converted = pd.to_numeric(df['col'], errors='coerce') if converted.isna().sum() > 0: print(f"Failed to convert {converted.isna().sum()} values") print(df['col'][converted.isna()].head(10)) # 输出失败样本5.3 “时间全乱套”问题应急指南
时间错乱是最难调试的问题。我的标准排查流程:
Step1:确认原始格式
# 查看前5行原始值 print(df['time_col'].head().tolist()) # 检查是否含时区信息 print(df['time_col'].str.extract(r'([+-]\d{4})').dropna().head())Step2:验证解析结果
parsed = pd.to_datetime(df['time_col'], errors='coerce') print(f"Parse success rate: {100 * parsed.notna().mean():.1f}%") print(f"Min time: {parsed.min()}, Max time: {parsed.max()}") # 关键!检查是否出现明显错误 if parsed.min() < pd.Timestamp('2000-01-01'): print("Warning: suspiciously old dates detected")Step3:时区对齐
# 若原始无时区,按业务约定设置 parsed = parsed.dt.tz_localize('Asia/Shanghai') # 若需UTC,必须先localize再convert parsed_utc = parsed.dt.tz_convert('UTC')Step4:业务合理性验证
# 检查时间顺序是否颠倒(常见于日志文件乱序) if not parsed.is_monotonic_increasing: print("Warning: timestamps not monotonic!") # 按时间排序并警告 df_sorted = df.sort_values('time_col')5.4 “内存爆了”问题优化方案
当df.info(memory_usage='deep')显示内存超限时,按优先级处理:
P1:字符串列优化
# 将重复字符串转为category for col in df.select_dtypes(include=['object']).columns: if df[col].nunique() / len(df) < 0.5: # 唯一值占比<50% df[col] = df[col].astype('category') # 对长文本截断(如日志字段) df['log_text'] = df['log_text'].str.slice(0, 200)P2:数值列降级
# float64 → float32 for col in df.select_dtypes(include=['float64']).columns: df[col] = pd.to_numeric(df[col], downcast='float') # int64 → int32(需确认无溢出) for col in df.select_dtypes(include=['int64']).columns: if df[col].min() >= -2147483648 and df[col].max() <= 2147483647: df[col] = pd.to_numeric(df[col], downcast='integer')P3:分块处理
# 读取时分块 chunk_list = [] for chunk in pd.read_csv('large.csv', chunksize=10000): cleaned_chunk = pipeline.run(chunk) chunk_list.append(cleaned_chunk) df = pd.concat(chunk_list, ignore_index=True)我个人在实际操作中的体会是:清洗不是追求“完美数据”,而是建立“可信数据边界”。每次清洗后,我都会问自己三个问题:1)这个清洗操作会不会掩盖真实的业务异常?2)如果明天业务规则变更,这个清洗逻辑是否容易调整?3)当新同事接手时,能否在5分钟内理解每行代码的业务意图?答案若是否定的,那就得重写。毕竟,在数据世界里,最危险的不是脏数据,而是看起来很干净的错误数据。
