基于条件合并两个Numpy数组的Python代码

7

我可以帮您将以下两个数组进行合并,通过在数组B中查找数组A的值来实现。

数组A:

array([['GG', 'AB', IPv4Network('1.2.3.41/26')],
       ['GG', 'AC', IPv4Network('1.2.3.42/25')],
       ['GG', 'AD', IPv4Network('1.2.3.43/24')],
       ['GG', 'AE', IPv4Network('1.2.3.47/23')],
       ['GG', 'AF', IPv4Network('1.2.3.5/24')]],
      dtype=object)

以及数组B:

array([['123456', 'A1', IPv4Address('1.2.3.5'), nan],
       ['987654', 'B1', IPv4Address('1.2.3.47'), nan]],
      dtype=object)  

这里的目标是通过在Array A中查找IPv4Address并将其与Array B进行比较,获取相应数组的第二个值并将其存储,以创建Array C:

Array C:

array([['123456', 'A1', IPv4Address('1.2.3.5'), nan, 'AF'],
       ['987654', 'B1', IPv4Address('1.2.3.47'), nan, 'AE']],
      dtype=object) 

IP地址的类型如下:https://docs.python.org/3/library/ipaddress.html#ipaddress.ip_network

我该如何实现这个?

编辑:

请注意,合并是基于IP匹配条件进行的,因此生成的数组C将与数组B具有相同数量的数组,但它将多一个值。建议的重复链接没有回答相同的问题。


3
如果有了 Pandas,为什么还要用 NumPy 呢?这并不会使它更快。 - cs95
1
@huynhsamha 这只是虚拟数据,我没有放真实的 IP 地址,可能这就是原因。 - teebeetee
3
请努力确保数据的有效性,以便至少可以复制粘贴并运行。 - cs95
就算法复杂度而言,A和B的大小均为2 ** 32,但IPv4的/length最多具有32个值--最好进行32次查找,例如:1.2.3.5 -> 0b1000000100000001100000101 -> 0b1000000100000001100000100 -> 0b1000000100000001100000100 -> 0b1000000100000001100000000 -> .... - Dima Tisnek
2
你需要的唯一答案是,object-dtype numpy数组是毫无意义的,它们不提供我们使用numpy的速度或内存优势。只需使用本地Python中的列表即可完成。 - Andras Deak -- Слава Україні
显示剩余6条评论
3个回答

6
这应该能够做到您所要求的(至少输出结果与您想要的完全相同),我对您的#dummydata进行了一些小的假设处理,但这不应该太重要。

代码:

import numpy as np
import ipaddress as ip

array_A = np.array([['GG', 'AB', ip.ip_network('192.168.0.0/32')],
                    ['GG', 'AC', ip.ip_network('192.168.0.0/31')],
                    ['GG', 'AD', ip.ip_network('192.168.0.0/30')],
                    ['GG', 'AE', ip.ip_network('192.168.0.0/29')],
                    ['GG', 'AF', ip.ip_network('192.168.0.0/28')]],
                   dtype=object)

array_B = np.array([['123456', 'A1', ip.ip_network('192.168.0.0/28'), np.nan],
                    ['987654', 'B1', ip.ip_network('192.168.0.0/29'), np.nan]],
                   dtype=object)


def merge_by_ip(A, B):
    # initializing an empty array with len(B) rows and 5 columns for the values you want to save in it
    C = np.empty([len(B), 5],dtype=object)
    for n in range(len(B)):
        for a in A:
            # checking condition: if ip address in a is ip address in b
            if a[2] == B[n][2]:
                # add the entry of b with the second value of a to the new Array c
                C[n] = np.append(B[n], a[1])
    return C


print(merge_by_ip(array_A, array_B))

输出:

[['123456' 'A1' IPv4Network('192.168.0.0/28') nan 'AF']
 ['987654' 'B1' IPv4Network('192.168.0.0/29') nan 'AE']]

注意:

这个解决方案的复杂度为O(m * n),这并不是必要的,有很多开箱即用的(例如Pandas)和自定义的(例如使用dict)合并方式可以实现更低的复杂度。


你能否请注释一些数值?例如:np.empty([len(B), 5] 中的数字5代表什么?a[2]B[n][2]a[1] 中的数字3又分别代表什么? - teebeetee
我已经在代码中添加了它。但是我不明白你所说的3是什么意思。 - mrk

3

似乎没有理由你不能使用Pandas。如果你的IP地址完全对齐,你可以使用merge,然后使用pd.DataFrame.values返回一个NumPy数组:

import pandas as pd

# data from @mk18
df_A = pd.DataFrame(array_A[:, 1:], columns=['', 'IP'])
df_B = pd.DataFrame(array_B, columns=['id', 'value', 'IP', 'na'])

res = df_B.merge(df_A, on='IP').values

print(res)

array([['123456', 'A1', IPv4Network('192.168.0.0/28'), nan, 'AF'],
       ['987654', 'B1', IPv4Network('192.168.0.0/29'), nan, 'AE']],
      dtype=object)

如果您希望忽略网络组件,仅包含network_address进行合并,即使用'1.2.3.5'而不是'1.2.3.5/24',那么您可以在合并之前创建辅助系列:
import pandas as pd
from operator import attrgetter

df_A = pd.DataFrame(array_A[:, 1:], columns=['key', 'IP'])
df_B = pd.DataFrame(array_B, columns=['id', 'value', 'IP', 'na'])

df_A['IP_NoNetwork'] = df_A['IP'].map(attrgetter('network_address'))
df_B['IP_NoNetwork'] = df_B['IP'].map(attrgetter('network_address'))

res = df_B.merge(df_A.drop('IP', 1), on='IP_NoNetwork')\
          .loc[:, ['id', 'value', 'IP', 'na', 'key']].values

0

你的数据存在问题和复杂情况,这些问题阻止了你使用像你所链接的问题中建议的join_by或者rec_join

正如其他人指出的那样,你的数据的主要问题是像IPv4Network('1.2.3.4/24')这样的网络不是有效的网络,因为它们具有被/24掩码过滤掉的主机位。而/24表示最后的32-24 = 8个位是你的主机位,IPv4Network的构造器要求把这些位设为0,例如IPv4Network('1.2.3.0/24')是有效的。

主要的复杂性在于你在一个数组中有网络,但在另一个数组中有地址。像rec_joinjoin_by这样的方法使用比较(即==)来决定哪些记录应该放在一起。其他一些提议的答案通过用地址替换网络来“解决”这个问题,但这似乎不是你的问题。

还要注意,单个网络地址可能属于多个不同的网络。例如,IPv4Address('1.2.3.129') 同时属于 IPv4Network('1.2.3.0/24')IPv4Network('1.2.3.128/25')。所以,我想你希望在结果中看到这两个匹配项。

要将数组中的地址与实际属于的网络连接起来,您需要自己遍历数组并构造一个新数组。适用的比较类型是 IPv4Address('1.2.3.129') in IPv4Network('1.2.3.0/24')(这是True)。

将这些内容结合起来的工作代码示例:

from numpy import nan, asarray, concatenate
from ipaddress import IPv4Address, IPv4Network

a = asarray([
    ['GG', 'AA', IPv4Network('1.2.4.0/24')],
    ['GG', 'AB', IPv4Network('1.2.3.128/25')],
    ['GG', 'AC', IPv4Network('1.2.3.0/24')]
], dtype=object)

b = asarray([
    ['123456', 'A1', IPv4Address('1.2.3.4'), nan],
    ['987654', 'B1', IPv4Address('1.2.3.129'), nan],
    ['024680', 'C1', IPv4Address('1.2.4.0'), nan]
], dtype=object)


def join_addresses_networks(addresses, networks):
    for address in addresses:
        for network in networks:
            if address[2] in network[2]:
                yield concatenate((address, network[:-1]))


c = asarray(list(join_addresses_networks(b, a)))

print(c)

这个解决方案的复杂度为O(m * n),在这里并不是必要的,有许多开箱即用的(Pandas)和自定义的(例如使用dict)方法可以以更低的复杂度进行合并。 - jpp
你是对的 @jpp,但我认为直接回答更好,因为问题不是关于性能,而是基本的“如何”。答案可以改进为O(m + n)的效率,留给读者作为练习。 - Grismar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接