フィルタリングされたバイナリデカルト積を生成する


12

問題文

特定の排他的条件でフィルタリングされた完全なバイナリデカルト積(TrueとFalseの特定の数の列のすべての組み合わせを含むテーブル)を生成する効率的な方法を探しています。たとえば、3列/ビットの場合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がFalseで1がFalseの場合、2をTrueにすることはできません
  • 0がTrueの場合、2をTrueにすることはできません

これらのフィルターに基づいて、予想される出力は次のとおりです。

       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))。

以下は、3つの現在のソリューションで、それぞれ最後に議論されたそれぞれの長所と短所があります。


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:個々のフィルター(filt_merge)から作成されたDataFrameをマージすることは、私の実際のアプリケーションにとっては良い解決策のように感じられます(フィルターの数が増えると実行時間が短縮されcols_missingます。これはテーブルが小さいためです)。それでも、このアプローチは完全に満足できるものではありません。単一のフィルターにすべての列が含まれている場合、デカルト積(2**n)全体がメモリに格納され、この解決策はより悪くなりcomb_iteratorます。

質問:他のアイデアはありますか?クレイジーでスマートな派手なツーライナー?イテレータベースのアプローチをどうにかして改善できますか?


1
制約ソルバーは、検索スペースを削減することでこれらのソリューションを見つけるため、おそらくこれらのアプローチよりも優れています。多分or-toolsを見てください。ここだ SATのための一例。
ayhan

1
@ayhan、私は試しました(回答を参照)。これは興味深いアプローチですが、一般的な解決策としてはあまり適していません。入力いただきありがとうございます。私は何かを学びました:)
mcsoini

ええ、これはSATの問題のように聞こえるので、問題が十分に大きい場合は、必ずソルバーを使用する必要があります。or.stackexchange.comを
Stradivari

SAT問題としての@ストラディバリの定式化は間違いなく理にかなっています。ただし、このアプローチのフィルターの数への強い依存は好きではありません。ソリューションに適切にアクセスしていない可能性があります。or-toolsの使い方がわかっているので、多分私の対応する質問を確認したいと思うかもしれません...それでも受け入れられた回答が欠けています;)
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一度に1つのサブ範囲を割り当てて操作することにより、このタスクを分解できます。私はこれを最初の実装で使用しましたが、あまりメリットがなくn<=22、出力配列のサイズの計算が必要でした。フィルターが重複している場合は複雑になります。


これは本当に素晴らしいです!
mcsoini

1

@ayhanのコメントに基づいて、or-tools SATベースのソリューションを実装しました。アイデアは素晴らしいですが、これはバイナリ変数の数が増えると本当に苦労します。これは大規模なIPの問題に似ていると思います。これは、公園内を歩いているわけでもありません。ただし、フィルター番号への強い依存性により、これは特定のパラメーター構成に対して有効なオプションになる可能性があります。しかし、一般的な解決策として、私はそれを使用しません。

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

ここに画像の説明を入力してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.