#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’