Чи є більш швидкий спосіб у пітоні знайти найменше число у полі?


10

Використання arcgis desktop 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)])
БЕРА

Відповіді:


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функції.


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, повільна частина, ймовірно, пов'язана з методом обчислення поля. Як альтернатива іншим підходящим рішенням, і припускаючи, що ви використовуєте базу даних геоданих для зберігання своїх даних, ви можете використовувати команду Order By 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)

У цьому випадку пункт де видаляє нулі перед тим, як робити запит, або ви можете використовувати інший приклад, який перевіряє наявність "Ні" перед оновленням.


Приємно! Використання замовлення у порядку зростання та захоплення першого запису, безумовно, буде швидше, ніж отримання всіх значень, а потім знаходження min(). Я включу це в свої тести на швидкість, щоб показати підвищення продуктивності.
крмейк

Мені буде цікаво побачити, де він займає місце. Я не здивуюсь, якщо додаткові операції sql роблять це повільно.
dslamb

2
Додано орієнтири хронометражу, див. мою редагування. І я думаю, що ви були правильні, sql, здавалося, додав додаткових накладних витрат, але він виконав курсор, який перебирає весь список за 0.56секунди, що не так багато підвищення продуктивності, як я б очікував.
crmackey

1

Ви також можете використовувати 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

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.