Existe-t-il un moyen plus rapide en python de trouver le plus petit nombre dans un champ?


10

Utilisation d'arcgis desktop 10.3.1 J'ai un script qui utilise un curseur de recherche pour ajouter des valeurs à une liste, puis utilise min () pour trouver le plus petit entier. La variable est ensuite utilisée dans un script. La classe d'entités comporte 200 000 lignes et le script prend beaucoup de temps. Y a-t-il un moyen de le faire plus rapidement? Pour le moment, je pense que je le ferais à la main plutôt que d'écrire un script en raison du temps qu'il faut.

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

Je pense qu'il y a un moyen plus rapide sans Python de le faire sur lequel vous sembliez
PolyGeo

Une raison pour laquelle vous n'utilisez pas arcpy.Statistics_analysis? desktop.arcgis.com/en/arcmap/10.3/tools/analysis-toolbox/…
Berend

Oui. Je dois commencer quelque part et ne dois que très rarement faire de la programmation avec arcpy. C'est fantastique que tant de personnes puissent proposer autant d'approches. C'est la meilleure façon d'apprendre de nouvelles choses.
Robert Buckley

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

Réponses:


15

Je peux voir plusieurs choses qui peuvent ralentir votre script. La chose qui est probablement très lente est la arcpy.CalculateField_management()fonction. Vous devez utiliser un curseur, il sera plus rapide de plusieurs magnitudes. De plus, vous avez dit que vous utilisez ArcGIS Desktop 10.3.1, mais que vous utilisez les anciens curseurs de style ArcGIS 10.0, qui sont également beaucoup plus lents.

L'opération min () même sur une liste de 200K sera assez rapide. Vous pouvez le vérifier en exécutant ce petit extrait; cela se passe en un clin d'œil:

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

Voyez si c'est plus rapide:

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)

ÉDITER:

J'ai effectué des tests de synchronisation et, comme je le soupçonnais, la calculatrice de terrain a pris presque deux fois plus de temps que le nouveau curseur de style. Fait intéressant, le curseur à l'ancienne était ~ 3 fois plus lent que la calculatrice de terrain. J'ai créé 200 000 points aléatoires et utilisé les mêmes noms de champs.

Une fonction décoratrice a été utilisée pour chronométrer chaque fonction (il peut y avoir une légère surcharge dans la configuration et le démontage des fonctions, donc peut-être que le module timeit serait un peu plus précis pour tester les extraits).

Voici les résultats:

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

Et voici le code que j'ai utilisé (tout décomposé en fonctions individuelles pour utiliser le timeitdécorateur):

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)

Et enfin, c'est ce que l'impression réelle était de ma 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
>>> 

Edit 2: Je viens de publier des tests mis à jour, j'ai trouvé un léger défaut avec ma timeitfonction.


r [0] = (r [0] - valeur) / 20.0 TypeError: type (s) d'opérande non pris en charge pour -: 'NoneType' et 'int'
Robert Buckley

Cela signifie simplement que vous avez des valeurs nulles dans votre fichier "XKoordInt". Voir mon montage, tout ce que vous avez à faire est de sauter les null.
crmackey

2
Soyez prudent avec range. ArcGIS utilise toujours Python 2.7, il renvoie donc a list. Mais dans 3.x, rangec'est son propre type spécial d'objet qui peut avoir des optimisations. Un test plus fiable serait min(list(range(200000))), ce qui garantirait que vous travaillez avec une liste simple. Pensez également à utiliser le timeitmodule pour les tests de performances.
jpmc26

Vous pourriez probablement gagner un peu plus de temps en utilisant des ensembles plutôt que des listes. De cette façon, vous ne stockez pas de valeurs en double et vous recherchez uniquement des valeurs uniques.
Fezter

@Fezter Cela dépend de la distribution. Il devrait y avoir suffisamment de doublons exacts pour l'emporter sur le coût de hachage de toutes les valeurs et de vérifier si chacune est dans l'ensemble pendant la construction. Par exemple, si seulement 1% est dupliqué, cela ne vaut probablement pas le coût. Notez également que si la valeur est en virgule flottante, il est peu probable qu'il y ait de nombreux doublons exacts.
jpmc26

1

Comme le souligne @crmackey, la partie lente est probablement due à la méthode de calcul du champ. Comme alternative aux autres solutions appropriées, et en supposant que vous utilisez une géodatabase pour stocker vos données, vous pouvez utiliser la commande Trier par sql pour trier par ordre croissant avant d'effectuer le curseur de mise à jour.

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)

Dans ce cas, la clause where supprime les valeurs NULL avant d'effectuer la requête, ou vous pouvez utiliser l'autre exemple qui vérifie Aucun avant la mise à jour.


Agréable! Utiliser l'ordre en remontant et en saisissant le premier enregistrement sera certainement plus rapide que d'obtenir toutes les valeurs et de trouver ensuite le min(). Je vais également inclure cela dans mes tests de vitesse pour montrer le gain de performances.
crmackey

Je serai curieux de voir où il se classe. Je ne serais pas surpris si les opérations SQL supplémentaires le ralentissaient.
dslamb

2
des repères de synchronisation ont été ajoutés, voir ma modification. Et je pense que vous aviez raison, le sql semblait ajouter une surcharge supplémentaire, mais il a dépassé le curseur qui parcourt toute la liste en 0.56quelques secondes, ce qui n'est pas autant un gain de performances que je l'aurais prévu.
crmackey

1

Vous pouvez également utiliser numpy dans des cas comme celui-ci, bien qu'il soit plus gourmand en mémoire.

Vous obtiendrez toujours un goulot d'étranglement lors du chargement des données dans un tableau numpy, puis à nouveau dans la source de données, mais j'ai constaté que la différence de performances est meilleure (en faveur de numpy) avec des sources de données plus importantes, surtout si vous avez besoin de plusieurs statistiques / calculs .:

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

Pourquoi ne pas trier le tableau par ordre croissant, puis utiliser un curseur de recherche pour saisir la valeur de la première ligne? 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

Je voudrais envelopper l' expressionSearchCursor dans un générateur (c.-à-d. min()) Pour la vitesse et la concision. Incorporez ensuite la valeur minimale de l'expression du générateur dans un datype UpdateCursor. Quelque chose comme ceci:

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)

Ne devrait-il pas SearchCursorêtre fermé lorsque vous en avez terminé?
jpmc26

1
@ jpmc26 Un curseur peut être libéré à la fin du curseur. Source (curseurs et verrouillage): pro.arcgis.com/en/pro-app/arcpy/get-started/… . Un autre exemple d'Esri (voir exemple 2): pro.arcgis.com/en/pro-app/arcpy/data-access/…
Aaron

0

Dans votre boucle, vous avez deux références de fonction qui sont réévaluées pour chaque itération.

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

Il devrait être plus rapide (mais un peu plus complexe) d'avoir les références en dehors de la boucle:

getvalue = row.getValue
append = ListVal.append

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

Cela ne ralentirait-il pas réellement? Vous créez en fait une nouvelle référence distincte pour la append()méthode intégrée du listtype de données. Je ne pense pas que c'est là que son goulot d'étranglement se produit, je parierais que la fonction de calcul sur le terrain est le coupable. Cela peut être vérifié en synchronisant la calculatrice de champ avec un nouveau curseur de style.
crmackey

1
en fait, je serais également intéressé par les horaires :) Mais c'est un remplacement facile dans le code d'origine et donc rapide vérifié.
Mat

Je sais que j'ai fait quelques tests de référence il y a quelque temps sur les curseurs vs la calculatrice de terrain. Je ferai un autre test et rapporterai mes résultats dans ma réponse. Je pense qu'il serait également bon d'afficher la vitesse du curseur par rapport à l'ancienne.
crmackey
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.