Генерация комбинаций значений с пределом количества появлений значений - PullRequest
1 голос
/ 30 апреля 2020

Моя задача:

  1. создать список уникальных комбинаций, где каждая комбинация имеет определенную длину (var com_len) и содержит значения (целые) из заданного списка (значения var),
  2. каждая комбинация создается путем взятия случайных значений из данного списка (случайность очень важна!),
  3. каждая комбинация должна иметь уникальные значения внутри, никакое значение не может повторяться внутри комбинации,
  4. значения в комбинации должны быть отсортированы,
  5. подсчитывается появление каждого значения в наборе целых комбинаций (счетчик переменных),
  6. каждое значение должно появляться во всем наборе данных как близко к данному числу раз, насколько это возможно (var counter_expected). «как можно ближе» означает, что подсчитывать каждое появляющееся значение по ходу сценария и, если для создания больше не осталось комбинаций, просто завершить сценарий.

Например, мне нужно создать список комбинаций, где каждая комбинация имеет длину 3, имеет уникальные отсортированные значения внутри, каждое значение находится в диапазоне (256), и каждое значение появляется во всех комбинациях, сгенерированных настолько близко к 100 раз, насколько это возможно.

Моя проблема в том, насколько эффективно обнаружить, что больше нет уникальных комбинаций значений, которые можно создать, чтобы остановить l oop.

Проблема возникает, когда сценарий подходит к концу, и все еще остаются доступные значения и len (available_values)> com_len, но невозможно создать какие-либо новые уникальные комбинации, которые еще не появились.

Код создано до сих пор:

import numpy as np
import random

com_len = 3
length = 256
counter = np.zeros(length)
values = range(length)
exclude_values = []
counter_expected = 100
done = []

mask = np.ones(len(np.array(values)), np.bool)
mask[exclude_values] = False
available_values = set(values) - set(exclude_values)
available_values = list(available_values)

ii = 0
while True:
    """print progress"""
    ii = ii + 1
    if not ii % 1000: print('.', end='')

    #POSSIBLE CONDITION HERE
    ex = random.sample(available_values, k=com_len)
    ex.sort()
    if ex in done: continue
    done.append(ex)

    counter[ex] = counter[ex] + 1

    for l_ in ex:
        if counter[l_] == counter_expected:
            del available_values[available_values.index(l_)]

    if len(available_values) < com_len:break

    if all(counter[mask] == counter_expected): break
    #OR HERE

ПРИМЕЧАНИЕ. Сценарий обычно успешно завершается, потому что len (available_values) = com_len, но больше нет новых уникальных условий, доступных для создания, поэтому счетчик не

Мне нужно эффективное условие, чтобы остановить скрипт. Использование itertools.combinsk здесь не вариант, потому что список available_values ​​может быть длинным, то есть 10k элементов, в начале.

Грубая сила будет использовать itertools.combination, когда len (available_values) достигает определенного уровня, и проверять, есть ли комбинации, которые еще не были созданы, но это уродливое решение.

Возможно, есть лучший способ, который подходит не мне, а вам. Я буду благодарен за вашу помощь.

Ответы [ 3 ]

1 голос
/ 30 апреля 2020

Все еще в поисках лучшего решения, но используя решение грубой силы, я пришел к этому:

import numpy as np
import random
import itertools

com_len = 3
length = 256
counter = np.zeros(length)
values = range(length)
exclude_values = []
counter_expected = 100
done = []

mask = np.ones(len(np.array(values)), np.bool)
mask[exclude_values] = False
available_values = set(values) - set(exclude_values)
available_values = list(available_values)

ii = 0
while True:
    """print progress"""
    ii = ii + 1
    if not ii % 1000: print('.', end='')

    """BRUTE-FORCE SOLUTION, CHECK ALL COMBIONATIONS LEFT WHEN 
    len(available_values) REACHES CERTAIN LEVEL, com_len * 2"""
    if len(available_values) <= com_len * 2:
        comb_left = set(list(itertools.combinations(available_values, com_len))) - set(map(tuple, done))
        comb_left = list(comb_left)
        if len(comb_left) == 0:
            break

    ex = random.sample(available_values, k=com_len)
    ex.sort()
    if ex in done: continue
    done.append(ex)

    counter[ex] = counter[ex] + 1

    for l_ in ex:
        if counter[l_] == counter_expected:
            del available_values[available_values.index(l_)]

    if len(available_values) < com_len:break

    if all(counter[mask] == counter_expected): break

print('')
print(available_values)
print(counter[np.where(counter[mask] < counter_expected)])

Может кто-нибудь придумать лучшее решение? Спасибо!

0 голосов
/ 01 мая 2020

Вот еще один ответ, сфокусированный на случайном аспекте.

У вас есть n выбор k возможностей, которые вы хотите выбрать случайным образом, чтобы получить приблизительно z вхождений каждого значения (используя мою запись из другой ответ). Я полагаю, что если вы возьмете образцы размером (n * z) // k k, и ваш генератор случайных чисел на самом деле равномерен, вы автоматически получите примерно z вхождений каждого элемента. В вашем примере, с n=256, k=3 и z=100, вполне вероятно, что среди 8533 распределение действительно будет довольно равномерным среди 256 бинов.

Если вы готовы принять некоторый уровень несовершенство в однородности, python х random.sample - хороший выбор. Все население - целые числа от нуля до n выберите k.

n выберите k в этом случае 256 * 255 * 254 / 6 = 2763520. Это просто вне диапазона для 32-разрядного целого числа со знаком, но удобно вписывается в беззнаковое. А еще лучше, вы можете просто использовать целые числа бесконечной точности Python.

Хитрость заключается в том, чтобы сопоставить эти числа с уникальной комбинацией значений. Это делается с помощью комбинаторной системы счисления , как описано здесь .

from random import sample
from scipy.misc import combs

def gen_samples(n, k, z):
    codes = sample(range(combs(n, k)), (n * z) // k)
    return [unrank(n, k, code) for code in codes]

def unrank(n, k, i):
    """
    Implementation of Lehmer's greedy algorithm left as
    exercise for the reader
    """
    # return k-element sequence

См. здесь для подсказок о разблокировке.

0 голосов
/ 30 апреля 2020

В общей сложности n выберите k возможных комбинаций, соответствующих вашим критериям, с n = length и k = com_len. n choose k оценивается как n! / (k! * (n - k)!). Если вы генерируете все различные возможности, каждое из значений n появляется (n - 1)! / ((k - 1)! * (n - k)!) раз (https://math.stackexchange.com/q/26619/295281). Вы должны быть в состоянии решить эту проблему, предполагая, что z <= (n - 1)! / ((k - 1)! * (n - k)!), где z = counter_expected.

Для вашего примера:

  • n = 256
  • k = 3
  • z = 100 <= 32385

Одним из распространенных способов генерации комбинаций в целом является пошаговое выполнение k битов через логический массив длины n, всегда увеличивая наименьший возможный бит. Всякий раз, когда увеличивается старший бит, все нижние биты сбрасываются в исходное положение. Вот пример последовательности:

0 0 0 0 3 2 1
0 0 0 3 0 2 1
0 0 0 3 2 0 1
0 0 0 3 2 1 0
0 0 3 0 0 2 1
...
3 2 0 0 1 0 0
3 2 0 1 0 0 0
3 2 1 0 0 0 0

Я пометил позиции, чтобы вы могли видеть, что, если значения отсортированы для начала, комбинации всегда будут выходить отсортированными. Имейте в виду, что вы можете реализовать это как массив n логических значений или k индексов. Оба имеют свои преимущества и недостатки.

Для вашего конкретного случая использования есть поворот. Вы не используете немного, если счет превысил определенное количество. Есть несколько способов пройти через биты, но все они сводятся к наличию счетного массива размером n.

Если n * z кратно k, вы автоматически сможете чтобы получить точные подсчеты во всех корзинах. Ни n, ни z сами по себе не должны быть кратны k. Однако, если это не так, у вас неизбежно будут переполнение или переполнение. Интуитивно понятно, что вы хотите сгенерировать цель из n * z общих значений, k за раз. Совершенно очевидно, что для того, чтобы это стало возможным, нужно умножить число на последующие.

Вы можете иметь два типа критериев выхода. Учитывая общее накопленное количество всех битов, s,

  1. s >= n * z: все биты имеют счет не менее z. Максимум k - 1 битов имеют счетчик z + 1.
  2. s > n * z - k: все биты имеют счетчик z, кроме максимум k - 1 битов, поэтому добавление еще одной комбинации приведет к условию 1.

Одним из последних вариантов дизайна, который необходимо обсудить, является порядок перемещения битов. Поскольку генерация серии комбинаций исчерпывает ячейку, я хотел бы иметь возможность сохранять накопленные исчерпывающие ячейки последовательно, в предсказуемом порядке на одной стороне корзины. Это удалит много проверок из алгоритма. Таким образом, вместо увеличения минимально возможного бита, я буду увеличивать максимальный возможный бит и увеличивать тот, который находится ниже, всякий раз, когда он сбрасывается. В этом случае исчерпанные сегменты всегда будут самыми младшими битами.

Итак, давайте, наконец, прекратим делать недоказанные математически звучащие утверждения и покажем реализацию:

def generate_combos(n, k, z):
    full_locs = np.arange(k + 1, dtype=np.uint)
    full_locs[k] = n                      # makes partial vectorization easier
    locs = full_locs[:k]                  # bit indices
    counts = np.zeros(n, dtype=np.uint)   # counter buckets
    values = np.arange(n, dtype=np.uint)  # values
    min_index = 0                         # index of lowest non-exhausted bin

    for _ in range((n * z) // k):
        counts[locs] += 1
        yield values[locs]

        if counts[min_index] == z:
            # if lowest bin filled, shift and reset
            min_index += np.argmax(counts[min_index:] < z)
            locs[:] = min_index + np.arange(k)
        else:
            # otherwise, increment highest available counter
            i = np.flatnonzero(np.diff(full_locs) > 1)
            if i.size:
                i = i[-1]
                locs[i] += 1
                # reset the remainder
                locs[i + 1:] = locs[i] + np.arange(1, k - i)
            else:
                break

Используется условие 2. Если Вы хотите условие 1, добавьте следующие строки после:

if counters[-1] < z:
    yield values[-k:]

Изменение l oop на что-то вроде for _ in range(-((n * z) // -k)): (любезность { ссылка }) не поможет, потому что счетчики не предназначены для его обработки.

Вот ссылка IDEOne , показывающая первые сто элементов generate_combos(256, 3, 10):

[0 1 2]
[0 1 3]
[0 1 4]
[0 1 5]
[0 1 6]
[0 1 7]
[0 1 8]
[0 1 9]
[ 0  1 10]
[ 0  1 11]
[2 3 4]
[2 3 5]
[2 3 6]
[2 3 7]
[2 3 8]
[2 3 9]
[ 2  3 10]
[ 2  3 11]
[ 2  3 12]
[4 5 6]
[4 5 7]
[4 5 8]
[4 5 9]
[ 4  5 10]
[ 4  5 11]
[ 4  5 12]
[ 4  5 13]
[6 7 8]
[6 7 9]
[ 6  7 10]
[ 6  7 11]
[ 6  7 12]
[ 6  7 13]
[ 6  7 14]
[ 8  9 10]
[ 8  9 11]
[ 8  9 12]
[ 8  9 13]
[ 8  9 14]
[ 8  9 15]
[10 11 12]
[10 11 13]
[10 11 14]
[10 11 15]
[10 11 16]
[12 13 14]
[12 13 15]
[12 13 16]
[12 13 17]
[12 13 18]
[13 14 15]
[14 15 16]
[14 15 17]
[14 15 18]
[14 15 19]
[14 15 20]
[15 16 17]
[16 17 18]
[16 17 19]
[16 17 20]
[16 17 21]
[16 17 22]
[16 17 23]
[17 18 19]
[18 19 20]
[18 19 21]
[18 19 22]
[18 19 23]
[18 19 24]
[18 19 25]
[19 20 21]
[20 21 22]
[20 21 23]
[20 21 24]
[20 21 25]
[20 21 26]
[20 21 27]
[21 22 23]
[22 23 24]
[22 23 25]
[22 23 26]
[22 23 27]
[22 23 28]
[22 23 29]
[24 25 26]
[24 25 27]
[24 25 28]
[24 25 29]
[24 25 30]
[24 25 31]
[24 25 32]
[26 27 28]
[26 27 29]
[26 27 30]
[26 27 31]
[26 27 32]
[26 27 33]
[26 27 34]
[28 29 30]
[28 29 31]
...

Обратите внимание, что после первые 10 элементов, 0 и 1, появились 10 раз. 2 и 3 появились один раз, поэтому они привыкли только после еще 9 итераций и т. Д.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...