¿Hay una manera más rápida en Python de encontrar el número más pequeño en un campo?

10

Usando arcgis desktop 10.3.1 Tengo un script que usa un cursor de búsqueda para agregar valores a una lista y luego uso min () para encontrar el entero más pequeño. La variable se usa luego en un script. La clase de entidad tiene 200,000 filas y el script tarda mucho tiempo en completarse. ¿Hay alguna manera de hacer esto más rápido? Por el momento, creo que lo haría a mano en lugar de escribir un guión debido al tiempo que lleva.

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
fuente
Creo que hay una manera más rápida sin Python de hacer esto en la que parecía estar trabajando en gis.stackexchange.com/q/197873/115
PolyGeo
¿Alguna razón por la que no estás usando arcpy.Statistics_analysis? desktop.arcgis.com/en/arcmap/10.3/tools/analysis-toolbox/…
Berend
Si. Tengo que comenzar en algún lugar y muy rara vez tengo que programar con arcpy. Es fantástico que tanta gente pueda sugerir tantos enfoques. Esta es la mejor manera de aprender cosas nuevas.
Robert Buckley
min_val = min([i[0] for i in arcpy.da.SearchCursor(fc,Xfield)])
BERA

Respuestas:

15

Puedo ver varias cosas que pueden estar causando que su script sea lento. Lo que probablemente sea muy lento es la arcpy.CalculateField_management()función. Debe usar un cursor, lo hará varias magnitudes más rápido. Además, dijo que está utilizando ArcGIS Desktop 10.3.1, pero está utilizando los antiguos cursores de estilo ArcGIS 10.0, que también son mucho más lentos.

La operación min () incluso en una lista de 200K será bastante rápida. Puede verificar esto ejecutando este pequeño fragmento; sucede en un abrir y cerrar de ojos:

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

Vea si esto es más rápido:

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)

EDITAR:

Realicé algunas pruebas de tiempo y, como sospechaba, la calculadora de campo tardó casi el doble de tiempo que el nuevo cursor de estilo. Curiosamente, el cursor de estilo antiguo era ~ 3 veces más lento que la calculadora de campo. Creé 200,000 puntos aleatorios y usé los mismos nombres de campo.

Se usó una función de decorador para cronometrar cada función (puede haber un poco de sobrecarga en la configuración y desmantelamiento de las funciones, por lo que tal vez el módulo timeit sería un poco más preciso para probar fragmentos).

Aquí están los resultados:

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

Y aquí está el código que usé (desglosé todo en funciones individuales para usar el timeitdecorador):

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)

Y finalmente, esta es la impresión real de mi consola.

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

Edición 2: Acabo de publicar algunas pruebas actualizadas, encontré un pequeño defecto con mi timeitfunción.

crmackey
fuente
r [0] = (r [0] - valor) / 20.0 TypeError: tipo (s) de operando no admitidos para:: 'NoneType' e 'int'
Robert Buckley
Eso solo significa que tienes algunos valores nulos en tu "XKoordInt". Vea mi edición, todo lo que tiene que hacer es omitir los valores nulos.
crmackey
2
Tenga cuidado con el range. ArcGIS todavía usa Python 2.7, por lo que devuelve a list. Pero en 3.x, rangees su propio tipo especial de objeto que puede tener optimizaciones. Sería una prueba más confiable min(list(range(200000))), que aseguraría que está trabajando con una lista simple. También considere usar el timeitmódulo para pruebas de rendimiento.
jpmc26
Probablemente podría ganar más tiempo usando conjuntos en lugar de listas. De esa manera no está almacenando valores duplicados, y está buscando solo valores únicos.
Fezter
@Fezter Depende de la distribución. Tendría que haber suficientes duplicados exactos para superar el costo de cifrar todos los valores y verificar si cada uno está en el conjunto durante la construcción. Por ejemplo, si solo se duplica el 1%, probablemente no valga la pena el costo. También tenga en cuenta que si el valor es punto flotante, es poco probable que haya muchos duplicados exactos.
jpmc26
1

Como señala @crmackey, la parte lenta probablemente se deba al método de cálculo del campo. Como alternativa a las otras soluciones adecuadas, y suponiendo que esté utilizando una geodatabase para almacenar sus datos, puede usar el comando Ordenar por sql para ordenar en orden ascendente antes de hacer el cursor de actualización.

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)

En este caso, la cláusula where elimina los valores nulos antes de realizar la consulta, o puede usar el otro ejemplo que verifica Ninguno antes de actualizar.

dslamb
fuente
¡Agradable! Usar el orden como ascendente y tomar el primer registro definitivamente será más rápido que obtener todos los valores y luego encontrar el min(). Incluiré esto en mis pruebas de velocidad también para mostrar la ganancia de rendimiento.
crmackey
Tendré curiosidad por ver dónde se ubica. No me sorprendería si las operaciones sql adicionales lo hacen lento.
dslamb
2
se han agregado puntos de referencia de tiempo, vea mi edición. Y creo que estabas en lo correcto, el sql parecía agregar una sobrecarga adicional, pero superó el cursor que recorre toda la lista por 0.56segundos, lo que no es una ganancia de rendimiento tan grande como esperaba.
crmackey
1

También puede usar numpy en casos como este, aunque requerirá más memoria.

Todavía obtendrá un cuello de botella cuando cargue los datos en una matriz numpy y luego vuelva a la fuente de datos nuevamente, pero descubrí que la diferencia de rendimiento es mejor (a favor de numpy) con fuentes de datos más grandes, especialmente si necesita múltiples estadísticas / cálculos .:

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)
Genio malvado
fuente
1

¿Por qué no ordenar la tabla de forma ascendente y luego usar un cursor de búsqueda para obtener el valor de la primera fila? 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
fuente
1

Lo envolvería SearchCursoren una expresión generadora (es decir min()) para la velocidad y la concisión. Luego incorpore el valor mínimo de la expresión del generador en un datipo UpdateCursor. Algo como lo siguiente:

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
fuente
¿No SearchCursordebería cerrarse cuando hayas terminado?
jpmc26
1
@ jpmc26 Se puede liberar un cursor al completar el cursor. Fuente (cursores y bloqueo): pro.arcgis.com/en/pro-app/arcpy/get-started/… . Otro ejemplo de Esri (ver ejemplo 2): pro.arcgis.com/en/pro-app/arcpy/data-access/…
Aaron
0

En su bucle, tiene dos referencias de función que se vuelven a evaluar para cada iteración.

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

Debería ser más rápido (pero un poco más complejo) tener las referencias fuera del ciclo:

getvalue = row.getValue
append = ListVal.append

for row in cursor:
    append(getvalue(Xfield))
Mate
fuente
¿No sería esto realmente más lento? En realidad, está creando una nueva referencia separada para el append()método integrado del listtipo de datos. No creo que sea aquí donde está ocurriendo su cuello de botella, apostaría dinero, la función de campo de cálculo es la culpable. Esto se puede verificar cronometrando la calculadora de campo frente a un nuevo cursor de estilo.
crmackey
1
de hecho, también me interesarían los tiempos :) Pero es un reemplazo fácil en el código original y, por lo tanto, se verifica rápidamente.
Mate
Sé que hice algunas pruebas de referencia hace un tiempo en cursores vs calculadora de campo. Haré otra prueba e informaré mis hallazgos en mi respuesta. Creo que también sería bueno mostrar la velocidad del cursor antigua frente a la nueva.
crmackey