Báo cáo vấn đề
Tôi đang tìm kiếm một cách hiệu quả để tạo ra các sản phẩm cartesian nhị phân đầy đủ (các bảng có tất cả kết hợp Đúng và Sai với một số cột nhất định), được lọc theo các điều kiện độc quyền nhất định. Ví dụ: đối với ba cột / bit, n=3
chúng ta sẽ có được bảng đầy đủ
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
...
Điều này được cho là được lọc bởi các từ điển xác định các kết hợp loại trừ lẫn nhau như sau:
mutually_excl = [{0: False, 1: False, 2: True},
{0: True, 2: True}]
Trong đó các phím biểu thị các cột trong bảng trên. Ví dụ sẽ được đọc là:
- Nếu 0 là Sai và 1 là Sai, 2 không thể đúng
- Nếu 0 là True, 2 không thể là True
Dựa trên các bộ lọc này, đầu ra dự kiến là:
0 1 2
1 True True False
3 True False False
4 False True True
5 False True False
7 False False False
Trong trường hợp sử dụng của tôi, bảng được lọc có nhiều đơn đặt hàng có độ lớn nhỏ hơn sản phẩm cartesian đầy đủ (ví dụ: khoảng 1000 thay vì 2**24 (16777216)
).
Dưới đây là ba giải pháp hiện tại của tôi, mỗi giải pháp đều có ưu và nhược điểm riêng, được thảo luận ở phần cuối.
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
Giải pháp 1: Lọc trước, sau đó hợp nhất.
Mở rộng từng mục bộ lọc đơn (ví dụ {0: True, 2: True}
) thành một bảng phụ với các cột tương ứng với các chỉ mục trong mục bộ lọc này ( [0, 2]
). Xóa hàng được lọc khỏi bảng phụ này ( [True, True]
). Hợp nhất với bảng đầy đủ để có được danh sách đầy đủ các kết hợp được lọc.
@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)
Giải pháp 2: Mở rộng hoàn toàn, sau đó lọc
Tạo DataFrame cho sản phẩm cartesian đầy đủ: Toàn bộ mọi thứ kết thúc trong bộ nhớ. Lặp qua các bộ lọc và tạo mặt nạ cho mỗi bộ lọc. Áp dụng mỗi mặt nạ vào bảng.
@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)
Giải pháp 3: Bộ lọc lặp
Giữ cho sản phẩm cartesian đầy đủ một vòng lặp. Lặp lại trong khi kiểm tra từng hàng xem nó có bị loại trừ bởi bất kỳ bộ lọc nào không.
@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)
Chạy ví dụ
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]}
Phân tích
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())
Giải pháp 3 : Cách tiếp cận dựa trên iterator ( comb_iterator
) có thời gian chạy ảm đạm, nhưng không sử dụng bộ nhớ đáng kể. Tôi cảm thấy có chỗ để cải thiện, mặc dù vòng lặp không thể tránh khỏi có thể áp đặt các giới hạn cứng về thời gian chạy.
Giải pháp 2 : Việc mở rộng toàn bộ sản phẩm cartesian thành DataFrame ( exp_filt
) gây ra các đột biến đáng kể trong bộ nhớ, điều mà tôi muốn tránh. Thời gian chạy là ok mặc dù.
Giải pháp 1 : Hợp nhất DataFrames được tạo từ các bộ lọc riêng lẻ ( filt_merge
) tạo cảm giác giống như một giải pháp tốt cho ứng dụng thực tế của tôi (lưu ý việc giảm thời gian chạy cho số lượng bộ lọc lớn hơn, là kết quả của cols_missing
bảng nhỏ hơn ). Tuy nhiên, cách tiếp cận này không hoàn toàn thỏa mãn: Nếu một bộ lọc duy nhất bao gồm tất cả các cột, toàn bộ sản phẩm cartesian ( 2**n
) sẽ kết thúc trong bộ nhớ, làm cho giải pháp này tồi tệ hơn comb_iterator
.
Câu hỏi: Có ý tưởng nào khác không? Một điên hai numpy thông minh? Cách tiếp cận dựa trên iterator có thể được cải thiện bằng cách nào đó?