Tạo các sản phẩm nhị phân được lọc


12

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=3chú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())

nhập mô tả hình ảnh ở đây

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_missingbả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 đó?


1
Những người giải quyết hạn chế có thể sẽ vượt trội hơn những cách tiếp cận này vì họ tìm thấy những giải pháp này bằng cách giảm không gian tìm kiếm. Có thể hãy xem hoặc công cụ. Đây là một ví dụ cho SAT.
ayhan

1
@ayhan, mình đã thử (xem câu trả lời). Đó là một cách tiếp cận thú vị, nhưng không thực sự phù hợp như một giải pháp chung. Cảm ơn các đầu vào. Tôi đã học được điều gì đó :)
mcsoini

Vâng, điều này nghe có vẻ như là một vấn đề SAT , vì vậy bạn chắc chắn nên sử dụng một bộ giải nếu vấn đề đủ lớn. Bạn cũng có thể thử or.stackexchange.com
Stradivari

@Stradivari xây dựng như một vấn đề SAT chắc chắn có ý nghĩa. Tôi không thích sự phụ thuộc mạnh mẽ vào số lượng bộ lọc của phương pháp này. Có thể là tôi không truy cập các giải pháp đúng cách. Vì bạn biết cách của mình xung quanh hoặc các công cụ, có thể bạn muốn xem câu hỏi tương ứng của tôi ... nó vẫn thiếu một câu trả lời được chấp nhận;)
mcsoini

Câu trả lời:


1

Hãy thử thời gian sau:

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]

Nó xử lý các sản phẩm nhị phân của Cartesian như các bit được mã hóa trong phạm vi số nguyên 0..<2**nvà sử dụng các hàm vectơ để loại bỏ đệ quy các số có chuỗi bit khớp với các bộ lọc đã cho.

Hiệu quả bộ nhớ tốt hơn so với việc phân bổ tất cả các [True, False]sản phẩm của Cartesian vì mỗi Boolean sẽ được lưu trữ với ít nhất 8 bit mỗi cái (sử dụng nhiều hơn 7 bit so với yêu cầu), nhưng nó sẽ sử dụng nhiều bộ nhớ hơn so với cách tiếp cận dựa trên iterator. Nếu bạn yêu cầu một giải pháp lớn n, bạn có thể chia nhỏ nhiệm vụ này bằng cách phân bổ và vận hành trên một phạm vi con tại một thời điểm. Tôi đã có điều này trong lần triển khai đầu tiên, nhưng nó không mang lại nhiều lợi ích cho n<=22nó và nó đòi hỏi việc tính toán kích thước của mảng đầu ra, điều này trở nên phức tạp khi có các bộ lọc chồng chéo.


Điều này thực sự tuyệt vời!
mcsoini

1

Dựa trên nhận xét của @ ayhan, tôi đã triển khai một giải pháp dựa trên SAT hoặc công cụ. Trong khi ý tưởng là tuyệt vời, điều này thực sự đấu tranh cho số lượng lớn hơn các biến nhị phân. Tôi nghi ngờ điều này tương tự như các vấn đề IP lớn, cũng không phải là không đi bộ trong công viên. Tuy nhiên, sự phụ thuộc mạnh mẽ vào số bộ lọc có thể làm cho điều này trở thành một tùy chọn hợp lệ cho các cấu hình tham số nhất định. Nhưng như một giải pháp chung, tôi sẽ không sử dụng nó.

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

nhập mô tả hình ảnh ở đây

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.