Pandas 按范围合并 ip-address 上的数据帧

Pandas merge dataframes on ip-address by range

我有两个数据帧,其中包含一些我想合并的 ip 信息(相当于 sql 中的左连接)。数据框具有以下字段:

df1: ["company","ip","actions"]  
df2: ["ip_range_start","ip_range_end","country","state","city"]

结果数据框应具有 headers:["company","ip","actions","country","state","city"]。这里的问题是我的合并标准。 df1 包含一个 ip,我想用它从 df2 中提取国家、州和城市信息。

这个单个ip 落入df2的"ip_range_start""ip_range_end"字段指定的范围之一。我不确定如何完成此操作,因为正常 merge/join 显然不会成功,因为 df1 和 df2 之间没有匹配值。

我的问题似乎与这个问题非常相似,但差异足以保证单独提出一个问题:Pandas: how to merge two dataframes on offset dates?

假设您有以下数据框:

In [5]: df1
Out[5]:
  company           ip actions
0   comp1    10.10.1.2    act1
1   comp2   10.10.2.20    act2
2   comp3   10.10.3.50    act3
3   comp4  10.10.4.100    act4

In [6]: df2
Out[6]:
  ip_range_start ip_range_end   country   state   city
0      10.10.2.1  10.10.2.254  country2  state2  city2
1      10.10.3.1  10.10.3.254  country3  state3  city3
2      10.10.4.1  10.10.4.254  country4  state4  city4

我们可以创建一个向量化函数,它将计算类似于 int(netaddr.IPAddress('192.0.2.1')):

的数字 IP 表示
def ip_to_int(ip_ser):
    ips = ip_ser.str.split('.', expand=True).astype(np.int16).values
    mults = np.tile(np.array([24, 16, 8, 0]), len(ip_ser)).reshape(ips.shape)
    return np.sum(np.left_shift(ips, mults), axis=1)

让我们将所有 IP 转换为它们的数字表示:

df1['_ip'] = ip_to_int(df1.ip)
df2[['_ip_range_start','_ip_range_end']] = df2.filter(like='ip_range').apply(lambda x: ip_to_int(x))

In [10]: df1
Out[10]:
  company           ip actions        _ip
0   comp1    10.10.1.2    act1  168427778
1   comp2   10.10.2.20    act2  168428052
2   comp3   10.10.3.50    act3  168428338
3   comp4  10.10.4.100    act4  168428644

In [11]: df2
Out[11]:
  ip_range_start ip_range_end   country   state   city  _ip_range_start  _ip_range_end
0      10.10.2.1  10.10.2.254  country2  state2  city2        168428033      168428286
1      10.10.3.1  10.10.3.254  country3  state3  city3        168428289      168428542
2      10.10.4.1  10.10.4.254  country4  state4  city4        168428545      168428798

现在让我们向 df1 DF 添加一个新列,它将包含来自 df2 DF 的第一个 匹配 IP 间隔的索引:

In [12]: df1['x'] = (df1._ip.apply(lambda x: df2.query('_ip_range_start <= @x <= _ip_range_end')
   ....:                                       .index
   ....:                                       .values)
   ....:                   .apply(lambda x: x[0] if len(x) else -1))

In [14]: df1
Out[14]:
  company           ip actions        _ip  x
0   comp1    10.10.1.2    act1  168427778 -1
1   comp2   10.10.2.20    act2  168428052  0
2   comp3   10.10.3.50    act3  168428338  1
3   comp4  10.10.4.100    act4  168428644  2

终于可以合并两个DF了:

In [15]: (pd.merge(df1.drop('_ip',1),
   ....:           df2.filter(regex=r'^((?!.?ip_range_).*)$'),
   ....:           left_on='x',
   ....:           right_index=True,
   ....:           how='left')
   ....:    .drop('x',1)
   ....: )
Out[15]:
  company           ip actions   country   state   city
0   comp1    10.10.1.2    act1       NaN     NaN    NaN
1   comp2   10.10.2.20    act2  country2  state2  city2
2   comp3   10.10.3.50    act3  country3  state3  city3
3   comp4  10.10.4.100    act4  country4  state4  city4

让我们将标准 int(IPAddress) 的速度与我们的函数进行比较(我们将使用 4M 行 DF 进行比较):

In [21]: big = pd.concat([df1.ip] * 10**6, ignore_index=True)

In [22]: big.shape
Out[22]: (4000000,)

In [23]: big.head(10)
Out[23]:
0      10.10.1.2
1     10.10.2.20
2     10.10.3.50
3    10.10.4.100
4      10.10.1.2
5     10.10.2.20
6     10.10.3.50
7    10.10.4.100
8      10.10.1.2
9     10.10.2.20
Name: ip, dtype: object

In [24]: %timeit
%timeit  %%timeit

In [24]: %timeit big.apply(lambda x: int(IPAddress(x)))
1 loop, best of 3: 1min 3s per loop

In [25]: %timeit ip_to_int(big)
1 loop, best of 3: 25.4 s per loop

结论:我们的函数大约是。快 2.5 倍

如果你愿意使用 R 而不是 Python,我写了一个 ipaddress 包可以解决这个问题。

使用来自 MaxU 答案的相同数据:

library(tidyverse)
library(ipaddress)
library(fuzzyjoin)

addr <- tibble(
  company = c("comp1", "comp2", "comp3", "comp4"),
  ip = ip_address(c("10.10.1.2", "10.10.2.20", "10.10.3.50", "10.10.4.100")),
  actions = c("act1", "act2", "act3", "act4")
)
nets <- tibble(
  ip_range_start = ip_address(c("10.10.2.1", "10.10.3.1", "10.10.4.1")),
  ip_range_end = ip_address(c("10.10.2.254", "10.10.3.254", "10.10.4.254")),
  country = c("country2", "country3", "country4"),
  state = c("state2", "state3", "state4"),
  city = c("city2", "city3", "city4")
)

nets <- nets %>%
  mutate(network = common_network(ip_range_start, ip_range_end)) %>%
  select(network, country, state, city)

fuzzy_left_join(addr, nets, c("ip" = "network"), is_within)
#> # A tibble: 4 x 7
#>   company          ip actions      network country  state  city 
#>   <chr>     <ip_addr> <chr>     <ip_netwk> <chr>    <chr>  <chr>
#> 1 comp1     10.10.1.2 act1              NA <NA>     <NA>   <NA> 
#> 2 comp2    10.10.2.20 act2    10.10.2.0/24 country2 state2 city2
#> 3 comp3    10.10.3.50 act3    10.10.3.0/24 country3 state3 city3
#> 4 comp4   10.10.4.100 act4    10.10.4.0/24 country4 state4 city4

使用 400 万个地址的相同基准,网络成员检查在 3.25 秒内完成。

big <- tibble(ip = rep(addr$ip, 1e6))
big
#> # A tibble: 4,000,000 x 1
#>             ip
#>      <ip_addr>
#>  1   10.10.1.2
#>  2  10.10.2.20
#>  3  10.10.3.50
#>  4 10.10.4.100
#>  5   10.10.1.2
#>  6  10.10.2.20
#>  7  10.10.3.50
#>  8 10.10.4.100
#>  9   10.10.1.2
#> 10  10.10.2.20
#> # … with 3,999,990 more rows
bench::mark(fuzzy_left_join(big, nets, c("ip" = "network"), is_within))$median
#> [1] 3.25s