Create new rows in a dataframe by range of dates

Viewed 732

I need to generate a list of dates in a dataframe by days and that each day is a row in the new dataframe, taking into account the start date and the end date of each record.

Input Dataframe:

A B Start End
A1 B1 2021-05-15 00:00:00 2021-05-17 00:00:00
A1 B2 2021-05-30 00:00:00 2021-06-02 00:00:00
A2 B3 2021-05-10 00:00:00 2021-05-12 00:00:00
A2 B4 2021-06-02 00:00:00 2021-06-04 00:00:00

Expected Output:

A B Start End
A1 B1 2021-05-15 00:00:00 2021-05-16 00:00:00
A1 B1 2021-05-16 00:00:00 2021-05-17 00:00:00
A1 B2 2021-05-30 00:00:00 2021-05-31 00:00:00
A1 B2 2021-05-31 00:00:00 2021-06-01 00:00:00
A1 B2 2021-06-01 00:00:00 2021-06-02 00:00:00
A2 B3 2021-05-10 00:00:00 2021-05-11 00:00:00
A2 B3 2021-05-11 00:00:00 2021-05-12 00:00:00
A2 B4 2021-06-02 00:00:00 2021-06-03 00:00:00
A2 B4 2021-06-03 00:00:00 2021-06-04 00:00:00
2 Answers

Use:

#convert columns to datetimes
df["Start"] = pd.to_datetime(df["Start"])
df["End"] = pd.to_datetime(df["End"])

#subtract values and convert to days
s = df["End"].sub(df["Start"]).dt.days

#repeat index
df = df.loc[df.index.repeat(s)].copy()

#add days by timedeltas, add 1 day for End column
add = pd.to_timedelta(df.groupby(level=0).cumcount(), unit='d')
df['Start'] = df["Start"].add(add)
df['End'] = df["Start"] + pd.Timedelta(1, 'd')

#default index
df = df.reset_index(drop=True)
print (df)
    A   B      Start        End
0  A1  B1 2021-05-15 2021-05-16
1  A1  B1 2021-05-16 2021-05-17
2  A1  B2 2021-05-30 2021-05-31
3  A1  B2 2021-05-31 2021-06-01
4  A1  B2 2021-06-01 2021-06-02
5  A2  B3 2021-05-10 2021-05-11
6  A2  B3 2021-05-11 2021-05-12
7  A2  B4 2021-06-02 2021-06-03
8  A2  B4 2021-06-03 2021-06-04

Performance:

#4k rows
df = pd.concat([df] * 1000, ignore_index=True)

In [136]: %timeit jez(df)
16.9 ms ± 3.94 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [137]: %timeit andreas(df)
888 ms ± 136 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

#800 rows
df = pd.concat([df] * 200, ignore_index=True)


In [139]: %timeit jez(df)
6.25 ms ± 46.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [140]: %timeit andreas(df)
170 ms ± 28.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

def andreas(df):
    df['d_range'] = df.apply(lambda row: list(pd.date_range(start=row['Start'], end=row['End'])), axis=1)
    return df.explode('d_range')

def jez(df):
    df["Start"] = pd.to_datetime(df["Start"])
    df["End"] = pd.to_datetime(df["End"])
    
    #subtract values and convert to days
    s = df["End"].sub(df["Start"]).dt.days
    
    #repeat index
    df = df.loc[df.index.repeat(s)].copy()
    
    #add days by timedeltas, add 1 day for End column
    add = pd.to_timedelta(df.groupby(level=0).cumcount(), unit='d')
    df['Start'] = df["Start"].add(add)
    df['End'] = df["Start"] + pd.Timedelta(1, 'd')
    
    #default index
    return df.reset_index(drop=True)

You can create a list of dates and explode it:

df['d_range'] = df.apply(lambda row: list(pd.date_range(start=row['Start'], end=row['End'])), axis=1)
df = df.explode('d_range')
Related