如何在 python Pandas 中执行/解决条件连接?
Posted
技术标签:
【中文标题】如何在 python Pandas 中执行/解决条件连接?【英文标题】:How to do/workaround a conditional join in python Pandas? 【发布时间】:2014-06-23 20:56:38 【问题描述】:我正在尝试根据存储在单独表中的日期值来计算 Pandas 中基于时间的聚合。
第一个表table_a的顶部是这样的:
COMPANY_ID DATE MEASURE
1 2010-01-01 00:00:00 10
1 2010-01-02 00:00:00 10
1 2010-01-03 00:00:00 10
1 2010-01-04 00:00:00 10
1 2010-01-05 00:00:00 10
创建表格的代码如下:
table_a = pd.concat(\
[pd.DataFrame('DATE': pd.date_range("01/01/2010", "12/31/2010", freq="D"),\
'COMPANY_ID': 1 , 'MEASURE': 10),\
pd.DataFrame('DATE': pd.date_range("01/01/2010", "12/31/2010", freq="D"),\
'COMPANY_ID': 2 , 'MEASURE': 10)])
第二个表,table_b 如下所示:
COMPANY END_DATE
1 2010-03-01 00:00:00
1 2010-06-02 00:00:00
2 2010-03-01 00:00:00
2 2010-06-02 00:00:00
创建它的代码是:
table_b = pd.DataFrame('END_DATE':pd.to_datetime(['03/01/2010','06/02/2010','03/01/2010','06/02/2010']),\
'COMPANY':(1,1,2,2))
我希望能够在 table_b 中的 END_DATE 之前的每个 30 天期间获取每个 COMPANY_ID 的度量列的总和。
这是(我认为)SQL 等价物:
select
b.COMPANY_ID,
b.DATE
sum(a.MEASURE) AS MEASURE_TO_END_DATE
from table_a a, table_b b
where a.COMPANY = b.COMPANY and
a.DATE < b.DATE and
a.DATE > b.DATE - 30
group by b.COMPANY;
感谢您的帮助
【问题讨论】:
table_b 中的end_date
是否每个都有重叠窗口;例如,公司 1 的结束日期可以是 2010-03-01 和 2010-03-15。
嗨@KarlD 是的,可能。
【参考方案1】:
嗯,我能想到几个办法:
-
基本上通过仅在确切字段 (
company
) 上合并来炸毁数据框...然后在合并后的 30 天窗口上进行过滤。
-
将 30 天窗口上的合并和过滤移至
groupby()
。
选项#1
假设您的数据如下所示(我扩展了您的示例数据):
print df
company date measure
0 0 2010-01-01 10
1 0 2010-01-15 10
2 0 2010-02-01 10
3 0 2010-02-15 10
4 0 2010-03-01 10
5 0 2010-03-15 10
6 0 2010-04-01 10
7 1 2010-03-01 5
8 1 2010-03-15 5
9 1 2010-04-01 5
10 1 2010-04-15 5
11 1 2010-05-01 5
12 1 2010-05-15 5
print windows
company end_date
0 0 2010-02-01
1 0 2010-03-15
2 1 2010-04-01
3 1 2010-05-15
为 30 天窗口创建开始日期:
windows['beg_date'] = (windows['end_date'].values.astype('datetime64[D]') -
np.timedelta64(30,'D'))
print windows
company end_date beg_date
0 0 2010-02-01 2010-01-02
1 0 2010-03-15 2010-02-13
2 1 2010-04-01 2010-03-02
3 1 2010-05-15 2010-04-15
现在进行合并,然后根据date
是否在beg_date
和end_date
范围内进行选择:
df = df.merge(windows,on='company',how='left')
df = df[(df.date >= df.beg_date) & (df.date <= df.end_date)]
print df
company date measure end_date beg_date
2 0 2010-01-15 10 2010-02-01 2010-01-02
4 0 2010-02-01 10 2010-02-01 2010-01-02
7 0 2010-02-15 10 2010-03-15 2010-02-13
9 0 2010-03-01 10 2010-03-15 2010-02-13
11 0 2010-03-15 10 2010-03-15 2010-02-13
16 1 2010-03-15 5 2010-04-01 2010-03-02
18 1 2010-04-01 5 2010-04-01 2010-03-02
21 1 2010-04-15 5 2010-05-15 2010-04-15
23 1 2010-05-01 5 2010-05-15 2010-04-15
25 1 2010-05-15 5 2010-05-15 2010-04-15
您可以通过对company
和end_date
进行分组来计算 30 天窗口总和:
print df.groupby(['company','end_date']).sum()
measure
company end_date
0 2010-02-01 20
2010-03-15 30
1 2010-04-01 10
2010-05-15 15
选项 #2 将所有合并移动到一个 groupby 中。这在内存上应该会更好,但我认为要慢得多:
windows['beg_date'] = (windows['end_date'].values.astype('datetime64[D]') -
np.timedelta64(30,'D'))
def cond_merge(g,windows):
g = g.merge(windows,on='company',how='left')
g = g[(g.date >= g.beg_date) & (g.date <= g.end_date)]
return g.groupby('end_date')['measure'].sum()
print df.groupby('company').apply(cond_merge,windows)
company end_date
0 2010-02-01 20
2010-03-15 30
1 2010-04-01 10
2010-05-15 15
另一种选择现在,如果您的窗口从不重叠(如示例数据中所示),您可以执行以下操作作为替代方案,它不会破坏数据框但速度非常快:
windows['date'] = windows['end_date']
df = df.merge(windows,on=['company','date'],how='outer')
print df
company date measure end_date
0 0 2010-01-01 10 NaT
1 0 2010-01-15 10 NaT
2 0 2010-02-01 10 2010-02-01
3 0 2010-02-15 10 NaT
4 0 2010-03-01 10 NaT
5 0 2010-03-15 10 2010-03-15
6 0 2010-04-01 10 NaT
7 1 2010-03-01 5 NaT
8 1 2010-03-15 5 NaT
9 1 2010-04-01 5 2010-04-01
10 1 2010-04-15 5 NaT
11 1 2010-05-01 5 NaT
12 1 2010-05-15 5 2010-05-15
这种合并实质上是将您的窗口结束日期插入到数据框中,然后(按组)回填结束日期将为您提供一个结构来轻松创建汇总窗口:
df['end_date'] = df.groupby('company')['end_date'].apply(lambda x: x.bfill())
print df
company date measure end_date
0 0 2010-01-01 10 2010-02-01
1 0 2010-01-15 10 2010-02-01
2 0 2010-02-01 10 2010-02-01
3 0 2010-02-15 10 2010-03-15
4 0 2010-03-01 10 2010-03-15
5 0 2010-03-15 10 2010-03-15
6 0 2010-04-01 10 NaT
7 1 2010-03-01 5 2010-04-01
8 1 2010-03-15 5 2010-04-01
9 1 2010-04-01 5 2010-04-01
10 1 2010-04-15 5 2010-05-15
11 1 2010-05-01 5 2010-05-15
12 1 2010-05-15 5 2010-05-15
df = df[df.end_date.notnull()]
df['beg_date'] = (df['end_date'].values.astype('datetime64[D]') -
np.timedelta64(30,'D'))
print df
company date measure end_date beg_date
0 0 2010-01-01 10 2010-02-01 2010-01-02
1 0 2010-01-15 10 2010-02-01 2010-01-02
2 0 2010-02-01 10 2010-02-01 2010-01-02
3 0 2010-02-15 10 2010-03-15 2010-02-13
4 0 2010-03-01 10 2010-03-15 2010-02-13
5 0 2010-03-15 10 2010-03-15 2010-02-13
7 1 2010-03-01 5 2010-04-01 2010-03-02
8 1 2010-03-15 5 2010-04-01 2010-03-02
9 1 2010-04-01 5 2010-04-01 2010-03-02
10 1 2010-04-15 5 2010-05-15 2010-04-15
11 1 2010-05-01 5 2010-05-15 2010-04-15
12 1 2010-05-15 5 2010-05-15 2010-04-15
df = df[(df.date >= df.beg_date) & (df.date <= df.end_date)]
print df.groupby(['company','end_date']).sum()
measure
company end_date
0 2010-02-01 20
2010-03-15 30
1 2010-04-01 10
2010-05-15 15
另一种选择是将您的第一个数据帧重新采样为每日数据,然后以 30 天的窗口计算 rolling_sums;并在末尾选择您感兴趣的日期。这也可能会占用大量内存。
【讨论】:
+1 用于展示两种策略及其优势/劣势。【参考方案2】:有一种非常简单实用(或者可能是唯一直接的方法)在 pandas 中进行条件连接。由于在 pandas 中没有直接的方法来进行条件连接,因此您将需要一个额外的库,即pandasql
使用命令 pip install pandasql
从 pip 安装库 pandasql
。该库允许您使用 SQL 查询来操作 pandas 数据帧。
import pandas as pd
from pandasql import sqldf
df = pd.read_excel(r'play_data.xlsx')
df
id Name Amount
0 A001 A 100
1 A002 B 110
2 A003 C 120
3 A005 D 150
现在让我们做一个条件连接来比较 ID 的数量
# Make your pysqldf object:
pysqldf = lambda q: sqldf(q, globals())
# Write your query in SQL syntax, here you can use df as a normal SQL table
cond_join= '''
select
df_left.*,
df_right.*
from df as df_left
join df as df_right
on
df_left.[Amount] > (df_right.[Amount]+10)
'''
# Now, get your queries results as dataframe using the sqldf object that you created
pysqldf(cond_join)
id Name Amount id Name Amount
0 A003 C 120 A001 A 100
1 A005 D 150 A001 A 100
2 A005 D 150 A002 B 110
3 A005 D 150 A003 C 120
【讨论】:
这正是我想要的。感谢您的回答!! PS - 我在一个函数中调用它并使用locals()
而不是globals()
。 cond_join = """ select df_left.*, df_right.* from t_p as df_left join t_s as df_right on df_left.short_key = df_right.short_key """
combo = sqldf(cond_join, locals())
任何人都知道 pandas 在幕后实际上做了什么。我想它会遵循上述策略之一。以上是关于如何在 python Pandas 中执行/解决条件连接?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 FOR 循环中对 Python Pandas 列表中的元素执行字符串更改