pd.groupby на другой groupby, перенос результатов pd.cut

#python #pandas #dataframe #pandas-groupby

#python #pandas #фрейм данных #pandas-groupby

Вопрос:

Еще один довольно сложный вопрос, на котором я застрял, касающийся Pandas и его функции groupby и cut. Ситуация следующая, допустим, у меня есть фрейм данных, который выглядит следующим образом:

 import Pandas as pd

pd.DataFrame(data)

   A  B  C       ipv4
0  1  3  3    0.0.0.0
1  2  2  1  140.0.0.0
2  3  1  3  230.0.0.0
3  1  1  2  140.0.0.0
4  3  1  2        NaN
  

На этом этапе я должен добавить, что фактические фреймы данных, с которыми я здесь работаю, могут содержать миллионы (!) строк, Поэтому здесь я должен иметь в виду производительность.
Я создал функцию, которая дает мне набор полномочий A, B и C, так что pset = [(A), (B), (C), (A,B), ... ] без пустого, вы поняли идею. Сейчас я группируюсь по каждой из этих комбинаций в цикле и создаю count_df для каждой из них вот так:

 for combination in pset:
    df.groupby(list(combination))
    count_df = df.size().reset_index().rename(columns={0: 'count'})
    print(count_df)

   A  count
0  1      2
1  2      1
2  3      2
...
   A  B  count
0  1  1      1
1  1  3      1
2  2  2      1
3  3  1      2
...
  

Мы приближаемся к моей проблеме: мне нужно добавить некоторую очень-очень базовую информацию о IP-классах в каждую строку count_df с их соответствующей комбинацией A-B-C (вы можете прокрутить вниз по предоставленной ссылке до старшего бита (HOB) и посмотреть на таблицу, чтобы получить краткое представление о том, что я пытаюсь здесь сделать). df Для этого я добавил в свой еще одну строку, содержащую первый октет каждой строки ipv4, и использовал сокращение Pandas, чтобы довольно быстро получить количество для каждого интервала:

 # I use 256 as value for any row that has "NaN" instead of a real address
df["ipv4"].replace(to_replace="NaN", value="256.0.0.0", inplace=True)
df["first_octet"] = df["ipv4"].apply(lambda x: int(x.partition(".")[0]))
df["cut_group"] = pd.cut(data["first_octet"], [0, 127, 191, 223, 239, 255, 256])

print(df)
   A  B  C       ipv4  first_octet       cut_group
0  1  3  3    0.0.0.0            0      (0, 127.0]
1  2  2  1  140.0.0.0          140  (127.0, 191.0]
2  3  1  3  230.0.0.0          230  (223.0, 239.0]
3  1  1  2  140.0.0.0          140  (127.0, 191.0]
4  3  1  2  256.0.0.0          256  (255.0, 256.0]

for combination in pset:
    df.groupby(list(combination)   ["cut_group"])
    count_df = df.size().reset_index().rename(columns={0: 'count'})
    print(count_df)

    A   cut_group  count
0   1    (0, 127]      1
1   1  (127, 191]      1
2   1  (191, 223]      0
3   1  (223, 239]      0
4   1  (239, 255]      0
5   1  (255, 256]      0
6   2    (0, 127]      0
7   2  (127, 191]      1
8   2  (191, 223]      0
9   2  (223, 239]      0
10  2  (239, 255]      0
11  2  (255, 256]      0
12  3    (0, 127]      0
13  3  (127, 191]      0
14  3  (191, 223]      0
15  3  (223, 239]      1
16  3  (239, 255]      0
17  3  (255, 256]      1
...
    A  B   cut_group  count
0   1  1    (0, 127]      0
1   1  1  (127, 191]      1
2   1  1  (191, 223]      0
3   1  1  (223, 239]      0
4   1  1  (239, 255]      0
5   1  1  (255, 256]      0
6   1  2    (0, 127]      0
7   1  2  (127, 191]      0
8   1  2  (191, 223]      0
9   1  2  (223, 239]      0
10  1  2  (239, 255]      0
11  1  2  (255, 256]      0
12  1  3    (0, 127]      1
13  1  3  (127, 191]      0
14  1  3  (191, 223]      0
15  1  3  (223, 239]      0
16  1  3  (239, 255]      0
17  1  3  (255, 256]      0
18  2  1    (0, 127]      0
19  2  1  (127, 191]      0
20  2  1  (191, 223]      0
21  2  1  (223, 239]      0
22  2  1  (239, 255]      0
23  2  1  (255, 256]      0
24  2  2    (0, 127]      0
25  2  2  (127, 191]      1
26  2  2  (191, 223]      0
27  2  2  (223, 239]      0
28  2  2  (239, 255]      0
29  2  2  (255, 256]      0
30  2  3    (0, 127]      0
31  2  3  (127, 191]      0
32  2  3  (191, 223]      0
33  2  3  (223, 239]      0
34  2  3  (239, 255]      0
35  2  3  (255, 256]      0
36  3  1    (0, 127]      0
37  3  1  (127, 191]      0
38  3  1  (191, 223]      0
39  3  1  (223, 239]      1
40  3  1  (239, 255]      0
41  3  1  (255, 256]      1
42  3  2    (0, 127]      0
43  3  2  (127, 191]      0
44  3  2  (191, 223]      0
45  3  2  (223, 239]      0
46  3  2  (239, 255]      0
47  3  2  (255, 256]      0
48  3  3    (0, 127]      0
49  3  3  (127, 191]      0
50  3  3  (191, 223]      0
51  3  3  (223, 239]      0
52  3  3  (239, 255]      0
53  3  3  (255, 256]      0
...
  

Хорошо, итак, следующий шаг здесь для меня теперь отсутствует. Что мне нужно, так это выходные данные, которые выглядят следующим образом для каждой комбинации pset:

 for combination in pset:
    <???>
    print(count_df)

   A  count  (0, 127]  (127, 191]  (191, 223]  (223, 239]  (239, 255]  (255, 256]
0  1      2         1           1           0           0           0           0
1  2      1         0           1           0           0           0           0
2  3      1         0           0           0           1           0           1
...
   A  B  count  (0, 127]  (127, 191]  (191, 223]  (223, 239]  (239, 255]  (255, 256]
0  1  1      1         0           1           0           0           0           0
1  1  2      0         0           0           0           0           0           0
2  1  3      1         1           0           0           0           0           0
3  2  1      0         0           0           0           0           0           0
4  2  2      1         0           1           0           0           0           0
5  2  3      0         0           0           0           0           0           0
6  3  1      2         0           0           0           1           0           1
7  3  2      0         0           0           0           0           0           0
8  3  3      0         0           0           0           0           0           0
...
  

Я не уверен, как к этому подойти. Столбцы count_df также могут быть A-B-C count classA classB classC classD classE classNaN для пояснения. count Столбец должен указывать количество того, сколько базовых строк содержало индивидуальную комбинацию A-B-C, как я бы вызвал df.groupby(list(combination)).size().reset_index().rename(columns={0: 'count'}) , столбцы интервалов должны указывать количество того, сколько базовых строк было подсчитано для отдельного класса отдельной комбинации A-B-C. Вы можете свести проблему к чему-то вроде наличия groupby с groupby1 = df.groupby(list(combination) ["cut_group"]) , а после этого другой groupby на этой, подобной groupby2 = groupby1.groupby(list(combination)) , и добавления информации о количестве классов из groupby1 , транспонированной в строки. Эти последние строки здесь являются бессмысленным кодом, просто чтобы прояснить, что я имею в виду.

Я открыт для любого предложения относительно заполнения упомянутого «пробела» в моем коде, а также для любого предложения, возможно, сделать что-то другое здесь, используя другие функции Pandas, о которых я пока не знаю. Как всегда, я рад изучить различные способы использования Pandas. Спасибо!

Ответ №1:

что вы можете сделать, так это join a pd.get_dummies столбца cut_group, а затем использовать sum в groupby , что-то вроде:

 # get dummies
dummies = pd.get_dummies(df["cut_group"])
df_ = df.join(dummies) #you can reassign to df if you want

for combination in pset:
    gr = df_.groupby(list(combination)) #change to df if you reassign the join to df before
    count_df = (gr.size().to_frame('count')
                  .join(gr[dummies.columns].sum())
               )
    print(count_df)
  

Комментарии:

1. Спасибо за ваше предложение, я только что узнал о pd.get_dummies из-за этого. Вероятно, я буду использовать эту функцию позже в другом контексте!

Ответ №2:

У вашего pd.cut есть небольшая ошибка: он игнорирует строки, первый октет которых равен 0, поскольку по умолчанию он не включает нижний край. Вместо этого запустите свои ячейки с -1.

 df["first_octet"] = df["ipv4"].str.split('.', expand=True)[0].fillna(256).astype('int')

# Pivoting as CategoricalDType takes forever. I think this is a bug in pandas.
# Converting to string to make pivot_table faster
df["cut_group"] = pd.cut(df["first_octet"], [-1, 127, 191, 223, 239, 255, 256]).astype('str')
  

Сначала запустите сводку для всех трех столбцов (A, B, C),

 tmp = pd.pivot_table(df, index=['A','B','C'], columns='cut_group', values='ipv4',
                     aggfunc='count', fill_value=0).rename_axis(None, axis=1)
tmp['count'] = tmp.sum(axis=1)
  

Затем вы можете groupby и суммировать любые комбинации из 3:

 from itertools import combinations
cols = list('ABC')

pset = []
for size in [1,2,3]:
    pset  = [list(c) for c in combinations(cols, size)]
# pset = [['A'], ['B'], ['C'], ['A', 'B'], ['A', 'C'], ['B', 'C'], ['A', 'B', 'C']]

result = [tmp.groupby(p).sum() for p in pset]
  

Время выполнения зависит от изменчивости в A, B и C. Это занимает больше времени, когда есть более четкие значения. Для моего случайного набора данных (A, B, C — случайные 1-1000, 5M строк) это заняло около 40 секунд.

Комментарии:

1. Я, наконец, согласился с вашим предложением. Прежде всего, спасибо, что нашли ошибку с помощью pd.cut 🙂 работа с pd.pivot_table — потрясающая идея, я думаю, что переделаю часть своего кода, чтобы использовать эти таблицы с самого начала, так что вы помогли мне даже больше, чем вы, возможно, думаете! Это действительно повысило общую производительность всего моего проекта, отлично!