当前位置: 首页 > news >正文

真实场景下的Python数据清洗:12类高频任务与工程化实践

1. 这不是教科书里的数据清洗——而是我每天在Jupyter里真实敲出来的那几十行

“Common Data Cleaning Tasks in Everyday Work of a Data Scientist/Analyst in Python”——这个标题看起来平平无奇,甚至有点像培训课大纲。但如果你真在一线做过半年以上数据分析或建模工作,就会明白:所谓“常见”,不是指教材里列的那几条定义,而是指你每天早上打开Notebook,还没喝完第一口咖啡,就不得不面对的、带着业务气味的混乱现实。缺失值不是NaN,是销售同事手填Excel时写的“暂无”“待确认”“???”;重复记录不是df.duplicated()一跑就完事,而是同一客户在CRM里被录入三次,电话、邮箱、身份证号各错一位;时间字段不是ISO格式字符串,而是“2023-03-15”“23/03/15”“三月十五日”“昨天”混在同一个字段里。我做过27个不同行业的数据清洗项目,从电商订单流水到医院检验报告,从银行信贷审批表到社区网格员手写台账的OCR识别结果——所有项目里,真正花掉70%以上时间的,从来不是模型调参,而是把原始数据“扶正坐直”,让它能被pandas读进去、算得准、画得出图。这篇文章不讲抽象理论,不列函数手册,只复盘我在真实工单里反复敲、反复改、反复验证过的12类高频清洗场景,每一条都附带:为什么这么写(不是语法,是业务逻辑)、哪一行容易翻车(实测踩坑位置)、怎么一眼看出它没洗干净(验证技巧)、以及——最关键的一点:当业务方突然说“这个字段含义变了”,你该从哪一行代码开始改。适合刚转行的数据新人、想摆脱“清洗靠猜”的中级分析师,以及需要快速交付清洗脚本给下游团队的TL。你不需要背函数,但得知道什么时候该打断点、什么时候该加assert、什么时候该拉业务方一起看原始表头。

2. 整体设计思路:为什么不用“全自动清洗库”,而坚持手写每一段逻辑

2.1 清洗不是标准化流水线,而是带上下文的诊断过程

很多人一上来就想找“最强数据清洗库”,比如dataprepautofeat,甚至尝试用LLM生成清洗代码。我试过——在三个项目里全推倒重写了。原因很简单:清洗的本质不是“把脏数据变干净”,而是把业务逻辑错误显性化、可追溯、可复验。举个真实例子:某次处理用户注册时间字段,自动工具把所有非标准格式统一转成NaT,结果上线后发现漏掉了23%的早期用户(他们注册时系统还没接入时间服务,字段存的是“1970-01-01”)。而手动清洗时,我加了这一段:

# 注册时间字段清洗 —— 关键:区分“无效时间”和“默认时间” df['reg_time'] = pd.to_datetime(df['reg_time'], errors='coerce') # 检查是否大量集中于1970-01-01(Unix纪元起点) if (df['reg_time'].dt.year == 1970).mean() > 0.1: print("⚠️ 警告:10%以上注册时间为1970-01-01,疑似系统未就绪期数据") # 此时必须人工确认:是bug?还是真实时间?是否需单独标记? df['reg_time_flag'] = np.where( df['reg_time'].dt.year == 1970, 'system_not_ready', 'valid_time' )

这段代码的价值不在“转时间”,而在把模糊的业务判断变成可审计的日志。自动库做不到这点——它只会默默吞掉异常,或者报错中断。而真实工作中,你得告诉产品经理:“这1278条1970年注册记录,我们按‘系统未就绪’打标,后续分析会排除,您确认吗?”——这句话,比任何clean_df()函数都重要。

2.2 我的清洗脚本结构:四层防御体系

我所有清洗脚本都强制遵循这个骨架,不是为了炫技,而是为了应对三种最常发生的意外:

  • 意外1:上游数据源字段名/类型突变(如昨天叫user_id,今天叫uid
  • 意外2:业务规则临时调整(如“VIP等级”从1-5级变成A-E级)
  • 意外3:下游模型对缺失值容忍度变化(如原来允许10%缺失,现在要求0%)

所以我的脚本永远包含这四层:

  1. Schema声明层:用字典明确定义每个字段的预期类型、允许值、业务含义
  2. 原始快照层:保存清洗前的df.shape、缺失率、唯一值分布(用df.nunique()/len(df)
  3. 原子操作层:每个清洗动作独立函数,输入df+参数,输出df+日志字典
  4. 断言验证层:每个函数后紧跟assert检查关键约束(如“清洗后user_id不能为空”)

这样做的好处是:当某天凌晨三点报警说“模型AUC暴跌”,你能5分钟内定位到是清洗脚本第37行的fillna()逻辑被上游新字段干扰了,而不是花两小时翻Git历史。下面这张表是我最近一个电商项目清洗脚本的结构对照:

层级代码位置核心作用实际案例
Schema声明SCHEMA = { "order_id": {"dtype": "str", "required": True}, "amount": {"dtype": "float", "min": 0.01} }定义字段契约,作为所有清洗的基准当上游新增order_amt_yuan字段,脚本立即报错“未知字段”,而非静默跳过
原始快照raw_stats = { "n_rows": len(df), "null_rate": df.isnull().mean().to_dict() }记录清洗前状态,用于对比验证发现phone字段清洗后缺失率从5%升到12%,立刻回溯是正则替换过度
原子操作def clean_phone(df: pd.DataFrame) -> Tuple[pd.DataFrame, dict]: ...单一职责,可单独测试、复用、调试同一clean_phone函数,在用户表、客服通话表、物流单表中复用
断言验证assert df['order_id'].notna().all(), "order_id存在空值"强制校验清洗结果符合业务底线防止因fillna('')导致ID变为空字符串,后续join失效

提示:不要把断言写成assert not df['order_id'].isna().any()——这种写法报错时只显示AssertionError,你得再跑一遍才知道哪行出问题。一定要写成带描述的assert ... , "错误信息",这是节省调试时间的关键细节。

2.3 为什么拒绝“清洗-建模”一体化Pipeline

很多教程推荐用sklearn.Pipeline把清洗和模型打包。我在生产环境坚决不用,原因有三:

  1. 调试成本指数级上升:当模型预测异常,你得在Pipeline里一层层set_params()去排查,而实际中90%的问题出在清洗环节(比如StandardScaler对含空值的列报错,但错误堆栈指向模型层);
  2. 版本管理灾难:清洗逻辑迭代快(每周可能改3次),模型迭代慢(每月1次),绑在一起会导致每次清洗小修都要触发模型重新训练和部署;
  3. 协作壁垒:数据工程师要部署清洗脚本到Airflow,算法工程师要调用清洗后数据,如果混在Pipeline里,双方得协调Python环境、依赖版本、甚至Jupyter内核配置。

我的解法是:清洗脚本输出Parquet文件 + 数据质量报告(JSON),模型代码只读取清洗后的Parquet。两者通过文件路径和schema.json解耦。例如:

# 清洗脚本输出 /data/cleaned/orders_20240515.parquet # 清洗后数据 /data/cleaned/orders_20240515_report.json # 包含缺失率、异常值数量、清洗耗时等

模型代码只需:

df = pd.read_parquet("/data/cleaned/orders_20240515.parquet") # 不关心怎么洗的,只关心数据是否达标 with open("/data/cleaned/orders_20240515_report.json") as f: report = json.load(f) assert report["null_rate"]["amount"] < 0.01, "金额字段缺失超阈值"

这种解耦让清洗团队可以独立发布、灰度、回滚,而算法团队专注特征工程——这才是真实团队协作的常态。

3. 核心清洗任务拆解:12类高频场景的实操细节与避坑指南

3.1 缺失值处理:别急着fillna(),先问“它为什么空”

缺失值是最容易被草率处理的部分。新手常犯的错误是:看到df.isnull().sum()就直接df.fillna(0)df.dropna()。我在某金融风控项目里因此返工过两次——第一次用fillna(0)填充“逾期天数”,结果把“从未借款”的用户和“逾期0天”的用户混为一谈;第二次用dropna()删掉“收入”为空的记录,却漏掉了大量自由职业者(他们收入字段存的是“面议”“协商”)。

正确流程是三步诊断法

  1. 分类缺失类型(业务驱动,非技术驱动):

    • 结构性缺失:字段本就不适用于该记录(如“孕妇并发症”字段对男性用户为空)→ 应填充"N/A"pd.NA
    • 采集性缺失:本该有但没采到(如用户跳过填写“年收入”)→ 需分析缺失模式(是否与年龄/地域强相关?)
    • 逻辑性缺失:值存在但无法解析(如“2023-13-01”)→ 先尝试修复,再判断是否真缺失
  2. 量化缺失影响(用数据说话,不是拍脑袋):

# 计算每个字段缺失率,并关联业务指标 def analyze_null_impact(df: pd.DataFrame, target_col: str = "is_churn") -> pd.DataFrame: null_stats = df.isnull().mean().sort_values(ascending=False) # 关键:计算缺失组 vs 非缺失组的目标变量差异 impact = {} for col in null_stats.index[:10]: # 只看缺失率最高的10个 if null_stats[col] > 0.01: # 缺失率>1% missing_target = df[df[col].isna()][target_col].mean() non_missing_target = df[~df[col].isna()][target_col].mean() impact[col] = { "null_rate": null_stats[col], "churn_rate_missing": missing_target, "churn_rate_non_missing": non_missing_target, "delta": abs(missing_target - non_missing_target) } return pd.DataFrame(impact).T.sort_values("delta", ascending=False) # 实际输出示例: # null_rate churn_rate_missing churn_rate_non_missing delta # last_login_days 0.123 0.456 0.123 0.333 # income 0.087 0.210 0.205 0.005

看到last_login_days缺失组流失率是正常组的3.7倍,你就知道:这个缺失不是随机噪声,而是强信号,应该创建is_last_login_missing特征,而不是简单填充。

  1. 选择填充策略(按优先级排序):
    • 业务规则填充(最高优):如“注册渠道”为空,但用户手机号归属地是浙江,且浙江用户99%来自“微信小程序”,则填"wechat_mini"
    • 统计量填充(次优):用中位数(数值型)或众数(分类型),但必须加后缀标记,如amount_median_filled
    • 模型预测填充(慎用):仅当缺失率<5%且有强相关特征时,用XGBoost预测缺失值,但必须验证预测误差<业务容忍度

注意:永远不要用df.fillna(method="ffill")处理时间序列外的字段!我在某次处理用户等级变更日志时误用了,导致把“VIP3→VIP4”的记录,用上一条“VIP1→VIP2”的时间填充,整个用户生命周期分析全错。记住:ffill只适用于明确有序的时序数据,其他场景一律禁用。

3.2 重复记录识别:不只是df.duplicated(),要看业务语义

df.duplicated(subset=["user_id", "order_id"])能解决80%的重复,但剩下20%才是坑。比如电商订单表:

  • 场景1:同一订单,支付成功和支付失败各记一条
    字段几乎全同,只有statusupdate_time不同。技术上不重复,但业务上应保留status=="success"的那条。

  • 场景2:用户下单后修改地址,产生两条记录
    order_id相同,但shipping_address不同。此时不能删,而要标记为“地址变更版本”。

我的解决方案是:先做技术去重,再做业务去重

# 步骤1:技术去重(保留最新时间) df_sorted = df.sort_values(["order_id", "update_time"], ascending=[True, False]) df_dedup_tech = df_sorted.drop_duplicates(subset=["order_id"], keep="first") # 步骤2:业务去重(识别需保留多条的场景) # 规则:同一order_id下,若status包含"success",则只留success记录 def business_dedup(group): if (group["status"] == "success").any(): return group[group["status"] == "success"].iloc[0:1] else: return group.iloc[0:1] # 否则留最早一条 df_final = df_dedup_tech.groupby("order_id", group_keys=False).apply(business_dedup)

更关键的是建立重复检测的SOP:每次清洗前,固定运行这三行代码,把结果写入日志:

# 重复检测黄金三行 print("🔍 技术重复(全字段):", df.duplicated().sum()) print("🔍 业务重复(关键字段):", df.duplicated(subset=["user_id", "product_id", "order_date"]).sum()) print("🔍 潜在冲突(同ID不同值):", df.groupby("user_id").filter(lambda x: x["phone"].nunique() > 1).shape[0])

最后一行特别重要——它揪出“同一用户ID对应多个手机号”的脏数据,这往往是CRM和APP数据未打通的征兆,必须反馈给数据治理团队。

3.3 文本字段清洗:正则不是万能的,业务词典才是核心

文本清洗最容易陷入“狂写正则”的陷阱。比如清洗用户留言中的联系方式:

# 错误示范:试图用一个正则匹配所有手机号 df["message_clean"] = df["message"].str.replace(r"1[3-9]\d{9}", "[PHONE]", regex=True) # 问题:漏掉座机(010-12345678)、微信ID(wxid_abc123)、邮箱(user@domain.com)

我的做法是:分层清洗 + 业务词典兜底

  1. 第一层:标准化格式(消除书写差异)

    # 统一空格、换行、全角字符 df["message"] = df["message"].str.replace(r"\s+", " ", regex=True) # 多空格→单空格 df["message"] = df["message"].str.replace(r"[,。!?;:""'‘’“”()\[\]{}]", "", regex=True) # 删中文标点 df["message"] = df["message"].str.replace(r"[^\x00-\x7F]+", "", regex=True) # 删emoji和生僻字(可选)
  2. 第二层:实体识别(用业务词典,非NER模型)
    构建contact_terms.txt(业务方确认的联系方式关键词):

    手机号 微信号 QQ号 邮箱 电话 联系方式

    然后:

    # 加载词典 with open("contact_terms.txt") as f: contact_keywords = [line.strip() for line in f if line.strip()] # 标记含联系方式的记录 df["has_contact_hint"] = df["message"].str.contains("|".join(contact_keywords), case=False, na=False)
  3. 第三层:精准脱敏(正则只处理已知模式)

    # 分别处理不同模式,便于调试 df["message"] = df["message"].str.replace(r"1[3-9]\d{9}", "[MOBILE]", regex=True) # 国内手机号 df["message"] = df["message"].str.replace(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", regex=True) # 邮箱 df["message"] = df["message"].str.replace(r"(?:微信|WX|VX)[::\s]*([A-Za-z0-9_]+)", r"[WECHAT:\1]", regex=True) # 微信号

实操心得:永远把正则替换写成str.replace(pattern, replacement, regex=True),不要省略regex=True参数!我曾在某次升级pandas后发现替换失效,排查3小时才发现新版本默认regex=False,而旧代码没显式声明。

3.4 时间字段解析:从混乱字符串到可计算的datetime

时间字段是清洗中最痛苦的部分。我见过的格式包括:"2023-03-15T14:30:00Z""15/03/2023 14:30""2023年3月15日""昨天14:30""20230315""Mar 15, 2023"……pd.to_datetime()errors='coerce'只能帮你把错的变NaT,但你得知道哪些是真错误,哪些是格式差异。

我的标准流程是:先聚类,再分治,最后验证

  1. 聚类分析(用value_counts观察分布):
# 取前1000条样本,看时间字段格式分布 sample_times = df["event_time"].dropna().head(1000).astype(str) format_dist = sample_times.str.extract(r"^(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})").dropna().shape[0] print(f"YYYY-MM-DD类占比: {format_dist/1000:.1%}") # 同样方法检查其他格式:YYYYMMDD、DD/MM/YYYY等
  1. 分治解析(为每种主流格式写专用解析器):
def parse_event_time(series: pd.Series) -> pd.Series: # 创建结果数组,初始为NaT result = pd.Series(pd.NaT, index=series.index, dtype="datetime64[ns]") # 规则1:ISO格式(最快,先匹配) iso_mask = series.str.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}") result.loc[iso_mask] = pd.to_datetime(series.loc[iso_mask], errors="coerce") # 规则2:YYYY-MM-DD ymd_mask = series.str.match(r"^\d{4}-\d{1,2}-\d{1,2}") result.loc[ymd_mask] = pd.to_datetime(series.loc[ymd_mask], format="%Y-%m-%d", errors="coerce") # 规则3:YYYY/MM/DD ymd_slash_mask = series.str.match(r"^\d{4}/\d{1,2}/\d{1,2}") result.loc[ymd_slash_mask] = pd.to_datetime(series.loc[ymd_slash_mask], format="%Y/%m/%d", errors="coerce") # 规则4:中文日期(需先替换) cn_mask = series.str.contains("年|月|日") if cn_mask.any(): # 替换中文字符为英文符号 temp_series = series.loc[cn_mask].str.replace("年", "-").str.replace("月", "-").str.replace("日", "") result.loc[cn_mask] = pd.to_datetime(temp_series, format="%Y-%m-%d", errors="coerce") return result df["event_time_parsed"] = parse_event_time(df["event_time"])
  1. 验证与兜底(确保没有意外):
# 检查解析后是否还有大量NaT null_after_parse = df["event_time_parsed"].isna().sum() if null_after_parse > len(df) * 0.05: # 超5%未解析 print(f"⚠️ 解析失败{null_after_parse}条,查看未解析样本:") print(df[df["event_time_parsed"].isna()]["event_time"].head(10)) # 此时必须人工介入,补充新规则

注意:永远不要用infer_datetime_format=True!它在pandas 1.5+版本中已被弃用,且在混合格式下极易出错。显式指定format参数虽然多写几行,但稳定性和可读性高十倍。

3.5 数值字段清洗:警惕“看起来是数字”的陷阱

"123.45"是数字,"123.45元"不是,"123,456.78"也不是,"1.23e+05"是,"N/A"不是……数值清洗的难点在于:字符串和数字的边界在业务中是模糊的

我的检查清单:

  1. 先看数据类型df["price"].dtype):如果是object,别急着astype(float),先探查内容;
  2. pd.api.types.is_numeric_dtype()验证:它比dtype == 'float64'更准确;
  3. pd.to_numeric(errors='coerce')转换,但必须配合isna()分析失败原因

实战代码:

def clean_numeric_col(series: pd.Series, col_name: str) -> pd.Series: # 步骤1:移除常见干扰字符 cleaned = series.astype(str).str.replace(r"[¥$€, ]", "", regex=True) # 删货币符号、逗号、空格 # 步骤2:处理特殊标记 cleaned = cleaned.str.replace(r"^(?i)(null|none|nan|n/a|—)$", "NaN", regex=True) # 步骤3:转换为数值 numeric_series = pd.to_numeric(cleaned, errors="coerce") # 步骤4:分析转换失败情况 failed_mask = cleaned.isna() & series.notna() # 原始不空,但转换后空 if failed_mask.sum() > 0: print(f"🔍 {col_name} 转换失败{failed_mask.sum()}条,样本:") print(series[failed_mask].head(5).tolist()) # 常见失败:单位混入("123kg")、范围值("100-200")、分数("1/2") return numeric_series df["price_clean"] = clean_numeric_col(df["price"], "price")

对于“范围值”这类难题(如"100-200"),我从不强行拆分。而是创建新特征:

# 提取范围信息 df["price_min"] = df["price"].str.extract(r"(\d+)-\d+").astype(float) df["price_max"] = df["price"].str.extract(r"\d+-(\d+)").astype(float) df["price_is_range"] = df["price"].str.contains("-")

这样既保留了原始信息,又提供了可计算的数值,业务方要均值还是区间,自己选。

3.6 分类字段标准化:别迷信map(),要建业务映射表

df["gender"].map({"M": "Male", "F": "Female", "m": "Male"})看似简洁,但当业务方说“从下周起,增加‘Non-binary’选项,代码是‘NB’”时,你得改三处:map字典、缺失值处理、下游模型label编码。

我的方案是:用CSV维护业务映射表gender_mapping.csv):

raw_value,standard_value,description M,Male,男性 F,Female,女性 m,Male,男性(小写) f,Female,女性(小写) NB,Non-binary,非二元性别 Unknown,Unknown,未知

然后清洗时动态加载:

def standardize_category(series: pd.Series, mapping_file: str, col_name: str) -> pd.Series: mapping_df = pd.read_csv(mapping_file) # 创建映射字典,注意处理大小写 map_dict = dict(zip(mapping_df["raw_value"].str.lower(), mapping_df["standard_value"])) # 应用映射(忽略大小写) standardized = series.astype(str).str.lower().map(map_dict) # 标记未映射项 unmapped_mask = standardized.isna() & series.notna() if unmapped_mask.sum() > 0: print(f"⚠️ {col_name} 存在{unmapped_mask.sum()}个未映射值:{series[unmapped_mask].unique()}") # 此时必须更新mapping.csv,不能硬编码 return standardized df["gender_std"] = standardize_category(df["gender"], "gender_mapping.csv", "gender")

优势:业务方可以直接编辑CSV,无需动代码;审计时可追溯每次映射变更;新增值时,脚本会主动报错提醒,而不是静默填NaN

3.7 异常值检测:IQR和Z-score只是起点,业务阈值才是终点

df["age"].between(0, 120)zscore < 3有用得多。我在某健康APP项目中,用Z-score筛出“年龄异常”,结果把一群102岁的老红军用户标记为异常——他们的数据完全真实,只是超出了常规分布。

我的异常值处理铁律:先定业务阈值,再用统计方法辅助发现

  1. 业务阈值优先(必须由业务方签字确认):

    • 年龄:0-150岁(医疗场景允许更高)
    • 订单金额:≥0.01元(低于1分钱视为无效)
    • 登录次数:0-1000次/天(超过需人工审核)
  2. 统计方法辅助(仅用于发现“阈值外的合理值”):

    # 对金额字段,业务阈值是0.01-100000,但用IQR找中间异常 Q1 = df["amount"].quantile(0.25) Q3 = df["amount"].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 标记:在业务阈值内,但IQR外的值(可能是新业务模式) df["amount_outlier_iqr"] = ( (df["amount"] < lower_bound) | (df["amount"] > upper_bound) ) & df["amount"].between(0.01, 100000)
  3. 处理策略分层

    • 业务阈值外:直接dropclip(如df["age"] = df["age"].clip(0, 150)
    • IQR外但业务内:创建is_amount_suspicious特征,供风控模型使用
    • 两者都外:人工复核(如amount=0status=="paid",明显矛盾)

3.8 ID类字段清洗:去重只是开始,一致性才是关键

ID字段(用户ID、订单ID、设备ID)清洗的核心不是“有没有重复”,而是“跨表ID是否一致”。比如用户表里user_id="U123",订单表里却是user_id="u123"(大小写不一致),join时就全为空。

我的ID清洗五步法:

  1. 统一大小写(除非业务规定区分):

    df["user_id"] = df["user_id"].str.upper() # 或lower(),全项目统一
  2. 清理不可见字符(最隐蔽的坑):

    # 删除零宽空格、BOM头等 df["user_id"] = df["user_id"].str.replace(r"[\u200b\u200c\u200d\uFEFF]", "", regex=True)
  3. 标准化分隔符(如"U-123""U123"):

    df["user_id"] = df["user_id"].str.replace(r"[^A-Za-z0-9]", "", regex=True)
  4. 长度校验(业务方确认的ID长度):

    expected_len = 5 df["user_id_len_ok"] = df["user_id"].str.len() == expected_len if (~df["user_id_len_ok"]).sum() > 0: print("❌ 用户ID长度异常,请检查:", df[~df["user_id_len_ok"]]["user_id"].unique())
  5. 跨表一致性检查(关键!):

    # 假设有user_df和order_df user_ids_in_orders = set(order_df["user_id"].unique()) valid_user_ids = set(user_df["user_id"].unique()) orphan_orders = user_ids_in_orders - valid_user_ids if orphan_orders: print(f"⚠️ 订单表存在{len(orphan_orders)}个用户ID在用户表中不存在")

3.9 地址字段清洗:别追求“完美标准化”,要保“可分组性”

把“北京市朝阳区建国路8号SOHO现代城C座2305室”标准化成“北京/朝阳/建国路/8号/2305”是理想,但现实中,你更需要的是:让“朝阳区”和“朝阳路”能被分到同一地理层级

我的地址清洗策略是:分层提取 + 业务关键词匹配

  1. 分层提取(用正则抓大放小):

    # 提取省级(省/自治区/直辖市) df["province"] = df["address"].str.extract(r"(北京市|上海市|广东省|新疆维吾尔自治区)") # 提取市级(市/自治州/盟) df["city"] = df["address"].str.extract(r"(北京市|广州市|深圳市|杭州市)") # 提取区级(区/县/旗) df["district"] = df["address"].str.extract(r"(朝阳区|福田区|西湖区|武侯区)")
  2. 业务关键词兜底(处理简写和别名):

    # 创建区级别名映射 district_aliases = { "朝阳": "朝阳区", "福田": "福田区", "杭城": "杭州市", "魔都": "上海市" } df["district"] = df["district"].fillna( df["address"].str.extract(f"({'|'.join(district_aliases.keys())})")[0] .map(district_aliases) )
  3. 创建地理分组码(核心产出):

    # 生成可用于聚类的code df["geo_code"] = ( df["province"].str[:2] + "_" + df["city"].str[:2] + "_" + df["district"].str[:2].fillna("XX") ) # 结果如:"北_北_朝"、"广_深_福"

这样即使地址没完全标准化,geo_code也能保证同区域用户被分到一组,满足90%的分析需求。

3.10 多语言混合文本清洗:中文为主,英文为辅,其他字符谨慎处理

国内项目常遇到中英混排(“订单状态:Order Status”)、中日韩字符(“東京”)、甚至阿拉伯数字(“١٢٣”)。我的原则:中文环境以中文为准,英文仅作辅助,其他文字除非业务必需,否则统一转拼音或删除

清洗步骤:

  1. 检测主要语言(用langdetect库):

    from langdetect import detect def detect_lang(text): try: return detect(text) except: return "unknown" df["lang"] = df["title"].apply(detect_lang)
  2. 按语言分流处理

    # 中文为主:保留中文,删英文括号内内容 df.loc[df["lang"]=="zh", "title_clean"] = df["title"].str.replace(r"([^)]*)", "", regex=True) # 英文为主:转小写,删中文字符 df.loc[df["lang"]=="en", "title_clean"] = df["title"].str.replace(r"[^\x00-\x7F]", "", regex=True).str.lower()

3

http://www.jsqmd.com/news/1024492/

相关文章:

  • AI都生成代码了,Java程序员还学基础干嘛?讲点扎心的大实话
  • 2026年郑州企业获客新赛道|短视频+GEO+AI智能体完整解决方案对标分析 - 精选优质企业推荐官
  • 2026保姆级EPS转PDF教程!在线/PS/AI/Python全方法汇总 - 办公小帮手
  • 北京迷你仓短租服务推荐 装修搬家存物实用指南 - 速递信息
  • 从本地到生产:迁移到 GitHub Actions 自动化 CI/CD,总结了这 5 个坑
  • 如何借助微信投票制作平台制作投票活动?2026最新免费教程|防刷+批量导入+图文视频全支持 - 微信投票小程序
  • PingFangSC跨平台字体架构:现代Web应用的中文字体解决方案
  • 设计师与开发者的对话:Marketch如何让Sketch设计稿自动“开口说话“?
  • Docker,容器,容器化,DevOps,虚拟化
  • FigmaCN终极指南:3分钟免费搞定Figma中文汉化
  • 3大核心功能深度解析:Scan Tailor如何让扫描文档处理效率提升500%
  • AI标书软件技术原理解析:从招标文件解析到标书生成的全链路技术拆解 - 陈工0237
  • 杭州索川科技:专业电摩控制器与电机测试台解决方案
  • 福州奢侈品黄金回收商家实力榜单2026综合测评:综合实力榜首花落谁家 - 奢侈品回收评测
  • 亨得利官方辟谣避坑全指南:线上虚假广告实地核查 + 真伪辨别教程(推荐收藏备用) - 亨得利官方维修中心
  • 普通学生学AI,重点是把工具变成解决问题的能力
  • Python SSL与TLS安全连接实现细节
  • 2026年6月青岛奢侈品回收分级测评!7家正规平台评级,最优口碑出炉 - 薛定谔的梨花猫
  • LVI-SAM实战:从传感器标定到参数调优,跑通自定义数据全指南
  • 2026 智能外呼系统实测排行:综合能力出众,数企 AI 成企业降本优选 - 兔兔不是荼荼
  • 2026中小艺培校长亲测:培训机构管理系统避坑指南,搞定排课家校
  • 藏饰盘活不踩坑|2026哈尔滨首饰回收实测排行与行情解析 - 名奢变现站
  • 2026年营口鲅鱼圈区防身格斗培训真实测评与挑选标准 - 速递信息
  • 2026珠海甲醛治理品牌测评:海景房高盐高湿环境7大技术指标实测,谁扛得住回南天 - 环保除醛知识库
  • 5分钟获取免费OpenAI API密钥的终极指南:零成本解锁AI开发能力
  • 开发者有必要长期用 ChatGPT Plus 吗?从 Debug、代码解释和数据安全说清楚
  • 单视频生成多样性内容的技术原理与边界
  • 2026年青岛品牌首饰回收TOP榜|七家机构硬核实测 添价收黄金奢侈品回收最值得托付 - 薛定谔的梨花猫
  • Java 三大修饰符
  • 2026年全国知名中空板厂家行业三大趋势解读 - 速递信息