J'ai un DataFrame de plages de dates (le DataFrame réel a plus de données attachées mais a les mêmes colonnes start et end). Les données doivent finalement être analysées semaine par semaine du dimanche au samedi. Ainsi, je voudrais parcourir le DataFrame et diviser toutes les plages de dates (start à finish) qui s'étendent du samedi au dimanche. Par exemple, étant donné le DataFrame:

import pandas as pd

date_ranges = [
    {'start': '2020-01-16 22:30:00', 'end': '2020-01-17 01:00:00'}, # spans thurs-fri, ok as is
    {'start': '2020-01-17 04:30:00', 'end': '2020-01-17 12:30:00'}, # no span, ok as is
    {'start': '2020-01-18 10:15:00', 'end': '2020-01-18 14:00:00'}, # no span, ok as is
    {'start': '2020-01-18 22:30:00', 'end': '2020-01-19 02:00:00'}  # spans sat-sun, must split
]
data_df = pd.DataFrame(date_ranges)

Je veux que mon résultat ressemble à:

result_ranges = [
    {'start': '2020-01-16 22:30:00', 'end': '2020-01-17 01:00:00'}, # spans thurs-fri, ok as is
    {'start': '2020-01-17 04:30:00', 'end': '2020-01-17 12:30:00'}, # no span, ok as is
    {'start': '2020-01-18 10:15:00', 'end': '2020-01-18 14:00:00'}, # no span, ok as is
    {'start': '2020-01-18 22:30:00', 'end': '2020-01-19 00:00:00'}, # split out saturday portion
    {'start': '2020-01-19 00:00:00', 'end': '2020-01-19 02:00:00'}  # and the sunday portion
]

result_df = pd.DataFrame(result_ranges)

Toute réflexion sur la façon de le faire efficacement chez les pandas serait grandement appréciée. Actuellement, je fais la mauvaise chose et j'itère sur les lignes, et c'est assez lent lorsque l'ensemble de données devient volumineux.

4
MarkD 17 janv. 2020 à 20:02

3 réponses

Meilleure réponse

Des manipulations comme celle-ci sont toujours difficiles et à un certain niveau, je pense qu'une boucle est nécessaire. Dans ce cas, au lieu de boucler sur les lignes, nous pouvons boucler sur les bords. Cela devrait conduire à un gain de performances assez important lorsque le nombre de semaines pendant lesquelles votre période de données est beaucoup plus petite que le nombre de lignes dont vous disposez.

Nous définissons les bords et modifions les points de terminaison DataFrame si nécessaire. En fin de compte, le DataFrame souhaité est tout ce qui reste du DataFrame que nous avons modifié, plus tous les intervalles de temps séparés que nous avons stockés dans l. L'index d'origine est conservé, vous pouvez donc voir exactement quelles lignes ont été divisées. Si un intervalle de temps unique chevauche N bords, il est divisé en N+1 lignes distinctes.

Installer

import pandas as pd

df[['start', 'end']]= df[['start', 'end']].apply(pd.to_datetime)

edges = pd.date_range(df.start.min().normalize() - pd.Timedelta(days=7),
                      df.end.max().normalize() + pd.Timedelta(days=7), freq='W-Sun')

Code

l = []
for edge in edges:
    m = df.start.lt(edge) & df.end.gt(edge)  # Rows to modify
    l.append(df.loc[m].assign(end=edge))     # Clip end of modified rows
    df.loc[m, 'start'] = edge                # Fix start for next edge

result = pd.concat(l+[df]).sort_values('start')

Production

                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 00:00:00
3 2020-01-19 00:00:00 2020-01-19 02:00:00
6
ALollz 17 janv. 2020 à 20:22

Ma solution est encore plus générale que vous avez définie, à savoir qu'elle crée une séquence de "lignes de semaine" à partir de chaque ligne source, même si les deux dates contenir entre eux par exemple deux pauses samedi / dimanche.

Pour vérifier que cela fonctionne, j'ai ajouté une telle ligne à votre DataFrame, afin qu'elle contienne:

                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 02:00:00
4 2020-01-25 20:30:00 2020-02-02 03:00:00

Notez que la dernière ligne comprend deux pauses samedi / dimanche, du 25.01 à 26.01 et de 1.02 à 2.02 .

Commencez par la conversion des deux colonnes en datetime :

data_df.start = pd.to_datetime(data_df.start)
data_df.end = pd.to_datetime(data_df.end)

Pour traiter vos données, définissez la fonction suivante, à appliquer à chaque ligne:

def weekRows(row):
    row.index = pd.DatetimeIndex(row)
    gr = row.resample('W-SUN', closed='left')
    ngr = gr.ngroups  # Number of groups
    i = 1
    data = []
    for key, grp in gr:
        dt1 = key - pd.Timedelta('7D')
        dt2 = key
        if i == 1:
            dt1 = row.iloc[0]
        if i == ngr:
            dt2 = row.iloc[1]
        data.append([dt1, dt2])
        i += 1
    return pd.DataFrame(data, columns=['start', 'end'])

Présentons "individuellement", comment il fonctionne sur les 2 dernières lignes:

Lorsque vous exécutez:

row = data_df.loc[3]
weekRows(row)

(pour la dernière ligne mais une), vous obtiendrez:

                start                 end
0 2020-01-18 22:30:00 2020-01-19 00:00:00
1 2020-01-19 00:00:00 2020-01-19 02:00:00

Et lorsque vous exécutez:

row = data_df.loc[4]
weekRows(row)

(pour le dernier), vous obtiendrez:

                start                 end
0 2020-01-25 20:30:00 2020-01-26 00:00:00
1 2020-01-26 00:00:00 2020-02-02 00:00:00
2 2020-02-02 00:00:00 2020-02-02 03:00:00

Et pour obtenir le résultat souhaité, exécutez:

result = pd.concat(data_df.apply(weekRows, axis=1).values, ignore_index=True)

Le résultat est:

                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 00:00:00
4 2020-01-19 00:00:00 2020-01-19 02:00:00
5 2020-01-25 20:30:00 2020-01-26 00:00:00
6 2020-01-26 00:00:00 2020-02-02 00:00:00
7 2020-02-02 00:00:00 2020-02-02 03:00:00

Les 3 premières lignes résultent de vos 3 premières lignes source. Deux lignes suivantes (index 3 et 4 ) résultent de la ligne source avec l'index 3 . Et les 3 dernières lignes (index 5 à 7 ) résultent de la dernière ligne source.

2
Valdi_Bo 17 janv. 2020 à 20:13

Semblable à la @ réponse de Valdi_Bo, j'ai cherché à décomposer un seul intervalle de (start, end) en une série d'intervalles, y compris tous les midis du dimanche entre les deux.

Ceci est accompli par la fonction suivante:

def break_weekly(start, end):
    edges = list(pd.date_range(start, end, freq='W', normalize=True, closed='right'))
    if edges and edges[-1] == end:
        edges.pop()
    return pd.Series(list(zip([start] + edges, edges + [end])))

Ce code créera une plage de dates hebdomadaire de "début" à "fin", normalisant à minuit (donc dimanche minuit) et gardera l'intervalle ouvert à gauche (il commence donc le dimanche suivant le début.)

Il y a un cas d'angle pour quand "fin" est exactement minuit dimanche, puisque l'intervalle doit être fermé d'un côté, nous le gardons fermé à droite, donc nous vérifions si ces deux correspondent et le supprimons s'ils sont les mêmes.

Nous utilisons ensuite zip() pour créer des tuples avec chaque paire des dates, y compris le "début" au début à gauche, et l'horodatage "fin" à la fin de la droite.

Nous retournons finalement un pd.Series de ces tuples, car cela fait apply() faire ce que nous attendons.

Exemple d'utilisation:

>>> break_weekly(pd.Timestamp('2020-01-18 22:30:00'), pd.Timestamp('2020-01-19 02:00:00'))
0    (2020-01-18 22:30:00, 2020-01-19 00:00:00)
1    (2020-01-19 00:00:00, 2020-01-19 02:00:00)
dtype: object

À ce stade, vous pouvez l'appliquer au bloc de données d'origine pour trouver la liste complète des intervalles.

Tout d'abord, convertissez les types de colonnes en pd.Timestamp (vous avez des chaînes dans les colonnes de votre exemple):

data_df = data_df.apply(pd.to_datetime)

Ensuite, vous pouvez trouver la liste complète des intervalles avec:

intervals = (data_df
    .apply(lambda r: break_weekly(r.start, r.end), axis=1)
    .unstack().dropna().reset_index(level=0, drop=True)
    .apply(lambda r: pd.Series(r, index=['start', 'end'])))

La première étape applique break_weekly() aux colonnes "début" et "fin", ligne par ligne. Puisque break_weekly() renvoie un pd.Series, il finira par produire un nouveau DataFrame avec une colonne par intervalle de dates (autant qu'il y a de semaines dans un intervalle).

Ensuite, unstack() fusionnera ces colonnes et dropna() supprimera le NaN qui a été généré car chaque ligne avait un nombre différent de colonnes (nombre d'intervalles différent pour chaque ligne).

À ce stade, nous avons un multi-index, donc reset_index(level=0, drop=True) supprimera le niveau d'index qui nous importe peu et ne gardera que celui qui correspond au DataFrame d'origine.

Enfin, le dernier apply() convertira les entrées des tuples Python en pd.Series et nommera à nouveau les colonnes "start" et "end".

En regardant le résultat jusqu'à ce point:

>>> intervals
                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 00:00:00
3 2020-01-19 00:00:00 2020-01-19 02:00:00

Étant donné que les indices correspondent à ceux de votre DataFrame d'origine, vous pouvez maintenant utiliser ce DataFrame pour le reconnecter à votre original, si vous aviez plus de colonnes avec des valeurs et que vous souhaitez les dupliquer ici, il suffit de les réunir .

Par exemple:

>>> data_df['value'] = ['abc', 'def', 'ghi', 'jkl']
>>> intervals.join(df.drop(['start', 'end'], axis=1))
                start                 end value
0 2020-01-16 22:30:00 2020-01-17 01:00:00   abc
1 2020-01-17 04:30:00 2020-01-17 12:30:00   def
2 2020-01-18 10:15:00 2020-01-18 14:00:00   ghi
3 2020-01-18 22:30:00 2020-01-19 00:00:00   jkl
3 2020-01-19 00:00:00 2020-01-19 02:00:00   jkl

Vous remarquerez que la valeur de la dernière ligne a été copiée dans les deux lignes de cet intervalle.

0
filbranden 17 janv. 2020 à 22:44