熊猫:带有日期条件的SQL SelfJoin

时间:2018-10-11 21:19:49

标签: python pandas pandas-groupby

我经常在关系数据库中的SQL中执行的一个查询是将表重新连接到自身,并根据相同ID的记录向后或向前及时汇总每一行。

例如,假设table1为列“ ID”,“日期”,“变量1”

在SQL中,我可以对每个记录的过去三个月的var1求和:

Select a.ID, a.Date, sum(b.Var1) as sum_var1
from table1 a
left outer join table1 b
on a.ID = b.ID
and months_between(a.date,b.date) <0
and months_between(a.date,b.date) > -3

熊猫有没有办法做到这一点?

3 个答案:

答案 0 :(得分:2)

似乎您需要GroupBy + rolling。以与用SQL编写的方式完全相同的方式来实现逻辑可能很昂贵,因为它将涉及重复的循环。让我们来看一个示例数据框:

        Date  ID  Var1
0 2015-01-01   1     0
1 2015-02-01   1     1
2 2015-03-01   1     2
3 2015-04-01   1     3
4 2015-05-01   1     4
5 2015-01-01   2     5
6 2015-02-01   2     6
7 2015-03-01   2     7
8 2015-04-01   2     8
9 2015-05-01   2     9

您可以添加一列,该列按组回顾并在固定期间内对变量求和。首先使用pd.Series.rolling定义一个函数:

def lookbacker(x):
    """Sum over past 70 days"""
    return x.rolling('70D').sum().astype(int)

然后将其应用于GroupBy对象并提取值以进行赋值:

df['Lookback_Sum'] = df.set_index('Date').groupby('ID')['Var1'].apply(lookbacker).values

print(df)

        Date  ID  Var1  Lookback_Sum
0 2015-01-01   1     0             0
1 2015-02-01   1     1             1
2 2015-03-01   1     2             3
3 2015-04-01   1     3             6
4 2015-05-01   1     4             9
5 2015-01-01   2     5             5
6 2015-02-01   2     6            11
7 2015-03-01   2     7            18
8 2015-04-01   2     8            21
9 2015-05-01   2     9            24

pd.Series.rolling似乎不能用几个月的时间,例如使用'2M'(2个月)而不是'70D'(70天)得出ValueError: <2 * MonthEnds> is a non-fixed frequency。这是有道理的,因为考虑到月份的天数不同,“月份”是不明确的。

值得一提的另一点是,您可以直接使用GroupBy + rolling,并且可以通过绕过apply来更有效地使用它,但这需要确保索引是单项的。例如,通过sort_index

df['Lookback_Sum'] = df.set_index('Date').sort_index()\
                       .groupby('ID')['Var1'].rolling('70D').sum()\
                       .astype(int).values

答案 1 :(得分:0)

我认为pandas.DataFrame.rolling()不支持滚动窗口聚合几个月;当前,您必须指定固定的天数或其他固定长度的时间段。

但是正如@jpp所提到的,您可以使用python循环对日历月份中指定的窗口大小执行滚动聚合,其中每个窗口中的天数会有所不同,具体取决于您要滚动到日历的哪一部分

以下方法基于this SO answer和@jpp:

# Build some example data:
# 3 unique IDs, each with 365 samples, one sample per day throughout 2015
df = pd.DataFrame({'Date': pd.date_range('2015-01-01', '2015-12-31', freq='D'),
                   'Var1': list(range(365))})
df = pd.concat([df] * 3)
df['ID'] = [1]*365 + [2]*365 + [3]*365
df.head()
        Date  Var1  ID
0 2015-01-01     0   1
1 2015-01-02     1   1
2 2015-01-03     2   1
3 2015-01-04     3   1
4 2015-01-05     4   1

# Define a lookback function that mimics rolling aggregation,
# but uses DateOffset() slicing, rather than a window of fixed size.
# Use .count() here as a sanity check; you will need .sum()
def lookbacker(ser): 
    return pd.Series([ser.loc[d - pd.offsets.DateOffset(months=3):d].count() 
                      for d in ser.index])

# By default, groupby.agg output is sorted by key. So make sure to 
# sort df by (ID, Date) before inserting the flattened groupby result 
# into a new column
df.sort_values(['ID', 'Date'], inplace=True)
df.set_index('Date', inplace=True)
df['window_size'] = df.groupby('ID')['Var1'].apply(lookbacker).values

# Manually check the resulting window sizes
df.head()
            Var1  ID  window_size
Date                             
2015-01-01     0   1            1
2015-01-02     1   1            2
2015-01-03     2   1            3
2015-01-04     3   1            4
2015-01-05     4   1            5

df.tail()
            Var1  ID  window_size
Date                             
2015-12-27   360   3           92
2015-12-28   361   3           92
2015-12-29   362   3           92
2015-12-30   363   3           92
2015-12-31   364   3           93

df[df.ID == 1].loc['2015-05-25':'2015-06-05']
            Var1  ID  window_size
Date                             
2015-05-25   144   1           90
2015-05-26   145   1           90
2015-05-27   146   1           90
2015-05-28   147   1           90
2015-05-29   148   1           91
2015-05-30   149   1           92
2015-05-31   150   1           93
2015-06-01   151   1           93
2015-06-02   152   1           93
2015-06-03   153   1           93
2015-06-04   154   1           93
2015-06-05   155   1           93

最后一列提供了以天为单位的回溯窗口大小,从该日期开始回溯,包括开始和结束日期。

2016-05-31之前查找“ 3个月”将使您进入2015-02-31,但是2015年2月只有28天。如您在上述健全性检查的顺序90, 91, 92, 93中所见,这种DateOffset方法将5月的最后四天映射到2月的最后一天:

pd.to_datetime('2015-05-31') - pd.offsets.DateOffset(months=3)
Timestamp('2015-02-28 00:00:00')

pd.to_datetime('2015-05-30') - pd.offsets.DateOffset(months=3)
 Timestamp('2015-02-28 00:00:00')

pd.to_datetime('2015-05-29') - pd.offsets.DateOffset(months=3)
Timestamp('2015-02-28 00:00:00')

pd.to_datetime('2015-05-28') - pd.offsets.DateOffset(months=3)
Timestamp('2015-02-28 00:00:00')

我不知道这是否与SQL的行为相匹配,但是无论如何,您都需要对此进行测试,并确定这是否对您有意义。

答案 2 :(得分:0)

您可以使用lambda来实现。

table1['sum_var1'] = table1.apply(lambda row: findSum(row), axis=1)

我们应该为months_between写一个等效的方法

完整的示例是

from datetime import datetime
import datetime as dt
import pandas as pd

def months_between(date1, date2):
    if date1.day == date2.day:
        return (date1.year - date2.year) * 12 + date1.month - date2.month
    # if both are last days
    if date1.month != (date1 + dt.timedelta(days=1)).month :
        if date2.month != (date2 + dt.timedelta(days=1)).month :
            return date1.month - date2.month
    return (date1 - date2).days / 31

def findSum(cRow):
    table1['month_diff'] = table1['Date'].apply(months_between, date2=cRow['Date'])
    filtered_table = table1[(table1["month_diff"] < 0) & (table1["month_diff"] > -3) & (table1['ID'] == cRow['ID'])]
    if filtered_table.empty:
        return 0
    return filtered_table['Var1'].sum()


table1 = pd.DataFrame(columns = ['ID', 'Date', 'Var1'])
table1.loc[len(table1)] = [1, datetime.strptime('2015-01-01','%Y-%m-%d'), 0]
table1.loc[len(table1)] = [1, datetime.strptime('2015-02-01','%Y-%m-%d'), 1]
table1.loc[len(table1)] = [1, datetime.strptime('2015-03-01','%Y-%m-%d'), 2]
table1.loc[len(table1)] = [1, datetime.strptime('2015-04-01','%Y-%m-%d'), 3]
table1.loc[len(table1)] = [1, datetime.strptime('2015-05-01','%Y-%m-%d'), 4]

table1.loc[len(table1)] = [2, datetime.strptime('2015-01-01','%Y-%m-%d'), 5]
table1.loc[len(table1)] = [2, datetime.strptime('2015-02-01','%Y-%m-%d'), 6]
table1.loc[len(table1)] = [2, datetime.strptime('2015-03-01','%Y-%m-%d'), 7]
table1.loc[len(table1)] = [2, datetime.strptime('2015-04-01','%Y-%m-%d'), 8]
table1.loc[len(table1)] = [2, datetime.strptime('2015-05-01','%Y-%m-%d'), 9]

table1['sum_var1'] = table1.apply(lambda row: findSum(row), axis=1)
table1.drop(columns=['month_diff'], inplace=True)
print(table1)
相关问题