我如何与熊猫执行(INNER|(LEFT|RIGHT|FULL)OUTER)JOIN?如何在合并后为缺少的行添加NaN?合并后如何消除NaN?我可以在索引上合并吗?如何合并多个DataFrame?与熊猫交叉连接合并参加凹面?使现代化谁什么为什么?

…等等。我看到了这些反复出现的问题,这些问题涉及熊猫合并功能的各个方面。今天,关于合并及其各种用例的大部分信息都分散在数十篇措辞糟糕、无法搜索的帖子中。这里的目的是为后人整理一些更重要的观点。

这篇问答旨在成为一系列关于熊猫常用习惯用法的有用用户指南的下一篇(请参阅这篇关于旋转的文章和这篇关于串联的文章,稍后我将对此进行讨论)。

请注意,这篇文章并不是用来替代文档的,所以请同时阅读!其中一些例子就是从那里得到的。


目录

为了方便访问。

合并基础知识-连接的基本类型(请先阅读)基于索引的联接推广到多个数据帧交叉联接


这篇文章旨在向读者介绍SQL风格的与Pandas的合并,如何使用它,以及何时不使用它。

特别是,这篇文章将经历以下内容:

基础知识-连接类型(左、右、外、内)使用不同的列名合并合并多个列避免输出中出现重复的合并键列

这篇帖子(以及我在这篇帖子上的其他帖子)不会经历什么:

与绩效相关的讨论和时间安排(目前)。最值得注意的是,在适当的情况下,提到了更好的替代方案。处理后缀、删除额外列、重命名输出和其他特定用例。还有其他(读:更好)的帖子可以解决这个问题,所以好好想想吧!

笔记除非另有规定,大多数示例在演示各种功能时默认使用INNER JOIN操作。此外,可以复制和复制此处的所有数据帧你可以和他们一起玩。另外,请参见邮递如何从剪贴板读取DataFrames。最后,JOIN操作的所有可视化表示都是使用GoogleDrawings手工绘制的。灵感来自这里。



说得够多了-告诉我如何使用合并!

设置和基础知识

np.random.seed(0)
left = pd.DataFrame({'key': ['A', 'B', 'C', 'D'], 'value': np.random.randn(4)})
right = pd.DataFrame({'key': ['B', 'D', 'E', 'F'], 'value': np.random.randn(4)})

left

  key     value
0   A  1.764052
1   B  0.400157
2   C  0.978738
3   D  2.240893

right

  key     value
0   B  1.867558
1   D -0.977278
2   E  0.950088
3   F -0.151357

为了简单起见,键列具有相同的名称(目前)。

INNER JOIN表示为

笔记这一点以及即将公布的数字都遵循这一惯例:蓝色表示合并结果中存在的行红色表示从结果中排除的行(即已删除)绿色表示结果中缺少用NaN替换的值

要执行INNER JOIN,请在左侧DataFrame上调用merge,将右侧DataFrame和连接键(至少)指定为参数。

left.merge(right, on='key')
# Or, if you want to be explicit
# left.merge(right, on='key', how='inner')

  key   value_x   value_y
0   B  0.400157  1.867558
1   D  2.240893 -0.977278

这只返回共享一个公共键(在本例中为“B”和“D”)的左右行。

LEFT OUTER JOIN或LEFT JOIN由

这可以通过指定how='left'来执行。

left.merge(right, on='key', how='left')

  key   value_x   value_y
0   A  1.764052       NaN
1   B  0.400157  1.867558
2   C  0.978738       NaN
3   D  2.240893 -0.977278

仔细注意此处NaN的位置。如果指定how='left',则只使用左侧的键,右侧缺失的数据将被NaN替换。

同样,对于RIGHT OUTER JOIN或RIGHT JOIN,它是。。。

…指定如何=“正确”:

left.merge(right, on='key', how='right')

  key   value_x   value_y
0   B  0.400157  1.867558
1   D  2.240893 -0.977278
2   E       NaN  0.950088
3   F       NaN -0.151357

这里,使用右边的键,左边缺失的数据用NaN替换。

最后,对于FULL OUTER JOIN,由

指定how=“外部”。

left.merge(right, on='key', how='outer')

  key   value_x   value_y
0   A  1.764052       NaN
1   B  0.400157  1.867558
2   C  0.978738       NaN
3   D  2.240893 -0.977278
4   E       NaN  0.950088
5   F       NaN -0.151357

这将使用两个帧中的关键帧,并为两帧中缺少的行插入NaN。

文档很好地总结了这些不同的合并:


其他联接-左排除、右排除和全排除/反联接

如果需要两个步骤中的LEFT Exclusive JOIN和RIGHT Exclusive JOINs。

对于LEFT Exclude JOIN,表示为

首先执行LEFT OUTER JOIN,然后过滤到仅来自左侧的行(不包括来自右侧的所有行),

(left.merge(right, on='key', how='left', indicator=True)
     .query('_merge == "left_only"')
     .drop('_merge', 1))

  key   value_x  value_y
0   A  1.764052      NaN
2   C  0.978738      NaN

哪里

left.merge(right, on='key', how='left', indicator=True)

  key   value_x   value_y     _merge
0   A  1.764052       NaN  left_only
1   B  0.400157  1.867558       both
2   C  0.978738       NaN  left_only
3   D  2.240893 -0.977278       both

同样,对于RIGHT Exclusive JOIN,

(left.merge(right, on='key', how='right', indicator=True)
     .query('_merge == "right_only"')
     .drop('_merge', 1))

  key  value_x   value_y
2   E      NaN  0.950088
3   F      NaN -0.151357

最后,如果需要执行只保留左键或右键而不同时保留两者的合并(IOW,执行ANTI-JOIN),

你可以用类似的方式-

(left.merge(right, on='key', how='outer', indicator=True)
     .query('_merge != "both"')
     .drop('_merge', 1))

  key   value_x   value_y
0   A  1.764052       NaN
2   C  0.978738       NaN
4   E       NaN  0.950088
5   F       NaN -0.151357

键列的不同名称

如果键列的名称不同,例如,left有keyLeft,right有keyRight而不是key,则必须将left_on和right_on指定为参数而不是on:

left2 = left.rename({'key':'keyLeft'}, axis=1)
right2 = right.rename({'key':'keyRight'}, axis=1)

left2

  keyLeft     value
0       A  1.764052
1       B  0.400157
2       C  0.978738
3       D  2.240893

right2

  keyRight     value
0        B  1.867558
1        D -0.977278
2        E  0.950088
3        F -0.151357
left2.merge(right2, left_on='keyLeft', right_on='keyRight', how='inner')

  keyLeft   value_x keyRight   value_y
0       B  0.400157        B  1.867558
1       D  2.240893        D -0.977278

避免输出中的重复键列

在从左侧合并keyLeft和从右侧合并keyRight时,如果只希望输出中包含keyLeft或keyRight(但不同时包含两者),则可以首先将索引设置为初步步骤。

left3 = left2.set_index('keyLeft')
left3.merge(right2, left_index=True, right_on='keyRight')

    value_x keyRight   value_y
0  0.400157        B  1.867558
1  2.240893        D -0.977278

与前面命令的输出(即left2.merge的输出(right2,left_on='keyLeft',right_on='keyRight',how='inner')相比,您会发现缺少keyLeft。您可以根据将哪个帧的索引设置为关键帧来确定要保留哪个列。例如,当执行一些OUTER JOIN操作时,这可能很重要。


仅合并其中一个DataFrame中的一列

例如,考虑

right3 = right.assign(newcol=np.arange(len(right)))
right3
  key     value  newcol
0   B  1.867558       0
1   D -0.977278       1
2   E  0.950088       2
3   F -0.151357       3

如果您只需要合并“newcol”(没有任何其他列),则通常可以在合并之前仅对列进行子集:

left.merge(right3[['key', 'newcol']], on='key')

  key     value  newcol
0   B  0.400157       0
1   D  2.240893       1

如果您正在执行LEFT OUTER JOIN,则更高性能的解决方案将涉及映射:

# left['newcol'] = left['key'].map(right3.set_index('key')['newcol']))
left.assign(newcol=left['key'].map(right3.set_index('key')['newcol']))

  key     value  newcol
0   A  1.764052     NaN
1   B  0.400157     0.0
2   C  0.978738     NaN
3   D  2.240893     1.0

如上所述,这类似于,但比

left.merge(right3[['key', 'newcol']], on='key', how='left')

  key     value  newcol
0   A  1.764052     NaN
1   B  0.400157     0.0
2   C  0.978738     NaN
3   D  2.240893     1.0

合并多个列

要在多个列上连接,请为on(或left_on和right_on,视情况而定)指定一个列表。

left.merge(right, on=['key1', 'key2'] ...)

或者在名称不同的情况下,

left.merge(right, left_on=['lkey1', 'lkey2'], right_on=['rkey1', 'rkey2'])

其他有用的合并*操作和功能

将DataFrame与索引上的Series合并:请参阅此答案。除了合并,在某些情况下,还使用DataFrame.update和DataFrame.combine_first来更新一个DataFrame与另一个DataFrame。pd.merge_ordered是有序JOIN的一个有用函数。pd.merge_asof(读:merge_asof)对于近似连接很有用。

本节仅介绍最基本的内容,目的仅在于刺激您的食欲。有关更多示例和案例,请参阅关于合并、联接和连接的文档以及函数规范的链接。



继续阅读

跳转到Pandas Merging 101中的其他主题继续学习:

合并基础知识-连接的基本类型*基于索引的联接推广到多个数据帧交叉联接

*你在这里。


我认为你应该在解释中包括这一点,因为这是我经常看到的相关合并,我认为这被称为交叉连接。这是一个合并,当唯一的df不共享任何列时发生,它只是并排合并2个dfs:

设置:

names1 = [{'A':'Jack', 'B':'Jill'}]

names2 = [{'C':'Tommy', 'D':'Tammy'}]

df1=pd.DataFrame(names1)
df2=pd.DataFrame(names2)
df_merged= pd.merge(df1.assign(X=1), df2.assign(X=1), on='X').drop('X', 1)

这将创建一个虚拟X列,在X上合并,然后将其放置以生成

数据_合并:

      A     B      C      D
0  Jack  Jill  Tommy  Tammy

pd.contat([df0,df1],kwargs)的补充视觉视图。注意,kwargaxis=0或axis=1的含义不像df.min()或df.apply(func)那样直观



在这个答案中,我将考虑以下实例:

pandas.contat公司pandas.DataFrame.merge合并一个索引和另一个索引的列中的数据帧。

我们将为每种情况使用不同的数据帧。


1.pandas目录

考虑以下具有相同列名的DataFrames:

价格2018,尺寸(8784,5)年-月-日-小时价格0 2018 1 1 1 6.741 2018 1 1 2 4.742 2018 1 1 3 3.663 2018 1 1 4 2.304 2018 1 1 5 2.305 2018 1 1 6 2.066 2018 1 1 7 2.067 2018 1 1 8 2.068 2018 1 1 9 2.309 2018 1 1 10 2.30价格2019与尺寸(8760,5)年-月-日-小时价格0 2019 1 1 1 66.881 2019 1 1 2 66.882 2019 1 1 3 66.003 2019 1 1 4 63.644 2019 1 1 5 58.855 2019 1 1 6 55.476 2019 1 1 7 56.007 2019 1 1 8 61.098 2019 1 1 9 61.019 2019 1 1 10 61.00

可以使用pandas.cocat组合它们,只需

import pandas as pd

frames = [Price2018, Price2019]

df_merged = pd.concat(frames)

这导致DataFrame的大小为(17544,5)

如果一个人想清楚地了解发生了什么,它是这样工作的

(来源)


2.pandas.DataFrame.merge

在本节中,我们将考虑一个特定的情况:合并一个数据帧的索引和另一个数据框架的列。

假设有一个具有54列的数据帧Geo,是Date列之一,其类型为datetime64[ns]。

                 Date         1         2  ...        51        52        53
0 2010-01-01 00:00:00  0.565919  0.892376  ...  0.593049  0.775082  0.680621
1 2010-01-01 01:00:00  0.358960  0.531418  ...  0.734619  0.480450  0.926735
2 2010-01-01 02:00:00  0.531870  0.221768  ...  0.902369  0.027840  0.398864
3 2010-01-01 03:00:00  0.475463  0.245810  ...  0.306405  0.645762  0.541882
4 2010-01-01 04:00:00  0.954546  0.867960  ...  0.912257  0.039772  0.627696

以及数据帧Price,其中一列的价格名为Price,索引对应于日期(Date)

                     Price
Date                      
2010-01-01 00:00:00  29.10
2010-01-01 01:00:00   9.57
2010-01-01 02:00:00   0.00
2010-01-01 03:00:00   0.00
2010-01-01 04:00:00   0.00

为了合并它们,可以按如下方式使用pandas.DataFrame.merge

df_merged = pd.merge(Price, Geo, left_index=True, right_on='Date')

其中Geo和Price是先前的数据帧。

这将导致以下数据帧

   Price                Date         1  ...        51        52        53
0  29.10 2010-01-01 00:00:00  0.565919  ...  0.593049  0.775082  0.680621
1   9.57 2010-01-01 01:00:00  0.358960  ...  0.734619  0.480450  0.926735
2   0.00 2010-01-01 02:00:00  0.531870  ...  0.902369  0.027840  0.398864
3   0.00 2010-01-01 03:00:00  0.475463  ...  0.306405  0.645762  0.541882
4   0.00 2010-01-01 04:00:00  0.954546  ...  0.912257  0.039772  0.627696

本文将介绍以下主题:

如何正确地推广到多个DataFrame(以及为什么合并有缺点)在唯一密钥上合并在非唯一密钥上合并

回到顶部



推广到多个数据帧

通常,当多个数据帧要合并在一起时,就会出现这种情况。实际上,这可以通过链接合并调用来实现:

df1.merge(df2, ...).merge(df3, ...)

然而,对于许多DataFrame来说,这很快就失控了。此外,可能需要对未知数量的数据帧进行概括。

在这里,我介绍了pd.concat用于在唯一键上的多路连接,以及DataFrame.join用于在非唯一键上进行多路连接。首先,设置。

# Setup.
np.random.seed(0)
A = pd.DataFrame({'key': ['A', 'B', 'C', 'D'], 'valueA': np.random.randn(4)})    
B = pd.DataFrame({'key': ['B', 'D', 'E', 'F'], 'valueB': np.random.randn(4)})
C = pd.DataFrame({'key': ['D', 'E', 'J', 'C'], 'valueC': np.ones(4)})
dfs = [A, B, C] 

# Note: the "key" column values are unique, so the index is unique.
A2 = A.set_index('key')
B2 = B.set_index('key')
C2 = C.set_index('key')

dfs2 = [A2, B2, C2]

对唯一键进行多路合并

如果您的键(这里,键可以是列或索引)是唯一的,那么您可以使用pd.concat。请注意,pd.conct在索引上加入DataFrames。

# Merge on `key` column. You'll need to set the index before concatenating
pd.concat(
    [df.set_index('key') for df in dfs], axis=1, join='inner'
).reset_index()

  key    valueA    valueB  valueC
0   D  2.240893 -0.977278     1.0

# Merge on `key` index.
pd.concat(dfs2, axis=1, sort=False, join='inner')

       valueA    valueB  valueC
key                            
D    2.240893 -0.977278     1.0

对于完全外部联接,省略join='inner'。请注意,不能指定LEFT或RIGHT OUTER联接(如果需要,请使用下面描述的联接)。


对具有重复项的关键点进行多路合并

concat速度快,但也有缺点。它无法处理重复项。

A3 = pd.DataFrame({'key': ['A', 'B', 'C', 'D', 'D'], 'valueA': np.random.randn(5)})
pd.concat([df.set_index('key') for df in [A3, B, C]], axis=1, join='inner')
ValueError: Shape of passed values is (3, 4), indices imply (3, 2)

在这种情况下,我们可以使用join,因为它可以处理非唯一键(请注意,join在数据帧的索引上连接数据帧;除非另有规定,否则它在后台调用merge并执行LEFT OUTER join)。

# Join on `key` column. Set as the index first.
# For inner join. For left join, omit the "how" argument.
A.set_index('key').join([B2, C2], how='inner').reset_index()

  key    valueA    valueB  valueC
0   D  2.240893 -0.977278     1.0

# Join on `key` index.
A3.set_index('key').join([B2, C2], how='inner')

       valueA    valueB  valueC
key                            
D    1.454274 -0.977278     1.0
D    0.761038 -0.977278     1.0


继续阅读

跳转到Pandas Merging 101中的其他主题继续学习:

合并基础知识-连接的基本类型基于索引的联接推广到多个数据帧*交叉联接

*你在这里


本文将介绍以下主题:

不同条件下的索引合并基于索引的联接的选项:合并、联接、连接合并索引合并一个索引,另一个列有效使用命名索引简化合并语法

回到顶部



基于索引的联接

TL;博士

有几个选项,有些比其他选项更简单,具体取决于用途案例使用left_index和right_index的DataFrame.merge(或使用命名索引的left_on和right_on)支持内部/左侧/右侧/完整一次只能加入两个支持列列、索引列、索引索引联接DataFrame.join(加入索引)支持内部/左侧(默认)/右侧/完整一次可以连接多个DataFrame支持索引索引联接pd.concat(索引上的联接)支持内部/完整(默认)一次可以连接多个DataFrame支持索引索引联接


索引到索引联接

设置和基础知识

import pandas as pd
import numpy as np

np.random.seed([3, 14])
left = pd.DataFrame(data={'value': np.random.randn(4)}, 
                    index=['A', 'B', 'C', 'D'])    
right = pd.DataFrame(data={'value': np.random.randn(4)},  
                     index=['B', 'D', 'E', 'F'])
left.index.name = right.index.name = 'idxkey'

left
           value
idxkey          
A      -0.602923
B      -0.402655
C       0.302329
D      -0.524349

right
 
           value
idxkey          
B       0.543843
D       0.013135
E      -0.326498
F       1.385076

通常,索引上的内部联接如下所示:

left.merge(right, left_index=True, right_index=True)

         value_x   value_y
idxkey                    
B      -0.402655  0.543843
D      -0.524349  0.013135

其他联接遵循类似的语法。

值得注意的替代方案

DataFrame.join默认为索引上的联接。默认情况下,DataFrame.join执行LEFT OUTER join,因此how='inner'在这里是必要的。left.join(right,how='inner',lsuffix='_x',rsuffix='_y')值_x值_yidx键B-0.402655 0.543843D-0.524349 0.013135注意,我需要指定lsuffix和rsuffix参数,否则join将出错:left.join(右)ValueError:列重叠,但未指定后缀:Index(['value'],dtype='object')因为列名相同。如果它们的名称不同,这不会是一个问题。left.rename(columns={‘value‘:‘left value‘}).join(right,how=‘inner‘)左值idx键B-0.402655 0.543843D-0.524349 0.013135pd.concat在索引上连接,可以同时连接两个或多个DataFrame。默认情况下,它执行完全外部联接,因此此处需要how='inner'。。pd.contat([left,right],axis=1,sort=False,join='inner')价值,价值idx键B-0.402655 0.543843D-0.524349 0.013135有关concat的更多信息,请参阅本文。


索引到列连接

要使用左索引、右列索引执行内部联接,您将使用DataFrame.merge组合left_index=True和right_on=。。。。

right2 = right.reset_index().rename({'idxkey' : 'colkey'}, axis=1)
right2
 
  colkey     value
0      B  0.543843
1      D  0.013135
2      E -0.326498
3      F  1.385076

left.merge(right2, left_index=True, right_on='colkey')

    value_x colkey   value_y
0 -0.402655      B  0.543843
1 -0.524349      D  0.013135

其他连接遵循类似的结构。请注意,只有合并才能执行索引到列的连接。如果左侧的索引级别数等于右侧的列数,则可以连接多个列。

join和concat不能混合合并。您需要使用DataFrame.set_index将索引设置为预步骤。


有效使用命名索引[熊猫>=0.23]

如果索引已命名,则从panda>=0.23开始,DataFrame.merge允许您将索引名称指定为on(或根据需要指定left_on和right_on)。

left.merge(right, on='idxkey')

         value_x   value_y
idxkey                    
B      -0.402655  0.543843
D      -0.524349  0.013135

对于前面的合并索引为left、列为right的示例,可以使用索引名为left的left_on:

left.merge(right2, left_on='idxkey', right_on='colkey')

    value_x colkey   value_y
0 -0.402655      B  0.543843
1 -0.524349      D  0.013135


继续阅读

跳转到Pandas Merging 101中的其他主题继续学习:

合并基础知识-连接的基本类型基于索引的联接*推广到多个数据帧交叉联接

*你在这里


连接101

这些动画可能更能直观地解释您。信贷:Garrick Aden Buie tidyexplain repo

内部联接

外部联接或完全联接

右联接

左联接


Pandas目前不支持合并语法中的不等式连接;一个选项是使用pyjanator的conditional_join函数-我是这个库的贡献者:

# pip install pyjanitor
import pandas as pd
import janitor 

left.conditional_join(right, ('value', 'value', '>'))

   left           right
    key     value   key     value
0     A  1.764052     D -0.977278
1     A  1.764052     F -0.151357
2     A  1.764052     E  0.950088
3     B  0.400157     D -0.977278
4     B  0.400157     F -0.151357
5     C  0.978738     D -0.977278
6     C  0.978738     F -0.151357
7     C  0.978738     E  0.950088
8     D  2.240893     D -0.977278
9     D  2.240893     F -0.151357
10    D  2.240893     E  0.950088
11    D  2.240893     B  1.867558

left.conditional_join(right, ('value', 'value', '<'))

  left           right
   key     value   key     value
0    A  1.764052     B  1.867558
1    B  0.400157     E  0.950088
2    B  0.400157     B  1.867558
3    C  0.978738     B  1.867558

列作为元组的变量参数传递,每个元组由来自左侧数据帧的列、来自右侧数据帧的行和联接运算符组成,联接运算符可以是(>,<,>=,<=,!=)中的任何一个。在上面的示例中,由于列名重叠,返回了一个MultiIndex列。

就性能而言,这比天真的交叉连接要好:

np.random.seed(0)
dd = pd.DataFrame({'value':np.random.randint(100000, size=50_000)})
df = pd.DataFrame({'start':np.random.randint(100000, size=1_000), 
                   'end':np.random.randint(100000, size=1_000)})

dd.head()

   value
0  68268
1  43567
2  42613
3  45891
4  21243

df.head()

   start    end
0  71915  47005
1  64284  44913
2  13377  96626
3  75823  38673
4  29151    575


%%timeit
out = df.merge(dd, how='cross')
out.loc[(out.start < out.value) & (out.end > out.value)]
5.12 s ± 19 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit df.conditional_join(dd, ('start', 'value' ,'<'), ('end', 'value' ,'>'))
280 ms ± 5.56 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit df.conditional_join(dd, ('start', 'value' ,'<'), ('end', 'value' ,'>'), use_numba=True)
124 ms ± 12.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

out = df.merge(dd, how='cross')
out = out.loc[(out.start < out.value) & (out.end > out.value)]
A = df.conditional_join(dd, ('start', 'value' ,'<'), ('end', 'value' ,'>'))
columns = A.columns.tolist()
A = A.sort_values(columns, ignore_index = True)
out = out.sort_values(columns, ignore_index = True)

A.equals(out)
True

根据数据大小,当存在等连接时,可以获得更高的性能。在这种情况下,使用panda合并函数,但最终数据帧被延迟,直到计算出非等连接。当存在同等条件时,没有数字支持。让我们看看这里的数据:

import pandas as pd
import numpy as np
import random
import datetime

def random_dt_bw(start_date,end_date):
    days_between = (end_date - start_date).days
    random_num_days = random.randrange(days_between)
    random_dt = start_date + datetime.timedelta(days=random_num_days)
    return random_dt

def generate_data(n=1000):
    items = [f"i_{x}" for x in range(n)]
    start_dates = [random_dt_bw(datetime.date(2020,1,1),datetime.date(2020,9,1)) for x in range(n)]
    end_dates = [x + datetime.timedelta(days=random.randint(1,10)) for x in start_dates]
    
    offerDf = pd.DataFrame({"Item":items,
                            "StartDt":start_dates,
                            "EndDt":end_dates})
    
    transaction_items = [f"i_{random.randint(0,n)}" for x in range(5*n)]
    transaction_dt = [random_dt_bw(datetime.date(2020,1,1),datetime.date(2020,9,1)) for x in range(5*n)]
    sales_amt = [random.randint(0,1000) for x in range(5*n)]
    
    transactionDf = pd.DataFrame({"Item":transaction_items,"TransactionDt":transaction_dt,"Sales":sales_amt})

    return offerDf,transactionDf

offerDf,transactionDf = generate_data(n=100000)


offerDf = (offerDf
           .assign(StartDt = offerDf.StartDt.astype(np.datetime64), 
                   EndDt = offerDf.EndDt.astype(np.datetime64)
                  )
           )

transactionDf = transactionDf.assign(TransactionDt = transactionDf.TransactionDt.astype(np.datetime64))

# you can get more performance when using ints/datetimes
# in the equi join, compared to strings

offerDf = offerDf.assign(Itemr = offerDf.Item.str[2:].astype(int))

transactionDf = transactionDf.assign(Itemr = transactionDf.Item.str[2:].astype(int))

transactionDf.head()
      Item TransactionDt  Sales  Itemr
0  i_43407    2020-05-29    692  43407
1  i_95044    2020-07-22    964  95044
2  i_94560    2020-01-09    462  94560
3  i_11246    2020-02-26    690  11246
4  i_55974    2020-03-07    219  55974

offerDf.head()
  Item    StartDt      EndDt  Itemr
0  i_0 2020-04-18 2020-04-19      0
1  i_1 2020-02-28 2020-03-07      1
2  i_2 2020-03-28 2020-03-30      2
3  i_3 2020-08-03 2020-08-13      3
4  i_4 2020-05-26 2020-06-04      4

# merge on strings 
merged_df = pd.merge(offerDf,transactionDf,on='Itemr')
classic_int = merged_df[(merged_df['TransactionDt']>=merged_df['StartDt']) &
                        (merged_df['TransactionDt']<=merged_df['EndDt'])]

# merge on ints ... usually faster
merged_df = pd.merge(offerDf,transactionDf,on='Item')
classic_str = merged_df[(merged_df['TransactionDt']>=merged_df['StartDt']) &            
                        (merged_df['TransactionDt']<=merged_df['EndDt'])]

# merge on integers
cond_join_int = (transactionDf
                 .conditional_join(
                     offerDf, 
                     ('Itemr', 'Itemr', '=='), 
                     ('TransactionDt', 'StartDt', '>='), 
                     ('TransactionDt', 'EndDt', '<=')
                  )
                 )

# merge on strings
cond_join_str = (transactionDf
                 .conditional_join(
                     offerDf, 
                     ('Item', 'Item', '=='), 
                     ('TransactionDt', 'StartDt', '>='), 
                     ('TransactionDt', 'EndDt', '<=')
                  )
                )

%%timeit
merged_df = pd.merge(offerDf,transactionDf,on='Item')
classic_str = merged_df[(merged_df['TransactionDt']>=merged_df['StartDt']) &
                        (merged_df['TransactionDt']<=merged_df['EndDt'])]
292 ms ± 3.84 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit
merged_df = pd.merge(offerDf,transactionDf,on='Itemr')
classic_int = merged_df[(merged_df['TransactionDt']>=merged_df['StartDt']) &
                        (merged_df['TransactionDt']<=merged_df['EndDt'])]
253 ms ± 2.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit 
(transactionDf
.conditional_join(
    offerDf, 
    ('Item', 'Item', '=='), 
    ('TransactionDt', 'StartDt', '>='), 
    ('TransactionDt', 'EndDt', '<=')
   )
)
256 ms ± 9.66 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit 
(transactionDf
.conditional_join(
    offerDf, 
    ('Itemr', 'Itemr', '=='), 
    ('TransactionDt', 'StartDt', '>='), 
    ('TransactionDt', 'EndDt', '<=')
   )
)
71.8 ms ± 2.24 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

# check that both dataframes are equal
cols = ['Item', 'TransactionDt', 'Sales', 'Itemr_y','StartDt', 'EndDt', 'Itemr_x']
cond_join_str = cond_join_str.drop(columns=('right', 'Item')).set_axis(cols, axis=1)

(cond_join_str
.sort_values(cond_join_str.columns.tolist())
.reset_index(drop=True)
.reindex(columns=classic_str.columns)
.equals(
    classic_str
    .sort_values(classic_str.columns.tolist())
    .reset_index(drop=True)
))

True