Повышение производительности при расчете случайной выборки, соответствующей определенным условиям в пандах - PullRequest
0 голосов
/ 12 мая 2019

Для некоторого набора данных group_1 Мне нужно перебрать все строки k раз для устойчивости и найти подходящую случайную выборку другого фрейма данных group_2 в соответствии с некоторыми критериями, выраженными в виде столбцов фрейма данных. К сожалению, это довольно медленно. Как я могу улучшить производительность?

Узким местом является функция apply, т.е. randomMatchingCondition.

import tqdm                                                                                                   
import numpy as np
import pandas as pd
from tqdm import tqdm
tqdm.pandas()

seed = 47
np.random.seed(seed)

###################################################################
# generate dummy data
size = 10000
df = pd.DataFrame({i: np.random.randint(1,100,size=size) for i in ['metric']})
df['label'] =  np.random.randint(0,2, size=size)
df['group_1'] =  pd.Series(np.random.randint(1,12, size=size)).astype(object)
df['group_2'] =  pd.Series(np.random.randint(1,10, size=size)).astype(object)

group_0 = df[df['label'] == 0]
group_0 = group_0.reset_index(drop=True)
group_0 = group_0.rename(index=str, columns={"metric": "metric_group_0"})

join_columns_enrich = ['group_1', 'group_2']
join_real = ['metric_group_0']
join_real.extend(join_columns_enrich)
group_0 = group_0[join_real]
display(group_0.head())
group_1 = df[df['label'] == 1]
group_1 = group_1.reset_index(drop=True)
display(group_1.head())

###################################################################
# naive find random element matching condition
def randomMatchingCondition(original_element, group_0, join_columns, random_state):
    limits_dict = original_element[join_columns_enrich].to_dict()
    query = ' & '.join([f"{k} == {v}" for k, v in limits_dict.items()])
    candidates = group_0.query(query)
    if len(candidates) > 0:
        return candidates.sample(n=1, random_state=random_state)['metric_group_0'].values[0]
    else:
        return np.nan
###################################################################
# iterate over pandas dataframe k times for more robust sampling
k = 3
resulting_df = None
for i in range(1, k+1):
    group_1['metric_group_0'] = group_1.progress_apply(randomMatchingCondition,
                                                                  args=[group_0, join_columns_enrich, None],
                                                                  axis = 1)
    group_1['run'] = i
    if resulting_df is None:
        resulting_df = group_1.copy()
    else:
        resulting_df = pd.concat([resulting_df, group_1])
resulting_df.head()

Эксперимент с предварительной сортировкой данных:

group_0 = group_0.sort_values(join_columns_enrich)
group_1 = group_1.sort_values(join_columns_enrich)

не показывает никакой разницы.

Ответы [ 2 ]

0 голосов
/ 12 мая 2019

@ smiandras, вы правы.Важно избавиться от цикла for.

Вариант 1: несколько выборок:

def randomMatchingCondition(original_element, group_0, join_columns, k, random_state):
    limits_dict = original_element[join_columns_enrich].to_dict()
    query = ' & '.join([f"{k} == {v}" for k, v in limits_dict.items()])
    candidates = group_0.query(query)
    if len(candidates) > 0:
        return candidates.sample(n=k, random_state=random_state, replace=True)['metric_group_0'].values
    else:
        return np.nan
###################################################################
# iterate over pandas dataframe k times for more robust sampling
k = 3
resulting_df = None

#######################
# trying to improve performance: sort both dataframes
group_0 = group_0.sort_values(join_columns_enrich)
group_1 = group_1.sort_values(join_columns_enrich)
#######################

group_1['metric_group_0'] = group_1.progress_apply(randomMatchingCondition,
                                                   args=[group_0, join_columns_enrich, k, None],
                                                   axis = 1)
print(group_1.isnull().sum())
group_1 = group_1[~group_1.metric_group_0.isnull()]
display(group_1.head())

s=pd.DataFrame({'metric_group_0':np.concatenate(group_1.metric_group_0.values)},index=group_1.index.repeat(group_1.metric_group_0.str.len()))
s = s.join(group_1.drop('metric_group_0',1),how='left')
s['pos_in_array'] = s.groupby(s.index).cumcount()
s.head()

Вариант 2: все возможные выборки оптимизированы с помощью собственной операции JOIN.

ПРЕДУПРЕЖДЕНИЕ, это немного небезопасно, поскольку может генерировать гигантское количество строк:

size = 1000
df = pd.DataFrame({i: np.random.randint(1,100,size=size) for i in ['metric']})
df['label'] =  np.random.randint(0,2, size=size)
df['group_1'] =  pd.Series(np.random.randint(1,12, size=size)).astype(object)
df['group_2'] =  pd.Series(np.random.randint(1,10, size=size)).astype(object)

group_0 = df[df['label'] == 0]
group_0 = group_0.reset_index(drop=True)
join_columns_enrich = ['group_1', 'group_2']
join_real = ['metric']
join_real.extend(join_columns_enrich)
group_0 = group_0[join_real]
display(group_0.head())
group_1 = df[df['label'] == 1]
group_1 = group_1.reset_index(drop=True)
display(group_1.head())
df = group_1.merge(group_0, on=join_columns_enrich)
display(df.head())
print(group_1.shape)
df.shape
0 голосов
/ 12 мая 2019
  1. IIUC, вы хотите получить k количество случайных выборок для каждой строки (комбинации метрик) во входном кадре данных. Так почему бы не candidates.sample(n=k, ...), а избавиться от цикла for? В качестве альтернативы вы можете объединить ваш фрейм данных k раз с pd.concat([group1] * k).

  2. Это зависит от ваших реальных данных, но я бы дал шанс для группировки входных данных по столбцам метрики с group1.groupby(join_columns_enrich) (если их мощность достаточно мала), и применил бы случайную выборку к этим группам, выбрав k * len(group.index) случайных образцов для каждого. groupby дорого, ОТО, вы можете сэкономить на итерации / выборке, как только это будет сделано.

...