Pandas: самый быстрый способ преобразовать IP в страну

#python #pandas #dataframe #geo

#python #панды #фрейм данных #гео #pandas #география

Вопрос:

У меня есть функция find_country_from_connection_ip , которая принимает IP-адрес и после некоторой обработки возвращает страну. Как показано ниже:

 def find_country_from_connection_ip(ip):
    # Do some processing
    return county
  

Я использую функцию внутри apply метода. как показано ниже:

 df['Country'] = df.apply(lambda x: find_country_from_ip(x['IP']), axis=1)
  

Поскольку это довольно просто, я хочу оценить новый столбец из существующего столбца в DataFrame, который содержит >400000 строки.

Он запускается, но ужасно медленно и выдает исключение, подобное приведенному ниже:

………..: SettingWithCopyWarning: значение пытается быть установлено на копии фрагмента из фрейма данных. Попробуйте вместо этого использовать .loc[row_indexer,col_indexer] = value

Смотрите предостережения в документации: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

если name == ‘main‘: В [38]:

Я понимаю проблему, но не совсем понимаю, как использовать loc with apply и lambda .

Примечание. Пожалуйста, подскажите, есть ли у вас более эффективное альтернативное решение, которое может принести конечный результат.

**** РЕДАКТИРОВАТЬ ********

Функция в основном представляет собой поиск в mmdb базе данных, как показано ниже:

 def find_country_from_ip(ip):
    result = subprocess.Popen("mmdblookup --file GeoIP2-Country.mmdb --ip {} country names en".format(ip).split(" "), stdout=subprocess.PIPE).stdout.read()
    if result:
        return re.search(r'"(. ?)"', result).group(1) 
    else:
        final_output = subprocess.Popen("mmdblookup --file GeoIP2-Country.mmdb --ip {} registered_country names en".format(ip).split(" "), stdout=subprocess.PIPE).stdout.read()
        return re.search(r'"(. ?)"', final_output).group(1)
  

Тем не менее, это дорогостоящая операция, и когда у вас есть фрейм данных с >400000 строками, это должно занять время. Но сколько? Вот в чем вопрос. Это занимает около 2 часов, что, по-моему, довольно много.

Комментарии:

1. Я думаю, что более эффективным решением может быть опустить apply и переписать пользовательскую функцию в некоторую векторизованную функцию pandas, если это возможно.

2. Итак, можете ли вы добавить функцию question all к question all find_country_from_ip ?

3. @jezrael, отредактировано. Вы можете взглянуть сейчас.

4. Хммм, это сложно, возможно, я даю вам только некоторое предложение. subprocess.Popen("mmdblookup --file GeoIP2-Country.mmdb --ip {} country names en".format(ip).split(" "), stdout=subprocess.PIPE).stdout.read() должен вызываться для каждой строки? Или вам нужно вызывать его для каждого уникального IP ? Есть ли дубликаты IP или нет?

5. Что такое print (len(df.IP.drop_duplicates())) ?

Ответ №1:

Я бы использовал для этого maxminddb-geolite2 модуль (GeoLite).

Сначала установите maxminddb-geolite2 модуль

 pip install maxminddb-geolite2
  

Код Python:

 import pandas as pd
from geolite2 import geolite2

def get_country(ip):
    try:
        x = geo.get(ip)
    except ValueError:
        return pd.np.nan
    try:
        return x['country']['names']['en'] if x else pd.np.nan
    except KeyError:
        return pd.np.nan

geo = geolite2.reader()

# it took me quite some time to find a free and large enough list of IPs ;)
# IP's for testing: http://upd.emule-security.org/ipfilter.zip
x = pd.read_csv(r'D:downloadipfilter.zip',
                usecols=[0], sep='s*-s*',
                header=None, names=['ip'])

# get unique IPs
unique_ips = x['ip'].unique()
# make series out of it
unique_ips = pd.Series(unique_ips, index = unique_ips)
# map IP --> country
x['country'] = x['ip'].map(unique_ips.apply(get_country))

geolite2.close()
  

Вывод:

 In [90]: x
Out[90]:
                     ip     country
0       000.000.000.000         NaN
1       001.002.004.000         NaN
2       001.002.008.000         NaN
3       001.009.096.105         NaN
4       001.009.102.251         NaN
5       001.009.106.186         NaN
6       001.016.000.000         NaN
7       001.055.241.140         NaN
8       001.093.021.147         NaN
9       001.179.136.040         NaN
10      001.179.138.224    Thailand
11      001.179.140.200    Thailand
12      001.179.146.052         NaN
13      001.179.147.002    Thailand
14      001.179.153.216    Thailand
15      001.179.164.124    Thailand
16      001.179.167.188    Thailand
17      001.186.188.000         NaN
18      001.202.096.052         NaN
19      001.204.179.141       China
20      002.051.000.165         NaN
21      002.056.000.000         NaN
22      002.095.041.202         NaN
23      002.135.237.106  Kazakhstan
24      002.135.237.250  Kazakhstan
...                 ...         ...
  

Время: для 171.884 уникальных IP-адресов:

 In [85]: %timeit unique_ips.apply(get_country)
1 loop, best of 3: 14.8 s per loop

In [86]: unique_ips.shape
Out[86]: (171884,)
  

Вывод: это займет ок. 35 секунд для вашего DF с 400 тысячами уникальных IP-адресов на моем оборудовании:

 In [93]: 400000/171884*15
Out[93]: 34.90726303786274
  

Комментарии:

1. Отличный… именно то решение, которое я ищу.

2. @AhsanulHaque, рад, что смог помочь 🙂

Ответ №2:

Ваша проблема не в том, как использовать apply или loc . Проблема в том, что ваш df помечен как копия другого фрейма данных.

Давайте немного изучим это

 df = pd.DataFrame(dict(IP=[1, 2, 3], A=list('xyz')))
df
  

введите описание изображения здесь

 def find_country_from_connection_ip(ip):
    return {1: 'A', 2: 'B', 3: 'C'}[ip]

df['Country'] = df.IP.apply(find_country_from_connection_ip)
df
  

введите описание изображения здесь

Никаких проблем
Давайте создадим несколько проблем

 # This should make a copy
print(bool(df.is_copy))
df = df[['A', 'IP']]
print(df)
print(bool(df.is_copy))

False
   A  IP
0  x   1
1  y   2
2  z   3
True
  

Отлично, теперь у нас есть копия. Давайте выполним то же назначение с apply

 df['Country'] = df.IP.apply(find_country_from_connection_ip)
df
  
 //anaconda/envs/3.5/lib/python3.5/site-packages/ipykernel/__main__.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  if __name__ == '__main__':
  

введите описание изображения здесь


как вы это исправляете?
Где бы вы ни создавали, df вы можете использовать df.loc . Мой пример выше, где я df = df[:] запустил копирование. Если бы я использовал loc вместо этого, я бы избежал этого беспорядка.

 print(bool(df.is_copy))
df = df.loc[:]
print(df)
print(bool(df.is_copy))

False
   A  IP
0  x   1
1  y   2
2  z   3
False
  

Вам нужно либо найти, где df создается, и использовать loc или iloc вместо этого, когда вы разрезаете исходный фрейм данных. Или вы можете просто сделать это…

 df.is_copy = None
  

Полная демонстрация

 df = pd.DataFrame(dict(IP=[1, 2, 3], A=list('xyz')))

def find_country_from_connection_ip(ip):
    return {1: 'A', 2: 'B', 3: 'C'}[ip]

df = df[:]

df.is_copy = None

df['Country'] = df.IP.apply(find_country_from_connection_ip)
df
  

введите описание изображения здесь

Комментарии:

1. Спасибо, хорошо объяснил. Но после использования d2 = d1.loc[d1['Country'] == '$$'] bool(d2.is_copy) все равно вычисляется как True . И всего 100 строк занимают около 5 seconds , что означает, что для завершения 400000 строк потребуется около 6 hours . Довольно долгое время, не так ли?

2. Единственный способ ускорить это — показать нам код преобразования ip, чтобы узнать, можем ли мы каким-либо образом его векторизовать.

Ответ №3:

IIUC вы можете использовать свою пользовательскую функцию с Series.apply таким способом:

 df['Country'] = df['IP'].apply(find_country_from_ip)
  

Пример:

 df = pd.DataFrame({'IP':[1,2,3],
                   'B':[4,5,6]})




def find_country_from_ip(ip):
            # Do some processing 
            # some testing formula
            country = ip   5
            return country



   df['Country'] = df['IP'].apply(find_country_from_ip)

print (df)
   B  IP  Country
0  4   1        6
1  5   2        7
2  6   3        8
  

Ответ №4:

Прежде всего, ответ @MaxU — это правильный путь, эффективный и идеальный для параллельного приложения на векторизованном pd.series / dataframe.

Сопоставит производительность двух популярных библиотек для возврата данных о местоположении с учетом информации об IP-адресе. TLDR: используйте метод geolite2.

1. geolite2 пакет из geolite2 библиотеки

Ввод

 # !pip install maxminddb-geolite2
import time
from geolite2 import geolite2
geo = geolite2.reader()
df_1 = train_data.loc[:50,['IP_Address']]

def IP_info_1(ip):
    try:
        x = geo.get(ip)
    except ValueError:   #Faulty IP value
        return np.nan
    try:
        return x['country']['names']['en'] if x is not None else np.nan
    except KeyError:   #Faulty Key value
        return np.nan


s_time = time.time()
# map IP --> country
#apply(fn) applies fn. on all pd.series elements
df_1['country'] = df_1.loc[:,'IP_Address'].apply(IP_info_1)
print(df_1.head(), 'n')
print('Time:',str(time.time()-s_time) 's n')

print(type(geo.get('48.151.136.76')))
  

Вывод

        IP_Address         country
0   48.151.136.76   United States
1    94.9.145.169  United Kingdom
2   58.94.157.121           Japan
3  193.187.41.186         Austria
4   125.96.20.172           China 

Time: 0.09906983375549316s 

<class 'dict'>
  

2. DbIpCity пакет из ip2geotools библиотеки

Ввод

 # !pip install ip2geotools
import time
s_time = time.time()
from ip2geotools.databases.noncommercial import DbIpCity
df_2 = train_data.loc[:50,['IP_Address']]
def IP_info_2(ip):
    try:
        return DbIpCity.get(ip, api_key = 'free').country
    except:
        return np.nan
df_2['country'] = df_2.loc[:, 'IP_Address'].apply(IP_info_2)
print(df_2.head())
print('Time:',str(time.time()-s_time) 's')

print(type(DbIpCity.get('48.151.136.76',api_key = 'free')))
  

Вывод

        IP_Address country
0   48.151.136.76      US
1    94.9.145.169      GB
2   58.94.157.121      JP
3  193.187.41.186      AT
4   125.96.20.172      CN

Time: 80.53318452835083s 

<class 'ip2geotools.models.IpLocation'>
  

Причина, по которой огромная разница во времени может быть связана со структурой выходных данных, т.е. прямое подмножество из словарей кажется более эффективным, чем индексирование из специализированного ip2geotools.models.Объект IpLocation.

Кроме того, результатом первого метода является словарь, содержащий данные о географическом местоположении, соответственно, подмножество для получения необходимой информации:

 x = geolite2.reader().get('48.151.136.76')
print(x)

>>>
    {'city': {'geoname_id': 5101798, 'names': {'de': 'Newark', 'en': 'Newark', 'es': 'Newark', 'fr': 'Newark', 'ja': 'ニューアーク', 'pt-BR': 'Newark', 'ru': 'Ньюарк'}},

 'continent': {'code': 'NA', 'geoname_id': 6255149, 'names': {'de': 'Nordamerika', 'en': 'North America', 'es': 'Norteamérica', 'fr': 'Amérique du Nord', 'ja': '北アメリカ', 'pt-BR': 'América do Norte', 'ru': 'Северная Америка', 'zh-CN': '北美洲'}}, 

'country': {'geoname_id': 6252001, 'iso_code': 'US', 'names': {'de': 'USA', 'en': 'United States', 'es': 'Estados Unidos', 'fr': 'États-Unis', 'ja': 'アメリカ合衆国', 'pt-BR': 'Estados Unidos', 'ru': 'США', 'zh-CN': '美国'}}, 

'location': {'accuracy_radius': 1000, 'latitude': 40.7355, 'longitude': -74.1741, 'metro_code': 501, 'time_zone': 'America/New_York'}, 

'postal': {'code': '07102'}, 

'registered_country': {'geoname_id': 6252001, 'iso_code': 'US', 'names': {'de': 'USA', 'en': 'United States', 'es': 'Estados Unidos', 'fr': 'États-Unis', 'ja': 'アメリカ合衆国', 'pt-BR': 'Estados Unidos', 'ru': 'США', 'zh-CN': '美国'}}, 

'subdivisions': [{'geoname_id': 5101760, 'iso_code': 'NJ', 'names': {'en': 'New Jersey', 'es': 'Nueva Jersey', 'fr': 'New Jersey', 'ja': 'ニュージャージー州', 'pt-BR': 'Nova Jérsia', 'ru': 'Нью-Джерси', 'zh-CN': '新泽西州'}}]}
  

Ответ №5:

я передал фрейм данных со столбцом ipaddress с помощью приведенного ниже кода — в df было около 300 тысяч строк. это заняло около 20 секунд.

 import pandas as pd
from geolite2 import geolite2

def get_country(row,ip):
    try:
        x = geo.get(row[ip])
    except ValueError:
        return pd.np.nan
    try:
        return x['country']['names']['en'] if x else pd.np.nan
    except KeyError:
        return pd.np.nan

geo = geolite2.reader()

# map IP --> country
df_test['login_ip_country'] = df_test.apply(lambda row: get_country(row,'login_ip_address'), axis = 1)
df_test['registered_ip_country'] = df_test.apply(lambda row: get_country(row,'registered_ip_address'), axis = 1)

geolite2.close()

df_test.head()
  

не нужно делать это серией. просто передайте ‘row’ в вашу функцию, которая действует как ‘df’