使用熊猫merge_asof()识别范围关系

时间:2020-04-13 01:03:58

标签: python pandas dataframe

给出以下两个表示范围的数据框:

df1 =

  start   end
0   200   300
1   600   900
2   950  1050

df2 =

  start   end
0   350   550
1   650   800
2   900  1100

它们可以这样表示:

df1  [200 300]            [600    900] [950 1050]
df2            [350  550]   [650 800] [900   1100]

我的任务是确定df1df2范围之间的四种不同类型的关系:

    df2
  1. df1子集
      df2 [650 800]
    • df1 [600 900]子集
  2. df2
  3. df1超集
    • df2 [900 1100]的{​​{1}}超集
  4. df1 [950 1050]之后的
  5. df2(最近的邻居,不包括子集/超集)
    • df1df2 [350 550]之后
    • df1 [200 300]df2 [900 1100]之后
  6. df1 [600 900]df2之前(最近的邻居,不包括子集/超集)
    • df1之前df2 [350 550]
    • df1 [600 900]之前df2 [650 800]

我正在尝试使用从this answer中学到的df1 [950 1050],但是由于超集/子集关系增加了复杂性,因此它无法正常工作,例如:

merge_asof()

输出:

# Create "before" condition
df_before = pd.merge_asof(
    df2.rename(columns={col:f'before_{col}' for col in df2.columns}).sort_values('before_end'),
    df1.assign(before_end=lambda x: x['end']).sort_values('before_end'),
    on='before_end',
    direction='forward'
).query('end > before_end')

print(df_before)

目标输出:

  before_start  before_end  start    end
0          350         550  600.0  900.0
1          650         800  600.0  900.0

问题是

  before_start  before_end  start     end
0          350         550  600.0   900.0
1          650         800  950.0  1050.0

pd.merge_asof( df2.rename(columns={col:f'before_{col}' for col in df2.columns}).sort_values('before_end'), df1.assign(before_end=lambda x: x['end']).sort_values('before_end'), on='before_end', direction='forward' ) 中找到800之后最接近的df1.end,即df2 [650 800]

df1 [600    900]

是否有可能根据特定条件进行 before_start before_end start end 0 350 550 600.0 900.0 1 650 800 600.0 900.0 2 900 1100 NaN NaN 查找最近的值,例如,只有在该范围内的merge_asof()大于800时才“找到最近的df1.end( 950)”?如此复杂,也许还有另一个更适合此任务的功能?

注意:

  • df1.start中的范围可以相互重叠,但绝不能完全相同。
  • df1中的范围可以相互重叠,但绝不能完全相同。
  • df2df1分别有20万多行。
  • df2df1的行数不同。
  • 关系是相对于df2的,因此df1中的每一行只需要一个匹配项,每行最多有四个关系。鉴于以上提供的数据,合并回df1后,最终输出将如下所示:

df1 =

df1

1 个答案:

答案 0 :(得分:1)

可以使用pd.merge_asof查找之前和之后的选项。

before_df = pd.merge_asof(df1, df2, left_on='start', right_on='end', suffixes=['', '_before'])
before_df
#    start   end  start_before  end_before
# 0    200   300           NaN         NaN
# 1    600   900         350.0       550.0
# 2    950  1050         650.0       800.0

after_df = pd.merge_asof(df2, df1, left_on='start', right_on='end', suffixes=['_after', ''])
#    start_after  end_after  start  end
# 0          350        550    200  300
# 1          650        800    200  300
# 2          900       1100    600  900

但是要使其工作或进行子集和超集计算并不容易。对于那些人,我会尝试一种可以一次通过的算法。

def range_intersect(lh_ranges, rh_ranges): 
    all_ranges = sorted(
        [(b, e, 'lh') for b, e in lh_ranges] +
        [(b, e, 'rh') for b, e in rh_ranges]
    ) 

    res = [] 
    max_b, max_e = None, None 
    for b, e, which in all_ranges: 
        if which == 'rh': 
            if max_e is None or e > max_e: 
                max_b, max_e = b, e 
        elif max_e is not None and e <= max_e: 
            res.append((b, e, max_b, max_e)) 

    return res

这将找到lh中元素的子集rh中的元素。要查找超集,可以反向运行它。为简单起见,它使用范围列表代替DataFrame。转换很简单。

lh = df1.to_dict('split')['data']
rh = df2.to_dict('split')['data']

lh
# [[200, 300], [600, 900], [950, 1050]]

rh                                                                                                                                                                                                                                  
# [[350, 550], [650, 800], [900, 1100]]

在那之后,您想要的结果DataFrame仅几处合并。

# Compute supersets, then run in reverse to get the subsets.
superset_df = pd.DataFrame(range_intersect(lh, rh), columns=['start', 'end', 'start_superset', 'end_superset'])
subset_df = pd.DataFrame(range_intersect(rh, lh), columns=['start_subset', 'end_subset', 'start', 'end'])

# Merge all the results together.
result = df1.merge(subset_df, how='left').merge(superset_df, how='left').merge(before_df, how='left').merge(after_df, how='left')

# The reversed operations, after and subset, can have many matches in df1.
result.drop_duplicates(['start', 'end'])
#    start   end  start_subset  end_subset  start_superset  end_superset  start_before  end_before  start_after  end_after
# 0    200   300           NaN         NaN             NaN           NaN           NaN         NaN        350.0      550.0
# 2    600   900         650.0       800.0             NaN           NaN         350.0       550.0        900.0     1100.0
# 3    950  1050           NaN         NaN           900.0        1100.0         650.0       800.0          NaN        NaN