Панды объединяют фреймы данных по IP-адресу по диапазону

У меня есть два фрейма данных, содержащие некоторую информацию об 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 попадет в один из диапазонов, указанных в полях "ip_range_start" и "ip_range_end" df2. Я не уверен, как это сделать, поскольку обычное слияние / соединение явно не поможет, поскольку между df1 и df2 нет совпадающих значений.

Мой вопрос кажется очень похожим на этот, но достаточно другим, чтобы оправдать отдельный вопрос: Панды: как объединить два фрейма данных по смещенным датам?


person sastrup    schedule 28.08.2016    source источник
comment
Хотите поделиться некоторыми данными?   -  person Abdou    schedule 28.08.2016
comment
Вы можете разделить иерархические номера IP на отдельные поля. Это может упростить решение.   -  person Mini Fridge    schedule 29.08.2016


Ответы (2)


Предположим, у вас есть следующие фреймы данных:

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

мы можем создать векторизованную функцию, которая будет вычислять числовое представление IP, подобное int (netaddr.IPAddress (' 192.0.2.1 ')):

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, который будет содержать индекс первого совпадающего IP-интервала из df2 DF:

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) с нашей функцией (для сравнения мы будем использовать DF 4M строк):

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 раза быстрее

person MaxU    schedule 29.08.2016

Если вы хотите использовать 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

Используя тот же эталонный показатель в 4 миллиона адресов, проверка членства в сети выполняется за 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
person David Hall    schedule 03.09.2020