Czy istnieje szybszy sposób na znalezienie najmniejszej liczby w pythonie?

10

Korzystanie z Arcgis Desktop 10.3.1 Mam skrypt, który używa kursora wyszukiwania, aby dołączyć wartości do listy, a następnie użyj min (), aby znaleźć najmniejszą liczbę całkowitą. Zmienna jest następnie używana w skrypcie. Klasa Feature ma 200 000 wierszy, a wykonanie skryptu zajmuje bardzo dużo czasu. Czy istnieje sposób, aby to zrobić szybciej? W tej chwili myślę, że po prostu zrobiłbym to ręcznie, zamiast pisać scenariusz ze względu na czas, jaki zajmuje.

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")
Robert Buckley
źródło
Wydaje
PolyGeo
Z jakichś powodów nie korzystasz arcpy.Statistics_analysis? desktop.arcgis.com/en/arcmap/10.3/tools/analysis-toolbox/…
Berend
Tak. Muszę gdzieś zacząć i bardzo rzadko muszę programować przy pomocy arcpy. To fantastyczne, że tak wiele osób jest w stanie zaproponować tak wiele podejść. To najlepszy sposób na naukę nowych rzeczy.
Robert Buckley
min_val = min([i[0] for i in arcpy.da.SearchCursor(fc,Xfield)])
BERA

Odpowiedzi:

15

Widzę kilka rzeczy, które mogą powodować spowolnienie skryptu. To, co prawdopodobnie jest bardzo wolne, to arcpy.CalculateField_management()funkcja. Powinieneś użyć kursora, będzie on o kilka wielkości szybszy. Powiedziałeś także, że używasz ArcGIS Desktop 10.3.1, ale używasz starych kursorów ArcGIS 10.0, które również są znacznie wolniejsze.

Operacja min () nawet na liście 200K będzie dość szybka. Możesz to sprawdzić, uruchamiając ten mały fragment kodu; dzieje się to w mgnieniu oka:

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

Sprawdź, czy jest to szybsze:

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)

EDYTOWAĆ:

Przeprowadziłem testy czasowe i, jak podejrzewałem, kalkulator pola zajął prawie dwa razy więcej czasu niż kursor nowego stylu. Co ciekawe, kursor w starym stylu był ~ 3 razy wolniejszy niż kalkulator polowy. Utworzyłem 200 000 losowych punktów i użyłem tych samych nazw pól.

Do odmierzania czasu każdej funkcji użyto funkcji dekoratora (może to być niewielki narzut w konfiguracji i zrywaniu funkcji, więc może moduł timeit byłby nieco bardziej dokładny do testowania fragmentów).

Oto wyniki:

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

A oto kod, którego użyłem (rozbiłem wszystko na poszczególne funkcje, aby użyć timeitdekoratora):

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)

I wreszcie taki właśnie był wydruk z mojej konsoli.

>>> 
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
>>> 

Edycja 2: Właśnie opublikowałem kilka zaktualizowanych testów, zauważyłem niewielką wadę mojej timeitfunkcji.

Crmackey
źródło
r [0] = (r [0] - wartość) / 20.0 TypeError: nieobsługiwane typy operandów dla -: „NoneType” i „int”
Robert Buckley,
To tylko oznacza, że ​​masz w sobie jakieś wartości zerowe "XKoordInt". Zobacz moją edycję, wszystko, co musisz zrobić, to pominąć wartości null.
crmackey,
2
Ostrożnie z range. ArcGIS nadal używa Python 2.7, więc zwraca a list. Ale w wersji 3.x rangejest to własny specjalny obiekt, który może być zoptymalizowany. Byłby bardziej wiarygodny test min(list(range(200000))), który zapewniłby pracę z prostą listą. Rozważ także użycie timeitmodułu do testowania wydajności.
jpmc26
Prawdopodobnie możesz zyskać więcej czasu, używając zestawów niż list. W ten sposób nie przechowujesz zduplikowanych wartości i szukasz tylko unikatowych wartości.
Fezter
@ Fezter To zależy od dystrybucji. Musi istnieć wystarczająca ilość dokładnych duplikatów, aby przeważyć koszt haszowania wszystkich wartości i sprawdzenia, czy każda z nich jest w zestawie podczas budowy. Na przykład, jeśli zduplikowany jest tylko 1%, prawdopodobnie nie jest to warte kosztu. Należy również pamiętać, że jeśli wartość jest zmiennoprzecinkowa, prawdopodobnie nie będzie wielu dokładnych duplikatów.
jpmc26
1

Jak wskazuje @crmackey, wolna część jest prawdopodobnie spowodowana metodą obliczania pola. Jako alternatywę dla innych odpowiednich rozwiązań i zakładając, że używasz geobazy do przechowywania danych, możesz użyć polecenia Sortuj według sql, aby posortować w porządku rosnącym przed wykonaniem kursora aktualizacji.

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)

W takim przypadku klauzula where usuwa wartości zerowe przed wykonaniem zapytania lub możesz użyć innego przykładu, który sprawdza przed aktualizacją brak.

dslamb
źródło
Miły! Używanie kolejności jako wstępującej i pobierając pierwszy rekord będzie zdecydowanie szybsze niż uzyskanie wszystkich wartości, a następnie znalezienie min(). Uwzględnię to również w moich testach prędkości, aby pokazać wzrost wydajności.
crmackey,
Będę ciekawy, gdzie się znajduje. Nie zdziwiłbym się, gdyby dodatkowe operacje sql spowolniły.
dslamb
2
Dodano testy porównawcze pomiaru czasu, patrz moja edycja. I myślę, że miałeś rację, sql wydawało się dodać trochę narzutu, ale wykonał kursor, który przechodzi przez całą listę o 0.56kilka sekund, co nie jest tak dużym wzrostem wydajności, jak bym się spodziewał.
crmackey,
1

Możesz także użyć numpy w takich przypadkach, chociaż będzie to wymagało więcej pamięci.

Nadal dostaniesz przewagę podczas ładowania danych do tablicy numpy, a następnie z powrotem do źródła danych, ale zauważyłem, że różnica wydajności jest lepsza (na korzyść numpy) przy większych źródłach danych, szczególnie jeśli potrzebujesz wielu statystyki / obliczenia .:

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)
Zły geniusz
źródło
1

Dlaczego nie posortować tabeli rosnąco, a następnie użyć kursora wyszukiwania, aby pobrać wartość dla pierwszego wiersza? 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
źródło
1

Chciałbym zawrzeć SearchCursorw wyrażeniu generatora (tj. min()) Zarówno szybkość, jak i zwięzłość. Następnie włącz minimalną wartość z wyrażenia generatora do datypu UpdateCursor. Coś w stylu:

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)
Aaron
źródło
Czy nie powinno SearchCursorsię zamykać, kiedy z tym skończysz?
jpmc26
1
@ jpmc26 Kursor można zwolnić po zakończeniu kursora. Źródło (Kursory i blokowanie): pro.arcgis.com/en/pro-app/arcpy/get-started/… . Kolejny przykład z Esri (patrz przykład 2): pro.arcgis.com/en/pro-app/arcpy/data-access/…
Aaron
0

W swojej pętli masz dwa odwołania do funkcji, które są przeszacowane dla każdej iteracji.

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

Powinno być szybsze (ale nieco bardziej złożone), aby mieć odwołania poza pętlą:

getvalue = row.getValue
append = ListVal.append

for row in cursor:
    append(getvalue(Xfield))
Matowy
źródło
Czy to nie spowolniłoby? W rzeczywistości tworzysz nowe oddzielne odwołanie do wbudowanej append()metody listtypu danych. Nie sądzę, że tutaj właśnie dzieje się jego wąskie gardło, postawiłbym pieniądze, że winowajcą jest funkcja obliczania pola. Można to zweryfikować, mierząc czas kalkulatora pola względem nowego kursora stylu.
crmackey
1
tak naprawdę byłbym zainteresowany również czasami :) Ale to jest łatwa zamiana w oryginalny kod i dlatego szybko sprawdzona.
Matte
Wiem, że jakiś czas temu testowałem testy porównawcze kursorów vs kalkulator pola. Zrobię kolejny test i przedstawię moje odkrycia w mojej odpowiedzi. Myślę, że dobrze byłoby też pokazać szybkość kursora starą i nową.
crmackey,