У меня есть функция ранжирования, которую я применяю к большому количеству столбцов из нескольких миллионов строк, для чего требуется несколько минут. Удалив всю логику, готовящую данные для применения метода .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
в коде быстрым, векторизованным эквивалентом? (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. E.g. 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
, а не векторный подход:
Линия # Время хитов% Хит% Содержание строки
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 e.g. 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 #e.g. for subranks where all factors had nan so ranked as 50 e.g. 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