我有一个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。
答案 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)]
输出与上述方法相同。
基准测试结果
要了解这三种方法的效果(NearestDict
,KDDict(True)
(插入时重新生成)和KDDict(False)
(推迟重新生成)),我会简要地对它们进行基准测试。
我跑了3个不同的测试。在测试中保持相同的参数是:
timeit.repeat
默认为3)。第一个测试使用了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)
要么快一些(在查找中),要么相当慢一些(在插入中)。因此,我将其排除在讨论之外并专注于NearestDict
和KDDict(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维度。