AI编程(二十九):数据清洗入门 — 给”脏”数据洗个澡,才能下锅

作者:

在

真实世界的数据从来不干净:空格、错格式、缺胳膊少腿的行混在一起。今天学两招——字符串处理和条件筛选,把脏数据洗成能用的数据

一、一份”看着没问题”的表格,坑有多深

先说一个你八成遇到过的场景。

朋友发来一份”聚餐报名表.csv”,说:”帮我算算一共来多少人,按男女分个组。”你打开一看,十几行,有姓名、性别、手机号,挺整齐的嘛。结果一算——人数比实际多了3个;按性别分组,分出来四组:”男”、”男 “(带空格)、”女”、”M”。手机号更离谱,有的带横杠,有的带”+86″,还有人写了”没手机”。

你找朋友对质,朋友说:”我就是让大家自己填的啊。”

问题就出在这——真实世界的数据,是人手填出来的,不是机器吐出来的。人会多打一个空格、会把”男”写成”M”、会在手机号栏里写小作文。数据分析圈有句老话:“数据科学家80%的时间在洗数据,20%在分析”——虽然是吐槽,但说明了一件事:数据不洗,分析结果就是错的。

给数据挑毛病、修毛病的过程,就叫数据清洗。

今天不求洗得多专业,只学最实用的两招:

  1. 字符串处理——把每个格子里的文本修整干净(去空格、统一大小写、替换错写法)
  2. 条件筛选——把不合格的行挑出来(手机号不是11位数字的不要、性别没填的单独放)

好消息是:今天也没有全新语法。字符串处理用的是字符串自带的方法(比第14篇玩字符串时多了几个”自带工具”),条件筛选用的是你早就会的if和循环。今天学的是把它们串成一条清洗流水线——和上一篇的”配置套路”一样,是组合拳,不是新招式。

二、第一招:字符串处理——每个格子的”修理工”

先看”脏”文本长什么样。假设这一列是大家填的性别:

性别列 = ["男", "男 ", " 女", "M", "女", "male", "女n"]

坑全在这了:有多余空格("男 ")、有换行符("女n")、有英文写法("M"、"male")。注意 "男" 和 "男 " 在程序眼里是两个不同的字符串——就像”王小明”和”王小明 “在点名册上是两个人。这就是分组分出四组的原因。

字符串身上带一套”修理工工具”,叫字符串方法。今天重点用三个:

text = "  男 n"

text.strip()      # '男'  —— 剪掉首尾所有空格、换行、制表符
text.strip().lower()   # '男'  —— 字母全变小写(对付'M'/'Male'/'MALE')
text.replace("M", "男")  # 替换:把'M'换成'男'

三个方法各管一段:

方法 干什么 例子

|——|——–|——|

.strip() 剪掉首尾的空格、换行、制表符 " 女n".strip() → "女"
.lower() 字母全变小写 "Male".lower() → "male"
.replace(旧, 新) 把指定内容换掉 "M".replace("M", "男") → "男"

关键细节:这些方法不会改动原来的字符串,而是返回一个新的干净字符串。所以要接住它:

text = " 男 "
干净 = text.strip()   # 不接住就白修了
print(干净)           # 男
print(text)           # ' 男 ' —— 原来的没变

现在把”性别列洗干净”写成一个函数——套路就是每来一个值,先strip,再统一写法:

def 洗性别(值):
    值 = 值.strip().lower()          # 第1步:剪空格 + 统一小写
    if 值 in ("男", "m", "male"):
        return "男"
    elif 值 in ("女", "f", "female"):
        return "女"
    else:
        return "未知"                # 认不出来的,老实标"未知",别瞎猜

性别列 = ["男", "男 ", " 女", "M", "女", "male", "女n", "保密"]
print([洗性别(x) for x in 性别列])
# ['男', '男', '女', '男', '女', '女', '女', '未知']

七零八落的8个值,洗完整整齐齐。这就是字符串处理的威力——同一个意思的不同写法,归一成一个标准写法。行话叫”归一化”,你就理解成”统一口径”。

三、第二招:条件筛选——把不合格的挑出来

洗完字段,还得挑行。手机号那列最典型:有的不是11位、有的混着横杠、有的干脆是”没手机”。这种行不该硬着头皮往下算,得挑出来单独处理。

第5篇学的if,配上学过的列表和循环,就能干这活。先看”挑出来”的标准写法:

手机号列 = ["13800138000", "138-0013-8000", "没手机", "13912345678", "1380013800"]

def 是合法手机号(值):
    值 = 值.strip().replace("-", "").replace("+86", "")   # 先修整
    return 值.isdigit() and len(值) == 11                 # 再判断:全是数字且11位

干净行 = []
脏行 = []
for 号 in 手机号列:
    if 是合法手机号(号):
        干净行.append(号)
    else:
        脏行.append(号)

print("能用的:", 干净行)   # 能用的: ['13800138000', '13912345678']
print("要返修:", 脏行)     # 要返修: ['138-0013-8000', '没手机', '1380013800']

两个新面孔,都是字符串自带的小工具:

  • 值.isdigit()——判断字符串是不是全是数字("13800138000".isdigit() → True,"没手机".isdigit() → False)。注意它是字符串的方法,整数没有这个方法
  • len(值)——数字符串有多少个字符,老朋友了(第15篇数单词用过)

这里有个容易忽略的好习惯:先修整,再判断。"138-0013-8000"先去掉横杠就是合法的,别一棍子打死;而”没手机”修完还是不合格,才进”脏行”。清洗不是当保安,是当修理工——能修的修好留下,修不了的才请出去。

另外注意我把判断写成了独立函数是合法手机号。这样”什么算合格”只写一处,明天标准变了(比如要11位还必须以1开头)只改这一个函数。上一篇”逻辑和设置分家”的思想又出现了——标准要独立成函数,别散落在代码各处。

四、实战:把聚餐报名表洗干净

两招都学会了,串起来就是一条清洗流水线:读进来 → 每行洗字段 → 挑出坏行 → 输出干净数据 + 问题报告。完整代码(建议存成洗数据.py):

import csv

def 洗性别(值):
    值 = 值.strip().lower()
    if 值 in ("男", "m", "male"):
        return "男"
    elif 值 in ("女", "f", "female"):
        return "女"
    return "未知"

def 是合法手机号(值):
    值 = 值.strip().replace("-", "").replace("+86", "")
    return 值.isdigit() and len(值) == 11

def 洗一行(行):
    """把一行原始数据洗干净,返回(干净行, 问题清单)"""
    问题 = []
    干净 = {
        "姓名": 行["姓名"].strip(),
        "性别": 洗性别(行["性别"]),
        "手机号": 行["手机号"].strip().replace("-", "").replace("+86", ""),
    }
    if not 干净["姓名"]:
        问题.append("姓名是空的")
    if 干净["性别"] == "未知":
        问题.append(f"性别看不懂:{行['性别']!r}")
    if not 是合法手机号(干净["手机号"]):
        问题.append(f"手机号不对:{行['手机号']!r}")
    return 干净, 问题

# ———— 流水线主体 ————
干净行 = []
待返修行 = []

with open("报名表.csv", encoding="utf-8") as f:
    for 行 in csv.DictReader(f):
        干净, 问题 = 洗一行(行)
        if 问题:
            待返修行.append((干净["姓名"], 问题))
        else:
            干净行.append(干净)

# 输出洗好的数据
with open("报名表_干净版.csv", "w", encoding="utf-8", newline="") as f:
    写手 = csv.DictWriter(f, fieldnames=["姓名", "性别", "手机号"])
    写手.writeheader()
    写手.writerows(干净行)

# 输出"问题报告"——脏数据不丢,单独给人看
print(f"洗完:{len(干净行)} 行能用,{len(待返修行)} 行要返修")
for 姓名, 问题 in 待返修行:
    print(f"  {姓名}:{';'.join(问题)}")

跑之前自己造一份报名表.csv试试(记住第25篇的规矩:UTF-8编码):

姓名,性别,手机号
张三,男,13800138000
李四 ,男 ,138-0013-8000
王五, 女,+8613912345678
赵六,M,没手机
,女,13700137000
钱七,female,1380013800

预期输出:

洗完:3 行能用,3 行要返修
  赵六:手机号不对:'没手机'
  :姓名是空的
  钱七:手机号不对:'1380013800'

(上面这段就是我实际跑出来的结果。注意两个细节,正好都是今天讲的重点:李四的号138-0013-8000去横杠后是合法的,所以他通过了——”先修整再判断”救回来一行;赵六填的"M"被归一成了”男”,性别没问题,他只是手机号栏写了小作文。要是你之前无脑strip()就判断,李四这行就被冤枉删了。)

注意这条流水线的两个”讲究”,比代码本身还值钱:

  1. 脏数据不直接删,要留”问题报告”。删了就永远不知道出过什么事;留着报告,可以回头找填表的人返修。真实工作里,”这行我删了”和”这行有问题,名单给你”是两种职业水平
  2. 每行独立洗,一行坏了不连累别行。洗一行只管自己那行,坏行进”待返修”,好行照常放行——流水线不会因为一个坏零件停产

再把这个套路丢给AI用一次你就明白了。说:”帮我把洗数据.py 改成也检查邮箱格式(有@就行),不合格的加进问题报告”——改动会非常小:洗一行里加一个判断。清洗流水线是可生长的,这是散装if给不了的。

五、四个必踩的坑(先打疫苗)

坑1:strip()以为能去掉所有空格。 strip()只剪首尾的空格,中间的空格它不管——"张 三".strip()还是"张 三"。要收拾中间的空格用replace(" ", "")(把空格全替换掉)或者更稳的一招:先replace(" ", "")再去判断。疫苗:想清楚你要”剪两头”还是”全删”,别无脑strip。

坑2:洗完忘了接住。 值.strip()只是”返回”干净版,不会改动原字符串。写了行["姓名"].strip()但没赋值回去,等于白洗。疫苗:清洗必赋值——x = x.strip().lower()这种”洗完覆盖自己”的写法最不容易忘。

坑3:清洗和判断写成一锅粥。 把”去横杠、判长度、判数字”揉在一大段if里,明天换个手机号规则就得重写。疫苗:修整归修整,判断归判断——先干净值 = 修整(原始值),再是否合格 = 判断(干净值)。今天的是合法手机号故意把修整和判断放一起了(因为它俩太短),但心里要有数这是两件事。

坑4:脏数据直接del,问题没记录。 程序跑完只剩干净数据,看起来很爽,但你不知道丢了多少、丢的都是谁——万一整列手机号格式都错,你删完就剩个位数了还不自知。疫苗:删之前先数、先记。print(f"{len(待返修行)} 行要返修")这一行,就是你的”数据体检单”。

六、跟AI协作的正确姿势(清洗特供)

清洗活是AI的强项,但要指挥好三个要点:

  1. 把”脏的样子”贴给AI。别说”帮我清洗数据”,说”这列性别里有’男’、’男 ‘、’M’、’male’、’女n’,帮我写函数统一成’男/女’,认不出来的返回’未知’”——把脏数据样本原样贴出来,AI才能对症下药。贴真实数据比描述规则管用十倍
  2. 要求”问题报告”,不许静默删除。在需求里加一句:”不合格的行不要删,收集到一个列表里,最后打印出来给我看”——这句能防住AI写出”坏行直接跳过”的糊弄代码
  3. 让它写成函数,别写成一坨。”清洗逻辑写成独立函数,一行一个函数调用”——改规则时只改一个函数,这种结构红利你下一篇马上会再吃到

高频句式送你:”每列写一个清洗函数(输入原始字符串,输出干净值),坏行收集到问题报告,好行存进新文件“——把这句丢给AI,出来的就是今天实战代码的骨架。

七、今日小练习

练习1:给聚餐表加一列”忌口”

在洗一行里加上”忌口”字段的清洗:去掉首尾空格,把顿号、逗号统一成斜杠("辣,香菜" → "辣/香菜"),空的填”无”。(提示:replace(",", "/").replace(",", "/")链式替换;判空用if not 值。改完记得fieldnames里也加上这一列,不然存的时候会丢——第25篇的老坑。)

练习2:手机号体检单

写一个独立小程序:读任意CSV,找出所有”像手机号但不合法”的值,输出每行行号和原因(太短/太长/含字母)。(提示:原因可以用elif分三段判:len < 11、len > 11、not 值.isdigit()。行号用for i, 行 in enumerate(数据, 1)——enumerate自带计数,第15篇数单词时见过它。)

练习3:两份名单对账

手造名单A.csv和名单B.csv(各有姓名、手机号),写程序找出:”A有B没有”的人、”两边都有但手机号不同”的人。(提示:手机号是天然的唯一标识,把两份名单各自读成{手机号: 姓名}的字典——第8篇的字典招。然后对比两个字典的键:键 not in 另一个字典查单边,值不同查冲突。这就是程序员说的”对账/去重”,工作中高频得吓人。)

写在后面:你的数据开始”讲卫生”了

盘点今天:字符串修理工三件套(strip剪两头、lower统一大小写、replace换写法)、条件筛选(isdigit+len验手机号,先修整再判断)、清洗流水线(读进来→逐行洗→好行放行→坏行记录)。

还是没有硬核新语法——第29篇了,你早就发现规律了吧?编程的大部分功夫不在语法,在”讲究”:数据要洗、标准要集中、坏数据要记录。这些讲究,才是让AI给你写的代码从”能跑”变成”能用”的关键。

下一篇把目光从数据挪到文件上。你电脑里是不是有一堆”报告1.csv、报告2.csv、最终版.csv、最终版2.csv”?批量文件处理——让程序一口气收拾一整个文件夹,你只要说一句”把这文件夹里的CSV全都洗一遍”。

下篇预告

第三十篇:批量文件处理——一个文件夹里躺着几十个文件,怎么让程序自动找到它们、逐个处理、批量改名?os和glob两个新朋友登场。下一篇见。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注