- 文档
- 教程
【免费下载链接】Python-100-Days
Python - 100天从新手到大师
导读
本文是《Python-100-Days》系列中 pandas 深度应用的第 3 篇,聚焦数据分析流程中承上启下的关键环节:数据重塑(拼接与合并)、数据清洗(缺失值、重复值、异常值)与数据预处理(日期拆解、字符串处理、类型编码、分箱离散化)。文中所有示例均源自仓库配套的 Jupyter Notebook(day04.ipynb)及内置数据集(Day66-80/code/res),你可以跟着示例在本地复现完整流程。读完本文,你将掌握用concat/merge整合多张表、用isna/dropna/fillna处理缺失值、用duplicated/drop_duplicates治理重复值、用 Z-score 与 IQR 检测异常值,以及把日期、字符串、连续值加工成可分析形态的完整方法论。
一、数据重塑:把多张表整合成一张表
在做数据分析时,原始数据往往不是来自同一个地方。上一章我们从关系型数据库中读取了三张表,得到了三个DataFrame,而实际工作通常需要把它们整合到一起。整合方式分为两种:拼接(把结构一致的数据上下接在一起,相当于 SQL 的UNION)和合并(把事实表与维度表按关联键左右对接,相当于 SQL 的JOIN)。
1.1 用concat拼接结构一致的数据
例如emp_df和emp2_df都是员工数据且结构完全一致,可以直接用pd.concat把它们纵向拼接:
all_emp_df = pd.concat([emp_df, emp2_df])输出(eno仍作为索引):
ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 分析师 7800.0 5000 1500.0 20 ... 9600 王大锤 程序员 9800.0 8000 600.0 20 9700 张三丰 总裁 NaN 60000 6000.0 20 9800 骆昊 架构师 7800.0 30000 5000.0 20 9900 陈小刀 分析师 9800.0 10000 1200.0 20这一用法在仓库 Notebook 中得到了直接印证(见 day04.ipynb),注释为“拼接两个DataFrame - union”,并可用all_emp_df.shape验证拼接后的形状为(19, 6)。
concat还常用于批量读取多个结构相同的文件。Notebook 中的课后作业就是遍历res/jobs目录下的多个城市招聘 CSV,把它们全部拼到一个DataFrame并落盘(见 day04.ipynb),对应数据集位于 Day66-80/code/res/jobs:
import os dfs = [pd.read_csv(os.path.join('res/jobs', filename)) for filename in os.listdir('res/jobs') if filename.endswith('.csv')] pd.concat(dfs, ignore_index=True).to_csv('res/all_jobs.csv', index=False)这里额外使用了ignore_index=True,让拼接后的行索引重新按 0 开始连续编号,适合多文件“堆叠”场景。
1.2 用merge合并事实表与维度表
拼接之后,用merge把员工表和部门表合并成一张宽表。首先用reset_index把eno从索引变回普通列(inplace=True表示直接在原对象上修改,不返回新对象):
all_emp_df.reset_index(inplace=True)然后执行内连接:
pd.merge(all_emp_df, dept_df, how='inner', on='dno')输出:
eno ename job mgr sal comm dno dname dloc 0 1359 胡一刀 销售员 3344.0 1800 200.0 30 销售部 重庆 1 3344 黄蓉 销售主管 7800.0 3000 800.0 30 销售部 重庆 ... 18 5566 宋远桥 会计师 7800.0 4000 1000.0 10 会计部 北京Notebook 中对应的源码注释明确了其语义(见 day04.ipynb):
# 连表 - 连接事实表和维度表 - 用维度把数据分组然后再做聚合 # 连接两个DataFrame(内连接、左外连接、右外连接、全外连接)- join # how - 连表方式 - inner、left、right、outer # on - 基于哪个字段连表 - left_on、right_on all_emp_df = pd.merge(all_emp_df, dept_df, how='inner', on='dno')merge的参数与 SQL 表连接一一对应,有 SQL 编程经验会倍感亲切:
| 参数 | 含义 | 取值/说明 |
|---|---|---|
how | 连表方式 | left(左外连接)、right(右外连接)、inner(内连接)、outer(全外连接) |
on | 连表字段 | 左右两表同名列,相当于 SQL 的连表条件 |
left_on/right_on | 分别指定连表字段 | 当左右两表对应列名不同时,取代on使用 |
例如把how改为'right':
pd.merge(all_emp_df, dept_df, how='right', on='dno')结果会比内连接多出下面一行,因为右外连接会完整保留右表dept_df的全部记录,而all_emp_df中没有编号为40部门的员工,对应位置被填入空值:
19 NaN NaN NaN NaN NaN NaN 40 运维部 深圳理解四种how的取舍是数据重塑的关键:内连接只保留两表都匹配的行,外连接则保证一侧(或两侧)数据完整,选择哪种取决于分析时希望保留哪些维度的完整性。
二、数据清洗:缺失值、重复值与异常值
从 Excel、CSV 或数据库中拿到的数据通常并不完美:可能混入重复值或异常值,也可能在某些字段上缺失。因此,在开始分析之前先做数据清洗,是保证结论可靠的前提。
2.1 缺失值的甄别、删除与填充
甄别缺失值
使用isnull或isna(二者等价)找出缺失值所在位置:
emp_df.isnull() # 或 emp_df.isna()输出(True表示该位置缺失):
ename job mgr sal comm dno eno 1359 False False False False False False ... 3211 False False False False True False # comm 缺失 ... 7800 False False True False False False # mgr 缺失对应的notnull/notna则把非空值标记为True。Notebook 中还有两个实用变体(见 day04.ipynb):单列缺失值统计all_emp_df['comm'].isnull(),以及用value_counts()汇总缺失数量(该例中comm有 11 个缺失、8 个非缺失)。
删除缺失值:dropna
dropna的axis参数决定沿 0 轴(删除整行)还是 1 轴(删除整列)删除,默认沿 0 轴:
emp_df.dropna() # 删除任何包含空值的行输出(仅保留无缺失的行):
ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 ... 5566 宋远桥 会计师 7800.0 4000 1000.0 10沿 1 轴删除(丢弃含缺失值的列,如mgr、comm):
emp_df.dropna(axis=1)输出:
ename job sal dno eno 1359 胡一刀 销售员 1800 30 ... 7800 张三丰 总裁 9000 20注意:
DataFrame的很多方法都有inplace参数,默认False,表示操作不修改原对象,而是返回处理后的新对象;设为True则直接在原对象上修改,返回值是None。上面的操作并没有修改emp_df。
填充缺失值:fillna
某些场景更适合填充而非删除,fillna支持用指定值、前值或后值填充:
emp_df.fillna(value=0) # 用常量填充 emp_df.fillna(method='ffill') # 用前一个单元格填充(forward fill) emp_df.fillna(method='bfill') # 用后一个单元格填充(backward fill)用0填充后的输出(comm、mgr的空值变为0.0):
ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 ... 3211 张无忌 程序员 2056.0 3200 0.0 20 ... 7800 张三丰 总裁 0.0 9000 1200.0 20注意:填充值如何选取本身值得探讨。实际工作中可能用统计量(均值、众数等)、插值法(随机插值、拉格朗日插值等)填充,甚至用回归模型、贝叶斯模型等对缺失数据建模填充。
Notebook 中对填充手段的演示更为完整(见 day04.ipynb):
all_emp_df.comm.fillna(0).astype('i8') # 填 0 并转 int64 all_emp_df.comm.bfill() # 下方非空值向上填充 all_emp_df.comm.ffill() # 上方非空值向下填充 all_emp_df['comm'] = all_emp_df.comm.interpolate(method='linear') # 线性插值 all_emp_df['mgr'] = all_emp_df.mgr.fillna(-1).astype('i8') # 填 -1 表示“无上级”其中interpolate(method='linear')是按序列位置做线性插值,fillna(-1)常用于把“无上级”的缺失值编码为有意义的业务值,再转成整数类型,方便后续数值运算。
2.2 重复值的甄别与删除
先给部门表添加两行数据,让“研发部”和“销售部”各出现两次:
dept_df.loc[50] = {'dname': '研发部', 'dloc': '上海'} dept_df.loc[60] = {'dname': '销售部', 'dloc': '长沙'} dept_df输出:
dname dloc dno 10 会计部 北京 20 研发部 成都 30 销售部 重庆 40 运维部 天津 50 研发部 上海 60 销售部 长沙用duplicated判断重复,不指定参数时默认判断行索引是否重复,这里指定按dname判断:
dept_df.duplicated('dname')输出:
dno 10 False 20 False 30 False 40 False 50 True 60 True dtype: bool删除重复值用drop_duplicates,keep参数决定保留第一项('first',默认)、最后一项('last')还是全部删除(False):
dept_df.drop_duplicates('dname') # 默认 keep='first' dept_df.drop_duplicates('dname', keep='last')keep='last'的输出:
dname dloc dno 10 会计部 北京 40 运维部 天津 50 研发部 上海 60 销售部 长沙同样的思路可以清理员工数据。例如认定ename和job两个字段完全相同即为重复(all_emp_df中有三条“张三丰/总裁”记录),直接在原对象上删除:
all_emp_df.drop_duplicates(['ename', 'job'], inplace=True)说明:加上
inplace=True后该方法不返回新对象,而是在原DataFrame上直接删除。可以查看all_emp_df确认重复员工已被移除。
Notebook 中配套展示了完整的重复值“体检”手段(见 day04.ipynb):单列all_emp_df.ename.duplicated()、多列all_emp_df.duplicated(['ename', 'job'])、频次统计value_counts()(如“张三丰”出现 3 次、程序员岗位 5 人)、去重计数nunique(),以及keep='last'的删除写法。
2.3 异常值的检测与处理
异常值在统计学上全称“疑似异常值”,也叫离群点(outlier)。它指样本中出现的“极端值”,数值明显偏离其余观测值。有些异常值由系统或人为原因造成,但有些却能重复且稳定地出现,属于正常极端值——例如很多游戏产品中头部玩家的数据往往就是离群的极端值。所以既不能忽视异常值,也不能简单地把它们从分析中剔除;重视异常值、分析其产生原因,常常是发现问题进而改进决策的契机。
异常值检测有 Z-score、IQR、DBSCAN 聚类、孤立森林等方法,这里重点介绍前两种。
Z-score 方法
如果数据服从正态分布,依据 3σ 法则,与平均值偏差超过三倍标准差的值被定义为异常值;正态分布下距离均值 3σ 之外的值出现的概率为 P(|x − μ| > 3σ) < 0.003,属于小概率事件。如果数据不服从正态分布,可以用“远离均值多少倍标准差”来描述,这个倍数就是 Z-score——它以标准差为单位度量原始分数偏离平均值的距离:
$$ z = \frac {X - \mu} {\sigma} $$
通常把 $|z| > 3$ 的数据点视为离群点(阈值需结合经验与实际场景调整):
def detect_outliers_zscore(data, threshold=3): avg_value = np.mean(data) std_value = np.std(data) z_score = np.abs((data - avg_value) / std_value) return data[z_score > threshold]IQR 方法
IQR(Inter-Quartile Range)代表四分位距离,即上四分位数 Q3 与下四分位数 Q1 的差值。通常认为小于 Q1 − 1.5 × IQR 或大于 Q3 + 1.5 × IQR 的值为异常值,这也是箱线图(后续章节会讲到)默认使用的判据:
def detect_outliers_iqr(data, whis=1.5): q1, q3 = np.quantile(data, [0.25, 0.75]) iqr = q3 - q1 lower, upper = q1 - whis * iqr, q3 + whis * iqr return data[(data < lower) | (data > upper)]在 Notebook 中,detect_outliers_by_zscore也被实际用于删除异常成本数据(见 day04.ipynb),做法是先找出异常行索引,再配合下面的drop删除。
异常值的删除与替换
删除异常值可以用drop,它根据行索引或列索引删除指定行/列。例如认为月薪低于2000或高于8000是员工表中的异常值:
emp_df.drop(emp_df[(emp_df.sal > 8000) | (emp_df.sal < 2000)].index)替换异常值可以通过给单元格赋值实现,也可以使用replace。例如把月薪1800和9000替换为平均值、把补贴800替换为1000:
avg_sal = np.mean(emp_df.sal).astype(int) emp_df.replace({'sal': [1800, 9000], 'comm': 800}, {'sal': avg_sal, 'comm': 1000})replace还支持正则表达式批量归并文本,Notebook 中就有把“5-10年|10年以上”统一替换为“5年以上”、把“中专|高中”归并为“学历不限”的用法(见 day04.ipynb)。
三、数据预处理:拆解、变换与离散化
数据预处理是一个很大的话题,包含数据的拆解、变换、归约、离散化等操作。核心目标之一是把非数值类型(字符串、日期时间)加工成可计算的数值形态,因为非数值类型无法计算相关性,也无法做 χ² 检验等操作。
3.1 日期时间的拆解
如果数据表中的时间是日期时间,通常需要从年、季度、月、日、星期、小时、分钟等维度拆解。字符串形式的时间可先用to_datetime转换。下面读取 Excel 销售数据,把“销售日期”拆解为“月份”“季度”“星期”:
sales_df = pd.read_excel( 'data/2020年销售数据.xlsx', usecols=['销售日期', '销售区域', '销售渠道', '品牌', '销售额'] ) sales_df.info()说明:代码使用相对路径读取文件,即 Excel 文件位于当前工作路径下名为
data的文件夹中。仓库中已附带同主题数据集 2020年销售数据.xlsx,你可以参照本仓库 day04.ipynb 的组织方式自行验证。
输出:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1945 entries, 0 to 1944 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 销售日期 1945 non-null datetime64[ns] 1 销售区域 1945 non-null object 2 销售渠道 1945 non-null object 3 品牌 1945 non-null object 4 销售额 1945 non-null int64 dtypes: datetime64ns, int64(1), object(3) memory usage: 76.1+ KB借助日期时间Series的dt属性(常被称为“日期时间向量”)完成拆解:
sales_df['月份'] = sales_df['销售日期'].dt.month sales_df['季度'] = sales_df['销售日期'].dt.quarter sales_df['星期'] = sales_df['销售日期'].dt.weekday sales_df输出(新增三列):
销售日期 销售区域 销售渠道 品牌 销售额 月份 季度 星期 0 2020-01-01 上海 拼多多 八匹马 8217 1 1 2 1 2020-01-01 上海 抖音 八匹马 6351 1 1 2 ... 1944 2020-12-31 福建 天猫 八匹马 2673 12 3 3通过dt属性(如year、month、quarter、hour)取到的仍然是Series,包含整组时间信息,因此称其为“日期时间向量”。拆分出的“月份”“季度”等列,正是下一步分组聚合(pivot 透视、groupby 汇总)的天然分组维度,仓库 day05.ipynb 中的透视表正是基于销售日期拆出的“月份”对销售额做透视并fillna(0)补齐空值。
3.2 字符串数据的处理
先读取招聘网站的招聘数据:
jobs_df = pd.read_csv( 'data/某招聘网站招聘数据.csv', usecols=['city', 'companyFullName', 'positionName', 'salary'] ) jobs_df.info()输出:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 3140 entries, 0 to 3139 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 city 3140 non-null object 1 companyFullName 3140 non-null object 2 positionName 3140 non-null object 3 salary 3140 non-null object dtypes: object(4)查看前 5 条数据:
jobs_df.head()输出:
city companyFullName positionName salary 0 北京 达疆网络科技(上海)有限公司 数据分析岗 15k-30k 1 北京 北京音娱时光科技有限公司 数据分析 10k-18k 2 北京 北京千喜鹤餐饮管理有限公司 数据分析 20k-30k ...3140 条数据并非全是“数据分析”岗位。用str属性获得字符串向量,即可调用熟悉的字符串方法做模糊匹配:
jobs_df = jobs_df[jobs_df.positionName.str.contains('数据分析')] jobs_df.shape输出:(1515, 4),筛选后还剩 1515 条。
salary是15k-30k这样的范围字符串,要统计平均工资,需先抽取出上下限并求中间值。用str.extract配合正则捕获组抽取两组数字:
jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?')说明:通过正则捕获组从字符串中抽出两组数字,分别对应工资下限和上限。对正则表达式不熟悉的读者,可以参考本仓库 30.正则表达式的应用.md。
抽取出的两列都是字符串类型,先转换为int,再按行求均值:
temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int) temp_df.apply(np.mean, axis=1)applymap的参数是一个函数,作用于DataFrame的每个元素(此处把上下限批量转成int);apply的参数也是一个函数,可通过axis指定作用于行或列(此处axis=1对每行的上下限求均值)。最终把结果写回salary列:
temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int) jobs_df['salary'] = temp_df.apply(np.mean, axis=1) jobs_df.head()输出:
city companyFullName positionName salary 0 北京 达疆网络科技(上海)有限公司 数据分析岗 22.5 ... 4 北京 韦博网讯科技(北京)有限公司 数据分析 12.5apply、transform与map的定位差异
applymap和apply在预处理中非常常用,Series对象也有apply方法。此外DataFrame还有一个transform方法,同样通过传入函数对数据做变换,类似于Series的map。关键区别在于:apply具有归约效果,能把较多的数据加工成较少的数据甚至一条数据;transform没有归约效果,只能逐元素变换,原有数据量不变。需要“汇总成一条”时用apply,需要“一对一加工”时用transform。
3.3 字符串变量的三类编码策略
要对数据做深度分析与挖掘,字符串需要转成数值。按语义可以把字符串变量分为三类,采取不同处理:
| 类型 | 特征 | 处理方式 |
|---|---|---|
| 有序变量(Ordinal Variable) | 字符串之间有顺序关系 | 对字符串做序号化处理 |
| 分类变量 / 名义变量(Categorical / Nominal Variable) | 无大小关系和等级之分 | 用独热编码生成哑变量(虚拟变量)矩阵 |
| 定距变量(Scale Variable) | 本质对应有大小高低、可加减的数值 | 直接转换为对应数值 |
第一、三类可用apply/transform处理(后续课程也会介绍scikit-learn的OrdinalEncoder);第二类用pd.get_dummies()生成哑变量矩阵。先构造示例数据:
persons_df = pd.DataFrame( data={ '姓名': ['关羽', '张飞', '赵云', '马超', '黄忠'], '职业': ['医生', '医生', '程序员', '画家', '教师'], '学历': ['研究生', '大专', '研究生', '高中', '本科'] } ) persons_df把“职业”处理成哑变量矩阵(每个取值一列,命中为 1):
pd.get_dummies(persons_df['职业'])输出:
医生 教师 画家 程序员 0 1 0 0 0 1 1 0 0 0 2 0 0 0 1 3 0 0 1 0 4 0 1 0 0把“学历”这种有序变量映射成递增的数值:
def handle_education(x): edu_dict = {'高中': 1, '大专': 3, '本科': 5, '研究生': 10} return edu_dict.get(x, 0) persons_df['学历'].apply(handle_education)输出:
0 10 1 3 2 10 3 1 4 5 Name: 学历, dtype: int643.4 数据离散化(分箱)
离散化也叫分箱:连续变量的取值有无数种可能,分组统计时很不方便,于是预先设置若干“箱子”(每个箱子代表一段取值区间),把连续值分配到不同箱子中。下面用 2018 年北京积分落户数据,按积分对数据进行分组:
luohu_df = pd.read_csv('data/2018年北京积分落户数据.csv', index_col='id') luohu_df.score.describe()说明:仓库中附带了同主题的 2023年北京积分落户数据.csv,结构类似,可用于练习同样的分箱流程。
输出:
count 6019.000000 mean 95.654552 std 4.354445 min 90.750000 25% 92.330000 50% 94.460000 75% 97.750000 max 122.590000 Name: score, dtype: float64积分的最大值是122.59、最小值是90.75,构造从90分到125分、每 5 分一组的 7 个箱子,用cut分箱:
bins = np.arange(90, 126, 5) pd.cut(luohu_df.score, bins, right=False)说明:
cut的right参数默认True,表示箱子左开右闭;改为False后右边界为开区间、左边界为闭区间。
输出:
id 1 [120, 125) 2 [120, 125) ... 6019 [90, 95) Name: score, Length: 6019, dtype: category Categories (7, interval[int64, left]): [[90, 95) < [95, 100) < [100, 105) < [105, 110) < [110, 115) < [115, 120) < [120, 125)]可以看到结果是一个category类型,7 个区间从左到右有序排列,这正好为“按分箱结果分组后做聚合统计”做好准备(下一章节会详细讲解分组聚合)。此外pandas还提供qcut函数,可按分位数对数据进行等频分箱,当数据分布不均时比等距分箱更合适,有兴趣的读者可以自行研究。
四、小结
本文围绕 pandas 数据分析的“重塑—清洗—预处理”三段式流程展开:
- 数据重塑:
concat拼接结构一致的数据(可配合ignore_index=True批量堆叠多文件),merge按how/on(或left_on/right_on)连接事实表与维度表,行为与 SQL 的UNION/JOIN一一对应; - 数据清洗:
isna甄别、dropna按轴删除、fillna常量/前值/后值/插值填充缺失值;duplicated甄别、drop_duplicates配合keep参数删除重复值;Z-score 与 IQR 两类方法检测异常值,再用drop删除或replace替换; - 数据预处理:
dt向量拆解日期时间、str向量做字符串筛选与正则提取、applymap/apply/transform完成元素级或行列级变换,分类变量用get_dummies独热编码、有序变量映射为数值,连续值用cut/qcut分箱离散化。
本仓库配套的 day04.ipynb 完整复现了上述全部案例(含运行输出),数据文件位于 Day66-80/code/res,推荐在 Jupyter 环境中边运行边理解。掌握了这些基本功,下一章就可以放心地对数据进行分组聚合与深度挖掘了。
- 文档
- 教程
【免费下载链接】Python-100-Days
Python - 100天从新手到大师
相关推荐
Duix Avatar 上手全解:离线克隆 AI 数字人,本地生成口播视频
Duix Avatar 上手全解:离线克隆 AI 数字人,本地生成口播视频 Duix Avatar 是一款完全离线运行的开源 AI 数字人工具。上传一段 10
人工智能AI 应用数字人媒体生成桌面应用Python-100-Days 数据分析实战:pandas 数据透视与可视化(深入浅出 pandas-4 精讲)
Python 100 Days 数据分析实战:pandas 数据透视与可视化(深入浅出 pandas 4 精讲) 数据准备与清洗只是数据分析的前半程,真正产生业
文档教程PT助手Plus安装与配置完整指南:10分钟打通批量下载种子
PT助手Plus安装与配置完整指南:10分钟打通批量下载种子 PT助手Plus是一款浏览器插件(Web Extensions),支持 Microsoft Edg
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考