Problema de rendimiento al convertir filas con inicio – termina en un dataframe con TimeIndex

Tengo un conjunto de datos grande donde cada línea representa el valor de un determinado tipo (piense en un sensor) para un intervalo de tiempo (entre el inicio y el final). Se parece a esto:

start end type value 2015-01-01 2015-01-05 1 3 2015-01-06 2015-01-08 1 2 2015-01-05 2015-01-08 3 3 2015-01-13 2015-01-16 2 1 

Quiero convertirlo en un cuadro diario indexado en el tiempo como este:

 day type value 2015-01-01 1 3 2015-01-02 1 3 2015-01-03 1 3 2015-01-04 1 3 2015-01-05 1 3 2015-01-06 1 2 2015-01-07 1 2 2015-01-08 1 2 2015-01-05 3 3 2015-01-16 3 3 2015-01-07 3 3 2015-01-08 3 3 2015-01-13 2 1 2015-01-14 2 1 2015-01-15 2 1 2015-01-16 2 1 

(Tenga en cuenta que no podemos hacer ninguna suposición con respecto al intervalo: deben ser contiguos y no superpuestos, pero no podemos garantizar que)

Según estas respuestas de desbordamiento de stack [1] ( remuestreo de DataFrame en rangos de fecha ) [2] ( pandas: Agregado según fecha de inicio / finalización ), parece que existen dos métodos: uno alrededor de itertuples, uno alrededor de fusión (2 encima de la stack usada / Unstack pero es similar a fundir). Vamos a compararlos para el rendimiento.

 # Creating a big enough dataframe date_range = pd.date_range(start=dt.datetime(2015,1,1), end=dt.datetime(2019,12,31), freq='4D') to_concat = [] for val in range(1,50): frame_tmp = pd.DataFrame() frame_tmp['start'] = date_range frame_tmp['end'] = frame_tmp['start']+ dt.timedelta(3) frame_tmp['type'] = val frame_tmp['value'] = np.random.randint(1, 6, frame_tmp.shape[0]) to_concat.append(frame_tmp) df = pd.concat(to_concat, ignore_index=True) # Method 1 def method_1(df): df1 = (pd.concat([pd.Series(r.Index, pd.date_range(r.start, r.end, freq='D')) for r in df.itertuples()])) \ .reset_index() df1.columns = ['start_2', 'idx'] df2 = df1.set_index('idx').join(df).reset_index(drop=True) return df2.set_index('start_2') df_method_1=df.groupby(['type']).apply(method_1) # Method 2 df_tmp= df.reset_index() df1 = (df_tmp.melt(df_tmp.columns.difference(['start','end']), ['start', 'end'], value_name='current_time') ) df_method_2 = df1.set_index('current_time').groupby('index', group_keys=False)\ .resample('D').ffill() 

Con %%timeit en Jupyter, el método 1 toma ~ 8s y el método 2 toma ~ 25s para el dataframe definido como ejemplo. Esto es demasiado lento ya que el conjunto de datos real con el que estoy tratando es mucho más grande que esto. En ese dataframe, el método 1 tarda unos 20 minutos.

¿Tienes alguna idea de cómo hacer esto más rápido?

Esto es aproximadamente 1.7 veces más rápido que tu method_1 y un poco más ordenado:

 df_expand = pd.DataFrame.from_records( ( (d, r.type, r.value) for r in df.itertuples() for d in pd.date_range(start=r.start, end=r.end, freq='D') ), columns=['day', 'type', 'row'] ) 

Puede obtener aproximadamente 7 veces más rápido creando su propio rango de fechas en lugar de llamar a pd.date_range() :

 one_day = dt.timedelta(1) df_expand = pd.DataFrame.from_records( ( (r.start + i * one_day, r.type, r.value) for r in df.itertuples() for i in range(int((r.end-r.start)/one_day)+1) ), columns=['day', 'type', 'row'] ) 

O puede obtener hasta 24 veces más rápido usando la función arange de numpy para generar las fechas:

 one_day = dt.timedelta(1) df_expand = pd.DataFrame.from_records( ( (d, r.type, r.value) for r in df.itertuples() for d in np.arange(r.start.date(), r.end.date()+one_day, dtype='datetime64[D]') ), columns=['day', 'type', 'row'] ) 

No pude resistir agregar uno más que es un poco más del doble de rápido que el anterior. Desafortunadamente, es mucho más difícil de leer. Esto agrupa las lecturas según la cantidad de días que abarcan (‘dur’), ​​y luego utiliza operaciones numéricas vectorizadas para expandir cada grupo en un solo lote.

 def expand_group(g): dur = g.dur.iloc[0] # how many days for each reading in this group? return pd.DataFrame({ 'day': (g.start.values[:,None] + np.timedelta64(1, 'D') * np.arange(dur)).ravel(), 'type': np.repeat(g.type.values, dur), 'value': np.repeat(g.value.values, dur), }) # take all readings with the same duration and process them together using vectorized code df_expand = ( df.assign(dur=(df['end']-df['start']).dt.days + 1) .groupby('dur').apply(expand_group) .reset_index('dur', drop=True) ) 

Actualización: Respondiendo a su comentario, a continuación se muestra una versión simplificada del enfoque vectorizado, que es más rápido y más fácil de leer. En lugar de usar el paso groupby , esto hace que una sola matriz sea tan ancha como la lectura más larga, luego filtra las entradas innecesarias. Esto debería ser bastante eficiente a menos que la duración máxima de sus lecturas sea mucho más larga que el promedio. Con el dataframe de prueba (todas las lecturas duran 4 días), esto es aproximadamente groupby veces más rápido que la solución groupby y aproximadamente 700 veces más rápido que method_1 .

 dur = (df['end']-df['start']).max().days + 1 df_expand = pd.DataFrame({ 'day': ( df['start'].values[:,None] + np.timedelta64(1, 'D') * np.arange(dur) ).ravel(), 'type': np.repeat(df['type'].values, dur), 'value': np.repeat(df['value'].values, dur), 'end': np.repeat(df['end'].values, dur), }) df_expand = df_expand.loc[df_expand['day']<=df_expand['end'], 'day':'value']