【问题标题】:Best way to join / merge by range in pandas在熊猫中按范围加入/合并的最佳方式
【发布时间】:2017-11-06 03:38:55
【问题描述】:

我经常使用 pandas 通过范围条件进行合并(连接)。

例如,如果有 2 个数据框:

A(A_id,A_value)

B(B_id、B_low、B_high、B_name)

它们很大且大小大致相同(假设每个记录 2M 条)。

我想在 A 和 B 之间进行内部连接,因此 A_value 将在 B_low 和 B_high 之间。

使用 SQL 语法:

SELECT *
FROM A,B
WHERE A_value between B_low and B_high

这将非常简单、简短且高效。

同时,在 pandas 中,唯一的方法(我发现没有使用循环)是在两个表中创建一个虚拟列,加入它(相当于交叉加入),然后过滤掉不需要的行。这听起来沉重而复杂:

A['dummy'] = 1
B['dummy'] = 1
Temp = pd.merge(A,B,on='dummy')
Result = Temp[Temp.A_value.between(Temp.B_low,Temp.B_high)]

我的另一个解决方案是通过使用B[(x>=B.B_low) & (x<=B.B_high)] 掩码对 B 上的每个 A 值应用搜索功能,但这听起来效率也很低,可能需要优化索引。

是否有更优雅和/或更有效的方式来执行此操作?

【问题讨论】:

  • This Q&A 可能是相关的。
  • 看起来他们使用的方法与我自己建议的方法相似(虚拟列、笛卡尔积和掩码过滤器)。令人惊讶的是没有内置解决方案。
  • 您是否还查看了接受的答案...?永远不要从 Stack Overflow 上的问题中学习。虽然我可能没有意识到答案是一样的,但在这种情况下很抱歉:)
  • 看起来像是一个更优化内存的解决方案,但 - 一点也不优雅:(谢谢!
  • 嗯,是的,merge 不接受任何函数参数这一事实表明,可能无法提供优雅的解决方案。但我几乎不了解熊猫,所以让我们希望专家出现并证明相反的情况:)

标签: python pandas numpy join


【解决方案1】:

设置
考虑数据框AB

A = pd.DataFrame(dict(
        A_id=range(10),
        A_value=range(5, 105, 10)
    ))
B = pd.DataFrame(dict(
        B_id=range(5),
        B_low=[0, 30, 30, 46, 84],
        B_high=[10, 40, 50, 54, 84]
    ))

A

   A_id  A_value
0     0        5
1     1       15
2     2       25
3     3       35
4     4       45
5     5       55
6     6       65
7     7       75
8     8       85
9     9       95

B

   B_high  B_id  B_low
0      10     0      0
1      40     1     30
2      50     2     30
3      54     3     46
4      84     4     84

numpy
✌最简单的✌方式是使用numpy广播。
我们查找A_value 大于或等于B_low 的每个实例,同时A_value 小于或等于B_high

a = A.A_value.values
bh = B.B_high.values
bl = B.B_low.values

i, j = np.where((a[:, None] >= bl) & (a[:, None] <= bh))

pd.DataFrame(
    np.column_stack([A.values[i], B.values[j]]),
    columns=A.columns.append(B.columns)
)

   A_id  A_value  B_high  B_id  B_low
0     0        5      10     0      0
1     3       35      40     1     30
2     3       35      50     2     30
3     4       45      50     2     30

为了解决 cmets 并提供类似于左连接的内容,我附加了 A 中不匹配的部分。

pd.DataFrame(
    np.column_stack([A.values[i], B.values[j]]),
    columns=A.columns.append(B.columns)
).append(
    A[~np.in1d(np.arange(len(A)), np.unique(i))],
    ignore_index=True, sort=False
)

    A_id  A_value  B_id  B_low  B_high
0      0        5   0.0    0.0    10.0
1      3       35   1.0   30.0    40.0
2      3       35   2.0   30.0    50.0
3      4       45   2.0   30.0    50.0
4      1       15   NaN    NaN     NaN
5      2       25   NaN    NaN     NaN
6      5       55   NaN    NaN     NaN
7      6       65   NaN    NaN     NaN
8      7       75   NaN    NaN     NaN
9      8       85   NaN    NaN     NaN
10     9       95   NaN    NaN     NaN

【讨论】:

  • 惊人的解决方案.. 我们可以说这是一个交叉连接...如果我只想保留A 的所有行(基本上在A 上留下连接)那么我会有什么改变需要制作吗?
  • 我也想减少发生的行突发。有什么想法吗?
  • 这太好了,我和 Joel 有同样的问题,是否可以保留表 A 的所有值,比如左连接?
  • @piRSquared 如何只保留 A_id == B_id 的行?我们可以事后做,但我认为这不是最有效的。在我的情况下,我有一个 79k 行的原始 df,它在您的操作之后到 2.3m 行,然后当我只保留 A_id == B_id 的行时,我有 74k 行,这是我所期望的。这不能一次完成吗?
  • 对于具有可能行和列的非常大的数据集,我得到了这个答案的内存溢出。 =/
【解决方案2】:

不确定这是否更有效,但是您可以直接使用 sql(例如来自模块 sqlite3)和 pandas(灵感来自 this question),例如:

conn = sqlite3.connect(":memory:") 
df2 = pd.DataFrame(np.random.randn(10, 5), columns=["col1", "col2", "col3", "col4", "col5"])
df1 = pd.DataFrame(np.random.randn(10, 5), columns=["col1", "col2", "col3", "col4", "col5"])
df1.to_sql("df1", conn, index=False)
df2.to_sql("df2", conn, index=False)
qry = "SELECT * FROM df1, df2 WHERE df1.col1 > 0 and df1.col1<0.5"
tt = pd.read_sql_query(qry,conn)

您可以根据应用程序的需要调整查询

【讨论】:

    【解决方案3】:

    我不知道它的效率如何,但是有人编写了一个包装器,允许您将 SQL 语法与 pandas 对象一起使用。这称为pandasql。文档明确声明支持连接。这可能至少更容易阅读,因为 SQL 语法非常易读。

    【讨论】:

      【解决方案4】:

      来自pyjanitorconditional_join可能有助于抽象/方便;:

      # pip install pyjanitor
      import pandas as pd
      import janitor
      

      内联

      A.conditional_join(B, 
                         ('A_value', 'B_low', '>='), 
                         ('A_value', 'B_high', '<=')
                        )
      
         A_id  A_value  B_id  B_low  B_high
      0     0        5     0      0      10
      1     3       35     1     30      40
      2     3       35     2     30      50
      3     4       45     2     30      50
      

      左加入

      A.conditional_join(
             B, 
             ('A_value', 'B_low', '>='), 
             ('A_value', 'B_high', '<='), 
             how = 'left'
          )
      
          A_id  A_value  B_id  B_low  B_high
      0      0        5   0.0    0.0    10.0
      1      1       15   NaN    NaN     NaN
      2      2       25   NaN    NaN     NaN
      3      3       35   1.0   30.0    40.0
      4      3       35   2.0   30.0    50.0
      5      4       45   2.0   30.0    50.0
      6      5       55   NaN    NaN     NaN
      7      6       65   NaN    NaN     NaN
      8      7       75   NaN    NaN     NaN
      9      8       85   NaN    NaN     NaN
      10     9       95   NaN    NaN     NaN
      

      【讨论】:

      • 我收到以下错误:Cannot interpret '&lt;PandasArray&gt; [ 5, 15, 25, 35, 35, 35, 45, 45, 45, 55, 55, 55, 55, 65, 65, 65, 65, 75, 75, 75, 75] Length: 21, dtype: int64' as a data type 有什么想法吗?
      • 嗨@Quixotic22,pyjanitor 的新版本最近发布了;你不应该再有那个错误了。如果遇到任何问题,可以在 GitHub 页面上留下问题。
      【解决方案5】:

      举个简单的例子:

      df=pd.DataFrame([2,3,4,5,6],columns=['A'])
      

      返回

          A
      0   2
      1   3
      2   4
      3   5
      4   6
      

      现在让我们定义第二个数据框

      df2=pd.DataFrame([1,6,2,3,5],columns=['B_low'])
      df2['B_high']=[2,8,4,6,6]
      

      结果

          B_low   B_high
      0   1       2
      1   6       8
      2   2       4
      3   3       6
      4   5       6
      

      我们来了;我们希望输出是索引 3 和 A 值 5

      df.where(df['A']>=df2['B_low']).where(df['A']<df2['B_high']).dropna()
      

      结果

          A
      3   5.0
      

      【讨论】:

      • 那不是连接,只是堆叠
      【解决方案6】:

      我知道这是一个老问题,但对于新手来说,现在有 pandas.merge_asof 函数可以根据最接近的匹配执行连接。

      如果您想进行合并,以便一个 DataFrame (df_right) 的一列介于另一个 DataFrame (df_left) 的两列之间,您可以执行以下操作:

      df_left = pd.DataFrame({
          "time_from": [1, 4, 10, 21],
          "time_to": [3, 7, 15, 27]
      })
      
      df_right = pd.DataFrame({
          "time": [2, 6, 16, 25]
      })
      
      df_left
      
        time_from time_to
      0        1       3
      1        4       7
      2       10      15
      3       21      27
      
      
      df_right
      
        time
      0    2
      1    6
      2   16
      3   25
      

      首先,找到与左侧 DataFrame 的左边界 (time_from) 最接近但最大的右侧 DataFrame 的匹配项:

      merged = pd.merge_asof(
          left=df_1,
          right=df_2.rename(columns={"time": "candidate_match_1"}),
          left_on="time_from",
          right_on="candidate_match_1",
          direction="forward"
      )
      
      merged
      
        time_from time_to candidate_match_1
      0        1       3                 2
      1        4       7                 6
      2        10      15               16
      3        21      27               25
      

      如您所见,索引 2 中的候选匹配是错误匹配的,因为 16 不在 10 和 15 之间。

      然后,找到最接近但小于左侧 DataFrame 的右边界 (time_to) 的右侧 DataFrame 的匹配项:

      merged = pd.merge_asof(
          left=merged,
          right=df_2.rename(columns={"time": "candidate_match_2"}),
          left_on="time_to",
          right_on="candidate_match_2",
          direction="backward"
      )
      
      merged
      
        time_from time_to candidate_match_1   candidate_match_2
      0        1        3                2                   2
      1        4        7                6                   6
      2        10      15               16                   6
      3        21      27               25                  25
      

      最后,保持候选匹配相同的匹配,即右侧DataFrame的值在左侧DataFrame的2列值之间:

      merged["match"] = None
      merged.loc[merged["candidate_match_1"] == merged["candidate_match_2"], "match"] = \
          merged.loc[merged["candidate_match_1"] == merged["candidate_match_2"], "candidate_match_1"]
      
      merged
      
        time_from time_to candidate_match_1   candidate_match_2   match
      0        1       3                 2                   2       2
      1        4       7                 6                   6       6
      2        10     15                16                   6    None
      3        21     27                25                   25     25
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-06-26
        • 2018-02-21
        • 1970-01-01
        • 2017-09-04
        • 2012-04-04
        相关资源
        最近更新 更多