在字典中查找整数nearest-neighbor

时间:2015-03-17 08:34:46

标签: python algorithm dictionary nearest-neighbor

我有一个dict,它带有整数键:

a = {}
a[1] = 100
a[55] = 101
a[127] = 102

我希望能够在询问时采取最近的邻居:

a[20] # should return a[1] = 100
a[58] # should return a[55] = 101
a[167] # should return a[127] = 102

有没有这样做的pythonic方式?(我想这可以通过循环所有的dict来完成,但这可能不是最优雅的解决方案?)


与双索引相同的问题(也是整数):

 b[90, 1] = 100, b[90, 55] = 101, b[90, 127] = 102
 b[70, 1] = 40, b[70, 45] = 41, b[70, 107] = 42

我希望能够得到 b[73, 40] = b[70, 45] = 41即。在2D平面上最近的neighboor。

6 个答案:

答案 0 :(得分:3)

更新:在对此答案中的两种方法进行基准测试后,第二方法明显更好,以至于它几乎是严格要求。


以下方法同样处理 n - 维度:

class NearestDict(dict):
    def __init__(self, ndims):
        super(NearestDict, self).__init__()
        self.ndims = ndims

    # Enforce dimensionality
    def __setitem__(self, key, val):
        if not isinstance(key, tuple): key = (key,)
        if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
        super(NearestDict, self).__setitem__(key, val)

    @staticmethod
    def __dist(ka, kb):
        assert len(ka) == len(kb)
        return sum((ea-eb)**2 for (ea, eb) in zip(ka, kb))

    # Helper method and might be of use
    def nearest_key(self, key):
        if not isinstance(key, tuple): key = (key,)
        nk = min((k for k in self), key=lambda k: NearestDict.__dist(key, k))
        return nk

    def __missing__(self, key):
        if not isinstance(key, tuple): key = (key,)
        if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
        return self[self.nearest_key(key)]

演示:

a = NearestDict(1)
a[1] = 100
a[55] = 101
a[127] = 102
print a[20]    # 100
print a[58]    # 100
print a[167]   # 102
print a.nearest_key(20)     # (1,)
print a.nearest_key(58)     # (55,)
print a.nearest_key(127)    # (127,)

b = NearestDict(2)
b[90, 1]   = 100
b[90, 55]  = 101
b[90, 127] = 102
b[70, 1]   = 40
b[70, 45]  = 41
b[70, 107] = 42
print b[73, 40] # 41
print b.nearest_key((73,40)) # (70, 45)

请注意,如果密钥存在,则查找速度不会低于标准字典查找。如果密钥不存在,则计算每个现有密钥之间的距离。没有什么是缓存的,尽管你可以在我想的时候解决这个问题。

修改

根据Kasra's answer建议的方法,以下方法使用scipy's cKDTree实现与上述相同的类:

请注意,还有一个额外的可选参数regenOnAdd,允许您在完成(大部分)插入之后推迟(重新)构建KDTree:

from scipy.spatial import cKDTree

class KDDict(dict):
    def __init__(self, ndims, regenOnAdd=False):
        super(KDDict, self).__init__()
        self.ndims = ndims
        self.regenOnAdd = regenOnAdd
        self.__keys = []
        self.__tree = None
        self.__stale = False

    # Enforce dimensionality
    def __setitem__(self, key, val):
        if not isinstance(key, tuple): key = (key,)
        if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
        self.__keys.append(key)
        self.__stale = True
        if self.regenOnAdd: self.regenTree()
        super(KDDict, self).__setitem__(key, val)

    def regenTree(self):
        self.__tree = cKDTree(self.__keys)
        self.__stale = False

    # Helper method and might be of use
    def nearest_key(self, key):
        if not isinstance(key, tuple): key = (key,)
        if self.__stale: self.regenTree()
        _, idx = self.__tree.query(key, 1)
        return self.__keys[idx]

    def __missing__(self, key):
        if not isinstance(key, tuple): key = (key,)
        if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
        return self[self.nearest_key(key)]

输出与上述方法相同。

基准测试结果

要了解这三种方法的效果(NearestDictKDDict(True)(插入时重新生成)和KDDict(False)(推迟重新生成)),我会简要地对它们进行基准测试。

我跑了3个不同的测试。在测试中保持相同的参数是:

  • 测试迭代次数我每次测试5次并花费最少的时间。 (注意timeit.repeat默认为3)。
  • 点边界:我生成的范围为0< = x<的整数关键点1000
  • 查找次数:我分别对插入和查找进行了定时。下面的三个测试都使用了10,000次查找。

第一个测试使用了4个维度的密钥和1,000个插入。

{'NDIMS': 4, 'NITER': 5, 'NELEMS': 1000, 'NFINDS': 10000, 'DIM_LB': 0, 'DIM_UB': 1000, 'SCORE_MUL': 100}
insert::NearestDict       0.125
insert::KDDict(regen)    35.957
insert::KDDict(defer)     0.174
search::NearestDict    2636.965
search::KDDict(regen)    49.965
search::KDDict(defer)    51.880

第二次测试使用4维和100次插入的键。我想改变插入次数,看看两种方法在字典密度变化时表现得有多好。

{'NDIMS': 4, 'NITER': 5, 'NELEMS': 100, 'NFINDS': 10000, 'DIM_LB': 0, 'DIM_UB': 1000, 'SCORE_MUL': 100}
insert::NearestDict       0.013
insert::KDDict(regen)     0.629
insert::KDDict(defer)     0.018
search::NearestDict     247.920
search::KDDict(regen)    44.523
search::KDDict(defer)    44.718

第三次测试使用了100次插入(如第二次测试),但是使用了12次。我想看看这些方法如何在关键维度上增加。

{'NDIMS': 12, 'NITER': 5, 'NELEMS': 100, 'NFINDS': 10000, 'DIM_LB': 0, 'DIM_UB': 1000, 'SCORE_MUL': 100}
insert::NearestDict       0.013
insert::KDDict(regen)     0.722
insert::KDDict(defer)     0.017
search::NearestDict     405.092
search::KDDict(regen)    49.046
search::KDDict(defer)    50.601

<强>讨论

具有连续再生(KDDict)的

KDDict(True)要么快一些(在查找中),要么相当慢一些(在插入中)。因此,我将其排除在讨论之外并专注于NearestDictKDDict(False),现在简称为KDDict

结果令人惊讶地支持KDDict延迟再生。

对于插入,在所有情况下,KDDict表现略差于NearestDict。由于额外的列表附加操作,这是预期的。

对于搜索,在所有情况下,KDDict比NearestDict执行 明显更好

随着字典的稀疏性/密度的增加,NearestDict的表现比KDDict的表现要大得多。从100键到1000键时,NearestDict搜索时间增加了9.64倍,而KDDict搜索时间仅增加了0.16倍。

随着字典维数的增加,NearestDict的表现下降到比KDDict更大的程度。从4维到12维时,NearestDict搜索时间增加了0.64倍,而KDDict搜索时间仅增加了0.13倍。

鉴于此,以及两个类的相对相同的复杂性,如果您可以访问scipy工具包,强烈建议使用KDDict方法。

答案 1 :(得分:2)

类似的东西:

class CustomDict(dict):
    def __getitem__(self, key):
        try:
            return dict.__getitem__(self, key)
        except KeyError:
            closest_key = min(self.keys(), key=lambda x: abs(x - key))
            return dict.__getitem__(self, closest_key)

或者这个:

class CustomDict(dict):
    def __getitem__(self, key):
        if key in self:
            return dict.__getitem__(self, key)
        else:
            closest_key = min(self.keys(), key=lambda x: abs(x - key))
            return dict.__getitem__(self, closest_key)

两者都给出了这个结果:

a = CustomDict()
a[1] = 100
a[55] = 101
a[127] = 102

print a[20] # prints 100
print a[58] # prints 101
print a[167] # prints 102

对于双索引版本:

class CustomDoubleDict(dict):
    def __getitem__(self, key):
        if key in self:
            return dict.__getitem__(self, key)
        else:
            closest_key = min(self.keys(), key=lambda c: (c[0] - key[0]) ** 2 + (c[1] - key[1]) ** 2)
            return dict.__getitem__(self, closest_key)


b = CustomDoubleDict()
b[90, 1] = 100
b[90, 55] = 101
b[90, 127] = 102
b[70, 1] = 40
b[70, 45] = 41
b[70, 107] = 42

print b[73, 40]  # prints 41
print b[70, 45]  # prints 41

答案 2 :(得分:2)

选项1:

维护一个单独且有序的密钥列表(或使用OrderedDict)。使用二进制搜索查找最近的密钥。这应该是 O(log n)

选项2 :(如果数据不是很大且稀疏)

由于您提到dict是静态的,因此请通过dict来填充所有缺少的值一次。保持最大和最小键,并覆盖__getitem__,使高于最大值或低于最小值的键返回正确的值。这应该是 O(1)

选项3:

每次只使用键上的循环,它将是 O(n)。在您的应用程序中尝试它,您可能会发现简单的解决方案非常快速和充足。

答案 3 :(得分:1)

如何使用min使用正确的键功能:

>>> b ={(90, 55): 101, (90, 127): 102, (90, 1): 100}
>>> def nearest(x,y):
...   m=min(((i,j) for i,j in b ),key= lambda v:abs(v[0]-x)+abs(v[1]-y))
...   return b[m]
... 
>>> nearest(40,100)
102
>>> nearest(90,100)
102
>>> b
{(90, 55): 101, (90, 127): 102, (90, 1): 100}
>>> nearest(90,10)
100

前面的答案是我建议的pythonic答案,但如果你寻找一种快速的方式,你可以使用scipy.spatial.KDTree

  

class scipy.spatial.KDTree(data, leafsize=10)

     

用于快速最近邻查找的kd-tree

     

此类提供一组k维点的索引,可用于快速查找任意点的最近邻居。

另请查看http://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.cKDTree.html#scipy-spatial-ckdtree

http://docs.scipy.org/doc/scipy/reference/spatial.distance.html#module-scipy.spatial.distance

答案 4 :(得分:0)

一维案例的未经测试的O(log n)示例:

import collections
import bisect


class ProximityDict(collections.MutableMapping):
    def __init__(self, *args, **kwargs):
        self.store = dict()
        self.ordkeys = []
        self.update(dict(*args, **kwargs))

    def __getitem__(self, key):
        try: return self.store[key]
        except KeyError:
            cand = bisect.bisect_left(self.ordkeys, key)
            if cand == 0: return self.store[self.ordkeys[0]]

            return self.store[
                min(self.ordkeys[cand], self.ordkeys[cand-1],
                    key=lambda k: abs(k - key))
            ]

    def __setitem__(self, key, value):
        if not key in self.store: bisect.insort_left(self.ordkeys, key)
        self.store[key] = value

    def __delitem__(self, key):
        del self.store[key]
        del self.keys[bisect.bisect_left(self.ordkeys, key)]

    def __iter__(self):
        return iter(self.store)

    def __len__(self):
        return len(self.store)

二维情况显然更令人讨厌(您需要存储四叉树中订购的密钥),但可以采用类似的方式。

我没有删除代码也没有&#39; proximity&#39;行为,但你也可以这样做。

答案 5 :(得分:0)

这里有一个主要依赖于地图/过滤操作的pythonic解决方案

class NeirestSearchDictionnary1D(dict):
    """ An extended dictionnary that returns the value that is the nearest to 
    the requested key. As it's key distance is defined for simple number 
    values, trying to add other keys will throw error. """
    def __init__(self):
        """ Constructor of the dictionnary.
        It only allow to initialze empty dict """
        dict.__init__(self)

    def keyDistance(self, key1, key2):
        """ returns a distance between 2 dic keys """
        return abs(key1-key2)

    def __setitem__(self, key, value):
        """ override  the addition of a couple in the dict."""
        #type checking
        if not (isinstance(key, int) or isinstance(key, float)):
            raise TypeError("The key of such a "+ type(self) + "must be a simple numerical value")
        else:
            dict.__setitem__(self, key, value)

    def __getitem__(self, key):
        """ Override the getting item operation """
        #compute the minial distance
        minimalDistance = min(map(lambda x : self.keyDistance(key, x), self.keys()))
        #get the list of key that minimize the distance
        resultSetKeys = filter(lambda x : self.keyDistance(key, x) <= minimalDistance, self.keys())
        #return the values binded to the keys minimizing the distances.
        return list(map(lambda x : dict.__getitem__(self, x), resultSetKeys))

if __name__ == "__main__":

    dic = NeirestSearchDictionnary1D()
    dic[1] = 100
    dic[55] = 101
    dic[57] = 102
    dic[127] = 103
    print("the entire dict :", dic)
    print("dic of '20'", dic[20])
    print("dic of '56'", dic[56])

显然,你可以用很少的工作将它扩展到2D维度。