![](/img/trans.png)
[英]How to distribute N rows into X groups and to attribute a value D in PySpark?
[英]How to find the best way to distribute N observations into M groups?
我會盡量以最清晰的方式解釋我的問題。 假設我們有df
dataframe:
import pandas as pd
users = ['a','b','c','d','e','f','g','h', 'a','b','c','g','h', 'b','c','d','e']
groups = ['g1']*8 + ['g2']*5 + ['g3']*4
scores = [0.54, 0.02, 0.78, 0.9 , 0.98, 0.27, 0.25, 0.98, 0.47, 0.02, 0.8, 0.51, 0.28, 0.53, 0.01, 0.51, 0.6 ]
df = pd.DataFrame({'user': users,
'group': groups,
'score': scores}).sort_values('score', ascending=False)
這將返回如下內容:
user group score
7 h g1 0.98
4 e g1 0.98
3 d g1 0.90
10 c g2 0.80
2 c g1 0.78
16 e g3 0.60
0 a g1 0.54
13 b g3 0.53
11 g g2 0.51
15 d g3 0.51
8 a g2 0.47
12 h g2 0.28
5 f g1 0.27
6 g g1 0.25
1 b g1 0.02
9 b g2 0.02
14 c g3 0.01
每個用戶在屬於每個組時都有一定的分數。 問題是每個組可以有有限數量的成員。 這些數字存儲在字典中:
members = {'g1': 3,
'g2': 2,
'g3': 1}
這就是問題所在:我必須選擇將用戶分組的最佳方式,同時考慮到他們的分數和每個組可以托管的用戶數量。
如果我們看一下上面的 dataframe,將用戶分配到組的最佳方法是以下一種:
g1
的h
、 e
和d
的分數。 假設g1
最多可以容納 3 個成員,則將這三個用戶分配給它。 現在g1
不能再接受任何成員了。g2
的c
的成績。 因此g2
現在還剩下一個插槽。c
,但是這個用戶已經被分配了,所以不能分配兩次。 因此,它必須被忽略。 下面的情況也是如此,它將e
(已分配給g1)
與g3
相關聯。a
與g1
相關的,但該組已滿。 因此,它也必須被忽略。我找到的解決方案是這個:
final = pd.DataFrame([])
# As long as there are non-assigned users and groups with free slots...
while len(df):
# Take the first row (i.e. the best score of the rows left)
i = df.first_valid_index()
# If there are free slots...
if members[df.loc[i,'group']] > 0:
# Subtract 1 from the slots left of this group
members[df.loc[i,'group']] -= 1
# Append this row to the 'final' DataFrame
final = final.append(df.loc[i])
# Delete all rows belonging to this user, as it was already assigned
df = df.loc[df.user != df.loc[i,'user']]
# If the group has no free slots left...
else:
# Delete all rows belonging to this group, as it is already full
df = df.loc[df.group != df.loc[i,'group']]
final = final.groupby('group').agg({'user': ['unique','count']})
這將返回以下 DataFrame:
user
unique count
group
g1 [h, d, f] 3
g2 [c, g] 2
g3 [b] 1
問題是:這段代碼在現實生活中運行需要很長時間。 我有超過 2000 萬不同的用戶,大約有 10 個不同的組要填充。 所以這種方法真的不可行。
有沒有更有效的方法來做到這一點? 如有必要,我願意采取次優解決方案。 也就是說,將幾乎最好的用戶分配給每個組......如果這是有道理的。
不完全是答案,但評論時間太長了。
對 2000 萬個數據集進行排序應該不會花費那么長時間,並且它之后的所有內容都應該在線性時間內運行。 我有一種預感,刪除會變得非常昂貴,特別是df = df.loc[...]
行。 讓我們假設您有 20M 用戶,每個用戶出現兩次,因此有 40M 行。 每個用戶將被刪除一次。 如果每個用戶刪除掃描整個 DataFrame,那就是 20M 刪除,平均剩余 20M 行,所以 400*10^12 操作。
您可以在不刪除任何內容的情況下實現相同的算法,每掃描行的時間為 O(1)。 只需為每個用戶保留一個“已分配”位(在較低級別的語言中,您將擁有一個 boolean 數組)。 當您分配一個用戶時,將其位設置為 1。對於每一行,檢查該組是否有剩余點並且該用戶未分配。 現在不需要刪除; 自然會跳過分配了用戶的行。
抱歉,我對 Python 不夠流利,無法提供代碼。
這就是我要做的:
df = df.pivot_table(index='user', columns='group', values='score').reset_index().fillna(0)
final = {}
df['sum'] = df.loc[:, 'g1':].sum(axis=1)
for group in members.keys():
df[group] = df[group] / df['sum']
for group in members.keys():
df = df.sort_values(group, ascending=False)
final[group] = list(df.head(members[group])['user'])
df = df.iloc[members[group]:, :]
final
Output:
{'g1': ['f', 'h', 'd'], 'g2': ['g', 'c'], 'g3': ['b']}
說明:對於每個用戶,我正在計算他與任何組的相關性,以及所有組的相關性。 然后每個組都會獲得最適合該組的用戶,然后我刪除這些用戶,並對其他組執行相同的操作。
下面是按照 Cătălin Frâncu 建議的嘗試(使用 numpy 而不是 pandas)
這是根據您的要求顯示調度的簡化版本。
可以直接訪問ref
數組(而不是使用映射(在OP
中的user_id
))
我沒有按分數排序(很少有人感興趣)
調度似乎減慢了大約 9M 很可能是因為所有用戶都已被調度
n_users = 1e5
大約需要3s
,而1e7
我不知道我之前退出了。
def OP():
groups = [0,3,2,1] #respectively group
ref = []
users = ['a','b','c','d','e','f','g','h']
user_id = {}
for i in range(len(users)):
user_id[users[i]] = i
ref.append(False)
entries = []
entries.append(('h',1,'0.98'))
entries.append(('e',1,'0.98'))
entries.append(('d',1,'0.90'))
entries.append(('c',2,'0.80'))
entries.append(('c',1,'0.78'))
entries.append(('e',3,'0.60'))
entries.append(('a',1,'0.54'))
entries.append(('b',3,'0.53'))
entries.append(('g',2,'0.51'))
entries.append(('d',3,'0.51'))
entries.append(('a',2,'0.47'))
entries.append(('h',2,'0.28'))
entries.append(('f',1,'0.27'))
entries.append(('g',1,'0.25'))
entries.append(('b',1,'0.02'))
entries.append(('b',2,'0.02'))
entries.append(('c',3,'0.01'))
out = []
for u,g,s in entries:
if ref[user_id[u]] == True:
continue
if groups[g] > 0:
groups[g]-=1
out.append((u,g,s))
ref[user_id[u]] = True
print(out)
#[('h', 1, '0.98'), ('e', 1, '0.98'), ('d', 1, '0.90'), ('c', 2, '0.80'), ('b', 3, '0.53'), ('g', 2, '0.51')]
def test():
import numpy as np
n_users = int(1e7)
n_groups = 10
groups = [3,1e6,1e7,1e6,1e6,1e6,1e6,1e6,1e6,1e6]
print('allocating array')
N = n_users * n_groups
dscores = np.random.random((N,1))
dusers = np.random.randint(0, n_users, (N,1))
dgroups = np.random.randint(0, n_groups, (N,1))
print('building ref')
ref = np.zeros(n_users, dtype=int)
print('hstack')
entries = np.hstack((dusers, dgroups, dscores))
print('dispatching')
out = np.zeros((n_users, 3))
z = 0
counter = 0
for e in entries:
counter += 1
if counter % 1e6 == 0:
print('ccc', counter)
u,g,s = e
u = int(u)
g = int(g)
if ref[u] == 1:
continue
if groups[g] > 0:
groups[g]-=1
out[z][0] = u
out[z][1] = g
out[z][2] = s
ref[u] = 1
z += 1
if z % 1e5==0:
print('z : ', z)
print('done')
OP()
test()
我絕不是 Numba 的專家,這可能會更慢。 但我過去曾成功使用 Numba 和循環編寫復雜的算法。 如果您有大量數據,您可能需要將 int8 更改為更大的數據類型。
import pandas as pd
import numpy as np
import numba
# Basic setup:
users = ['a','b','c','d','e','f','g','h', 'a','b','c','g','h', 'b','c','d','e']
groups = ['g1']*8 + ['g2']*5 + ['g3']*4
scores = [0.54, 0.02, 0.78, 0.9 , 0.98, 0.27, 0.25, 0.98, 0.47, 0.02, 0.8, 0.51, 0.28, 0.53, 0.01, 0.51, 0.6 ]
df = pd.DataFrame({'user': users,
'group': groups,
'score': scores}).sort_values('score', ascending=False)
# Convert user, groups and limits to numbers:
df['user'] = df.user.astype('category')
df['group'] = df.group.astype('category')
df['usercat'] = df.user.cat.codes
df['groupcat'] = df.group.cat.codes
member_mapping_temp = dict( enumerate(df['group'].cat.categories ) )
members = {'g1': 3,
'g2': 2,
'g3': 1}
member_map = np.array([(x,members.get(y)) for x,y in member_mapping_temp.items()])
# Define numba njit function to solve problem:
from numba import types
from numba.typed import Dict, List
int_array = types.int8[:]
@numba.njit()
def calc_scores(numpy_array, member_map):
member_map_limits = Dict.empty(
key_type=types.int8,
value_type=types.int8,
)
member_count = Dict.empty(
key_type=types.int8,
value_type=types.int8,
)
memeber_list = []
for ix in range(len(member_map)):
group = member_map[ix,0]
limit = member_map[ix,1]
member_map_limits[group] = limit
member_count[group] = 0
seen_users = set()
for ix in range(len(numpy_array)):
user = numpy_array[ix,0]
group = numpy_array[ix,1]
if user in seen_users:
continue
if member_map_limits[group] == member_count[group]:
continue
member_count[group] = member_count[group] + 1
memeber_list.append((group,user))
seen_users.add(user)
return memeber_list
# Call function:
res = calc_scores(df[['usercat','groupcat']].to_numpy(), member_map)
# Add result to DF
res = pd.DataFrame(res, columns=['group','member'])
# Map back to values
res['group'] = pd.Categorical.from_codes(codes=res['group'], dtype=df['group'].dtype)
res['member'] = pd.Categorical.from_codes(codes=res['member'], dtype=df['user'].dtype)
請讓我知道這在真實數據集上是否更快。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.