Python Pandas – combinando 2 líneas desde el dataframe – con la condición

Tengo un dataframe de Pandas que se ve así:

ABC Stime Etime 1220627 a 10.0 18:00:00 18:09:59 1220627 a 12.0 18:15:00 18:26:59 1220683 b 3.0 18:36:00 18:38:59 1220683 a 3.0 18:36:00 18:38:59 1220732 a 59.0 18:00:00 18:58:59 1220760 A 16.0 18:24:00 18:39:59 1220760 a 16.0 18:24:00 18:39:59 1220760 A 19.0 18:40:00 18:58:59 1220760 b 19.0 18:40:00 18:58:59 1220760 a 19.0 18:40:00 18:58:59 1220775 a 3.0 18:03:00 18:05:59 

Stime y Etime cols son de tipo datetime.

C es el número de minutos entre Stime y Etime.

Un col es la identificación del hogar y la col es la identificación de la persona en el hogar.

(de modo que los cols A y B juntos representan una persona única).

Lo que tengo que hacer es actualizar la tabla de modo que si, para una determinada persona, el Stime llega justo después de la hora final, uniré las 2 líneas y actualizaré C.

por ejemplo aquí, para la persona a en HH 1220760 el primer Etime es 18:39:59

y el segundo Stime es a las 18:40:00 , que viene justo después de las 18:39:59, así que me gustaría unir las líneas y actualizar C para que esta persona tenga 35 (16 + 19).

Intenté usar groupby pero no sé cómo agregar la condición de que Stime vendrá justo después de Etime .

Si agregamos un segundo a Etime , podemos encontrar filas que se unirán agrupando por ['A', 'B'] y luego para cada grupo que compare Etime s desplazado con el siguiente Stime :

 df['Etime'] += pd.Timedelta(seconds=1) df = df.sort_values(by=['A', 'B', 'Stime']) df['keep'] = df.groupby(['A','B'])['Etime'].shift(1) != df['Stime'] # ABC Etime Stime keep # 0 1220627 a 10.0 2016-05-29 18:10:00 2016-05-29 18:00:00 True # 1 1220627 a 12.0 2016-05-29 18:27:00 2016-05-29 18:15:00 True # 3 1220683 a 3.0 2016-05-29 18:39:00 2016-05-29 18:36:00 True # 2 1220683 b 3.0 2016-05-29 18:39:00 2016-05-29 18:36:00 True # 4 1220732 a 59.0 2016-05-29 18:59:00 2016-05-29 18:00:00 True # 5 1220760 A 16.0 2016-05-29 18:40:00 2016-05-29 18:24:00 True # 7 1220760 A 19.0 2016-05-29 18:59:00 2016-05-29 18:40:00 False # 12 1220760 a 0.0 2016-05-29 18:10:00 2016-05-29 18:00:00 True # 6 1220760 a 16.0 2016-05-29 18:40:00 2016-05-29 18:24:00 True # 9 1220760 a 19.0 2016-05-29 18:59:00 2016-05-29 18:40:00 False # 11 1220760 a 11.0 2016-05-29 19:10:00 2016-05-29 18:59:00 False # 8 1220760 b 19.0 2016-05-29 18:59:00 2016-05-29 18:40:00 True # 10 1220775 a 3.0 2016-05-29 18:06:00 2016-05-29 18:03:00 True 

Queremos mantener las filas donde keep es verdadero y eliminar las filas donde keep es falso, excepto que también queremos actualizar el Etime s según corresponda.

Sería bueno si pudiéramos asignar un “número de grupo” a cada fila para que podamos agrupar por ['A', 'B', 'group_number'] – y de hecho podemos ['A', 'B', 'group_number'] . Todo lo que necesitamos hacer es aplicar cumsum a la columna de keep :

 df['group_number'] = df.groupby(['A','B'])['keep'].cumsum() # ABC Etime Stime keep group_number # 0 1220627 a 10.0 2016-05-29 18:10:00 2016-05-29 18:00:00 True 1.0 # 1 1220627 a 12.0 2016-05-29 18:27:00 2016-05-29 18:15:00 True 2.0 # 3 1220683 a 3.0 2016-05-29 18:39:00 2016-05-29 18:36:00 True 1.0 # 2 1220683 b 3.0 2016-05-29 18:39:00 2016-05-29 18:36:00 True 1.0 # 4 1220732 a 59.0 2016-05-29 18:59:00 2016-05-29 18:00:00 True 1.0 # 5 1220760 A 16.0 2016-05-29 18:40:00 2016-05-29 18:24:00 True 1.0 # 7 1220760 A 19.0 2016-05-29 18:59:00 2016-05-29 18:40:00 False 1.0 # 12 1220760 a 0.0 2016-05-29 18:10:00 2016-05-29 18:00:00 True 1.0 # 6 1220760 a 16.0 2016-05-29 18:40:00 2016-05-29 18:24:00 True 2.0 # 9 1220760 a 19.0 2016-05-29 18:59:00 2016-05-29 18:40:00 False 2.0 # 11 1220760 a 11.0 2016-05-29 19:10:00 2016-05-29 18:59:00 False 2.0 # 8 1220760 b 19.0 2016-05-29 18:59:00 2016-05-29 18:40:00 True 1.0 # 10 1220775 a 3.0 2016-05-29 18:06:00 2016-05-29 18:03:00 True 1.0 

Ahora se puede encontrar el resultado deseado agrupando por ['A', 'B', 'group_number'] , y encontrando el Stime mínimo y el Etime máximo para cada grupo:

 result = df.groupby(['A','B', 'group_number']).agg({'Stime':'min', 'Etime':'max'}) Stime Etime AB group_number 1220627 a 1.0 2016-05-29 18:00:00 2016-05-29 18:10:00 2.0 2016-05-29 18:15:00 2016-05-29 18:27:00 1220683 a 1.0 2016-05-29 18:36:00 2016-05-29 18:39:00 b 1.0 2016-05-29 18:36:00 2016-05-29 18:39:00 1220732 a 1.0 2016-05-29 18:00:00 2016-05-29 18:59:00 1220760 A 1.0 2016-05-29 18:24:00 2016-05-29 18:59:00 a 1.0 2016-05-29 18:00:00 2016-05-29 18:10:00 2.0 2016-05-29 18:24:00 2016-05-29 19:10:00 b 1.0 2016-05-29 18:40:00 2016-05-29 18:59:00 1220775 a 1.0 2016-05-29 18:03:00 2016-05-29 18:06:00 

Poniendolo todo junto,

 import numpy as np import pandas as pd df = pd.DataFrame( {'A': [1220627, 1220627, 1220683, 1220683, 1220732, 1220760, 1220760, 1220760, 1220760, 1220760, 1220775, 1220760, 1220760], 'B': ['a', 'a', 'b', 'a', 'a', 'A', 'a', 'A', 'b', 'a', 'a', 'a', 'a'], 'C': [10.0, 12.0, 3.0, 3.0, 59.0, 16.0, 16.0, 19.0, 19.0, 19.0, 3.0, 11.0, 0], 'Stime': ['18:00:00', '18:15:00', '18:36:00', '18:36:00', '18:00:00', '18:24:00', '18:24:00', '18:40:00', '18:40:00', '18:40:00', '18:03:00', '18:59:00', '18:00:00'], 'Etime': ['18:09:59', '18:26:59', '18:38:59', '18:38:59', '18:58:59', '18:39:59', '18:39:59', '18:58:59', '18:58:59', '18:58:59', '18:05:59', '19:09:59', '18:09:59'],}) for col in ['Stime', 'Etime']: df[col] = pd.to_datetime(df[col]) df['Etime'] += pd.Timedelta(seconds=1) df = df.sort_values(by=['A', 'B', 'Stime']) df['keep'] = df.groupby(['A','B'])['Etime'].shift(1) != df['Stime'] df['group_number'] = df.groupby(['A','B'])['keep'].cumsum() result = df.groupby(['A','B', 'group_number']).agg({'Stime':'min', 'Etime':'max'}) result = result.reset_index() result['C'] = (result['Etime']-result['Stime']).dt.total_seconds() / 60.0 result = result[['A', 'B', 'C', 'Stime', 'Etime']] print(result) 

rendimientos

  ABC Stime Etime 0 1220627 a 10.0 2016-05-29 18:00:00 2016-05-29 18:10:00 1 1220627 a 12.0 2016-05-29 18:15:00 2016-05-29 18:27:00 2 1220683 a 3.0 2016-05-29 18:36:00 2016-05-29 18:39:00 3 1220683 b 3.0 2016-05-29 18:36:00 2016-05-29 18:39:00 4 1220732 a 59.0 2016-05-29 18:00:00 2016-05-29 18:59:00 5 1220760 A 35.0 2016-05-29 18:24:00 2016-05-29 18:59:00 6 1220760 a 10.0 2016-05-29 18:00:00 2016-05-29 18:10:00 7 1220760 a 46.0 2016-05-29 18:24:00 2016-05-29 19:10:00 8 1220760 b 19.0 2016-05-29 18:40:00 2016-05-29 18:59:00 9 1220775 a 3.0 2016-05-29 18:03:00 2016-05-29 18:06:00 

Una de las ventajas de usar intervalos semiabiertos de la forma [start, end) lugar de intervalos completamente cerrados [start, end] es que cuando dos intervalos a tope, el end de uno es igual al start del siguiente.

Otra ventaja es que la cantidad de minutos en un intervalo medio abierto es igual a la end-start . Con un intervalo completamente cerrado, la fórmula se convierte en end-start+1 .

El range incorporado de Python y la syntax de corte de la lista utilizan intervalos semiabiertos por las mismas razones . Por lo tanto, recomendaría el uso de intervalos medio abiertos [Stime, Etime) en su DataFrame también.

¿Qué pasa con este enfoque?

 In [68]: df.groupby(['A','B', df.Stime - df['Etime'].shift() <= pd.Timedelta('1S')], as_index=False)['C'].sum() Out[68]: ABC 0 1220627 a 22.0 1 1220683 a 3.0 2 1220683 b 3.0 3 1220732 a 59.0 4 1220760 A 35.0 5 1220760 a 35.0 6 1220760 b 19.0 7 1220775 a 3.0 

Ok, creo que tengo una solución, pero es muy burda y estoy segura de que alguien puede mejorarla.

asumiendo que df = los datos que ha proporcionado anteriormente:

 df['Stime'] = pd.to_datetime(df['Stime'], format='%H:%M:%S') # needs to be converted to datetime df['Etime'] = pd.to_datetime(df['Etime'], format='%H:%M:%S') # needs to be converted to datetime df = df.sort_values(['A','B','Stime']) # data needs to be sorted by unique person : Stime df = df.reset_index(drop=True) df = df.reset_index() def new_person(row): if row.name > 0: if row['A'] != df.ix[row.name-1][1] or row['B'] != df.ix[row.name-1][2]: return 'Yes' def update(row): if row.name > 0: if row['B'] == df.ix[row.name-1][2]: if df.ix[row.name][4] - df.ix[row.name-1][5] >= pd.Timedelta(seconds=0) and df.ix[row.name][4] - df.ix[row.name-1][5] < pd.Timedelta(seconds=2): return df.groupby(['A','B'])['C'].cumsum().ix[row.name] def rewrite(row): if row['update'] > 0: return row['update'] else: return row['C'] df['new_person'] = df.apply(new_person, axis=1) # adds column where value = 'Yes' if person is not the same as row above df['update'] = df.apply(update,axis=1) # adds a column 'update' to allow for a cumulative sum rewritten to 'C' in rewrite function print df df['Stime'] = pd.to_datetime(df['Stime'], format='%H:%M:%S').dt.time # removes date from datetime df['Etime'] = pd.to_datetime(df['Etime'], format='%H:%M:%S').dt.time # removes date from datetime df['C'] = df.apply(rewrite,axis=1) # rewrites values for 'C' column # hacky way of combining idxmax and indices of rows where the person is 'new' updated = df.groupby(['A','B'])['C'].agg(pd.Series.idxmax).values not_updated = df['new_person'].isnull().tolist() combined = [x for x in df.index if (x in updated or x in not_updated)] df = df.iloc[combined] df = df.drop(['new_person','update','index'],axis=1) print df 

Disculpas por la respuesta extremadamente hacky, pero creo que debería lograr lo que necesitas. No estoy seguro de qué tan bien funcionará si su dataframe es muy grande.

Marco de datos resultante:

  ABC Stime Etime 0 1220627 a 10 18:00:00 18:09:59 1 1220627 a 12 18:15:00 18:26:59 2 1220683 a 3 18:36:00 18:38:59 3 1220683 b 3 18:36:00 18:38:59 4 1220732 a 59 18:00:00 18:58:59 6 1220760 A 35 18:40:00 18:58:59 9 1220760 a 46 18:59:00 18:09:59 10 1220760 b 19 18:40:00 18:58:59 11 1220775 a 3 18:03:00 18:05:59