簡體   English   中英

如何找到將 N 個觀測值分配到 M 個組中的最佳方法?

[英]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,將用戶分配到組的最佳方法是以下一種:

  1. 最高分是分配給屬於g1hed的分數。 假設g1最多可以容納 3 個成員,則將這三個用戶分配給它。 現在g1不能再接受任何成員了。
  2. 以下最好成績是分配給屬於g2c的成績。 因此g2現在還剩下一個插槽。
  3. 注意下面的分數也是指c ,但是這個用戶已經被分配了,所以不能分配兩次。 因此,它必須被忽略。 下面的情況也是如此,它將e (已分配給g1)g3相關聯。
  4. 以下是ag1相關的,但該組已滿。 因此,它也必須被忽略。
  5. 該過程必須打開 go 直到所有組都已滿,或者直到沒有更多的行可以填充組(在這種情況下,一些組將有空閑插槽)。

我找到的解決方案是這個:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM