Pandas - Groupby с числовыми пороговыми категориями? - PullRequest
1 голос
/ 04 августа 2020

У меня довольно сложный рабочий процесс, который я постарался максимально упростить. Учитывая это DataFrame ...

df = pd.DataFrame(
    [
        ["Johnson", "Female", 1.8, 1, 4],
        ["Johnson", "Female", 1.6, 2, 5],
        ["Johnson", "Female", 1.4, 3, 8],
        ["Johnson", "Female", 1.7, 4, 6],
        ["Johnson", "Male", 1.8, 4, 2],
        ["Johnson", "Male", 2.0, 2, 5],
        ["Johnson", "Male", 2.2, 2, 5],
        ["Smith", "Female", 1.7, 2, 2],
        ["Smith", "Female", 1.5, 4, 1],
        ["Smith", "Male", 1.7, 3, 3],
        ["Smith", "Male", 1.7, 3, 3],
        ["Smith", "Male", 1.9, 4, 3],
        ["Smith", "Male", 1.6, 2, 2],
    ],
    columns=["Family", "Gender", "Height", "Hunger", "Thirst"],
)

... моя цель состоит из четырех частей:

  1. Группировка по семье и полу
  2. Иметь разные ограничения / пороговые значения для количества людей на определенной высоте (например, <1,7, <2,0) </li>
  3. Не принимать во внимание groupby с малой численностью населения
  4. Рассчитать оценку «Счастье» на основе моей метрики «Голод» и "Жажда"

Что-то вроде следующего:

scores = []
min_population_size = 1

# Step 1.
for group, dfg, in df.groupby(["Family", "Gender"]):
    
    # Step 2.
    for threshold in [1.7, 2.0]:
        dfg_threshold = dfg[dfg["Height"] < threshold]
        
        # Step 3. and 4.
        if (count := len(dfg_threshold)) > min_population_size:
            happiness = 1 - (1 / (dfg_threshold["Hunger"].mean() * dfg_threshold["Thirst"].mean()))  # placeholder for complex calculation
        else:
            happiness = None
             
        scores.append([group[0], group[1], threshold, count, happiness])
        
pd.DataFrame(scores, columns=["Family", "Gender", "Height-Threshold", "Count", "Happiness"])

Эта довольно уродливая реализация работает, но поскольку мой настоящий набор данных составляет около 80 ГБ, а у меня около 500 пороговых значений, этот цикл чрезвычайно трудоемкий. Есть ли способ преобразовать это в одну функцию groupby или apply, чтобы ее можно было выполнять параллельно через Dask или, по крайней мере, ускорить в Pandas?

Заранее спасибо.

Ответы [ 2 ]

1 голос
/ 04 августа 2020

Всегда, когда вы можете использовать векторизованный расчет, вы должны это делать.

В вашем случае вы можете использовать expanding().mean() в каждой группе, если высоты отсортированы, а затем выбрать точки разреза, которые вы хочу сообщить.

В аналогичных ситуациях, когда для вычислений требуется сортировка в каждой группе, я обнаружил, что это хорошая идея - сортировать весь DataFrame один раз (я знаю, это противоречит интуиции, поскольку sort равно O (n log n), но обычно это быстрее, чем сортировка внутри каждой группы).

Итак, как насчет этого:

def worker(g, min_population, show_mean=False):
    z = g[['Hunger', 'Thirst']].expanding().mean().rename(columns=lambda x: f'{x}.mean')
    z = z.assign(threshold=g.threshold, count=np.arange(1, g.shape[0] + 1))
    z = z.loc[np.concatenate((z['threshold'].values[:-1] != z['threshold'].values[1:], [True]))]
    z = z.loc[z['count'] >= min_population]
    z['happiness'] = 1 - (1 / (z['Hunger.mean'] * z['Thirst.mean']))
    columns = ['count', 'happiness']
    if show_mean:
        columns = ['Hunger.mean', 'Thirst.mean'] + columns
    return z.set_index('threshold')[columns]

def my_stats(df, thresholds, min_population, show_mean=False):
    res = (df
           .assign(threshold=pd.cut(
               df['Height'], np.concatenate(([-np.Inf], thresholds)),
               labels=thresholds, right=False))
           .dropna(subset=['threshold'])
           .sort_values('Height')
           .groupby(['Family', 'Gender'])
           .aggregate(worker, min_population=min_population, show_mean=show_mean)
          )
    return res

Учитывая DataFrame в вашем пример :

my_stats(df, thresholds=[1.7, 2.0], min_population=2)

Out[ ]:
                          count  happiness
Family  Gender threshold                  
Johnson Female 1.7            2   0.938462
               2.0            4   0.930435
Smith   Female 2.0            2   0.777778
        Male   2.0            4   0.878788

Или, если вы хотите показать средства, которые использовались в ваших расчетах:

my_stats(df, thresholds=[1.7, 2.0], min_population=2, show_mean=True)

Out[ ]:
                          Hunger.mean  Thirst.mean  count  happiness
Family  Gender threshold                                            
Johnson Female 1.7                2.5         6.50      2   0.938462
               2.0                2.5         5.75      4   0.930435
Smith   Female 2.0                3.0         1.50      2   0.777778
        Male   2.0                3.0         2.75      4   0.878788

Тест скорости

%%time
thresholds = np.sort(np.random.uniform(0, 3, size=500))
my_stats(df, thresholds=thresholds, min_population=2)

Out[ ]:
CPU times: user 225 ms, sys: 0 ns, total: 225 ms
Wall time: 224 ms
                          count  happiness
Family  Gender threshold                  
Coulson Female 0.331058       2   0.928571
               0.349368       3   0.931818
               0.352899       4   0.960000
               0.364010       5   0.958333
               0.380945       6   0.959596
...                         ...        ...
Ward    Male   2.912417    7349   0.950410
               2.928181    7350   0.950410
               2.972472    7351   0.950402
               2.986354    7352   0.950401
               2.992616    7353   0.950404
1 голос
/ 04 августа 2020

Вы можете добавить третий уровень к groupby и использовать pandas.cut в качестве дискретизатора бинов для группировки по:

min_population_size = 1

cutter = pd.cut(df['Height'], [0, 1.7, 2.], right=False)
grouper = df.groupby(['Family', 'Gender', cutter])
# do your calculations...
happiness = 1 - (1 / (grouper["Hunger"].mean() * grouper["Thirst"].mean()))
happiness[grouper.size() <= 1] = None

При дискретизации в бункеры right=False/True устанавливает, обрабатывается ли бин как включающие или исключающие право.

Насколько мне известно, нет способа дискретизировать перекрывающиеся ячейки с помощью pandas без создания нескольких группировок и циклического перебора их ... Возможно, у кого-то есть идея, как сделать это с помощью agg? В любом случае, вот мой подход, пытаясь максимально сократить до дорогостоящих циклов:

bins = [1.7, 2.]
# make overlapping cutters and groupers
cutters = [pd.cut(df['Height'], [0, i], right=False) for i in bins]
groupers = [df.groupby(['Family', 'Gender', cttr]) for cttr in cutters]

# do your calculations... this could still take some time. no idea how to avoid this loop...
happiness = [1 - (1 / (grpr["Hunger"].mean() * grpr["Thirst"].mean())) for grpr in groupers]

# this loop should be fairly cheap...
for i in range(len(groupers)):
    happiness[i][groupers[i].size() <= 1] = None
    happiness[i].index.rename(  # this part is fully optional
        'Height-Thresh-{0:.1f}'.format(bins[i]), level=2, inplace=True)

Следующая конкатенация снова должна быть довольно дешевой:

score = pd.concat(happiness, axis=1)
score.index.rename(['Family', 'Gender', 'Height-Threshold'], inplace=True)

Если вы хотите иметь высота-thread sh как столбцы:

score = score.unstack(-1).droplevel(axis=1, level=0).dropna(how='all', axis=1)

И, возможно, задайте несколько «более простых» имен столбцам:

score.columns = bins
...