2015-11-03 17 views
9

Sto lavorando a un programma che richiede grandi quantità di dati. Sto usando il modulo Python per cercare errori nei miei dati. Questo di solito funziona molto velocemente. Tuttavia questo attuale pezzo di codice che ho scritto sembra essere molto più lento di quanto dovrebbe essere e sto cercando un modo per accelerarlo.Il gruppo di Pandas si applica lentamente

Per poter testare correttamente, ho caricato un pezzo di codice piuttosto grande. Dovresti essere in grado di eseguirlo così com'è. I commenti nel codice dovrebbero spiegare cosa sto cercando di fare qui. Qualsiasi aiuto sarebbe molto apprezzato.

# -*- coding: utf-8 -*- 

import pandas as pd 
import numpy as np 

# Filling dataframe with data 
# Just ignore this part for now, real data comes from csv files, this is an example of how it looks 
TimeOfDay_options = ['Day','Evening','Night'] 
TypeOfCargo_options = ['Goods','Passengers'] 
numpy.random.seed(1234) 
n = 10000 

df = pd.DataFrame() 
df['ID_number'] = np.random.randint(3, size=n) 
df['TimeOfDay'] = np.random.choice(TimeOfDay_options, size=n) 
df['TypeOfCargo'] = np.random.choice(TypeOfCargo_options, size=n) 
df['TrackStart'] = np.random.randint(400, size=n) * 900 
df['SectionStart'] = np.nan 
df['SectionStop'] = np.nan 

grouped_df = df.groupby(['ID_number','TimeOfDay','TypeOfCargo','TrackStart']) 
for index, group in grouped_df: 
    if len(group) == 1: 
     df.loc[group.index,['SectionStart']] = group['TrackStart'] 
     df.loc[group.index,['SectionStop']] = group['TrackStart'] + 899 

    if len(group) > 1: 
     track_start = group.loc[group.index[0],'TrackStart'] 
     track_end = track_start + 899 
     section_stops = np.random.randint(track_start, track_end, size=len(group)) 
     section_stops[-1] = track_end 
     section_stops = np.sort(section_stops) 
     section_starts = np.insert(section_stops, 0, track_start) 

     for i,start,stop in zip(group.index,section_starts,section_stops): 
      df.loc[i,['SectionStart']] = start 
      df.loc[i,['SectionStop']] = stop 

#%% This is what a random group looks like without errors 
#Note that each section neatly starts where the previous section ended 
#There are no gaps (The whole track is defined) 
grouped_df.get_group((2, 'Night', 'Passengers', 323100)) 

#%% Introducing errors to the data 
df.loc[2640,'SectionStart'] += 100 
df.loc[5390,'SectionStart'] += 7 

#%% This is what the same group looks like after introducing errors 
#Note that the 'SectionStop' of row 1525 is no longer similar to the 'SectionStart' of row 5592 
#This track now has a gap of 100, it is not completely defined from start to end 
grouped_df.get_group((2, 'Night', 'Passengers', 323100)) 

#%% Try to locate the errors 
#This is the part of the code I need to speed up 

def Full_coverage(group): 
    if len(group) > 1: 
     group.sort('SectionStart', ascending=True, inplace=True) #Sort the grouped data by column 'SectionStart' from low to high 

     #Some initial values, overwritten at the end of each loop 
     #These variables correspond to the first row of the group 
     start_km = group.iloc[0,4] 
     end_km = group.iloc[0,5] 
     end_km_index = group.index[0] 

     #Loop through all the rows in the group 
     #index is the index of the row 
     #i is the 'SectionStart' of the row 
     #j is the 'SectionStop' of the row 
     #The loop starts from the 2nd row in the group 
     for index, (i, j) in group.iloc[1:,[4,5]].iterrows(): 

      #The start of the next row must be equal to the end of the previous row in the group 
      if i != end_km: 

       #Add the faulty data to the error list 
       incomplete_coverage.append(('Expected startpoint: '+str(end_km)+' (row '+str(end_km_index)+')', \ 
            'Found startpoint: '+str(i)+' (row '+str(index)+')'))     

      #Overwrite these values for the next loop 
      start_km = i 
      end_km = j 
      end_km_index = index 

    return group 

#Check if the complete track is completely defined (from start to end) for each combination of: 
    #'ID_number','TimeOfDay','TypeOfCargo','TrackStart' 
incomplete_coverage = [] #Create empty list for storing the error messages 
df_grouped = df.groupby(['ID_number','TimeOfDay','TypeOfCargo','TrackStart']).apply(lambda x: Full_coverage(x)) 

#Print the error list 
print('\nFound incomplete coverage in the following rows:') 
for i,j in incomplete_coverage: 
    print(i) 
    print(j) 
    print() 

#%%Time the procedure -- It is very slow, taking about 6.6 seconds on my pc 
%timeit df.groupby(['ID_number','TimeOfDay','TypeOfCargo','TrackStart']).apply(lambda x: Full_coverage(x)) 
+0

Hai provato a utilizzare un profiler per vedere dove si trova il collo di bottiglia? – jakevdp

+0

Il collo di bottiglia sembra essere la funzione apply, anche quando rimuovo il ciclo for nella funzione rimane lento (~ 4.25s per loop). Mi chiedo se c'è un altro modo per applicare la funzione (senza il comando apply). Eseguo alcune altre procedure sui dati in questo codice usando il comando agg. Funziona molto più velocemente, ma non so se è possibile eseguire questo controllo (full_coverage) usando il comando agg. – Alex

+0

Il collo di bottiglia è sicuramente nella funzione che stai applicando. Ci sono oltre 5300 gruppi distinti nei tuoi dati. Basta chiamare '' sort'' su 5300 gruppi per alcuni secondi.Quindi, l'iterazione di tutti i valori all'interno di ciascuno di questi 5300 gruppi richiederà alcuni secondi. Suggerirei di rimuovere il ciclo '' for'' a favore di un'operazione vettoriale - potreste essere in grado di portare il tempo di esecuzione a ~ 2-3 secondi con questa strategia. Se è ancora troppo lento, dovrai capire come farlo senza ordinare i dati in ogni gruppo. – jakevdp

risposta

5

Il problema, credo, è che i dati contengano 5300 gruppi distinti. A causa di ciò, tutto ciò che è lento nella tua funzione verrà ingrandito. Probabilmente potresti utilizzare una funzione vettoriale anziché un ciclo for nella tua funzione per risparmiare tempo, ma un modo molto più semplice per sbarazzarti di qualche secondo è quello di return 0 anziché return group. Quando si utilizza return group, i panda creano effettivamente un nuovo oggetto dati che combina i gruppi ordinati, che non sembrano utilizzare. Quando si è return 0, i panda combineranno invece 5300 zeri, che è molto più veloce.

Ad esempio:

cols = ['ID_number','TimeOfDay','TypeOfCargo','TrackStart'] 
groups = df.groupby(cols) 
print(len(groups)) 
# 5353 

%timeit df.groupby(cols).apply(lambda group: group) 
# 1 loops, best of 3: 2.41 s per loop 

%timeit df.groupby(cols).apply(lambda group: 0) 
# 10 loops, best of 3: 64.3 ms per loop 

Proprio combinando i risultati non si usa sta prendendo circa 2,4 secondi; il resto del tempo è un vero calcolo nel ciclo che dovresti tentare di vettorizzare.


Edit:

Con un rapido controllo vectorized ulteriore prima del ciclo for e il ritorno 0 invece di group, ho avuto il tempo fino a circa ~ 2sec, che è sostanzialmente il costo di ordinare ogni gruppo. Prova questa funzione:

def Full_coverage(group): 
    if len(group) > 1: 
     group = group.sort('SectionStart', ascending=True) 

     # this condition is sufficient to find when the loop 
     # will add to the list 
     if np.any(group.values[1:, 4] != group.values[:-1, 5]): 
      start_km = group.iloc[0,4] 
      end_km = group.iloc[0,5] 
      end_km_index = group.index[0] 

      for index, (i, j) in group.iloc[1:,[4,5]].iterrows(): 
       if i != end_km: 
        incomplete_coverage.append(('Expected startpoint: '+str(end_km)+' (row '+str(end_km_index)+')', \ 
             'Found startpoint: '+str(i)+' (row '+str(index)+')'))     
       start_km = i 
       end_km = j 
       end_km_index = index 

    return 0 

cols = ['ID_number','TimeOfDay','TypeOfCargo','TrackStart'] 
%timeit df.groupby(cols).apply(Full_coverage) 
# 1 loops, best of 3: 1.74 s per loop 

Edit 2: ecco un esempio che incorpora il mio suggerimento per spostare il tipo al di fuori del GroupBy e di togliere i loop inutili. Rimozione dei loop non è molto più veloce per l'esempio dato, ma sarà più veloce se ci sono un sacco di incompleti:

def Full_coverage_new(group): 
    if len(group) > 1: 
     mask = group.values[1:, 4] != group.values[:-1, 5] 
     if np.any(mask): 
      err = ('Expected startpoint: {0} (row {1}) ' 
        'Found startpoint: {2} (row {3})') 
      incomplete_coverage.extend([err.format(group.iloc[i, 5], 
                group.index[i], 
                group.iloc[i + 1, 4], 
                group.index[i + 1]) 
             for i in np.where(mask)[0]]) 
    return 0 

incomplete_coverage = [] 
cols = ['ID_number','TimeOfDay','TypeOfCargo','TrackStart'] 
df_s = df.sort_values(['SectionStart','SectionStop']) 
df_s.groupby(cols).apply(Full_coverage_nosort) 
+0

Wow gratta il mio ultimo commento, l'ho cancellato. Questa funzione riduce il tempo da 6,6 a 1,6 sec/loop. Progressi incredibili! Pensi che ci sia un modo per ordinare i dati al di fuori del ciclo? Ho provato alcune cose, ma continuo a ottenere risultati errati. – Alex

+0

Sì, il groupby conserva l'ordine, quindi è possibile prima ordinare il dataframe con '' "SectionStart" '' e quindi utilizzare la funzione sopra descritta con il passo di ordinamento rimosso. Per l'ordinamento completo seguito da groupby, ottengo 0.4 secondi. – jakevdp

+0

Questo è esattamente ciò che sto provando, e mentre accelera significativamente il processo, i risultati che sono memorizzati nell'elenco sembrano sbagliati. Ho provato df.sort ('SectionStart', ascending = True, inplace = True) prima di eseguire il groupby, ma ora l'elenco degli errori sembra contenere 8 errori. Chiaramente ho introdotto solo due errori nei dati. – Alex

0

ho trovato il panda individuare comandi (.loc o .iloc) sono stati anche rallentare la progressione . Spostando l'ordinamento fuori dal ciclo e convertendo i dati in array numpy all'inizio della funzione, ho ottenuto un risultato ancora più veloce. Sono consapevole che i dati non sono più un dataframe, ma gli indici restituiti nell'elenco possono essere utilizzati per trovare i dati nel file originale df.

Se c'è un modo per accelerare ulteriormente il processo, gradirei l'aiuto. Quello che ho finora:

def Full_coverage(group): 

    if len(group) > 1: 
     group_index = group.index.values 
     group = group.values 

     # this condition is sufficient to find when the loop will add to the list 
     if np.any(group[1:, 4] != group[:-1, 5]): 
      start_km = group[0,4] 
      end_km = group[0,5] 
      end_km_index = group_index[0] 

      for index, (i, j) in zip(group_index, group[1:,[4,5]]): 

       if i != end_km: 
        incomplete_coverage.append(('Expected startpoint: '+str(end_km)+' (row '+str(end_km_index)+')', \ 
             'Found startpoint: '+str(i)+' (row '+str(index)+')'))    
       start_km = i 
       end_km = j 
       end_km_index = index 

    return 0 

incomplete_coverage = [] 
df.sort(['SectionStart','SectionStop'], ascending=True, inplace=True) 
cols = ['ID_number','TimeOfDay','TypeOfCargo','TrackStart'] 
%timeit df.groupby(cols).apply(Full_coverage) 
# 1 loops, best of 3: 272 ms per loop 
+0

Con il controllo '' np.any'', è possibile rimuovere completamente il ciclo '' for''. Ciò accelererebbe le cose nel caso di molte discrepanze. Altrimenti, ~ 0,2 secondi per la logica su ~ 5000 gruppi distinti è probabilmente buono come si può sperare. – jakevdp

+0

Ho cercato di sbarazzarmi del ciclo, ma non riesco a trovare alcun modo per aggiungere una linea di stampa separata per ogni errore, senza utilizzare il ciclo. Hai qualche idea? – Alex

+0

Vedere la mia risposta modificata. – jakevdp

Problemi correlati