Найти ближайшего целого числа в dict

У меня есть 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

Есть ли питонический способ сделать это? (Я предполагаю, что это можно сделать, зациклив весь словарь, но это, вероятно, не самое элегантное решение?)


Тот же вопрос с двойным индексом (включая целые числа):

 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-плоскости.


person Basj    schedule 17.03.2015    source источник
comment
@wim Да, диктофон статичен.   -  person Basj    schedule 17.03.2015
comment
Что вы хотите в случае ничьей на расстоянии?   -  person wim    schedule 17.03.2015
comment
@wim При равном расстоянии оба соседа будут работать на меня   -  person Basj    schedule 17.03.2015
comment
ты хочешь сказать, что хочешь обоих? или подойдет? обратите внимание, что для 2-го случая может быть 4-сторонняя ничья   -  person wim    schedule 17.03.2015
comment
Скажем, всегда брать верхнего соседа (в обоих направлениях)   -  person Basj    schedule 17.03.2015
comment
@wim Нет, может быть неограниченное количество связей, если разрешены настоящие ключи.   -  person orlp    schedule 17.03.2015
comment
я написал dict на C, который упрощает эту задачу. как это работает, у него есть прямые и обратные методы. найдите [20], и он вернет статус не найдено, но помнит, где находится. дикт заказан. просто идите вперед и назад, и это дает [55] и [1] для этих двух методов. затем вызывающий абонент может решить, какой из них.   -  person Skaperen    schedule 17.03.2015
comment
@orlp не с целочисленными индексами, не может   -  person wim    schedule 17.03.2015
comment
хотите ли вы знать, когда у вас появится сосед?   -  person Skaperen    schedule 17.03.2015
comment
@wim не бесконечен, но все дискретные точки на окружности будут равными.   -  person thebjorn    schedule 17.03.2015
comment
@thebjorn может просить, чтобы центр усложнил задачу   -  person Skaperen    schedule 17.03.2015
comment
Порядок величины: у меня около ~ 2000 статических элементов в файле dict. И доступ к словарю может осуществляться 100 раз в секунду с помощью a[i, j], с помощью 0<i<128, 0<j<128. Это должно работать на Raspberry Pi и быть очень легким;)   -  person Basj    schedule 17.03.2015
comment
Со стандартным словарем вы не сможете найти ближайших соседей быстрее, чем за линейное время, из-за особенностей хэш-функции. Если вы просто хотите избежать явных циклов for в своем коде, ответ Тимоти подойдет, но цикл все еще существует. Если вам нужна меньшая сложность, вам придется рассмотреть альтернативные контейнеры (см. методы разделения пространства), которые, как правило, более затратны для однократного создания, но их поиск выполняется быстрее.   -  person Francis Colas    schedule 17.03.2015
comment
Затем вам нужно выполнить некоторые предварительные расчеты, чтобы создать структуру данных, которая больше соответствует вашим потребностям. Какая-то древовидная структура кажется естественным выбором...   -  person thebjorn    schedule 17.03.2015
comment
@Basj звучит так, будто вы делаете картографическое приложение. есть лучшие способы организовать объекты.   -  person Skaperen    schedule 17.03.2015
comment
Если вы хотите, чтобы это было эффективно, numpy/scipy, возможно, лучший способ, например. stackoverflow.com/questions/10818546/   -  person georg    schedule 17.03.2015


Ответы (6)


Что-то похожее на это:

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
person Timothée Jeannin    schedule 17.03.2015
comment
Спасибо за решение. Могут ли отрицатели объяснить, почему это не очень хорошее решение? (Я пока точно не понимаю, почему это было бы хорошо или плохо...) - person Basj; 17.03.2015
comment
Да, было бы здорово, если бы даунвотеры могли подробно объяснить, почему они думают, что это может быть неправильно. Просто пытаюсь учиться здесь. :) - person Timothée Jeannin; 17.03.2015
comment
Я проголосовал против, потому что это перебирает все ключи каждый раз, когда нет точного совпадения. См. мой ответ для решения O (log n). - person orlp; 17.03.2015
comment
Трудно было выбрать приемлемый ответ. Это для простоты и быстрого решения проблемы (но не оптимально, потому что зацикливается на всех ключах). Джедвардса для N-мерного обобщения. И ответ вима на приложение к моей проблеме. - person Basj; 17.03.2015
comment
@Basj это признак того, что это хороший вопрос, когда он вызывает много разных ответов и дискуссий, хорошая работа. - person jedwards; 17.03.2015

Обновление. После сравнительного анализа двух подходов в этом ответе второй подход значительно лучше, до такой степени, что его следует почти отдавать предпочтение.


Следующий подход одинаково обрабатывает 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 answer, следующий подход реализует тот же класс, что и выше, с использованием scipy 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 разных теста. Параметры, которые оставались неизменными во всех тестах:

  • Количество тестовых итераций. Я выполнил каждый тест 5 раз и занял минимальное время. (Примечание timeit.repeat по умолчанию равно 3).
  • Границы точек: я сгенерировал целочисленные ключевые точки в диапазоне 0 ‹= x ‹ 1000
  • Количество поисковых запросов. Я замерял время вставок и поисковых запросов отдельно. Три приведенных ниже теста используют 10 000 поисковых запросов.

В первом тесте использовались ключи 4 измерений и 1000 вставок.

{'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,64x, в то время как время поиска KDDict увеличилось только на 0,13x.

В свете этого и относительно равной сложности двух классов, если у вас есть доступ к инструментарию scipy, настоятельно рекомендуется использовать подход KDDict.

person jedwards    schedule 17.03.2015
comment
Однако это перебирает все возможные ключи для каждого неточного индекса. - person orlp; 17.03.2015
comment
@orlp Я только что отредактировал это. Я согласен. Может быть реализована какая-то фильтрация (эти ключи строго хуже, чем другие кандидаты), или кэширование, или, может быть, более подходящая схема хеширования может немного улучшить это для больших словарей. - person jedwards; 17.03.2015
comment
Как возможность N-измерения! - person Arthur Vaïsse; 17.03.2015
comment
Спасибо за все эти исследования по этому вопросу! - person Basj; 20.03.2015

Вариант 1:

Ведите отдельный и упорядоченный список ключей (или используйте OrderedDict). Найдите ближайший ключ с помощью бинарного поиска. Это должно быть O(log n).

Вариант 2: (если данные не очень большие и разреженные)

Поскольку вы упомянули, что словарь является статическим, сделайте проход по словарю, чтобы заполнить все отсутствующие значения один раз. Поддерживайте максимальный и минимальный ключи и переопределите __getitem__, чтобы ключи выше максимума или ниже минимума возвращали правильное значение. Это должно быть O(1).

Вариант 3:

Просто используйте цикл по ключам каждый раз, это будет O(n). Попробуйте это в своем приложении, и вы обнаружите, что простое решение в любом случае достаточно быстрое и адекватное.

person wim    schedule 17.03.2015
comment
У меня есть около 2000 элементов в словаре, но их можно искать с помощью [i, j] с i и j в [1, 127]. Таким образом, вариант 2 подразумевает заполнение dict ‹16384 элементами (i,j). Наверное, это может быть лучшим. Вероятно, это займет ‹ 1 секунды и ‹ 100 КБ памяти, верно? Как вы думаете? - person Basj; 17.03.2015
comment
Да, это не было бы проблемой. Однако было бы лучше использовать numpy ndarray, чем dict. Поскольку ваши данные являются числовыми, таким образом у вас наверняка будет лучшая производительность и объем памяти. - person wim; 17.03.2015
comment
На самом деле я немного упростил, но значения не числовые, это звуковые объекты. Моя цель — написать сэмплер с Samples[Midinote, Velocity] = Sound('mysound.wav'). Иногда у вас есть всего несколько файлов .wav, и вы хотите иметь возможность играть для любого значения MIDI-ноты и для любой скорости (на MIDI-клавиатуре). Вот почему мне нужно Samples[60, 43], чтобы использовать MiddleC-60-Velocity50.wav, если нет доступного семпла для Velocity=43. - person Basj; 17.03.2015
comment
Хорошо, тогда, возможно, подойдет dict с ключами кортежа (или ndarray из object). Важно убедиться, что повторяющиеся значения не являются копиями, а являются ссылками на один и тот же объект Sound. - person wim; 17.03.2015
comment
Также для ключей поиска, ограниченных 1, 127, простое (вариант 3) решение, вероятно, будет весьма полезным, возможно, даже самым быстрым! - person wim; 17.03.2015
comment
Что касается варианта 3, вы имеете в виду, например, ответ Тимоти Жаннин, stackoverflow.com/a/29094641/1422096? Я все еще думаю, что зацикливание на 2000 элементов, и это 100 раз в секунду на RaspPi, будет слишком много (более того, эта задача должна быть очень легкой: задачей, требующей процессорного времени, будет микширование аудио .wav!) - person Basj; 17.03.2015
comment
У вас есть пример кода для варианта 2, то есть заполнения dict за один проход: for i in range(127): for j in range(127): a[i,j] = nearest... - person Basj; 17.03.2015
comment
вы хотите найти i, j, которые минимизируют sum((i - y)**2 + (j - x)**2 for x, y in zip(xs, ys)). xs, ys — это списки 2000 вершин, в которых у вас есть точки данных. - person wim; 17.03.2015
comment
Давайте продолжим это обсуждение в чате. - person Basj; 17.03.2015

Как насчет использования 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

Предыдущий ответ - это питонический ответ, который я предложил, но если вы ищете быстрый способ, вы можете использовать scipy.spatial.KDTree :

класс scipy. пространственное.KDTree(данные, 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

person kasravnd    schedule 17.03.2015
comment
Это перебирает каждый ключ для каждого индекса. - person orlp; 17.03.2015
comment
@orlp я просто хотел предложить ответ pythoinc! - person kasravnd; 17.03.2015
comment
Подход KDTree выглядит многообещающе. - person jedwards; 17.03.2015
comment
Спасибо за это питоническое решение и другое, оптимизированное для скорости. - person Basj; 17.03.2015
comment
@Kasra, вы планируете внедрить подход KDTree? Если нет, я изменю свой класс, чтобы использовать KDTree (и, возможно, когда-нибудь проверю его, если найду время) - person jedwards; 17.03.2015
comment
@jedwards Нет, нет проблем, не стесняйтесь;) - person kasravnd; 17.03.2015
comment
@Касра Спасибо! Реализация была на самом деле довольно простой, большое преимущество в KDTree - профилирование, чтобы следовать :) - person jedwards; 17.03.2015

Непроверенный пример 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)

Двумерный случай значительно более раздражает (вам нужно хранить ключи, упорядоченные в дереве квадрантов), но возможен аналогичным образом.

Я не кодировал удаление, чтобы также иметь поведение «близости», но вы тоже можете это сделать.

person orlp    schedule 17.03.2015
comment
Ваш ProximityDict не является объектом dict и полагается на упорядоченные ключи. Вопрос Is there a pythonic way of doing this? не Is there a fast way to do this?. Pythonic прост и понятен, быстрота приходит позже. - person Timothée Jeannin; 17.03.2015
comment
Тем не менее, это питонический код, и в вопросе четко говорилось, что они знали об очевидном решении O(n), поэтому нет необходимости предоставлять ответ O(n) imo. +1 - person wim; 17.03.2015
comment
@wim спрашивающий спросил способ pythonic сделать это, imo, который означает тот, который использует преимущества различных синтаксических возможностей для создания легко читаемого и поддерживаемого кода. Его заметка о цикле for не о производительности, а об элегантности (его слова, не мои). При этом orlp все же получил от меня положительный отзыв, несмотря на то, что он не обрабатывает 2-мерные измерения (о чем также спрашивали). - person jedwards; 17.03.2015
comment
@TimothéeJeannin Мой ProximityDict наследуется от collections.MutableMapping, и, насколько мне известно, это лучший способ сделать это. И что вы имеете в виду, что я полагаюсь на заказанные ключи? Я сам явно отслеживаю заказанные ключи. - person orlp; 17.03.2015

Вот одно 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-измерения с небольшой работой.

person Arthur Vaïsse    schedule 17.03.2015