合并 pandas 个数据框,其中一个值介于两个值之间

Merge pandas dataframes where one value is between two others

我需要在一个标识符上合并两个 pandas 数据帧,以及一个数据帧中的日期介于另一个数据帧中的两个日期之间的条件。

Dataframe A 有一个日期 ("fdate") 和一个 ID ("cusip"):

我需要将它与这个数据框 B 合并:

A.cusip==B.ncusipA.fdate 之间 B.namedtB.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'])]

这真的是最好的方法吗?似乎如果可以在合并中进行过滤以避免在合并之后但在过滤器完成之前有一个可能非常大的数据框,那就更好了。

目前没有 pandamic 方法可以做到这一点。

这个答案曾经是关于解决多态性问题的,结果证明这是一个非常糟糕的主意

然后另一个答案中出现了numpy.piecewise函数,但是没有什么解释,所以我想我会澄清这个函数是如何使用的。

分段的 Numpy 方式(内存很重)

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

这是一个数组列表,其中每个数组告诉我们术语时间跨度是否与我们拥有的两个 war 声明中的每一个匹配。 随着数据集的增加,条件可能会爆炸,因为它将是左 df 和右 df 的长度相乘。

分段"magic"

现在分段将从条款中取出 president_id 并将其放入每个相应 war 的 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 与包含它的时间跨度进行比较,它们 return 为真。

之后,您可以用这些对象填充您的 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 中-> 2015-2-5,所以这加强了我的上述假设。

如果大家有其他的想法,欢迎评论,我可以试试

pandas 解决方案如果实现类似于 R 中 data.table 包中的 foverlaps() 会很棒。到目前为止,我发现 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)

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

您现在应该可以使用包 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 正在为您做同样的事情。我没有对两者进行基准测试,但它更容易阅读。