Повышение эффективности функции ранжирования путем замены лямбда x на векторизация

У меня есть функция ранжирования, которую я применяю к большому количеству столбцов из нескольких миллионов строк, для чего требуется несколько минут. Удалив всю логику, готовящую данные для применения .rank( метод, т. .rank( образом:

 ranked = df[['period_id', 'sector_name'] + to_rank].groupby(['period_id', 'sector_name']).transform(lambda x: (x.rank(ascending = True) - 1)*100/len(x)) 

Мне удалось довести это до нескольких секунд. Тем не менее, мне нужно сохранить свою логику и изо всех сил пытаюсь перестроить свой код: в конечном итоге самым большим узким местом является мое двойное использование лямбда x :, но, очевидно, другие аспекты замедляют работу (см. Ниже). Я предоставил образец кадра данных вместе с моими ранжирующими функциями ниже, то есть MCVE. В целом, я думаю, что мои вопросы сводятся к:

(i) Как заменить приложение .apply(lambda x использование .apply(lambda x в коде с быстрым, векторизованным эквивалентом?) (ii) Как можно циклически перебирать мультииндексированные, сгруппированные, фреймы данных и применять функцию? в моем случае, чтобы каждая уникальная комбинация столбцов date_id и категории.
(iii) Что еще я могу сделать, чтобы ускорить мою рейтинговую логику? основные накладные расходы, по-видимому, находятся в .value_counts() . Это перекрывается с (i) выше; возможно, большую часть этой логики можно использовать для df, возможно, путем построения временных столбцов, прежде чем отправлять рейтинг. Аналогично, можно ли ранжировать субканал данных в одном вызове?
(iv) Зачем использовать pd.qcut() а не df.rank() ? последний cythonized и, кажется, имеет более гибкую обработку связей, но я не вижу сравнения между ними, и pd.qcut() кажется наиболее широко используемым.

Примеры входных данных:

 import pandas as pd import numpy as np import random to_rank = ['var_1', 'var_2', 'var_3'] df = pd.DataFrame({'var_1' : np.random.randn(1000), 'var_2' : np.random.randn(1000), 'var_3' : np.random.randn(1000)}) df['date_id'] = np.random.choice(range(2001, 2012), df.shape[0]) df['category'] = ','.join(chr(random.randrange(97, 97 + 4 + 1)).upper() for x in range(1,df.shape[0]+1)).split(',') 

Две функции ранжирования:

 def rank_fun(df, to_rank): # calls ranking function f(x) to rank each category at each date #extra data tidying logic here beyond scope of question - can remove ranked = df[to_rank].apply(lambda x: f(x)) return ranked def f(x): nans = x[np.isnan(x)] # Remove nans as these will be ranked with 50 sub_df = x.dropna() # nans_ranked = nans.replace(np.nan, 50) # give nans rank of 50 if len(sub_df.index) == 0: #check not all nan. If no non-nan data, then return with rank 50 return nans_ranked if len(sub_df.unique()) == 1: # if all data has same value, return rank 50 sub_df[:] = 50 return sub_df #Check that we don't have too many clustered values, such that we can't bin due to overlap of ties, and reduce bin size provided we can at least quintile rank. max_cluster = sub_df.value_counts().iloc[0] #value_counts sorts by counts, so first element will contain the max max_bins = len(sub_df) / max_cluster if max_bins > 100: #if largest cluster <1% of available data, then we can percentile_rank max_bins = 100 if max_bins < 5: #if we don't have the resolution to quintile rank then assume no data. sub_df[:] = 50 return sub_df bins = int(max_bins) # bin using highest resolution that the data supports, subject to constraints above (max 100 bins, min 5 bins) sub_df_ranked = pd.qcut(sub_df, bins, labels=False) #currently using pd.qcut. pd.rank( seems to have extra functionality, but overheads similar in practice sub_df_ranked *= (100 / bins) #Since we bin using the resolution specified in bins, to convert back to decile rank, we have to multiply by 100/bins. Eg with quintiles, we'll have scores 1 - 5, so have to multiply by 100 / 5 = 20 to convert to percentile ranking ranked_df = pd.concat([sub_df_ranked, nans_ranked]) return ranked_df 

И код для вызова моей функции ранжирования и рекомбинации с df:

 # ensure don't get duplicate columns if ranking already executed ranked_cols = [col + '_ranked' for col in to_rank] ranked = df[['date_id', 'category'] + to_rank].groupby(['date_id', 'category'], as_index = False).apply(lambda x: rank_fun(x, to_rank)) ranked.columns = ranked_cols ranked.reset_index(inplace = True) ranked.set_index('level_1', inplace = True) df = df.join(ranked[ranked_cols]) 

Я пытаюсь получить эту логику ранжирования как можно быстрее, удалив оба лямбда-х-звонка; Я могу удалить логику в rank_fun, так что применима только логика f (x), но я также не знаю, как обрабатывать мультииндексные кадры данных в векторном виде. Дополнительный вопрос заключался бы в различиях между pd.qcut( и df.rank( : кажется, что у обоих есть разные способы решения связей, но накладные расходы кажутся похожими, несмотря на то, что .rank (нарисован на cythonized, возможно, это вводит в заблуждение , учитывая, что основные накладные расходы связаны с моим использованием лямбда x.

Я %lprun на f(x) что дало мне следующие результаты, хотя основными издержками являются использование .apply(lambda x а не векторный подход:

Линия # Время хитов% Hit Time Time Содержание

  2 def tst_fun(df, field): 3 1 685 685.0 0.2 x = df[field] 4 1 20726 20726.0 5.8 nans = x[np.isnan(x)] 5 1 28448 28448.0 8.0 sub_df = x.dropna() 6 1 387 387.0 0.1 nans_ranked = nans.replace(np.nan, 50) 7 1 5 5.0 0.0 if len(sub_df.index) == 0: 8 pass #check not empty. May be empty due to nans for first 5 years eg no revenue/operating margin data pre 1990 9 return nans_ranked 10 11 1 65559 65559.0 18.4 if len(sub_df.unique()) == 1: 12 sub_df[:] = 50 #eg for subranks where all factors had nan so ranked as 50 eg in 1990 13 return sub_df 14 15 #Finally, check that we don't have too many clustered values, such that we can't bin, and reduce bin size provided we can at least quintile rank. 16 1 74610 74610.0 20.9 max_cluster = sub_df.value_counts().iloc[0] #value_counts sorts by counts, so first element will contain the max 17 # print(counts) 18 1 9 9.0 0.0 max_bins = len(sub_df) / max_cluster # 19 20 1 3 3.0 0.0 if max_bins > 100: 21 1 0 0.0 0.0 max_bins = 100 #if largest cluster <1% of available data, then we can percentile_rank 22 23 24 1 0 0.0 0.0 if max_bins < 5: 25 sub_df[:] = 50 #if we don't have the resolution to quintile rank then assume no data. 26 27 # return sub_df 28 29 1 1 1.0 0.0 bins = int(max_bins) # bin using highest resolution that the data supports, subject to constraints above (max 100 bins, min 5 bins) 30 31 #should track bin resolution for all data. To add. 32 33 #if get here, then neither nans_ranked, nor sub_df are empty 34 # sub_df_ranked = pd.qcut(sub_df, bins, labels=False) 35 1 160530 160530.0 45.0 sub_df_ranked = (sub_df.rank(ascending = True) - 1)*100/len(x) 36 37 1 5777 5777.0 1.6 ranked_df = pd.concat([sub_df_ranked, nans_ranked]) 38 39 1 1 1.0 0.0 return ranked_df 

2 Solutions collect form web for “Повышение эффективности функции ранжирования путем замены лямбда x на векторизация”

Я предлагаю вам попробовать этот код. Это в 3 раза быстрее, чем у вас, и яснее.

ранговая функция:

 def rank(x): counts = x.value_counts() bins = int(0 if len(counts) == 0 else x.count() / counts.iloc[0]) bins = 100 if bins > 100 else bins if bins < 5: return x.apply(lambda x: 50) else: return (pd.qcut(x, bins, labels=False) * (100 / bins)).fillna(50).astype(int) 

применяется одна нить:

 for col in to_rank: df[col + '_ranked'] = df.groupby(['date_id', 'category'])[col].apply(rank) 

накладывать масляную нить:

 import sys from multiprocessing import Pool def tfunc(col): return df.groupby(['date_id', 'category'])[col].apply(rank) pool = Pool(len(to_rank)) result = pool.map_async(tfunc, to_rank).get(sys.maxint) for (col, val) in zip(to_rank, result): df[col + '_ranked'] = val 

Я бы построил функцию, используя numpy
Я планирую использовать это в каждой группе, определенной в группе pandas

 def rnk(df): a = df.values.argsort(0) n, m = a.shape r = np.arange(a.shape[1]) b = np.empty_like(a) b[a, np.arange(m)[None, :]] = np.arange(n)[:, None] return pd.DataFrame(b / n, df.index, df.columns) gcols = ['date_id', 'category'] rcols = ['var_1', 'var_2', 'var_3'] df.groupby(gcols)[rcols].apply(rnk).add_suffix('_ranked') var_1_ranked var_2_ranked var_3_ranked 0 0.333333 0.809524 0.428571 1 0.160000 0.360000 0.240000 2 0.153846 0.384615 0.461538 3 0.000000 0.315789 0.105263 4 0.560000 0.200000 0.160000 ... 

Как это работает

  • Поскольку я знаю, что ранжирование связано с сортировкой, я хочу использовать некоторую умную сортировку, чтобы сделать это быстрее.
  • argsort numpy argsort перестановку, которая может быть использована для разбиения массива на отсортированный массив.

     a = np.array([25, 300, 7]) b = a.argsort() print(b) [2 0 1] print(a[b]) [ 7 25 300] 
  • Поэтому вместо этого я собираюсь использовать argsort чтобы сообщить мне, где находятся первые, второй и третий ранжированные элементы.

     # create an empty array that is the same size as b or a # but these will be ranks, so I want them to be integers # so I use empty_like(b) because b is the result of # argsort and is already integers. u = np.empty_like(b) # now just like when I sliced a above with a[b] # I slice u the same way but instead I assign to # those positions, the ranks I want. # In this case, I defined the ranks as np.arange(b.size) + 1 u[b] = np.arange(b.size) + 1 print(u) [2 3 1] 
  • И это было точно. 7 был в последней позиции, но был нашим первым рангом. 300 был на второй позиции и был нашим третьим ранга. 25 был на первом месте и был нашим вторым званием.

  • Наконец, я делясь на число в ранге, чтобы получить процентили. Так получилось, что, поскольку я использовал нулевой рейтинг np.arange(n) , в отличие от одного основанного np.arange(1, n+1) или np.arange(n) + 1 как в нашем примере, я могу сделать простое разделение, чтобы получить процентили.
  • Остается только применить эту логику к каждой группе. Мы можем сделать это в groupby с groupby
  • Некоторые из недостающих деталей включают в себя то, как я использую argsort(0) чтобы получить независимые сортировки на столбец`, и что я делаю какую-то причудливую нарезку, чтобы независимо распределять каждый столбец.

Можем ли мы избежать groupby и сделать все, чтобы сделать это?
Я также воспользуюсь тем, что numba просто компилирует, чтобы ускорить некоторые действия с помощью njit

 from numba import njit @njit def count_factor(f): c = np.arange(f.max() + 2) * 0 for i in f: c[i + 1] += 1 return c @njit def factor_fun(f): c = count_factor(f) cc = c[:-1].cumsum() return c[1:][f], cc[f] def lexsort(a, f): n, m = a.shape f = f * (a.max() - a.min() + 1) return (f.reshape(-1, 1) + a).argsort(0) def rnk_numba(df, gcols, rcols): tups = list(zip(*[df[c].values.tolist() for c in gcols])) f = pd.Series(tups).factorize()[0] a = lexsort(np.column_stack([df[c].values for c in rcols]), f) c, cc = factor_fun(f) c = c[:, None] cc = cc[:, None] n, m = a.shape r = np.arange(a.shape[1]) b = np.empty_like(a) b[a, np.arange(m)[None, :]] = np.arange(n)[:, None] return pd.DataFrame((b - cc) / c, df.index, rcols).add_suffix('_ranked') 

Как это работает

  • Честно говоря, это трудно обрабатывать мысленно. Я буду придерживаться того, что я объяснил выше.
  • Я хочу снова использовать argsort чтобы понизить рейтинг в правильные позиции. Тем не менее, я должен бороться с столбцами группировки. Итак, что я делаю, это составить список tuple s и factorize их по factorize как было рассмотрено в этом вопросе здесь
  • Теперь, когда у меня есть факторизованный набор tuple я могу выполнить модифицированный lexsort который сортируется внутри моих факторизованных групп tuple . Этот вопрос касается lexsort .
  • Сложный бит еще предстоит решить, где я должен отключить новые найденные ранги по размеру каждой группы, чтобы получить новые ряды для каждой группы. Об этом говорится в крошечном фрагменте b - cc в приведенном ниже коде. Но вычисление cc является необходимым компонентом.

Так вот, это философия высокого уровня. Как насчет @njit ?

  • Обратите внимание, что когда я факторизую, я сопоставляю целые числа от 0 до n - 1 где n – количество уникальных tuple s. Я могу использовать массив длины n как удобный способ отслеживания счетчиков.
  • Чтобы выполнить смещение groupby , мне нужно было отслеживать подсчеты и кумулятивные подсчеты в позициях этих групп, поскольку они представлены в списке tuples или факторизованной версии этих tuple . Я решил провести линейное сканирование через факторизованный массив f и подсчитать наблюдения в цикле numba . Хотя у меня была эта информация, я бы также предоставил необходимую информацию для создания кумулятивных смещений, которые мне также нужны.
  • numba предоставляет интерфейс для создания высокоэффективных скомпилированных функций. Это очень сложно, и вам нужно приобрести некоторый опыт, чтобы узнать, что возможно, и что невозможно. Я решил numba fy две функции, которым предшествует numba decorator @njit . Эта кодировка работает так же хорошо без этих декораторов, но ускоряется с ними.

тайминг

 %%timeit ranked_cols = [col + '_ranked' for col in to_rank]​ ranked = df[['date_id', 'category'] + to_rank].groupby(['date_id', 'category'], as_index = False).apply(lambda x: rank_fun(x, to_rank)) ranked.columns = ranked_cols ranked.reset_index(inplace = True) ranked.set_index('level_1', inplace = True) 1 loop, best of 3: 481 ms per loop gcols = ['date_id', 'category'] rcols = ['var_1', 'var_2', 'var_3'] %timeit df.groupby(gcols)[rcols].apply(rnk_numpy).add_suffix('_ranked') 100 loops, best of 3: 16.4 ms per loop %timeit rnk_numba(df, gcols, rcols).head() 1000 loops, best of 3: 1.03 ms per loop  %%timeit ranked_cols = [col + '_ranked' for col in to_rank]​ ranked = df[['date_id', 'category'] + to_rank].groupby(['date_id', 'category'], as_index = False).apply(lambda x: rank_fun(x, to_rank)) ranked.columns = ranked_cols ranked.reset_index(inplace = True) ranked.set_index('level_1', inplace = True) 1 loop, best of 3: 481 ms per loop gcols = ['date_id', 'category'] rcols = ['var_1', 'var_2', 'var_3'] %timeit df.groupby(gcols)[rcols].apply(rnk_numpy).add_suffix('_ranked') 100 loops, best of 3: 16.4 ms per loop %timeit rnk_numba(df, gcols, rcols).head() 1000 loops, best of 3: 1.03 ms per loop 
 
Interesting Posts for Van-Lav

Как получить доступ к полю геометрии (точки) в базе данных PostGIS из Django?

Как я могу пропустить соединения Fabric, которые запрашивают пароль?

BeautifulSoup: Как извлечь все <li> s из списка <ul> s, который содержит некоторые вложенные <ul> s?

Matplotlib colormap, участок рассеяния, передающий третью переменную для цвета: недопустимый аргумент RGBA

Какой модуль JSON можно использовать в Python 2.5?

Связывание Python Selenium с браузером TOR

Python Threads (или их эквивалент) в решении Google Application Engine?

Найти все элементы с учетом атрибута имен

Почему я получаю SyntaxError для escape-кода Unicode в моем пути к файлу?

Изменение цвета фона оболочки python

Является ли os.listdir () детерминированным?

django admin, регистрирующий динамическую модель от действия

пул grequests с несколькими request.session?

Как максимизировать производительность в Python при выполнении многих операций ввода-вывода?

Отклонено ли Twisted's так же, как обещание в JavaScript?

Python - лучший язык программирования в мире.