필터링 된 이진 카티 전 곱 생성


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이 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)).

아래에는 각각의 장단점이있는 세 가지 현재 솔루션이 맨 끝에 설명되어 있습니다.


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있습니다.

질문 : 다른 아이디어가 있습니까? 미친 똑똑한 numpy 2 강선? 어떻게 반복자 기반 접근 방식을 개선 할 수 있습니까?


1
제약 조건 솔버는 검색 공간을 줄임으로써 이러한 솔루션을 찾기 때문에 이러한 접근 방식보다 성능이 뛰어납니다. 또는 도구를 살펴보십시오. 다음 은 SAT의 예입니다.
ayhan

1
@ayhan, 시도했습니다 (답변 참조). 흥미로운 접근 방식이지만 일반적인 솔루션으로는 적합하지 않습니다. 입력 주셔서 감사합니다. 나는 무언가를 배웠다 :)
mcsoini

예, 이것은 SAT 문제 처럼 들리 므로 문제가 충분히 큰 경우 솔버를 사용해야합니다. or.stackexchange.com
Stradivari

SAT 문제로서 @Stradivari 공식은 확실히 의미가 있습니다. 나는이 접근법의 필터 수에 대한 강한 의존성을 좋아하지 않습니다. 솔루션에 올바르게 액세스하지 못했을 수 있습니다. 당신은 당신의 방법의 주위에 또는-도구를 알고 있기 때문에, 어쩌면 당신이 내 해당 살펴 갖고 싶어 질문을 ... 그것은 여전히 허용 대답을 결여)
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]

Cartesian 바이너리 제품을 정수 범위로 인코딩 된 비트로 취급하고 0..<2**n벡터화 된 함수를 사용하여 주어진 필터와 일치하는 비트 시퀀스를 가진 숫자를 재귀 적으로 제거합니다.

[True, False]각 부울이 각각 8 비트 이상 (필수보다 7 비트 더 많이 사용됨)으로 저장되므로 메모리 효율성은 모든 데카르트 제품을 할당하는 것보다 낫지 만 반복기 기반 방식보다 더 많은 메모리를 사용합니다. 큰 솔루션이 필요한 경우 n한 번에 하나의 하위 범위를 할당하고 운영하여이 작업을 분류 할 수 있습니다. 나는 첫 번째 구현에서 이것을 가지고 있었지만 많은 이점을 제공하지 않았고 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.