web-dev-qa-db-fra.com

Python - Pour des millions de lignes en boucle

J'ai un dataframe c avec beaucoup de colonnes différentes. De plus, arr est une trame de données qui correspond à un sous-ensemble de c: arr = c[c['A_D'] == 'A'].

L'idée principale de mon code est d'itérer sur toutes les lignes du cadre de données c- et de rechercher tous les cas possibles (dans le cadre de données arr) où certaines conditions spécifiques doivent se produire:

  • Il est seulement nécessaire d'itérer sur les lignes étaient c['A_D'] == D Et c['Already_linked'] == 0
  • Le hour dans le cadre de données arr doit être inférieur au hour_aux Dans le cadre de données c
  • La colonne Already_linked De la trame de données arr doit être nulle: arr.Already_linked == 0
  • Terminal et Operator doivent être les mêmes dans le cadre de données c et arr

À l'heure actuelle, les conditions sont stockées à l'aide de l'indexation booléenne et de groupby get_group:

  • Regroupez la base de données arr afin de choisir le même opérateur et terminal: g = groups.get_group((row.Operator, row.Terminal))
  • Choisissez uniquement les arrivées où l'heure est inférieure à l'heure dans le cadre de données c et où Déjà_lié == 0: vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]

Pour chacune des lignes de la trame de données c qui vérifient toutes les conditions, une trame de données vb est créée. Naturellement, cette trame de données a des longueurs différentes à chaque itération. Après avoir créé le cadre de données vb, mon objectif est de choisir l'index du cadre de données vb qui minimise le temps entre vb.START Et c [x]. Le FightID qui correspond à cet index est ensuite stocké dans le cadre de données c sur la colonne a. De plus, l'arrivée étant liée à un départ, la colonne Already_linked Dans le cadre de données arr est passée de 0 à 1.

Il est important de noter que la colonne Already_linked Du cadre de données arr peut changer à chaque itération (et arr.Already_linked == 0 Est l'une des conditions pour créer le vb trame de données). Par conséquent, il n'est pas possible de paralléliser ce code.

J'ai déjà utilisé c.itertuples() pour plus d'efficacité, mais comme c a des millions de lignes, ce code prend encore trop de temps.

Une autre option serait également d'utiliser pd.apply Pour chaque ligne. Néanmoins, ce n'est pas vraiment simple car dans chaque boucle il y a des valeurs qui changent à la fois c et arr (aussi, je crois que même avec pd.apply Ce serait extrêmement lent ).

Existe-t-il un moyen possible de convertir cela pour la boucle dans une solution vectorisée (ou de réduire le temps d'exécution de 10X (si possible encore plus))?

Trame de données initiale:

START     END       A_D     Operator     FlightID    Terminal   TROUND_ID   tot
0   2017-03-26 16:55:00 2017-10-28 16:55:00 A   QR  QR001   4   QR002       70
1   2017-03-26 09:30:00 2017-06-11 09:30:00 D   DL  DL001   3   "        "  84
2   2017-03-27 09:30:00 2017-10-28 09:30:00 D   DL  DL001   3   "        "  78
3   2017-10-08 15:15:00 2017-10-22 15:15:00 D   VS  VS001   3   "        "  45
4   2017-03-26 06:50:00 2017-06-11 06:50:00 A   DL  DL401   3   "        "  9
5   2017-03-27 06:50:00 2017-10-28 06:50:00 A   DL  DL401   3   "        "  19
6   2017-03-29 06:50:00 2017-04-19 06:50:00 A   DL  DL401   3   "        "  3
7   2017-05-03 06:50:00 2017-10-25 06:50:00 A   DL  DL401   3   "        "  32
8   2017-06-25 06:50:00 2017-10-22 06:50:00 A   DL  DL401   3   "        "  95
9   2017-03-26 07:45:00 2017-10-28 07:45:00 A   DL  DL402   3   "        "  58

Sortie souhaitée (certaines des colonnes ont été exclues dans la trame de données ci-dessous. Seules les colonnes a et Already_linked Sont pertinentes):

    START                    END             A_D  Operator  a   Already_linked
0   2017-03-26 16:55:00 2017-10-28 16:55:00 A   QR  0               1
1   2017-03-26 09:30:00 2017-06-11 09:30:00 D   DL  DL402           1
2   2017-03-27 09:30:00 2017-10-28 09:30:00 D   DL  DL401           1
3   2017-10-08 15:15:00 2017-10-22 15:15:00 D   VS  No_link_found   0
4   2017-03-26 06:50:00 2017-06-11 06:50:00 A   DL  0               0
5   2017-03-27 06:50:00 2017-10-28 06:50:00 A   DL  0               1
6   2017-03-29 06:50:00 2017-04-19 06:50:00 A   DL  0               0
7   2017-05-03 06:50:00 2017-10-25 06:50:00 A   DL  0               0
8   2017-06-25 06:50:00 2017-10-22 06:50:00 A   DL  0               0
9   2017-03-26 07:45:00 2017-10-28 07:45:00 A   DL  0               1

Code:

groups = arr.groupby(['Operator', 'Terminal'])
for row in c[(c.A_D == "D") & (c.Already_linked == 0)].itertuples():
    try:
        g = groups.get_group((row.Operator, row.Terminal))
        vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]
        aux = (vb.START - row.x).abs().idxmin()
        c.loc[row.Index, 'a'] = vb.loc[aux].FlightID
        arr.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

c['Already_linked'] = np.where((c.a != 0) & (c.a != 'No_link_found') & (c.A_D == 'D'), 1, c['Already_linked'])
c.Already_linked.loc[arr.Already_linked.index] = arr.Already_linked
c['a'] = np.where((c.Already_linked  == 0) & (c.A_D == 'D'),'No_link_found',c['a'])

Code pour la trame de données initiale c:

import numpy as np
import pandas as pd
import io

s = '''
 A_D     Operator     FlightID    Terminal   TROUND_ID   tot
 A   QR  QR001   4   QR002       70
 D   DL  DL001   3   "        "  84
 D   DL  DL001   3   "        "  78
 D   VS  VS001   3   "        "  45
 A   DL  DL401   3   "        "  9
 A   DL  DL401   3   "        "  19
 A   DL  DL401   3   "        "  3
 A   DL  DL401   3   "        "  32
 A   DL  DL401   3   "        "  95
 A   DL  DL402   3   "        "  58
'''

data_aux = pd.read_table(io.StringIO(s), delim_whitespace=True)
data_aux.Terminal = data_aux.Terminal.astype(str)
data_aux.tot= data_aux.tot.astype(str)

d = {'START': ['2017-03-26 16:55:00', '2017-03-26 09:30:00','2017-03-27 09:30:00','2017-10-08 15:15:00',
           '2017-03-26 06:50:00','2017-03-27 06:50:00','2017-03-29 06:50:00','2017-05-03 06:50:00',
           '2017-06-25 06:50:00','2017-03-26 07:45:00'], 'END': ['2017-10-28 16:55:00' ,'2017-06-11 09:30:00' ,
           '2017-10-28 09:30:00' ,'2017-10-22 15:15:00','2017-06-11 06:50:00' ,'2017-10-28 06:50:00', 
           '2017-04-19 06:50:00' ,'2017-10-25 06:50:00','2017-10-22 06:50:00' ,'2017-10-28 07:45:00']}    

aux_df = pd.DataFrame(data=d)
aux_df.START = pd.to_datetime(aux_df.START)
aux_df.END = pd.to_datetime(aux_df.END)
c = pd.concat([aux_df, data_aux], axis = 1)
c['A_D'] = c['A_D'].astype(str)
c['Operator'] = c['Operator'].astype(str)
c['Terminal'] = c['Terminal'].astype(str)

c['hour'] = pd.to_datetime(c['START'], format='%H:%M').dt.time
c['hour_aux'] = pd.to_datetime(c['START'] - pd.Timedelta(15, unit='m'), 
format='%H:%M').dt.time
c['start_day'] = c['START'].astype(str).str[0:10]
c['end_day'] = c['END'].astype(str).str[0:10]
c['x'] = c.START -  pd.to_timedelta(c.tot.astype(int), unit='m')
c["a"] = 0
c["Already_linked"] = np.where(c.TROUND_ID != "        ", 1 ,0)

arr = c[c['A_D'] == 'A']
17
Miguel Lambelho

Cette solution utilise pd.DataFrame.isin qui utilise numpy.in1d

Apparemment, "isin" n'est pas nécessairement plus rapide pour les petits ensembles de données (comme cet exemple), mais est beaucoup plus rapide pour les grands ensembles de données. Vous devrez l'exécuter avec vos données pour déterminer les performances.

flight_record_linkage.ipynb

Développement de l'ensemble de données à l'aide de c = pd.concat([c] * 10000, ignore_index=True)

  • Augmentez la longueur de l'ensemble de données de 3 ordres de grandeur (10000 lignes au total).
    • Méthode originale: Durée du mur: 8,98 s
    • Nouvelle méthode: durée du mur: 16,4 s
  • Augmentez la longueur de l'ensemble de données de 4 ordres de grandeur (100 000 lignes au total).
    • Méthode originale: Durée du mur: 8min 17s
    • Nouvelle méthode: Durée du mur: 1min 14s
  • Augmentez la longueur de l'ensemble de données de 5 ordres de grandeur (1000000 lignes au total).
    • Nouvelle méthode: Durée du mur: 11min 33s

Nouvelle méthode: utiliser isin et appliquer

def apply_do_g(it_row):
    """
    This is your function, but using isin and apply
    """

    keep = {'Operator': [it_row.Operator], 'Terminal': [it_row.Terminal]}  # dict for isin combined mask

    holder1 = arr[list(keep)].isin(keep).all(axis=1)  # create boolean mask
    holder2 = arr.Already_linked.isin([0])  # create boolean mask
    holder3 = arr.hour < it_row.hour_aux  # create boolean mask

    holder = holder1 & holder2 & holder3  # combine the masks

    holder = arr.loc[holder]

    if not holder.empty:

        aux = np.absolute(holder.START - it_row.x).idxmin()

        c.loc[it_row.name, 'a'] = holder.loc[aux].FlightID  # use with apply 'it_row.name'

        arr.loc[aux, 'Already_linked'] = 1


def new_way_2():
    keep = {'A_D': ['D'], 'Already_linked': [0]}
    df_test = c[c[list(keep)].isin(keep).all(axis=1)].copy()  # returns the resultant df
    df_test.apply(lambda row: apply_do_g(row), axis=1)  # g is multiple DataFrames"


#call the function
new_way_2()
1
Trenton_M

Votre question était de savoir s'il existe un moyen de vectoriser la boucle for, mais je pense que cette question cache ce que vous voulez vraiment, ce qui est un moyen facile d'accélérer votre code. Pour les questions de performances, un bon point de départ est toujours le profilage. Cependant, je soupçonne fortement que l'opération dominante dans votre code est .query(row.query_string). L'exécuter pour chaque ligne coûte cher si arr est grand.

Pour les requêtes arbitraires, ce runtime ne peut pas vraiment être amélioré du tout sans supprimer les dépendances entre les itérations et paralléliser l'étape coûteuse. Vous pourriez être un peu plus chanceux cependant. Votre chaîne de requête vérifie toujours deux colonnes différentes pour voir si elles sont égales à quelque chose qui vous tient à cœur. Cependant, pour chaque ligne qui nécessite de parcourir l'intégralité de votre tranche de arr. Étant donné que la tranche change à chaque fois, cela pourrait poser des problèmes, mais voici quelques idées:

  • Étant donné que vous découpez arr à chaque fois de toute façon, conservez une vue uniquement des lignes arr.Already_Linked==0 Afin de parcourir un objet plus petit.
  • Mieux encore, avant de faire une boucle, vous devez d'abord grouper arr par Terminal et Operator. Ensuite, au lieu de parcourir l'ensemble de arr, sélectionnez d'abord le groupe souhaité, puis effectuez votre découpage et votre filtrage. Cela nécessiterait de repenser un peu l'implémentation exacte de query_string, Mais l'avantage est que si vous avez beaucoup de terminaux et d'opérateurs, vous travaillerez généralement sur un objet beaucoup plus petit que arr . De plus, vous n'auriez même pas à interroger cet objet car cela a été implicitement fait par le groupby.
  • Selon la relation entre aux.hour Et row.hour_aux, Vous pouvez apporter des améliorations en triant aux au début par rapport à hour. En utilisant simplement l'opérateur d'inégalité, vous ne verriez probablement aucun gain, mais vous pouvez associer cela à une recherche logarithmique du point de coupure, puis simplement couper jusqu'à ce point de coupure.
  • Etc. Encore une fois, je soupçonne que n'importe quel moyen de restructurer la requête que vous faites tous les arr pour chaque ligne offrira beaucoup plus de gains que le simple changement de framework ou vectoriser des morceaux.

En développant un peu certains de ces points et en adaptant un peu le code de @ DJK, regardez ce qui se passe lorsque nous avons les changements suivants.

groups = arr.groupby(['Operator', 'Terminal'])

for row in c[(c.A_D == 'D') & (c.Already_linked == 0)].itertuples():
    g = groups.get_group((row.Operator, row.Terminal))
    vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]
    try:
        aux = (vb.START - row.x).abs().idxmin()
        print(row.x)
        c.loc[row.Index, 'a'] = vb.loc[aux,'FlightID']
        g.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

Une partie de la raison pour laquelle votre requête est si lente est qu'elle recherche à chaque fois sur arr. En revanche, la .groupby() s'exécute à peu près en même temps qu'une requête, mais pour à chaque itération suivante vous pouvez simplement utiliser .get_group() pour trouver efficacement la petite sous-ensemble des données qui vous intéressent.

Une règle de base utile (extrêmement grossière) lors de l'analyse comparative est qu'un milliard de choses prend une seconde. Si vous voyez des temps beaucoup plus longs que cela pour quelque chose mesuré en millions de choses, comme vos millions de lignes, cela signifie que pour chacune de ces lignes, vous faites des tonnes de choses pour atteindre des milliards d'opérations. Cela laisse une tonne de potentiel pour de meilleurs algorithmes pour réduire le nombre d'opérations, alors que la vectorisation ne donne vraiment que des améliorations constantes des facteurs (et pour de nombreuses opérations de chaîne/requête, pas même de grandes améliorations à cela).

3
Hans Musgrave

Bien que ce ne soit pas une solution optimisée, cela devrait accélérer les choses rapidement si votre échantillon de données imite votre véritable ensemble de données. Actuellement, vous perdez du temps à boucler sur chaque ligne, mais vous ne vous souciez que de boucler sur les lignes où ['A_D'] == 'D' et ['Already_linked'] ==0. Au lieu de cela, supprimez les if et la boucle sur la trame de données tronquée, qui ne représente que 30% de la trame de données initiale

for row in c[(c.A_D == 'D') & (c.Already_linked == 0)].itertuples():

    vb = arr[(arr.Already_linked == 0) & (arr.hour < row.hour_aux)].copy().query(row.query_string)
    try:
        aux = (vb.START - row.x).abs().idxmin()
        print(row.x)
        c.loc[row.Index, 'a'] = vb.loc[aux,'FlightID']
        arr.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue
3
DJK

Votre problème ressemble à l'un des problèmes les plus courants liés au fonctionnement de la base de données. Je ne comprends pas bien ce que vous voulez obtenir parce que vous n'avez pas formulé la tâche. Passons maintenant à la solution possible - éviter les boucles du tout.

Vous avez un table très long avec des colonnes time, FlightID, Operator, Terminal, A_D. Les autres colonnes et dates n'ont pas d'importance si je vous comprends bien. start_time Et end_time Sont également les mêmes sur chaque ligne. Par ailleurs, vous pouvez obtenir la colonne time avec le code table.loc[:, 'time'] = table.loc[:, 'START'].dt.time.

  1. table = table.drop_duplicates(subset=['time', 'FlightID', 'Operator', 'Terminal']). Et votre table va devenir beaucoup plus court.

  2. Divisez table en table_arr Et table_dep Selon la valeur de A_D: table_arr = table.loc[table.loc[:, 'A_D'] == 'A', ['FlightID', 'Operator', 'Terminal', 'time']], table_dep = table.loc[table.loc[:, 'A_D'] == 'D', ['FlightID', 'Operator', 'Terminal', 'time']]

  3. On dirait que tout ce que vous avez essayé d'obtenir avec des boucles peut être obtenu avec une seule ligne: table_result = table_arr.merge(table_dep, how='right', on=['Operator', 'Terminal'], suffixes=('_arr', '_dep')). Il s'agit essentiellement de la même opération que JOIN en SQL.

Selon ma compréhension de votre problème et le fait que vous ayez fourni le tout petit morceau de données, vous obtenez exactement la sortie souhaitée (correspondance entre FlightID_dep Et FlightID_arr Pour toutes les valeurs FlightID_dep) Sans n'importe quelle boucle tellement plus vite. table_result Est:

  FlightID_arr Operator  Terminal  time_arr FlightID_dep  time_dep
0        DL401       DL         3  06:50:00        DL001  09:30:00
1        DL402       DL         3  07:45:00        DL001  09:30:00
2          NaN       VS         3       NaN        VS001  15:15:00

Bien sûr, dans le cas général (avec des données réelles), vous aurez besoin d'une étape supplémentaire - filtrez table_result À la condition time_arr < time_dep Ou à toute autre condition que vous avez. Malheureusement, les données que vous avez fournies ne suffisent pas à résoudre complètement votre problème.

Le code complet est:

import io
import pandas as pd

data = '''
START,END,A_D,Operator,FlightID,Terminal,TROUND_ID,tot
2017-03-26 16:55:00,2017-10-28 16:55:00,A,QR,QR001,4,QR002,70
2017-03-26 09:30:00,2017-06-11 09:30:00,D,DL,DL001,3,,84
2017-03-27 09:30:00,2017-10-28 09:30:00,D,DL,DL001,3,,78
2017-10-08 15:15:00,2017-10-22 15:15:00,D,VS,VS001,3,,45
2017-03-26 06:50:00,2017-06-11 06:50:00,A,DL,DL401,3,,9
2017-03-27 06:50:00,2017-10-28 06:50:00,A,DL,DL401,3,,19
2017-03-29 06:50:00,2017-04-19 06:50:00,A,DL,DL401,3,,3
2017-05-03 06:50:00,2017-10-25 06:50:00,A,DL,DL401,3,,32
2017-06-25 06:50:00,2017-10-22 06:50:00,A,DL,DL401,3,,95
2017-03-26 07:45:00,2017-10-28 07:45:00,A,DL,DL402,3,,58
'''

table = pd.read_csv(io.StringIO(data), parse_dates=[0, 1])
table.loc[:, 'time'] = table.loc[:, 'START'].dt.time
table = table.drop_duplicates(subset=['time', 'FlightID', 'Operator', 'Terminal'])
table_arr = table.loc[table.loc[:, 'A_D'] == 'A', ['FlightID', 'Operator', 'Terminal', 'time']]
table_dep = table.loc[table.loc[:, 'A_D'] == 'D', ['FlightID', 'Operator', 'Terminal', 'time']]

table_result = table_arr.merge(
    table_dep,
    how='right',
    on=['Operator', 'Terminal'],
    suffixes=('_arr', '_dep'))
print(table_result)
1
Poolka