Durchlaufen von 16 Millionen Datensätzen mit ArcPy?

13

Ich habe eine Tabelle mit 8 Spalten und ~ 16,7 Millionen Datensätzen. Ich muss eine Reihe von if-else-Gleichungen für die Spalten ausführen. Ich habe ein Skript mit dem UpdateCursor-Modul geschrieben, aber nach einigen Millionen Datensätzen ist der Speicher voll. Ich habe mich gefragt, ob es einen besseren Weg gibt, diese 16,7 Millionen Datensätze zu verarbeiten.

import arcpy

arcpy.TableToTable_conversion("combine_2013", "D:/mosaic.gdb", "combo_table")

c_table = "D:/mosaic.gdb/combo_table"

fields = ['dev_agg', 'herb_agg','forest_agg','wat_agg', 'cate_2']

start_time = time.time()
print "Script Started"
with arcpy.da.UpdateCursor(c_table, fields) as cursor:
    for row in cursor:
        # row's 0,1,2,3,4 = dev, herb, forest, water, category
        #classficiation water = 1; herb = 2; dev = 3; forest = 4
        if (row[3] >= 0 and row[3] > row[2]):
            row[4] = 1
        elif (row[2] >= 0 and row[2] > row[3]):
            row[4] = 4
        elif (row[1] > 180):
            row[4] = 2
        elif (row[0] > 1):
            row[4] = 3
        cursor.updateRow(row)
end_time = time.time() - start_time
print "Script Complete - " +  str(end_time) + " seconds"

UPDATE # 1

Ich habe das gleiche Skript auf einem Computer mit 40 GB RAM ausgeführt (der ursprüngliche Computer hatte nur 12 GB RAM). Es wurde nach ca. 16 Stunden erfolgreich abgeschlossen. Ich halte 16 Stunden für zu lang, aber ich habe noch nie mit einem so großen Datensatz gearbeitet, dass ich nicht weiß, was mich erwartet. Die einzige Neuerung in diesem Skript ist arcpy.env.parallelProcessingFactor = "100%". Ich versuche zwei vorgeschlagene Methoden (1) 1 Million Datensätze in Stapeln zu erstellen und (2) SearchCursor zu verwenden und Ausgaben in csv zu schreiben. Ich werde in Kürze über die Fortschritte berichten.

UPDATE # 2

Das SearchCursor- und CSV-Update hat hervorragend funktioniert! Ich habe nicht die genauen Laufzeiten, ich werde den Post aktualisieren, wenn ich morgen im Büro bin, aber ich würde sagen, die ungefähre Laufzeit beträgt ~ 5-6 Minuten, was ziemlich beeindruckend ist. Ich habe es nicht erwartet. Ich teile meinen unpolierten Code, Kommentare und Verbesserungen sind willkommen:

import arcpy, csv, time
from arcpy import env

arcpy.env.parallelProcessingFactor = "100%"

arcpy.TableToTable_conversion("D:/mosaic.gdb/combine_2013", "D:/mosaic.gdb", "combo_table")
arcpy.AddField_management("D:/mosaic.gdb/combo_table","category","SHORT")

# Table
c_table = "D:/mosaic.gdb/combo_table"
fields = ['wat_agg', 'dev_agg', 'herb_agg','forest_agg','category', 'OBJECTID']

# CSV
c_csv = open("D:/combine.csv", "w")
c_writer = csv.writer(c_csv, delimiter= ';',lineterminator='\n')
c_writer.writerow (['OID', 'CATEGORY'])
c_reader = csv.reader(c_csv)

start_time = time.time()
with arcpy.da.SearchCursor(c_table, fields) as cursor:
    for row in cursor:
        #skip file headers
        if c_reader.line_num == 1:
            continue
        # row's 0,1,2,3,4,5 = water, dev, herb, forest, category, oid
        #classficiation water = 1; dev = 2; herb = 3; ; forest = 4
        if (row[0] >= 0 and row[0] > row[3]):
            c_writer.writerow([row[5], 1])
        elif (row[1] > 1):
            c_writer.writerow([row[5], 2])
        elif (row[2] > 180):
            c_writer.writerow([row[5], 3])
        elif (row[3] >= 0 and row[3] > row[0]):
            c_writer.writerow([row[5], 4])

c_csv.close()
end_time =  time.time() - start_time
print str(end_time) + " - Seconds"

UPDATE # 3 Letztes Update. Die Gesamtlaufzeit für das Skript beträgt ~ 199,6 Sekunden / 3,2 Minuten.

Cptpython
quelle
1
Verwenden Sie 64-Bit (entweder Hintergrund oder Server oder Pro)?
KHibma
Vergaß zu erwähnen. Ich lasse 10.4 x64 im Hintergrund laufen.
Cptpython
Devils Advocate - Haben Sie versucht, es im Vordergrund oder im IDLE-Modus auszuführen, um Ihr Skript zu betrachten, ohne dass ArcMap geöffnet sein muss?
Hornbydd
Führe es als eigenständiges Skript aus oder wenn du SQL kennst, lade das Shapefile auf PostgreSQL
hoch
1
Ich verstehe, dass es Open Source ist, aber der Genehmigungsprozess dauert ca. 1-2 Wochen und dies ist zeitkritisch, so dass ich es in diesem Fall nicht für machbar halte.
Cptpython

Antworten:

4

Sie können die Objectid und das Ergebnis der Berechnung (cate_2) in eine CSV-Datei schreiben. Fügen Sie dann die CSV zu Ihrer Originaldatei hinzu, füllen Sie ein Feld aus, um das Ergebnis beizubehalten. Auf diese Weise aktualisieren Sie die Tabelle nicht mit dem DA-Cursor. Sie können einen Suchcursor verwenden.

klewis
quelle
Ich habe das Gleiche gedacht, da es hier eine Diskussion gibt und es sich um noch größere Datensätze handelt.
Hornbydd
Danke, Klewis. Das klingt vielversprechend. Ich werde es zusammen mit FelixIPs Vorschlag und einer interessanten Diskussion versuchen, obwohl ich das ein paar Dutzend Mal machen muss.
Cptpython
Genial gearbeitet! Ich habe die Frage mit dem neuesten Skript aktualisiert. Vielen Dank!
Cptpython
2

Entschuldigung, wenn ich diesen alten Thread wiederbelebe. Die Idee war, die if-else-Anweisungen für das kombinierte Raster auszuführen und dann das neue Feld in Lookup zu verwenden, um ein neues Raster zu erstellen. Ich habe das Problem dadurch erschwert, dass ich die Daten als Tabelle exportierte und einen ineffizienten Workflow einführte, der von @Alex Tereshenkov angesprochen wurde. Nachdem ich das Offensichtliche erkannt hatte, stapelte ich die Daten in 17 Abfragen (je 1 Million), wie es von @FelixIP vorgeschlagen wurde. Es dauerte durchschnittlich ~ 1,5 Minuten, bis jede Charge fertiggestellt war, und die Gesamtlaufzeit betrug ~ 23,3 Minuten. Diese Methode macht Verknüpfungen überflüssig, und ich denke, diese Methode erfüllt die Aufgabe am besten. Hier ist ein überarbeitetes Skript zum späteren Nachschlagen:

import arcpy, time
from arcpy import env

def cursor():
    combine = "D:/mosaic.gdb/combine_2013"
    #arcpy.AddField_management(combine,"cat_1","SHORT")
    fields = ['wat_agg', 'dev_agg', 'herb_agg','forest_agg', 'cat_1']
    batch = ['"OBJECTID" >= 1 AND "OBJECTID" <= 1000000', '"OBJECTID" >= 1000001 AND "OBJECTID" <= 2000000', '"OBJECTID" >= 2000001 AND "OBJECTID" <= 3000000', '"OBJECTID" >= 3000001 AND "OBJECTID" <= 4000000', '"OBJECTID" >= 4000001 AND "OBJECTID" <= 5000000', '"OBJECTID" >= 5000001 AND "OBJECTID" <= 6000000', '"OBJECTID" >= 6000001 AND "OBJECTID" <= 7000000', '"OBJECTID" >= 7000001 AND "OBJECTID" <= 8000000', '"OBJECTID" >= 8000001 AND "OBJECTID" <= 9000000', '"OBJECTID" >= 9000001 AND "OBJECTID" <= 10000000', '"OBJECTID" >= 10000001 AND "OBJECTID" <= 11000000', '"OBJECTID" >= 11000001 AND "OBJECTID" <= 12000000', '"OBJECTID" >= 12000001 AND "OBJECTID" <= 13000000', '"OBJECTID" >= 13000001 AND "OBJECTID" <= 14000000', '"OBJECTID" >= 14000001 AND "OBJECTID" <= 15000000', '"OBJECTID" >= 15000001 AND "OBJECTID" <= 16000000', '"OBJECTID" >= 16000001 AND "OBJECTID" <= 16757856']
    for i in batch:
        start_time = time.time()
        with arcpy.da.UpdateCursor(combine, fields, i) as cursor:
            for row in cursor:
            # row's 0,1,2,3,4,5 = water, dev, herb, forest, category
            #classficiation water = 1; dev = 2; herb = 3; ; forest = 4
                if (row[0] >= 0 and row[0] >= row[3]):
                    row[4] = 1
                elif (row[1] > 1):
                    row[4] = 2
                elif (row[2] > 180):
                    row[4] = 3
                elif (row[3] >= 0 and row[3] > row[0]):
                    row[4] = 4
                cursor.updateRow(row)
        end_time =  time.time() - start_time
        print str(end_time) + " - Seconds"

cursor()
Cptpython
quelle
Also, nur um sicherzugehen, dass ich das richtig verstehe. In Ihrem ursprünglichen Beitrag haben Sie gesagt, dass es insgesamt ~ 16 Stunden gedauert hat, als Sie dies auf einem Computer mit 40 GB RAM ausgeführt haben. Aber jetzt, da Sie es in 17 Chargen aufgeteilt haben, hat es insgesamt ~ 23 Minuten gedauert. Ist das korrekt?
ianbroad
Richtig. Der erste Durchlauf dauerte ~ 16 Stunden mit 40 GB RAM, und der zweite Durchlauf dauerte ~ 23 Minuten + weitere ~ 15 Minuten, um Lookupdas Raster mit neu definierten Kategorien auszuführen und zu exportieren.
Cptpython
Nur eine Notiz, arcpy.env.parallelProcessingFactor = "100%"die keinen Einfluss auf Ihr Skript hat. Ich sehe dort keine Tools, die diese Umgebung nutzen.
KHibma
Du hast Recht. Ich werde den Code bearbeiten.
Cptpython
1

Sie können versuchen, CalculateField_management zu verwenden . Auf diese Weise wird vermieden, dass Sie mit Cursorn durchlaufen, und anhand Ihrer Optionen für den Kategoriewert können Sie dies als vier nacheinander erzeugte Unterprozesse einrichten. Wenn jeder Unterprozess beendet ist, wird sein Speicher freigegeben, bevor der nächste gestartet wird. Sie benötigen einen kleinen Treffer (Millisekunden), um jeden Unterprozess zu starten.

Wenn Sie Ihren aktuellen Ansatz beibehalten möchten, können Sie auch einen Unterprozess verwenden, der jeweils x-Zeilen umfasst. Haben Sie einen Hauptprozess, um es zu fahren, und wie zuvor bereinigen Sie Ihr Gedächtnis jedes Mal, wenn es beendet ist. Der Vorteil dieser Vorgehensweise (vor allem durch einen eigenständigen Python-Prozess) besteht darin, dass Sie alle Ihre Kerne in Pythons Multthreading-Prozessen, die Sie in der GIL-Umgebung durchführen, besser nutzen können. Dies ist mit ArcPy und einem Ansatz möglich, mit dem ich in der Vergangenheit massive Datenabwanderungen durchgeführt habe. Offensichtlich sollten Sie Ihre Datenmengen niedrig halten, da Ihnen sonst schneller der Speicher ausgeht!

MappaGnosis
quelle
Meiner Erfahrung nach ist die Verwendung von arcpy.da.UpdateCursor viel schneller als die von arcpy.CalculateField_management. Ich habe ein Skript geschrieben, das mit 55.000.000 Gebäude-Features ausgeführt wird. Mit dem CalculateField-Tool war es ungefähr fünfmal langsamer.
Offermann
Es geht darum, vier Unterprozesse einzurichten und den Speicher zu leeren, da dies hier der eigentliche Knackpunkt ist. Wie ich im zweiten Absatz skizziere, könnten Sie die Unterprozesse nach Zeilen aufteilen, aber das erfordert etwas mehr Verwaltung als eine einzelne Auswahl.
MappaGnosis
1

Die Datenmanipulationslogik kann als UPDATE-SQL-Anweisung mit einem CASE-Ausdruck geschrieben werden, den Sie mit GDAL / OGR ausführen können, z. B. über OSGeo4W mit gdal-filegdbinstalliertem.

Hier ist der Workflow, der osgeo.ogranstelle von arcpy:

import time
from osgeo import ogr

ds = ogr.Open('D:/mosaic.gdb', 1)
if ds is None:
    raise ValueError("You don't have a 'FileGDB' driver, or the dataset doesn't exist")
sql = '''\
UPDATE combo_table SET cate_2 = CASE
    WHEN wat_agg >= 0 AND wat_agg > forest_agg THEN 1
    WHEN dev_agg > 1 THEN 2
    WHEN herb_agg > 180 THEN 3
    WHEN forest_agg >= 0 AND forest_agg > wat_agg THEN 4
    END
'''
start_time = time.time()
ds.ExecuteSQL(sql, dialect='sqlite')
ds = None  # save, close
end_time =  time.time() - start_time
print("that took %.1f seconds" % end_time)

Bei einer ähnlichen Tabelle mit etwas mehr als 1 Million Datensätzen dauerte diese Abfrage 18 Minuten. Die Verarbeitung von 16 Millionen Datensätzen kann also noch ca. 4 bis 5 Stunden dauern.

Mike T
quelle
Leider ist das Skript Teil eines größeren Skripts, das mit geschrieben wurde, arcpyaber ich schätze die Antwort. Ich versuche langsam, GDAL mehr zu benutzen.
Cptpython
1

Die Aktualisierung des Codes in Abschnitt 2 Ihrer Frage zeigt nicht, wie Sie die .csvDatei wieder mit der Originaltabelle in Ihrer File-Geodatabase verbinden. Sie sagen, dass das Ausführen Ihres Skripts ca. 5 Minuten gedauert hat. Dies klingt fair, wenn Sie nur die .csvDatei exportiert haben , ohne dass Verknüpfungen vorgenommen wurden. Wenn Sie versuchen, die .csvDatei wieder in ArcGIS zu laden, treten die Leistungsprobleme auf.

1) Sie können keine direkten .csvVerknüpfungen mit der Geodatabase-Tabelle herstellen, da die .csvDatei keine OID hat (ein mit eindeutigen Werten berechnetes Feld hilft nicht, da Sie Ihre .csvDatei dennoch in eine Geodatabase-Tabelle konvertieren müssen ). Einige Minuten für das Table To TableGP-Tool (Sie könnten den in_memoryArbeitsbereich verwenden, um dort eine temporäre Tabelle zu erstellen, dies ist etwas schneller).

2) Nachdem Sie das .csvin eine Geodatabase-Tabelle geladen haben , möchten Sie einen Index für das Feld erstellen, in dem Sie den Join objectidausführen möchten (in Ihrem Fall die Quelle aus der .csvDatei). Dies würde einige Minuten für eine Tabelle mit 16 Millionen Zeilen dauern.

3) Dann müssten Sie entweder das GP-Tool Add Joinoder das Join FieldGP-Tool verwenden. Beides funktioniert nicht gut an Ihren großen Tischen.

4) Anschließend müssen Sie das Calculate FieldGP-Tool ausführen, um die neu verbundenen Felder zu berechnen. Viele Minuten vergehen hier; Außerdem dauert die Feldberechnung länger, wenn die an der Berechnung beteiligten Felder aus einer verknüpften Tabelle stammen.

Mit einem Wort, Sie werden in der Nähe von 5 Minuten, die Sie erwähnen, nichts bekommen. Wenn Sie es in einer Stunde schaffen, wäre ich beeindruckt.

Um zu vermeiden, dass große Datasets in ArcGIS verarbeitet werden, empfehle ich, Ihre Daten außerhalb von ArcGIS in einen pandasDatenrahmen zu übernehmen und alle Berechnungen dort durchzuführen. Wenn Sie fertig sind, schreiben Sie einfach die Datenrahmenzeilen zurück in eine neue Geodatabase-Tabelle da.InsertCursor(oder Sie können Ihre vorhandene Tabelle abschneiden und Ihre Zeilen in die Quelltabelle schreiben).

Der vollständige Code, den ich zum Benchmarking geschrieben habe, ist unten:

import time
from functools import wraps
import arcpy
import pandas as pd

def report_time(func):
    '''Decorator reporting the execution time'''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, round(end-start,3))
        return result
    return wrapper

#----------------------------------------------------------------------
@report_time
def make_df(in_table,limit):
    columns = [f.name for f in arcpy.ListFields(in_table) if f.name != 'OBJECTID']
    cur = arcpy.da.SearchCursor(in_table,columns,'OBJECTID < {}'.format(limit))
    rows = (row for row in cur)
    df = pd.DataFrame(rows,columns=columns)
    return df

#----------------------------------------------------------------------
@report_time
def calculate_field(df):
    df.ix[(df['DataField2'] % 2 == 0), 'Category'] = 'two'
    df.ix[(df['DataField2'] % 4 == 0), 'Category'] = 'four'
    df.ix[(df['DataField2'] % 5 == 0), 'Category'] = 'five'
    df.ix[(df['DataField2'] % 10 == 0), 'Category'] = 'ten'
    df['Category'].fillna('other', inplace=True)
    return df

#----------------------------------------------------------------------
@report_time
def save_gdb_table(df,out_table):
    rows_to_write = [tuple(r[1:]) for r in df.itertuples()]
    with arcpy.da.InsertCursor(out_table,df.columns) as ins_cur:
        for row in rows_to_write:
            ins_cur.insertRow(row)

#run for tables of various sizes
for limit in [100000,500000,1000000,5000000,15000000]:
    print '{:,}'.format(limit).center(50,'-')

    in_table = r'C:\ArcGIS\scratch.gdb\BigTraffic'
    out_table = r'C:\ArcGIS\scratch.gdb\BigTrafficUpdated'
    if arcpy.Exists(out_table):
        arcpy.TruncateTable_management(out_table)

    df = make_df(in_table,limit=limit)
    df = calculate_field(df)
    save_gdb_table(df, out_table)
    print

Nachfolgend finden Sie die Ausgabe des Debug-E / A (die angegebene Anzahl entspricht der Anzahl der Zeilen in einer Tabelle) mit Informationen zur Ausführungszeit für einzelne Funktionen:

---------------------100,000----------------------
('make_df', 1.141)
('calculate_field', 0.042)
('save_gdb_table', 1.788)

---------------------500,000----------------------
('make_df', 4.733)
('calculate_field', 0.197)
('save_gdb_table', 8.84)

--------------------1,000,000---------------------
('make_df', 9.315)
('calculate_field', 0.392)
('save_gdb_table', 17.605)

--------------------5,000,000---------------------
('make_df', 45.371)
('calculate_field', 1.903)
('save_gdb_table', 90.797)

--------------------15,000,000--------------------
('make_df', 136.935)
('calculate_field', 5.551)
('save_gdb_table', 275.176)

Das Einfügen einer Zeile mit da.InsertCursordauert konstant, dh, wenn das Einfügen einer Zeile beispielsweise 0,1 Sekunden dauert, dauert das Einfügen von 100 Zeilen 10 Sekunden. Leider werden 95% der gesamten Ausführungszeit für das Lesen der Geodatabase-Tabelle und das anschließende Einfügen der Zeilen in die Geodatabase aufgewendet.

Das Gleiche gilt für die Erstellung eines pandasDatenrahmens aus einem da.SearchCursorGenerator und für die Berechnung der Felder. Wenn sich die Anzahl der Zeilen in Ihrer Quell-Geodatabase-Tabelle verdoppelt, verdoppelt sich auch die Ausführungszeit des obigen Skripts. Natürlich müssen Sie weiterhin 64-Bit-Python verwenden, da während der Ausführung einige größere Datenstrukturen im Arbeitsspeicher verwaltet werden.

Alex Tereshenkov
quelle
Eigentlich wollte ich eine andere Frage stellen, die sich mit den Einschränkungen der von mir verwendeten Methode befasst, weil ich auf die Probleme gestoßen bin, die Sie oben angesprochen haben, also danke! Was ich versuche zu erreichen: Kombiniere vier Raster und führe dann die if-else-Anweisung basierend auf den Spalten aus und schreibe die Ausgaben in eine neue Spalte und Lookuperstelle schließlich ein Raster basierend auf den Werten in der neuen Spalte. Meine Methode hatte viele unnötige Schritte und ineffiziente Arbeitsabläufe. Ich hätte dies in meiner ursprünglichen Frage erwähnen sollen. Lebe und lerne. Ich werde Ihr Skript jedoch später in dieser Woche testen.
Cptpython