Pandas: присваивать индекс каждой группе, идентифицированной groupby
При использовании groupby(), как я могу создать DataFrame с новым столбцом, содержащим индекс номера группы, аналогично dplyr::group_indices
в R. Например, если у меня есть
>>> df=pd.DataFrame({'a':[1,1,1,2,2,2],'b':[1,1,2,1,1,2]})
>>> df
a b
0 1 1
1 1 1
2 1 2
3 2 1
4 2 1
5 2 2
Как я могу получить DataFrame как
a b idx
0 1 1 1
1 1 1 1
2 1 2 2
3 2 1 3
4 2 1 3
5 2 2 4
(порядок индексов idx
не имеет значения)
Ответы
Ответ 1
Вот краткий способ использования drop_duplicates
и merge
для получения уникального идентификатора.
group_vars = ['a','b']
df.merge( df.drop_duplicates( group_vars ).reset_index(), on=group_vars )
a b index
0 1 1 0
1 1 1 0
2 1 2 2
3 2 1 3
4 2 1 3
5 2 2 5
Идентификатор в этом случае идет 0,2,3,5 (только остаток от исходного индекса), но его можно легко изменить на 0,1,2,3 с дополнительным reset_index(drop=True)
.
Обновление: Более новые версии панд (0.20.2) предлагают более простой способ сделать это с помощью метода ngroup
, как отмечено в комментарии к вышеупомянутому вопросу @Constantino и последующем ответе @CalumYou. Я оставлю это здесь как альтернативный подход, но ngroup
кажется лучшим способом сделать это в большинстве случаев.
Ответ 2
Простой способ сделать это - объединить столбцы группировки (чтобы каждая комбинация их значений представляла уникальный элемент), затем преобразовать его в pandas Категориальный и сохранить только его метки:
df['idx'] = pd.Categorical(df['a'].astype(str) + '_' + df['b'].astype(str)).codes
df
a b idx
0 1 1 0
1 1 1 0
2 1 2 1
3 2 1 2
4 2 1 2
5 2 2 3
Изменение: свойства labels
изменены на codes
, так как прежние свойства устарели
Edit2: добавлен разделитель в соответствии с предложением Authman Apatira
Ответ 3
Вот решение, использующее ngroup
из комментария выше Константино, для тех, кто все еще ищет эту функцию (эквивалент dplyr::group_indices
в R, если вы пытались гуглить с такими ключевыми словами, как я), Это также примерно на 25% быстрее, чем решение, полученное с помощью maxliving в соответствии с моим собственным временем.
>>> import pandas as pd
>>> df = pd.DataFrame({'a':[1,1,1,2,2,2],'b':[1,1,2,1,1,2]})
>>> df['idx'] = df.groupby(['a', 'b']).ngroup()
>>> df
a b idx
0 1 1 0
1 1 1 0
2 1 2 1
3 2 1 2
4 2 1 2
5 2 2 3
>>> %timeit df['idx'] = create_index_usingduplicated(df, grouping_cols=['a', 'b'])
1.83 ms ± 67.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit df['idx'] = df.groupby(['a', 'b']).ngroup()
1.38 ms ± 30 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Ответ 4
Способ, который, по моему мнению, быстрее, чем текущий принятый ответ примерно на порядок (результаты синхронизации ниже):
def create_index_usingduplicated(df, grouping_cols=['a', 'b']):
df.sort_values(grouping_cols, inplace=True)
# You could do the following three lines in one, I just thought
# this would be clearer as an explanation of what going on:
duplicated = df.duplicated(subset=grouping_cols, keep='first')
new_group = ~duplicated
return new_group.cumsum()
Результаты синхронизации:
a = np.random.randint(0, 1000, size=int(1e5))
b = np.random.randint(0, 1000, size=int(1e5))
df = pd.DataFrame({'a': a, 'b': b})
In [6]: %timeit df['idx'] = pd.Categorical(df['a'].astype(str) + df['b'].astype(str)).codes
1 loop, best of 3: 375 ms per loop
In [7]: %timeit df['idx'] = create_index_usingduplicated(df, grouping_cols=['a', 'b'])
100 loops, best of 3: 17.7 ms per loop
Ответ 5
Я не уверен, что это такая тривиальная проблема. Вот несколько запутанное решение, которое сначала сортирует столбцы группировки, а затем проверяет, отличается ли каждая строка от предыдущей строки, и если это так накапливается на 1. Проверьте ниже ниже ответ на строковые данные.
df.sort_values(['a', 'b']).diff().fillna(0).ne(0).any(1).cumsum().add(1)
Выход
0 1
1 1
2 2
3 3
4 3
5 4
dtype: int64
Итак, разбирая это на шаги, давайте увидим вывод df.sort_values(['a', 'b']).diff().fillna(0)
, который проверяет, отличается ли каждая строка от предыдущей строки. Любая ненулевая запись указывает на новую группу.
a b
0 0.0 0.0
1 0.0 0.0
2 0.0 1.0
3 1.0 -1.0
4 0.0 0.0
5 0.0 1.0
Новая группа должна иметь только один столбец, так что это то, что .ne(0).any(1)
проверяет - не равно 0 для любого из столбцов. А затем просто кумулятивная сумма для отслеживания групп.
Ответ для столбцов как строки
#create fake data and sort it
df=pd.DataFrame({'a':list('aabbaccdc'),'b':list('aabaacddd')})
df1 = df.sort_values(['a', 'b'])
вывод df1
a b
0 a a
1 a a
4 a a
3 b a
2 b b
5 c c
6 c d
8 c d
7 d d
Сделайте аналогичный подход, проверив, изменилась ли группа
df1.ne(df1.shift().bfill()).any(1).cumsum().add(1)
0 1
1 1
4 1
3 2
2 3
5 4
6 5
8 5
7 6
Ответ 6
Определенно не самое простое решение, но вот что я сделал бы (комментарии в коде):
df=pd.DataFrame({'a':[1,1,1,2,2,2],'b':[1,1,2,1,1,2]})
#create a dummy grouper id by just joining desired rows
df["idx"] = df[["a","b"]].astype(str).apply(lambda x: "".join(x),axis=1)
print df
Это создало бы уникальный idx для каждой комбинации a
и b
.
a b idx
0 1 1 11
1 1 1 11
2 1 2 12
3 2 1 21
4 2 1 21
5 2 2 22
Но это по-прежнему довольно глупый индекс (подумайте о более сложных значениях в столбцах a
и b
. Поэтому снимите индекс:
# create a dictionary of dummy group_ids and their index-wise representation
dict_idx = dict(enumerate(set(df["idx"])))
# switch keys and values, so you can use dict in .replace method
dict_idx = {y:x for x,y in dict_idx.iteritems()}
#replace values with the generated dict
df["idx"].replace(dict_idx,inplace=True)
print df
Это даст желаемый результат:
a b idx
0 1 1 0
1 1 1 0
2 1 2 1
3 2 1 2
4 2 1 2
5 2 2 3