Como agrupar uma coluna polarizada em vários intervalos de outra nos pandas?

Dec 12 2020

Eu tenho o seguinte pd.DataFrame:

source = pd.DataFrame([[0.99, 0.98, 0.93, 0.81, 0.85, 0.71, 0.7, 0.69, 0.68, 0.66], 
              [100, 12, 312, 23, 2, 12, 32, 21, 21, 21]]).T

Eu gostaria de convertê-lo o mais rápido possível para:

desired_result = pd.DataFrame([[0.9, 0.8, 0.7, 0.6], [424, 25, 44, 63]]).T

Onde acima, eu defino um intervalo do 0.1qual aplico a coluna 0ao sourcedataframe e soma a 1coluna do mesmo dataframe. A ideia é que funcione com intervalos diferentes.

O que eu tentei:

  1. Pensei em usar pd.cutmas não parece ser o que procuro.

  2. Eu sei que se eu adicionar uma nova coluna sourcecom valores duplicados de 0,9, 0,8, 0,7 e 0,6 nas linhas correspondentes, posso usar groupbynesta nova coluna e então sum, mas estou me perguntando se há uma maneira mais rápida e limpa para fazer isso? por exemplo, smth assim:

interval = 0.1
source['ints'] = (source[0] / interval).astype(int)
result = source.groupby(source['ints']).sum().reset_index()
result

No entanto, o acima exposto não funcionaria se eu alterasse o intervalo de 0,1 para 0,05, por exemplo.

Qualquer ajuda seria apreciada.

Respostas

3 PierreD Dec 12 2020 at 21:28

Para velocidade: sempre tente vetorizar tudo o que puder e evitar apply o máximo possível.

Aqui está uma maneira mais rápida (crédito para @DavidErickson por sort=False):

interval = 0.1
source.groupby(np.trunc(source[0] / interval) * interval, sort=False)[1].sum().reset_index()
# out:
     0      1
0  0.9  424.0
1  0.8   25.0
2  0.7   12.0
3  0.6   95.0

A diferença de velocidade pode ser bastante dramática para grandes df.

Experimente com 1 milhão de linhas, agrupadas em caixas de 10K:

source = pd.DataFrame(np.random.normal(scale=1000, size=(int(1e6), 2)))

%%timeit
# ... (as above)
26.7 ms ± 292 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Com um em applyvez:

1.51 s ± 11 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

(50x mais lento).

1 DavidErickson Dec 12 2020 at 20:55

Você pode usar uma custom_roundfunção da qual fiz 3 modificaçõeshttps://stackoverflow.com/a/40372261/6366770:

  1. Usei em np.floorvez de roundcomo você quer descer.
  2. Isso bagunça os valores que estão na "borda" de uma caixa, então eu adiciono + base/100(assim 0.9seria 0.9 + .009 = 0.909e arredondaria para baixo para 0,9 em vez de incorretamente para 0,8), para que ficasse logo acima da borda e arredondasse para baixo corretamente. Acho que isso vai cobrir você. Você pode fazer 1 / 1000para estar seguro.
  3. A resposta que eu compartilho estava procurando int, então removida int, já que estamos olhando para flutuadores arredondados

source = pd.DataFrame(np.random.normal(scale=1000, size=(int(1e6), 2)))

def custom_round(x, y, base):
    return source.groupby((base * np.floor((x + (base / 100)) / base)), sort=False)[y].sum()


%timeit custom_round(source[0], 1, .1)
89.8 ms ± 1.14 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

No meu computador, a resposta aceita é mais lenta:

102 ms ± 1.86 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)