Pandas 数据清洗
上一章把 data/staff.csv 读成了 DataFrame,也留了一串悬念:dtype 清一色 object、员工号 E029 出现两次、月薪一列里同时住着 42k、20w/年、12k~15k 这些互不相识的写法…
上一章把 data/staff.csv 读成了 DataFrame,也留了一串悬念:dtype 清一色 object、员工号 E029 出现两次、月薪一列里同时住着 42k、20w/年、12k~15k 这些互不相识的写法。这些毛病统称「脏数据」——它不是练习题里的故意刁难,而是真实世界的常态:各部门各填各的表、导出工具五花八门、人工录入随手一敲。业内流传一个说法,清洗要吃掉分析师 60% 以上的时间;学完本章你会信。
本章的目标只有一句话:把 staff 花名册洗到「每个字段类型正确、写法统一、缺失有交代」,最后还能拿干净数据回答几个业务问题。所有代码从课程根目录运行,先导入三件套:
import re
import numpy as np
import pandas as pd
staff = pd.read_csv('data/staff.csv')
1. 动手之前,先学会「精准打击」:loc 与 iloc
清洗过程里你会不断需要「只看某几行某几列」「只改符合条件的格子」,这靠两个定位器:
loc:按标签定位,df.loc[行标签, 列标签]——行号、列名、布尔条件都算标签;iloc:按位置定位,df.iloc[行位置, 列位置]——纯整数序号,从 0 数起。
staff.loc[0:3, '姓名'] # 第 0~3 行的姓名(loc 切片含右端点)
staff.iloc[0:3, 0:2] # 前 3 行、前 2 列(iloc 切片不含右端点)
staff.iloc[0, 0] # 左上角第一个值 → 'E001'
# staff.loc[0:3, '姓名']
0 陈明远
1 林晓彤
2 赵启航
3 孙一诺
Name: 姓名, dtype: object
# staff.iloc[0:3, 0:2]
员工号 姓名
0 E001 陈明远
1 E002 林晓彤
2 E003 赵启航
loc 更常见的打开方式是布尔筛选——把一个条件表达式塞进中括号:
# 部门是「技术部」的所有行、所有列
staff.loc[staff['部门'] == '技术部']
# 月薪里带 w 的(年薪写法),只看姓名和月薪两列
staff.loc[staff['月薪'].str.contains('w', na=False), ['姓名', '月薪']]
loc |
iloc |
|
|---|---|---|
| 定位依据 | 标签(行号、列名、布尔条件) | 整数位置 |
| 切片右端点 | 包含 | 不包含 |
| 典型场景 | 按条件筛选、按列名取数 | 按位置批量截取 |
两者最容易踩的坑就是切片端点:loc[0:3] 取 4 行,iloc[0:3] 取 3 行。记住口诀「loc 是闭区间,iloc 照抄 Python 习惯」。
2. 数据体检:先看清楚,再动手
拿到脏数据,第一反应永远不是写清洗代码,而是「做体检」。三板斧:
第一斧:info() 总览——多少行、每列多少非空、什么类型:
staff.info()
<class 'pandas.DataFrame'>
RangeIndex: 37 entries, 0 to 36
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 员工号 37 non-null object
1 姓名 36 non-null object
2 部门 36 non-null object
3 职级 37 non-null object
4 工作年限 37 non-null object
5 月薪 36 non-null object
6 绩效 35 non-null object
dtypes: object(7)
memory usage: 2.1+ KB
第二斧:isna().sum() 数缺失——比 info 更直接地列出每列的空值数:
staff.isna().sum()
员工号 0
姓名 1
部门 1
职级 0
工作年限 0
月薪 1
绩效 2
dtype: int64
第三斧:value_counts() 查户口——每一列到底装了些什么值。比如员工号本该人人唯一:
staff['员工号'].value_counts().head(3)
员工号
E029 2
E001 1
E027 1
Name: count, dtype: int64
E029 出现了两次。用 duplicated 把重复行揪出来看:
staff[staff.duplicated(keep=False)] # keep=False:所有参与重复的行都标出来
员工号 姓名 部门 职级 工作年限 月薪 绩效
28 E029 晏清和 研发部 P7 12年 25k->22k->18k A+
31 E029 晏清和 研发部 P7 12年 25k->22k->18k A+
体检报告汇总,接下来对症下药:
- 重复:E029 整行重复登记;
- 类型:工作年限、月薪是 object(字符串),没法计算;
- 混入:工作年限里混着「26届」「25届」「应届」——那是毕业届别,不是工龄;
- 分类不统一:部门 14 种写法、职级有
p6/P6级、绩效混着字母制和3.75数字制,还有「待定」; - 缺失:姓名、部门、月薪、绩效各缺 1~2 个。
3. 重复行:先删为敬
重复数据会让人数翻倍、均值失真,越早处理越好。duplicated() 返回布尔 Series(默认把第二次及以后出现的标 True),配合布尔筛选就能查看;直接删则用 drop_duplicates():
staff = staff.drop_duplicates()
len(staff) # 37 → 36
duplicated(keep=...)与drop_duplicates(keep=...)共享同一个参数:'first'(默认)保留第一次出现、'last'保留最后一次、False全部标记/全部删除。判断「重复」是看整行所有列;也可以传subset=['员工号']只按工号判重——业务上工号唯一,按它判重更稳。
4. 把「届别」从工作年限里拆出来
工作年限列里混着的 26届、25届、应届,本质是毕业年份信息,被填表人顺手塞错了列。正确姿势是把它拆成独立的一列,而不是简单删掉——信息本身没有错,只是放错了抽屉。
先用 str.contains 圈出这些行(astype(str) 防止有非字符串值,na=False 让缺失值不炸):
mask = staff['工作年限'].astype(str).str.contains('届', na=False)
staff.loc[mask, ['员工号', '工作年限']]
员工号 工作年限
13 E014 26届
18 E019 25届
27 E028 应届
36 E036 26届
再用 str.extract() 按正则把数字抠出来(括号是捕获组,\d+ 表示一串数字):
staff.loc[mask, '工作年限'].str.extract(r'(\d+)\s*届')
0
13 26
18 25
27 NaN
36 26
注意「应届」提不出数字,得到 NaN——它只能告诉我们「还没毕业」,年份只能空着。接着三步落库:建新列、写入年份、把原列的届别值清为 0 年:
years = staff.loc[mask, '工作年限'].str.extract(r'(\d+)\s*届')[0]
staff['毕业年份'] = np.nan # 先整列置空
staff.loc[mask, '毕业年份'] = 2000 + years.dropna().astype(int) # 26 → 2026
staff.loc[mask, '工作年限'] = 0.0 # 届别提走后,工龄按 0 记
staff['毕业年份'].value_counts(dropna=False)
毕业年份
NaN 33
2026.0 2
2025.0 1
Name: count, dtype: int64
这里用
2000 + 年份后两位是因为填表人习惯写缩写。真实数据里遇到「25 届 vs 2025 届」混写也很常见,思路相同:先统一格式,再转换类型。
5. 分类列的标准化:部门、职级、绩效
分类列(值来自一个有限集合)的通病是同一个类别好几种写法。老规矩,先 value_counts() 看乱象:
staff['部门'].value_counts(dropna=False)
部门
技术部 12
市场部 4
研发部 4
技术研发 3
人事部 2
财务部 2
产品设计 2
用户运营 1
研发 1
财务 1
Marketing 1
人力资源 1
技术 1
NaN 1
Name: count, dtype: int64
同一个公司,部门愣是写出了 14 种花样。处理配方是先 str.strip() 去掉首尾空白,再 map() 按字典归一。但映射表很难一次写全——凭印象列一版初稿,再用 isin 反查「不在名单里」的值,这是补全映射表的固定套路:
draft = {
'技术部': '技术部', '研发部': '研发部', '市场部': '市场部',
'人事部': '人事部', '财务部': '财务部', '产品设计': '产品部',
}
# isin 反向筛选:列出初稿没收录的写法(~ 取反;注意 NaN 也会被筛出来)
staff.loc[~staff['部门'].str.strip().isin(draft), '部门'].unique()
array(['技术研发', '用户运营', '研发', '财务', 'Marketing', '人力资源', '技术 ', nan], dtype=object)
果然漏了 7 种:把「技术研发」「研发」并进研发部,「技术」并进技术部,「财务」并进财务部,「Marketing」并进市场部,「人力资源」并进人事部,「用户运营」并进运营部——补全后的完整版如下:
dept_map = {
'技术部': '技术部', '技术': '技术部',
'研发部': '研发部', '研发': '研发部', '技术研发': '研发部',
'市场部': '市场部', 'Marketing': '市场部',
'产品设计': '产品部',
'用户运营': '运营部',
'人力资源': '人事部', '人事部': '人事部',
'财务部': '财务部', '财务': '财务部',
}
staff['部门'] = staff['部门'].str.strip().map(dept_map)
staff['部门'].value_counts(dropna=False)
部门
技术部 13
研发部 8
市场部 5
人事部 3
财务部 3
产品部 2
运营部 1
NaN 1
Name: count, dtype: int64
14 种归成 7 种(外加 1 个真缺失)。注意 map() 的一个关键性质:字典里没登记的键,结果一律是 NaN——「不认识的值宁可置空,也不要瞎猜」。空着的部门后面单独处置。
顺手转成 category 类型:类别数固定、还省内存,语义也更诚实:
staff['部门'] = staff['部门'].astype('category')
staff['部门'].dtype # category
顺带记一个同门技法:遇上「市场-华东」「研发/前端组」这类带前缀的写法,
str.split('-').str[-1](或str.split('/'))取末段就能剥掉前缀,再交给map收尾。本表的部门没有这毛病,但真实数据里它出镜率不低。
职级的毛病是小写、带后缀(p6、P6级、M1级),一条链式调用搞定:
staff['职级'] = staff['职级'].str.strip().str.upper().str.replace('级', '', regex=False)
staff['职级'].value_counts()
职级
P5 11
P6 10
P4 7
P7 4
M1 3
P8 1
Name: count, dtype: int64
绩效更热闹——字母制(S/A/A+/B/B+)和数字制(3.75/3.5)混用,还有「待定」:
staff['绩效'].value_counts(dropna=False)
绩效
B 12
A 6
B+ 6
A+ 4
NaN 2
S 2
3.75 1
3.5 1
A 1
待定 1
Name: count, dtype: int64
(那个计数为 1 的 A 其实是 'A '——末尾带了个空格,和前面的 A 不是同一个值,strip 之后才会合流。)同样用映射表归一,两套打分制对齐到字母制:
perf_map = {
'S': 'S', 'A+': 'A+', 'A': 'A', 'B+': 'B+', 'B': 'B',
'3.75': 'A+', '3.5': 'B+', # 数字制 → 字母制
}
staff['绩效'] = staff['绩效'].str.strip().map(perf_map)
staff['绩效'] = staff['绩效'].astype('category')
staff['绩效'].value_counts(dropna=False)
绩效
B 12
A 7
B+ 7
A+ 5
NaN 3
S 2
Name: count, dtype: int64
「待定」和空值一样被 map 归入 NaN——无法解析的绩效,宁缺毋滥。
6. 工作年限:从字符串到数值
轮到本章的重头戏:把文本列转成能算的数值。先看看工作年限还剩哪些写法:
staff['工作年限'].value_counts()
工作年限
0.0 4
1年 3
2年 3
5年 3
4年 2
3年 2
6年 2
7年 2
8年 1
9.5年 1
12年 1
<1年 1
3.5年 1
<3年 1
- 1
8个月 1
2.5年 1
4.5年 1
15年+ 1
6个月 1
5.5年 1
10+年 1
10年 1
Name: count, dtype: int64
写法五花八门,得给每类定一条翻译规则:
| 原始写法 | 处理规则 | 结果 |
|---|---|---|
8年、2.5年 |
提取数字 | 8.0、2.5 |
8个月、6个月 |
数字除以 12 | 0.7、0.5 |
10+年、15年+ |
取下限 | 10.0、15.0 |
<1年、<3年 |
取中间值(减 0.5,不为负) | 0.5、2.5 |
- |
无法解析 | NaN |
| 数值(届别行上一步已置 0.0) | 直通 | 0.0 |
这种「一值一议」的活,正则 + 自定义函数 + apply() 是标准解法(第 4 章练的函数设计,在这里正式上岗——apply 会把列里每个值依次喂给你的函数,再把返回值拼成新列):
def to_years(val: object) -> float:
"""把各种工作年限写法统一成「年」的浮点数"""
if pd.isna(val):
return np.nan
if isinstance(val, (int, float)): # 已经是数值(如 0.0),直通
return float(val)
s = str(val).strip()
if s in ('', '-'): # 无信息
return np.nan
m = re.search(r'(\d+(?:\.\d+)?)\s*(?:\+\s*年|年\s*\+)', s) # 10+年 / 15年+
if m:
return float(m.group(1))
m = re.search(r'<\s*(\d+(?:\.\d+)?)\s*年', s) # <3年
if m:
return max(float(m.group(1)) - 0.5, 0.0)
years = re.findall(r'(\d+(?:\.\d+)?)\s*年', s) # N年
if years:
return sum(float(y) for y in years)
months = re.findall(r'(\d+(?:\.\d+)?)\s*个月', s) # N个月
if months:
return round(sum(float(x) for x in months) / 12, 1)
return np.nan
staff['工作年限'] = staff['工作年限'].apply(to_years)
前后对照几行(去重后行索引保持原样,正好用来点名):
staff.loc[[7, 10, 12, 23, 26], ['员工号', '姓名', '工作年限']]
员工号 姓名 工作年限
7 E008 冯敬亭 10.0
10 E011 杨知夏 0.5
12 E013 秦朗 0.7
23 E024 白若溪 2.5
26 E027 丛一鸣 0.5
再统计一下,确认列已经「能算」:
staff['工作年限'].describe()
count 35.000000
mean 4.248571
std 3.748441
min 0.000000
25% 1.000000
50% 3.500000
75% 6.000000
max 15.000000
Name: 工作年限, dtype: float64
describe() 一口气给出计数、均值、分位数——这些统计在 object 时代是想都别想的。平均工龄约 4.2 年,最长 15 年。
7. 月薪:解析难度再上一级
月薪是全表最「脏」的列,先把特殊写法的行请出来亮相:
staff.loc[[6, 13, 16, 17, 20, 21, 22, 28], ['员工号', '姓名', '月薪']]
员工号 姓名 月薪
6 E007 郑安然 20w/年
13 E014 尤佳琪 实习
16 E017 吕方旭 13k*15
17 E018 施雅文 12k~15k
20 E021 曹婉清 16k(含补贴)
21 E022 严子墨 40k+
22 E023 金书瑶 面议
28 E029 晏清和 25k->22k->18k
逐条立规矩(目标单位统一为千元/月):
| 写法 | 含义 | 规则 |
|---|---|---|
42k |
千元月薪 | 直接取数 |
20w/年 |
年薪 20 万 | × 10 ÷ 12 换算月薪 |
13k*15 |
13 薪 | 取基本月薪 13 |
12k~15k |
薪资区间 | 取中间值 |
40k+ |
下限写法 | 取下限 40 |
16k(含补贴) |
带备注 | 摘掉括号再取数 |
25k->22k->18k |
调薪历史 | 取最新一档 18 |
实习、面议、空 |
无数值 | NaN |
规则多了,函数就长,但骨架和上一节完全一样——从特殊到一般逐层剥离:
def to_monthly_k(val: object) -> float:
"""把各种月薪写法统一成「千元/月」的浮点数"""
if pd.isna(val):
return np.nan
s = str(val).strip()
if s in ('', '-', '面议', '实习'):
return np.nan
if '->' in s: # 调薪记录:取最新一档
s = s.split('->')[-1].strip()
s = re.sub(r'[((][^))]*[))]', '', s).strip() # 摘掉括号备注
m = re.search(r'(\d+(?:\.\d+)?)\s*w', s, re.IGNORECASE) # 年薪 w
if m:
return round(float(m.group(1)) * 10 / 12, 1)
m = re.search(r'(\d+(?:\.\d+)?)\s*k\s*[*x×]\s*\d+', s, re.IGNORECASE) # 13k*15
if m:
return float(m.group(1))
m = re.search(r'(\d+(?:\.\d+)?)\s*k\s*[~~至]\s*(\d+(?:\.\d+)?)\s*k', s) # 12k~15k
if m:
return round((float(m.group(1)) + float(m.group(2))) / 2, 1)
m = re.search(r'(\d+(?:\.\d+)?)\s*k\s*\+', s, re.IGNORECASE) # 40k+
if m:
return float(m.group(1))
m = re.search(r'(\d+(?:\.\d+)?)\s*k', s, re.IGNORECASE) # 42k
if m:
return float(m.group(1))
return np.nan
staff['月薪'] = staff['月薪'].apply(to_monthly_k)
还是那批行,验收解析结果:
staff.loc[[6, 13, 16, 17, 20, 21, 22, 28], ['员工号', '姓名', '月薪']]
员工号 姓名 月薪
6 E007 郑安然 16.7
13 E014 尤佳琪 NaN
16 E017 吕方旭 13.0
17 E018 施雅文 13.5
20 E021 曹婉清 16.0
21 E022 严子墨 40.0
22 E023 金书瑶 NaN
28 E029 晏清和 18.0
20w/年 → 16.7(20 万 ÷ 12 个月 ≈ 1.67 万)、12k~15k → 13.5、调薪记录取到最新的 18——规则全部按预期落地。整体分布:
staff['月薪'].describe()
count 33.000000
mean 22.112121
std 12.906122
min 7.000000
25% 13.500000
50% 17.000000
75% 27.000000
max 65.000000
Name: 月薪, dtype: float64
写这种解析函数的心法:先
value_counts()摸清全部写法,一条写法定一条规则,从特殊到一般排列 if 分支,最后兜底返回 NaN。真实业务里没人能一次写对,跑一遍、看哪些值掉进了 NaN、再补规则,是常态。
8. 缺失值:三种处置,按列施策
数值化完成后,重新盘点缺失:
staff.isna().sum()
员工号 0
姓名 1
部门 1
职级 0
工作年限 1
月薪 3
绩效 3
毕业年份 33
dtype: int64
缺失值没有万能解法,按业务语义三选一:
策略一:填充 fillna()——值可推、可约定时用。 姓名缺失的员工补一个占位标记;绩效缺失按公司惯例记默认档 B:
staff['姓名'] = staff['姓名'].fillna('(未登记)')
staff['绩效'] = staff['绩效'].fillna('B')
staff['绩效'].value_counts()
绩效
B 15
A 7
B+ 7
A+ 5
S 2
Name: count, dtype: int64
策略二:删行 dropna()——关键列缺失、行失去意义时用。 部门缺失的那一行,既没法分组统计也没法归属成本,留着只会污染结果:
staff = staff.dropna(subset=['部门'])
len(staff) # 36 → 35
subset 限定「只看这几列」,只要指定列非空行就保留——没有它,月薪、毕业年份的 NaN 会连累大片无辜的行。
策略三:保留 NaN——信息不足时,诚实比好看重要。 实习生的 实习、待议的 面议 转成的月薪 NaN,硬填(比如填中位数)反而制造假数据。Pandas 的统计函数(mean、describe、groupby)默认自动跳过 NaN,保留它们不影响计算。真要填也有现成写法,但要先问自己一句「业务上说得通吗」:
# 写法示例:本表不采用——给实习生填全员中位数月薪,纯属编数据
# staff['月薪'] = staff['月薪'].fillna(staff['月薪'].median())
毕业年份那 33 个 NaN 同理:老员工们的毕业年份本就无从得知,空着就是最准确的答案。
9. 收尾:改名、排序、类型与终检
数据洗干净了,做点「门面整理」,让表自己会说话:
# 单位写进列名,读者不用猜
staff = staff.rename(columns={'月薪': '月薪(k)', '工作年限': '工作年限(年)'})
# 调整列顺序:关键身份信息在前
staff = staff[['员工号', '姓名', '部门', '职级', '工作年限(年)', '月薪(k)', '绩效', '毕业年份']]
# 可空整数类型:既能存整数,又能存 <NA>(普通 int64 做不到)
staff['毕业年份'] = staff['毕业年份'].astype('Int32')
Int32(大写 I 开头)是 Pandas 的可空整数类型——普通 int32 不允许空值,遇到 NaN 会强制变 float,2026.0 这种尾巴就是那么来的。大写版本让年份显示成整数,缺失显示成 <NA>,两全其美。
终检,和体检时的惨状对比着看:
staff.info()
<class 'pandas.core.frame.DataFrame'>
Index: 35 entries, 0 to 36
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 员工号 35 non-null object
1 姓名 35 non-null object
2 部门 35 non-null category
3 职级 35 non-null object
4 工作年限(年) 34 non-null float64
5 月薪(k) 32 non-null float64
6 绩效 35 non-null category
7 毕业年份 3 non-null Int32
dtypes: Int32(1), category(2), float64(2), object(3)
memory usage: 2.4+ KB
staff.head()
员工号 姓名 部门 职级 工作年限(年) 月薪(k) 绩效 毕业年份
0 E001 陈明远 技术部 P7 8.0 42.0 A <NA>
1 E002 林晓彤 技术部 P6 5.0 26.0 A+ <NA>
2 E003 赵启航 研发部 P6 4.0 25.0 B+ <NA>
3 E004 孙一诺 市场部 M1 3.0 18.0 B <NA>
4 E005 周慕云 人事部 M1 6.0 16.0 B <NA>
分类是 category、年限和月薪是 float64、缺失各归各位——「干净、可用」四个字,落地了。
10. 洗完的数据要能用:分组聚合
数据洗干净不是终点,能回答业务问题才算功德圆满。这类问题的标准句式是「按 X 分组,看 Y 的统计量」,在 Pandas 里就是 groupby:
dept_stats = (
staff.groupby('部门', observed=True)['月薪(k)'] # 按部门分组,取月薪列
.agg(['count', 'mean', 'max']) # 人数、均值、最大值一把出
.round(1)
)
dept_stats
count mean max
部门
产品部 2 20.0 24.0
人事部 3 15.0 16.0
市场部 3 12.8 18.0
技术部 13 30.1 65.0
研发部 8 20.4 33.0
财务部 3 13.9 16.7
运营部 0 NaN NaN
一眼可读的结论:技术部人多(13 人)且平均月薪最高(30.1k);运营部那一行 count 是 0——部门里唯一的员工月薪缺失,count 只数非空值。分组键是 category 类型时,显式传 observed=True 表示只显示实际出现的类别,省得空组来凑热闹。
分组键也可以是加工出来的临时列。比如想知道 P 序列(专业路线)和 M 序列(管理路线)的薪资差:
staff['序列'] = staff['职级'].str[0] # 取职级首字母:P 或 M
staff.groupby('序列')['月薪(k)'].mean().round(1)
序列
M 16.9
P 23.1
Name: 月薪(k), dtype: float64
再比如各绩效档的薪资水平——上一节那句「宁缺毋滥」的回报在这里:
staff.groupby('绩效', observed=True)['月薪(k)'].agg(['count', 'mean']).round(1)
count mean
绩效
A 7 23.4
A+ 5 27.2
B 13 15.2
B+ 5 21.5
S 2 57.5
绩效和薪资的阶梯关系清清楚楚。这张表先存着——下一章学 Matplotlib,它就是现成的画图素材。
11. 导出:给干净数据一个家
清洗成果要及时落盘,别让下一次分析从头再洗一遍:
# CSV:encoding='utf-8-sig' 带 BOM,Excel 双击打开中文不乱码
staff.to_csv('data/staff_clean.csv', index=False, encoding='utf-8-sig')
# Excel:给不写代码的同事
staff.to_excel('data/staff_clean.xlsx', index=False)
index=False 的意思是不把行索引写进文件——默认编号没有信息量,写进去反而多出一列无名列。(上一章 set_index 过的表另说,那时索引是有业务含义的。)
12. 小结
回顾这次完整的数据清洗,流程可以固化成一张清单,换任何数据集都适用:
- 体检:
info()+isna().sum()+value_counts(),先看清楚再动手; - 去重:
duplicated()查、drop_duplicates()删,越早越好; - 拆混入:
str.contains圈出异类,str.extract提取成新列; - 标准化分类:
str.strip/str.upper清理外观,map()按字典归一,astype('category')收尾; - 数值化:写不清规则的用
str方法,写得出规则的用apply+ 正则; - 处置缺失:能填的
fillna,该删的dropna(subset=...),说不通的保留 NaN; - 验证与导出:
rename/ 列排序 / 可空类型做门面,info()终检,to_csv/to_excel落盘。
本章用到的高频 API 速查:
| API | 一句话 |
|---|---|
loc / iloc |
按标签 / 按位置定位,布尔筛选 |
isna() / fillna() / dropna() |
缺失值的查、填、删 |
duplicated() / drop_duplicates() |
重复行的查与删 |
str.strip / str.upper / str.contains / str.extract |
列级字符串处理 |
map() |
按字典或函数逐值映射,未命中得 NaN |
apply() |
自定义函数逐值转换(或逐行/列处理) |
astype() |
类型转换,category、Int32 各有妙用 |
groupby().agg() |
分组聚合,统计量的流水线 |
数据清洗没有标准答案,只有「业务上说得通」的判断——同一个月薪缺失,做薪酬报告时要删,做人员盘点时可以留。工具给你三把刀,用哪把、怎么用,永远是你的决定。
洗好的 staff_clean.csv 静静躺在 data 目录里,正等着变成图表。下一章,Matplotlib 登场。