Pandas Dataframe - корзина на несколько столбцов и получить статистику по другому столбцу - PullRequest
7 голосов
/ 21 февраля 2020

Проблема

У меня есть целевая переменная x и некоторые дополнительные переменные A и B. Я хочу рассчитать средние значения (и другие статистические данные) x, когда выполняются определенные условия для A и B. Примером из реальной жизни может быть вычисление средней температуры воздуха (x) из длинной серии измерений, когда солнечная радиация (A) и скорость ветра (B) попадают в определенные заранее определенные диапазоны бункера.

Потенциальные решения

Мне удалось выполнить sh с помощью циклов (см. Пример ниже), но я узнал, что мне следует избегать циклирования по фреймам данных. Из моих исследований на этом сайте я чувствую, что, вероятно, существует гораздо более элегантное / векторизованное решение, использующее либо pd.cut, либо np.select, но я, честно говоря, не мог понять, как это сделать.

Пример

Генерация выборочных данных

import pandas as pd
import numpy as np

n = 100
df = pd.DataFrame(
    {
        "x": np.random.randn(n),
        "A": np.random.randn(n)+5,
        "B": np.random.randn(n)+10
    }
)

df.head() output:

    x           A           B
0   -0.585313   6.038620    9.909762
1   0.412323    3.991826    8.836848
2   0.211713    5.019520    9.667349
3   0.710699    5.353677    9.757903
4   0.681418    4.452754    10.647738

Вычисление средних значений для бинов

# define bin ranges
bins_A = np.arange(3, 8)
bins_B = np.arange(8, 13)

# prepare output lists
A_mins= []
A_maxs= []
B_mins= []
B_maxs= []
x_means= []
x_stds= []
x_counts= []

# loop over bins
for i_A in range(0, len(bins_A)-1):
    A_min = bins_A[i_A]
    A_max = bins_A[i_A+1]
    for i_B in range(0, len(bins_B)-1):
        B_min = bins_B[i_B]
        B_max = bins_B[i_B+1]

        # binning conditions for current step
        conditions = np.logical_and.reduce(
            [
                df["A"] > A_min,
                df["A"] < A_max,
                df["B"] > B_min,
                df["B"] < B_max,
            ]
        )

        # calculate statistics for x and store values in lists
        x_values = df.loc[conditions, "x"]
        x_means.append(x_values.mean())
        x_stds.append(x_values.std())
        x_counts.append(x_values.count())

        A_mins.append(A_min)
        A_maxs.append(A_max)
        B_mins.append(B_min)
        B_maxs.append(B_max)

Сохранение результата в новом кадре данных

binned = pd.DataFrame(
    data={
        "A_min": A_mins,
        "A_max": A_maxs,
        "B_min": B_mins,
        "B_max": B_maxs,
        "x_mean": x_means,
        "x_std": x_stds,
        "x_count": x_counts 
        }
)

binned.head() вывод:

    A_min   A_max   B_min   B_max   x_mean      x_std       x_count
0   3       4       8       9       0.971624    0.790972    2
1   3       4       9       10      0.302795    0.380102    3
2   3       4       10      11      0.447398    1.787659    5
3   3       4       11      12      0.462149    1.195844    2
4   4       5       8       9       0.379431    0.983965    4

Ответы [ 3 ]

6 голосов
/ 25 февраля 2020

Подход № 1: Pandas + NumPy (некоторые - нет)

Мы попытаемся сохранить его в pandas / NumPy, чтобы мы могли использовать методы dataframe или массива и ufuncs, в то время как векторизация на своем уровне. Это облегчает расширение функциональных возможностей, когда нужно решать сложные проблемы или генерировать статистику, как это, кажется, имеет место здесь.

Теперь, чтобы решить проблему, сохраняя ее близкой к pandas , будет генерировать промежуточные идентификаторы или теги, которые напоминают комбинированное отслеживание A и B на заданных бинах bins_A и bins_B соответственно. Для этого одним из способов было бы использовать searchsorted для этих двух данных отдельно -

tagsA = np.searchsorted(bins_A,df.A)
tagsB = np.searchsorted(bins_B,df.B)

Теперь нас интересуют только случаи внутри границ, поэтому необходимо маскирование -

vm = (tagsB>0) & (tagsB<len(bins_B)) & (tagsA>0) & (tagsA<len(bins_A))

Давайте применим эту маску к исходному фрейму данных -

dfm = df.iloc[vm]

Добавьте теги для допустимых, которые будут представлять A_mins и B_min эквивалентов и, следовательно, будут отображаться в конечном выводе -

dfm['TA'] = bins_A[(tagsA-1)[vm]]
dfm['TB'] = bins_B[(tagsB-1)[vm]]

Итак, наш тегированный фрейм данных готов, что может составить describe-d для получения общей статистики после группировки по этим двум тегам -

df_out = dfm.groupby(['TA','TB'])['x'].describe()

Пример прогона, чтобы прояснить ситуацию при сравнении с опубликованным решением -

In [46]: np.random.seed(0)
    ...: n = 100
    ...: df = pd.DataFrame(
    ...:     {
    ...:         "x": np.random.randn(n),
    ...:         "A": np.random.randn(n)+5,
    ...:         "B": np.random.randn(n)+10
    ...:     }
    ...: )

In [47]: binned
Out[47]: 
    A_min  A_max  B_min  B_max    x_mean     x_std  x_count
0       3      4      8      9  0.400199  0.719007        5
1       3      4      9     10 -0.268252  0.914784        6
2       3      4     10     11  0.458746  1.499419        5
3       3      4     11     12  0.939782  0.055092        2
4       4      5      8      9  0.238318  1.173704        5
5       4      5      9     10 -0.263020  0.815974        8
6       4      5     10     11 -0.449831  0.682148       12
7       4      5     11     12 -0.273111  1.385483        2
8       5      6      8      9 -0.438074       NaN        1
9       5      6      9     10 -0.009721  1.401260       16
10      5      6     10     11  0.467934  1.221720       11
11      5      6     11     12  0.729922  0.789260        3
12      6      7      8      9 -0.977278       NaN        1
13      6      7      9     10  0.211842  0.825401        7
14      6      7     10     11 -0.097307  0.427639        5
15      6      7     11     12  0.915971  0.195841        2

In [48]: df_out
Out[48]: 
       count      mean       std  ...       50%       75%       max
TA TB                             ...                              
3  8     5.0  0.400199  0.719007  ...  0.302472  0.976639  1.178780
   9     6.0 -0.268252  0.914784  ... -0.001510  0.401796  0.653619
   10    5.0  0.458746  1.499419  ...  0.462782  1.867558  1.895889
   11    2.0  0.939782  0.055092  ...  0.939782  0.959260  0.978738
4  8     5.0  0.238318  1.173704  ... -0.212740  0.154947  2.269755
   9     8.0 -0.263020  0.815974  ... -0.365103  0.449313  0.950088
   10   12.0 -0.449831  0.682148  ... -0.436773 -0.009697  0.761038
   11    2.0 -0.273111  1.385483  ... -0.273111  0.216731  0.706573
5  8     1.0 -0.438074       NaN  ... -0.438074 -0.438074 -0.438074
   9    16.0 -0.009721  1.401260  ...  0.345020  1.284173  1.950775
   10   11.0  0.467934  1.221720  ...  0.156349  1.471263  2.240893
   11    3.0  0.729922  0.789260  ...  1.139401  1.184846  1.230291
6  8     1.0 -0.977278       NaN  ... -0.977278 -0.977278 -0.977278
   9     7.0  0.211842  0.825401  ...  0.121675  0.398750  1.764052
   10    5.0 -0.097307  0.427639  ... -0.103219  0.144044  0.401989
   11    2.0  0.915971  0.195841  ...  0.915971  0.985211  1.054452

Итак, как уже упоминалось ранее, у нас есть A_min и B_min в TA и TB, а соответствующие статистические данные фиксируются в других заголовках. Обратите внимание, что это будет мультииндексный фрейм данных. Если нам нужно захватить эквивалентные данные массива, просто выполните: df_out.loc[:,['count','mean','std']].values для статистики, а np.vstack(df_out.loc[:,['count','mean','std']].index) для интервала запуска бинов.

В качестве альтернативы, для захвата эквивалентных данных статистики без describe, но с использованием методов dataframe, мы можем сделать что-то вроде этого -

dfmg = dfm.groupby(['TA','TB'])['x']
dfmg.size().unstack().values
dfmg.std().unstack().values
dfmg.mean().unstack().values

Альтернатива # 1: Использование pd.cut

Мы также можем использовать pd.cut, как было предложено в вопросе, для замены searchsorted на более компактный, так как вне границ обрабатываются автоматически, сохраняя базовые значения * 1079. * идея такая же -

df['TA'] = pd.cut(df['A'],bins=bins_A, labels=range(len(bins_A)-1))
df['TB'] = pd.cut(df['B'],bins=bins_B, labels=range(len(bins_B)-1))
df_out = df.groupby(['TA','TB'])['x'].describe()

Итак, это дает нам статистику. Для A_min и B_min эквивалентов просто используйте уровни индекса -

A_min = bins_A[df_out.index.get_level_values(0)]
B_min = bins_B[df_out.index.get_level_values(1)]

или используйте какой-нибудь метод сетки -

mA,mB = np.meshgrid(bins_A[:-1],bins_B[:-1])
A_min,B_min = mA.ravel('F'),mB.ravel('F')

Подход № 2: С bincount

Мы можем использовать np.bincount, чтобы получить все эти три значения статистики metri c, включая стандартное отклонение, снова в векторизации -

lA,lB = len(bins_A),len(bins_B)
n = lA+1

x,A,B = df.x.values,df.A.values,df.B.values

tagsA = np.searchsorted(bins_A,A)
tagsB = np.searchsorted(bins_B,B)

t = tagsB*n + tagsA

L = n*lB

countT = np.bincount(t, minlength=L)
countT_x = np.bincount(t,x, minlength=L)
avg_all = countT_x/countT
count = countT.reshape(-1,n)[1:,1:-1].ravel('F')
avg = avg_all.reshape(-1,n)[1:,1:-1].ravel('F')

# Using numpy std definition for ddof case
ddof = 1.0 # default one for pandas std
grp_diffs = (x-avg_all[t])**2
std_all = np.sqrt(np.bincount(t,grp_diffs, minlength=L)/(countT-ddof))
stds = std_all.reshape(-1,n)[1:,1:-1].ravel('F')

Подход № 3: С sorting для плечо reduceat методов -

x,A,B = df.x.values,df.A.values,df.B.values
vm = (A>bins_A[0]) & (A<bins_A[-1]) & (B>bins_B[0]) & (B<bins_B[-1])

xm = x[vm]

tagsA = np.searchsorted(bins_A,A)
tagsB = np.searchsorted(bins_B,B)

tagsAB = tagsB*(tagsA.max()+1) + tagsA
tagsABm = tagsAB[vm]
sidx = tagsABm.argsort()
tagsAB_s = tagsABm[sidx]
xms = xm[sidx]

cut_idx = np.flatnonzero(np.r_[True,tagsAB_s[:-1]!=tagsAB_s[1:],True])
N = (len(bins_A)-1)*(len(bins_B)-1)

count = np.diff(cut_idx)
avg = np.add.reduceat(xms,cut_idx[:-1])/count
stds = np.empty(N)
for ii,(s0,s1) in enumerate(zip(cut_idx[:-1],cut_idx[1:])):
    stds[ii] = np.std(xms[s0:s1], ddof=1)

Чтобы получить тот же или аналогичный формат, что и вывод в стиле pandas в виде информационного кадра, нам нужно изменить форму. Следовательно, это будет avg.reshape(-1,len(bins_A)-1).T и т. Д.

3 голосов
/ 25 февраля 2020

Если вас интересует производительность , вы можете использовать циклы for с небольшими изменениями, если вы используете numba

Здесь у вас есть функция, которая выполняет расчеты. Ключ в том, что calculate использует numba, поэтому он действительно быстрый. Остальное только для создания pandas фрейма данных:

from numba import njit

def calc_numba(df, bins_A, bins_B):
    """ wrapper for the timeit. It only creates a dataframe """

    @njit
    def calculate(A, B, x, bins_A, bins_B):

        size = (len(bins_A) - 1)*(len(bins_B) - 1)
        out = np.empty((size, 7))

        index = 0
        for i_A, A_min in enumerate(bins_A[:-1]):
            A_max = bins_A[i_A + 1]

            for i_B, B_min in enumerate(bins_B[:-1]):
                B_max = bins_B[i_B + 1]

                mfilter = (A_min < A)*(A < A_max)*(B_min < B)*(B < B_max)
                x_values = x[mfilter]

                out[index, :] = [
                    A_min,
                    A_max,
                    B_min,
                    B_max,
                    x_values.mean(),
                    x_values.std(),
                    len(x_values)
                ]

                index += 1

        return out

    columns = ["A_min", "A_max", "B_min", "B_max", "mean", "std", "count"]
    out = calculate(df["A"].values, df["B"].values, df["x"].values, bins_A, bins_B)
    return pd.DataFrame(out, columns=columns)

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

Используя n = 1_000_000 и те же bins_A и bins_B мы получаем:

%timeit code_question(df, bins_A, bins_B)
15.7 s ± 428 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit calc_numba(df, bins_A, bins_B)
507 ms ± 12.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Это примерно на 30 быстрее, чем код из вопроса

Будет очень трудно превзойти производительность numba, поскольку встроенные методы pandas использует аналогичные улучшения.

2 голосов
/ 26 февраля 2020

Вот краткое решение, использующее только Numpy и pandas. Это, конечно, не самый эффективный способ, но я думаю, что самый простой и понятный способ.

import pandas as pd
import numpy as np


n = 20
df = pd.DataFrame(
    {
        "x": np.random.randn(n),
        "A": np.random.randn(n)+5,
        "B": np.random.randn(n)+10
    }
)

# define bin ranges
bins_A = np.arange(3, 8)
bins_B = np.arange(8, 13)

До сих пор я использую ваш пример. Затем я представляю нижний и верхний края бина, используя numpy

A_mins=bins_A[:-1]
A_maxs=bins_A[1:]
B_mins=bins_B[:-1]
B_maxs=bins_B[1:]

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

A_mins_list=np.repeat(A_mins,len(B_mins))
A_maxs_list=np.repeat(A_maxs,len(B_mins))
B_mins_list=np.tile(B_mins,len(A_mins))
B_maxs_list=np.tile(B_maxs,len(A_mins))

Новый информационный кадр инициализируется с информацией о корзине.

newdf=pd.DataFrame(np.array([A_mins_list,A_maxs_list,B_mins_list,B_maxs_list]).T,columns=['Amin','Amax','Bmin','Bmax'])

Столбец xvalues самый злой здесь, так как я должен сделать его массивом numpy, чтобы поместиться в фрейм данных. Этот под-массив тогда является массивом numpy и, кроме того, должен рассматриваться как один. Помните об этом, так как некоторые функции pandas могут не работать; в некоторых случаях это должна быть функция numpy.

newdf['xvalues']=newdf.apply(lambda row:np.array(df.x[(row.Amin<df.A) & (row.Amax>df.A) & (row.Bmin<df.B) & (row.Bmax>df.B)]),axis=1)

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

newdf['xmeans']=newdf.apply(lambda row: row.xvalues.mean(),axis=1)
newdf['stds']=newdf.apply(lambda row: row.xvalues.std(),axis=1)
newdf['xcounts']=newdf.apply(lambda row: row.xvalues.size,axis=1)

или как вам угодно.

Используя cython, производительность может быть значительно улучшена за счет отказа от лямбда-пути, но я не привык к cython, поэтому я скорее оставлю это экспертам .. .

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

...