Pandas GroupBy:数据分组与聚合的强大工具
Pandas GroupBy 是 Python 数据分析库 Pandas 中一个强大而灵活的功能,它允许我们对数据进行分组和聚合操作。通过 GroupBy,我们可以轻松地对数据集进行分类、汇总和分析,从而深入了解数据的结构和特征。本文将详细介绍 Pandas GroupBy 的使用方法、常见操作以及实际应用场景。
1. GroupBy 的基本概念
GroupBy 操作的核心思想是将数据按照某个或某些列的值进行分组,然后对每个分组应用特定的操作。这个过程可以分为三个步骤:
- 分割(Split):根据指定的键将数据分成不同的组。
- 应用(Apply):对每个分组应用指定的函数或操作。
- 组合(Combine):将操作结果组合成一个新的数据结构。
让我们通过一个简单的例子来理解 GroupBy 的基本用法:
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'age': [25, 30, 35, 28, 32],
'city': ['New York', 'London', 'Paris', 'New York', 'London'],
'salary': [50000, 60000, 70000, 55000, 65000]
}
df = pd.DataFrame(data)
# 按城市分组并计算平均工资
grouped = df.groupby('city')['salary'].mean()
print(grouped)
Output:
在这个例子中,我们首先创建了一个包含姓名、年龄、城市和工资信息的 DataFrame。然后,我们使用 groupby()
方法按城市分组,并计算每个城市的平均工资。
2. GroupBy 对象的创建
创建 GroupBy 对象的最常见方法是使用 DataFrame 的 groupby()
方法。这个方法可以接受多种类型的参数,包括:
- 单个列名
- 多个列名的列表
- 字典或 Series,用于将列映射到分组键
- 函数,用于从索引或列中生成分组键
让我们看几个创建 GroupBy 对象的例子:
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'age': [25, 30, 35, 28, 32],
'city': ['New York', 'London', 'Paris', 'New York', 'London'],
'department': ['Sales', 'IT', 'Marketing', 'Sales', 'IT'],
'salary': [50000, 60000, 70000, 55000, 65000]
}
df = pd.DataFrame(data)
# 按单个列分组
grouped_by_city = df.groupby('city')
# 按多个列分组
grouped_by_city_dept = df.groupby(['city', 'department'])
# 使用字典分组
group_mapping = {'Alice': 'Group A', 'Bob': 'Group B', 'Charlie': 'Group A', 'David': 'Group B', 'Eve': 'Group A'}
grouped_by_custom = df.groupby(group_mapping)
# 使用函数分组
grouped_by_age_range = df.groupby(lambda x: 'Young' if df.loc[x, 'age'] < 30 else 'Senior')
print("Group keys:", grouped_by_city.groups.keys())
print("Group keys:", grouped_by_city_dept.groups.keys())
print("Group keys:", grouped_by_custom.groups.keys())
print("Group keys:", grouped_by_age_range.groups.keys())
Output:
这些例子展示了创建 GroupBy 对象的不同方法。我们可以按单个列、多个列、自定义映射或使用函数来创建分组。
3. GroupBy 对象的基本操作
一旦创建了 GroupBy 对象,我们就可以对其执行各种操作。以下是一些常见的 GroupBy 操作:
3.1 聚合操作
聚合操作是 GroupBy 最常用的功能之一。我们可以使用内置的聚合函数或自定义函数来对分组数据进行汇总。
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'age': [25, 30, 35, 28, 32],
'city': ['New York', 'London', 'Paris', 'New York', 'London'],
'salary': [50000, 60000, 70000, 55000, 65000]
}
df = pd.DataFrame(data)
# 使用单个聚合函数
mean_salary = df.groupby('city')['salary'].mean()
print("Mean salary by city:")
print(mean_salary)
# 使用多个聚合函数
summary = df.groupby('city')['salary'].agg(['mean', 'min', 'max'])
print("\nSalary summary by city:")
print(summary)
# 使用自定义聚合函数
def salary_range(x):
return x.max() - x.min()
custom_agg = df.groupby('city')['salary'].agg(salary_range)
print("\nSalary range by city:")
print(custom_agg)
Output:
在这个例子中,我们展示了如何使用单个聚合函数、多个聚合函数以及自定义聚合函数来对分组数据进行汇总。
3.2 转换操作
转换操作允许我们对每个分组应用一个函数,并返回与原始数据具有相同索引的结果。
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'age': [25, 30, 35, 28, 32],
'city': ['New York', 'London', 'Paris', 'New York', 'London'],
'salary': [50000, 60000, 70000, 55000, 65000]
}
df = pd.DataFrame(data)
# 使用内置转换函数
normalized_salary = df.groupby('city')['salary'].transform(lambda x: (x - x.mean()) / x.std())
df['normalized_salary'] = normalized_salary
print("DataFrame with normalized salary:")
print(df)
# 使用自定义转换函数
def salary_rank(x):
return x.rank(ascending=False)
df['salary_rank'] = df.groupby('city')['salary'].transform(salary_rank)
print("\nDataFrame with salary rank:")
print(df)
Output:
这个例子展示了如何使用转换操作来计算每个城市内的标准化工资和工资排名。
3.3 过滤操作
过滤操作允许我们根据组级别的条件来选择或排除某些组。
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
'age': [25, 30, 35, 28, 32, 40],
'city': ['New York', 'London', 'Paris', 'New York', 'London', 'Paris'],
'salary': [50000, 60000, 70000, 55000, 65000, 75000]
}
df = pd.DataFrame(data)
# 过滤平均工资大于60000的城市
high_salary_cities = df.groupby('city').filter(lambda x: x['salary'].mean() > 60000)
print("Cities with high average salary:")
print(high_salary_cities)
# 过滤员工数量大于2的城市
large_cities = df.groupby('city').filter(lambda x: len(x) > 2)
print("\nCities with more than 2 employees:")
print(large_cities)
Output:
在这个例子中,我们展示了如何使用过滤操作来选择平均工资高于特定值的城市,以及员工数量超过特定阈值的城市。
4. 高级 GroupBy 操作
除了基本操作外,Pandas GroupBy 还提供了许多高级功能,使我们能够更灵活地处理分组数据。
4.1 多级索引和分组
当使用多个列进行分组时,GroupBy 会创建一个多级索引。我们可以使用这个多级索引来进行更复杂的数据分析。
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
'age': [25, 30, 35, 28, 32, 40],
'city': ['New York', 'London', 'Paris', 'New York', 'London', 'Paris'],
'department': ['Sales', 'IT', 'Marketing', 'Sales', 'IT', 'Marketing'],
'salary': [50000, 60000, 70000, 55000, 65000, 75000]
}
df = pd.DataFrame(data)
# 按城市和部门分组
grouped = df.groupby(['city', 'department'])
# 计算每个组的平均工资
mean_salary = grouped['salary'].mean()
print("Mean salary by city and department:")
print(mean_salary)
# 使用多级索引选择特定组
print("\nMean salary for IT department in London:")
print(mean_salary.loc['London', 'IT'])
# 使用 unstack() 重塑结果
unstacked = mean_salary.unstack()
print("\nUnstacked mean salary:")
print(unstacked)
Output:
这个例子展示了如何使用多个列进行分组,以及如何处理和重塑多级索引的结果。
4.2 分组迭代
GroupBy 对象可以被迭代,允许我们访问每个组的名称和数据。这在需要对每个组进行自定义操作时非常有用。
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
'age': [25, 30, 35, 28, 32, 40],
'city': ['New York', 'London', 'Paris', 'New York', 'London', 'Paris'],
'salary': [50000, 60000, 70000, 55000, 65000, 75000]
}
df = pd.DataFrame(data)
# 按城市分组
grouped = df.groupby('city')
# 迭代每个组
for name, group in grouped:
print(f"City: {name}")
print(group)
print(f"Average salary: {group['salary'].mean():.2f}")
print()
# 使用 get_group() 方法获取特定组
london_group = grouped.get_group('London')
print("London group:")
print(london_group)
Output:
这个例子展示了如何迭代 GroupBy 对象,以及如何使用 get_group()
方法获取特定的组。
4.3 分组聚合与转换的组合
有时我们需要同时执行聚合和转换操作。Pandas 提供了 agg()
和 transform()
方法的组合使用,使我们能够灵活地处理复杂的分组操作。
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
'age': [25, 30, 35, 28, 32, 40],
'city': ['New York', 'London', 'Paris', 'New York', 'London', 'Paris'],
'salary': [50000, 60000, 70000, 55000, 65000, 75000]
}
df = pd.DataFrame(data)
# 定义自定义聚合函数
def salary_range(x):
return x.max() - x.min()
# 组合聚合和转换操作
result = df.groupby('city').agg({
'salary': ['mean', 'median', salary_range],
'age': ['min', 'max']
})
# 重命名列
result.columns = ['avg_salary', 'median_salary', 'salary_range', 'min_age', 'max_age']
print("Combined aggregation result:")
print(result)
# 添加相对于组平均值的工资差异
df['salary_diff'] = df.groupby('city')['salary'].transform(lambda x: x - x.mean())
print("\nDataFrame with salary difference:")
print(df)
Output:
这个例子展示了如何组合使用聚合和转换操作,以及如何自定义列名和添加基于组的计算结果。
5. GroupBy 的性能优化
当处理大型数据集时,GroupBy 操作可能会变得很慢。以下是一些提高 GroupBy 性能的技巧:
5.1 使用 categoricals
将分组键转换为 categorical 类型可以显著提高 GroupBy 的性能,特别是当分组键的唯一值数量相对较少时。
import pandas as pd
# 创建示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'] * 1000,
'city': ['New York', 'London', 'Paris', 'Tokyo', 'Berlin'] * 1000,
'salary': [50000, 60000, 70000, 55000, 65000] * 1000
}
df = pd.DataFrame(data)
# 将 city 列转换为 categorical 类型
df['city'] = pd.Categorical(df['city'])
# 使用 categorical 列进行分组
grouped = df.groupby('city')['salary'].mean()
print("Mean salary by city (using categorical):")
print(grouped)
这个例子展示了如何将分组键转换为 categorical 类型以提高性能。虽然在这个小规模示例中可能看不出明显的性能差异,但在处理大型数据集时,这种优化可以带来显著的速度提升。
5.2 使用 numba 加速
对于自定义聚合函数,我们可以使用 numba 库来加速计算。Numba 是一个 JIT(即时编译)编译器,可以将 Python 函数编译成机器代码,从而显著提高性能。
import pandas as pd
import numpy as np
from numba import jit
# 创建示例数据
np.random.seed(0)
data = {
'group': np.random.choice(['A', 'B', 'C'], size=100000),
'value': np.random.randn(100000)
}
df = pd.DataFrame(data)
# 定义 Numba 加速的自定义聚合函数
@jit(nopython=True)
def custom_agg(values):
return np.mean(values) * np.median(values)
# 使用 Numba 加速的函数进行分组聚合
result = df.groupby('group')['value'].agg(custom_agg)
print("Result of custom aggregation:")
print(result)
这个例子展示了如何使用 Numba 来加速自定义聚合函数。虽然这个简单的函数可能看不出明显的性能提升,但对于更复杂的计算,Numba 可以带来显著的速度改善。
5.3 使用 Dask 进行并行处理
对于非常大的数据集,我们可以使用 Dask 库来进行并行处理。Dask 提供了与 Pandas 类似的 API,但可以处理超出内存大小的数据集,并支持并行计算。
import pandas as pd
import dask.dataframe as dd
# 创建大型示例数据
data = {
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'] * 1000000,
'city': ['New York', 'London', 'Paris', 'Tokyo', 'Berlin'] * 1000000,
'salary': [50000, 60000, 70000, 55000, 65000] * 1000000
}
df = pd.DataFrame(data)
# 将 Pandas DataFrame 转换为 Dask DataFrame
ddf = dd.from_pandas(df, npartitions=4)
# 使用 Dask 进行分组聚合
result = ddf.groupby('city')['salary'].mean().compute()
print("Mean salary by city (using Dask):")
print(result)
这个例子展示了如何使用 Dask 来处理大型数据集的 GroupBy 操作。Dask 可以自动将计算分散到多个 CPU 核心上,从而提高处理大数据的效率。
6. GroupBy 的实际应用场景
GroupBy 在数据分析中有广泛的应用。以下是一些常见的应用场景:
6.1 销售数据分析
import pandas as pd
import numpy as np
# 创建示例销售数据
np.random.seed(0)
data = {
'date': pd.date_range(start='2023-01-01', end='2023-12-31', freq='D'),
'product': np.random.choice(['A', 'B', 'C', 'D'], size=365),
'sales': np.random.randint(100, 1000, size=365)
}
df = pd.DataFrame(data)
# 按月份和产品分组,计算销售总额
monthly_sales = df.groupby([df['date'].dt.to_period('M'), 'product'])['sales'].sum().unstack()
print("Monthly sales by product:")
print(monthly_sales)
# 计算每个产品的累计销售额
cumulative_sales = df.groupby('product')['sales'].cumsum()
df['cumulative_sales'] = cumulative_sales
print("\nDataFrame with cumulative sales:")
print(df.head(10))
Output:
这个例子展示了如何使用 GroupBy 来分析销售数据,包括计算月度销售额和累计销售额。
6.2 客户分群分析
import pandas as pd
import numpy as np
# 创建示例客户数据
np.random.seed(0)
data = {
'customer_id': range(1000),
'age': np.random.randint(18, 80, size=1000),
'income': np.random.randint(20000, 200000, size=1000),
'purchases': np.random.randint(0, 100, size=1000)
}
df = pd.DataFrame(data)
# 定义年龄段和收入段
df['age_group'] = pd.cut(df['age'], bins=[0, 30, 50, 70, 100], labels=['18-30', '31-50', '51-70', '71+'])
df['income_group'] = pd.cut(df['income'], bins=[0, 50000, 100000, 150000, 200000], labels=['Low', 'Medium', 'High', 'Very High'])
# 按年龄段和收入段分组,计算平均购买次数
customer_segments = df.groupby(['age_group', 'income_group'])['purchases'].mean().unstack()
print("Average purchases by customer segment:")
print(customer_segments)
# 找出购买次数最多的前10个客户
top_customers = df.nlargest(10, 'purchases')
print("\nTop 10 customers by purchases:")
print(top_customers)
这个例子展示了如何使用 GroupBy 来进行客户分群分析,包括按年龄和收入分组计算平均购买次数,以及找出最有价值的客户。
6.3 时间序列数据分析
import pandas as pd
import numpy as np
# 创建示例时间序列数据
np.random.seed(0)
date_rng = pd.date_range(start='2023-01-01', end='2023-12-31', freq='H')
data = {
'timestamp': date_rng,
'temperature': np.random.normal(25, 5, size=len(date_rng)),
'humidity': np.random.normal(60, 10, size=len(date_rng))
}
df = pd.DataFrame(data)
# 计算每日平均温度和湿度
daily_avg = df.groupby(df['timestamp'].dt.date).agg({
'temperature': 'mean',
'humidity': 'mean'
})
print("Daily average temperature and humidity:")
print(daily_avg.head())
# 计算每月最高温度和最低温度
monthly_extremes = df.groupby(df['timestamp'].dt.to_period('M')).agg({
'temperature': ['max', 'min']
})
print("\nMonthly temperature extremes:")
print(monthly_extremes)
# 计算滚动7天平均温度
df['rolling_temp'] = df.groupby(df['timestamp'].dt.date)['temperature'].transform(lambda x: x.rolling(7).mean())
print("\nDataFrame with 7-day rolling average temperature:")
print(df.head(10))
这个例子展示了如何使用 GroupBy 来分析时间序列数据,包括计算每日平均值、月度极值和滚动平均值。
7. GroupBy 的注意事项和最佳实践
在使用 Pandas GroupBy 时,有一些注意事项和最佳实践可以帮助我们更有效地使用这个功能:
- 选择合适的分组键:分组键的选择会直接影响分析的结果和性能。尽量选择具有合理数量唯一值的列作为分组键。
-
使用
agg()
方法进行多个聚合:当需要对多个列执行不同的聚合操作时,使用agg()
方法可以提高代码的可读性和效率。 -
注意内存使用:GroupBy 操作可能会消耗大量内存,特别是在处理大型数据集时。如果遇到内存问题,考虑使用 Dask 或其他大数据处理工具。
-
利用
transform()
方法:当需要将聚合结果广播回原始 DataFrame 时,transform()
方法比手动合并更高效。 -
使用
filter()
方法进行条件筛选:当需要基于组级别的条件来筛选数据时,filter()
方法比先聚合再筛选更高效。 -
注意 NaN 值的处理:GroupBy 操作默认会排除 NaN 值。如果需要包含 NaN 值,可以使用
dropna=False
参数。 -
使用
apply()
方法进行复杂操作:当内置的聚合函数无法满足需求时,可以使用apply()
方法来执行自定义的复杂操作。 -
优化性能:对于大型数据集,考虑使用 categoricals、Numba 或 Dask 来提高性能。
-
合理使用多级索引:多级索引可以提供更细粒度的分组,但也可能增加复杂性。根据实际需求权衡使用。
-
利用
groupby()
的as_index
参数:设置as_index=False
可以将结果作为普通 DataFrame 返回,而不是使用分组键作为索引。
总结
Pandas GroupBy 是一个强大的数据分析工具,它允许我们轻松地对数据进行分组、聚合和转换。通过本文的详细介绍,我们了解了 GroupBy 的基本概念、常见操作、高级功能以及实际应用场景。我们还探讨了如何优化 GroupBy 的性能,以及使用时的注意事项和最佳实践。
掌握 Pandas GroupBy 可以大大提高我们处理和分析结构化数据的能力。无论是进行简单的数据汇总,还是复杂的多维分析,GroupBy 都能为我们提供灵活而强大的解决方案。通过不断实践和探索,我们可以充分发挥 GroupBy 的潜力,从数据中获取更多有价值的洞察。