Pandas按范围

时间:2016-08-28 19:55:24

标签: python-3.x pandas merge left-join ip-address

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

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

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

此单个IP 属于df2的"ip_range_start""ip_range_end"字段指定的范围之一。我不知道如何实现这一点,因为正常的合并/连接显然不会起作用,因为df1和df2之间没有匹配的值。

我的问题似乎与此问题非常相似,但又不同以保证单独的问题:Pandas: how to merge two dataframes on offset dates?

2 个答案:

答案 0 :(得分:3)

假设您有以下数据框:

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间隔的索引:< / p>

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倍

答案 1 :(得分:0)

如果您愿意使用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