第1章 Pandas入门与核心操作
本章的目标是掌握Pandas最基础但最常用的内容:什么是Series、什么是
DataFrame,如何查看数据,如何筛选行列,如何排序,以及如何新增计算列。
Pandas 最核心的两个数据结构是:
Series 可以理解成“一列数据”。
1 2 3 s = pd.Series([80 , 90 , 75 ], index=["张三" , "李四" , "王五" ])print (s)
输出类似:
1 2 3 4 张三 80 李四 90 王五 75 dtype: int64
DataFrame 可以理解成“一张二维表格”,类似 Excel
表。
1 2 3 4 5 6 7 8 9 10 11 data = { "name" : ["张三" , "李四" , "王五" , "赵六" , "钱七" ], "class" : ["一班" , "一班" , "二班" , "二班" , "一班" ], "math" : [85 , 92 , 78 , 90 , 66 ], "english" : [88 , 81 , 95 , 76 , 70 ], "age" : [18 , 19 , 18 , 20 , 19 ] } df = pd.DataFrame(data)print (df)
输出:
1 2 3 4 5 6 name class math english age 0 张三 一班 85 88 18 1 李四 一班 92 81 19 2 王五 二班 78 95 18 3 赵六 二班 90 76 20 4 钱七 一班 66 70 19
1.1 读取数据
考试中常见的数据来源是 CSV、Excel 或直接手动构造 DataFrame。
读取 CSV:
1 df = pd.read_csv("students.csv" )
读取 Excel:
1 df = pd.read_excel("students.xlsx" )
指定编码,中文 CSV 经常会用到:
1 df = pd.read_csv("students.csv" , encoding="utf-8" )
如果遇到中文乱码,可以尝试:
1 df = pd.read_csv("students.csv" , encoding="gbk" )
保存为 CSV:
1 df.to_csv("result.csv" , index=False )
这里的 index=False
很重要,它表示保存时不要把行索引单独存成一列。考试和实际项目里经常会因为忘记写这个参数,导致导出的文件多一列
Unnamed: 0。
1.2 快速查看数据
拿到一个 DataFrame 后,不要直接开始算,先观察结构。
查看前 5 行。
查看后 5 行。
查看行数和列数。例如结果 (5, 5) 表示 5 行 5 列。
查看所有列名。
查看行索引。
查看每列的数据类型、非空数量。这个非常重要,尤其适合检查缺失值和数据类型。
查看数值列的统计信息,例如平均值、标准差、最小值、最大值、四分位数。
示例:
可能输出:
1 2 3 4 5 6 7 8 9 math english agecount 5 .000000 5 .000000 5 .000000 mean 82 .200000 82 .000000 18 .800000 std 10 .401923 9 .695360 0 .836660 min 66 .000000 70 .000000 18 .000000 25 % 78 .000000 76 .000000 18 .000000 50 % 85 .000000 81 .000000 19 .000000 75 % 90 .000000 88 .000000 19 .000000 max 92 .000000 95 .000000 20 .000000
认证考试中,head()、info()、describe()
经常用于“探索性数据分析”的第一步。
1.3 选择列
选择一列,返回 Series:
选择多列,返回 DataFrame:
1 df[["name" , "math" , "english" ]]
注意,多列选择时需要两层中括号:
外层 [] 表示从 DataFrame 中取列,内层 ["name", "math"]
是列名列表。
错误写法:
这通常会报错,因为 Pandas 会把 ("name", "math")
当作一个整体列名。
1.4 选择行:loc 和 iloc
Pandas 选择行列时,最常用的是 loc 和 iloc。
loc 按标签选择。
表示选择索引标签为 0 的那一行。
选择第 0 到第 2 行,并选择指定列:
1 df.loc[0 :2 , ["name" , "math" ]]
注意:loc[0:2] 会包含索引 2。
iloc 按位置选择。
表示选择第 1 行,也就是位置为 0 的行。
表示选择前 3 行、前 3 列。
注意:iloc[0:3] 不包含位置 3。这个和 Python 切片一致。
总结一下:
1 2 df.loc[行标签, 列标签] df.iloc[行位置, 列位置]
最容易混淆的是:
包含 2。
不包含 2。
1.5 条件筛选
筛选数学成绩大于 80 的学生:
筛选一班学生:
多个条件需要使用 & 或 |。
数学成绩大于 80 且英语成绩大于 80:
1 df[(df["math" ] > 80 ) & (df["english" ] > 80 )]
数学成绩大于 90 或英语成绩大于 90:
1 df[(df["math" ] > 90 ) | (df["english" ] > 90 )]
注意:每个条件都要加括号。
错误写法:
1 df[df["math" ] > 80 & df["english" ] > 80 ]
这个写法容易报错或得到错误结果,因为 &
的优先级会影响表达式计算。
筛选不等于某个值:
筛选某列属于多个值:
1 df[df["class" ].isin(["一班" , "二班" ])]
筛选名字在指定列表中的学生:
1 df[df["name" ].isin(["张三" , "王五" ])]
1.6 query 简化筛选
query() 可以让条件筛选更接近普通表达式。
多个条件:
1 df.query("math > 80 and english > 80" )
字符串条件:
1 df.query("class == '一班'" )
如果列名中有空格或特殊字符,需要用反引号:
1 df.query("`math score` > 80" )
考试中更推荐掌握普通布尔筛选,因为它适用范围更广:
1 df[(df["math" ] > 80 ) & (df["english" ] > 80 )]
1.7 新增列和计算列
新增总分列:
1 2 3 df["total" ] = df["math" ] + df["english" ]print (df)
新增平均分列:
1 df["avg" ] = df["total" ] / 2
根据条件生成是否及格:
1 df["pass_math" ] = df["math" ] >= 60
结果是布尔值:
也可以生成中文标签:
1 2 3 df["math_level" ] = "不及格" df.loc[df["math" ] >= 60 , "math_level" ] = "及格" df.loc[df["math" ] >= 85 , "math_level" ] = "优秀"
这里有一个重要技巧:不要用循环逐行处理,优先使用 Pandas
的列运算和条件赋值。
更推荐的写法是用 apply 或 pd.cut,不过这部分可以放在后续文章。
1.8 排序
按数学成绩升序排序:
按数学成绩降序排序:
1 df.sort_values("math" , ascending=False )
按班级升序、总分降序排序:
1 df.sort_values(["class" , "total" ], ascending=[True , False ])
排序后重置索引:
1 df_sorted = df.sort_values("total" , ascending=False ).reset_index(drop=True )
drop=True 表示丢弃旧索引,不把旧索引变成新的一列。
1.9 常见统计操作
数学平均分:
数学最高分:
数学最低分:
数学成绩中位数:
数学成绩标准差:
统计班级人数:
1 df["class" ].value_counts()
统计唯一值数量:
查看唯一值:
1.10
完整示例:筛选优秀学生并排序
需求:找出总分大于 170
的学生,按总分从高到低排序,只保留姓名、班级、数学、英语、总分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import pandas as pd data = { "name" : ["张三" , "李四" , "王五" , "赵六" , "钱七" ], "class" : ["一班" , "一班" , "二班" , "二班" , "一班" ], "math" : [85 , 92 , 78 , 90 , 66 ], "english" : [88 , 81 , 95 , 76 , 70 ], "age" : [18 , 19 , 18 , 20 , 19 ] } df = pd.DataFrame(data) df["total" ] = df["math" ] + df["english" ] excellent = df[df["total" ] > 170 ] excellent = excellent.sort_values("total" , ascending=False ) excellent = excellent[["name" , "class" , "math" , "english" , "total" ]]print (excellent)
输出:
1 2 3 4 name class math english total 1 李四 一班 92 81 173 0 张三 一班 85 88 173 2 王五 二班 78 95 173
这个例子覆盖了认证考试中很常见的一条链路:
读取或构造数据 → 新增计算列 → 条件筛选 → 排序 → 选择指定列。
1.11 高频易错点
第一,选择多列必须使用双层中括号。
第二,多个筛选条件必须给每个条件加括号。
1 df[(df["math" ] > 80 ) & (df["english" ] > 80 )]
第三,loc 是标签选择,iloc 是位置选择。
1 2 df.loc[0 :2 ] df.iloc[0 :2 ]
第四,排序不会默认修改原 DataFrame。
这只是返回排序后的新结果。如果要保存结果,需要赋值:
1 df = df.sort_values("math" )
或者使用:
1 df.sort_values("math" , inplace=True )
不过更推荐第一种赋值写法,因为更清晰。
第五,导出 CSV 时经常要加 index=False。
1 df.to_csv("result.csv" , index=False )
1.12 本章练习题
使用下面的数据完成练习:
1 2 3 4 5 6 7 8 9 import pandas as pd df = pd.DataFrame({ "name" : ["Alice" , "Bob" , "Cindy" , "David" , "Eva" ], "gender" : ["F" , "M" , "F" , "M" , "F" ], "python" : [90 , 75 , 88 , 60 , 95 ], "sql" : [85 , 80 , 92 , 70 , 89 ], "class" : ["A" , "A" , "B" , "B" , "A" ] })
练习 1:查看前 3 行。
练习 2:筛选 Python 成绩大于 80 的学生。
练习 3:筛选 Python 和 SQL 都大于 85 的学生。
1 df[(df["python" ] > 85 ) & (df["sql" ] > 85 )]
练习 4:新增总分列 total。
1 df["total" ] = df["python" ] + df["sql" ]
练习 5:按总分从高到低排序。
1 df.sort_values("total" , ascending=False )
练习 6:只保留 name、class、total 三列。
1 df[["name" , "class" , "total" ]]
练习 7:找出 A 班中总分大于 170 的学生。
1 df[(df["class" ] == "A" ) & (df["total" ] > 170 )]
本章必须掌握的代码模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import pandas as pd df = pd.read_csv("data.csv" ) df.head() df.info() df.describe() df["col" ] df[["col1" , "col2" ]] df[df["score" ] > 80 ] df[(df["score" ] > 80 ) & (df["age" ] >= 18 )] df["total" ] = df["score1" ] + df["score2" ] df.sort_values("total" , ascending=False ) df.to_csv("result.csv" , index=False )
第2章 数据清洗与预处理
本章是 Pandas
认证考试和实际数据分析中都非常高频的内容。很多题目或实际场景下不会直接让读者“数据清洗”,而是给读者一份脏数据,让读者完成统计、分组、排序。如果不先处理缺失值、重复值、数据类型和异常字符串,后面的分析结果很容易错。
本章重点掌握 5 类操作:
缺失值处理
重复值处理
数据类型转换
字符串清洗
日期时间处理
先准备一份示例数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 import pandas as pdimport numpy as np df = pd.DataFrame({ "name" : [" 张三 " , "李四" , "王五" , "赵六" , "赵六" , None ], "class" : ["一班" , "一班" , "二班" , "二班" , "二班" , "一班" ], "math" : [85 , 92 , np.nan, 90 , 90 , 66 ], "english" : [88 , None , 95 , 76 , 76 , 70 ], "age" : ["18" , "19" , "18" , "20" , "20" , "未知" ], "date" : ["2024-01-01" , "2024/01/02" , "2024-01-03" , None , "2024-01-03" , "bad_date" ] })print (df)
输出大致如下:
1 2 3 4 5 6 7 name class math english age date0 张三 一班 85 .0 88 18 2024 -01 -01 1 李四 一班 92 .0 None 19 2024 /01 /02 2 王五 二班 NaN 95 18 2024 -01 -03 3 赵六 二班 90 .0 76 20 None4 赵六 二班 90 .0 76 20 2024 -01 -03 5 None 一班 66 .0 70 未知 bad_date
注意:真实输出中的对齐格式可能略有不同,不影响理解。
2.1 发现缺失值
缺失值通常包括 NaN、None、NaT 等。Pandas 中常用 isna() 或 isnull()
检查缺失值,这两个方法基本等价。
查看每个位置是否缺失:
统计每列缺失值数量:
输出可能是:
1 2 3 4 5 6 7 name 1 class 0 math 1 english 1 age 0 date 1 dtype: int64
统计整个 DataFrame 的缺失值总数:
查看有缺失值的行:
1 df[df.isna().any (axis=1 )]
这里的 axis=1
表示按行判断。只要某一行任意一列存在缺失值,就返回这一行。
查看没有缺失值的行:
1 df[df.notna().all (axis=1 )]
2.2 删除缺失值
删除含有任意缺失值的行:
1 df_drop_any = df.dropna()
只要某一行有一个缺失值,就会被删除。
删除全部都是缺失值的行:
1 df_drop_all = df.dropna(how="all" )
只根据某些关键列判断是否删除:
1 df_drop_score_na = df.dropna(subset=["math" , "english" ])
这表示如果 math 或 english 中有缺失值,就删除该行。
保留至少有 5 个非空值的行:
1 df_thresh = df.dropna(thresh=5 )
考试常见点:
不会默认修改原 DataFrame。要保存结果,需要赋值:
或者使用:
但实际写代码更推荐赋值方式,因为逻辑更清晰。
2.3 填充缺失值
填充固定值:
1 df["name" ] = df["name" ].fillna("未知" )
数值列填充平均值:
1 df["math" ] = df["math" ].fillna(df["math" ].mean())
数值列填充中位数:
1 df["math" ] = df["math" ].fillna(df["math" ].median())
分类列填充众数:
1 df["class" ] = df["class" ].fillna(df["class" ].mode()[0 ])
多个列分别填充:
1 2 3 4 5 6 df = df.fillna({ "name" : "未知" , "math" : df["math" ].mean(), "english" : df["english" ].mean(), "date" : "未知日期" })
向前填充:
1 df["math" ] = df["math" ].ffill()
向后填充:
1 df["math" ] = df["math" ].bfill()
常见使用场景:
1 df["date" ] = df["date" ].ffill()
时间序列数据中,如果某一天没有记录,可以用前一天的数据填充,但要根据业务合理使用。
2.4 重复值处理
检查每一行是否重复:
统计重复行数量:
查看重复行:
删除完全重复的行:
1 df_unique = df.drop_duplicates()
根据指定列判断重复:
1 df_unique_name = df.drop_duplicates(subset=["name" ])
保留最后一条重复记录:
1 df_unique_last = df.drop_duplicates(subset=["name" ], keep="last" )
删除所有重复记录,不保留任何一条:
1 df_no_duplicate = df.drop_duplicates(subset=["name" ], keep=False )
示例:
1 2 3 4 5 6 df = pd.DataFrame({ "id" : [1 , 2 , 2 , 3 , 3 , 3 ], "score" : [80 , 90 , 90 , 70 , 75 , 75 ] })print (df.drop_duplicates())
结果:
1 2 3 4 5 id score0 1 80 1 2 90 3 3 70 4 3 75
因为第 4、5 行完全相同,所以只保留一条。
如果按 id 去重:
1 df.drop_duplicates(subset=["id" ])
结果:
1 2 3 4 id score0 1 80 1 2 90 3 3 70
2.5 数据类型查看与转换
查看每列类型:
或者:
常见类型包括:
1 2 3 4 5 int64 float64 object bool datetime64
2.5.1 转成整数或小数
假设 age 列里面有 "18"、"19"、"未知"。
直接转换会报错:
因为 "未知" 不能转成整数。
更稳妥的方式:
1 df["age_num" ] = pd.to_numeric(df["age" ], errors="coerce" )
errors="coerce" 的含义是:无法转换的值变成 NaN。
然后可以填充缺失值:
1 df["age_num" ] = df["age_num" ].fillna(df["age_num" ].median())
最后再转整数:
1 df["age_num" ] = df["age_num" ].astype(int )
2.5.2 转成字符串
1 df["name" ] = df["name" ].astype(str )
但要注意,如果原本有缺失值 None,转成字符串后会变成
"None",这有时不是你想要的结果。
更稳妥的写法是先填充:
1 df["name" ] = df["name" ].fillna("未知" ).astype(str )
2.5.3 转成类别类型
当某一列取值种类很少时,可以转成 category。
1 df["class" ] = df["class" ].astype("category" )
例如班级、性别、地区等字段,经常适合转成类别类型。
2.6 字符串清洗
Pandas 中字符串列可以通过 .str 调用字符串方法。
先看这个例子:
1 2 3 4 5 df = pd.DataFrame({ "name" : [" 张三 " , "李四" , "王五 " , " 赵六" ], "phone" : ["138-0000-1111" , "139 0000 2222" , "137.0000.3333" , "13600004444" ], "email" : ["A@qq.com" , "b@QQ.com" , " c@163.com " , "D@GMAIL.COM" ] })
去除首尾空格:
1 df["name" ] = df["name" ].str .strip()
转小写:
1 df["email" ] = df["email" ].str .lower()
转大写:
1 df["email" ] = df["email" ].str .upper()
判断是否包含某个字符串:
1 df[df["email" ].str .contains("qq" , case =False , na=False )]
这里:
case=False 表示忽略大小写。
na=False 表示如果遇到缺失值,不要报错,直接当作 False。
替换字符串:
1 df["phone" ] = df["phone" ].str .replace("-" , "" , regex=False )
多个符号统一删除,可以使用正则:
1 df["phone" ] = df["phone" ].str .replace(r"[-.\s]" , "" , regex=True )
这里的意思是把 -、.、空白符都替换为空字符串。
提取邮箱后缀:
1 df["email_domain" ] = df["email" ].str .split("@" ).str [1 ]
判断是否以某个字符串开头:
1 df[df["phone" ].str .startswith("138" )]
判断是否以某个字符串结尾:
1 df[df["email" ].str .endswith("@qq.com" )]
字符串长度:
1 df["phone_len" ] = df["phone" ].str .len ()
2.7 日期时间处理
日期列在刚读取时经常是字符串,需要转换成日期类型。
1 df["date" ] = pd.to_datetime(df["date" ], errors="coerce" )
同样,errors="coerce" 表示无法转换的值变成缺失日期 NaT。
示例:
1 2 3 4 5 6 7 df = pd.DataFrame({ "date" : ["2024-01-01" , "2024/01/02" , "bad_date" , None ] }) df["date" ] = pd.to_datetime(df["date" ], errors="coerce" )print (df)
结果:
1 2 3 4 5 date 0 2024 -01 -01 1 2024 -01 -02 2 NaT3 NaT
提取年份:
1 df["year" ] = df["date" ].dt.year
提取月份:
1 df["month" ] = df["date" ].dt.month
提取日期中的日:
1 df["day" ] = df["date" ].dt.day
提取星期几:
1 df["weekday" ] = df["date" ].dt.day_name()
筛选某个日期之后的数据:
1 df[df["date" ] >= "2024-01-02" ]
筛选某个日期范围:
1 df[(df["date" ] >= "2024-01-01" ) & (df["date" ] <= "2024-01-31" )]
计算两个日期之间相差多少天:
1 df["days_diff" ] = df["end_date" ] - df["start_date" ]
如果想得到整数天数:
1 df["days_diff" ] = (df["end_date" ] - df["start_date" ]).dt.days
2.8 异常值处理
异常值不一定是缺失值。例如年龄为 999、成绩为 -10、价格为
0,都可能是异常值。
示例数据:
1 2 3 4 df = pd.DataFrame({ "name" : ["A" , "B" , "C" , "D" ], "score" : [85 , 92 , -10 , 1000 ] })
查看异常值:
1 df[(df["score" ] < 0 ) | (df["score" ] > 100 )]
把异常值替换为缺失值:
1 df.loc[(df["score" ] < 0 ) | (df["score" ] > 100 ), "score" ] = np.nan
再用平均值或中位数填充:
1 df["score" ] = df["score" ].fillna(df["score" ].median())
也可以直接过滤掉异常值:
1 df = df[(df["score" ] >= 0 ) & (df["score" ] <= 100 )]
什么时候删,什么时候填?
一般规则是:
缺失或异常数据很少,可以删除。
数据量小、删除会影响样本分布,可以填充。
成绩、金额、年龄这类字段,填充前要考虑业务含义,不要机械地填平均值。
2.9 apply 函数:自定义清洗逻辑
当内置方法不够用时,可以使用 apply()。
示例:根据成绩生成等级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 df = pd.DataFrame({ "name" : ["A" , "B" , "C" , "D" ], "score" : [95 , 82 , 67 , 40 ] })def get_level (score ): if score >= 90 : return "优秀" elif score >= 80 : return "良好" elif score >= 60 : return "及格" else : return "不及格" df["level" ] = df["score" ].apply(get_level)print (df)
输出:
1 2 3 4 5 name score level0 A 95 优秀1 B 82 良好2 C 67 及格3 D 40 不及格
也可以用 lambda:
1 df["pass" ] = df["score" ].apply(lambda x: "及格" if x >= 60 else "不及格" )
不过对于简单条件,后面学到的 np.where() 或 pd.cut() 更高效。
2.10 where、mask 和 np.where
这部分也很适合考试。
2.10.1 np.where
根据条件生成新列:
1 2 3 import numpy as np df["pass" ] = np.where(df["score" ] >= 60 , "及格" , "不及格" )
含义是:
1 np.where(条件, 条件为True 时的值, 条件为False 时的值)
2.10.2 where
where 是保留满足条件的值,不满足条件的值替换掉。
1 df["score_clean" ] = df["score" ].where(df["score" ] >= 60 , 0 )
意思是:如果成绩大于等于 60,就保留原成绩;否则变成 0。
2.10.3 mask
mask 和 where 相反,它替换满足条件的值。
1 df["score_clean" ] = df["score" ].mask(df["score" ] < 60 , 0 )
意思是:如果成绩小于 60,就替换成 0;否则保留原成绩。
2.11 完整清洗流程示例
需求:
给定学生成绩数据,完成以下清洗:
去除姓名前后空格
删除重复学生记录,保留最后一次
将年龄转成数值
将无法转换的年龄填充为中位数
将日期转成日期类型
删除数学或英语缺失的记录
新增总分列
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import pandas as pdimport numpy as np df = pd.DataFrame({ "name" : [" 张三 " , "李四" , "王五" , "赵六" , "赵六" , None ], "class" : ["一班" , "一班" , "二班" , "二班" , "二班" , "一班" ], "math" : [85 , 92 , np.nan, 90 , 90 , 66 ], "english" : [88 , None , 95 , 76 , 76 , 70 ], "age" : ["18" , "19" , "18" , "20" , "20" , "未知" ], "date" : ["2024-01-01" , "2024/01/02" , "2024-01-03" , None , "2024-01-03" , "bad_date" ] }) df["name" ] = df["name" ].fillna("未知" ).str .strip() df = df.drop_duplicates(subset=["name" ], keep="last" ) df["age" ] = pd.to_numeric(df["age" ], errors="coerce" ) df["age" ] = df["age" ].fillna(df["age" ].median()) df["date" ] = pd.to_datetime(df["date" ], errors="coerce" ) df = df.dropna(subset=["math" , "english" ]) df["total" ] = df["math" ] + df["english" ]print (df)
这段代码是数据清洗题的标准模板,认证考试可以直接套用。
2.12 高频易错点
第一,dropna()、fillna()、drop_duplicates()
默认都不会修改原数据。
只是返回新结果。要保存必须写:
第二,判断缺失值不要用:
正确写法是:
因为 NaN 和任何值比较都不相等,包括它自己。
第三,字符串操作要通过 .str。
不能写成:
第四,日期操作要先转成 datetime,再用 .dt。
1 2 df["date" ] = pd.to_datetime(df["date" ]) df["year" ] = df["date" ].dt.year
第五,类型转换遇到脏数据时,优先考虑:
1 2 pd.to_numeric(df["age" ], errors="coerce" ) pd.to_datetime(df["date" ], errors="coerce" )
第六,多条件筛选异常值时,每个条件都要加括号。
1 df[(df["score" ] < 0 ) | (df["score" ] > 100 )]
2.13 本章练习题
使用下面的数据完成练习:
1 2 3 4 5 6 7 8 9 import pandas as pdimport numpy as np df = pd.DataFrame({ "student" : [" Alice " , "Bob" , "Cindy" , "David" , "David" , None ], "score" : ["90" , "85" , "bad" , "70" , "70" , "60" ], "city" : ["Beijing" , "Shanghai" , None , "Guangzhou" , "Guangzhou" , "Beijing" ], "date" : ["2024-03-01" , "2024/03/02" , "wrong" , None , "2024-03-04" , "2024-03-05" ] })
练习 1:统计每列缺失值数量。
练习 2:将 student 缺失值填充为 "Unknown",并去除前后空格。
1 df["student" ] = df["student" ].fillna("Unknown" ).str .strip()
练习 3:按 student 去重,保留最后一次。
1 df = df.drop_duplicates(subset=["student" ], keep="last" )
练习 4:将 score 转成数值,无法转换的变为缺失值。
1 df["score" ] = pd.to_numeric(df["score" ], errors="coerce" )
练习 5:用平均分填充 score 的缺失值。
1 df["score" ] = df["score" ].fillna(df["score" ].mean())
练习 6:将 date 转成日期类型。
1 df["date" ] = pd.to_datetime(df["date" ], errors="coerce" )
练习 7:新增 month 列,表示月份。
1 df["month" ] = df["date" ].dt.month
练习 8:筛选分数大于等于 80 的学生。
练习 9:将缺失城市填充为出现次数最多的城市。
1 df["city" ] = df["city" ].fillna(df["city" ].mode()[0 ])
练习 10:新增是否及格列。
1 df["pass" ] = np.where(df["score" ] >= 60 , "及格" , "不及格" )
本章必须掌握的代码模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import pandas as pdimport numpy as np df.isna().sum () df = df.dropna() df = df.dropna(subset=["关键列" ]) df["col" ] = df["col" ].fillna(0 ) df["score" ] = df["score" ].fillna(df["score" ].mean()) df = df.drop_duplicates() df = df.drop_duplicates(subset=["id" ], keep="last" ) df["age" ] = pd.to_numeric(df["age" ], errors="coerce" ) df["date" ] = pd.to_datetime(df["date" ], errors="coerce" ) df["name" ] = df["name" ].str .strip() df["email" ] = df["email" ].str .lower() df["phone" ] = df["phone" ].str .replace(r"[-.\s]" , "" , regex=True ) df["year" ] = df["date" ].dt.year df["month" ] = df["date" ].dt.month df["pass" ] = np.where(df["score" ] >= 60 , "及格" , "不及格" ) df.loc[(df["score" ] < 0 ) | (df["score" ] > 100 ), "score" ] = np.nan
第3章 统计分析与聚合
本章是 Pandas
认证考试中最容易出综合题的部分。很多题目会要求读者按照班级、性别、地区、日期等字段进行分组,然后计算平均值、总和、人数、最高分、最低分,或者找出每组排名靠前的记录。
本章重点掌握:
groupby 分组统计
agg 多指标聚合
transform 分组后回填结果
pivot_table 透视表
rank 排名
cut 分箱统计
分组 Top N 问题
先准备一份学生成绩数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import pandas as pdimport numpy as np df = pd.DataFrame({ "name" : ["张三" , "李四" , "王五" , "赵六" , "钱七" , "孙八" , "周九" , "吴十" ], "class" : ["一班" , "一班" , "一班" , "二班" , "二班" , "二班" , "三班" , "三班" ], "gender" : ["M" , "F" , "M" , "F" , "M" , "F" , "M" , "F" ], "math" : [85 , 92 , 76 , 88 , 95 , 67 , 73 , 81 ], "english" : [80 , 89 , 90 , 75 , 85 , 70 , 88 , 92 ], "city" : ["北京" , "上海" , "北京" , "广州" , "广州" , "北京" , "上海" , "广州" ] }) df["total" ] = df["math" ] + df["english" ]print (df)
输出大致如下:
1 2 3 4 5 6 7 8 9 name class gender math english city total 0 张三 一班 M 85 80 北京 165 1 李四 一班 F 92 89 上海 181 2 王五 一班 M 76 90 北京 166 3 赵六 二班 F 88 75 广州 163 4 钱七 二班 M 95 85 广州 180 5 孙八 二班 F 67 70 北京 137 6 周九 三班 M 73 88 上海 161 7 吴十 三班 F 81 92 广州 173
3.1 groupby 基础:按一列分组
按班级统计数学平均分:
1 df.groupby("class" )["math" ].mean()
输出类似:
1 2 3 4 5 class 一班 84.333333 三班 77.000000 二班 83.333333 Name: math, dtype: float64
按班级统计总分平均值:
1 df.groupby("class" )["total" ].mean()
按班级统计人数:
1 df.groupby("class" )["name" ].count()
或者更常用:
1 df.groupby("class" ).size()
count() 和 size() 的区别很重要:
1 df.groupby("class" )["name" ].count()
统计的是 name 列非缺失值数量。
1 df.groupby("class" ).size()
统计的是每组总行数,包括缺失值。
考试中如果题目问“每个班有多少条记录”,优先用 size()。
3.2 按多列分组
按班级和性别统计数学平均分:
1 df.groupby(["class" , "gender" ])["math" ].mean()
输出类似:
1 2 3 4 5 6 7 8 class gender 一班 F 92.0 M 80.5 三班 F 81.0 M 73.0 二班 F 77.5 M 95.0 Name: math, dtype: float64
如果想把结果变回普通表格,可以加:
1 df.groupby(["class" , "gender" ])["math" ].mean().reset_index()
输出会变成 DataFrame:
1 2 3 4 5 6 7 class gender math0 一班 F 92 .0 1 一班 M 80 .5 2 三班 F 81 .0 3 三班 M 73 .0 4 二班 F 77 .5 5 二班 M 95 .0
reset_index()
是分组题中非常常用的收尾操作。它可以把分组字段从索引恢复成普通列。
3.3 常见聚合函数
常用聚合函数如下:
1 2 3 4 5 6 7 8 9 10 mean() sum () count() size() max () min () median() std() var() nunique()
示例:按班级统计总分最大值:
1 df.groupby("class" )["total" ].max ()
按城市统计学生数量:
1 df.groupby("city" ).size()
按班级统计数学成绩标准差:
1 df.groupby("class" )["math" ].std()
按班级统计不同城市数量:
1 df.groupby("class" )["city" ].nunique()
3.4 agg:一次统计多个指标
agg() 是分组统计的核心方法。
按班级统计数学成绩的平均值、最高分、最低分:
1 df.groupby("class" )["math" ].agg(["mean" , "max" , "min" ])
输出:
1 2 3 4 5 mean max min class 一班 84.333333 92 76 三班 77.000000 81 73 二班 83.333333 95 67
按班级统计多个字段:
1 2 3 4 5 df.groupby("class" ).agg({ "math" : ["mean" , "max" , "min" ], "english" : ["mean" , "max" , "min" ], "total" : ["mean" , "max" , "min" ] })
这种写法结果会产生多层列索引。考试中可以接受,但实际使用时有时希望列名更清晰。
更推荐命名聚合写法:
1 2 3 4 5 6 7 8 9 result = df.groupby("class" ).agg( math_avg=("math" , "mean" ), math_max=("math" , "max" ), english_avg=("english" , "mean" ), total_avg=("total" , "mean" ), student_count=("name" , "count" ) ).reset_index()print (result)
输出类似:
1 2 3 4 class math_avg math_max english_avg total_avg student_count0 一班 84 .333333 92 86 .333333 170 .666667 3 1 三班 77 .000000 81 90 .000000 167 .000000 2 2 二班 83 .333333 95 76 .666667 160 .000000 3
这类写法非常适合期末综合题,因为列名直接就是你想要的结果。
3.5 分组后筛选
需求:找出平均总分大于 165 的班级。
1 2 3 4 5 class_avg = df.groupby("class" )["total" ].mean().reset_index() result = class_avg[class_avg["total" ] > 165 ]print (result)
也可以写成链式代码:
1 2 3 4 5 6 7 8 result = ( df.groupby("class" )["total" ] .mean() .reset_index() .query("total > 165" ) )print (result)
链式写法在考试中不是必须,但适合写综合分析代码。
agg() 会把多行压缩成一行,而 transform() 会保持原来的行数。
需求:给每个学生新增一列,表示所在班级的数学平均分。
1 df["class_math_avg" ] = df.groupby("class" )["math" ].transform("mean" )
结果类似:
1 2 3 4 5 6 7 8 9 name class math class_math_avg0 张三 一班 85 84 .333333 1 李四 一班 92 84 .333333 2 王五 一班 76 84 .333333 3 赵六 二班 88 83 .333333 4 钱七 二班 95 83 .333333 5 孙八 二班 67 83 .333333 6 周九 三班 73 77 .000000 7 吴十 三班 81 77 .000000
然后可以继续判断学生是否高于本班平均分:
1 df["above_class_avg" ] = df["math" ] > df["class_math_avg" ]
筛选出数学成绩高于本班平均分的学生:
1 df[df["math" ] > df["class_math_avg" ]]
transform() 常见用途:
1 2 3 df["组内平均值" ] = df.groupby("分组列" )["数值列" ].transform("mean" ) df["组内最大值" ] = df.groupby("分组列" )["数值列" ].transform("max" ) df["组内排名依据" ] = df["数值列" ] / df["组内平均值" ]
记住一句话:想得到每组一个结果,用
agg;想把每组结果贴回原表,用 transform。
3.7 rank:排名
按总分排名:
1 df["rank_total" ] = df["total" ].rank(ascending=False )
默认排名遇到并列时会取平均名次。比如两个并列第 1,名次可能是
1.5。
更常用的是:
1 df["rank_total" ] = df["total" ].rank(ascending=False , method="min" )
method="min" 表示并列时取最小名次。例如两个并列第 1,都是第 1
名,下一个是第 3 名。
如果想要连续排名:
1 df["rank_dense" ] = df["total" ].rank(ascending=False , method="dense" )
dense 表示并列后不跳号。例如两个并列第 1,下一个是第 2 名。
常见排名方法:
1 2 3 4 5 method="average" method="min" method="max" method="dense" method="first"
3.8 组内排名
需求:每个班内部按总分排名。
1 2 3 4 df["class_rank" ] = df.groupby("class" )["total" ].rank( ascending=False , method="dense" )
如果想看每个班的排名结果:
1 df.sort_values(["class" , "class_rank" ])
筛选每个班第 1 名:
1 df[df["class_rank" ] == 1 ]
筛选每个班前 2 名:
1 df[df["class_rank" ] <= 2 ]
这是认证考试特别常见的题型:每个分组内取排名前 N
的记录 。
3.9 nlargest 和 nsmallest
找出总分最高的 3 个学生:
找出数学成绩最低的 2 个学生:
如果是每个班取总分最高的 1 人,可以这样写:
1 df.groupby("class" , group_keys=False ).apply(lambda x: x.nlargest(1 , "total" ))
每个班取总分最高的 2 人:
1 df.groupby("class" , group_keys=False ).apply(lambda x: x.nlargest(2 , "total" ))
这里的 group_keys=False 是为了避免结果出现多层索引。
不过考试中更推荐用 rank(),因为更容易解释:
1 2 df["class_rank" ] = df.groupby("class" )["total" ].rank(ascending=False , method="dense" ) df[df["class_rank" ] <= 2 ]
3.10 value_counts:频数统计
统计每个班级的人数:
1 df["class" ].value_counts()
统计每个城市的人数:
1 df["city" ].value_counts()
计算比例:
1 df["city" ].value_counts(normalize=True )
转换成百分比:
1 df["city" ].value_counts(normalize=True ) * 100
如果想变成 DataFrame:
1 df["city" ].value_counts().reset_index()
在较新版本 Pandas 中,结果列名通常是:
如果需要改列名:
1 2 city_count = df["city" ].value_counts().reset_index() city_count.columns = ["city" , "count" ]
3.11 pivot_table:透视表
透视表可以理解成 Pandas 里的 Excel 数据透视表。
需求:按班级和性别统计数学平均分。
1 2 3 4 5 6 7 pd.pivot_table( df, values="math" , index="class" , columns="gender" , aggfunc="mean" )
输出类似:
1 2 3 4 5 gender F Mclass 一班 92.0 80.5 三班 81.0 73.0 二班 77.5 95.0
多个统计指标:
1 2 3 4 5 6 7 pd.pivot_table( df, values=["math" , "english" ], index="class" , columns="gender" , aggfunc="mean" )
填充缺失值:
1 2 3 4 5 6 7 8 pd.pivot_table( df, values="math" , index="class" , columns="gender" , aggfunc="mean" , fill_value=0 )
添加汇总行和汇总列:
1 2 3 4 5 6 7 8 pd.pivot_table( df, values="total" , index="class" , columns="gender" , aggfunc="mean" , margins=True )
margins=True 会添加 All 行和列。
3.12 交叉表 crosstab
crosstab 主要用于统计两个分类变量的频数。
按班级和性别统计人数:
1 pd.crosstab(df["class" ], df["gender" ])
输出类似:
1 2 3 4 5 gender F Mclass 一班 1 2 三班 1 1 二班 2 1
加汇总:
1 pd.crosstab(df["class" ], df["gender" ], margins=True )
计算比例:
1 pd.crosstab(df["class" ], df["gender" ], normalize="index" )
normalize="index" 表示按行计算比例。
例如每个班中男女比例:
1 pd.crosstab(df["class" ], df["gender" ], normalize="index" )
3.13 cut:分箱统计
分箱就是把连续数值划分成区间。例如把成绩分成不及格、及格、良好、优秀。
1 2 3 4 5 6 df["math_level" ] = pd.cut( df["math" ], bins=[0 , 60 , 80 , 90 , 100 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False )
这里:
bins=[0, 60, 80, 90, 100] 表示区间边界。
labels 表示每个区间对应的标签。
right=False 表示左闭右开,即 [0, 60)、[60, 80)、[80, 90)、[90,
100)。
如果希望 100 也包含进去,可以把最后边界写成 101:
1 2 3 4 5 6 df["math_level" ] = pd.cut( df["math" ], bins=[0 , 60 , 80 , 90 , 101 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False )
统计每个等级的人数:
1 df["math_level" ].value_counts()
按班级和等级统计人数:
1 pd.crosstab(df["class" ], df["math_level" ])
3.14 qcut:按分位数分箱
cut 是按照固定区间分箱。qcut 是按照样本数量尽量均匀分箱。
例如把总分分成 4 组:
1 2 3 4 5 df["total_group" ] = pd.qcut( df["total" ], q=4 , labels=["低" , "中低" , "中高" , "高" ] )
这会尽量保证每组人数差不多。
如果数据中重复值太多,可能会报错。可以加:
1 2 3 4 5 6 df["total_group" ] = pd.qcut( df["total" ], q=4 , labels=["低" , "中低" , "中高" , "高" ], duplicates="drop" )
3.15 完整综合示例:班级成绩分析
需求:
新增总分和平均分
按班级统计人数、数学平均分、英语平均分、总分平均分
找出每个班总分第一名
给每个学生添加班级内排名
统计每个班数学等级人数
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import pandas as pd df = pd.DataFrame({ "name" : ["张三" , "李四" , "王五" , "赵六" , "钱七" , "孙八" , "周九" , "吴十" ], "class" : ["一班" , "一班" , "一班" , "二班" , "二班" , "二班" , "三班" , "三班" ], "gender" : ["M" , "F" , "M" , "F" , "M" , "F" , "M" , "F" ], "math" : [85 , 92 , 76 , 88 , 95 , 67 , 73 , 81 ], "english" : [80 , 89 , 90 , 75 , 85 , 70 , 88 , 92 ], "city" : ["北京" , "上海" , "北京" , "广州" , "广州" , "北京" , "上海" , "广州" ] }) df["total" ] = df["math" ] + df["english" ] df["avg" ] = df["total" ] / 2 class_summary = df.groupby("class" ).agg( student_count=("name" , "count" ), math_avg=("math" , "mean" ), english_avg=("english" , "mean" ), total_avg=("total" , "mean" ) ).reset_index() df["class_rank" ] = df.groupby("class" )["total" ].rank( ascending=False , method="dense" ) top_students = df[df["class_rank" ] == 1 ].sort_values("class" ) df["math_level" ] = pd.cut( df["math" ], bins=[0 , 60 , 80 , 90 , 101 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False ) level_count = pd.crosstab(df["class" ], df["math_level" ])print ("班级统计:" )print (class_summary)print ("每班第一名:" )print (top_students[["class" , "name" , "total" , "class_rank" ]])print ("数学等级人数:" )print (level_count)
这个综合题覆盖了
groupby、agg、rank、cut、crosstab,非常适合作为期末复习模板。
3.16 高频易错点
第一,分组后如果想恢复普通表格,记得:
第二,count() 不统计缺失值,size() 统计行数。
1 2 df.groupby("class" )["name" ].count() df.groupby("class" ).size()
第三,agg() 多指标统计时,推荐使用命名聚合。
1 2 3 4 df.groupby("class" ).agg( math_avg=("math" , "mean" ), count=("name" , "count" ) ).reset_index()
第四,组内排名要先 groupby,再 rank。
1 df["rank" ] = df.groupby("class" )["total" ].rank(ascending=False )
第五,transform() 不会减少行数,适合把分组统计结果加回原表。
1 df["class_avg" ] = df.groupby("class" )["math" ].transform("mean" )
第六,pivot_table 适合做二维统计表,crosstab
适合统计分类变量频数。
1 2 3 pd.pivot_table(df, values="math" , index="class" , columns="gender" , aggfunc="mean" ) pd.crosstab(df["class" ], df["gender" ])
3.17 本章练习题
使用下面的数据完成练习:
1 2 3 4 5 6 7 8 9 10 11 import pandas as pd df = pd.DataFrame({ "student" : ["Alice" , "Bob" , "Cindy" , "David" , "Eva" , "Frank" , "Grace" , "Helen" ], "class" : ["A" , "A" , "A" , "B" , "B" , "B" , "C" , "C" ], "gender" : ["F" , "M" , "F" , "M" , "F" , "M" , "F" , "F" ], "python" : [90 , 75 , 88 , 60 , 95 , 70 , 82 , 91 ], "sql" : [85 , 80 , 92 , 70 , 89 , 76 , 88 , 95 ] }) df["total" ] = df["python" ] + df["sql" ]
练习 1:按班级统计 Python 平均分。
1 df.groupby("class" )["python" ].mean()
练习 2:按班级统计人数。
1 df.groupby("class" ).size()
练习 3:按班级统计 Python 平均分、最高分、最低分。
1 df.groupby("class" )["python" ].agg(["mean" , "max" , "min" ])
练习 4:用命名聚合统计每个班的人数、SQL 平均分、总分平均分。
1 2 3 4 5 summary = df.groupby("class" ).agg( student_count=("student" , "count" ), sql_avg=("sql" , "mean" ), total_avg=("total" , "mean" ) ).reset_index()
练习 5:给每个学生新增一列
class_total_avg,表示所在班的总分平均值。
1 df["class_total_avg" ] = df.groupby("class" )["total" ].transform("mean" )
练习 6:筛选总分高于本班平均分的学生。
1 df[df["total" ] > df["class_total_avg" ]]
练习 7:给每个学生添加班级内总分排名。
1 2 3 4 df["class_rank" ] = df.groupby("class" )["total" ].rank( ascending=False , method="dense" )
练习 8:找出每个班总分第一名。
1 df[df["class_rank" ] == 1 ]
练习 9:用透视表统计不同班级、不同性别的 Python 平均分。
1 2 3 4 5 6 7 pd.pivot_table( df, values="python" , index="class" , columns="gender" , aggfunc="mean" )
练习 10:统计每个班不同性别的人数。
1 pd.crosstab(df["class" ], df["gender" ])
练习 11:把 Python 成绩分成“不及格、及格、良好、优秀”。
1 2 3 4 5 6 df["python_level" ] = pd.cut( df["python" ], bins=[0 , 60 , 80 , 90 , 101 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False )
练习 12:统计每个班不同 Python 等级的人数。
1 pd.crosstab(df["class" ], df["python_level" ])
本章必须掌握的代码模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 df.groupby("group_col" )["value_col" ].mean() df.groupby(["group_col1" , "group_col2" ])["value_col" ].sum ().reset_index() df.groupby("group_col" ).size() df.groupby("group_col" ).agg( count=("id_col" , "count" ), avg_value=("value_col" , "mean" ), max_value=("value_col" , "max" ), min_value=("value_col" , "min" ) ).reset_index() df["group_avg" ] = df.groupby("group_col" )["value_col" ].transform("mean" ) df["rank" ] = df["score" ].rank(ascending=False , method="dense" ) df["group_rank" ] = df.groupby("group_col" )["score" ].rank( ascending=False , method="dense" ) df[df["group_rank" ] <= N] pd.pivot_table( df, values="value_col" , index="row_group" , columns="col_group" , aggfunc="mean" , fill_value=0 ) pd.crosstab(df["group_col1" ], df["group_col2" ]) df["level" ] = pd.cut( df["score" ], bins=[0 , 60 , 80 , 90 , 101 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False )
第4章 多表合并与连接
本章是 Pandas
综合应用的高频内容。比如给出两张或多张表,例如学生信息表、成绩表、班级表、课程表,然后要求读者合并成一张完整表,再进行筛选、统计或分组分析。
本章重点掌握:
concat:纵向或横向拼接
merge:按键合并,类似 SQL 的 join
join:按索引合并
一对一、一对多、多对多合并
inner、left、right、outer 的区别
多表合并后的缺失值、重复列、数据膨胀问题
先准备几张示例表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import pandas as pd students = pd.DataFrame({ "student_id" : [1 , 2 , 3 , 4 , 5 ], "name" : ["张三" , "李四" , "王五" , "赵六" , "钱七" ], "class_id" : ["C1" , "C1" , "C2" , "C2" , "C3" ] }) scores = pd.DataFrame({ "student_id" : [1 , 2 , 3 , 4 , 6 ], "math" : [85 , 92 , 78 , 90 , 88 ], "english" : [88 , 81 , 95 , 76 , 84 ] }) classes = pd.DataFrame({ "class_id" : ["C1" , "C2" , "C3" ], "class_name" : ["一班" , "二班" , "三班" ], "teacher" : ["王老师" , "李老师" , "陈老师" ] })print (students)print (scores)print (classes)
三张表的含义是:
students 是学生信息表。scores 是成绩表。classes 是班级信息表。
注意一个细节:scores 表中有 student_id = 6,但这个学生不在 students
表里;students 表中有 student_id =
5,但他没有成绩。这是专门设计的,用来理解不同连接方式的区别。
4.1 concat:拼接数据
concat 主要用于“把结构相同或相似的表拼起来”。
4.1.1 纵向拼接:增加行
假设有两个班的成绩表:
1 2 3 4 5 6 7 8 9 df1 = pd.DataFrame({ "name" : ["张三" , "李四" ], "math" : [85 , 92 ] }) df2 = pd.DataFrame({ "name" : ["王五" , "赵六" ], "math" : [78 , 90 ] })
纵向拼接:
1 2 3 result = pd.concat([df1, df2])print (result)
输出:
1 2 3 4 5 name math0 张三 85 1 李四 92 0 王五 78 1 赵六 90
你会发现索引重复了。如果希望重新编号,需要加:
1 result = pd.concat([df1, df2], ignore_index=True )
输出:
1 2 3 4 5 name math0 张三 85 1 李四 92 2 王五 78 3 赵六 90
考试中最常用的写法是:
1 pd.concat([df1, df2], ignore_index=True )
4.1.2 横向拼接:增加列
1 2 3 4 5 6 7 8 9 10 11 12 13 df_info = pd.DataFrame({ "name" : ["张三" , "李四" , "王五" ], "class" : ["一班" , "一班" , "二班" ] }) df_score = pd.DataFrame({ "math" : [85 , 92 , 78 ], "english" : [88 , 81 , 95 ] }) result = pd.concat([df_info, df_score], axis=1 )print (result)
输出:
1 2 3 4 name class math english 0 张三 一班 85 88 1 李四 一班 92 81 2 王五 二班 78 95
axis=0 表示按行拼接,这是默认值。axis=1 表示按列拼接。
1 2 pd.concat([df1, df2], axis=0 ) pd.concat([df1, df2], axis=1 )
4.1.3 concat 遇到列不完全相同
1 2 3 4 5 6 7 8 9 10 11 12 13 df1 = pd.DataFrame({ "name" : ["张三" , "李四" ], "math" : [85 , 92 ] }) df2 = pd.DataFrame({ "name" : ["王五" , "赵六" ], "english" : [95 , 76 ] }) result = pd.concat([df1, df2], ignore_index=True )print (result)
输出:
1 2 3 4 5 name math english0 张三 85 .0 NaN1 李四 92 .0 NaN2 王五 NaN 95 .0 3 赵六 NaN 76 .0
默认会保留所有列,缺失的位置填 NaN。
如果只想保留共有列,可以写:
1 result = pd.concat([df1, df2], join="inner" , ignore_index=True )
此时只保留两张表共有的列。
4.2 merge:按键合并
merge 是多表操作的核心,类似 SQL 里的 JOIN。
基本语法:
1 pd.merge(left, right, on="key" , how="inner" )
含义是:
先看学生表和成绩表:
1 2 3 4 5 6 student_id name class_id0 1 张三 C11 2 李四 C12 3 王五 C23 4 赵六 C24 5 钱七 C3
1 2 3 4 5 6 student_id math english0 1 85 88 1 2 92 81 2 3 78 95 3 4 90 76 4 6 88 84
4.3 inner
join:只保留两边都能匹配的数据
1 2 3 result = pd.merge(students, scores, on="student_id" , how="inner" )print (result)
输出:
1 2 3 4 5 student_id name class_id math english0 1 张三 C1 85 88 1 2 李四 C1 92 81 2 3 王五 C2 78 95 3 4 赵六 C2 90 76
解释:
students 中的 student_id = 5 没有成绩,所以被丢掉。scores 中的
student_id = 6 没有学生信息,所以也被丢掉。
inner 只保留左右两边都存在的键。
适用场景:只关心完整匹配的数据。
4.4 left join:保留左表全部数据
1 2 3 result = pd.merge(students, scores, on="student_id" , how="left" )print (result)
输出:
1 2 3 4 5 6 student_id name class_id math english0 1 张三 C1 85 .0 88 .0 1 2 李四 C1 92 .0 81 .0 2 3 王五 C2 78 .0 95 .0 3 4 赵六 C2 90 .0 76 .0 4 5 钱七 C3 NaN NaN
解释:
左表 students 的所有学生都保留下来。钱七 没有成绩,所以 math 和
english 是缺失值。
这是实际项目和考试中最常用的连接方式。
适用场景:以主表为准,补充其他表的信息。
例如:
1 pd.merge(学生信息表, 成绩表, on="student_id" , how="left" )
表示保留所有学生,再把成绩补上。
4.5 right
join:保留右表全部数据
1 2 3 result = pd.merge(students, scores, on="student_id" , how="right" )print (result)
输出:
1 2 3 4 5 6 student_id name class_id math english0 1 张三 C1 85 88 1 2 李四 C1 92 81 2 3 王五 C2 78 95 3 4 赵六 C2 90 76 4 6 NaN NaN 88 84
解释:
右表 scores 的所有成绩记录都保留下来。student_id = 6
在学生表中没有对应信息,所以 name 和 class_id 是缺失值。
right join 没有 left join 常用,因为大多数情况下可以交换左右表,用
left join 表达同样的逻辑。
4.6 outer
join:保留两边全部数据
1 2 3 result = pd.merge(students, scores, on="student_id" , how="outer" )print (result)
输出:
1 2 3 4 5 6 7 student_id name class_id math english0 1 张三 C1 85 .0 88 .0 1 2 李四 C1 92 .0 81 .0 2 3 王五 C2 78 .0 95 .0 3 4 赵六 C2 90 .0 76 .0 4 5 钱七 C3 NaN NaN5 6 NaN NaN 88 .0 84 .0
解释:
左表有但右表没有的保留。 右表有但左表没有的也保留。 匹配不上的位置填
NaN。
适用场景:想检查两张表有哪些数据无法匹配。
4.7 indicator:查看数据来自哪里
合并时可以加 indicator=True,生成 _merge
列,用来判断每一行的来源。
1 2 3 4 5 6 7 8 9 result = pd.merge( students, scores, on="student_id" , how="outer" , indicator=True )print (result)
输出:
1 2 3 4 5 6 7 student_id name class_id math english _merge0 1 张三 C1 85 .0 88 .0 both1 2 李四 C1 92 .0 81 .0 both2 3 王五 C2 78 .0 95 .0 both3 4 赵六 C2 90 .0 76 .0 both4 5 钱七 C3 NaN NaN left_only5 6 NaN NaN 88 .0 84 .0 right_only
含义:
both 表示左右表都存在。left_only 表示只在左表存在。right_only
表示只在右表存在。
找出没有成绩的学生:
1 result[result["_merge" ] == "left_only" ]
找出有成绩但没有学生信息的记录:
1 result[result["_merge" ] == "right_only" ]
这个技巧非常适合检查数据质量。
4.8 左右表连接列名不同怎么办
有时候两张表的连接字段名字不同。
1 2 3 4 5 6 7 8 9 students = pd.DataFrame({ "student_id" : [1 , 2 , 3 ], "name" : ["张三" , "李四" , "王五" ] }) scores = pd.DataFrame({ "sid" : [1 , 2 , 3 ], "math" : [85 , 92 , 78 ] })
左表叫 student_id,右表叫 sid。
这时不能用 on,要用 left_on 和 right_on:
1 2 3 4 5 6 7 8 9 result = pd.merge( students, scores, left_on="student_id" , right_on="sid" , how="left" )print (result)
输出:
1 2 3 4 student_id name sid math0 1 张三 1 85 1 2 李四 2 92 2 3 王五 3 78
如果不想保留右表的 sid 列,可以删除:
1 result = result.drop(columns=["sid" ])
4.9 按多个键合并
有时候只按一个字段合并不够。例如同一个学生可能有多门课程成绩。
1 2 3 4 5 6 7 8 9 10 11 score1 = pd.DataFrame({ "student_id" : [1 , 1 , 2 , 2 ], "course" : ["math" , "english" , "math" , "english" ], "score" : [85 , 88 , 92 , 81 ] }) score2 = pd.DataFrame({ "student_id" : [1 , 1 , 2 , 2 ], "course" : ["math" , "english" , "math" , "english" ], "rank" : [2 , 1 , 1 , 3 ] })
需要同时按 student_id 和 course 合并:
1 2 3 4 5 6 7 8 result = pd.merge( score1, score2, on=["student_id" , "course" ], how="left" )print (result)
输出:
1 2 3 4 5 student_id course score rank0 1 math 85 2 1 1 english 88 1 2 2 math 92 1 3 2 english 81 3
多个键合并的模板:
1 pd.merge(df1, df2, on=["key1" , "key2" ], how="left" )
4.10 处理重复列名:suffixes
如果左右表有同名列,但这个列不是连接键,Pandas 会自动加后缀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 df1 = pd.DataFrame({ "student_id" : [1 , 2 ], "name" : ["张三" , "李四" ], "score" : [85 , 92 ] }) df2 = pd.DataFrame({ "student_id" : [1 , 2 ], "score" : [88 , 81 ] }) result = pd.merge(df1, df2, on="student_id" , how="left" )print (result)
输出:
1 2 3 student_id name score_x score_y0 1 张三 85 88 1 2 李四 92 81
默认后缀是 _x 和 _y,但这不够清晰。推荐自定义:
1 2 3 4 5 6 7 8 9 result = pd.merge( df1, df2, on="student_id" , how="left" , suffixes=("_math" , "_english" ) )print (result)
输出:
1 2 3 student_id name score_math score_english0 1 张三 85 88 1 2 李四 92 81
考试中如果看到 score_x、score_y,说明合并前两张表有重复列名。
4.11 一对一、一对多、多对多合并
4.11.1 一对一合并
学生信息表:
1 2 3 4 students = pd.DataFrame({ "student_id" : [1 , 2 , 3 ], "name" : ["张三" , "李四" , "王五" ] })
成绩表:
1 2 3 4 scores = pd.DataFrame({ "student_id" : [1 , 2 , 3 ], "math" : [85 , 92 , 78 ] })
每个 student_id 在两张表中都只出现一次,这是一对一合并。
1 pd.merge(students, scores, on="student_id" , how="left" )
4.11.2 一对多合并
学生表:
1 2 3 4 students = pd.DataFrame({ "student_id" : [1 , 2 ], "name" : ["张三" , "李四" ] })
成绩表:
1 2 3 4 5 scores = pd.DataFrame({ "student_id" : [1 , 1 , 2 , 2 ], "course" : ["math" , "english" , "math" , "english" ], "score" : [85 , 88 , 92 , 81 ] })
一个学生有多门课成绩,这是典型的一对多。
1 2 3 result = pd.merge(students, scores, on="student_id" , how="left" )print (result)
输出:
1 2 3 4 5 student_id name course score0 1 张三 math 85 1 1 张三 english 88 2 2 李四 math 92 3 2 李四 english 81
一对多合并后,左表中的一行会被复制多次。
这是正常现象,不是错误。
4.11.3 多对多合并:最危险
1 2 3 4 5 6 7 8 9 df1 = pd.DataFrame({ "key" : ["A" , "A" , "B" ], "value1" : [1 , 2 , 3 ] }) df2 = pd.DataFrame({ "key" : ["A" , "A" , "B" ], "value2" : [10 , 20 , 30 ] })
合并:
1 2 3 result = pd.merge(df1, df2, on="key" , how="inner" )print (result)
输出:
1 2 3 4 5 6 key value1 value20 A 1 10 1 A 1 20 2 A 2 10 3 A 2 20 4 B 3 30
因为 A 在左表出现 2 次,在右表也出现 2 次,合并后会得到 2 × 2 = 4
行。
这叫笛卡尔式扩张,是多表合并里最容易导致结果错误的地方。
合并前可以检查键是否重复:
1 2 df1["key" ].duplicated().sum () df2["key" ].duplicated().sum ()
查看重复键:
1 2 df1[df1["key" ].duplicated(keep=False )] df2[df2["key" ].duplicated(keep=False )]
如果本来应该是一对一合并,但发现键重复,就要先去重或重新选择连接键。
4.12 validate:检查连接关系
Pandas 的 merge 提供了 validate 参数,用来检查合并关系。
1 2 3 4 5 6 7 pd.merge( students, scores, on="student_id" , how="left" , validate="one_to_one" )
常用选项:
1 2 3 4 validate="one_to_one" validate="one_to_many" validate="many_to_one" validate="many_to_many"
如果你指定 one_to_one,但实际数据中存在重复键,Pandas
会直接报错。
例如:
1 pd.merge(df1, df2, on="key" , validate="one_to_one" )
如果 key 重复,就会报错。
这是防止合并后行数异常膨胀的好办法。
认证考试中不一定要求写
validate,但理解它可以帮助你判断合并逻辑是否正确。
4.13 join:按索引合并
join 主要用于按索引合并。
1 2 3 4 5 6 7 8 9 students = pd.DataFrame({ "name" : ["张三" , "李四" , "王五" ], "class" : ["一班" , "一班" , "二班" ] }, index=[1 , 2 , 3 ]) scores = pd.DataFrame({ "math" : [85 , 92 , 78 ], "english" : [88 , 81 , 95 ] }, index=[1 , 2 , 3 ])
按索引合并:
1 2 3 result = students.join(scores)print (result)
输出:
1 2 3 4 name class math english 1 张三 一班 85 88 2 李四 一班 92 81 3 王五 二班 78 95
join 默认是左连接。
1 students.join(scores, how="left" )
如果你不熟悉索引合并,考试中优先使用 merge,因为它更直观。
把索引变成普通列后再 merge:
1 2 3 4 students_reset = students.reset_index().rename(columns={"index" : "student_id" }) scores_reset = scores.reset_index().rename(columns={"index" : "student_id" }) result = pd.merge(students_reset, scores_reset, on="student_id" , how="left" )
4.14 set_index 和 reset_index
多表合并中经常需要处理索引。
设置某一列为索引:
1 df = df.set_index("student_id" )
把索引恢复成普通列:
示例:
1 2 3 4 5 6 students_indexed = students.set_index("student_id" ) scores_indexed = scores.set_index("student_id" ) result = students_indexed.join(scores_indexed, how="left" )print (result)
如果想再把 student_id 变回普通列:
1 result = result.reset_index()
4.15 完整多表合并示例
需求:
以学生信息表为主表
合并成绩表
合并班级表
计算总分和平均分
找出没有成绩的学生
按班级统计平均总分
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import pandas as pd students = pd.DataFrame({ "student_id" : [1 , 2 , 3 , 4 , 5 ], "name" : ["张三" , "李四" , "王五" , "赵六" , "钱七" ], "class_id" : ["C1" , "C1" , "C2" , "C2" , "C3" ] }) scores = pd.DataFrame({ "student_id" : [1 , 2 , 3 , 4 , 6 ], "math" : [85 , 92 , 78 , 90 , 88 ], "english" : [88 , 81 , 95 , 76 , 84 ] }) classes = pd.DataFrame({ "class_id" : ["C1" , "C2" , "C3" ], "class_name" : ["一班" , "二班" , "三班" ], "teacher" : ["王老师" , "李老师" , "陈老师" ] }) df = pd.merge( students, scores, on="student_id" , how="left" ) df = pd.merge( df, classes, on="class_id" , how="left" ) df["total" ] = df["math" ] + df["english" ] df["avg" ] = df["total" ] / 2 no_score = df[df["math" ].isna() | df["english" ].isna()] class_summary = df.groupby("class_name" ).agg( student_count=("student_id" , "count" ), total_avg=("total" , "mean" ), math_avg=("math" , "mean" ), english_avg=("english" , "mean" ) ).reset_index()print ("合并后的总表:" )print (df)print ("没有成绩的学生:" )print (no_score[["student_id" , "name" , "class_name" ]])print ("班级成绩统计:" )print (class_summary)
输出大致如下:
1 2 3 4 5 6 7 合并后的总表: student_id name class_id math english class_name teacher total avg 0 1 张三 C1 85.0 88.0 一班 王老师 173.0 86.5 1 2 李四 C1 92.0 81.0 一班 王老师 173.0 86.5 2 3 王五 C2 78.0 95.0 二班 李老师 173.0 86.5 3 4 赵六 C2 90.0 76.0 二班 李老师 166.0 83.0 4 5 钱七 C3 NaN NaN 三班 陈老师 NaN NaN
注意:钱七 没有成绩,所以他的 total 和 avg 也是缺失值。
groupby().mean() 默认会跳过缺失值,所以三班的平均总分可能是
NaN,因为三班没有任何有效成绩。
4.16 合并后检查数据质量
多表合并后,不要马上做统计,先做检查。
4.16.1 检查行数
1 2 3 print (students.shape)print (scores.shape)print (df.shape)
如果你期望一对一合并,但合并后行数突然变多,通常说明连接键重复。
4.16.2 检查缺失值
4.16.3 检查重复键
1 2 students["student_id" ].duplicated().sum () scores["student_id" ].duplicated().sum ()
4.16.4 检查哪些数据没有匹配上
1 2 3 4 5 6 7 8 9 check = pd.merge( students, scores, on="student_id" , how="outer" , indicator=True ) check["_merge" ].value_counts()
查看未匹配记录:
1 check[check["_merge" ] != "both" ]
4.16.5 检查合并后的重复行
根据关键字段检查重复:
1 df.duplicated(subset=["student_id" ]).sum ()
4.17 常见业务题型
题型 1:以主表为准补充信息
题目:学生信息表和成绩表合并,要求保留所有学生。
答案:
1 df = pd.merge(students, scores, on="student_id" , how="left" )
关键词是:保留所有学生 。 所以用
left,并且学生表放左边。
题型 2:只保留有成绩的学生
1 df = pd.merge(students, scores, on="student_id" , how="inner" )
关键词是:只保留两张表都匹配的数据 。 所以用
inner。
题型 3:找出没有成绩的学生
1 2 3 df = pd.merge(students, scores, on="student_id" , how="left" ) no_score = df[df["math" ].isna()]
或者用 indicator:
1 2 3 check = pd.merge(students, scores, on="student_id" , how="left" , indicator=True ) no_score = check[check["_merge" ] == "left_only" ]
题型
4:找出成绩表中没有学生信息的记录
1 2 3 check = pd.merge(students, scores, on="student_id" , how="outer" , indicator=True ) unknown_score = check[check["_merge" ] == "right_only" ]
题型 5:多表合并后统计
1 2 3 4 5 6 df = pd.merge(students, scores, on="student_id" , how="left" ) df = pd.merge(df, classes, on="class_id" , how="left" ) df["total" ] = df["math" ] + df["english" ] summary = df.groupby("class_name" )["total" ].mean().reset_index()
4.18 高频易错点
第一,concat 是拼接,merge 是按键合并。
1 2 pd.concat([df1, df2]) pd.merge(df1, df2, on="id" )
第二,纵向拼接通常要加:
第三,left join 保留左表全部数据,所以主表要放左边。
1 pd.merge(主表, 明细表, on="key" , how="left" )
第四,连接字段名相同用 on,字段名不同用 left_on 和 right_on。
1 2 3 pd.merge(df1, df2, on="id" ) pd.merge(df1, df2, left_on="id" , right_on="student_id" )
第五,多键合并要传入列表。
1 pd.merge(df1, df2, on=["student_id" , "course" ])
第六,合并后出现 _x、_y,说明左右表有重复列名。
1 suffixes=("_left" , "_right" )
第七,合并后行数变多,不一定是错,但要检查是不是一对多或多对多。
1 df["key" ].duplicated().sum ()
第八,想检查哪些行没有匹配上,用:
第九,索引合并用 join,普通列合并优先用 merge。
第十,合并后一定检查:
1 2 3 df.shape df.isna().sum () df.duplicated().sum ()
4.19 本章练习题
使用下面的数据完成练习:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import pandas as pd students = pd.DataFrame({ "sid" : [101 , 102 , 103 , 104 ], "name" : ["Alice" , "Bob" , "Cindy" , "David" ], "class_id" : ["A" , "A" , "B" , "C" ] }) scores = pd.DataFrame({ "sid" : [101 , 102 , 103 , 105 ], "python" : [90 , 75 , 88 , 95 ], "sql" : [85 , 80 , 92 , 89 ] }) classes = pd.DataFrame({ "class_id" : ["A" , "B" , "C" ], "class_name" : ["一班" , "二班" , "三班" ] })
练习 1:以学生表为主表,合并成绩表。
1 df = pd.merge(students, scores, on="sid" , how="left" )
练习 2:只保留有学生信息且有成绩的数据。
1 df_inner = pd.merge(students, scores, on="sid" , how="inner" )
练习 3:找出没有成绩的学生。
1 2 3 df = pd.merge(students, scores, on="sid" , how="left" ) no_score = df[df["python" ].isna()]
练习 4:找出成绩表中没有学生信息的记录。
1 2 3 check = pd.merge(students, scores, on="sid" , how="outer" , indicator=True ) unknown_score = check[check["_merge" ] == "right_only" ]
练习 5:合并班级信息。
1 2 df = pd.merge(students, scores, on="sid" , how="left" ) df = pd.merge(df, classes, on="class_id" , how="left" )
练习 6:新增总分列。
1 df["total" ] = df["python" ] + df["sql" ]
练习 7:按班级统计平均总分。
1 summary = df.groupby("class_name" )["total" ].mean().reset_index()
练习 8:检查合并后每列缺失值数量。
练习 9:检查 scores 表中 sid 是否重复。
1 scores["sid" ].duplicated().sum ()
练习 10:使用 indicator=True 查看每条数据的匹配来源。
1 2 3 4 5 6 7 8 9 check = pd.merge( students, scores, on="sid" , how="outer" , indicator=True )print (check)
本章必须掌握的代码模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import pandas as pd df = pd.concat([df1, df2], ignore_index=True ) df = pd.concat([df1, df2], axis=1 ) df = pd.merge(df1, df2, on="key" , how="inner" ) df = pd.merge(df1, df2, on="key" , how="left" ) df = pd.merge(df1, df2, on="key" , how="right" ) df = pd.merge(df1, df2, on="key" , how="outer" ) df = pd.merge( df1, df2, left_on="left_key" , right_on="right_key" , how="left" ) df = pd.merge( df1, df2, on=["key1" , "key2" ], how="left" ) df = pd.merge( df1, df2, on="key" , how="left" , suffixes=("_left" , "_right" ) ) df = pd.merge( df1, df2, on="key" , how="outer" , indicator=True ) df = df1.join(df2, how="left" ) df = df.set_index("key" ) df = df.reset_index() df["key" ].duplicated().sum () df.isna().sum () df.shape
第5章 综合实战与考点速查
本章是整套教程的收尾。前 4 章分别讲了:
第1章 Pandas入门与核心操作(基础操作、筛选、排序、新增列)
第2章
数据清洗与预处理(缺失值、重复值、类型转换、字符串和日期清洗)
第3章 统计分析与聚合(groupby、聚合、透视表、排名、分箱)
第4章 多表合并与连接(concat、merge、join 多表合并)
本章的目标是把这些内容串起来,完成一套接近认证综合题或开发生产场景下的
Pandas 分析流程。
本章要重点掌握这条主线:
1 2 3 4 5 6 7 8 9 读取数据 → 查看数据结构 → 清洗数据 → 合并多表 → 新增计算列 → 分组统计 → 排名筛选 → 透视分析 → 导出结果
5.1 综合题背景
假设现在有三张表。
第一张:学生信息表 students
1 2 3 4 5 6 7 8 9 10 import pandas as pdimport numpy as np students = pd.DataFrame({ "student_id" : [1001 , 1002 , 1003 , 1004 , 1005 , 1006 ], "name" : [" Alice " , "Bob" , "Cindy" , "David" , "Eva" , None ], "class_id" : ["C1" , "C1" , "C2" , "C2" , "C3" , "C3" ], "gender" : ["F" , "M" , "F" , "M" , "F" , "M" ], "age" : ["18" , "19" , "18" , "未知" , "20" , "19" ] })
第二张:成绩表 scores
1 2 3 4 5 6 scores = pd.DataFrame({ "student_id" : [1001 , 1002 , 1003 , 1004 , 1005 , 1007 ], "python" : ["90" , "75" , "bad" , "60" , "95" , "88" ], "sql" : [85 , 80 , 92 , np.nan, 89 , 76 ], "exam_date" : ["2024-06-01" , "2024/06/01" , "wrong" , "2024-06-02" , "2024-06-02" , "2024-06-03" ] })
第三张:班级信息表 classes
1 2 3 4 5 classes = pd.DataFrame({ "class_id" : ["C1" , "C2" , "C3" ], "class_name" : ["一班" , "二班" , "三班" ], "teacher" : ["王老师" , "李老师" , "陈老师" ] })
这三张表故意包含一些脏数据:
name 有空格和缺失值。age 中有 "未知"。python 中有 "bad"。sql
有缺失值。exam_date 中有无法转换的日期。scores 表里有 student_id =
1007,但学生表里没有这个学生。students 表里有 student_id =
1006,但成绩表里没有成绩。
这非常接近考试综合题的真实情况。
5.2 第一步:查看数据基本情况
拿到数据后,第一步永远不是直接分析,而是先查看。
1 2 3 print (students.head())print (scores.head())print (classes.head())
查看行列数:
1 2 3 print (students.shape)print (scores.shape)print (classes.shape)
查看字段类型和缺失值:
1 2 3 students.info() scores.info() classes.info()
查看缺失值数量:
1 2 3 print (students.isna().sum ())print (scores.isna().sum ())print (classes.isna().sum ())
查看重复值:
1 2 3 print (students.duplicated().sum ())print (scores.duplicated().sum ())print (classes.duplicated().sum ())
查看主键是否重复:
1 2 3 print (students["student_id" ].duplicated().sum ())print (scores["student_id" ].duplicated().sum ())print (classes["class_id" ].duplicated().sum ())
这一步在期末题中很重要。很多题目会问:
“请检查数据中是否存在缺失值。” “请检查学生编号是否重复。”
“请查看数据基本信息。”
对应代码基本就是:
1 2 3 4 df.info() df.isna().sum () df.duplicated().sum () df["id" ].duplicated().sum ()
5.3 第二步:清洗学生信息表
学生表 students 的问题主要有两个:
name 有空格和缺失值
age 是字符串,而且有 "未知"
清洗代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 students_clean = students.copy() students_clean["name" ] = students_clean["name" ].fillna("Unknown" ).str .strip() students_clean["age" ] = pd.to_numeric(students_clean["age" ], errors="coerce" ) students_clean["age" ] = students_clean["age" ].fillna(students_clean["age" ].median()) students_clean["age" ] = students_clean["age" ].astype(int )print (students_clean)
清洗后结果大致为:
1 2 3 4 5 6 7 student_id name class_id gender age0 1001 Alice C1 F 18 1 1002 Bob C1 M 19 2 1003 Cindy C2 F 18 3 1004 David C2 M 19 4 1005 Eva C3 F 20 5 1006 Unknown C3 M 19
这里的关键点:
用于清理字符串前后空格。
1 pd.to_numeric(..., errors="coerce" )
用于把脏字符串转成缺失值。
用于填充缺失值。
5.4 第三步:清洗成绩表
成绩表 scores 的问题主要有三个:
python 是字符串,并且包含 "bad"
sql 有缺失值
exam_date 有错误日期
清洗代码:
1 2 3 4 5 6 7 8 9 10 11 12 scores_clean = scores.copy() scores_clean["python" ] = pd.to_numeric(scores_clean["python" ], errors="coerce" ) scores_clean["sql" ] = pd.to_numeric(scores_clean["sql" ], errors="coerce" ) scores_clean["exam_date" ] = pd.to_datetime(scores_clean["exam_date" ], errors="coerce" )print (scores_clean)
此时 python 中的 "bad" 会变成 NaN,exam_date 中的 "wrong" 会变成
NaT。
对于成绩缺失值,常见处理方式有两种。
方式一:删除成绩缺失的记录。
1 scores_drop = scores_clean.dropna(subset=["python" , "sql" ])
方式二:用平均值或中位数填充。
1 2 scores_clean["python" ] = scores_clean["python" ].fillna(scores_clean["python" ].mean()) scores_clean["sql" ] = scores_clean["sql" ].fillna(scores_clean["sql" ].mean())
认证考试中,如果题目没有特殊说明,常见做法是:
数值型成绩缺失,用平均值或中位数填充。
关键字段缺失,例如学生编号缺失,通常删除。
这里我们采用平均值填充:
1 2 3 4 5 6 7 scores_clean["python" ] = scores_clean["python" ].fillna(scores_clean["python" ].mean()) scores_clean["sql" ] = scores_clean["sql" ].fillna(scores_clean["sql" ].mean()) scores_clean["exam_month" ] = scores_clean["exam_date" ].dt.monthprint (scores_clean)
5.5 第四步:多表合并
现在要把学生信息、成绩、班级信息合并到一张总表中。
考试题一般会说:
“以学生信息表为主表,合并成绩表和班级表。”
关键词是:以学生信息表为主表 。
所以使用 left join,并且 students_clean 放左边。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 df = pd.merge( students_clean, scores_clean, on="student_id" , how="left" ) df = pd.merge( df, classes, on="class_id" , how="left" )print (df)
合并后大致结果:
1 2 3 4 5 6 7 student_id name class_id gender age python sql exam_date exam_month class_name teacher0 1001 Alice C1 F 18 90 .0 85 .0 2024 -06 -01 6 .0 一班 王老师1 1002 Bob C1 M 19 75 .0 80 .0 2024 -06 -01 6 .0 一班 王老师2 1003 Cindy C2 F 18 81 .6 92 .0 NaT NaN 二班 李老师3 1004 David C2 M 19 60 .0 84 .4 2024 -06 -02 6 .0 二班 李老师4 1005 Eva C3 F 20 95 .0 89 .0 2024 -06 -02 6 .0 三班 陈老师5 1006 Unknown C3 M 19 NaN NaN NaT NaN 三班 陈老师
注意:student_id = 1006 没有成绩,所以成绩相关字段是缺失值。
合并后必须检查:
1 2 3 print (df.shape)print (df.isna().sum ())print (df.duplicated().sum ())
5.6 第五步:找出未匹配数据
多表合并题经常会问:
“找出没有成绩的学生。” “找出成绩表中没有学生信息的记录。”
找出没有成绩的学生:
1 2 3 no_score_students = df[df["python" ].isna() | df["sql" ].isna()]print (no_score_students[["student_id" , "name" , "class_name" ]])
找出成绩表中没有学生信息的记录,需要用 outer join + indicator:
1 2 3 4 5 6 7 8 9 check = pd.merge( students_clean, scores_clean, on="student_id" , how="outer" , indicator=True )print (check["_merge" ].value_counts())
查看只存在于成绩表中的记录:
1 2 3 unknown_scores = check[check["_merge" ] == "right_only" ]print (unknown_scores)
这里可以找到 student_id = 1007。
考试中非常推荐掌握这个模板:
1 2 3 4 5 check = pd.merge(df1, df2, on="key" , how="outer" , indicator=True ) check[check["_merge" ] == "left_only" ] check[check["_merge" ] == "right_only" ] check[check["_merge" ] == "both" ]
5.7 第六步:新增分析字段
合并后,我们可以新增总分、平均分、是否及格、成绩等级等字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 df["total" ] = df["python" ] + df["sql" ] df["avg" ] = df["total" ] / 2 df["is_pass" ] = np.where( (df["python" ] >= 60 ) & (df["sql" ] >= 60 ), "及格" , "不及格" ) df["level" ] = pd.cut( df["avg" ], bins=[0 , 60 , 80 , 90 , 101 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False )print (df[["student_id" , "name" , "class_name" , "python" , "sql" , "total" , "avg" , "is_pass" , "level" ]])
这里的核心考点有三个。
第一个,新增列:
1 df["total" ] = df["python" ] + df["sql" ]
第二个,多条件判断:
1 (df["python" ] >= 60 ) & (df["sql" ] >= 60 )
第三个,分箱:
5.8 第七步:班级统计分析
现在按班级统计:
学生人数
Python 平均分
SQL 平均分
总分平均分
最高总分
及格人数
1 2 3 4 5 6 7 8 9 10 class_summary = df.groupby("class_name" ).agg( student_count=("student_id" , "count" ), python_avg=("python" , "mean" ), sql_avg=("sql" , "mean" ), total_avg=("total" , "mean" ), total_max=("total" , "max" ), pass_count=("is_pass" , lambda x: (x == "及格" ).sum ()) ).reset_index()print (class_summary)
输出大致如下:
1 2 3 4 class_name student_count python_avg sql_avg total_avg total_max pass_count0 一班 2 82 .5 82 .5 165 .0 175 .0 2 1 三班 2 95 .0 89 .0 184 .0 184 .0 1 2 二班 2 70 .8 88 .2 159 .0 173 .6 2
注意:三班有一个学生没有成绩,mean()
默认会跳过缺失值,所以三班的平均分只根据有效成绩计算。
如果你希望统计有成绩的人数,可以单独写:
1 2 3 4 5 class_summary = df.groupby("class_name" ).agg( student_count=("student_id" , "count" ), score_count=("total" , "count" ), total_avg=("total" , "mean" ) ).reset_index()
其中:
1 student_count=("student_id" , "count" )
统计学生数量。
1 score_count=("total" , "count" )
统计有成绩的人数,因为 count() 不统计缺失值。
5.9 第八步:排名分析
5.9.1 全体学生总分排名
1 2 3 4 df["total_rank" ] = df["total" ].rank( ascending=False , method="dense" )
查看总分排名:
1 2 3 rank_result = df.sort_values("total_rank" )print (rank_result[["student_id" , "name" , "class_name" , "total" , "total_rank" ]])
5.9.2 每个班内部排名
1 2 3 4 df["class_rank" ] = df.groupby("class_name" )["total" ].rank( ascending=False , method="dense" )
查看每班排名:
1 2 3 4 5 print ( df.sort_values(["class_name" , "class_rank" ])[ ["student_id" , "name" , "class_name" , "total" , "class_rank" ] ] )
5.9.3 找出每个班第一名
1 2 3 top1_each_class = df[df["class_rank" ] == 1 ]print (top1_each_class[["student_id" , "name" , "class_name" , "total" , "class_rank" ]])
5.9.4 找出每个班前 2 名
1 2 3 top2_each_class = df[df["class_rank" ] <= 2 ]print (top2_each_class[["student_id" , "name" , "class_name" , "total" , "class_rank" ]])
这是期末常见题:
“找出每个班总分最高的学生。” “找出每个地区销售额前 3 的商品。”
“找出每个部门工资最高的员工。”
统一模板:
1 2 3 4 5 6 df["rank" ] = df.groupby("分组列" )["排序列" ].rank( ascending=False , method="dense" ) result = df[df["rank" ] <= N]
5.10 第九步:透视表和交叉表
5.10.1 按班级和性别统计平均总分
1 2 3 4 5 6 7 8 9 10 pivot_avg = pd.pivot_table( df, values="total" , index="class_name" , columns="gender" , aggfunc="mean" , fill_value=0 )print (pivot_avg)
这可以回答:
“不同班级、不同性别的平均总分是多少?”
5.10.2 按班级和等级统计人数
1 2 3 level_count = pd.crosstab(df["class_name" ], df["level" ])print (level_count)
这可以回答:
“每个班各成绩等级的人数是多少?”
5.10.3 加汇总行和汇总列
1 2 3 4 5 6 7 level_count_all = pd.crosstab( df["class_name" ], df["level" ], margins=True )print (level_count_all)
margins=True 会添加总计。
5.11 第十步:导出分析结果
期末项目或作业中,经常要求导出结果。
导出总表:
1 df.to_csv("student_analysis.csv" , index=False , encoding="utf-8-sig" )
导出班级统计表:
1 class_summary.to_csv("class_summary.csv" , index=False , encoding="utf-8-sig" )
导出每班第一名:
1 top1_each_class.to_csv("top1_each_class.csv" , index=False , encoding="utf-8-sig" )
如果导出 Excel,可以写:
1 2 3 4 5 with pd.ExcelWriter("analysis_result.xlsx" ) as writer: df.to_excel(writer, sheet_name="总表" , index=False ) class_summary.to_excel(writer, sheet_name="班级统计" , index=False ) top1_each_class.to_excel(writer, sheet_name="每班第一名" , index=False ) level_count.to_excel(writer, sheet_name="等级人数" )
注意:
CSV 常用:
这样用 Excel 打开中文不容易乱码。
5.12 完整综合代码
下面是一份完整可运行代码,适合作为期末综合题模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 import pandas as pdimport numpy as np students = pd.DataFrame({ "student_id" : [1001 , 1002 , 1003 , 1004 , 1005 , 1006 ], "name" : [" Alice " , "Bob" , "Cindy" , "David" , "Eva" , None ], "class_id" : ["C1" , "C1" , "C2" , "C2" , "C3" , "C3" ], "gender" : ["F" , "M" , "F" , "M" , "F" , "M" ], "age" : ["18" , "19" , "18" , "未知" , "20" , "19" ] }) scores = pd.DataFrame({ "student_id" : [1001 , 1002 , 1003 , 1004 , 1005 , 1007 ], "python" : ["90" , "75" , "bad" , "60" , "95" , "88" ], "sql" : [85 , 80 , 92 , np.nan, 89 , 76 ], "exam_date" : ["2024-06-01" , "2024/06/01" , "wrong" , "2024-06-02" , "2024-06-02" , "2024-06-03" ] }) classes = pd.DataFrame({ "class_id" : ["C1" , "C2" , "C3" ], "class_name" : ["一班" , "二班" , "三班" ], "teacher" : ["王老师" , "李老师" , "陈老师" ] })print ("学生表缺失值:" )print (students.isna().sum ())print ("成绩表缺失值:" )print (scores.isna().sum ())print ("班级表缺失值:" )print (classes.isna().sum ())print ("学生编号是否重复:" )print (students["student_id" ].duplicated().sum ()) students_clean = students.copy() students_clean["name" ] = students_clean["name" ].fillna("Unknown" ).str .strip() students_clean["age" ] = pd.to_numeric( students_clean["age" ], errors="coerce" ) students_clean["age" ] = students_clean["age" ].fillna( students_clean["age" ].median() ) students_clean["age" ] = students_clean["age" ].astype(int ) scores_clean = scores.copy() scores_clean["python" ] = pd.to_numeric( scores_clean["python" ], errors="coerce" ) scores_clean["sql" ] = pd.to_numeric( scores_clean["sql" ], errors="coerce" ) scores_clean["exam_date" ] = pd.to_datetime( scores_clean["exam_date" ], errors="coerce" ) scores_clean["python" ] = scores_clean["python" ].fillna( scores_clean["python" ].mean() ) scores_clean["sql" ] = scores_clean["sql" ].fillna( scores_clean["sql" ].mean() ) scores_clean["exam_month" ] = scores_clean["exam_date" ].dt.month df = pd.merge( students_clean, scores_clean, on="student_id" , how="left" ) df = pd.merge( df, classes, on="class_id" , how="left" )print ("合并后的数据:" )print (df) check = pd.merge( students_clean, scores_clean, on="student_id" , how="outer" , indicator=True )print ("匹配情况:" )print (check["_merge" ].value_counts()) unknown_scores = check[check["_merge" ] == "right_only" ]print ("成绩表中没有学生信息的记录:" )print (unknown_scores) df["total" ] = df["python" ] + df["sql" ] df["avg" ] = df["total" ] / 2 df["is_pass" ] = np.where( (df["python" ] >= 60 ) & (df["sql" ] >= 60 ), "及格" , "不及格" ) df["level" ] = pd.cut( df["avg" ], bins=[0 , 60 , 80 , 90 , 101 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False ) class_summary = df.groupby("class_name" ).agg( student_count=("student_id" , "count" ), score_count=("total" , "count" ), python_avg=("python" , "mean" ), sql_avg=("sql" , "mean" ), total_avg=("total" , "mean" ), total_max=("total" , "max" ), pass_count=("is_pass" , lambda x: (x == "及格" ).sum ()) ).reset_index()print ("班级统计:" )print (class_summary) df["total_rank" ] = df["total" ].rank( ascending=False , method="dense" ) df["class_rank" ] = df.groupby("class_name" )["total" ].rank( ascending=False , method="dense" ) top1_each_class = df[df["class_rank" ] == 1 ]print ("每班第一名:" )print (top1_each_class[["student_id" , "name" , "class_name" , "total" , "class_rank" ]]) pivot_avg = pd.pivot_table( df, values="total" , index="class_name" , columns="gender" , aggfunc="mean" , fill_value=0 ) level_count = pd.crosstab( df["class_name" ], df["level" ] )print ("班级-性别平均总分:" )print (pivot_avg)print ("班级-等级人数:" )print (level_count) df.to_csv("student_analysis.csv" , index=False , encoding="utf-8-sig" ) class_summary.to_csv("class_summary.csv" , index=False , encoding="utf-8-sig" ) top1_each_class.to_csv("top1_each_class.csv" , index=False , encoding="utf-8-sig" )
这份代码几乎覆盖了 Pandas 认证考试最常见的综合题结构。
5.13 期末综合题答题模板
遇到 Pandas 综合题,可以按下面模板写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import pandas as pdimport numpy as np df = pd.read_csv("data.csv" ) df.head() df.info() df.describe() df.isna().sum () df.duplicated().sum () df = df.dropna(subset=["关键列" ]) df["数值列" ] = df["数值列" ].fillna(df["数值列" ].mean()) df["分类列" ] = df["分类列" ].fillna(df["分类列" ].mode()[0 ]) df["数值列" ] = pd.to_numeric(df["数值列" ], errors="coerce" ) df["日期列" ] = pd.to_datetime(df["日期列" ], errors="coerce" ) df["姓名列" ] = df["姓名列" ].str .strip() df["邮箱列" ] = df["邮箱列" ].str .lower() df = pd.merge(df1, df2, on="key" , how="left" ) df["total" ] = df["score1" ] + df["score2" ] df["avg" ] = df["total" ] / 2 result = df[(df["score1" ] >= 60 ) & (df["score2" ] >= 60 )] summary = df.groupby("group_col" ).agg( count=("id" , "count" ), avg_score=("score" , "mean" ), max_score=("score" , "max" ) ).reset_index() df["rank" ] = df.groupby("group_col" )["score" ].rank( ascending=False , method="dense" ) top_n = df[df["rank" ] <= 3 ] pivot = pd.pivot_table( df, values="score" , index="group_col" , columns="category_col" , aggfunc="mean" , fill_value=0 ) summary.to_csv("summary.csv" , index=False , encoding="utf-8-sig" )
5.14 Pandas 高频考点速查表
5.14.1 读取和保存
1 2 3 4 5 pd.read_csv("data.csv" ) pd.read_excel("data.xlsx" ) df.to_csv("result.csv" , index=False , encoding="utf-8-sig" ) df.to_excel("result.xlsx" , index=False )
5.14.2 查看数据
1 2 3 4 5 6 7 8 df.head() df.tail() df.shape df.columns df.index df.info() df.describe() df.dtypes
5.14.3 选择列
1 2 df["col" ] df[["col1" , "col2" ]]
5.14.4 选择行列
1 2 df.loc[行标签, 列标签] df.iloc[行位置, 列位置]
示例:
1 2 df.loc[0 :3 , ["name" , "score" ]] df.iloc[0 :3 , 0 :2 ]
5.14.5 条件筛选
1 2 3 4 5 6 7 df[df["score" ] > 80 ] df[(df["score" ] > 80 ) & (df["age" ] >= 18 )] df[(df["score" ] > 80 ) | (df["class" ] == "A" )] df[df["class" ].isin(["A" , "B" ])]
5.14.6 新增列
1 2 df["total" ] = df["math" ] + df["english" ] df["avg" ] = df["total" ] / 2
5.14.7 排序
1 2 3 4 5 df.sort_values("score" ) df.sort_values("score" , ascending=False ) df.sort_values(["class" , "score" ], ascending=[True , False ])
5.14.8 缺失值
1 2 3 4 5 6 7 8 9 df.isna().sum () df = df.dropna() df = df.dropna(subset=["关键列" ]) df["score" ] = df["score" ].fillna(df["score" ].mean()) df["city" ] = df["city" ].fillna(df["city" ].mode()[0 ])
5.14.9 重复值
1 2 3 4 5 df.duplicated().sum () df = df.drop_duplicates() df = df.drop_duplicates(subset=["id" ], keep="last" )
5.14.10 类型转换
1 2 3 4 5 df["age" ] = pd.to_numeric(df["age" ], errors="coerce" ) df["date" ] = pd.to_datetime(df["date" ], errors="coerce" ) df["class" ] = df["class" ].astype("category" )
5.14.11 字符串处理
1 2 3 4 5 6 7 df["name" ] = df["name" ].str .strip() df["email" ] = df["email" ].str .lower() df["phone" ] = df["phone" ].str .replace(r"[-.\s]" , "" , regex=True ) df[df["email" ].str .contains("qq" , case =False , na=False )]
5.14.12 日期处理
1 2 3 4 5 6 df["date" ] = pd.to_datetime(df["date" ], errors="coerce" ) df["year" ] = df["date" ].dt.year df["month" ] = df["date" ].dt.month df["day" ] = df["date" ].dt.day df["weekday" ] = df["date" ].dt.day_name()
5.14.13 分组统计
1 2 3 4 5 df.groupby("class" )["score" ].mean() df.groupby(["class" , "gender" ])["score" ].mean().reset_index() df.groupby("class" ).size()
5.14.14 多指标聚合
1 2 3 4 5 6 summary = df.groupby("class" ).agg( student_count=("student_id" , "count" ), score_avg=("score" , "mean" ), score_max=("score" , "max" ), score_min=("score" , "min" ) ).reset_index()
1 2 3 df["class_avg" ] = df.groupby("class" )["score" ].transform("mean" ) df[df["score" ] > df["class_avg" ]]
5.14.16 排名
1 2 3 4 5 6 7 8 9 df["rank" ] = df["score" ].rank( ascending=False , method="dense" ) df["class_rank" ] = df.groupby("class" )["score" ].rank( ascending=False , method="dense" )
5.14.17 每组 Top N
1 2 3 4 5 6 df["rank" ] = df.groupby("class" )["score" ].rank( ascending=False , method="dense" ) top3 = df[df["rank" ] <= 3 ]
5.14.18 透视表
1 2 3 4 5 6 7 8 pd.pivot_table( df, values="score" , index="class" , columns="gender" , aggfunc="mean" , fill_value=0 )
5.14.19 交叉表
1 2 3 pd.crosstab(df["class" ], df["gender" ]) pd.crosstab(df["class" ], df["level" ], margins=True )
5.14.20 分箱
1 2 3 4 5 6 df["level" ] = pd.cut( df["score" ], bins=[0 , 60 , 80 , 90 , 101 ], labels=["不及格" , "及格" , "良好" , "优秀" ], right=False )
5.14.21 合并表
1 2 3 4 5 6 7 pd.concat([df1, df2], ignore_index=True ) pd.merge(df1, df2, on="key" , how="left" ) pd.merge(df1, df2, left_on="id1" , right_on="id2" , how="left" ) pd.merge(df1, df2, on=["key1" , "key2" ], how="left" )
5.14.22 检查合并匹配情况
1 2 3 4 5 6 7 8 9 10 11 12 13 check = pd.merge( df1, df2, on="key" , how="outer" , indicator=True ) check["_merge" ].value_counts() check[check["_merge" ] == "left_only" ] check[check["_merge" ] == "right_only" ]
5.15 最容易丢分的 20 个点
多列选择忘记双层中括号。
多条件筛选忘记给每个条件加括号。
1 df[(df["score" ] > 80 ) & (df["age" ] >= 18 )]
用 and、or 连接 Series 条件。
错误:
1 df[(df["score" ] > 80 ) and (df["age" ] >= 18 )]
正确:
1 df[(df["score" ] > 80 ) & (df["age" ] >= 18 )]
判断缺失值写成:
正确:
dropna() 之后没有赋值。
错误:
正确:
sort_values() 之后没有赋值。
1 df = df.sort_values("score" , ascending=False )
导出 CSV 忘记 index=False。
1 df.to_csv("result.csv" , index=False )
中文 CSV 打开乱码,忘记编码。
1 df.to_csv("result.csv" , index=False , encoding="utf-8-sig" )
字符串处理忘记 .str。
日期处理忘记先转 datetime。
1 2 df["date" ] = pd.to_datetime(df["date" ], errors="coerce" ) df["year" ] = df["date" ].dt.year
loc 和 iloc 混用。
1 2 df.loc[0 :2 ] df.iloc[0 :2 ]
分组后忘记 reset_index()。
1 df.groupby("class" )["score" ].mean().reset_index()
不理解 count() 和 size() 的区别。
1 2 df.groupby("class" )["score" ].count() df.groupby("class" ).size()
合并主表放错位置。
1 pd.merge(主表, 明细表, on="key" , how="left" )
合并后不检查行数,导致多对多膨胀。
1 2 df.shape df["key" ].duplicated().sum ()
出现 _x、_y 不知道原因。
说明左右表有重复列名,可以用:
1 suffixes=("_left" , "_right" )
排名默认并列名次是平均值。
推荐:
分箱时忘记考虑 100 分是否被包含。
1 bins=[0 , 60 , 80 , 90 , 101 ]
用平均值填充前没把列转成数值。
1 2 df["score" ] = pd.to_numeric(df["score" ], errors="coerce" ) df["score" ] = df["score" ].fillna(df["score" ].mean())
没有区分 agg 和 transform。
1 2 3 4 5 df.groupby("class" )["score" ].mean() df["class_avg" ] = df.groupby("class" )["score" ].transform("mean" )
5.16 复习建议
Pandas 备考不要只背函数名,要训练“题目关键词 → 代码模板”的反应。
看到“筛选”,想到:
看到“多个条件”,想到:
看到“缺失值”,想到:
1 2 3 df.isna().sum () df.dropna() df.fillna()
看到“分组平均”,想到:
1 df.groupby("分组列" )["数值列" ].mean()
看到“多个统计指标”,想到:
1 df.groupby("分组列" ).agg(...)
看到“每组前 N 名”,想到:
1 df.groupby("分组列" )["排序列" ].rank(...)
看到“两张表合并”,想到:
看到“保留主表全部数据”,想到:
看到“数据透视表”,想到:
看到“分类变量频数”,想到:
5.17 整套 5 篇教程总结
这 5 篇文章已经覆盖 Pandas 认证考试的主要内容:
第 1 篇:基础操作 掌握
DataFrame、Series、读取数据、查看数据、筛选、排序、新增列。
第 2 篇:数据清洗
掌握缺失值、重复值、类型转换、字符串处理、日期处理、异常值处理。
第 3 篇:统计分析 掌握
groupby、agg、transform、rank、pivot_table、crosstab、cut。
第 4 篇:多表操作 掌握
concat、merge、join、连接方式、未匹配检查、多对多陷阱。
第 5 篇:综合实战
掌握从原始数据到清洗、合并、统计、排名、透视、导出的完整流程。
最终要形成的能力不是“记住 Pandas 有多少函数”,而是:
给你一份表,你知道怎么检查。
给你一份脏数据,你知道怎么清洗。
给你两张表,你知道怎么合并。
给你一个统计需求,你知道怎么 groupby。
给你一个排名需求,你知道怎么 rank。
给你一个综合题,你知道怎么一步一步拆解。