Sorun bildirimi
Tam ikili kartezyen ürünler (doğru ve yanlış tüm sütunları ile belirli sayıda sütun içeren tablolar), belirli özel koşullara göre filtre oluşturmak için verimli bir yol arıyorum. Örneğin, üç sütun / bit n=3
için tam tabloyu alırız
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
...
Bunun, birbirini dışlayan kombinasyonları tanımlayan sözlükler tarafından filtrelenmesi beklenir:
mutually_excl = [{0: False, 1: False, 2: True},
{0: True, 2: True}]
Anahtarlar yukarıdaki tabloda yer alan sütunları gösterir. Örnek şu şekilde okunur:
- 0 yanlış ve 1 yanlışsa, 2 doğru olamaz
- 0 Doğru ise, 2 Doğru olamaz
Bu filtrelere dayanarak, beklenen çıktı:
0 1 2
1 True True False
3 True False False
4 False True True
5 False True False
7 False False False
Benim kullanım durumumda, filtrelenmiş tablo, tam kartezyen üründen daha küçük çokluk büyüklüğündedir (örneğin, bunun yerine 1000 2**24 (16777216)
).
Aşağıda, her biri kendi artıları ve eksileri olan üç güncel çözümüm en sonunda tartışıldı.
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
Çözüm 1: Önce filtreleyin, ardından birleştirin.
Her bir filtre girişini (örneğin {0: True, 2: True}
), bu filtre girişindeki ( [0, 2]
) dizinlere karşılık gelen sütunlarla bir alt tabloya genişletin . Filtrelenen tek satırı bu alt tablodan ( [True, True]
) kaldırın . Filtrelenmiş kombinasyonların tam listesini almak için tam tablo ile birleştirin.
@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)
Çözüm 2: Tam genişleme, ardından filtre
Tam kartezyen ürün için DataFrame oluşturun: Her şey hafızada sona erer. Filtreler arasında geçiş yapın ve her biri için bir maske oluşturun. Her maskeyi masaya uygulayın.
@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)
Çözüm 3: Filtre Yineleyici
Kartezyen ürünün tamamını bir yineleyici olarak saklayın. Filtrelerin herhangi biri tarafından hariç tutulup tutulmadığını kontrol ederken her satırı kontrol edin.
@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)
Örnekleri çalıştır
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]}
analiz
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())
Çözüm 3 : Yineleyici tabanlı yaklaşımın ( comb_iterator
) kasvetli çalışma süreleri vardır, ancak önemli miktarda bellek kullanımı yoktur. Kaçınılmaz döngü muhtemelen çalışma süresi açısından zor sınırlar getirmesine rağmen, iyileştirme için yer olduğunu hissediyorum.
Çözüm 2 : Tam kartezyen ürünü bir DataFrame ( exp_filt
) içine genişletmek, kaçınmak istediğim bellekte önemli artışlara neden olur. Çalışma süreleri olsa Tamam.
Çözüm 1 : Tek tek filtrelerden ( filt_merge
) oluşturulan DataFrames birleştirmek pratik uygulamam için iyi bir çözüm gibi geliyor (daha küçük cols_missing
tablonun bir sonucu olan daha fazla sayıda filtre için çalışma süresindeki azalmaya dikkat edin ). Yine de, bu yaklaşım tamamen tatmin edici değildir: Tek bir filtre tüm sütunları içeriyorsa, tüm kartezyen ürün ( 2**n
) bellekte kalır ve bu çözümü daha da kötüleştirir comb_iterator
.
Soru: Başka fikir var mı? Çılgın bir akıllı numpy iki katlı? Yineleyici tabanlı yaklaşım bir şekilde geliştirilebilir mi?