合并Pandas数据框,其中一个值介于另外两个值之间。

97
我需要在一个标识符和一个条件上合并两个pandas数据框,其中一个数据框中的日期位于另一个数据框的两个日期之间。
数据框A有一个日期(“fdate”)和一个ID(“cusip”): enter image description here 我需要将其与此数据框B合并: enter image description here 在 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'])]

这真的是做这件事情的最佳方式吗?似乎更好的方式是可以在合并过程中进行筛选,以避免在筛选完成之前出现可能非常大的数据框。


2
@EdChum,你觉得在GitHub上提出这个功能请求有意义吗?你认为为什么这不是一个现成的功能呢? - itzy
在处理dfs时,某种程度上不应期望像SQL那样的功能。特别是在合并之前/之后,您可以对dfs进行过滤。如果需要,您可以提出一个请求,因为我不是开发人员,但他们非常乐于回应。 - EdChum
2
如果还没有完成,我认为在 GitHub 上提出功能请求是个好主意。这种问题在这里经常出现,但我从未见过一个真正好的答案——好的意思是使用 pandas 很容易实现。 - JohnE
1
我猜这里的问题是为此定义一个合理的API,目前有命名参数,如果您开始允许诸如小于、大于等的东西,那么它可能会变得混乱,否则您可以允许用户字符串进行评估,但即使这样也可能很麻烦,因为在比较时顺序可能很重要,这是我的想法。 - EdChum
为了完整起见,我在GitHub上开了一个问题:https://github.com/pydata/pandas/issues/10309 - itzy
显示剩余5条评论
4个回答

88

正如你所说,这在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

14
你介意提供一些关于这个解决方案与其他解决方案相比表现的基准测试吗?这个回答似乎并没有回答问题,更像是规避问题,除非这个解决方案实际上是表现良好的解决方案。 - firelynx
4
工作合适的工具。 - hibernado
1
我今天在解决一个相关问题时使用了这个想法,效果很好。值得注意的是,对于大型数据集,通过为连接条件中使用的任何列创建索引,您很可能会获得显着的速度提升。 - sjw
4
我发现这个解决方案比这里提供的解决方案快了10倍以上:https://dev59.com/IFYN5IYBdhLWcg3w_84w#46526249 - user915
1
我对这个答案感到满意。只是想确认一下(将近两年后)这是否已经被集成到pandas中了?看起来是个好主意。 - MYK
显示剩余4条评论

40

现在,您可以使用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也会为您完成同样的工作。我还没有对这两个进行基准测试,但pandasql更易于阅读。


为什么不把where条件包含在ON条件中: 从A表内连接B表ON A.cusip = B.ncusip AND A.fdate>= B.namedt AND fdate <= B.nameenddt 按A.cusip分组。 - Wael Hussein
@WaelHussein 我认为内连接并不重要。你应该使用上述的外连接。 - chris dorn

14

目前没有流行病学方式做到这一点。

这个答案原本是关于使用多态性解决问题的,但结果证明这是一个非常糟糕的想法。

然后,在另一个答案中出现了numpy.piecewise函数,但没有太多解释,因此我想澄清如何使用此函数。

Numpy方法使用 piecewise(内存占用高)

np.piecewise函数可用于生成自定义连接的行为。这涉及大量开销,并不是非常有效,但它能胜任工作。

生成连接条件

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)]

这是一个数组列表,每个数组告诉我们每个战争宣布的时间跨度是否匹配。 随着数据集的增大,条件可能会变得复杂,因为左侧df和右侧df的长度将被乘以。

分段“魔法”

现在,piecewise将从术语中获取 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

现在,为了完成这个例子,我们只需要定期合并总统的名称。

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对象列的数据类型为datetime dtype,而由于时间跨度不同,pandas会默默地拒绝在它们上面合并。

如果我们实例化这些类的两个对象,它们现在可以进行比较:

pit = PointInTime(2015,1,1)
ts = Timespan(datetime(2014,1,1), datetime(2015,2,2))
pit == ts
True
我们也可以用这些对象填充两个数据帧:
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仅包括在时间跨度2015-2-1 -> 2015-2-5中,因此这加强了我上面的假设。

如果有其他想法,请留言让我尝试。


__neq__ doesn't work, you need __ne__ - Jean-François Fabre
4
阅读这个内容时,2020年的“pandamic”让人想到了“pandemic”。不过答案仍然很不错。 - flow2k
使用numpy的分段函数时需要注意,如果没有任何条件被满足,分段函数会默认输出0。这可能会导致误导行为。 - Giovanni Patruno
非常感谢您详细的回答和对多态性概念的贡献。 - pablete

4
如果实现方式与R语言中data.table包中的foverlaps()类似,那么使用pandas解决方案将会很好。 到目前为止,我发现numpy的piecewise()函数非常高效。 我提供了基于早期讨论 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)

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