我需要在标识符和条件上合并两个pandas数据帧,其中一个数据帧中的日期在另一个数据帧中的两个日期之间。
数据框A有一个日期(" fdate")和一个ID(" cusip"):
我需要将此合并到此数据框B:
A.cusip==B.ncusip
和A.fdate
上的位于B.namedt
和B.nameenddt
之间。
在SQL中这将是微不足道的,但我能看到如何在pandas中执行此操作的唯一方法是首先在标识符上无条件合并,然后在日期条件上进行过滤:
df = pd.merge(A, B, how='inner', left_on='cusip', right_on='ncusip')
df = df[(df['fdate']>=df['namedt']) & (df['fdate']<=df['nameenddt'])]
这真的是最好的方法吗?似乎如果可以在合并中进行过滤以避免在合并之后但在过滤器完成之前具有可能非常大的数据帧,则会好得多。
答案 0 :(得分:25)
正如你所说,这在SQL中非常简单,那么为什么不在SQL中呢?
import pandas as pd
import sqlite3
#We'll use firelynx's tables:
presidents = pd.DataFrame({"name": ["Bush", "Obama", "Trump"],
"president_id":[43, 44, 45]})
terms = pd.DataFrame({'start_date': pd.date_range('2001-01-20', periods=5, freq='48M'),
'end_date': pd.date_range('2005-01-21', periods=5, freq='48M'),
'president_id': [43, 43, 44, 44, 45]})
war_declarations = pd.DataFrame({"date": [datetime(2001, 9, 14), datetime(2003, 3, 3)],
"name": ["War in Afghanistan", "Iraq War"]})
#Make the db in memory
conn = sqlite3.connect(':memory:')
#write the tables
terms.to_sql('terms', conn, index=False)
presidents.to_sql('presidents', conn, index=False)
war_declarations.to_sql('wars', conn, index=False)
qry = '''
select
start_date PresTermStart,
end_date PresTermEnd,
wars.date WarStart,
presidents.name Pres
from
terms join wars on
date between start_date and end_date join presidents on
terms.president_id = presidents.president_id
'''
df = pd.read_sql_query(qry, conn)
DF:
PresTermStart PresTermEnd WarStart Pres
0 2001-01-31 00:00:00 2005-01-31 00:00:00 2001-09-14 00:00:00 Bush
1 2001-01-31 00:00:00 2005-01-31 00:00:00 2003-03-03 00:00:00 Bush
答案 1 :(得分:13)
您现在应该可以使用包pandasql
执行此操作import pandasql as ps
sqlcode = '''
select A.cusip
from A
inner join B on A.cusip=B.ncusip
where A.fdate >= B.namedt and A.fdate <= B.nameenddt
group by A.cusip
'''
newdf = ps.sqldf(sqlcode,locals())
我认为来自@ChuHo的答案很好。我相信pandasql正在为你做同样的事情。我没有对这两者进行基准测试,但它更容易阅读。
答案 2 :(得分:7)
这个答案过去常常是关于解决多态性的问题,这个问题被认为是一个非常糟糕的主意。
然后numpy.piecewise
函数出现在另一个答案中,但几乎没有解释,所以我想我会澄清如何使用这个函数。
np.piecewise
函数可用于生成自定义联接的行为。涉及到很多开销,并且它不是非常有效的perse,但它可以完成这项任务。
import pandas as pd
from datetime import datetime
presidents = pd.DataFrame({"name": ["Bush", "Obama", "Trump"],
"president_id":[43, 44, 45]})
terms = pd.DataFrame({'start_date': pd.date_range('2001-01-20', periods=5, freq='48M'),
'end_date': pd.date_range('2005-01-21', periods=5, freq='48M'),
'president_id': [43, 43, 44, 44, 45]})
war_declarations = pd.DataFrame({"date": [datetime(2001, 9, 14), datetime(2003, 3, 3)],
"name": ["War in Afghanistan", "Iraq War"]})
start_end_date_tuples = zip(terms.start_date.values, terms.end_date.values)
conditions = [(war_declarations.date.values >= start_date) &
(war_declarations.date.values <= end_date) for start_date, end_date in start_end_date_tuples]
> conditions
[array([ True, True], dtype=bool),
array([False, False], dtype=bool),
array([False, False], dtype=bool),
array([False, False], dtype=bool),
array([False, False], dtype=bool)]
这是一个数组列表,其中每个数组告诉我们,对于我们拥有的两个war声明中的每一个,术语时间跨度是否匹配。 条件可能会因较大的数据集而爆炸,因为它将是左侧df的长度和右侧df的倍数。
现在,分段将从条款中取出president_id
并将其放入每个相应战争的war_declarations
数据框中。
war_declarations['president_id'] = np.piecewise(np.zeros(len(war_declarations)),
conditions,
terms.president_id.values)
date name president_id
0 2001-09-14 War in Afghanistan 43.0
1 2003-03-03 Iraq War 43.0
现在要完成这个例子,我们只需要定期合并总统&#39;名。
war_declarations.merge(presidents, on="president_id", suffixes=["_war", "_president"])
date name_war president_id name_president
0 2001-09-14 War in Afghanistan 43.0 Bush
1 2003-03-03 Iraq War 43.0 Bush
我想分享我的研究工作,所以即使无法解决问题,我希望允许允许在此处作为有用的回复最小。由于很难发现错误,其他人可能会尝试这个并认为他们有一个有效的解决方案,而事实上,他们并没有。
我唯一能想到的另一种方法是创建两个新类,一个PointInTime和一个Timespan
两者都应该有__eq__
方法,如果将PointInTime与包含它的Timespan进行比较,它们将返回true。
之后,您可以使用这些对象填充DataFrame,并加入它们所在的列。
这样的事情:
class PointInTime(object):
def __init__(self, year, month, day):
self.dt = datetime(year, month, day)
def __eq__(self, other):
return other.start_date < self.dt < other.end_date
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "{}-{}-{}".format(self.dt.year, self.dt.month, self.dt.day)
class Timespan(object):
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
def __eq__(self, other):
return self.start_date < other.dt < self.end_date
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "{}-{}-{} -> {}-{}-{}".format(self.start_date.year, self.start_date.month, self.start_date.day,
self.end_date.year, self.end_date.month, self.end_date.day)
重要提示:我没有将datetime子类化,因为pandas会将datetime对象列的dtype视为datetime dtype,并且由于时间跨度不是,pandas会默默地拒绝合并它们。
如果我们实例化这些类的两个对象,现在可以比较它们:
pit = PointInTime(2015,1,1)
ts = Timespan(datetime(2014,1,1), datetime(2015,2,2))
pit == ts
True
我们还可以用这些对象填充两个DataFrame:
df = pd.DataFrame({"pit":[PointInTime(2015,1,1), PointInTime(2015,2,2), PointInTime(2015,3,3)]})
df2 = pd.DataFrame({"ts":[Timespan(datetime(2015,2,1), datetime(2015,2,5)), Timespan(datetime(2015,2,1), datetime(2015,4,1))]})
然后是合并的作品:
pd.merge(left=df, left_on='pit', right=df2, right_on='ts')
pit ts
0 2015-2-2 2015-2-1 -> 2015-2-5
1 2015-2-2 2015-2-1 -> 2015-4-1
但只是一种。
PointInTime(2015,3,3)
也应该包含在此Timespan(datetime(2015,2,1), datetime(2015,4,1))
但事实并非如此。
我认为pandas将PointInTime(2015,3,3)
与PointInTime(2015,2,2)
进行比较,并假设由于它们不相等,PointInTime(2015,3,3)
不能等于Timespan(datetime(2015,2,1), datetime(2015,4,1))
,因为此时间跨度等于PointInTime(2015,2,2)
有点像这样:
Rose == Flower
Lilly != Rose
因此:
Lilly != Flower
编辑:
我试图使所有PointInTime彼此相等,这改变了加入的行为以包括2015-3-3,但2015-2-2仅包含在Timespan 2015-2-1中 - &gt ; 2015-2-5,所以这加强了我的上述假设。
如果有人有任何其他想法,请发表评论,我可以试试。
答案 3 :(得分:3)
如果实现类似于来自R中data.table包的foverlaps(),大熊猫解决方案会很棒。到目前为止,我发现numpy的分段()效率很高。我已根据之前的讨论Merging dataframes based on date range
提供了代码A['permno'] = np.piecewise(np.zeros(A.count()[0]),
[ (A['cusip'].values == id) & (A['fdate'].values >= start) & (A['fdate'].values <= end) for id, start, end in zip(B['ncusip'].values, B['namedf'].values, B['nameenddt'].values)],
B['permno'].values).astype(int)