Python案例如何用Pandas做数据分组距离矩阵

wen python案例 1

本文目录导读:

Python案例如何用Pandas做数据分组距离矩阵

  1. 目录导读
  2. 为什么要用Pandas做距离矩阵?
  3. 核心概念:分组、矩阵与距离度量
  4. 前置准备:数据与库的导入
  5. 方法一:基于pdist + squareform的分组计算
  6. 方法二:自定义分组循环与scipy.spatial.distance
  7. 方法三:纯Pandas向量化技巧(高性能)
  8. 案例:电商用户行为分组距离分析
  9. 常见问题与避坑指南(含问答)
  10. 选择最适合你的分组距离方案

Python实战:用Pandas构建数据分组距离矩阵的完整指南

目录导读

  1. 为什么要用Pandas做距离矩阵?
  2. 核心概念:分组、矩阵与距离度量
  3. 前置准备:数据与库的导入
  4. 基于pdist + squareform的分组计算
  5. 自定义分组循环与scipy.spatial.distance
  6. 纯Pandas向量化技巧(高性能)
  7. 案例:电商用户行为分组距离分析
  8. 常见问题与避坑指南(含问答)
  9. 选择最适合你的分组距离方案

为什么要用Pandas做距离矩阵?

在数据分析中,我们常需要计算同一组内样本间的距离。

  • 按客户群计算消费特征距离,找出异常值
  • 按产品类别计算质量指标的相似度矩阵
  • 按时间段计算传感器读数的差异矩阵

Pandas作为Python最核心的数据处理工具,结合SciPy的pdist函数,能高效实现分组内距离矩阵的计算,相比纯循环,Pandas的groupby与矢量化操作可将代码量减少60%以上。


核心概念:分组、矩阵与距离度量

什么是距离矩阵?

距离矩阵是一个N×N的对称矩阵,其中d[i][j]表示样本ij之间的距离,对于同一组内的数据,我们关注的是组内矩阵,组间不计算。

常用距离度量

度量 适用场景 公式简写
欧氏距离 连续数值特征 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遍历每组,结合pdistsquareform

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时指定indexcolumns为原DataFrame的索引。

Q2:如果有缺失值,如何计算距离矩阵?

:可在pdist前填充或删除缺失值,推荐使用:

# 只选择非缺失行
clean = group.dropna(subset=feature_cols)

或使用pdistnan策略(需SciPy 1.6+):

condensed = pdist(values, metric='euclidean', w=None)  # 默认忽略NaN配对

Q3:大量分组(如10万组)时哪种方法最快?

:方法一最均衡,方法三虽然单组快,但构建MultiIndex的额外开销在组数极多时会拖慢整体,建议:

  • 组数多+每组样本少:方法一
  • 组数少+每组样本多:方法三

Q4:如何只返回上三角或下三角矩阵节省内存?

:使用scipy.spatial.distance.squareformforce参数或直接保留pdist的压缩形式:

condensed = pdist(values)
# 只保存压缩距离
group['_dist'] = list(condensed)  # 每个组存一个列表

选择最适合你的分组距离方案

方法 代码复杂度 性能 内存占用 灵活性
pdist+groupby
手动循环
向量化

最终建议

  • 数据量小(<1万行)且组数少:方法一最方便
  • 数据量大(>10万行)且特征少:方法三用内存换速度
  • 需要自定义距离(如DTW、马氏距离):方法二扩展性好

掌握这三种技巧,你就能在Pandas中灵活处理任何分组距离矩阵问题,推荐将方法一封装为自定义函数,作为日常工具箱的核心组件。

抱歉,评论功能暂时关闭!