Encontrando a maior área sob uma curva | pandas, matplotlib

Sep 15 2020

Estou lutando um pouco com este aqui - para encontrar a capacidade necessária da bateria, preciso analisar a produção / demanda ao longo de um ano de dados. Para fazer isso, descobri que preciso calcular a maior área sob a linha 0. Acho que preciso encontrar os pontos inicial / final dessa área e multiplicar tudo por seu respectivo valor y.

Aqui está uma versão abreviada do gráfico que tenho:

Essa é a maior área abaixo do 0 na imagem, mas no conjunto de dados completo pode ser qualquer área. Sei como integrá-lo no caso de encontrar os limites da área em questão, mas estou lutando para encontrar uma maneira eficiente de fazer isso.

Meu dataframe é parecido com este:

                     demand  Production    diff
Time
2019-01-01 00:15:01   17.25      32.907  15.657
2019-01-01 00:30:01   17.80      32.954  15.154
...                     ...         ...     ...
2019-01-16 22:15:02   17.34      27.704  10.364
2019-01-16 22:30:01   18.67      35.494  16.824

Eu uso este trecho para encontrar a duração em passos de tempo da área mais longa, mas estou perdendo se há uma maneira de multiplicar os pontos por seus valores y (diff). Não é tecnicamente correto , entretanto, considerando que uma área pode ser longa, mas estreita, e outra pode ser mais curta e alta, portanto, com uma área geral maior.

def max0(sr):
     return (sr >= 0).cumsum().value_counts().max() - (0 if (sr >= 0).cumsum().value_counts().idxmax() < 0 else 1)

Respostas

1 MichaelSzczesny Sep 15 2020 at 16:47

Você pode encontrar a maior área sob a linha 0. Eu gerei meus próprios dados

x = np.random.randn(100000)
x = x.cumsum()-x.mean()
plt.plot(x);

Agora calcule os pontos inicial e final para sequências positivas e negativas. Cada valor em uma sequência recebe um número inteiro crescente para poder agrupar por sequência.

x1 = np.diff(x < 0).cumsum()

Use pandas groupby para calcular todas as áreas e encontrar o maior negativo

df = pd.DataFrame({
    'value': x[1:],
    'border': x1
})
dfg = df.groupby('border')
mingr = dfg.apply(lambda x: np.trapz(x.value)).idxmin()
plt.plot(x[1:])
plt.plot(
    dfg.get_group(mingr).value
);
plt.title(
    "position from {} to {}".format(
        dfg.get_group(mingr).index[0],
        dfg.get_group(mingr).index[-1]));

Como isso funciona

Eu crio um conjunto de dados que é mais fácil de acompanhar

x = np.array([3,4,4.5,3,2])
X = np.r_[x,-x,x,-x]+np.random.normal(0,.2,20)
plt.figure(figsize=(12,5))
plt.axhline(0, color='gray')
plt.plot(X, 'o--');

Quero saber as sequências com valores consecutivos negativos ou positivos. Isso pode ser arquivado com o filtro X <0.

df = pd.DataFrame({'value': X, 'lt_zero': X < 0})
df[:10]
      value  lt_zero
0  3.125986    False
1  3.885588    False
2  4.580410    False
3  2.998920    False
4  1.913088    False
5 -2.902447     True
6 -3.986654     True
7 -4.373026     True
8 -2.878661     True
9 -1.929964     True

Agora posso encontrar os índices onde o sinal muda, quando difiro todos os valores consecutivos. Eu concato um False antes dos dados para não perder o primeiro valor.

df['sign_switch'] = np.diff(np.r_[False, X < 0])
df[:10]
      value  lt_zero  sign_switch
0  3.125986    False        False
1  3.885588    False        False
2  4.580410    False        False
3  2.998920    False        False
4  1.913088    False        False
5 -2.902447     True         True
6 -3.986654     True        False
7 -4.373026     True        False
8 -2.878661     True        False
9 -1.929964     True        False

Com cumsum()eu obtenho para cada sequência um valor inteiro crescente. Agora tenho uma variável de agrupamento para cada sequência.

df['sign_sequence'] = np.diff(np.r_[False, X < 0]).cumsum()
df[:10]
      value  lt_zero  sign_switch  sign_sequence
0  3.125986    False        False              0
1  3.885588    False        False              0
2  4.580410    False        False              0
3  2.998920    False        False              0
4  1.913088    False        False              0
5 -2.902447     True         True              1
6 -3.986654     True        False              1
7 -4.373026     True        False              1
8 -2.878661     True        False              1
9 -1.929964     True        False              1

Para cada grupo, posso calcular a integral para os valores do grupo.

sign_groups = df.groupby('sign_sequence')
sign_groups.apply(lambda x: np.trapz(x.value))
sign_sequence
0    13.984455
1   -13.654547
2    14.370044
3   -14.549090

Você pode acessar cada grupo mais tarde e usar as áreas. Por exemplo, para plotar as áreas.

plt.figure(figsize=(12,5))
plt.plot(X,'o--')
plt.axhline(0, c='gray')
for e,group in enumerate(sign_groups):
    plt.fill_between(group[1].index,0, group[1].value)
    area = np.trapz(group[1].value)
    plt.text((e)*5+1.5, np.sign(area) * 1.25, f'{area:.2f}', fontsize=12)