문제 설명
특정 배타적 조건으로 필터링 된 전체 이진 카티 전 곱 (특정 수의 열이있는 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 강선? 어떻게 반복자 기반 접근 방식을 개선 할 수 있습니까?