Есть ли в python более быстрый способ найти наименьшее число в поле?

10

Использование рабочего стола arcgis 10.3.1 У меня есть скрипт, который использует курсор поиска для добавления значений в список, а затем использует min (), чтобы найти наименьшее целое число. Переменная затем используется в скрипте. Класс Feature содержит 200 000 строк, и выполнение сценария занимает очень много времени. Есть ли способ сделать это быстрее? На данный момент я думаю, что я бы просто сделал это вручную, а не написал сценарий, потому что это занимает много времени.

import arcpy
fc = arcpy.env.workspace = arcpy.GetParameterAsText(0)
Xfield = "XKoordInt"
cursor = arcpy.SearchCursor(fc)
ListVal = []
for row in cursor:
    ListVal.append(row.getValue(Xfield))
value = min(ListVal)-20
print value
expression = "(!XKoordInt!-{0})/20".format(value)
arcpy.CalculateField_management (fc, "Matrix_Z" ,expression, "PYTHON")
Роберт Бакли
источник
Я думаю, что есть более быстрый не-Python способ сделать это, над которым вы, кажется, работали на gis.stackexchange.com/q/197873/115
PolyGeo
Любая причина, почему вы не используете arcpy.Statistics_analysis? desktop.arcgis.com/en/arcmap/10.3/tools/analysis-toolbox/…
Беренд
Да. Я должен начать где-нибудь, и только очень редко приходится заниматься программированием на arcpy. Это фантастика, что так много людей могут предложить так много подходов. Это лучший способ узнать что-то новое.
Роберт Бакли,
min_val = min([i[0] for i in arcpy.da.SearchCursor(fc,Xfield)])
BERA

Ответы:

15

Я вижу несколько вещей, которые могут вызывать медленный сценарий. То, что, вероятно, очень медленно, это arcpy.CalculateField_management()функция. Вы должны использовать курсор, он будет на несколько величин быстрее. Кроме того, вы сказали, что используете ArcGIS Desktop 10.3.1, но используете старые курсоры в стиле ArcGIS 10.0, которые также намного медленнее.

Операция min () даже в списке из 200K будет довольно быстрой. Вы можете убедиться в этом, запустив этот небольшой фрагмент; это происходит в мгновение ока:

>>> min(range(200000)) # will return 0, but is still checking a list of 200,000 values very quickly

Посмотрим, будет ли это быстрее:

import arcpy
fc = arcpy.env.workspace = arcpy.GetParameterAsText(0)
Xfield = "XKoordInt"
with arcpy.da.SearchCursor(fc, [Xfield]) as rows:
    ListVal = [r[0] for r in rows]

value = min(ListVal) - 20
print value

# now update
with arcpy.da.UpdateCursor(fc, [Xfield, 'Matrix_Z']) as rows:
    for r in rows:
        if r[0] is not None:
            r[1] = (r[0] - value) / 20.0
            rows.updateRow(r)

РЕДАКТИРОВАТЬ:

Я провел несколько временных тестов, и, как я подозревал, полевой калькулятор занял почти вдвое больше времени, чем курсор нового стиля. Интересно, что курсор в старом стиле был в 3 раза медленнее, чем полевой калькулятор. Я создал 200 000 случайных точек и использовал те же имена полей.

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

Вот результаты:

Getting the values with the old style cursor: 0:00:19.23 
Getting values with the new style cursor: 0:00:02.50 
Getting values with the new style cursor + an order by sql statement: 0:00:00.02

And the calculations: 

field calculator: 0:00:14.21 
old style update cursor: 0:00:42.47 
new style cursor: 0:00:08.71

И вот код, который я использовал (разбил все на отдельные функции, чтобы использовать timeitдекоратор):

import arcpy
import datetime
import sys
import os

def timeit(function):
    """will time a function's execution time
    Required:
        function -- full namespace for a function
    Optional:
        args -- list of arguments for function
        kwargs -- keyword arguments for function
    """
    def wrapper(*args, **kwargs):
        st = datetime.datetime.now()
        output = function(*args, **kwargs)
        elapsed = str(datetime.datetime.now()-st)[:-4]
        if hasattr(function, 'im_class'):
            fname = '.'.join([function.im_class.__name__, function.__name__])
        else:
            fname = function.__name__
        print'"{0}" from {1} Complete - Elapsed time: {2}'.format(fname, sys.modules[function.__module__], elapsed)
        return output
    return wrapper

@timeit
def get_value_min_old_cur(fc, field):
    rows = arcpy.SearchCursor(fc)
    return min([r.getValue(field) for r in rows])

@timeit
def get_value_min_new_cur(fc, field):
    with arcpy.da.SearchCursor(fc, [field]) as rows:
        return min([r[0] for r in rows])

@timeit
def get_value_sql(fc, field):
    """good suggestion to use sql order by by dslamb :) """
    wc = "%s IS NOT NULL"%field
    sc = (None,'Order By %s'%field)
    with arcpy.da.SearchCursor(fc, [field]) as rows:
        for r in rows:
            # should give us the min on the first record
            return r[0]

@timeit
def test_field_calc(fc, field, expression):
    arcpy.management.CalculateField(fc, field, expression, 'PYTHON')

@timeit
def old_cursor_calc(fc, xfield, matrix_field, value):
    wc = "%s IS NOT NULL"%xfield
    rows = arcpy.UpdateCursor(fc, where_clause=wc)
    for row in rows:
        if row.getValue(xfield) is not None:

            row.setValue(matrix_field, (row.getValue(xfield) - value) / 20)
            rows.updateRow(row)

@timeit
def new_cursor_calc(fc, xfield, matrix_field, value):
    wc = "%s IS NOT NULL"%xfield
    with arcpy.da.UpdateCursor(fc, [xfield, matrix_field], where_clause=wc) as rows:
        for r in rows:
            r[1] = (r[0] - value) / 20
            rows.updateRow(r)


if __name__ == '__main__':
    Xfield = "XKoordInt"
    Mfield = 'Matrix_Z'
    fc = r'C:\Users\calebma\Documents\ArcGIS\Default.gdb\Random_Points'

    # first test the speed of getting the value
    print 'getting value tests...'
    value = get_value_min_old_cur(fc, Xfield)
    value = get_value_min_new_cur(fc, Xfield)
    value = get_value_sql(fc, Xfield)

    print '\n\nmin value is {}\n\n'.format(value)

    # now test field calculations
    expression = "(!XKoordInt!-{0})/20".format(value)
    test_field_calc(fc, Xfield, expression)
    old_cursor_calc(fc, Xfield, Mfield, value)
    new_cursor_calc(fc, Xfield, Mfield, value)

И, наконец, это то, что было распечатано с моей консоли.

>>> 
getting value tests...
"get_value_min_old_cur" from <module '__main__' from 'C:/Users/calebma/Desktop/speed_test2.py'> Complete - Elapsed time: 0:00:19.23
"get_value_min_new_cur" from <module '__main__' from 'C:/Users/calebma/Desktop/speed_test2.py'> Complete - Elapsed time: 0:00:02.50
"get_value_sql" from <module '__main__' from 'C:/Users/calebma/Desktop/speed_test2.py'> Complete - Elapsed time: 0:00:00.02


min value is 5393879


"test_field_calc" from <module '__main__' from 'C:/Users/calebma/Desktop/speed_test2.py'> Complete - Elapsed time: 0:00:14.21
"old_cursor_calc" from <module '__main__' from 'C:/Users/calebma/Desktop/speed_test2.py'> Complete - Elapsed time: 0:00:42.47
"new_cursor_calc" from <module '__main__' from 'C:/Users/calebma/Desktop/speed_test2.py'> Complete - Elapsed time: 0:00:08.71
>>> 

Редактировать 2: только что опубликовал несколько обновленных тестов, я обнаружил небольшой недостаток с моей timeitфункцией.

crmackey
источник
r [0] = (r [0] - значение) / 20,0 TypeError: неподдерживаемые типы операндов для -: 'NoneType' и 'int'
Роберт Бакли
Это просто означает, что у вас есть некоторые нулевые значения в вашем "XKoordInt". Смотрите мое редактирование, все, что вам нужно сделать, это пропустить нули.
crmackey
2
Будьте осторожны с range. ArcGIS по-прежнему использует Python 2.7, поэтому возвращает a list. Но в 3.x rangeесть свой особый вид объекта, который может иметь оптимизации. Был бы более надежный тест min(list(range(200000))), который бы гарантировал, что вы работаете с простым списком. Также рассмотрите возможность использования timeitмодуля для тестирования производительности.
jpmc26
Возможно, вы могли бы выиграть больше времени, используя наборы, а не списки. Таким образом, вы не сохраняете повторяющиеся значения, а ищете только уникальные значения.
Фезтер
@Fezter Это зависит от дистрибутива. Должно быть достаточно точных дубликатов, чтобы перевесить стоимость хеширования всех значений и проверки наличия каждого в наборе во время построения. Например, если дублируется только 1%, это, вероятно, не стоит затрат. Также обратите внимание, что если значение является плавающей точкой, вряд ли будет много точных дубликатов.
jpmc26
1

Как указывает @crmackey, медленная часть, вероятно, связана с методом вычисления поля. В качестве альтернативы другим подходящим решениям и при условии, что вы используете базу геоданных для хранения ваших данных, вы можете использовать команду Упорядочить по sql для сортировки в порядке возрастания перед выполнением курсора обновления.

start = 0
Xfield = "XKoordInt"
minValue = None
wc = "%s IS NOT NULL"%Xfield
sc = (None,'Order By %s'%Xfield)
with arcpy.da.SearchCursor(fc, [Xfield],where_clause=wc,sql_clause=sc) as uc:
    for row in uc:
        if start == 0:
            minValue = row[0]
            start +=1
        row[0] = (row[0] - value) / 20.0
        uc.updateRow(row)

В этом случае предложение where удаляет пустые значения перед выполнением запроса, или вы можете использовать другой пример, который проверяет None перед обновлением.

dslamb
источник
Ницца! Использование порядка по возрастанию и захвату первой записи определенно будет быстрее, чем получение всех значений и затем поиск min(). Я включу это в свои тесты скорости, чтобы показать прирост производительности.
crmackey
Мне было бы любопытно увидеть, где он занимает место. Я не удивлюсь, если дополнительные SQL-операции сделают это медленно.
dslamb
2
отметки времени были добавлены, см. мое редактирование. И я думаю, что вы были правы, sql, казалось, добавил некоторые дополнительные издержки, но он не справился с выполнением курсора, который шагает по всему списку за 0.56секунды, что не так сильно, как я ожидал.
crmackey
1

Вы также можете использовать Numpy в таких случаях, хотя это будет более интенсивно использовать память.

У вас все еще будет узкое место при загрузке данных в массив numpy, а затем снова в источник данных, но я обнаружил, что разница в производительности лучше (в пользу numpy) с большими источниками данных, особенно если вам нужно несколько статистика / расчеты .:

import arcpy
import numpy as np
fc = arcpy.env.workspace = arcpy.GetParameterAsText(0)
Xfield = "XKoordInt"

allvals = arcpy.da.TableToNumPyArray(fc,['OID@',Xfield])
value = allvals[Xfield].min() - 20

print value

newval = np.zeros(allvals.shape,dtype=[('id',int),('Matrix_Z',int)])
newval['id'] = allvals['OID@']
newval['Matrix_Z'] = (allvals[Xfield] - value) / 20

arcpy.da.ExtendTable(fc,'OBJECTID',newval,'id',False)
Злой гений
источник
1

Почему бы не отсортировать таблицу по возрастанию, а затем использовать курсор поиска, чтобы получить значение для первой строки? http://pro.arcgis.com/en/pro-app/tool-reference/data-management/sort.htm

import arcpy
workspace = r'workspace\file\path'
arcpy.env.workspace = workspace

input = "input_data"
sort_table = "sort_table"
sort_field = "your field"

arcpy.Sort_management (input, sort_table, sort_field)

min_value = 0

count= 0
witha arcpy.da.SearchCursor(input, [sort_field]) as cursor:
    for row in cursor:
        count +=1
        if count == 1: min_value +=row[0]
        else: break
del cursor
CRLD
источник
1

Я бы обернул SearchCursorв выражение генератора (то есть min()) для скорости и краткости. Затем включите минимальное значение из выражения генератора в daтип UpdateCursor. Что-то вроде следующего:

import arcpy

fc = r'C:\path\to\your\geodatabase.gdb\feature_class'

minimum_value = min(row[0] for row in arcpy.da.SearchCursor(fc, 'some_field')) # Generator expression

with arcpy.da.UpdateCursor(fc, ['some_field2', 'some_field3']) as cursor:
    for row in cursor:
        row[1] = (row[0] - (minimum_value - 20)) / 20 # Perform the calculation
        cursor.updateRow(row)
Аарон
источник
Не должны ли SearchCursorбыть закрыты, когда вы закончите с этим?
jpmc26
1
@ jpmc26 Курсор может быть освобожден после завершения курсора. Источник (курсоры и блокировки): pro.arcgis.com/en/pro-app/arcpy/get-started/… . Другой пример от Esri (см. Пример 2): pro.arcgis.com/en/pro-app/arcpy/data-access/…
Аарон
0

В вашем цикле у вас есть две ссылки на функции, которые переоцениваются для каждой итерации.

for row in cursor: ListVal.append(row.getValue(Xfield))

Должно быть быстрее (но немного сложнее) иметь ссылки вне цикла:

getvalue = row.getValue
append = ListVal.append

for row in cursor:
    append(getvalue(Xfield))
штейн
источник
Не замедлит ли это? Вы фактически создаете новую отдельную ссылку для встроенного append()метода типа listданных. Я не думаю, что именно здесь происходит его узкое место, я бы поспорил, что функция вычисления поля является виновником. Это можно проверить, синхронизировав калькулятор поля с курсором нового стиля.
crmackey
1
на самом деле меня также интересуют временные характеристики :) Но это легко заменить в исходном коде и поэтому быстро проверяется.
Матовый
Я знаю, что некоторое время назад провел несколько тестов производительности на курсорах против полевого калькулятора. Я сделаю еще один тест и сообщу о своих выводах в своем ответе. Я думаю, что было бы также хорошо показать старые и новые скорости курсора.
crmackey