# 勤奋的凯尔森同学10000h刻意练习Day3:Pandas详解 —— 数据结构、清洗、分组聚合与透视表

所属专栏: 凯尔森的10000h刻意练习
Slogan: 一个36岁程序员, 研究生(学硕),前世界500强top20软件开发Java工程师,用10000小时深耕AI, 目标Top10 AI资深工程师, 全程日更记录。
tips: 文章末尾附有jupyter完整代码

Pandas是Python数据分析的核心库,它基于 NumPy 构建,提供了两种主要的数据结构:Series(一维)和 DataFrame(二维),并配备了丰富的数据处理函数。Pandas让数据清洗、转换、分析变得简单高效。

# 1.1 Pandas数据结构

# 1.1.1 Series

Series 是一种一维的带标签数组,可以存储任意数据类型(整数、浮点数、字符串、Python对象等)。它的标签统称为索引。

一个Series包含两个核心部分:

  • 数据(values):一维数组,可以是整数、浮点数、字符串等各种类型。
  • 索引(index):与数据一一对应的标签。如果没有显式指定索引,Pandas会自动生成从0开始的整数索引。

创建Series:

import pandas as pd
import numpy as np

# 从列表创建
s1 = pd.Series([1, 3, 5, np.nan, 6])
print(s1)
# 0 1.0
# 1 3.0
# 2 5.0
# 3 NaN
# 4 6.0
# dtype: float64

# 指定索引
s2 = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
print(s2)
# a 1
# b 2
# c 3
# dtype: int64

# 从字典创建(键成为索引)
s3 = pd.Series({'a': 1, 'b': 2, 'c': 3})
print(s3)
# a 1
# b 2
# c 3
# dtype: int64
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

Series的常用属性:

print(s2.values)      # [1 2 3]
print(s2.index)       # Index(['a', 'b', 'c'], dtype='str')
print(s2.dtype)       # int64
print(s2.shape)       # (3,)
1
2
3
4

# 1.1.2 DataFrame

DataFrame 是一个二维的、带行列标签的数据结构。你可以把它想象成一个Excel表格或SQL表。

一个DataFrame包含三个主要部分:

  • 数据(values):二维数组(类似NumPy的二维数组),存储表格中的数据。
  • 行索引(index):每一行的标签,可以是整数、字符串等。
  • 列名(columns):每一列的标签。

创建DataFrame:

# 从字典创建(键为列名,值为列表或数组)
data = {
    'name': ['Kelsen', 'Bob', 'Charlie'],
    'age': [25, 30, 35],
    'city': ['New York', 'Paris', 'London']
}
df = pd.DataFrame(data)
print(df)
# name age city
# 0 Alice 25 New York
# 1 Bob 30 Paris
# 2 Charlie 35 London

# 从二维NumPy数组创建,同时指定列名和索引
arr = np.random.randn(3, 4)
df2 = pd.DataFrame(arr, columns=['A', 'B', 'C', 'D'], index=['x', 'y', 'z'])
print(df2)
# A B C D
# x 0.123456 -0.123456 1.234567 -0.987654
# y 0.456789 -0.456789 1.345678 -0.876543
# z 0.789012 -0.789012 1.456789 -0.765432
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

常用属性:

print(df.shape)      # (3, 3)
print(df.columns)    # Index(['name', 'age', 'city'], dtype='str')
print(df.index)      # RangeIndex(start=0, stop=3, step=1)
print(df.dtypes)  # 每列数据类型
print(df.info())  # 查看数据信息,包括非空计数
1
2
3
4
5

# 1.2 数据导入与导出

Pandas支持多种文件格式的读写,最常用的是CSV、Excel、JSON等。

# 1.2.1 写入文件

为了读取文件,我们先生成对于的文件并写入内容。

import pandas as pd
import numpy as np
import json
import pymysql
from datetime import datetime

# 设置随机种子以便结果可复现
np.random.seed(42)

# -------------------- 1. 生成data.csv --------------------
csv_data = pd.DataFrame({
    'col1': [f'ID_{i}' for i in range(1, 6)],          # 字符串类型
    'col2': np.random.randint(10, 100, 5),              # 整数
    'date': pd.date_range('2026-09-09', periods=5, freq='D')  # 日期
})
csv_data.to_csv('data.csv', index=False, encoding='utf-8')
print("已生成data.csv")

# -------------------- 2. 生成data.xlsx --------------------
excel_data = pd.DataFrame({
    'A': [1, 2, 3, 4, 5],
    'B': ['apple', 'banana', 'cherry', 'date', 'elderberry'],
    'C': [1.1, 2.2, 3.3, 4.4, 5.5]
})
# 需要安装openpyxl
excel_data.to_excel('data.xlsx', sheet_name='Sheet1', index=False)
print("已生成data.xlsx")

# -------------------- 3. 生成data.json --------------------
json_data = [
    {"name": "Kelsen", "age": 36, "city": "ShangHai"},
    {"name": "Bob", "age": 30, "city": "Paris"},
    {"name": "Charlie", "age": 35, "city": "London"}
]
with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(json_data, f, indent=2)
print("已生成data.json")

# -------------------- 4. 生成MySQL数据脚本data.sql --------------------
sql_content = """-- 创建数据库(如果需要)
-- CREATE DATABASE IF NOT EXISTS testdb;
-- USE testdb;

-- 创建表(注意:表名避免使用关键字,这里用users)
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
    `id` INT PRIMARY KEY,
    `name` VARCHAR(50),
    `age` INT,
    `salary` DECIMAL(10,2)
);

-- 插入数据
INSERT INTO `users` (`id`, `name`, `age`, `salary`) VALUES
(1, 'John', 28, 50000),
(2, 'Jane', 32, 62000),
(3, 'Mike', 41, 75000),
(4, 'Emily', 26, 48000);
"""

with open('data.sql', 'w', encoding='utf-8') as f:
    f.write(sql_content)
print("已生成data.sql (MySQL脚本)")
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

# 1.2.2 读取文件

import pandas as pd
import pymysql

# 读取CSV
df_csv = pd.read_csv('data.csv', sep=',', header=0, index_col=0,
                     usecols=['col1', 'col2', 'date'], dtype={'col1': str},
                     parse_dates=['date'], encoding='utf-8')
print("CSV 读取结果:")
print(df_csv.head())

# 读取Excel
df_excel = pd.read_excel('data.xlsx', sheet_name='Sheet1')
print("\nExcel 读取结果:")
print(df_excel.head())

# 读取JSON
df_json = pd.read_json('data.json')
print("\nJSON 读取结果:")
print(df_json.head())

# 读取MySQL(请先修改连接信息)
conn = pymysql.connect(host='localhost', user='root', password='123456', database='testdb')
df_sql = pd.read_sql_query("SELECT * FROM users", conn)
conn.close()
print("\nMySQL 读取结果:")
print(df_sql.head())
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

# 1.3 数据查看与基本信息

df = pd.read_csv('hep.csv', encoding='utf-8')
"""
hep.csv的内容如下:
id,name,age,phone,email,city
1,Alice Johnson,28,13811223344,alice.johnson@gmail.com,Paris
2,Bob Smith,34,13999887766,bob.smith@163.com,London
3,Carol Davis,45,15933445566,carol.davis@qq.com,New York
4,David Brown,52,15877665544,david.brown@126.com,Tokyo
5,Eva Wilson,23,17788990011,eva.wilson@outlook.com,Beijing
6,Frank Miller,38,18822334455,frank.miller@hotmail.com,Sydney
7,Grace Lee,41,13644556677,grace.lee@yahoo.com,Moscow
8,Henry Taylor,29,18977889900,henry.taylor@sina.com,Rome
9,Ivy Anderson,33,15233448899,ivy.anderson@sohu.com,Berlin
10,Jack Thomas,47,18755667788,jack.thomas@gmail.com,Madrid
11,Karen Moore,26,18233445566,karen.moore@qq.com,Cairo
12,Larry Jackson,55,18399887766,larry.jackson@163.com,Rio
13,Megan White,31,18511223344,megan.white@126.com,Toronto
14,Nick Harris,44,18655667788,nick.harris@outlook.com,Dubai
"""
# 查看前几行
print(df.head())  # 默认前5行
print(df.head(10))

# 查看后几行
print(df.tail())

# 随机查看几行
print(df.sample(5))

# 查看概览信息
print(df.info())

# 描述性统计(数值列)
print(df.describe())

# 查看各列的数据类型
print(df.dtypes)

# 查看数据维度
print(df.shape)
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

# 1.4 数据选择与索引

Pandas提供了多种选择数据的方法,包括基于标签的 .loc 和基于整数位置的 .iloc,以及直接使用列名和切片。

# 1.4.1 列选择

# 选择单列 -> Series
names = df['name']
print(type(names))  # <class 'pandas.Series'>

# 选择多列 -> DataFrame
subset = df[['name', 'age']]

# 通过属性访问列(列名必须是有效标识符)
# df.name # 不推荐,容易与现有方法冲突
1
2
3
4
5
6
7
8
9

# 1.4.2 行选择

使用 [] 切片(按行位置):

# 前两行
print(df[:2])

# 第1行到第3行(不含第3行)
print(df[1:3])
1
2
3
4
5

使用 .loc[](基于标签):

# 假设索引是标签,例如设置name列为索引
df_indexed = df.set_index('name')
print(df_indexed.loc['Alice Johnson'])  # 选择单行 -> Series
print(df_indexed.loc[['Alice Johnson', 'Megan White']])  # 选择多行 -> DataFrame

# 同时选择行和列
print(df_indexed.loc['Alice Johnson', 'age'])  # 单个值
print(df_indexed.loc['Alice Johnson', ['age', 'phone']])
1
2
3
4
5
6
7
8

使用 .iloc[](基于整数位置):

# 选择第0行
print(df.iloc[0])

# 选择第0行和第2行
print(df.iloc[[0, 2]])

# 选择行和列(前两行,前两列)
print(df.iloc[0:2, 0:2])
1
2
3
4
5
6
7
8

# 1.4.3 布尔索引

根据条件筛选行。

# 年龄大于25的人
print(df[df['age'] > 25])

# 多个条件
print(df[(df['age'] > 25) & (df['city'] == 'Paris')])  # 与
print(df[(df['age'] > 30) | (df['city'] == 'New York')])  # 或

# 使用isin进行成员判断
print(df[df['city'].isin(['Paris', 'London'])])
1
2
3
4
5
6
7
8
9

# 1.5 数据处理

# 1.5.1 缺失值处理

现实数据中经常存在缺失值,Pandas使用 NaN(Not a Number)表示。

# 创建含缺失值的DataFrame
df_missing = pd.DataFrame({
    'A': [1, 2, np.nan, 4],
    'B': [5, np.nan, 7, 8],
    'C': [9, 10, 11, 12]
})

# 检测缺失值
print(df_missing.isnull())
print(df_missing.isna().sum())  # 每列缺失数量

# 删除缺失值
df_dropna = df_missing.dropna()  # 删除任何包含NaN的行
df_dropna_col = df_missing.dropna(axis=1)  # 删除任何包含NaN的列
df_dropna_thresh = df_missing.dropna(thresh=2)  # 至少保留2个非空值的行

# 填充缺失值
df_fill = df_missing.fillna(0)  # 用0填充
df_fill_mean = df_missing.fillna(df_missing.mean())  # 用该列均值填充
df_ffill = df_missing.ffill()  # 前向填充
df_bfill = df_missing.bfill()  # 后向填充

# 插值
df_interpolate = df_missing.interpolate()  # 线性插值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 1.5.2 重复值处理

# 创建含重复行的DataFrame
df_dup = pd.DataFrame({
    'A': [1, 1, 2, 2, 3],
    'B': ['x', 'x', 'y', 'z', 'z']
})
print(df_dup)

# 检测重复行
print(df_dup.duplicated())  # 返回布尔Series,标记重复行(除第一次外)

# 删除重复行
df_deduplicated = df_dup.drop_duplicates()  # 保留第一次出现
df_deduplicated_last = df_dup.drop_duplicates(keep='last')  # 保留最后一次出现
df_deduplicated_subset = df_dup.drop_duplicates(subset=['A'])  # 根据特定列去重
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 1.5.3 数据类型转换

import pandas as pd

# 读取CSV文件
df = pd.read_csv('hep.csv', encoding='utf-8')

# 1. 查看原始数据类型
print("原始数据类型:")
print(df.dtypes)

# 2. 转换类型示例
# 将age从整数转换为浮点数(演示数值类型转换)
df['age'] = df['age'].astype(float)

# 将id从整数转换为字符串(ID通常作为标签,不参与计算)
df['id'] = df['id'].astype(str)

# 将city转换为分类类型,节省内存并便于分组分析
df['city'] = df['city'].astype('category')

# 1. 再次查看转换后的数据类型
print("\n转换后的数据类型:")
print(df.dtypes)

# 4. 如果需要日期转换,可以添加一列
# 例如:添加当前日期作为注册日期
# df['reg_date'] = pd.to_datetime('2025-01-01')
# 或者从某列解析日期
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

# 1.5.4 数据替换

# 替换特定值
df = df.replace(28.0, 100)  # 将28.0替换为100
df.replace({'age': {77: 78, 44: 45}},inplace=True)  # 在df的age列中,将所有值为77的单元格替换为78,将所有值为44的单元格替换为45

# 使用条件替换
df.loc[df['age'] > 30, 'age'] = 30
1
2
3
4
5
6

# 1.5.5 数据排序

# 按值排序
df_sorted = df.sort_values(by='age', ascending=False)  # 降序
print(df_sorted)
df_sorted_multi = df.sort_values(by=['age', 'name'], ascending=[True, False])
print(df_sorted_multi)
# 按索引排序
df_sorted_index = df.sort_index(ascending=False)
print(df_sorted_index)
1
2
3
4
5
6
7
8

# 1.6 数据变换

# 1.6.1 添加新列

# 直接赋值
df['age_plus_10'] = df['age'] + 10
# print(df)
# 根据现有列计算
df['name_length'] = df['name'].apply(len)
# print(df)
# 使用assign方法(返回新DataFrame,不修改原数据)
df_new = df.assign(age_squared=lambda x: x['age'] ** 2)
# print(df_new)
1
2
3
4
5
6
7
8
9

# 1.6.2 删除列/行

# 删除列
df_dropped = df.drop('age_plus_10', axis=1)  # 或axis='columns'
df.drop('age_plus_10', axis=1, inplace=True)  # 原地删除

# 删除行
df_dropped_row = df.drop([0, 2])  # 删除索引为0和2的行
1
2
3
4
5
6

# 1.6.3 应用函数

  • apply():对行或列应用函数。
  • map():对Series每个元素应用函数(用于Series)。
  • applymap():对DataFrame每个元素应用函数(用于DataFrame)。
import pandas as pd
import numpy as np

# 读取数据
df = pd.read_csv('hep.csv', encoding='utf-8')

# 1. apply作用于列
df['age_sqrt'] = df['age'].apply(np.sqrt)  # 对age列开平方
df['age_category'] = df['age'].apply(lambda x: 'young' if x < 30 else 'old')
print(df[['name', 'age', 'age_sqrt', 'age_category']])  # 查看部分列

# 2. apply作用于行 (axis=1)
df['description'] = df.apply(lambda row: f"{row['name']} is {row['age']} years old", axis=1)
print(df[['name', 'description']])

# 3. map用于Series(常与字典配合)
city_map = {'New York': 'NYC', 'Paris': 'PAR', 'London': 'LDN'}
df['city_code'] = df['city'].map(city_map)
print(df[['city', 'city_code']])

# 4. 选取数值列并标准化(向量化操作)
df_numeric = df.select_dtypes(include=[np.number])  # 选取整数/浮点数列(如id, age)
df_normalized = (df_numeric - df_numeric.mean()) / df_numeric.std()  # 直接向量化计算
print("标准化后的数值列:")
print(df_normalized)

# 如果需要将标准化后的数据合并回原DataFrame,可以用concat
df_final = pd.concat([df, df_normalized.add_suffix('_norm')], axis=1)
print(df_final.head())
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

# 1.6.4 重命名轴

# 重命名列
df.rename(columns={'age': 'Age', 'city': 'City'}, inplace=True)

# 重命名索引
df.rename(index={0: 'first', 1: 'second'}, inplace=True)

# 设置某列为索引
df.set_index('name', inplace=True)

# 重置索引(将索引变为普通列)
df.reset_index(inplace=True)
1
2
3
4
5
6
7
8
9
10
11

# 1.7 分组聚合

分组聚合是数据分析中非常常见的操作,类似于SQL的 GROUP BY。

# 1.7.1 groupby基础

# 创建示例数据
df_sales = pd.DataFrame({
    'store': ['A', 'A', 'B', 'B', 'C', 'C'],
    'product': ['apple', 'banana', 'apple', 'banana', 'apple', 'banana'],
    'sales': [100, 150, 200, 250, 300, 350],
    'date': pd.date_range('2026-09-01', periods=6, freq='D')
})
print(df_sales)
# 按单个列分组
grouped = df_sales.groupby('store')

# 查看分组对象
print(grouped)  # <pandas.api.typing.DataFrameGroupBy object at 0x1104a69f0>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 1.7.2 聚合函数

可以使用内置聚合函数或自定义聚合。

# 对分组后的数值列求和
sum_sales = grouped['sales'].sum()
print(sum_sales)
# store
# A 250
# B 450
# C 650
# Name: sales, dtype: int64

# 多个聚合函数
agg_result = grouped['sales'].agg(['sum', 'mean', 'std'])
print(agg_result)
# sum mean std
# store
# A 250 125 35.355339
# B 450 225 35.355339
# C 650 325 35.355339

# 对不同列应用不同聚合
# agg_dict是一个字典,键为列名,值为要应用的聚合函数(可以是单个函数名或函数名列表)
agg_dict = {
    'sales': ['sum', 'mean'],  #对 sales 列计算总和(sum)和平均值(mean)
    'product': 'count'  # 对product列计算计数(count),即每个商店有多少个产品记录
}
print(agg_dict)
result = grouped.agg(agg_dict)
print(result)
# sales product
# sum mean count
# store
# A 250 125.0 2
# B 450 225.0 2
# C 650 325.0 2
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

# 1.7.3 转换与过滤

转换(transform):返回与原始数据相同形状的结果。

# 计算每个商店销售额占总销售额的比例
# df_sales.groupby('store'):按store列分组,将同一商店的所有行归为一组
# ['sales']:选取分组后的sales列(销售额)
# .transform('sum'):对每组内的sales列应用求和('sum'),但 返回一个与原DataFrame行数相同、索引对齐的Series。也就是说,对于每一行,store_total的值等于该行所属商店的销售额总和
df_sales['store_total'] = df_sales.groupby('store')['sales'].transform('sum')
# 直接用每行的sales值除以该行的store_total(即所属商店的总销售额),得到该笔销售在商店内的占比,结果存入新列store_ratio
df_sales['store_ratio'] = df_sales['sales'] / df_sales['store_total']
print(df_sales)
1
2
3
4
5
6
7
8

过滤(filter):根据分组条件过滤整个组。

# 保留总销售额大于400的商店
# df_sales.groupby('store'):按store列(商店名称)对数据进行分组,每个商店的所有行形成一个组
# .filter(...):对每个组应用一个判断函数,函数返回True的组会被保留,返回False的组会被过滤掉(即该组所有行都不出现在结果中)
# lambda x: x['sales'].sum() > 400,它计算该组sales列的总和,如果总和大于400,则返回True,否则返回False
filtered = df_sales.groupby('store').filter(lambda x: x['sales'].sum() > 400)
print(filtered)
1
2
3
4
5
6

# 1.7.4 多级索引与分组

分组可以按多个列进行,产生层次化索引。

# 按store和product分组
grouped_multi = df_sales.groupby(['store', 'product'])
result_multi = grouped_multi['sales'].sum()
print(result_multi)
# store product
# A apple 100
# banana 150
# B apple 200
# banana 250
# C apple 300
# banana 350
# Name: sales, dtype: int64

# 可以unstack转换为透视表形式
pivot = result_multi.unstack()
print(pivot)
# product apple banana
# store
# A 100 150
# B 200 250
# C 300 350
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 1.8 合并与连接

Pandas提供了多种合并数据的方式:concat、merge 和 join。

# 1.8.1 concat

concat 用于沿轴方向拼接多个DataFrame。

df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
df2 = pd.DataFrame({'A': [5, 6], 'B': [7, 8]})

# 垂直拼接(增加行)
df_concat = pd.concat([df1, df2], axis=0, ignore_index=True)
print(df_concat)
# A B
# 0 1 3
# 1 2 4
# 2 5 7
# 3 6 8

# 水平拼接(增加列)
df3 = pd.DataFrame({'C': [9, 10], 'D': [11, 12]})
df_concat_h = pd.concat([df1, df3], axis=1)
print(df_concat_h)
# A B C D
# 0 1 3 9 11
# 1 2 4 10 12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 1.8.2 merge

merge 类似于SQL的JOIN操作,根据一个或多个键将行连接起来。

import pandas as pd

# 左表:键K0, K1, K2, K3
left = pd.DataFrame({
    'key': ['K0', 'K1', 'K2', 'K3'],
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3']
})

# 右表:键K0, K1, K4(缺少K2,K3,新增K4)
right = pd.DataFrame({
    'key': ['K0', 'K1', 'K4'],
    'C': ['C0', 'C1', 'C4'],
    'D': ['D0', 'D1', 'D4']
})

print("左表:")
print(left)
print("\n右表:")
print(right)

# 1. 内连接:只保留两个表都有的键(K0, K1)
inner = pd.merge(left, right, on='key', how='inner')
print("\n内连接结果(交集):")
print(inner)

# 2. 左连接:保留左表所有行,右表匹配不上的填充NaN(K2, K3的C/D为NaN)
left_join = pd.merge(left, right, on='key', how='left')
print("\n左连接结果(左表全保留):")
print(left_join)

# 3. 右连接:保留右表所有行,左表匹配不上的填充NaN(K4的A/B为NaN)
right_join = pd.merge(left, right, on='key', how='right')
print("\n右连接结果(右表全保留):")
print(right_join)

# 4. 外连接:保留所有键(K0,K1,K2,K3,K4),缺失部分填充NaN
outer = pd.merge(left, right, on='key', how='outer')
print("\n外连接结果(并集):")
print(outer)

# 5. 连接键名称不同的情况
left_renamed = left.rename(columns={'key': 'left_key'})
right_renamed = right.rename(columns={'key': 'right_key'})
merged_diff = pd.merge(left_renamed, right_renamed,
                       left_on='left_key', right_on='right_key',
                       how='inner')  # 这里以inner为例
print("\n连接键名称不同时的合并(内连接):")
print(merged_diff)
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

# 1.8.3 join

join 是基于索引的合并,实际是 merge 的一种便捷方式。

left = pd.DataFrame({'A': [1, 2]}, index=['a', 'b'])
right = pd.DataFrame({'B': [3, 4]}, index=['a', 'c'])

# 默认左连接
joined = left.join(right, how='left')
print(joined)
# A B
# a 1 3.0
# b 2 NaN

# 外连接
joined_outer = left.join(right, how='outer')
1
2
3
4
5
6
7
8
9
10
11
12

# 1.9 时间序列

Pandas对时间序列数据提供了强大的支持,包括日期范围生成、重采样、时间偏移等。

import pandas as pd
import numpy as np

# 创建示例销售数据(包含日期列)
np.random.seed(42)
date_rng = pd.date_range(start='2026-01-01', end='2026-12-31', freq='D')
df_sales = pd.DataFrame({
    'date': date_rng,
    'sales': np.random.randint(100, 500, size=len(date_rng)),
    'store': np.random.choice(['A', 'B', 'C'], size=len(date_rng))
})

# 将日期列设置为索引
df_sales['date'] = pd.to_datetime(df_sales['date'])
df_sales = df_sales.set_index('date')

# 创建时间序列
ts = pd.Series(np.random.randn(365), # 从标准正态分布(均值0、标准差1)里抽365个随机数,可正可负,大部分落在[-2, 2]
               index=pd.date_range('2026-01-01', periods=365, freq='D')) # freq='D':步长为1天

# 降采样:日 -> 月(使用新频率 'ME')
# 'ME'是pandas2.2+ 的新写法,等价于老版本的'M',表示Month End(月末)。
# resample('ME')把数据按月分桶,再对桶内做聚合
monthly_mean = ts.resample('ME').mean()
print("月均值:")
print(monthly_mean.head())

# 升采样(日->小时)并填充(使用小写 'h')
# 从"每天1个点"变"每天24个点"
# 新产生的时刻全是NaN,必须用ffill()前向填充
hourly = ts.resample('h').ffill()
print("\n升采样到小时后的前几个值:")
print(hourly.head(10))

# 多个聚合函数
monthly_stats = ts.resample('ME').agg(['mean', 'std', 'count'])
print("\n每月统计:")
print(monthly_stats.head())

# 按商店分组后重采样
# 先按商店分组,再在每组内部按各自的时间索引做月度重采样,得到(store, date)的双层MultiIndex Series
# 等价SQL的GROUP BY store, DATE_TRUNC('month', date)
store_monthly = df_sales.groupby('store').resample('ME')['sales'].mean()
print("\n各商店月均销售额:")
print(store_monthly)
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

# 1.10 数据透视表与交叉表

Pandas的 pivot_table 和 crosstab 可以快速生成类似Excel透视表的汇总数据。

import pandas as pd
import numpy as np

# 设置随机种子以便结果可复现
np.random.seed(42)

# 创建示例数据
df_sales = pd.DataFrame({
    'store': ['A', 'A', 'B', 'B', 'C', 'C'] * 2,
    'product': ['apple', 'banana', 'apple', 'banana', 'apple', 'banana'] * 2,
    'sales': np.random.randint(100, 500, 12),
    'quarter': ['Q1'] * 6 + ['Q2'] * 6
})

print("原始数据:")
print(df_sales.head(10))
print()

# 1. 基本数据透视表:按商店和产品汇总销售额
pivot = pd.pivot_table(
    df_sales,
    values='sales',
    index='store',
    columns='product',
    aggfunc='sum',
    fill_value=0,
    margins=True,  # 添加行列总计(可选)
    margins_name='总计'
)
print("基本数据透视表(商店 x 产品):")
print(pivot)
print()

# 2. 多级索引和多聚合函数
pivot_multi = pd.pivot_table(
    df_sales,
    values='sales',
    index=['store', 'quarter'],  # 行索引为商店和季度
    columns='product',
    aggfunc=['sum', 'mean'],  # 同时计算总和与平均值
    fill_value=0
)
print("多级索引与多聚合函数:")
print(pivot_multi)
print()

# 3. 交叉表(crosstab):也可以用于聚合,但通常用于频次统计
# 这里演示按商店和季度对销售额求和
crosstab = pd.crosstab(
    index=df_sales['store'],
    columns=df_sales['quarter'],
    values=df_sales['sales'],
    aggfunc='sum',
    margins=True,
    margins_name='总计'
)
print("交叉表(商店 x 季度):")
print(crosstab)
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

# 1.11 性能优化与注意事项

  • 使用向量化操作:尽量避免逐行循环,利用Pandas和NumPy的内置向量化函数。
  • 选择合适的数据类型:对于分类数据使用 category 类型可节省内存;对于数值数据,如果范围合适,可使用 int8 等更小的类型。
  • 使用 inplace 谨慎:inplace=True 可能会引起链式赋值警告,且不易调试,通常建议赋值给新变量。
  • 链式索引的陷阱:避免使用 df[df.a > 0]['b'] 这种链式索引,它可能返回视图或副本,导致警告或错误。应使用 .loc:df.loc[df.a > 0, 'b']。
  • 使用 query 方法:对于复杂条件,df.query('a > 0 & b < 5') 可读性更好。
  • 处理大数据集:当数据量过大无法装入内存时,可以考虑分块读取(pd.read_csv(..., chunksize=...))或使用Dask等工具。

Pandas是数据处理的核心工具,掌握其各种操作能够高效地进行数据清洗、转换、聚合和分析。本部分涵盖了从基础数据结构到高级数据操作的方方面面,配合代码示例,希望读者能够熟练掌握。

明天Day4见!See you tomorrow!

最近更新: 2026/9/30
 |  Copyright © -Present Kelsen All Rights Reserved. | Author KelsenJing