Как создать взвешенную случайную выборку с ограниченным бюджетом, где элементы имеют различные вероятности и веса? - PullRequest
0 голосов
/ 03 января 2019

Предположим, я хочу выбрать две записи из набора из трех, где вероятности этих трех равны 0,1, 0,5 и 0,4 соответственно. За этот SO ответ , numpy.random.choice будет работать:

import pandas as pd
from numpy import random

df = pd.DataFrame({'prob': [0.1, 0.5, 0.4]})

random.seed(0)
random.choice(df.index, p=df.prob, size=2, replace=False)
# array([1, 2])

Теперь предположим, что у каждого предмета также есть вес, и вместо того, чтобы выбирать два предмета, я хочу выбрать максимальный вес. Таким образом, если эти элементы имеют вес 4, 5 и 6, а у меня бюджет 10, я мог бы выбрать {0, 1} или {0, 2}. Относительные вероятности каждого включаемого элемента все еще будут зависеть от вероятностей (хотя на практике я думаю, что алгоритм будет возвращать элемент 1 чаще, поскольку его малый вес может служить наполнителем).

Есть ли способ адаптировать random.choice для этого или другой подход для получения этого результата?

Ответы [ 2 ]

0 голосов
/ 03 января 2019

То, что вы можете сделать, - это использовать np.random.choice с такими же вероятностями, как у вас, но для полного размера ваших данных. Затем reindex df с новым заказом, полученным от np.random.choice. Используйте cumsum для веса столбца и, наконец, возвращайте только индекс, пока он не достигнет желаемого значения.

def weighted_budgeted_random_sample_all(df, budget):
   random_index_order = np.random.choice( df.index, size = len(df), 
                                          p = df.prob, replace = False)
   s = df.reindex(random_index_order).weight.cumsum()
   return s[s <= budget].index.values

Теперь проблема этого метода заключается в том, что при df, как в вопросе, и budget из 10, некоторые решения имеют только индекс 1 или 2, потому что если random_index_order равно [2,1,0] или [1,2,0], тогда cumsum выше 10 во втором ряду.

См. С Counter, использование tuple и np.sort просто для того, чтобы Counter заработало и чтобы было легче увидеть результат:

from collections import Counter
print (Counter([ tuple(np.sort(weighted_budgeted_random_sample_all(df,10))) 
                 for i in range(1000)]))
# Counter({(0, 1): 167, (0, 2): 111, (1,): 390, (2,): 332})

Как вы можете видеть, некоторые розыгрыши были в порядке с 2 и 3 в качестве первых 2 значений, и результат равен только 2 или 3, потому что сумма их весов равна 11.

Но на самом деле, если вы попробуете то же самое с бюджетом 11, вы получите ожидаемый результат:

print (Counter([ tuple(np.sort(weighted_budgeted_random_sample_all(df,11))) 
                 for i in range(1000)]))
# Counter({(0, 1): 169, (0, 2): 111, (1, 2): 720})

Здесь вы найдете три возможных набора и тот факт, что набор {1,2} получается чаще, имеет смысл.

Я видел, что вы пересмотрели свой вопрос после комментария, в котором говорилось, что вы будете работать над подходом «один элемент за один раз» . Я верю, что это повлияет на общие вероятности, но я не знаю достаточно вероятностей, чтобы сказать, почему. Если вы действительно хотите, то я предполагаю, что вы можете объединить свой подход и мой, чтобы выиграть время:

def weighted_budgeted_random_sample_mixed(df, budget):
    ids = []
    total = 0
    dftemp = df.copy()
    while total < budget:
        remaining = budget - total
        dftemp = dftemp[dftemp.weight <= remaining]
        # Stop if there are no records with small enough weight.
        if dftemp.shape[0] == 0:
            break
        # New order
        new_index = np.random.choice( dftemp.index, size = len(dftemp), 
                                      p = (dftemp.prob/dftemp.prob.sum()), 
                                      replace = False)
        s = dftemp.reindex(new_index).weight.cumsum()
        #select only the necessary rows
        s = s[s <= remaining] 
        total += s.max() #last value in s which is less than remaining
        dftemp.drop(s.index, inplace=True)
        ids += s.index.tolist()
    return ids

Теперь для сравнения с вашим методом с точки зрения результата:

#your approach
print (Counter([ tuple(np.sort(weighted_budgeted_random_sample(df,10))) 
                 for i in range(1000)]))
#Counter({(0, 1): 546, (0, 2): 454})

#mixed approach
print (Counter([ tuple(np.sort(weighted_budgeted_random_sample_mixed(df,10))) 
                 for i in range(1000)])
#Counter({(0, 1): 554, (0, 2): 446})

Как вы можете видеть, результат довольно схожий, и смешанный подход должен быть быстрее на больших фреймах данных, поскольку он минимизирует зацикливание в while

0 голосов
/ 03 января 2019

Вот индивидуальный подход:

  1. Получить набор предметов с весом ниже бюджета.
  2. Выберите случайный предмет из этого набора в соответствии с вероятностью каждого.
  3. Добавьте это в рабочий список и удалите его из набора доступных элементов.
  4. Повторяйте 1-3, пока ни один из оставшихся элементов не сможет заполнить пробел между начисленным весом и бюджетом.

Вот функция для этого, которая, как и ожидалось, производит только наборы {0, 1} и {0, 2} в примере:

def weighted_budgeted_random_sample(df, budget):
    """ Produce a weighted budgeted random sample.

    Args:
        df: DataFrame with columns for `prob` and `weight`.
        budget: Total weight budget.

    Returns:
        List of index values of df that constitute the sample.

    """
    ids = []
    total = 0
    while total < budget:
        remaining = budget - total
        df = df[df.weight <= remaining]
        # Stop if there are no records with small enough weight.
        if df.shape[0] == 0:
            break
        # Select one record.
        selection = random.choice(df.index, p=(df.prob / df.prob.sum()))
        total += df.loc[selection].weight
        df.drop(selection, inplace=True)
        ids.append(selection)
    return ids

Пример:

df = pd.DataFrame({
    'weight': [4, 5, 6],
    'prob': [0.1, 0.5, 0.8]
})

weighted_budgeted_random_sample(df, 10)
# [2, 0]

Вероятно, это можно оптимизировать, начав с random.choice для ряда элементов, которые не будут ограничены бюджетом.

...