Esiste un modo più veloce in Python per trovare il numero più piccolo in un campo?


10

Utilizzando arcgis desktop 10.3.1 Ho uno script che utilizza un cursore di ricerca per aggiungere valori a un elenco e quindi utilizzare min () per trovare il numero intero più piccolo. La variabile viene quindi utilizzata in uno script. La classe Feature ha 200.000 righe e il completamento dello script richiede molto tempo. C'è un modo per farlo più velocemente? Al momento penso che lo farei solo a mano piuttosto che scrivere una sceneggiatura a causa del tempo impiegato.

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

Penso che ci sia un modo no-Python più veloce per farlo che sembra stia lavorando a gis.stackexchange.com/q/197873/115
PolyGeo

Qualche motivo per cui non stai usando arcpy.Statistics_analysis? desktop.arcgis.com/en/arcmap/10.3/tools/analysis-toolbox/…
Berend

Sì. Devo iniziare da qualche parte e solo raramente devo fare alcuna programmazione con arcpy. È fantastico che così tante persone siano in grado di suggerire così tanti approcci. Questo è il modo migliore per imparare cose nuove.
Robert Buckley,

min_val = min([i[0] for i in arcpy.da.SearchCursor(fc,Xfield)])
BERA,

Risposte:


15

Vedo diverse cose che potrebbero causare un rallentamento della sceneggiatura. La cosa che probabilmente è molto lenta è la arcpy.CalculateField_management()funzione. Dovresti usare un cursore, sarà più veloce di parecchie magnitudini. Inoltre, hai detto che stai usando ArcGIS Desktop 10.3.1, ma stai usando i vecchi cursori in stile ArcGIS 10.0, che sono anche molto più lenti.

L'operazione min () anche su un elenco di 200K sarà piuttosto rapida. Puoi verificarlo eseguendo questo piccolo frammento; succede in un batter d'occhio:

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

Vedi se è più veloce:

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)

MODIFICARE:

Ho eseguito alcuni test di temporizzazione e, come sospettavo, il calcolatore di campo ha impiegato quasi il doppio del cursore del nuovo stile. È interessante notare che il cursore vecchio stile era ~ 3 volte più lento del calcolatore di campo. Ho creato 200.000 punti casuali e ho usato gli stessi nomi di campo.

Una funzione decoratore è stata utilizzata per cronometrare ciascuna funzione (potrebbe essere un leggero sovraccarico nell'impostazione e nello smantellamento delle funzioni, quindi forse il modulo timeit sarebbe un po 'più accurato per testare gli snippet).

Ecco i risultati:

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

Ed ecco il codice che ho usato (ho suddiviso tutto in singole funzioni per usare il timeitdecoratore):

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)

E infine, questo è ciò che la stampa reale è stata dalla mia console.

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

Modifica 2: Appena pubblicato alcuni test aggiornati, ho riscontrato un leggero difetto con la mia timeitfunzione.


r [0] = (r [0] - valore) / 20.0 TypeError: tipi di operando non supportati per -: 'NoneType' e 'int'
Robert Buckley

Ciò significa solo che hai alcuni valori null nel tuo "XKoordInt". Vedi la mia modifica, tutto ciò che devi fare è saltare i null.
Crmackey,

2
Stai attento con range. ArcGIS utilizza ancora Python 2.7, quindi restituisce a list. Ma in 3.x, rangeè il suo tipo speciale di oggetto che può avere ottimizzazioni. Un test più affidabile sarebbe min(list(range(200000))), che assicurerebbe che stai lavorando con un semplice elenco. Prendi anche in considerazione l'utilizzo del timeitmodulo per i test delle prestazioni.
jpmc26,

Probabilmente potresti guadagnare un po 'più di tempo usando set piuttosto che liste. In questo modo non stai memorizzando valori duplicati e stai cercando solo valori univoci.
Fezter

@Fezter Dipende dalla distribuzione. Dovrebbero esserci abbastanza duplicati esatti per superare i costi di hashing di tutti i valori e verificare se ognuno è nel set durante la costruzione. Ad esempio, se viene duplicato solo l'1%, probabilmente non vale il costo. Si noti inoltre che se il valore è in virgola mobile, è improbabile che vi siano molti duplicati esatti.
jpmc26,

1

Come sottolinea @crmackey, la parte lenta è probabilmente dovuta al metodo del campo di calcolo. In alternativa alle altre soluzioni adatte e supponendo che si stia utilizzando un geodatabase per archiviare i dati, è possibile utilizzare il comando Ordina per sql per ordinare in ordine crescente prima di eseguire il cursore di aggiornamento.

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)

In questo caso la clausola where rimuove i null prima di eseguire la query, oppure è possibile utilizzare l'altro esempio che verifica la presenza di None prima dell'aggiornamento.


Bello! Usando l'ordine come crescente e afferrando il primo record sarà sicuramente più veloce che ottenere tutti i valori e quindi trovare il min(). Lo includerò anche nei miei test di velocità per mostrare il miglioramento delle prestazioni.
Crmackey,

Sarò curioso di vedere dove si colloca. Non sarei sorpreso se le operazioni sql extra lo rallentassero.
dslamb,

2
sono stati aggiunti benchmark di temporizzazione, vedere la mia modifica. E penso che tu abbia ragione, l'sql ha aggiunto un po 'di sovraccarico in più, ma ha eseguito il cursore che percorre l'intero elenco in 0.56pochi secondi, il che non è un aumento delle prestazioni come mi sarei aspettato.
Crmackey,

1

Puoi anche usare numpy in casi come questo, anche se richiederà più memoria.

Otterrai comunque un collo di bottiglia quando caricherai i dati su un array numpy e poi di nuovo sull'origine dati, ma ho scoperto che la differenza di prestazioni è migliore (a favore di numpy) con origini dati più grandi, specialmente se hai bisogno di più statistiche / calcoli .:

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

Perché non ordinare la tabella in ordine crescente, quindi utilizzare un cursore di ricerca per afferrare il valore per la prima riga? 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

Vorrei racchiudere l' espressione diSearchCursor un generatore (cioè min()) sia per la velocità che per la sintonia. Quindi incorporare il valore minimo dall'espressione del generatore in un datipo UpdateCursor. Qualcosa di simile al seguente:

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)

Non dovrebbe SearchCursoressere chiuso quando hai finito con esso?
jpmc26,

1
@ jpmc26 Un cursore può essere rilasciato al completamento del cursore. Fonte (cursori e blocco): pro.arcgis.com/en/pro-app/arcpy/get-started/… . Un altro esempio di Esri (vedi esempio 2): pro.arcgis.com/en/pro-app/arcpy/data-access/…
Aaron

0

Nel tuo ciclo hai due riferimenti di funzione che vengono rivalutati per ogni iterazione.

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

Dovrebbe essere più veloce (ma un po 'più complesso) avere i riferimenti al di fuori del ciclo:

getvalue = row.getValue
append = ListVal.append

for row in cursor:
    append(getvalue(Xfield))

Non lo rallenterebbe davvero? In realtà stai creando un nuovo riferimento separato per il append()metodo incorporato del listtipo di dati. Non penso che sia qui che sta avvenendo il suo collo di bottiglia, scommetterei che il colpevole è la funzione di calcolo del campo. Ciò può essere verificato tempificando il calcolatore di campo rispetto a un nuovo cursore di stile.
Crmackey,

1
in realtà sarei interessato anche ai tempi :) Ma è un facile rimpiazzo nel codice originale e quindi controllato velocemente.
Matte,

So di aver fatto alcuni test di riferimento qualche tempo fa su cursori vs calcolatrice di campo. Farò un altro test e riporterò i miei risultati nella mia risposta. Penso che sarebbe anche bello mostrare anche la velocità del cursore vecchio vs nuovo.
Crmackey,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.