Различные группировки для каждого столбца с пандами GroupBy - PullRequest
0 голосов
/ 15 января 2019

Как я могу использовать многомерный Grouper, в данном случае другой фрейм данных, в качестве Grouper для другого фрейма данных? Можно ли это сделать за один шаг?

Мой вопрос, по сути, касается того, как выполнить фактическую группировку в этих условиях, но чтобы сделать ее более конкретной, скажем, я хочу затем transform и взять sum.

Рассмотрим для примера:

df1 = pd.DataFrame({'a':[1,2,3,4], 'b':[5,6,7,8]})

print(df1)
   a  b
0  1  5
1  2  6
2  3  7
3  4  8

df2  = pd.DataFrame({'a':['A','B','A','B'], 'b':['A','A','B','B']})

print(df2)
   a  b
0  A  A
1  B  A
2  A  B
3  B  B

Тогда ожидаемый результат будет:

   a  b
0  4  11
1  6  11
2  4  15
3  6  15

Где столбцы a и b в df1 сгруппированы по столбцам a и b из df2 соответственно.

Ответы [ 5 ]

0 голосов
/ 15 января 2019

Я собираюсь предложить (в основном) решение для numpythonic, которое использует scipy.sparse_matrix для выполнения векторизации groupby сразу для всего DataFrame, а не столбец за столбцом.


Ключом к эффективному выполнению этой операции является нахождение эффективного способа факторизации всего DataFrame, избегая дублирования в любых столбцах. Поскольку ваши группы представлены строками, вы можете просто объединить столбец имя в конце каждого значения (поскольку столбцы должны быть уникальными), а затем факторизовать результат, например: [*]

>>> df2 + df2.columns
    a   b
0  Aa  Ab
1  Ba  Ab
2  Aa  Bb
3  Ba  Bb

>>> pd.factorize((df2 + df2.columns).values.ravel())
(array([0, 1, 2, 1, 0, 3, 2, 3], dtype=int64),
array(['Aa', 'Ab', 'Ba', 'Bb'], dtype=object))

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

from scipy import sparse

a = df1.values.ravel()
b, _ = pd.factorize((df2 + df2.columns).values.ravel())

o = sparse.csr_matrix(
    (a, b, np.arange(a.shape[0] + 1)), (a.shape[0], b.max() + 1)
).sum(0).A1

res = o[b].reshape(df1.shape)

array([[ 4, 11],
       [ 6, 11],
       [ 4, 15],
       [ 6, 15]], dtype=int64)

Производительность

Функция

def gp_chris(f1, f2):
    a = f1.values.ravel()
    b, _ = pd.factorize((f2 + f2.columns).values.ravel())

    o = sparse.csr_matrix(
        (a, b, np.arange(a.shape[0] + 1)), (a.shape[0], b.max() + 1)
    ).sum(0).A1

    return pd.DataFrame(o[b].reshape(f1.shape), columns=df1.columns)


def gp_cs(f1, f2):
    return pd.concat([f1[c].groupby(f2[c]).transform('sum') for c in f1.columns], axis=1)


def gp_scott(f1, f2):
    return f1.apply(lambda x: x.groupby(f2[x.name]).transform('sum'))


def gp_wen(f1, f2):
    return f1.stack().groupby([f2.stack().index.get_level_values(level=1), f2.stack()]).transform('sum').unstack()

Настройка

import numpy as np
from scipy import sparse
import pandas as pd
import string
from timeit import timeit
import matplotlib.pyplot as plt
res = pd.DataFrame(
       index=[f'gp_{f}' for f in ('chris', 'cs', 'scott', 'wen')],
       columns=[10, 50, 100, 200, 400],
       dtype=float
)

for f in res.index:
    for c in res.columns:
        df1 = pd.DataFrame(np.random.rand(c, c))
        df2 = pd.DataFrame(np.random.choice(list(string.ascii_uppercase), (c, c)))
        df1.columns = df1.columns.astype(str)
        df2.columns = df2.columns.astype(str)

        stmt = '{}(df1, df2)'.format(f)
        setp = 'from __main__ import df1, df2, {}'.format(f)
        res.at[f, c] = timeit(stmt, setp, number=50)


ax = res.div(res.min()).T.plot(loglog=True)
ax.set_xlabel("N")
ax.set_ylabel("time (relative)")

plt.show()

Результаты

enter image description here


Валидация

df1 = pd.DataFrame(np.random.rand(10, 10))
df2 = pd.DataFrame(np.random.choice(list(string.ascii_uppercase), (10, 10)))
df1.columns = df1.columns.astype(str)
df2.columns = df2.columns.astype(str)

v = np.stack([gp_chris(df1, df2), gp_cs(df1, df2), gp_scott(df1, df2), gp_wen(df1, df2)])
print(np.all(v[:-1] == v[1:]))

True

Либо мы все не правы, либо мы все правы:)


[*] Существует вероятность того, что вы можете получить здесь повторяющееся значение, если один элемент - это конкатенация столбца, а другой - до того, как произойдет конкатенация. Однако, если это так, вам не нужно много настраивать, чтобы это исправить.

0 голосов
/ 15 января 2019

Использование stack и unstack

df1.stack().groupby([df2.stack().index.get_level_values(level=1),df2.stack()]).transform('sum').unstack()
Out[291]: 
   a   b
0  4  11
1  6  11
2  4  15
3  6  15
0 голосов
/ 15 января 2019

Вы должны будете сгруппировать каждый столбец индивидуально, так как каждый столбец использует свою схему группировки.

Если вы хотите более чистую версию, я бы порекомендовал понять список по именам столбцов и вызвать pd.concat для полученной серии:

pd.concat([df1[c].groupby(df2[c]).transform('sum') for c in df1.columns], axis=1)

   a   b
0  4  11
1  6  11
2  4  15
3  6  15

Не скажу, что с использованием apply, как в другом ответе, что-то не так, просто мне не нравится apply, так что это мое предложение :-)


Вот некоторые моменты для вашего прочтения. Только для ваших примеров данных вы заметите разницу во времени.

%%timeit 
(df1.stack()
    .groupby([df2.stack().index.get_level_values(level=1), df2.stack()])
    .transform('sum').unstack())
%%timeit 
df1.apply(lambda x: x.groupby(df2[x.name]).transform('sum'))
%%timeit 
pd.concat([df1[c].groupby(df2[c]).transform('sum') for c in df1.columns], axis=1)

8.99 ms ± 4.55 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
8.35 ms ± 859 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
6.13 ms ± 279 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Нельзя сказать, apply медленный, но явная итерация в этом случае быстрее. Кроме того, вы заметите, что второе и третье временное решение будет лучше масштабироваться при большей длине в / с, поскольку число итераций зависит от количества столбцов.

0 голосов
/ 15 января 2019

Вы можете сделать что-то вроде следующего:

res = df1.assign(a_sum=lambda df: df['a'].groupby(df2['a']).transform('sum'))\
         .assign(b_sum=lambda df: df['b'].groupby(df2['b']).transform('sum'))

Результаты:

   a   b
0  4  11
1  6  11
2  4  15
3  6  15
0 голосов
/ 15 января 2019

Попробуйте использовать apply, чтобы применить лямбда-функцию к каждому столбцу вашего фрейма данных, затем используйте имя этого pd.Series для группировки по второму фрейму данных:

df1.apply(lambda x: x.groupby(df2[x.name]).transform('sum'))

Выход:

   a   b
0  4  11
1  6  11
2  4  15
3  6  15
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...