Gibt es in Python einen schnelleren Weg, um die kleinste Zahl in einem Feld zu finden?


10

Verwenden von arcgis desktop 10.3.1 Ich habe ein Skript, das einen Suchcursor verwendet, um Werte an eine Liste anzuhängen, und dann min () verwendet, um die kleinste Ganzzahl zu finden. Die Variable wird dann in einem Skript verwendet. Die Feature-Class hat 200.000 Zeilen und die Fertigstellung des Skripts dauert sehr lange. Gibt es eine Möglichkeit, dies schneller zu tun? Im Moment denke ich, ich würde es einfach von Hand machen, anstatt ein Skript zu schreiben, da es so lange dauert.

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

Ich denke, es gibt eine schnellere No-Python-Methode, an der Sie anscheinend unter gis.stackexchange.com/q/197873/115
PolyGeo

Gibt es einen Grund, warum Sie nicht verwenden arcpy.Statistics_analysis? desktop.arcgis.com/de/arcmap/10.3/tools/analysis-toolbox/…
Berend

Ja. Ich muss irgendwo anfangen und muss nur sehr selten mit arcpy programmieren. Es ist fantastisch, dass so viele Menschen so viele Ansätze vorschlagen können. Dies ist der beste Weg, um neue Dinge zu lernen.
Robert Buckley

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

Antworten:


15

Ich kann verschiedene Dinge sehen, die dazu führen können, dass Ihr Skript langsam ist. Die Sache, die wahrscheinlich sehr langsam ist, ist die arcpy.CalculateField_management()Funktion. Sie sollten einen Cursor verwenden, er wird um einige Größenordnungen schneller. Sie sagten auch, dass Sie ArcGIS Desktop 10.3.1 verwenden, aber Sie verwenden die alten Cursor im ArcGIS 10.0-Stil, die ebenfalls viel langsamer sind.

Die min () -Operation selbst auf einer Liste von 200 KB ist ziemlich schnell. Sie können dies überprüfen, indem Sie dieses kleine Snippet ausführen. es passiert im Handumdrehen:

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

Sehen Sie, ob dies schneller geht:

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)

BEARBEITEN:

Ich habe einige Timing-Tests durchgeführt und wie ich vermutet habe, hat der Feldrechner fast doppelt so lange gedauert wie der neue Stilcursor. Interessanterweise war der Cursor im alten Stil ~ 3x langsamer als der Feldrechner. Ich habe 200.000 zufällige Punkte erstellt und dieselben Feldnamen verwendet.

Eine Dekorationsfunktion wurde verwendet, um jede Funktion zeitlich zu steuern (dies kann zu einem geringfügigen Aufwand beim Einrichten und Herunterfahren von Funktionen führen, sodass das Timeit- Modul möglicherweise etwas genauer ist, um Snippets zu testen).

Hier sind die Ergebnisse:

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

Und hier ist der Code, den ich verwendet habe (alles in einzelne Funktionen unterteilt, um den timeitDekorator zu verwenden):

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)

Und schließlich war dies der eigentliche Ausdruck von meiner Konsole.

>>> 
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: Ich habe gerade einige aktualisierte Tests gepostet und einen kleinen Fehler in meiner timeitFunktion festgestellt .


r [0] = (r [0] - Wert) / 20.0 TypeError: Nicht unterstützte Operandentypen für -: 'NoneType' und 'int'
Robert Buckley

Das bedeutet nur, dass Sie einige Nullwerte in Ihrem haben "XKoordInt". Siehe meine Bearbeitung, alles was Sie tun müssen, ist Nullen zu überspringen.
Crmackey

2
Sei vorsichtig mit range. ArcGIS verwendet weiterhin Python 2.7, daher wird a zurückgegeben list. Aber in 3.x rangeist es eine eigene Art von Objekt, das Optimierungen haben kann. Ein zuverlässigerer Test wäre min(list(range(200000))), der sicherstellen würde, dass Sie mit einer einfachen Liste arbeiten. Verwenden Sie das timeitModul auch für Leistungstests.
jpmc26

Sie könnten wahrscheinlich mehr Zeit gewinnen, wenn Sie Sets anstelle von Listen verwenden. Auf diese Weise speichern Sie keine doppelten Werte und suchen nur nach eindeutigen Werten.
Fezter

@Fezter Es hängt von der Verteilung ab. Es müssten genügend exakte Duplikate vorhanden sein, um die Kosten für das Hashing aller Werte und die Überprüfung, ob sich jedes während des Aufbaus im Set befindet, aufzuwiegen. Wenn beispielsweise nur 1% dupliziert wird, sind die Kosten wahrscheinlich nicht wert. Beachten Sie auch, dass es bei einem Gleitkommawert wahrscheinlich nicht viele exakte Duplikate gibt.
jpmc26

1

Wie @crmackey hervorhebt, ist der langsame Teil wahrscheinlich auf die Berechnungsfeldmethode zurückzuführen. Alternativ zu den anderen geeigneten Lösungen und unter der Annahme, dass Sie eine Geodatabase zum Speichern Ihrer Daten verwenden, können Sie den Befehl Order By sql verwenden, um in aufsteigender Reihenfolge zu sortieren, bevor Sie den Aktualisierungscursor ausführen.

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 diesem Fall entfernt die where-Klausel die Nullen, bevor die Abfrage ausgeführt wird, oder Sie können das andere Beispiel verwenden, das vor dem Aktualisieren nach None sucht.


Nett! Wenn Sie die Reihenfolge als aufsteigend verwenden und den ersten Datensatz erfassen, ist dies definitiv schneller, als alle Werte abzurufen und dann die zu finden min(). Ich werde dies auch in meine Geschwindigkeitstests einbeziehen, um den Leistungsgewinn zu zeigen.
Crmackey

Ich werde gespannt sein, wo es rangiert. Es würde mich nicht wundern, wenn die zusätzlichen SQL-Operationen es langsam machen.
Dslamb

2
Timing-Benchmarks wurden hinzugefügt, siehe meine Bearbeitung. Und ich denke, Sie hatten Recht, die SQL schien zusätzlichen Aufwand zu verursachen, aber sie hat den Cursor, der die gesamte Liste durchläuft, in 0.56Sekunden ausgeführt, was nicht so viel Leistungsgewinn bedeutet, wie ich erwartet hätte.
Crmackey

1

In solchen Fällen können Sie auch numpy verwenden, obwohl dies speicherintensiver ist.

Sie erhalten immer noch einen Flaschenhals, wenn Sie die Daten in ein Numpy-Array und dann wieder in die Datenquelle laden. Ich habe jedoch festgestellt, dass der Leistungsunterschied bei größeren Datenquellen besser ist (zugunsten von Numpy), insbesondere wenn Sie mehrere benötigen Statistiken / Berechnungen.:

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

Warum nicht die Tabelle aufsteigend sortieren und dann mit einem Suchcursor den Wert für die erste Zeile ermitteln? http://pro.arcgis.com/de/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

Ich würde das SearchCursorin einen Generatorausdruck (dh min()) für Geschwindigkeit und Prägnanz einwickeln . Fügen Sie dann den Mindestwert aus dem Generatorausdruck in einen daTyp ein UpdateCursor. So etwas wie das Folgende:

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)

Sollte das SearchCursornicht geschlossen werden, wenn Sie damit fertig sind?
jpmc26

1
@ jpmc26 Ein Cursor kann durch Vervollständigen des Cursors freigegeben werden. Quelle (Cursor und Sperren): pro.arcgis.com/de/pro-app/arcpy/get-started/… . Ein weiteres Beispiel von Esri (siehe Beispiel 2): pro.arcgis.com/de/pro-app/arcpy/data-access/…
Aaron

0

In Ihrer Schleife befinden sich zwei Funktionsreferenzen, die für jede Iteration neu bewertet werden.

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

Es sollte schneller (aber etwas komplexer) sein, die Referenzen außerhalb der Schleife zu haben:

getvalue = row.getValue
append = ListVal.append

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

Würde das nicht tatsächlich langsamer werden? Sie erstellen tatsächlich eine neue separate Referenz für die integrierte append()Methode des listDatentyps. Ich glaube nicht, dass hier sein Engpass passiert, ich würde wetten, dass die Berechnungsfeldfunktion der Schuldige ist. Dies kann überprüft werden, indem der Feldrechner mit einem neuen Stilcursor verglichen wird.
Crmackey

1
Eigentlich würde mich auch das Timing interessieren :) Aber es ist ein einfacher Ersatz im Originalcode und daher schnell überprüft.
Matte

Ich weiß, dass ich vor einiger Zeit einige Benchmark-Tests mit Cursorn gegen Feldrechner durchgeführt habe. Ich werde einen weiteren Test durchführen und meine Ergebnisse in meiner Antwort angeben. Ich denke, es wäre auch gut, alte und neue Cursorgeschwindigkeit zu zeigen.
Crmackey
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.