Recherche de la plus grande surface sous une courbe | pandas, matplotlib

Sep 15 2020

J'ai un peu de mal dans celui-ci - afin de trouver la capacité de batterie nécessaire, j'ai besoin d'analyser la production / la demande sur une année de données. Pour ce faire, j'ai pensé que je devais calculer la plus grande surface sous la ligne 0. Je suppose que j'ai besoin de trouver les points de début / fin de cette zone et de tout multiplier par sa valeur y respective?

Voici une version abrégée du graphique que j'ai:

c'est la plus grande zone sous le 0 dans l'image, mais dans l'ensemble de données complet, cela peut être n'importe quelle zone. Je sais comment l'intégrer dans le cas où je trouve les limites de la zone en question mais j'ai du mal à trouver un moyen efficace de le faire.

Mon dataframe ressemble à ceci:

                     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

J'utilise cet extrait de code pour trouver la longueur en pas de temps de la zone la plus longue, mais je manque s'il existe un moyen de multiplier les points par leurs valeurs y (diff). Ce n'est techniquement pas correct , cependant, étant donné qu'une zone peut être longue mais étroite et une autre peut être plus courte et plus grande, donc avec une zone globale plus grande.

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

Réponses

1 MichaelSzczesny Sep 15 2020 at 16:47

Vous pouvez trouver la plus grande zone sous la ligne 0. J'ai généré mes propres données

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

Calculez maintenant les points de début et de fin des séquences positives et négatives. Chaque valeur d'une séquence reçoit un entier croissant pour pouvoir être groupée par séquence.

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

Utilisez pandas groupby pour calculer toutes les zones et trouver le plus grand négatif

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]));

Comment ça marche

Je crée un jeu de données plus facile à suivre

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--');

Je veux connaître les séquences avec des valeurs négatives ou positives consécutives. Cela peut être archivé avec le filtre 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

Maintenant, je peux trouver les indices où le signe change, quand je diffère chaque valeur consécutive. Je concat un False avant les données pour ne pas perdre la première valeur.

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

Avec cumsum()j'obtiens pour chaque séquence une valeur entière croissante. Maintenant, j'ai une variable de regroupement pour chaque séquence.

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

Pour chaque groupe, je peux calculer l'intégrale des valeurs du groupe.

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

Vous pouvez accéder à chaque groupe ultérieurement et utiliser les zones. Par exemple pour tracer les zones.

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)