如果你正在学习数据分析,或者工作中需要处理Excel、CSV文件,那么你一定听说过Pandas。但你可能也遇到过这样的困惑:教程看了很多,代码也敲了不少,但一到自己动手处理真实数据,还是不知道从何下手,或者写出的代码又慢又容易出错。
这其实不是你的问题。很多Pandas教程只教了“怎么用”,却没讲清楚“为什么这么用”以及“什么时候该用什么”。结果就是,你记住了df.groupby(),却不知道它背后的分组逻辑和性能陷阱;你学会了df.merge(),却不清楚合并时数据对齐的细节会导致多少坑。
这篇文章要解决的,正是这个核心痛点。我们不追求面面俱到地罗列所有API,而是通过一个清晰的、由问题驱动的学习路径,带你快速掌握Pandas解决实际问题的核心能力。我们的目标是:在2小时内,让你不仅能看懂Pandas代码,更能写出高效、健壮的数据处理脚本,真正将Pandas变成你手中的数据分析利器。
我们将从最基础的“用Pandas思维看待数据”开始,逐步深入到数据清洗、转换、聚合分析等核心操作,并穿插大量真实场景的代码示例和避坑指南。无论你是编程新手,还是有一定基础但想系统提升的数据工作者,这篇文章都将为你提供一个扎实的起点。
1. 为什么你学了很多Pandas,却依然处理不好数据?
很多人在学习Pandas时,容易陷入两个误区。第一个误区是“命令式学习”,把Pandas当成一本命令字典,死记硬背df.iloc、df.loc、df.pivot_table的用法,却忽略了它们内在的数据模型和设计哲学。第二个误区是“Excel思维迁移”,试图用操作Excel表格的直觉来写Pandas代码,结果代码冗长低效,还常常得到错误的结果。
Pandas的核心,其实是一个基于标签索引的、内存中的、表格型数据结构。理解这一点,比记住一百个函数更重要。它意味着:
- 数据对齐是自动的:基于行/列标签进行操作时,Pandas会自动对齐数据,这既是便利也是陷阱。
- 向量化操作是高效的:避免使用Python原生的
for循环遍历DataFrame,而是使用Pandas提供的向量化方法或apply函数。 - 索引是灵魂:无论是行索引(Index)还是列索引(Columns),它们不仅仅是标签,更是高效数据访问和合并的关键。
举个例子,如果你有一份销售数据,需要计算每个地区的平均销售额。用Excel思维,你可能会想“筛选每个地区,然后求平均”。但在Pandas里,正确的思维是“按‘地区’列分组,然后对‘销售额’列应用均值函数”。这种思维转换,是高效使用Pandas的第一步。
2. 环境准备:不仅仅是安装一个包
在开始写代码之前,确保你的环境是正确且可复现的。这对于后续排查问题至关重要。
2.1 Python与Pandas版本选择
Pandas与Python版本有兼容性要求。对于新手,我们建议选择稳定的组合。
- Python: 推荐使用Python 3.8或3.9。这两个版本生态兼容性最好。
- Pandas: 推荐安装最新稳定版(如1.5.x或2.x)。Pandas 2.0是一个重要更新,性能提升显著,但部分API有细微变化。本文示例会兼顾常见版本。
你可以通过以下命令查看版本:
python --version pip show pandas2.2 使用虚拟环境
强烈建议为每个数据分析项目创建独立的虚拟环境,避免包冲突。
# 创建虚拟环境(以项目名`data_analysis`为例) python -m venv data_analysis_env # 激活虚拟环境 # Windows: data_analysis_env\Scripts\activate # macOS/Linux: source data_analysis_env/bin/activate # 在激活的虚拟环境中安装pandas及常用配套库 pip install pandas numpy matplotlib openpyxlnumpy: Pandas的底层计算依赖。matplotlib: 基础绘图库,用于数据可视化。openpyxl: 用于读写Excel.xlsx文件。
2.3 选择你的开发工具
对于数据分析,一个好的IDE或编辑器能极大提升效率。
- Jupyter Notebook / JupyterLab:交互式探索数据的首选。可以分段执行代码,即时查看数据和图表,非常适合数据清洗和分析过程中的试错。
- VS Code / PyCharm:大型脚本和项目开发的首选。拥有强大的代码补全、调试和版本管理功能。
对于初学者,从Jupyter Notebook开始体验会更好。你可以通过以下命令安装并启动:
pip install jupyter jupyter notebook3. 核心数据结构:Series与DataFrame,你的数据容器
理解Pandas,必须从它的两个核心数据结构开始:Series和DataFrame。你可以把DataFrame想象成Excel中的一个工作表(Sheet),而Series则是这个工作表中的单独一列。
3.1 Series:带标签的一维数组
一个Series由两部分组成:索引(index)和值(values)。索引可以是数字、字符串、时间等,它给了数据“名字”。
import pandas as pd # 创建一个Series s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd']) print(s)输出:
a 10 b 20 c 30 d 40 dtype: int64你可以通过索引来访问值:s['b']会返回20。Series支持向量化运算,这是它比Python列表强大的地方:
print(s * 2) # 每个元素乘以2 print(s[s > 25]) # 布尔索引,筛选出大于25的值3.2 DataFrame:二维表格,数据分析的主战场
DataFrame是多个Series的集合,它们共享同一个行索引。你可以把它看作一个字典,键是列名,值是该列对应的Series。
# 从一个字典创建DataFrame data = { '姓名': ['张三', '李四', '王五'], '年龄': [28, 34, 23], '城市': ['北京', '上海', '广州'] } df = pd.DataFrame(data) print(df)输出:
姓名 年龄 城市 0 张三 28 北京 1 李四 34 上海 2 王五 23 广州注意,Pandas自动为我们添加了行索引(0, 1, 2)。这个自动生成的整数索引是默认行为,但我们可以指定更有意义的索引。
df = pd.DataFrame(data, index=['emp001', 'emp002', 'emp003']) print(df)关键理解:DataFrame的列(Columns)是Series,它们拥有独立的数据类型(dtype)。你可以通过df.dtypes查看。理解并正确设置dtype(如将字符串日期转为datetime,将数字字符串转为int),是后续高效操作和避免内存浪费的基础。
4. 数据I/O:如何与外部世界交换数据?
数据分析的第一步和最后一步往往是读写数据。Pandas支持丰富的格式。
4.1 读取数据:从CSV、Excel、数据库到剪贴板
# 1. 读取CSV文件(最常用) df_csv = pd.read_csv('sales_data.csv', encoding='utf-8') # 指定编码,防止中文乱码 # 常用参数:sep(分隔符)、header(第几行作为列名)、names(自定义列名) # 2. 读取Excel文件 df_excel = pd.read_excel('财务报告.xlsx', sheet_name='Sheet1', engine='openpyxl') # 需要安装openpyxl或xlrd库。`sheet_name`可以指定工作表名或序号。 # 3. 从剪贴板读取(快速从Excel复制数据到Python) # 在Excel中选中数据区域并复制(Ctrl+C),然后运行: df_clipboard = pd.read_clipboard() print(df_clipboard.head()) # 查看前5行 # 4. 从数据库读取(以SQLite为例) import sqlite3 conn = sqlite3.connect('my_database.db') df_sql = pd.read_sql_query("SELECT * FROM sales WHERE amount > 1000", conn) conn.close()4.2 写入数据:保存你的分析成果
# 1. 写入CSV df.to_csv('processed_data.csv', index=False, encoding='utf-8-sig') # `index=False`表示不将行索引写入文件。`utf-8-sig`能更好地被Excel识别。 # 2. 写入Excel df.to_excel('分析结果.xlsx', sheet_name='汇总', index=False) # 3. 写入数据库 conn = sqlite3.connect('new_database.db') df.to_sql('result_table', conn, if_exists='replace', index=False) # if_exists: 'fail', 'replace', 'append' conn.close()避坑指南:
- 编码问题:处理中文文本时,最常遇到乱码。读取时尝试
encoding='utf-8'、'gbk'或'gb2312'。写入CSV供Excel打开时,使用encoding='utf-8-sig'。 - 大文件读取:如果文件太大,使用
chunksize参数分块读取,或使用dtype参数预先指定列类型以减少内存占用。 - 日期解析:CSV中的日期列可能被读成字符串。使用
parse_dates=['日期列名']参数让Pandas在读取时自动解析。
5. 数据查看与探索:你的第一份“数据体检报告”
拿到数据后,不要急着处理,先花几分钟了解它。
# 假设df是一个刚读入的DataFrame # 1. 宏观概览 print(df.shape) # 查看维度:(行数, 列数) print(df.info()) # 查看索引、列名、非空值数量、数据类型 print(df.describe()) # 针对数值列,生成描述性统计(计数、均值、标准差、最小值、四分位数、最大值) # 2. 查看头部和尾部数据 print(df.head(10)) # 查看前10行,默认5行 print(df.tail()) # 查看最后5行 # 3. 查看列信息 print(df.columns) # 查看所有列名 print(df.dtypes) # 查看每列的数据类型 # 4. 简单统计 print(df['销售额'].mean()) # 某一列的平均值 print(df['城市'].value_counts()) # 某一列的值分布(计数)这个阶段的目标是发现数据的“健康状况”:有没有缺失值?数据类型是否正确?有没有明显的异常值(比如年龄为200岁)?这些洞察将直接指导后续的数据清洗步骤。
6. 数据清洗实战:处理缺失值、重复值与异常值
脏数据是分析结果错误的罪魁祸首。数据清洗通常占据数据分析80%的时间。
6.1 处理缺失值(NaN/None)
Pandas用NaN(Not a Number)或None表示缺失值。
# 1. 检测缺失值 print(df.isnull().sum()) # 统计每列缺失值的数量 print(df[df['年龄'].isnull()]) # 筛选出‘年龄’列缺失的行 # 2. 删除缺失值 (谨慎使用,可能丢失大量信息) df_dropped = df.dropna() # 删除任何包含缺失值的行 df_dropped_col = df.dropna(axis=1) # 删除任何包含缺失值的列 df_dropped_subset = df.dropna(subset=['年龄', '收入']) # 仅在‘年龄’和‘收入’列同时缺失时才删除该行 # 3. 填充缺失值 (更常用的方法) # 用固定值填充 df_filled = df.fillna({'年龄': 0, '城市': '未知'}) # 用前向填充(用上一个有效值填充) df_ffill = df.fillna(method='ffill') # 用后向填充 df_bfill = df.fillna(method='bfill') # 用统计值填充(如均值、中位数) mean_age = df['年龄'].mean() df['年龄'].fillna(mean_age, inplace=True) # inplace=True表示直接修改原df选择策略:删除适用于缺失值很少的情况。填充时,需要根据业务逻辑选择合适的方法。例如,时间序列数据常用前向/后向填充,数值型数据常用均值/中位数填充,分类数据常用众数或“未知”填充。
6.2 处理重复值
# 检测重复行(所有列值完全相同) print(df.duplicated().sum()) # 基于特定列检查重复 print(df.duplicated(subset=['身份证号']).sum()) # 删除重复行 df_dedup = df.drop_duplicates() # 保留第一次出现的行 df_dedup_last = df.drop_duplicates(keep='last') # 保留最后一次出现的行 df_dedup_none = df.drop_duplicates(keep=False) # 删除所有重复行6.3 处理异常值
异常值没有统一标准,需要结合业务知识(如销售额不可能为负)或统计方法(如3σ原则)判断。
# 方法1:基于业务规则 df_clean = df[df['年龄'] >= 0] # 过滤掉年龄小于0的记录 df_clean = df[df['年龄'] <= 100] # 方法2:基于标准差(假设数据服从正态分布) mean = df['销售额'].mean() std = df['销售额'].std() lower_bound = mean - 3 * std upper_bound = mean + 3 * std df_clean = df[(df['销售额'] >= lower_bound) & (df['销售额'] <= upper_bound)] # 方法3:基于分位数(IQR方法) Q1 = df['销售额'].quantile(0.25) Q3 = df['销售额'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR df_clean = df[(df['销售额'] >= lower_bound) & (df['销售额'] <= upper_bound)]7. 数据选择与过滤:精准定位你需要的数据
这是Pandas最核心的操作之一。主要分为基于标签的.loc和基于整数位置的.iloc。
7.1.loc:基于标签进行选择
# 选择单行(返回Series) print(df.loc['emp001']) # 通过索引标签选择 print(df.loc[0]) # 如果索引是整数,也可以这样用,但推荐用.iloc # 选择多行 print(df.loc[['emp001', 'emp003']]) # 选择行和列(行在前,列在后,用逗号分隔) print(df.loc['emp001', '姓名']) # 标量:某个单元格的值 print(df.loc[['emp001', 'emp003'], '姓名']) # Series:指定行、指定列 print(df.loc['emp001': 'emp003', '姓名':'城市']) # 切片:注意.loc的切片是包含结束位置的! # 布尔索引(非常强大!) print(df.loc[df['年龄'] > 30]) # 选择年龄大于30的所有行 print(df.loc[(df['城市'] == '北京') & (df['年龄'] > 25)]) # 多条件筛选(与) print(df.loc[(df['城市'] == '北京') | (df['城市'] == '上海')]) # 多条件筛选(或) print(df.loc[df['城市'].isin(['北京', '上海', '广州'])]) # 判断是否在列表中7.2.iloc:基于整数位置进行选择
.iloc完全基于行和列的整数位置(从0开始),与索引标签无关。
print(df.iloc[0]) # 选择第一行 print(df.iloc[0:3]) # 选择第1到第3行(不包含第3行,与Python列表切片一致) print(df.iloc[0:3, 0:2]) # 选择第1到第3行,第1到第2列 print(df.iloc[[0, 2], [1, 3]]) # 选择第1、3行,第2、4列7.3 直接索引与条件赋值
除了.loc和.iloc,还有一些快捷方式,但需要注意它们可能产生视图(view)或副本(copy)的差异,在赋值时可能导致SettingWithCopyWarning警告。
# 选择单列(返回Series) ages = df['年龄'] # 选择多列(返回DataFrame) subset = df[['姓名', '城市']] # 条件赋值(安全做法:使用.loc) df.loc[df['城市'] == '北京', '补贴'] = 500 # 给北京员工添加补贴列并赋值核心原则:为了代码清晰和避免警告,进行数据选择时,优先使用.loc和.iloc;进行赋值操作时,务必使用.loc。
8. 数据转换与操作:让数据为你所用
清洗完的数据,需要进一步转换才能用于分析。
8.1 修改列
# 重命名列 df.rename(columns={'old_name': 'new_name', '年龄': 'Age'}, inplace=True) # 增加新列(基于现有列计算) df['年薪'] = df['月薪'] * 12 df['年龄分组'] = pd.cut(df['年龄'], bins=[0, 18, 35, 60, 100], labels=['少年', '青年', '中年', '老年']) # 删除列 df.drop(columns=['临时列', '无用列'], inplace=True) # 或者使用 del 关键字 # del df['临时列']8.2 类型转换
错误的数据类型会导致计算错误或性能低下。
# 查看类型 print(df.dtypes) # 转换类型 df['订单日期'] = pd.to_datetime(df['订单日期_str']) # 字符串转日期时间 df['用户ID'] = df['用户ID'].astype('int32') # 转为整数,节省内存 df['金额'] = df['金额'].astype('float64') # 转为浮点数 df['类别'] = df['类别'].astype('category') # 转为分类类型,适用于重复值多的文本列,能极大提升性能和节省内存8.3 字符串处理
Pandas的字符串方法通过.str访问器调用。
# 大小写转换 df['城市'] = df['城市'].str.upper() df['姓名'] = df['姓名'].str.title() # 去除空格 df['地址'] = df['地址'].str.strip() # 包含、匹配、替换 df['是否VIP'] = df['邮箱'].str.contains('vip') # 检查是否包含‘vip’ df['电话前缀'] = df['电话'].str.extract(r'(\d{3})') # 正则提取 df['地址'] = df['地址'].str.replace('街道', 'St.') # 替换 # 分割字符串 df[['姓', '名']] = df['姓名'].str.split(' ', expand=True) # 按空格分割成两列8.4 应用函数:apply、map、applymap
当内置方法不够用时,可以使用自定义函数。
# Series.apply: 将函数应用于Series的每个元素 def classify_age(age): if age < 30: return '青年' elif age < 50: return '中年' else: return '老年' df['年龄段'] = df['年龄'].apply(classify_age) # 也可以使用lambda表达式 df['年龄段'] = df['年龄'].apply(lambda x: '青年' if x < 30 else ('中年' if x < 50 else '老年')) # DataFrame.apply: 将函数应用于DataFrame的某一行或某一列 # axis=0 (默认): 将函数应用于每一列(传入Series) # axis=1: 将函数应用于每一行(传入Series) df['总成绩'] = df[['语文', '数学', '英语']].apply(sum, axis=1) # 对每行求和 # map: 用于Series,根据一个映射关系进行值替换(常用于编码转换) gender_map = {'M': '男', 'F': '女'} df['性别'] = df['性别代码'].map(gender_map) # applymap: 将函数应用于DataFrame的每个元素(效率较低,谨慎使用) df_numeric = df[['语文','数学']].applymap(lambda x: x*1.1) # 每个成绩加10%性能提示:apply是灵活的,但速度比向量化操作慢。如果可能,优先使用Pandas内置的向量化方法(如.str.方法、算术运算)或np.where。
9. 数据聚合与分组分析:发现数据背后的模式
这是数据分析的精华所在,回答“是多少?”和“为什么?”的问题。
9.1 基本的聚合统计
# 对整个DataFrame或Series的聚合 print(df['销售额'].sum()) print(df['销售额'].mean()) print(df['销售额'].std()) print(df['销售额'].min()) print(df['销售额'].max()) print(df['销售额'].count()) # 非空计数 print(df['销售额'].describe()) # 一次性输出多个统计量 # 对多列同时聚合 print(df[['销售额', '利润']].agg(['sum', 'mean', 'std']))9.2 分组聚合:groupby
groupby是Pandas最强大的功能之一。它的核心思想是“拆分-应用-合并”。
# 单列分组,单列聚合 grouped = df.groupby('城市')['销售额'].sum() print(grouped) # 输出是一个Series,索引是城市,值是各城市销售额总和。 # 单列分组,多列聚合 grouped_multi = df.groupby('城市')[['销售额', '利润']].agg(['sum', 'mean']) print(grouped_multi) # 输出是一个带多级列索引的DataFrame。 # 多列分组 grouped_multi_index = df.groupby(['年份', '季度'])['销售额'].sum() print(grouped_multi_index) # 输出是一个具有多级索引的Series。 # 对不同的列应用不同的聚合函数 agg_dict = { '销售额': 'sum', '利润': 'mean', '订单ID': 'count' # 计算订单数 } result = df.groupby('销售员').agg(agg_dict) result = result.rename(columns={'订单ID': '订单数'}) # 重命名聚合后的列 print(result)9.3 分组后操作与转换
有时我们不仅需要聚合后的结果,还需要将聚合结果“广播”回原始数据的每一行。
# transform: 返回一个与原始分组数据形状相同的Series/DataFrame df['城市平均销售额'] = df.groupby('城市')['销售额'].transform('mean') # 这样,每一行数据都新增了一列,显示该行所在城市的平均销售额。 # 分组排序 df['组内排名'] = df.groupby('部门')['业绩'].rank(ascending=False, method='dense') # 在每个部门内,按业绩降序排名。 # 过滤组 def filter_func(group): return group['销售额'].sum() > 100000 # 只保留总销售额超过10万的组 filtered_df = df.groupby('城市').filter(filter_func)10. 数据合并与连接:整合多源数据
现实中的数据往往分散在多个表格中,需要合并。
10.1concat:简单的堆叠
用于沿某个轴(行或列)将多个DataFrame堆叠在一起。
df1 = pd.DataFrame({'A': ['A0', 'A1'], 'B': ['B0', 'B1']}) df2 = pd.DataFrame({'A': ['A2', 'A3'], 'B': ['B2', 'B3']}) # 纵向堆叠(增加行) result = pd.concat([df1, df2], ignore_index=True) # ignore_index重置索引 print(result) # 横向堆叠(增加列) df3 = pd.DataFrame({'C': ['C0', 'C1'], 'D': ['D0', 'D1']}) result_h = pd.concat([df1, df3], axis=1) print(result_h)10.2merge:基于键的连接(类似SQL JOIN)
这是最常用、功能最强大的合并方法。
# 模拟两个表 orders = pd.DataFrame({ 'order_id': [1, 2, 3, 4], 'customer_id': [101, 102, 103, 104], 'amount': [200, 150, 300, 400] }) customers = pd.DataFrame({ 'customer_id': [101, 102, 103, 105], 'name': ['Alice', 'Bob', 'Charlie', 'David'], 'city': ['NY', 'LA', 'NY', 'Chicago'] }) # 内连接(inner join):只保留两个表都有的键 inner_merged = pd.merge(orders, customers, on='customer_id', how='inner') print("内连接结果:") print(inner_merged) # 左连接(left join):以左表(orders)为基准 left_merged = pd.merge(orders, customers, on='customer_id', how='left') print("\n左连接结果:") print(left_merged) # 外连接(outer join):保留所有键,缺失值用NaN填充 outer_merged = pd.merge(orders, customers, on='customer_id', how='outer') print("\n外连接结果:") print(outer_merged) # 基于多个键连接 merged_multi = pd.merge(df1, df2, on=['key1', 'key2']) # 左右表键名不同时 merged_diff = pd.merge(orders, customers, left_on='order_cust_id', right_on='customer_id')10.3join:基于索引的连接
join是merge的快捷方式,默认按索引进行左连接。
df1.set_index('key', inplace=True) df2.set_index('key', inplace=True) result = df1.join(df2, how='inner', lsuffix='_left', rsuffix='_right')连接操作的核心:明确你的连接键(on)、连接方式(how)以及如何处理重复列名(suffixes)。在合并大型数据集前,先用小样本测试是一个好习惯。
11. 时间序列处理:让时间成为你的分析维度
Pandas对时间序列的支持是其另一大杀手锏。
11.1 时间类型转换与生成
# 将字符串转为时间戳 df['datetime_col'] = pd.to_datetime(df['date_str_col'], format='%Y-%m-%d %H:%M:%S') # format参数可以加速转换并避免歧义 # 生成时间范围 date_rng = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D') # 按天 period_rng = pd.period_range(start='2023Q1', end='2024Q1', freq='Q') # 按季度 # 从时间戳中提取属性 df['year'] = df['datetime_col'].dt.year df['month'] = df['datetime_col'].dt.month df['day'] = df['datetime_col'].dt.day df['dayofweek'] = df['datetime_col'].dt.dayofweek # 周一=0,周日=6 df['hour'] = df['datetime_col'].dt.hour df['is_weekend'] = df['datetime_col'].dt.dayofweek >= 511.2 重采样与频率转换
重采样(Resampling)是时间序列分析的核心,用于将数据从一个频率转换到另一个频率(如将日数据聚合为月数据)。
# 假设df_time的索引是时间戳(DatetimeIndex) df_time = df.set_index('datetime_col') # 降采样:高频到低频(如日->月) monthly_sales = df_time['销售额'].resample('M').sum() # 'M'表示月末 quarterly_avg = df_time['销售额'].resample('Q').mean() # 'Q'表示季度末 # 升采样:低频到高频(如月->日),会产生缺失值,需要填充 daily_from_monthly = monthly_sales.resample('D').asfreq() # 仅扩展索引,值为NaN daily_filled = monthly_sales.resample('D').ffill() # 前向填充11.3 滑动窗口操作
用于计算移动平均、移动标准差等,平滑数据或观察趋势。
# 简单移动平均 df['sales_ma_7'] = df['销售额'].rolling(window=7).mean() # 7天移动平均 # 扩展窗口平均(累计平均) df['sales_expanding_mean'] = df['销售额'].expanding().mean()12. 实战演练:一个完整的数据分析小案例
让我们用一个模拟的电商订单数据,串联以上知识点。
import pandas as pd import numpy as np # 1. 创建模拟数据 np.random.seed(42) # 确保结果可复现 n_records = 1000 dates = pd.date_range('2023-01-01', periods=n_records, freq='D') df = pd.DataFrame({ 'order_id': range(1000, 1000 + n_records), 'order_date': np.random.choice(dates, n_records), 'customer_id': np.random.randint(1001, 1100, n_records), 'city': np.random.choice(['北京', '上海', '广州', '深圳', '杭州'], n_records), 'category': np.random.choice(['电子产品', '服装', '食品', '图书', '家居'], n_records), 'amount': np.round(np.random.uniform(50, 500, n_records), 2), 'quantity': np.random.randint(1, 10, n_records) }) # 故意制造一些缺失值和重复值 df.loc[np.random.choice(df.index, 50), 'city'] = np.nan df = pd.concat([df, df.sample(20)], ignore_index=True) # 添加20行重复数据 print("1. 数据概览:") print(df.info()) print(df.head()) # 2. 数据清洗 print("\n2. 数据清洗:") print(f"重复行数: {df.duplicated().sum()}") df_clean = df.drop_duplicates() print(f"缺失值统计:\n{df_clean.isnull().sum()}") # 假设城市缺失用‘未知’填充 df_clean['city'].fillna('未知', inplace=True) # 检查异常值(金额为负) df_clean = df_clean[df_clean['amount'] > 0] # 3. 数据转换与增强 df_clean['order_date'] = pd.to_datetime(df_clean['order_date']) df_clean['year_month'] = df_clean['order_date'].dt.to_period('M') # 提取年月周期 df_clean['total_sales'] = df_clean['amount'] * df_clean['quantity'] # 4. 核心分析 print("\n3. 核心分析:") # 各城市总销售额 city_sales = df_clean.groupby('city')['total_sales'].sum().sort_values(ascending=False) print("各城市总销售额排名:") print(city_sales) # 每月销售额趋势 monthly_trend = df_clean.groupby('year_month')['total_sales'].sum() print("\n月度销售额趋势:") print(monthly_trend.head()) # 最畅销的商品类别 top_category = df_clean.groupby('category')['quantity'].sum().sort_values(ascending=False) print("\n商品类别销量排名:") print(top_category) # 高价值客户(总消费金额前10) top_customers = df_clean.groupby('customer_id')['total_sales'].sum().nlargest(10) print("\n高价值客户Top 10:") print(top_customers) # 5. 输出分析结果 df_clean.to_csv('cleaned_orders.csv', index=False, encoding='utf-8-sig') print("\n分析完成,清洗后的数据已保存至 'cleaned_orders.csv'。")13. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 读取CSV文件乱码 | 文件编码与encoding参数不匹配 | 用文本编辑器尝试不同编码打开,或用chardet库检测 | 尝试encoding='utf-8','gbk','gb2312','utf-8-sig' |
KeyError当使用df[‘列名’] | 列名拼写错误、包含空格、或列不存在 | print(df.columns.tolist())查看精确列名 | 检查列名大小写和空格,或使用df.get(‘列名’, default) |
SettingWithCopyWarning警告 | 对df的切片或索引结果进行赋值,Pandas不确定是修改视图还是副本 | 明确使用.copy()创建副本,或使用.loc进行赋值 | 赋值前使用.copy(),或直接使用df.loc[mask, ‘col’] = value |
MemoryError内存不足 | 数据量太大,或数据类型(如object)占用内存多 | 使用df.info(memory_usage=‘deep’)查看内存使用 | 1. 读取时指定dtype。2. 使用category类型。3. 分块读取(chunksize)。 |
合并(merge)后数据变多或变少 | 连接键(on)不唯一,或连接方式(how)选择错误 | 检查连接键在各表中的唯一性:df[‘key’].duplicated().sum() | 1. 确保理解每种how参数的含义。2. 合并前对键进行去重或聚合。 |
groupby结果不符合预期 | 分组键包含NaN,或分组后聚合函数用错 | 检查分组键是否有NaN:df[‘group_col’].isnull().sum() | 1. 处理分组键的缺失值。2. 确认聚合函数(如‘sum’vs‘count’)是否适用该列数据类型。 |
| 日期时间解析错误 | 日期字符串格式与默认解析器不匹配 | 打印出几行原始日期字符串查看格式 | 使用pd.to_datetime(df[‘col’], format=‘%Y/%m/%d’)指定格式 |
14. 最佳实践与性能优化建议
- 明确目标,再写代码:动手前,先用自然语言或伪代码描述你要做什么。这能帮你选择最合适的Pandas操作,避免绕弯路。
- 善用向量化,避免循环:Pandas的底层是NumPy,向量化操作比Python循环快成百上千倍。能用
df[‘col’] * 2就不要用for循环。 - 选择合适的数据类型:将文本列(尤其是重复值多的)转为
category类型,将整数转为int32/int16,能大幅减少内存占用和提升速度。 - 使用
.loc和.iloc进行索引和赋值:这是最清晰、最不容易出错的方式,能有效避免SettingWithCopyWarning。 - 链式操作与中间变量:适度的链式操作(如
df.query().groupby().agg())可以让代码更简洁。但过于复杂的链式操作会难以调试。对于复杂的多步处理,适当使用中间变量保存结果,方便检查和排错。 - 处理大型数据集的策略:
- 采样:先用
df.sample(10000)对小样本进行开发和测试。 - 指定数据类型:
pd.read_csv(..., dtype={‘col1’: ‘int32’})。 - 只读需要的列:
pd.read_csv(..., usecols=[‘col1’, ‘col2’])。 - 使用分块:
for chunk in pd.read_csv(..., chunksize=100000): process(chunk)。 - 考虑其他工具:数据量极大时(GB级以上),可以考虑Dask、Modin或直接使用数据库。
- 采样:先用
- 保持代码可读性:为列名、变量起有意义的名字,在复杂操作后添加注释,使用函数封装可复用的数据处理逻辑。
通过以上由浅入深、问题驱动的学习,你应该已经对Pandas的核心功能有了系统的了解。记住,熟练使用Pandas的关键不在于记住所有函数,而在于建立起“数据框思维”,并掌握如何将现实问题转化为筛选-分组-聚合-合并等一系列标准操作。接下来,最好的学习方式就是找到一份你自己的数据,从数据导入开始,完整地走一遍清洗、探索、分析和可视化的流程。每一次解决实际问题的过程,都会让你对Pandas的理解更加深刻。