跨时间窗口K-means聚类匹配:基于企业重叠度的编号对齐
实现跨月度滚动窗口的K-means聚类匹配与编号调整
我来给你梳理一下具体的实现思路和代码,完全贴合你提出的跨窗口聚类匹配需求——核心就是先量化聚类间的企业重叠度,再根据规则建立对应关系,最后调整聚类编号让跨窗口的同类聚类保持一致编号。
第一步:数据预处理,提取每个聚类的企业集合
首先我们需要把每个(窗口, 聚类)对应的企业列表转成集合,这样后续计算交集会非常高效。假设你的原始DataFrame名为df,代码如下:
import pandas as pd import numpy as np from itertools import product # 把窗口转成可排序的月度周期格式(比如'2023-01'转成Period类型) df['Window'] = pd.to_datetime(df['Window']).dt.to_period('M') # 按窗口和聚类分组,提取每个聚类的企业集合 cluster_firm_sets = df.groupby(['Window', 'Cluster'])['Company'].apply(set).reset_index() # 按窗口时间排序,确保后续处理是按滚动顺序来的 cluster_firm_sets = cluster_firm_sets.sort_values('Window').reset_index(drop=True)
第二步:定义匹配度计算函数
我们需要一个函数来计算窗口t的聚类和窗口t+1的聚类之间的匹配分数,也就是t聚类中同时出现在t+1聚类里的企业占比:
def calculate_match_ratio(firms_t, firms_t1): # 避免空聚类导致的除以0错误 if len(firms_t) == 0: return 0.0 # 计算t聚类企业在t1聚类中的占比 return len(firms_t & firms_t1) / len(firms_t)
第三步:遍历窗口对,建立聚类对应关系
接下来我们要逐个处理连续的窗口对(t和t+1),计算所有聚类间的匹配分数,然后根据≥60%的规则找到对应关系,同时处理可能的冲突(比如多个t聚类匹配同一个t+1聚类):
# 获取所有按时间排序的唯一窗口 sorted_windows = sorted(cluster_firm_sets['Window'].unique()) # 存储最终的聚类映射:键是(窗口, 原聚类编号),值是调整后的统一聚类编号 cluster_mapping = {} # 初始化第一个窗口的映射:聚类编号保持不变 first_window = sorted_windows[0] first_clusters = cluster_firm_sets[cluster_firm_sets['Window'] == first_window]['Cluster'].unique() for clus in first_clusters: cluster_mapping[(first_window, clus)] = clus # 从第二个窗口开始,逐个和前一个窗口匹配 for idx in range(1, len(sorted_windows)): curr_window = sorted_windows[idx] prev_window = sorted_windows[idx-1] # 提取前窗口和当前窗口的聚类数据 prev_cluster_data = cluster_firm_sets[cluster_firm_sets['Window'] == prev_window] curr_cluster_data = cluster_firm_sets[cluster_firm_sets['Window'] == curr_window] # 构建匹配矩阵:行=前窗口聚类,列=当前窗口聚类,值=匹配分数 match_matrix = pd.DataFrame( index=prev_cluster_data['Cluster'], columns=curr_cluster_data['Cluster'], dtype=np.float64 ) # 填充匹配矩阵的所有元素 for prev_clus, curr_clus in product(prev_cluster_data['Cluster'], curr_cluster_data['Cluster']): prev_firms = prev_cluster_data[prev_cluster_data['Cluster'] == prev_clus]['Company'].iloc[0] curr_firms = curr_cluster_data[curr_cluster_data['Cluster'] == curr_clus]['Company'].iloc[0] match_matrix.loc[prev_clus, curr_clus] = calculate_match_ratio(prev_firms, curr_firms) # 按匹配分数从高到低排序,优先处理分数高的匹配,避免冲突 sorted_matches = match_matrix.unstack().reset_index(name='score').sort_values('score', ascending=False) # 存储前窗口聚类到当前窗口聚类的对应关系,同时记录已被使用的当前聚类 prev_to_curr = {} used_curr_clusters = set() for _, row in sorted_matches.iterrows(): prev_clus = row['Cluster'] curr_clus = row['level_0'] score = row['score'] # 只保留满足≥60%、未被匹配过的对应关系 if prev_clus not in prev_to_curr and curr_clus not in used_curr_clusters and score >= 0.6: prev_to_curr[prev_clus] = curr_clus used_curr_clusters.add(curr_clus) # 构建当前窗口的聚类编号映射:原编号 → 调整后的统一编号(对应前窗口的编号) curr_cluster_map = {} unused_prev_clusters = set(prev_cluster_data['Cluster']) - set(prev_to_curr.keys()) for curr_clus in curr_cluster_data['Cluster']: # 找到当前聚类对应的前窗口聚类编号 matched_prev_clus = next((k for k, v in prev_to_curr.items() if v == curr_clus), None) if matched_prev_clus is not None: curr_cluster_map[curr_clus] = matched_prev_clus else: # 如果没有匹配到,分配一个未被使用的前窗口编号,避免重复 if unused_prev_clusters: curr_cluster_map[curr_clus] = unused_prev_clusters.pop() else: # 极端情况:所有前窗口编号都被用了,保留原编号 curr_cluster_map[curr_clus] = curr_clus # 把当前窗口的映射存入总字典 for curr_clus, new_clus in curr_cluster_map.items(): cluster_mapping[(curr_window, curr_clus)] = new_clus
第四步:将调整后的聚类编号应用到原始数据
最后我们把调整后的聚类编号添加到原始DataFrame中,同时可以生成新的Adjusted_WindClus字段:
# 添加调整后的聚类编号 df['Adjusted_Cluster'] = df.apply( lambda row: cluster_mapping[(row['Window'], row['Cluster'])], axis=1 ) # 生成新的窗口-聚类拼接字段 df['Adjusted_WindClus'] = df['Window'].astype(str) + '_' + df['Adjusted_Cluster'].astype(str)
关键细节说明
- 冲突处理:通过按匹配分数从高到低排序,优先分配分数最高的对应关系,避免一个当前聚类被多个前聚类抢占的情况。
- 未匹配聚类:对于没有满足≥60%重叠度的聚类,我们分配一个未被使用的前窗口编号,确保跨窗口的聚类编号始终是1-10的连续值(如果你的原始聚类编号是1-10的话)。
- 效率优化:用集合来计算企业交集,比列表遍历快得多,适合处理大量企业数据的场景。
内容的提问来源于stack exchange,提问作者Pierrick
相关产品推荐
相关产品推荐

