Filtrelenmiş ikili kartezyen ürünler üretin


12

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=3iç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())

resim açıklamasını buraya girin

Çö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_missingtablonun 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?


1
Kısıt çözücüler, arama alanlarını azaltarak bu çözümleri buldukları için muhtemelen bu yaklaşımlardan daha iyi performans gösterecektir. Belki or-aletlerine bir göz atın. İşte SAT için bir örnek.
ayhan

1
@ayhan, denedim (cevaba bakınız). Bu ilginç bir yaklaşım, ancak genel bir çözüm olarak gerçekten uygun değil. Giriş için teşekkürler. Bir şey öğrendim :)
mcsoini

Evet, bu bir SAT sorunu gibi görünüyor, bu yüzden sorun yeterince büyükse kesinlikle bir çözücü kullanmalısınız. Ayrıca deneyebilirsiniz or.stackexchange.com
Stradivari

@Stradivari formülasyonu bir SAT problemi olarak kesinlikle mantıklıdır. Yine de bu yaklaşımın filtre sayısına olan güçlü bağımlılığı sevmiyorum. Çözümlere düzgün bir şekilde erişemiyorum olabilir.
Or

Yanıtlar:


1

Aşağıdakileri zamanlamayı deneyin:

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]

Kartezyen ikili ürünleri, tamsayılar aralığında kodlanmış bitler olarak ele alır 0..<2**nve verilen filtrelerle eşleşen bit dizilerine sahip sayıları özyineli olarak kaldırmak için vectorized fonksiyonları kullanır.

Bellek verimliliği tüm [True, False]Kartezyen ürünleri tahsis etmekten daha iyidir, çünkü her bir Boolean her biri en az 8 bit ile depolanır (gerektiğinden 7 bit daha fazla), ancak yineleyici tabanlı bir yaklaşımdan daha fazla bellek kullanır. Büyük bir çözüme ihtiyacınız varsa n, bir seferde bir alt aralık tahsis ederek ve çalıştırarak bu görevi parçalayabilirsiniz. Bunu ilk uygulamamda yaptım, ancak çok fazla fayda sağlamadı n<=22ve çakışan filtreler olduğunda karmaşıklaşan çıkış dizisinin boyutunun hesaplanmasını gerektirdi.


Bu gerçekten inanılmaz!
mcsoini

1

@ Ayhan'ın yorumuna dayanarak bir or-tools SAT tabanlı çözüm uyguladım. Fikir harika olsa da, bu gerçekten çok sayıda ikili değişken için mücadele ediyor. Bu da parkta hiçbir yürüyüş değil büyük IP sorunlarına benzer şüpheli. Bununla birlikte, filtre numaralarına olan güçlü bağımlılık, bunu belirli parametre yapılandırmaları için geçerli bir seçenek haline getirebilir. Ama genel bir çözüm olarak kullanmazdım.

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

resim açıklamasını buraya girin

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.