Оптимизированное Python сравнение между списком Dict - PullRequest
0 голосов
/ 30 ноября 2010

Я пытаюсь увидеть, находятся ли узлы в объеме сферы, и добавить идентификатор узла в список.Однако эффективность алгоритма невероятно медленная, и я не уверен, как его улучшить.

У меня есть два списка.Список A имеет формат [{'num': ID, 'x': VALUE, 'y': VALUE, 'z': VALUE] , в то время как список B имеет формат [{'x': VALUE,' y ': VALUE,' z ': VALUE,' rad ': VALUE}] .

Размер обоих списков может превышать 100 000 элементов в каждом.

Мой текущий код опубликован ниже, но он очень неэффективен.

    filteredList = []
    for i in range(len(sList)):

            minx = (sList[i]['x']) - (sList[i]['radius'])
            maxx = (sList[i]['x']) + (sList[i]['radius'])
            miny = (sList[i]['y']) - (sList[i]['radius'])
            maxy = (sList[i]['y']) + (sList[i]['radius'])
            minz = (sList[i]['z']) - (sList[i]['radius'])
            maxz = (sList[i]['z']) + (sList[i]['radius'])

            for j in range(len(nList)):
                    if minx <= nList[j]['x'] <= maxx:
                            if miny <= nList[j]['y'] <= maxy:
                                    if minz <= nList[j]['z'] <= maxz:
                                            tmpRad = findRadius(sList[i],nList[j])
                                            if tmpRad <= sList[i]['radius']:
                                                    filteredList.append(int(nList[j]['num']))

Я в недоумении и ценю любые идеи.

Редактировать: Добавление дополнительной информации о формате данных.

Список A (nList) - определяет узлы с местоположениями x, y, z и идентификатором num
[{'y': 0.0, 'x': 0.0, 'num': 1.0, 'z': 0.0},
{' y ': 0.0,' x ': 1.0,' num ': 2.0,' z ': 0.0},
{' y ': 0.0,' x ': 2.0,'num': 3.0, 'z': 0.0},
{'y': 0.0, 'x': 3.0, 'num': 4.0, 'z': 0.0},
{'y':0,0, «x»: 4,0, «num»: 5,0, «z»: 0,0},
{«y»: 0,0, «x»: 5,0, «num»: 6,0, «z»: 0,0},
{'y': 0.0, 'x': 6.0, 'num': 7.0, 'z': 0.0},
{'y': 0.0, 'x': 7.0, 'num': 8.0, 'z': 0.0},
{'y': 0.0, 'x': 8.0, 'num': 9.0, 'z': 0.0},
{'y': 0.0, 'x': 9.0, 'num': 10.0, 'z': 0.0}]

Список B (sList) - определяет сферы, используя x, y, z, радиус
[{'y': 18,0, 'x': 25,0, 'z': 26,0, 'radius': 0,0056470000000000001},
{'y': 29,0, 'x': 23,0, 'z': 45,0, 'radius': 0,0066280000000000002},
{'y': 46.0, 'x': 29.0, 'z':13,0, «радиус»: 0,014350999999999999},
{«у»: 0,0, «х»: 20,0, «z»: 25,0, «радиус»: 0,014866000000000001},
{«у»: 27,0, «х»': 31.0,' z ': 18.0,' radius ': 0.018311999999999998},
{' y ': 10.0,' x ': 36.0,' z ': 46.0,' radius ': 0.024702000000000002},
{'y': 27,0, 'x': 13,0, 'z': 48,0, 'radius': 0,027300999999999999},
{'y': 14,0, 'x': 1,0, 'z': 13,0, 'radius': 0.033889000000000002},
{'y': 31.0, 'x': 20.0, 'z': 11.0, 'radius': 0.034118999999999997},
{'y': 23.0, 'x': 28.0, 'z ': 8,0,' радиус ': 0,036683}]

Ответы [ 6 ]

3 голосов
/ 30 ноября 2010

(Этот ответ касается простых оптимизаций и стиля Python; он работает с существующим алгоритмом, обучая некоторым точкам оптимизации, а не заменяя его более эффективным.)

Вот несколько моментов, чтобы сделать код более легким для чтения и понимания:

  • Итерация по списку sList, а не по диапазону (len (sList)). for i in range(len(sList)) становится for i in sList, а sList[i] становится i.

  • Нет необходимости в этом tmpRad; вставьте это в строку.

  • Вместо if a: if b: if c: используйте if a and b and c.

Теперь мы находимся на этом:

filteredList = []
for i in sList:
    minx = i['x'] - i['radius']
    maxx = i['x'] + i['radius']
    miny = i['y'] - i['radius']
    maxy = i['y'] + i['radius']
    minz = i['z'] - i['radius']
    maxz = i['z'] + i['radius']

    for j in nList:
        if minx <= j['x'] <= maxx and miny <= j['y'] <= maxy and minz <= j['z'] <= maxz and findRadius(i,j) <= i['radius']:
            filteredList.append(int(j['num']))

(PEP 8 рекомендует разбивать эту длинную строку на строки длиной не более 80 символов; PEP 8 также рекомендует filtered_list и s_list и n_list вместо filteredList, sList и nList .)


Я поставил findRadius(i, j) <= i['radius'] на первое место по стилю и потому, что, похоже, он с большей вероятностью оценит как ложное, ускоряя вычисления. Затем я также включил переменные minx и т. Д.:

filteredList = []
for i in sList:
    for j in nList:
        if findRadius(i, j) <= i['radius'] \
        and i['x'] - i['radius'] <= j['x'] <= i['x'] + i['radius'] \
        and i['y'] - i['radius'] <= j['y'] <= i['y'] + i['radius'] \
        and i['z'] - i['radius'] <= j['z'] <= i['z'] + i['radius']:
            filteredList.append(int(j['num']))

Надо думать, что i['x'] - i['radius'] <= j['x'] <= i['x'] + i['radius'] можно упростить; попробуйте вычесть i['x'] из всех трех частей.

Вы можете сократить это еще больше с пониманием списка.

filteredList = [int(j['num']) for j in nList for i in sList
        if findRadius(i, j) <= i['radius']
        and i['x'] - i['radius'] <= j['x'] <= i['x'] + i['radius']
        and i['y'] - i['radius'] <= j['y'] <= i['y'] + i['radius']
        and i['z'] - i['radius'] <= j['z'] <= i['z'] + i['radius']]

И, наконец, именованных кортежей (у этого тоже есть побочный эффект - сделать их неизменяемыми, что, вероятно, желательно? Также обратите внимание, что это только Python 2.6, прочитайте страницу, чтобы узнать, как это можно сделать с помощью старые версии Python):

from collections import namedtuple

node = namedtuple('node', 'x y z num')
sphere = namedtuple('sphere', 'x y z radius')

nList = [
        node(x=0.0, y=0.0, z=0.0, num=1.0),
        node(x=1.0, y=0.0, z=0.0, num=2.0),
        node(x=2.0, y=0.0, z=0.0, num=3.0),
        node(x=3.0, y=0.0, z=0.0, num=4.0),
        node(x=4.0, y=0.0, z=0.0, num=5.0),
        node(x=5.0, y=0.0, z=0.0, num=6.0),
        node(x=6.0, y=0.0, z=0.0, num=7.0),
        node(x=7.0, y=0.0, z=0.0, num=8.0),
        node(x=8.0, y=0.0, z=0.0, num=9.0),
        node(x=9.0, y=0.0, z=0.0, num=10.0)]

sList = [
        sphere(x=25.0, y=18.0, z=26.0, radius=0.0056470000000000001),
        sphere(x=23.0, y=29.0, z=45.0, radius=0.0066280000000000002),
        sphere(x=29.0, y=46.0, z=13.0, radius=0.014350999999999999),
        sphere(x=20.0, y=0.0, z=25.0, radius=0.014866000000000001),
        sphere(x=31.0, y=27.0, z=18.0, radius=0.018311999999999998),
        sphere(x=36.0, y=10.0, z=46.0, radius=0.024702000000000002),
        sphere(x=13.0, y=27.0, z=48.0, radius=0.027300999999999999),
        sphere(x=1.0, y=14.0, z=13.0, radius=0.033889000000000002),
        sphere(x=20.0, y=31.0, z=11.0, radius=0.034118999999999997),
        sphere(x=28.0, y=23.0, z=8.0, radius=0.036683)]

Тогда вместо sphere['radius'] вы можете сделать sphere.radius. Это делает код более аккуратным:

filteredList = [int(j.num) for j in nList for i in sList
        if findRadius(i, j) <= i.radius
        and i.x - i.radius <= j.x <= i.x + i.radius
        and i.y - i.radius <= j.y <= i.y + i.radius
        and i.z - i.radius <= j.z <= i.z + i.radius]

Или, без понимания списка,

filteredList = []
for i in sList:
    for j in nList:
        if findRadius(i, j) <= i.radius \
        and i.x - i.radius <= j.x <= i.x + i.radius \
        and i.y - i.radius <= j.y <= i.y + i.radius \
        and i.z - i.radius <= j.z <= i.z + i.radius:
            filteredList.append(int(j.num))

Наконец, выберите более подходящие имена; [стиль немного изменился в соответствии с комментариями, ставя findRadius в конце, так как это с большей вероятностью будет вычислительно дорогостоящим - вы, однако, лучший судья в этом отношении]

filteredList = [int(n.num) for n in nodes for s in spheres
        if s.x - s.radius <= n.x <= s.x + s.radius and
            s.y - s.radius <= n.y <= s.y + s.radius and
            s.z - s.radius <= n.z <= s.z + s.radius and
            findRadius(s, n) <= s.radius]

Или,

filteredList = []
for s in spheres:
    for n in nodes:
        if (s.x - s.radius <= n.x <= s.x + s.radius and
            s.y - s.radius <= n.y <= s.y + s.radius and
            s.z - s.radius <= n.z <= s.z + s.radius and
            findRadius(s, n) <= s.radius):
            filteredList.append(int(n.num))

(Вы можете поместить srad = s.radius во внешний цикл для возможного небольшого прироста производительности при желании.)

1 голос
/ 30 ноября 2010

Во-первых, Python не создан для такой итерации.Использование индексов для доступа к каждому элементу списка - это обратный процесс, своего рода повреждение мозга, которому учат языки низкого уровня, где это быстрее.В Python это на самом деле медленнее.range(len(whatever)) фактически создает новый список номеров, а затем вы работаете с номерами, которые вам передают из этого списка.То, что вы действительно хотите сделать, это просто работать с объектами, которые вам передаются из whatever.

Пока мы в этом, мы можем извлечь общий бит s['radius']это проверяется несколько раз и помещает все проверки if для ограничивающего прямоугольника в одну строку.О, и нам не нужен отдельный tmpRad, и я предполагаю, что num уже int s и не нуждаются в преобразовании (если они есть, почему? Почему бы просто не преобразовать их заранее?)

Ничто из этого не сделает огромную разницу, но, по крайней мере, облегчает чтение и определенно не повредит.

filteredList = []
for s in sList:
  radius = s['radius']
  minx = s['x'] - radius
  maxx = s['x'] + radius
  miny = s['y'] - radius
  maxy = s['y'] + radius
  minz = s['z'] - radius
  maxz = s['z'] + radius

  for n in nList:
    if (minx <= n['x'] <= maxx) and (miny <= n['y'] <= maxy) and \
       (minz <= n['z'] <= maxz) and (findRadius(s, n) <= radius): 
      filteredList.append(n['num'])

Теперьпо крайней мере, понятно, что происходит.

Однако, учитывая масштаб проблемы, с которой мы работаем, похоже, нам понадобятся улучшения алгоритмического характера.То, что вы, вероятно, хотите сделать здесь, это использовать какую-то технику BSP (разбиение двоичного пространства).Вот как это работает:

  • Сначала мы переставляем nList в дерево.Мы разрезали его на 8 меньших списков, основываясь на том, x> 0, y> 0 и z> 0 для каждой точки (8 комбинаций из 3 логических результатов).Затем каждый из них снова разрезается на 8, используя те же критерии - например, если возможный диапазон для x / y / z равен -10..10, то мы сокращаем "x> 0, y> 0, z>0 "список в зависимости от того, x> 5, y> 5, z> 5 и т. Д. Вы поняли.

  • Для каждой точки в списке sList мы проверяем, является ли minx>0 и т. Д. Прекрасная часть: если minx> 0, нам не нужно проверять ни один из списков 'x <0', а если maxx <0, нам не нужно проверять любой из 'x> 0списки.И так далее.Мы выясним, с каким из 8 «октантов» пространства пересекает ограничивающий прямоугольник;и для каждого из них мы рекурсивно проверяем соответствующие октанты этих октантов и т. д. до тех пор, пока не доберемся до листьев дерева, а затем выполним обычные тесты «точка-ограничитель», а затем тесты «точка-в-сфере».

1 голос
/ 30 ноября 2010

один, который мы можем удалить из образца

если вам не нужно перебирать список по индексу, этого не следует делать, также избегайте использования диапазона и объединяйте ifs вместе

filteredList = []
for a in sList:

        minx = (a['x']) - (a['radius'])
        maxx = (a['x']) + (a['radius'])
        miny = (a['y']) - (a['radius'])
        maxy = (a['y']) + (a['radius'])
        minz = (a['z']) - (a['radius'])
        maxz = (a['z']) + (a['radius'])

        for b in nList:
                if minx <= b['x'] <= maxx and miny <= b['y'] <= maxy and minz <= b['z'] <= maxz:
                    tmpRad = findRadius(a,b)
                    if tmpRad <= a['radius']:
                        filteredList.append(int(b['num']))
0 голосов
/ 02 декабря 2010

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

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

Первым делом я реализовал именованные кортежи. Я знал, как мой список узлов был пронумерован, что обеспечило хэш, необходимый для эффективности.

def findNodesInSpheres(sList,nList,nx,ny,nz):
    print "Running findNodesInSpheres"
    filteredList = []
    for a in sList:
            rad = a.radius
            minx = (a.x) - (rad) if (a.x - rad > 0) else 0
            maxx = (a.x) + (rad) if (a.x + rad < nx ) else nx
            miny = (a.y) - (rad) if (a.y - rad > 0) else 0
            maxy = (a.y) + (rad) if (a.y + rad < ny ) else ny
            minz = (a.z) - (rad) if (a.z - rad > 0) else 0
            maxz = (a.z) + (rad) if (a.z + rad < nz ) else nz
            boundingBox = set([ (i + j * (nx + 1) + k * (nx + 1) * (ny + 1)) for i in range (int(minx),int(maxx)+1)
                            for j in range (int(miny),int(maxy)+1) for k in range(int(minz),int(maxz)+1) ])

            for b in sorted(boundingBox):
                    if findRadius(a,nList[b]) <= rad:
                            filteredList.append(nList[b].num)
    return filteredList

Использование set () вместо списка обеспечило значительное ускорение. Чем больше набор данных (nx, ny, nz), тем больше ускорение.

Его все еще можно улучшить, используя реализацию дерева и декомпозицию домена, как было предложено, но на данный момент это работает.

Спасибо всем за совет!

0 голосов
/ 30 ноября 2010

(AFAICT, следующее решение алгоритмически быстрее, чем любой другой ответ, опубликованный до сих пор: приблизительно O ( N log N ) против O (N²). Предостережение: это предполагает, чтоу вас нет большого количества перекрытий между ограничивающими прямоугольниками.)

Если вам разрешено предварительно вычислять структуру индекса:

  1. Вставьте все значения min / max x вустановить и отсортировать их, создавая тем самым список вертикальных областей, охватывающих ось X.Свяжите каждую область с набором ограничивающих рамок, которые ее содержат.
  2. Повторите эту процедуру для значений min / max y, чтобы создать список горизонтальных областей, и свяжите каждую область с набором ограничивающих рамок, которые он содержит.
  3. Для каждой тестируемой точки:
    • Используйте бинарную рубку, чтобы найти горизонтальную область, содержащую координату x точки.Что вам действительно нужно, так это набор ограничительных рамок, связанных с регионом.
    • Аналогично найдите набор ограничивающих рамок, связанных с координатой y.
    • Найдите пересечение этих двухнаборы.
  4. Проверьте ограничивающие рамки в этом наборе остатков, используя Пифагор.
0 голосов
/ 30 ноября 2010

На самом деле, вы можете сохранить все это:

filteredList = [int(node['num']) for sphere in sList \
    for node in nList if findRadius(sphere,node)<=sphere['radius']]

Если расстояние от точки до шара меньше радиуса сферы, то, я думаю, мы можем сказать, что оно находится в сфере, верно?

Я полагаю, что findRadius определен как:

def findRadius(sphere,node):
    return ((node['x']-sphere['x'])**2 + \
            (node['y']-sphere['y'])**2 + \
            (node['z']-sphere['z'])**2)**.5
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...