拯救pandas計劃(19)——使用自定義方法計算兩列的相似度
拯救pandas計劃(19)——使用自定義方法計算兩列的相似度
最近發(fā)現(xiàn)周圍的很多小伙伴們都不太樂意使用pandas,轉而投向其他的數(shù)據(jù)操作庫,身為一個數(shù)據(jù)工作者,基本上是張口pandas,閉口pandas了,故而寫下此系列以讓更多的小伙伴們愛上pandas。
系列文章說明:
系列名(系列文章序號)——此次系列文章具體解決的需求
平臺:
windows 10 python 3.8 pandas >=1.2.4
/ 數(shù)據(jù)需求
最近在互聯(lián)網(wǎng)遨游時偶然發(fā)現(xiàn)一道很有趣的pandas實例訓練題目,題目距今已發(fā)布很久,也已經(jīng)有人為此解答過了,說:有一組數(shù)據(jù),每列分別記錄著樣品的各個指標情況,欲對每個數(shù)據(jù)各個指標進行計算,求各個樣品之間的指標差值的絕對值的總和最小的對應的幾個樣品,如:a樣品:[10.0, 9.0, 7.5, 8.6],b樣品:[11.2, 8.7, 6.4, 5.5]之間的相似度計算為:|10.0-11.2|+|9.0-8.7|+|7.5-6.4|+|8.6-5.5|。
對應位置相減取絕對值后再相加,為每個樣品對其余樣品進行計算,求得所有樣品的與其相似度最小的5個其他樣品。
根據(jù)上述描述,構造一組隨機數(shù)據(jù):
import numpy as np
import pandas as pd
np.random.seed(2022)
data = np.clip(np.random.normal(loc=70, scale=15, size=600).round(2), 0, 100).reshape(60, 10)
df = pd.DataFrame(data, columns=list('abcdefghij'), index=[f'測試_{i}' for i in range(1, 61)])
數(shù)據(jù)大小為:60*10,浮點型類型。

/ 需求處理
方法一:笛卡爾積
先計算每個樣品的交叉數(shù)據(jù),再分組獲取組內(nèi)排名在上的5個數(shù)據(jù)。
這個方法是該題目下方解答者大佬的方法,其使用的方法進行笛卡爾乘積非常有意思。
df = df.reset_index().rename(columns={'index': '名稱'})
# 設置輔助列
df['one'] = 1
# merge自身,以one列作為key參數(shù)合并數(shù)據(jù)框
df_merge = pd.merge(left=df, right=df, left_on='one', right_on='one')
原數(shù)據(jù)大小為60*10,乘積后為3600行,內(nèi)存占用大小相比處理前大了不少,如果數(shù)據(jù)過大,在處理方面,耗時上會顯著增加。

暫且不考慮數(shù)據(jù)量的影響,繼續(xù)處理。
# 剔掉名稱列和one列
columns = list(df.columns)
columns.remove('名稱')
columns.remove('one')
def sim_fun(row):
sim_value = 0.0
for col in columns:
sim_value += abs(round(row[col+'_x'] - row[col+'_y'], 2))
return round(sim_value, 2)
# 求出每個樣品之間的相似值
df_merge['sim'] = df_merge.apply(sim_fun, axis=1)
df_merge = df_merge[df_merge['名稱_x'] != df_merge['名稱_y']].copy()
# 獲取每個樣品與其相似值最小的5個樣品
def get_top_sims(df_sub):
df_sort = df_sub.sort_values('sim').head(5)
names = ','.join(df_sort['名稱_y'])
sims = ','.join(df_sort['sim'].astype(str))
return pd.Series({'names': names, 'sims': sims})
df_result = df_merge.groupby('名稱_x').apply(get_top_sims)
通過復現(xiàn)解答大佬的代碼如上,順利返回了相似度前五的數(shù)據(jù)框??梢钥吹皆?0條數(shù)據(jù)中使用笛卡爾積進行計算,耗用時間是可以接受的。

方法二:將每一行數(shù)據(jù)與原數(shù)據(jù)直接相減,利用pandas特性求和
在方法一中能注意到數(shù)據(jù)處理上稍有繁瑣,將數(shù)據(jù)擴容再處理,且數(shù)據(jù)的差值為逐個計算。仔細理解數(shù)據(jù),可以利用數(shù)組的廣播特性進行處理。
def get_sims(s):
# 注意:此處的df為全局變量
df_sim = (df - s).abs().sum(axis=1).round(2)
df_sim.pop(s.name)
df_sim = df_sim.sort_values(ascending=False).head(5).astype(str)
return pd.Series({'names': ','.join(df_sim.index), 'sims': ','.join(df_sim.values)})
# apply按行遍歷數(shù)據(jù),使其他行與該行進行數(shù)據(jù)計算
df_result = df.apply(get_sims, axis=1)
使用廣播的特性將方法一中的函數(shù)優(yōu)化為get_sims函數(shù),在計算效率上也大大提高。

/ 總結
本文通過了解題目目的及其他解答者的想法,根據(jù)自身掌握的pandas技巧對題目進行分析處理,在一定程度上減少數(shù)據(jù)計算的冗余度,滿足數(shù)據(jù)需求,如對本文有不理解之處,盡可發(fā)言表述自己的想法。
獨樂樂不如眾樂樂。
于二零二二年六月二十一日作
