☰
Pandas教学-5:数据清洗总结
2026/10/13 9:13:41 网站建设 项目流程
# -*- coding: utf-8 -*- """ 案例6:日期格式统一与信息提取 清洗场景: 同一列日期写法五花八门——"2024/3/1"、"2024-03-05"、"20240308"、 "2024.03.12"、"2024年3月15日",还有一个完全无法识别的错误值 知识点: 1. pandas 2.x 解析“多种格式混用”的日期列,必须加 format="mixed", 否则它只按第一个值的格式去套,其他写法会全部失败(本课重点踩坑点) 2. 中文“2024年3月15日”要先用正则把 年/月/日 替换成统一分隔符 3. pd.to_datetime(errors="coerce"):识别不了的变成 NaT(空日期),不报错 4. dt.year/month/day/day_name() 提取年、月、日、星期;日期相减得相隔天数 运行后: 生成 data/订单日期_脏数据.xlsx 和 data/订单日期_清洗结果.xlsx """ import os import pandas as pd BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") os.makedirs(DATA_DIR, exist_ok=True) SRC_FILE = os.path.join(DATA_DIR, "订单日期_脏数据.xlsx") OUT_FILE = os.path.join(DATA_DIR, "订单日期_清洗结果.xlsx") def make_sample(): df = pd.DataFrame({ "订单号": ["D001", "D002", "D003", "D004", "D005", "D006"], # 同一种日期,多种不同写法,最后一个是错误值 "下单日期": ["2024/3/1", "2024-03-05", "20240308", "2024.03.12", "2024年3月15日", "二零二四年"], "发货日期": ["2024-03-03", "2024-03-06", "2024-03-09", "2024-03-15", "2024-03-18", "2024-03-20"], }) df.to_excel(SRC_FILE, sheet_name="原始数据", index=False) print("已生成示例文件:", SRC_FILE) def clean(): raw = pd.read_excel(SRC_FILE, sheet_name="原始数据") df = raw.copy() # ---------- 1. 先处理中文格式:年/月/日 -> “-” ---------- # 2024年3月15日 -> 2024-3-15-,再去掉结尾多余的“-” -> 2024-3-15 df["下单日期"] = (df["下单日期"].astype(str) .str.replace(r"[年月]", "-", regex=True) .str.replace("日", "", regex=False) .str.rstrip("-")) # ---------- 2. 混合格式统一转 datetime ---------- # format="mixed":允许每行用不同日期格式;errors="coerce":坏值变 NaT df["下单日期"] = pd.to_datetime(df["下单日期"], format="mixed", errors="coerce") df["发货日期"] = pd.to_datetime(df["发货日期"], errors="coerce") bad_date = df[df["下单日期"].isna()] print("===== 无法识别的日期(NaT) =====") print(bad_date[["订单号"]].to_string(index=False)) # ---------- 3. 无法识别的日期做兜底处理(教学演示用固定值填充) ---------- # 实际工作中更常见的做法是删除该行或联系数据提供方核实 df["下单日期"] = df["下单日期"].fillna(pd.Timestamp("2024-03-01")) # ---------- 4. 从日期中提取年、月、日、星期几 ---------- df["下单年份"] = df["下单日期"].dt.year df["下单月份"] = df["下单日期"].dt.month df["下单日"] = df["下单日期"].dt.day week_map = {"Monday": "星期一", "Tuesday": "星期二", "Wednesday": "星期三", "Thursday": "星期四", "Friday": "星期五", "Saturday": "星期六", "Sunday": "星期日"} df["星期几"] = df["下单日期"].dt.day_name().map(week_map) # ---------- 5. 日期相减:下单到发货隔了几天 ---------- df["发货耗时_天"] = (df["发货日期"] - df["下单日期"]).dt.days # ---------- 6. 日期转成统一字符串格式写回 Excel ---------- df["下单日期"] = df["下单日期"].dt.strftime("%Y-%m-%d") df["发货日期"] = df["发货日期"].dt.strftime("%Y-%m-%d") print("\n===== 清洗后 =====") print(df.to_string(index=False)) with pd.ExcelWriter(OUT_FILE, engine="openpyxl") as writer: raw.to_excel(writer, sheet_name="清洗前", index=False) df.to_excel(writer, sheet_name="清洗后", index=False) print("\n清洗结果已写入:", OUT_FILE) if __name__ == "__main__": make_sample() clean()
# -*- coding: utf-8 -*- """ 案例7:异常值(离群值)检测与处理 清洗场景: 录入成绩时多敲了一个 0、年龄写成 200——这些“离谱数据”会严重拉偏平均值 知识点: 1. 业务范围规则:成绩必须在 0~100,年龄必须在 15~60(最直观、最常用) 2. IQR 箱线图法:Q1 - 1.5*IQR 与 Q3 + 1.5*IQR 之外视为离群值 3. 三种处理方式对比:标记出来 / 删除 / clip() 截断到边界 运行后: 生成 data/体检成绩_脏数据.xlsx 和 data/体检成绩_清洗结果.xlsx """ import os import pandas as pd BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") os.makedirs(DATA_DIR, exist_ok=True) SRC_FILE = os.path.join(DATA_DIR, "体检成绩_脏数据.xlsx") OUT_FILE = os.path.join(DATA_DIR, "体检成绩_清洗结果.xlsx") def make_sample(): df = pd.DataFrame({ "学号": ["A01", "A02", "A03", "A04", "A05", "A06", "A07", "A08"], "姓名": ["张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"], "年龄": [18, 19, 17, 18, 200, 19, 18, 5], # 200 和 5 明显异常 "成绩": [85, 90, 78, 95, 88, 150, 72, 82], # 150 超出满分 }) df.to_excel(SRC_FILE, sheet_name="原始数据", index=False) print("已生成示例文件:", SRC_FILE) def clean(): raw = pd.read_excel(SRC_FILE, sheet_name="原始数据") print("===== 原始数据(注意平均值被异常值带偏) =====") print(raw.to_string(index=False)) print("年龄平均值:", raw["年龄"].mean(), " 成绩平均值:", raw["成绩"].mean()) df = raw.copy() # ---------- 方法一:业务范围规则,先把异常值“揪出来”标记 ---------- df["年龄异常"] = ~df["年龄"].between(15, 60) # ~ 表示“不满足” df["成绩异常"] = ~df["成绩"].between(0, 100) bad_rows = df[df["年龄异常"] | df["成绩异常"]] print("\n===== 范围规则发现的异常行 =====") print(bad_rows[["学号", "姓名", "年龄", "成绩"]].to_string(index=False)) # ---------- 方法二:clip() 截断 —— 超出边界的值压回边界内 ---------- # 例如年龄 200 -> 60,成绩 150 -> 100;下限同理 df["年龄_截断"] = df["年龄"].clip(15, 60) df["成绩_截断"] = df["成绩"].clip(0, 100) # ---------- 方法三(选学):IQR 箱线图法自动找离群值 ---------- # 以“成绩_截断”列为例(先用业务规则处理过明显错误,再找统计离群值) q1 = df["成绩_截断"].quantile(0.25) q3 = df["成绩_截断"].quantile(0.75) iqr = q3 - q1 low, high = q1 - 1.5 * iqr, q3 + 1.5 * iqr df["IQR离群"] = ~df["成绩_截断"].between(low, high) print(f"\nIQR 判定区间:[{low:.1f}, {high:.1f}],区间外即离群值") # ---------- 整理最终结果:保留截断后的列,去掉临时标记列 ---------- result = df[["学号", "姓名", "年龄_截断", "成绩_截断"]] result.columns = ["学号", "姓名", "年龄", "成绩"] # 改回规范列名 print("\n===== 截断清洗后 =====") print(result.to_string(index=False)) print("清洗后年龄平均值:", result["年龄"].mean(), " 成绩平均值:", round(result["成绩"].mean(), 1)) with pd.ExcelWriter(OUT_FILE, engine="openpyxl") as writer: raw.to_excel(writer, sheet_name="清洗前", index=False) df.to_excel(writer, sheet_name="异常标记过程", index=False) result.to_excel(writer, sheet_name="清洗后", index=False) print("\n清洗结果已写入:", OUT_FILE) if __name__ == "__main__": make_sample() clean()
# -*- coding: utf-8 -*- """ 案例8:文本字段标准化 清洗场景: 同一个部门有多种叫法(技术部 / 技术 / 研发部 / Tech)、 邮箱大小写混用、手机号带空格和横线、全角字符混排 知识点: 1. str.strip() 去空格、str.upper()/lower() 统一大小写 2. unicodedata.normalize("NFKC") 全角转半角 3. replace(映射字典) 把别名统一成标准名称 4. str.replace(正则) 去掉手机号中的空格、横线 5. isin() 检查是否还有“不认识”的类别 运行后: 生成 data/会员信息_脏数据.xlsx 和 data/会员信息_清洗结果.xlsx """ import os import unicodedata import pandas as pd BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") os.makedirs(DATA_DIR, exist_ok=True) SRC_FILE = os.path.join(DATA_DIR, "会员信息_脏数据.xlsx") OUT_FILE = os.path.join(DATA_DIR, "会员信息_清洗结果.xlsx") # 部门别名 -> 标准名称 的映射表(以后新增别名只改这里) DEPT_MAP = { "技术部": "技术部", "技术": "技术部", "研发部": "技术部", "tech": "技术部", "市场部": "市场部", "市场": "市场部", "营销部": "市场部", "marketing": "市场部", "人事部": "人事部", "人力资源部": "人事部", "hr": "人事部", } def make_sample(): df = pd.DataFrame({ "姓名": ["张三", " 李四 ", "Wang Wu"], "部门": ["技术", "研发部", "Tech"], # 最后一个是全角英文 "邮箱": ["ZS@Example.COM", "ls@example.com", "ww@Example.com"], "手机号": ["138-0000-0001", " 139 0000 0002", "13700000003"], }) df.to_excel(SRC_FILE, sheet_name="原始数据", index=False) print("已生成示例文件:", SRC_FILE) def to_half_width(text): """全角字符转半角:Tech -> Tech""" return unicodedata.normalize("NFKC", str(text)) def clean(): raw = pd.read_excel(SRC_FILE, sheet_name="原始数据") df = raw.copy() # ---------- 1. 姓名:转半角、去首尾空格 ---------- df["姓名"] = df["姓名"].map(to_half_width).str.strip() # ---------- 2. 部门:去空格 -> 全角转半角 -> 英文小写 -> 别名映射 ---------- df["部门"] = (df["部门"].map(to_half_width) .str.strip() .str.lower() .replace(DEPT_MAP)) # ---------- 3. 邮箱:统一转小写(邮箱大小写不敏感,统一后才能去重) ---------- df["邮箱"] = df["邮箱"].str.strip().str.lower() # ---------- 4. 手机号:正则删除所有非数字字符(空格、横线等) ---------- df["手机号"] = df["手机号"].str.replace(r"\D", "", regex=True) # 校验:清洗后必须是 11 位数字 df["手机号合法"] = df["手机号"].str.fullmatch(r"1\d{10}").notna() # ---------- 5. 检查部门列里是否还有映射表“不认识”的值 ---------- unknown = set(df["部门"]) - set(DEPT_MAP.values()) print("未识别的部门类别:", unknown if unknown else "无,全部标准化成功") print("\n===== 清洗后 =====") print(df.to_string(index=False)) with pd.ExcelWriter(OUT_FILE, engine="openpyxl") as writer: raw.to_excel(writer, sheet_name="清洗前", index=False) df.to_excel(writer, sheet_name="清洗后", index=False) print("\n清洗结果已写入:", OUT_FILE) if __name__ == "__main__": make_sample() clean()
# -*- coding: utf-8 -*- """ 案例9:列名整理、字段筛选与重排 清洗场景: 从别人那里拿到的表,列名带空格、中英混用,还混着空列和备注列, 直接分析既容易出错又不方便 知识点: 1. 列名批量清洗:去空格、统一英文列名为中文(rename 字典) 2. 删除全空列:dropna(how="all", axis=1) 3. 只保留需要的列,并按指定顺序重新排列 4. drop_duplicates() 对单列去重、reset_index() 重置行号 运行后: 生成 data/课程登记_脏数据.xlsx 和 data/课程登记_清洗结果.xlsx """ import os import numpy as np import pandas as pd BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") os.makedirs(DATA_DIR, exist_ok=True) SRC_FILE = os.path.join(DATA_DIR, "课程登记_脏数据.xlsx") OUT_FILE = os.path.join(DATA_DIR, "课程登记_清洗结果.xlsx") def make_sample(): df = pd.DataFrame({ " ID ": [1, 2, 3, 3], # 列名带空格,且 ID=3 重复登记 "Name": ["张三", "李四", "王五", "王五"], "课程 ": ["Python", "Python", "Excel", "Excel"], "备注": ["", np.nan, "请假", "请假"], # 基本无用的备注列 "空列": [np.nan, np.nan, np.nan, np.nan], # 全空列 }) df.to_excel(SRC_FILE, sheet_name="原始数据", index=False) print("已生成示例文件:", SRC_FILE) def clean(): raw = pd.read_excel(SRC_FILE, sheet_name="原始数据") print("原始列名:", list(raw.columns)) df = raw.copy() # ---------- 1. 列名批量去空格 ---------- df.columns = df.columns.str.strip() print("去空格后:", list(df.columns)) # ---------- 2. 英文列名重命名为中文 ---------- df = df.rename(columns={"ID": "编号", "Name": "姓名"}) # ---------- 3. 删除全空列(how="all" 表示整列都是空才删) ---------- df = df.dropna(how="all", axis=1) # ---------- 4. 只保留需要的列,并顺便指定最终顺序 ---------- keep_cols = ["编号", "姓名", "课程"] df = df[keep_cols] # ---------- 5. 文本内容去空格,按“编号”去重(重复登记只留一条) ---------- df["姓名"] = df["姓名"].str.strip() df["课程"] = df["课程"].str.strip() before = len(df) df = df.drop_duplicates(subset="编号") print(f"按编号去重:{before} 行 -> {len(df)} 行") # ---------- 6. 重置行号(去重、筛选后旧行号会断号) ---------- df = df.reset_index(drop=True) print("\n===== 清洗后 =====") print(df.to_string(index=False)) with pd.ExcelWriter(OUT_FILE, engine="openpyxl") as writer: raw.to_excel(writer, sheet_name="清洗前", index=False) df.to_excel(writer, sheet_name="清洗后", index=False) print("\n清洗结果已写入:", OUT_FILE) if __name__ == "__main__": make_sample() clean()
# -*- coding: utf-8 -*- """ 案例10:按主键去重,保留最新一条记录 清洗场景: 客户信息表是“流水式”保存的——同一个客户每次修改都会新增一行, 分析时每个客户只应保留“最后修改”的那一条 知识点: 1. 主键概念:能唯一标识一行的列(这里是“客户编号”) 2. sort_values() 先按修改时间排好顺序 3. drop_duplicates(subset=主键, keep="last") 保留最后一条(即最新) 4. 顺手演示:数值缺失用同列均值填充、分组内缺失用 ffill 前向填充 运行后: 生成 data/客户信息_脏数据.xlsx 和 data/客户信息_清洗结果.xlsx """ import os import pandas as pd BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") os.makedirs(DATA_DIR, exist_ok=True) SRC_FILE = os.path.join(DATA_DIR, "客户信息_脏数据.xlsx") OUT_FILE = os.path.join(DATA_DIR, "客户信息_清洗结果.xlsx") def make_sample(): df = pd.DataFrame({ "客户编号": ["C01", "C02", "C01", "C03", "C02"], "客户名称": ["张三", "李四", "张三", "王五", "李四"], "城市": ["北京", "上海", "北京", None, "杭州"], # C02 先填上海后改杭州;C03 城市缺失 "积分": [100, 200, 350, None, 260], # C01 两次修改积分;C03 积分缺失 "修改时间": ["2024-01-05", "2024-01-06", "2024-02-10", "2024-02-15", "2024-03-01"], }) df.to_excel(SRC_FILE, sheet_name="原始数据", index=False) print("已生成示例文件:", SRC_FILE) def clean(): raw = pd.read_excel(SRC_FILE, sheet_name="原始数据") print("===== 原始流水(同客户有多条) =====") print(raw.to_string(index=False)) df = raw.copy() df["修改时间"] = pd.to_datetime(df["修改时间"]) # ---------- 1. 核心:先按修改时间从早到晚排序,再按主键保留最后一条 ---------- # keep="last" = 排序后重复主键只留最后一行,也就是“最新修改”的那条 df = df.sort_values("修改时间") df = df.drop_duplicates(subset="客户编号", keep="last") # ---------- 2. 缺失的积分用现有积分的平均值填充 ---------- df["积分"] = df["积分"].fillna(df["积分"].mean()).round(0).astype(int) # ---------- 3. 缺失的城市填充为“未知”(类别型缺失常用固定占位) ---------- df["城市"] = df["城市"].fillna("未知") # ---------- 4. 时间转回字符串,按客户编号排序输出 ---------- df["修改时间"] = df["修改时间"].dt.strftime("%Y-%m-%d") df = df.sort_values("客户编号").reset_index(drop=True) print("\n===== 清洗后(每个客户只留最新一条) =====") print(df.to_string(index=False)) with pd.ExcelWriter(OUT_FILE, engine="openpyxl") as writer: raw.to_excel(writer, sheet_name="清洗前", index=False) df.to_excel(writer, sheet_name="清洗后", index=False) print("\n清洗结果已写入:", OUT_FILE) if __name__ == "__main__": make_sample() clean()

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询