本文目录导读:

- 目录导读
- 为什么要用Pandas做距离矩阵?
- 核心概念:分组、矩阵与距离度量
- 前置准备:数据与库的导入
- 方法一:基于
pdist+squareform的分组计算 - 方法二:自定义分组循环与
scipy.spatial.distance - 方法三:纯Pandas向量化技巧(高性能)
- 案例:电商用户行为分组距离分析
- 常见问题与避坑指南(含问答)
- 选择最适合你的分组距离方案
Python实战:用Pandas构建数据分组距离矩阵的完整指南
目录导读
- 为什么要用Pandas做距离矩阵?
- 核心概念:分组、矩阵与距离度量
- 前置准备:数据与库的导入
- 基于
pdist+squareform的分组计算 - 自定义分组循环与
scipy.spatial.distance - 纯Pandas向量化技巧(高性能)
- 案例:电商用户行为分组距离分析
- 常见问题与避坑指南(含问答)
- 选择最适合你的分组距离方案
为什么要用Pandas做距离矩阵?
在数据分析中,我们常需要计算同一组内样本间的距离。
- 按客户群计算消费特征距离,找出异常值
- 按产品类别计算质量指标的相似度矩阵
- 按时间段计算传感器读数的差异矩阵
Pandas作为Python最核心的数据处理工具,结合SciPy的pdist函数,能高效实现分组内距离矩阵的计算,相比纯循环,Pandas的groupby与矢量化操作可将代码量减少60%以上。
核心概念:分组、矩阵与距离度量
什么是距离矩阵?
距离矩阵是一个N×N的对称矩阵,其中d[i][j]表示样本i和j之间的距离,对于同一组内的数据,我们关注的是组内矩阵,组间不计算。
常用距离度量
| 度量 | 适用场景 | 公式简写 |
|---|---|---|
| 欧氏距离 | 连续数值特征 | sqrt(sum((x-y)^2)) |
| 曼哈顿距离 | 高维稀疏数据 | sum(abs(x-y)) |
| 余弦距离 | 文本或方向向量 | 1 - cos(x,y) |
Pandas本身不直接提供距离计算,但结合scipy.spatial.distance可完美实现。
前置准备:数据与库的导入
import pandas as pd
import numpy as np
from scipy.spatial.distance import pdist, squareform
from itertools import combinations
# 示例数据:不同城市的气温与湿度记录
data = {
'city': ['Beijing', 'Beijing', 'Beijing',
'Shanghai', 'Shanghai', 'Shanghai',
'Guangzhou', 'Guangzhou'],
'temp': [22, 25, 20, 28, 30, 27, 32, 33],
'humidity': [45, 50, 40, 70, 75, 65, 80, 85]
}
df = pd.DataFrame(data)
print(df)
输出:
city temp humidity
0 Beijing 22 45
1 Beijing 25 50
2 Beijing 20 40
3 Shanghai 28 70
4 Shanghai 30 75
5 Shanghai 27 65
6 Guangzhou 32 80
7 Guangzhou 33 85
目标:计算每个城市内部样本间的欧氏距离矩阵。
方法一:基于pdist + squareform的分组计算
这是最经典的方法,利用groupby遍历每组,结合pdist和squareform。
def group_distance_matrix(group):
# 只取数值列
numeric_cols = group.select_dtypes(include=[np.number]).values
if len(numeric_cols) < 2:
return None # 不足两个样本返回空
# 计算压缩距离,再转为方阵
condensed = pdist(numeric_cols, metric='euclidean')
matrix = squareform(condensed)
# 以原索引命名行和列
return pd.DataFrame(matrix, index=group.index, columns=group.index)
result = df.groupby('city', group_keys=False).apply(group_distance_matrix)
print(result)
输出示例(北京组):
0 1 2
0 0.0 5.83 5.39
1 5.83 0.0 10.30
2 5.39 10.30 0.0
优点:代码简洁,直接利用成熟的优化库。
缺点:每组需单独调用pdist,组数多时会略慢。
方法二:自定义分组循环与scipy.spatial.distance
如果需要更灵活控制(例如自定义距离函数或只计算上三角),手动循环更直观。
all_matrices = {}
for city, group in df.groupby('city'):
numeric = group[['temp', 'humidity']].values
n = len(numeric)
dist_matrix = np.zeros((n, n))
for i, j in combinations(range(n), 2):
# 自定义曼哈顿距离
d = np.sum(np.abs(numeric[i] - numeric[j]))
dist_matrix[i][j] = d
dist_matrix[j][i] = d
all_matrices[city] = pd.DataFrame(dist_matrix,
index=group.index,
columns=group.index)
# 合并结果
result_manual = pd.concat(all_matrices.values(), keys=all_matrices.keys())
优点:可完全控制计算逻辑(如加权距离)。
缺点:纯循环在大数据量下效率低。
方法三:纯Pandas向量化技巧(高性能)
对于特征列较少的情况,可以预先构造所有点对,利用广播计算,这种方法避免循环,性能最佳。
def fast_group_distance(df, group_col='city', feature_cols=['temp', 'humidity']):
# 添加组内序号,用于配对
df = df.copy()
df['_idx'] = range(len(df))
groups = df.groupby(group_col)
all_results = []
for name, group in groups:
n = len(group)
if n < 2:
continue
# 生成所有点对组合(笛卡尔积)
keys = group['_idx'].values
# 交叉连接(自连接)
comb = pd.MultiIndex.from_product([range(n), range(n)], names=['i', 'j'])
pair_df = group.iloc[comb.get_level_values(0)][feature_cols].reset_index(drop=True)
pair_df2 = group.iloc[comb.get_level_values(1)][feature_cols].reset_index(drop=True)
# 计算欧氏距离(向量化)
diff = pair_df - pair_df2
distance = np.sqrt((diff ** 2).sum(axis=1))
# 重塑为矩阵
matrix = distance.values.reshape(n, n)
all_results.append(pd.DataFrame(matrix, index=keys, columns=keys))
return pd.concat(all_results)
result_fast = fast_group_distance(df)
优点:全程矢量化,速度极快。
缺点:当组内样本数很大时,临时矩阵会膨胀(n²大小),需注意内存。
案例:电商用户行为分组距离分析
假设我们有1000个用户,按level分为VIP、普通、新客三类,需要计算每类用户的购买频率和客单价的组内距离矩阵,用于识别异常用户。
# 模拟数据
np.random.seed(42)
n = 1000
users = pd.DataFrame({
'level': np.random.choice(['VIP', 'Normal', 'New'], size=n, p=[0.2,0.5,0.3]),
'freq': np.random.poisson(lam=5, size=n),
'amount': np.random.exponential(scale=100, size=n)
})
# 计算每组距离矩阵(方法一)
result_big = users.groupby('level', group_keys=False).apply(
lambda g: pd.DataFrame(
squareform(pdist(g[['freq', 'amount']], metric='euclidean')),
index=g.index, columns=g.index))
# 找出组内最异常的点(与组内其他点平均距离最大)
def find_anomalies(dist_matrix):
avg_dist = dist_matrix.mean(axis=1)
return avg_dist.nlargest(3).index.tolist()
anomalies = users.groupby('level').apply(
lambda g: find_anomalies(result_big.loc[g.index, g.index]))
输出示例:发现VIP用户中索引为35、102、287的用户与其他VIP用户差异最大,可能为异常值。
常见问题与避坑指南(含问答)
Q1:为什么squareform返回的矩阵索引不对?
答:pdist返回的是压缩距离,squareform只还原矩阵形状,不会自动匹配索引,请务必在groupby内部重新构建DataFrame时指定index和columns为原DataFrame的索引。
Q2:如果有缺失值,如何计算距离矩阵?
答:可在pdist前填充或删除缺失值,推荐使用:
# 只选择非缺失行 clean = group.dropna(subset=feature_cols)
或使用pdist的nan策略(需SciPy 1.6+):
condensed = pdist(values, metric='euclidean', w=None) # 默认忽略NaN配对
Q3:大量分组(如10万组)时哪种方法最快?
答:方法一最均衡,方法三虽然单组快,但构建MultiIndex的额外开销在组数极多时会拖慢整体,建议:
- 组数多+每组样本少:方法一
- 组数少+每组样本多:方法三
Q4:如何只返回上三角或下三角矩阵节省内存?
答:使用scipy.spatial.distance.squareform的force参数或直接保留pdist的压缩形式:
condensed = pdist(values) # 只保存压缩距离 group['_dist'] = list(condensed) # 每个组存一个列表
选择最适合你的分组距离方案
| 方法 | 代码复杂度 | 性能 | 内存占用 | 灵活性 |
|---|---|---|---|---|
| pdist+groupby | ||||
| 手动循环 | ||||
| 向量化 |
最终建议:
- 数据量小(<1万行)且组数少:方法一最方便
- 数据量大(>10万行)且特征少:方法三用内存换速度
- 需要自定义距离(如DTW、马氏距离):方法二扩展性好
掌握这三种技巧,你就能在Pandas中灵活处理任何分组距离矩阵问题,推荐将方法一封装为自定义函数,作为日常工具箱的核心组件。