将DataFrame中的一行按日期范围拆分为多行(仅当日期范围跨越2个月时)

3

我有一个DataFrame,每行都标识了一个客人及其预订编号、姓名、到达日期、离开日期和停留天数。

df = pd.DataFrame({'Booking_ID': ['34532', '43242', '43242', '32414'],
 'Name': ['Me', 'Myself', 'You', 'I'],
 'Start_Date': ['Jan 1, 2022', 'Mar 31, 2022', 'Mar 31, 2022', 'Jun 1, 2022'],
 'End_Date': ['Jan 5, 2022', 'Apr 3, 2022', 'Apr 3, 2022', 'Jun 5, 2022'],
 'Nights': [4, 3, 3, 4]}) 

一些客户在两个月内作为客人入住。出于统计原因,我需要将这些行分成两个不同的行:一个是第一个月的夜晚,另一个是第二个月的夜晚。

我想要得到的是:

预订编号 姓名 开始日期 结束日期 晚数
34532 2022年1月1日 2022年1月5日 4
43242 我自己 2022年3月31日 2022年4月1日 1
43242 我自己 2022年4月1日 2022年4月3日 2
43242 2022年3月31日 2022年4月1日 1
43242 2022年4月1日 2022年4月3日 2
32414 2022年6月1日 2022年6月5日 4
我遇到了另一个问题:按日期范围将一行拆分为多行(到另一个数据框),这个问题类似并且有用,但是我仍然无法得到我想要的结果。
我修改了之前链接中的代码:
import pandas as pd
import datetime as dt

df = pd.DataFrame({'Booking_ID': ['34532', '43242', '43242', '3241413'],
 'Name': ['Me', 'Myself', 'You', 'I'],
 'Start_Date': ['Jan 1, 2022', 'Mar 31, 2022', 'Mar 31, 2022', 'Jun 1, 2022'],
 'End_Date': ['Jan 5, 2022', 'Apr 3, 2022', 'Apr 3, 2022', 'Jun 5, 2022'],
 'Nights': [4, 3, 3, 4]}) 

df['Start_Date'] = pd.to_datetime(df['Start_Date'])
df['End_Date'] = pd.to_datetime(df['End_Date'])

df[['Start_Date', 'End_Date']] = df.apply(lambda x: (pd.date_range(x['Start_Date'], x['End_Date'] - dt.timedelta(days=1), freq='D'), pd.date_range(x['Start_Date'] + dt.timedelta(days=1), x['End_Date'], freq='D')) if x['Start_Date'].month != x['End_Date'].month else (pd.date_range(x['Start_Date'], x['Start_Date'], freq='D'), pd.date_range(x['End_Date'], x['End_Date'], freq='D')), axis=1, result_type='expand')

df = df.explode(['Start_Date', 'End_Date']).reset_index(drop=True)

df['Nights'] = df.groupby(['Booking_ID', 'Name', df.Start_Date.dt.month], as_index=False)['Nights'].transform(lambda x: x/len(x)).astype(int)

我拥有的是这个:

     Booking_ID      Name Start_Date   End_Date  Nights

0         34532        Me 2022-01-01 2022-01-05       4
1         43242    Myself 2022-03-31 2022-04-01       3
2         43242    Myself 2022-04-01 2022-04-02       1
3         43242    Myself 2022-04-02 2022-04-03       1
4         43242       You 2022-03-31 2022-04-01       3
5         43242       You 2022-04-01 2022-04-02       1
6         43242       You 2022-04-02 2022-04-03       1
7         32414         I 2022-06-01 2022-06-05       4

这部分是我想要的。

日期格式并不是很重要,因为可以轻松更改。

df['Start_Date'] = df['Start_Date'].dt.strftime('%b %#d, %Y')
df['End_Date'] = df['End_Date'].dt.strftime('%b %#d, %Y')

但是我真的需要:

  1. 将所有具有相同预订 ID、名称和 Start_Date 月份的行聚合成一个带有 Nights 列的行。这会导致聚合行的总晚数,并且 Start_Date/End_Date 对结果为聚合行的第一个 Start_Date 和最后一个 End_Date。

  2. 如果有一行起源于前一行由日期范围拆分而来,其 Start_Date 和 End_Date 之间有 1 天的距离,但原始夜数,则 Nights 值必须更改为 1。

2个回答

2

尝试使用这个:

import pandas as pd
import datetime as dt

def split_across_months(row):
    start = row['Start_Date']
    end = row['End_Date']
    booking_id = row['Booking_ID']
    name = row['Name']
    
    dates = [start]
    
    while start.month != end.month:
        start = start.replace(day=1) + pd.DateOffset(months=1)
        dates.append(start)
    
    dates.append(end)
    
    rows = []
    
    for i in range(len(dates) - 1):
        nights = (dates[i + 1] - dates[i]).days
        rows.append({'Booking_ID': booking_id, 'Name': name, 'Start_Date': dates[i], 'End_Date': dates[i+1], 'Nights': nights})
    
    return pd.DataFrame(rows)

df = pd.DataFrame({'Booking_ID': ['34532', '43242', '43242', '3241413'],
 'Name': ['Me', 'Myself', 'You', 'I'],
 'Start_Date': ['Jan 1, 2022', 'Mar 31, 2022', 'Mar 31, 2022', 'Jun 1, 2022'],
 'End_Date': ['Jan 5, 2022', 'Apr 3, 2022', 'Apr 3, 2022', 'Jun 5, 2022'],
 'Nights': [4, 3, 3, 4]})

df['Start_Date'] = pd.to_datetime(df['Start_Date'])
df['End_Date'] = pd.to_datetime(df['End_Date'])

result_df = pd.concat(df.apply(split_across_months, axis=1).tolist()).reset_index(drop=True)

print(result_df)

这有点暴力,但基本上如果开始和结束月份不同,您需要迭代每个月并创建新行。由于我怀疑没有成千上万个月的预订,所以这应该可以正常工作。


嗨,布鲁诺,感谢你的帮助。你的解决方案很好用,我会使用它,但我仍然开放于其他更加pandas风格的方式。 - Mondonauta
Bruno,我修改了你的代码以便管理一行具有更多标签的情况,所以他的部分: 'rows.append({'Booking_ID': booking_id, 'Name': name, 'Start_Date': dates[i], 'End_Date': dates[i+1], 'Nights': nights})' 变成了 'new_row = {label: dates[i] if label == 'Start_Date' else dates[i+1] if label == 'End_Date' else nights if label == 'Nights' else value for label, value in row.to_dict().items()} rows.append(new_row)' - Mondonauta

1
你可以使用 .reindex + .repeat 来添加行。每月想要1行:
num_months = 1 + (df['End_Date'].dt.month - df['Start_Date'].dt.month)
df = df.reindex(df.index.repeat(num_months)).reset_index(drop=True)
  Booking_ID    Name Start_Date   End_Date  Nights
0      34532      Me 2022-01-01 2022-01-05       4
1      43242  Myself 2022-03-31 2022-04-03       3
2      43242  Myself 2022-03-31 2022-04-03       3
3      43242     You 2022-03-31 2022-04-03       3
4      43242     You 2022-03-31 2022-04-03       3
5      32414       I 2022-06-01 2022-06-05       4

您可以使用 .groupby 来生成每个月份的数量以及每个“组”的总行数。
groups = df.groupby(df.columns.to_list())
month  = groups.cumcount() + 1
rows   = groups.transform('size')
>>> groups.cumcount() + 1
0    1
1    1
2    2
3    1
4    2
5    1
dtype: int64
>>> groups.transform('size')
0    1
1    2
2    2
3    2
4    2
5    1
dtype: int64

你需要加上月份来得到新的结束日期。你需要将结束日期移动,以成为下一行的开始日期。

date_range = pd.DataFrame()
date_range['End_Date'] = (df['Start_Date'].dt.to_period('M') + month).dt.to_timestamp()
date_range['Start_Date'] = date_range.groupby(groups.ngroup()).shift()
  Start_Date   End_Date
0        NaT 2022-02-01
1        NaT 2022-04-01 # <- first
2 2022-04-01 2022-05-01 # <- last
3        NaT 2022-04-01 # <- first
4 2022-04-01 2022-05-01 # <- last
5        NaT 2022-07-01

对于每个组的第一行和最后一行,您希望填写原始的开始/结束日期。

date_range.loc[month == 1, 'Start_Date'] = df
date_range.loc[month == rows, 'End_Date'] = df
  Start_Date   End_Date
0 2022-01-01 2022-01-05
1 2022-03-31 2022-04-01
2 2022-04-01 2022-04-03
3 2022-03-31 2022-04-01
4 2022-04-01 2022-04-03
5 2022-06-01 2022-06-05

我们将新日期添加到原始数据框中并重新计算晚数:

df.update(date_range)

df['Nights'] = (df['End_Date'] - df['Start_Date']).dt.days
  Booking_ID    Name Start_Date   End_Date  Nights
0      34532      Me 2022-01-01 2022-01-05       4
1      43242  Myself 2022-03-31 2022-04-01       1
2      43242  Myself 2022-04-01 2022-04-03       2
3      43242     You 2022-03-31 2022-04-01       1
4      43242     You 2022-04-01 2022-04-03       2
5      32414       I 2022-06-01 2022-06-05       4

完整代码:

df = pd.DataFrame(
    {'Booking_ID': ['34532', '43242', '43242', '32414'],
     'Name': ['Me', 'Myself', 'You', 'I'],
     'Start_Date': ['Jan 1, 2022', 'Mar 31, 2022', 'Mar 31, 2022', 'Jun 1, 2022'],
     'End_Date': ['Jan 5, 2022', 'Apr 3, 2022', 'Apr 3, 2022', 'Jun 5, 2022'],
     'Nights': [4, 3, 3, 4]}
)

df['Start_Date'] = pd.to_datetime(df['Start_Date'])
df['End_Date']   = pd.to_datetime(df['End_Date'])

num_months = 1 + (df['End_Date'].dt.month - df['Start_Date'].dt.month)

df = df.reindex(df.index.repeat(num_months)).reset_index(drop=True)

groups = df.groupby(df.columns.to_list())
month  = groups.cumcount() + 1
rows   = groups.transform('size')

date_range = pd.DataFrame()
date_range['End_Date'] = (df['Start_Date'].dt.to_period('M') + month).dt.to_timestamp()
date_range['Start_Date'] = date_range.groupby(groups.ngroup()).shift()

date_range.loc[month == 1, 'Start_Date'] = df
date_range.loc[month == rows, 'End_Date'] = df

df.update(date_range)

df['Nights'] = (df['End_Date'] - df['Start_Date']).dt.days

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接