Створюють відфільтровані двійкові декартові продукти


12

Постановка проблеми

Я шукаю ефективний спосіб генерування повної бінарної декартової продукції (таблиці з усіма комбінаціями True і False з певною кількістю стовпців), відфільтрованих за певними ексклюзивними умовами. Наприклад, для трьох стовпців / біт n=3ми отримали б повну таблицю

df_combs = pd.DataFrame(itertools.product(*([[True, False]] * n)))
       0      1      2
0   True   True   True
1   True   True  False
2   True  False   True
3   True  False  False
...

Це має бути відфільтровано словниками, що визначають взаємовиключні комбінації наступним чином:

mutually_excl = [{0: False, 1: False, 2: True},
                 {0: True, 2: True}]

Де ключі позначають стовпці в таблиці вище. Приклад читатиметься як:

  • Якщо 0 є помилковим, а 1 - хибним, 2 не може бути істинним
  • Якщо 0 - це правда, 2 не може бути правдою

На основі цих фільтрів очікуваний вихід:

       0      1      2
1   True   True  False
3   True  False  False
4  False   True   True
5  False   True  False
7  False  False  False

У моєму випадку використання відфільтрована таблиця на кілька порядків менша за повний декартовий продукт (наприклад, приблизно 1000 замість 2**24 (16777216)).

Нижче наведено три мої поточні рішення, кожне зі своїми плюсами та мінусами, обговорені в самому кінці.


import random
import pandas as pd
import itertools
import wrapt
import time
import operator
import functools

def get_mutually_excl(n, nfilt):  # generate random example filter
    ''' Example: `get_mutually_excl(9, 2)` creates a list of two filters with
    maximum index `n=9` and each filter length between 2 and `int(n/3)`:
    `[{1: True, 2: False}, {3: False, 2: True, 6: False}]` '''
    random.seed(2)
    return [{random.choice(range(n)): random.choice([True, False])
                           for _ in range(random.randint(2, int(n/3)))}
                           for _ in range(nfilt)]

@wrapt.decorator
def timediff(f, _, args, kwargs):
    t = time.perf_counter()
    res = f(*args)
    return res, time.perf_counter() - t

Рішення 1: Спершу відфільтруйте, а потім з’єднайте.

Розгорніть кожен окремий запис фільтра (наприклад {0: True, 2: True}) у підтаблицю зі стовпцями, відповідними індексам цього запису фільтра ( [0, 2]). Видаліть один відфільтрований рядок із цієї підтаблиці ( [True, True]). Об’єднайте повну таблицю, щоб отримати повний список відфільтрованих комбінацій.

@timediff
def make_df_comb_filt_merge(n, nfilt):

    mutually_excl = get_mutually_excl(n, nfilt)

    # determine missing (unfiltered) columns
    cols_missing = set(range(n)) - set(itertools.chain.from_iterable(mutually_excl))

    # complete dataframe of unfiltered columns with column "temp" for full outer merge
    df_comb = pd.DataFrame(itertools.product(*([[True, False]] * len(cols_missing))),
                            columns=cols_missing).assign(temp=1)

    for filt in mutually_excl:  # loop through individual filters

        # get columns and bool values of this filters as two tuples with same order
        list_col, list_bool = zip(*filt.items())

        # construct dataframe
        df = pd.DataFrame(itertools.product(*([[True, False]] * len(list_col))),
                                columns=list_col)

        # filter remove a *single* row (by definition)
        df = df.loc[df.apply(tuple, axis=1) != list_bool]

        # determine which rows to merge on
        merge_cols = list(set(df.columns) & set(df_comb.columns))
        if not merge_cols:
            merge_cols = ['temp']
            df['temp'] = 1

        # merge with full dataframe
        df_comb = pd.merge(df_comb, df, on=merge_cols)

    df_comb.drop('temp', axis=1, inplace=True)
    df_comb = df_comb[range(n)]
    df_comb = df_comb.sort_values(df_comb.columns.tolist(), ascending=False)

    return df_comb.reset_index(drop=True)

Рішення 2: Повне розширення, потім фільтруйте

Створіть DataFrame для повного декартового продукту: вся справа залишається в пам'яті. Проведіть фільтри через фільтри та створіть маску для кожного. Нанесіть кожну маску на стіл.


@timediff
def make_df_comb_exp_filt(n, nfilt):

    mutually_excl = get_mutually_excl(n, nfilt)

    # expand all bool combinations into dataframe
    df_comb = pd.DataFrame(itertools.product(*([[True, False]] * n)),
                           dtype=bool)

    for filt in mutually_excl:

        # generate total filter mask for given excluded combination
        mask = pd.Series(True, index=df_comb.index)
        for col, bool_act in filt.items():
            mask = mask & (df_comb[col] == bool_act)

        # filter dataframe
        df_comb = df_comb.loc[~mask]

    return df_comb.reset_index(drop=True)

Рішення 3: Ітератор фільтра

Зберігайте повний декартовий продукт ітератором. Цикл, перевіряючи для кожного ряду, чи не виключається жоден із фільтрів.

@timediff
def make_df_iter_filt(n, nfilt):

    mutually_excl = get_mutually_excl(n, nfilt)

    # switch to [[(1, 13), (True, False)], [(4, 9), (False, True)], ...]
    mutually_excl_index = [list(zip(*comb.items()))
                                for comb in mutually_excl]

    # create iterator
    combs_iter = itertools.product(*([[True, False]] * n))

    @functools.lru_cache(maxsize=1024, typed=True)  # small benefit
    def get_getter(list_):
        # Used to access combs_iter row values as indexed by the filter
        return operator.itemgetter(*list_)

    def check_comb(comb_inp, comb_check):
        return get_getter(comb_check[0])(comb_inp) == comb_check[1]

    # loop through the iterator
    # drop row if any of the filter matches
    df_comb = pd.DataFrame([comb_inp for comb_inp in combs_iter
                       if not any(check_comb(comb_inp, comb_check)
                                  for comb_check in mutually_excl_index)])

    return df_comb.reset_index(drop=True)

Виконайте приклади

dict_time = dict.fromkeys(itertools.product(range(16, 23, 2), range(3, 20)))

for n, nfilt in dict_time:
    dict_time[(n, nfilt)] = {'exp_filt': make_df_comb_exp_filt(n, nfilt)[1],
                             'filt_merge': make_df_comb_filt_merge(n, nfilt)[1],
                             'iter_filt': make_df_iter_filt(n, nfilt)[1]}

Аналіз

import seaborn as sns
import matplotlib.pyplot as plt

df_time = pd.DataFrame.from_dict(dict_time, orient='index',
                                 ).rename_axis(["n", "nfilt"]
                                 ).stack().reset_index().rename(columns={'level_2': 'solution', 0: 'time'})

g = sns.FacetGrid(df_time.query('n in %s' % str([16,18,20,22])),
                  col="n",  hue="solution", sharey=False)
g = (g.map(plt.plot, "nfilt", "time", marker="o").add_legend())

введіть тут опис зображення

Рішення 3 : Підхід на основі ітератора ( comb_iterator) має похмурі робочі часи, але не має значного використання пам'яті. Я вважаю, що є місце для вдосконалення, хоча неминучий цикл, ймовірно, накладає важкі межі з точки зору тривалості часу.

Рішення 2 : Розширення повного декартового продукту на DataFrame ( exp_filt) викликає значні сплески пам’яті, яких я хотів би уникнути. Хоча час роботи нормально.

Рішення 1 : Об'єднання DataFrames, створених з окремих фільтрів ( filt_merge), вважає себе гарним рішенням для мого практичного застосування (зауважте, скорочення часу роботи для більшої кількості фільтрів, що є результатом меншої cols_missingтаблиці). І все-таки такий підхід не цілком задовольняє: якщо один фільтр включає всі стовпці, весь декартовий продукт ( 2**n) залишиться в пам'яті, що робить це рішення гіршим, ніж comb_iterator.

Питання: Будь-які інші ідеї? Божевільний розумний нудний двоколісний? Чи можна якось вдосконалити підхід на основі ітератора?


1
Вирішувачі обмежень, ймовірно, перевершують ці підходи, оскільки вони знаходять ці рішення, зменшуючи простір пошуку. Можливо, погляньте на або-інструменти. Ось приклад для SAT.
Айхан

1
@ayhan, я спробував (див. відповідь). Це цікавий підхід, але не дуже підходить як загальне рішення. Дякуємо за вклад. Я щось дізнався :)
mcsoini

Так, це звучить як проблема SAT , тому вам обов'язково слід скористатися розв’язувачем, якщо проблема досить велика. Ви також можете спробувати or.stackexchange.com
Страдіварі

Формулювання @Stradivari як проблема SAT, безумовно, має сенс. Мені не подобається сильна залежність від кількості фільтрів такого підходу. Можливо, я не отримую належного доступу до рішень. Оскільки ви знаєте свій шлях навколо або інструментів, можливо, ви хочете подивитися на моє відповідне запитання ... на нього все ще немає прийнятої відповіді;)
mcsoini

Відповіді:


1

Спробуйте встановити такий час:

def in_filter(arr, arr_filt, n):
    return ((arr[:, None] >> (n-1-arr_filt[:, 0])) & 1 == arr_filt[:, 1]).all(axis=1)

def bits_to_boolean(arr, n):
    return ((arr[:, None] >> np.arange(n, dtype=arr.dtype)[::-1]) & 1).astype(bool)

@timediff
def recursive_filter(n, nfilt, dtype='uint32'):
    filts = get_mutually_excl(n, nfilt)
    out = np.arange(2**n, dtype=dtype)
    for filt in filts:
        arr_filt = np.array(list(filt.items()))
        out = out[~in_filter(out, arr_filt, n)]
    return bits_to_boolean(out, n)[::-1]

Він розглядає декартові двійкові продукти як біти, закодовані в діапазоні цілих чисел, 0..<2**nі використовує векторизовані функції для рекурсивного видалення чисел, що мають бітові послідовності, що відповідають заданим фільтрам.

Ефективність пам’яті краще, ніж розподіляти всі [True, False]декартові продукти, оскільки кожен булевий файл зберігається щонайменше по 8 біт (використовуючи 7 біт більше, ніж потрібно), але він буде використовувати більше пам’яті, ніж підхід на основі ітератора. Якщо вам потрібне рішення для великих n, ви можете розбити це завдання, розподіливши та керуючи одним піддіапазоном одночасно. У мене це було в моїй першій реалізації, але це не принесло великої користі n<=22і вимагало обчислення розміру вихідного масиву, який ускладнюється, коли є фільтри, що перекриваються.


Це справді дивовижно!
mcsoini

1

На основі коментаря @ ayhan я реалізував рішення на основі SAT на основі інструментів. Хоча ідея чудова, вона справді бореться за більшу кількість бінарних змінних. Я підозрюю, що це схоже на великі проблеми з ІР, які також не гуляють у парку. Однак сильна залежність від номерів фільтрів може зробити це дійсним варіантом для певних конфігурацій параметрів. Але як загальне рішення я би його не використовував.

from ortools.sat.python import cp_model

class VarArraySolutionCollector(cp_model.CpSolverSolutionCallback):

    def __init__(self, variables):
        cp_model.CpSolverSolutionCallback.__init__(self)
        self.__variables = variables
        self.solution_list = []

    def on_solution_callback(self):
        self.solution_list.append([self.Value(v) for v in self.__variables])


@timediff
def make_df_comb_sat(n, nfilt):

    mutually_excl = get_mutually_excl(n, nfilt)

    model = cp_model.CpModel()

    make_var_name = 'x{:02d}'.format
    vrs = dict.fromkeys(map(make_var_name, range(n)))
    for var_name in vrs:
        vrs[var_name] = model.NewBoolVar(var_name)

    for filt in mutually_excl:
        list_expr = [vrs[make_var_name(iv)]
                     if not bool_ else getattr(vrs[make_var_name(iv)], 'Not')()
                     for iv, bool_ in filt.items()]
        model.AddBoolOr(list_expr)

    solver = cp_model.CpSolver()
    solution_printer = VarArraySolutionCollector(vrs.values())
    solver.SearchForAllSolutions(model, solution_printer)

    df_comb = pd.DataFrame(solution_printer.solution_list).astype(bool)
    df_comb = df_comb.sort_values(df_comb.columns.tolist(), ascending=False)
    df_comb = df_comb.reset_index(drop=True)

    return df_comb

введіть тут опис зображення

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.