Pandas: 最快的IP解析方式
Pandas: fastest way to resolve IP to country
我有一个函数 find_country_from_connection_ip
需要一个 ip,经过一些处理 returns 一个国家。如下所示:
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:
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':
In [38]:
我明白这个问题,但不太明白如何将 loc
与 apply
和 lambda
一起使用。
N.B。请建议您是否有更有效的替代解决方案,可以带来最终结果。
**** 编辑 ********
该函数主要是在 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
行的 DataFrame 时,这应该需要一些时间。但是多少钱?就是那个问题。大约需要 2 小时,我认为差不多。
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
您的问题不在于如何使用 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
我会为此使用 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:\download\ipfilter.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,)
结论:大约需要。在我的硬件上使用 400K 唯一 IP 的 DF 35 秒:
In [93]: 400000/171884*15
Out[93]: 34.90726303786274
首先,@MaxU 的回答是在矢量化 pd 上并行应用的有效且理想的方法。series/dataframe。
将两个流行库的性能与 return location 数据给定 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 对象建立索引更有效。
此外,第一种方法的输出是包含geo-location数据的字典,分别子集以获得所需信息:
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': '新泽西州'}}]}
我有一个函数 find_country_from_connection_ip
需要一个 ip,经过一些处理 returns 一个国家。如下所示:
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: 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': In [38]:
我明白这个问题,但不太明白如何将 loc
与 apply
和 lambda
一起使用。
N.B。请建议您是否有更有效的替代解决方案,可以带来最终结果。
**** 编辑 ********
该函数主要是在 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
行的 DataFrame 时,这应该需要一些时间。但是多少钱?就是那个问题。大约需要 2 小时,我认为差不多。
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
您的问题不在于如何使用 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
我会为此使用 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:\download\ipfilter.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,)
结论:大约需要。在我的硬件上使用 400K 唯一 IP 的 DF 35 秒:
In [93]: 400000/171884*15
Out[93]: 34.90726303786274
首先,@MaxU 的回答是在矢量化 pd 上并行应用的有效且理想的方法。series/dataframe。
将两个流行库的性能与 return location 数据给定 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 对象建立索引更有效。
此外,第一种方法的输出是包含geo-location数据的字典,分别子集以获得所需信息:
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': '新泽西州'}}]}