Pandas: 滚动 window 来计算频率 - 最快的方法
Pandas: Rolling window to count the frequency - Fastest approach
我想计算一个值在过去 x 天出现的频率。在下面的示例中,我想计算过去 28 天 Name
列中值的频率。数据已按 Date
排序
import pandas as pd
import time
d = {'Name': ['Jack', 'Jim', 'Jack', 'Jim', 'Jack', 'Jack', 'Jim', 'Jack', 'Jane', 'Jane'],
'Date': ['08/01/2021',
'27/01/2021',
'05/02/2021',
'10/02/2021',
'17/02/2021',
'18/02/2021',
'20/02/2021',
'21/02/2021',
'22/02/2021',
'29/03/2021']}
df = pd.DataFrame(data=d)
df['Date'] = pd.to_datetime(df.Date, format='%d/%m/%Y')
# Make sure pandas is sorted by Date
df = df.sort_values('Date')
我在 Whosebug 上找到了一些解决方案,但所有这些解决方案在数据集上都不正确,也不快速。
方法 1 - 不太正确
df['count1'] = df.set_index('Date').groupby('Name', sort=False)['Name'].rolling('28d', closed='both').count().tolist()
方法 2 - 正确的方法但是很慢 <~ 来自这个
df['count2'] = df.assign(count=1).groupby(['Name']).apply(lambda x: x.rolling('28d', on='Date').sum())['count']
方法 3 - 使用 sum
- 不正确
df['count3'] = df.assign(count=1).groupby('Name').rolling('28d', on='Date').sum().reset_index().sort_values('Date')['count']
方法 4 - 也使用 sum
- 不正确,因为索引不正确 <~ 这个
df['count4'] = df.set_index('Date').assign(count_last=1).groupby('Name').rolling('28d').sum().reset_index()["count_last"]
输出
Name Date count1 count2 count3 count4
0 Jack 2021-01-08 1.0 1.0 1.0 1.0
1 Jim 2021-01-27 2.0 1.0 1.0 1.0
2 Jack 2021-02-05 2.0 1.0 2.0 2.0
3 Jim 2021-02-10 3.0 2.0 3.0 3.0
4 Jack 2021-02-17 4.0 2.0 4.0 4.0 #<~ all are wrong here except approach 2
5 Jack 2021-02-18 1.0 3.0 1.0 1.0
6 Jim 2021-02-20 2.0 3.0 1.0 1.0
7 Jack 2021-02-21 3.0 4.0 1.0 1.0
8 Jane 2021-02-22 1.0 1.0 2.0 2.0
9 Jane 2021-03-29 1.0 1.0 3.0 3.0
表演
Method 1: 0.0014538764953613281 ms
Method 2: 0.0034720897674560547 ms
Method 3: 0.002077817916870117 ms
Method 4: 0.0035729408264160156 ms
已更新 <~ 解决方案
根据标记的答案,我编写了这个解决方案,我认为它在数据具有 NULL 值和重复项时有效。此外,它不会改变原始数据集的大小。
def frequency_of_last_n_days(df: pd.DataFrame, identifier: str, timestamp: str, delta: int) -> pd.DataFrame:
col_name = "count_%s" % identifier
temp_df = df.set_index(timestamp) \
.groupby(identifier, sort=False)[identifier] \
.rolling('%sd' % delta, closed='both') \
.count() \
.rename(col_name)
temp_df = temp_df[~temp_df.index.duplicated(keep="first")]
return df.merge(temp_df, how="left", left_on=[identifier, timestamp], right_index=True)
frequency_of_last_n_days(df, "Name", "Date", 30)
IIUC,问题出在您的 tolist()
上,它弄乱了索引对齐并打乱了输出。
改用merge
:
df2 = (df
.merge(df.set_index('Date')
.groupby('Name', sort=False)['Name']
.rolling('28d', closed='both') # do you really want closed="both"?
.count().rename('count'),
left_on=['Name', 'Date'], right_index=True
)
)
输出:
Name Date count
0 Jack 2021-01-08 1.0
1 Jim 2021-01-27 1.0
2 Jack 2021-02-05 2.0 <- if you want 1 here, remove closed='both'
3 Jim 2021-02-10 2.0
4 Jack 2021-02-17 2.0
5 Jack 2021-02-18 3.0
6 Jim 2021-02-20 3.0
7 Jack 2021-02-21 4.0
8 Jane 2021-02-22 1.0
9 Jane 2021-03-29 1.0
DataFrame.join
追加 Series with MultiIndex
的解决方案:
df = df.join(df.set_index('Date').groupby('Name')['Name'].rolling('28d', closed='both').count().rename('count'), on=['Name', 'Date'])
print (df)
Name Date count
0 Jack 2021-01-08 1.0
1 Jim 2021-01-27 1.0
2 Jack 2021-02-05 2.0
3 Jim 2021-02-10 2.0
4 Jack 2021-02-17 2.0
5 Jack 2021-02-18 3.0
6 Jim 2021-02-20 3.0
7 Jack 2021-02-21 4.0
8 Jane 2021-02-22 1.0
9 Jane 2021-03-29 1.0
我想计算一个值在过去 x 天出现的频率。在下面的示例中,我想计算过去 28 天 Name
列中值的频率。数据已按 Date
import pandas as pd
import time
d = {'Name': ['Jack', 'Jim', 'Jack', 'Jim', 'Jack', 'Jack', 'Jim', 'Jack', 'Jane', 'Jane'],
'Date': ['08/01/2021',
'27/01/2021',
'05/02/2021',
'10/02/2021',
'17/02/2021',
'18/02/2021',
'20/02/2021',
'21/02/2021',
'22/02/2021',
'29/03/2021']}
df = pd.DataFrame(data=d)
df['Date'] = pd.to_datetime(df.Date, format='%d/%m/%Y')
# Make sure pandas is sorted by Date
df = df.sort_values('Date')
我在 Whosebug 上找到了一些解决方案,但所有这些解决方案在数据集上都不正确,也不快速。
方法 1 - 不太正确
df['count1'] = df.set_index('Date').groupby('Name', sort=False)['Name'].rolling('28d', closed='both').count().tolist()
方法 2 - 正确的方法但是很慢 <~ 来自这个
df['count2'] = df.assign(count=1).groupby(['Name']).apply(lambda x: x.rolling('28d', on='Date').sum())['count']
方法 3 - 使用 sum
- 不正确
df['count3'] = df.assign(count=1).groupby('Name').rolling('28d', on='Date').sum().reset_index().sort_values('Date')['count']
方法 4 - 也使用 sum
- 不正确,因为索引不正确 <~ 这个
df['count4'] = df.set_index('Date').assign(count_last=1).groupby('Name').rolling('28d').sum().reset_index()["count_last"]
输出
Name Date count1 count2 count3 count4
0 Jack 2021-01-08 1.0 1.0 1.0 1.0
1 Jim 2021-01-27 2.0 1.0 1.0 1.0
2 Jack 2021-02-05 2.0 1.0 2.0 2.0
3 Jim 2021-02-10 3.0 2.0 3.0 3.0
4 Jack 2021-02-17 4.0 2.0 4.0 4.0 #<~ all are wrong here except approach 2
5 Jack 2021-02-18 1.0 3.0 1.0 1.0
6 Jim 2021-02-20 2.0 3.0 1.0 1.0
7 Jack 2021-02-21 3.0 4.0 1.0 1.0
8 Jane 2021-02-22 1.0 1.0 2.0 2.0
9 Jane 2021-03-29 1.0 1.0 3.0 3.0
表演
Method 1: 0.0014538764953613281 ms
Method 2: 0.0034720897674560547 ms
Method 3: 0.002077817916870117 ms
Method 4: 0.0035729408264160156 ms
已更新 <~ 解决方案
根据标记的答案,我编写了这个解决方案,我认为它在数据具有 NULL 值和重复项时有效。此外,它不会改变原始数据集的大小。
def frequency_of_last_n_days(df: pd.DataFrame, identifier: str, timestamp: str, delta: int) -> pd.DataFrame:
col_name = "count_%s" % identifier
temp_df = df.set_index(timestamp) \
.groupby(identifier, sort=False)[identifier] \
.rolling('%sd' % delta, closed='both') \
.count() \
.rename(col_name)
temp_df = temp_df[~temp_df.index.duplicated(keep="first")]
return df.merge(temp_df, how="left", left_on=[identifier, timestamp], right_index=True)
frequency_of_last_n_days(df, "Name", "Date", 30)
IIUC,问题出在您的 tolist()
上,它弄乱了索引对齐并打乱了输出。
改用merge
:
df2 = (df
.merge(df.set_index('Date')
.groupby('Name', sort=False)['Name']
.rolling('28d', closed='both') # do you really want closed="both"?
.count().rename('count'),
left_on=['Name', 'Date'], right_index=True
)
)
输出:
Name Date count
0 Jack 2021-01-08 1.0
1 Jim 2021-01-27 1.0
2 Jack 2021-02-05 2.0 <- if you want 1 here, remove closed='both'
3 Jim 2021-02-10 2.0
4 Jack 2021-02-17 2.0
5 Jack 2021-02-18 3.0
6 Jim 2021-02-20 3.0
7 Jack 2021-02-21 4.0
8 Jane 2021-02-22 1.0
9 Jane 2021-03-29 1.0
DataFrame.join
追加 Series with MultiIndex
的解决方案:
df = df.join(df.set_index('Date').groupby('Name')['Name'].rolling('28d', closed='both').count().rename('count'), on=['Name', 'Date'])
print (df)
Name Date count
0 Jack 2021-01-08 1.0
1 Jim 2021-01-27 1.0
2 Jack 2021-02-05 2.0
3 Jim 2021-02-10 2.0
4 Jack 2021-02-17 2.0
5 Jack 2021-02-18 3.0
6 Jim 2021-02-20 3.0
7 Jack 2021-02-21 4.0
8 Jane 2021-02-22 1.0
9 Jane 2021-03-29 1.0