Ripercorrendo 16 milioni di dischi usando ArcPy?


13

Ho una tabella con 8 colonne e ~ 16,7 milioni di record. Devo eseguire una serie di equazioni if-else sulle colonne. Ho scritto uno script usando il modulo UpdateCursor, ma dopo qualche milione di dischi ha esaurito la memoria. Mi chiedevo se esiste un modo migliore per elaborare questi 16,7 milioni di record.

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"

AGGIORNAMENTO # 1

Ho eseguito lo stesso script su un computer con 40 GB di RAM (il computer originale aveva solo 12 GB di RAM). Si è completato con successo dopo ~ 16 ore. Sento che 16 ore sono troppo lunghe, ma non ho mai lavorato con un set di dati così grande, quindi non so cosa aspettarmi. L'unica nuova aggiunta a questo script è arcpy.env.parallelProcessingFactor = "100%". Sto provando due metodi suggeriti (1) facendo 1 milione di record in batch e (2) usando SearchCursor e scrivendo output in csv. Riferirò sui progressi a breve.

AGGIORNAMENTO # 2

L'aggiornamento di SearchCursor e CSV ha funzionato alla grande! Non ho i tempi di esecuzione precisi, aggiornerò il post quando sarò in carica domani, ma direi che il tempo di esecuzione approssimativo è di ~ 5-6 minuti, il che è piuttosto impressionante. Non me lo aspettavo. Condivido il mio codice non lucidato e sono ben accetti commenti e miglioramenti:

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 Aggiornamento finale. Il tempo di esecuzione totale per lo script è ~ 199,6 secondi / 3,2 minuti.


1
Stai usando 64 bit (sia in background o Server o Pro)?
KHibma,

Ho dimenticato di menzionare. Sto eseguendo 10,4 x64 in background.
cptpython,

Devils advocate - hai provato a eseguirlo in primo piano o da IDLE mentre guardando il tuo script non hai bisogno di avere ArcMap aperto?
Hornbydd,

eseguilo come script autonomo o se conosci SQL, carica lo shapefile su PostgreSQL e fallo lì
ziggy

1
Capisco che è open source, ma il processo di approvazione richiede ~ 1-2 settimane, e questo è sensibile al tempo, quindi non penso che sia fattibile in questo caso.
cptpython,

Risposte:


4

È possibile scrivere l'Objectid e il risultato del calcolo (cate_2) in un file CSV. Quindi unire CSV al file originale, popolare un campo, per preservare il risultato. In questo modo non si aggiorna la tabella utilizzando il cursore DA. È possibile utilizzare un cursore di ricerca.


Stavo pensando la stessa cosa che c'è una discussione qui e stanno parlando di set di dati ancora più grandi.
Hornbydd,

Grazie, Klewis. Sembra promettente. Lo proverò insieme al suggerimento di FelixIP e alla discussione interessante anche se dovrò eseguirlo alcune decine di volte.
cptpython,

Ha funzionato brillantemente! Ho aggiornato la domanda con l'ultimo script. Grazie!
cptpython,

2

Mi scuso se continuo a rianimare questo vecchio thread. L'idea era di eseguire le istruzioni if-else sull'associazione raster e quindi utilizzare il nuovo campo in Ricerca per creare un nuovo raster. Ho complicato il problema esportando i dati come tabella e introdotto un flusso di lavoro inefficiente che è stato risolto da @Alex Tereshenkov. Dopo aver realizzato l'ovvio, ho raggruppato i dati in 17 query (1 milione ciascuna) come suggerito da @FelixIP. Per completare il batch sono stati necessari in media ~ 1,5 minuti e il tempo di esecuzione totale è stato di ~ 23,3 minuti. Questo metodo elimina la necessità di join e penso che questo metodo svolga meglio l'attività. Ecco uno script rivisto per riferimento futuro:

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

Quindi, solo per essere sicuro di capirlo correttamente. Nel tuo post originale hai detto che quando lo hai eseguito su un computer con 40 GB di RAM, ci sono voluti circa 16 ore in totale. Ma ora che lo hai diviso in 17 lotti, e ci sono voluti circa 23 minuti in totale. È corretto?
ianbroad,

Corretta. La prima esecuzione ha richiesto ~ 16 ore con 40 GB di RAM e la seconda esecuzione ha richiesto ~ 23 minuti + un'altra ~ 15 minuti per eseguire Lookuped esportare il raster con categorie appena definite.
cptpython,

Solo una nota che arcpy.env.parallelProcessingFactor = "100%"non ha alcun effetto sulla tua sceneggiatura. Non vedo alcuno strumento che sfrutti quell'ambiente.
KHibma,

Hai ragione. Modificherò il codice.
cptpython,

1

Potresti provare a cambiare usando CalculateField_management . Questo evita di scorrere ciclicamente usando i cursori e, dall'aspetto delle tue opzioni per il valore della categoria, potresti impostarlo come quattro sottoprocessi generati in sequenza. Al termine di ogni sottoprocesso, la sua memoria viene rilasciata prima di iniziare il successivo. Prendi un piccolo colpo (millisecondi) generando ogni sottoprocesso.

Oppure, se si desidera mantenere l'approccio attuale, disporre di un sottoprocesso che occupi le x-righe alla volta. Avere un processo principale per guidarlo e, come prima, continui a scavare la tua memoria ogni volta che finisce. Il vantaggio di farlo in questo modo (specialmente attraverso un processo python autonomo) è che puoi sfruttare maggiormente tutti i tuoi core come sottoprocessi di generazione nel multthreading di Python che ti aggiri nel GIL. Questo è possibile con ArcPy e un approccio che ho usato in passato per fare enormi cambiamenti di dati. Ovviamente tieni giù i tuoi blocchi di dati altrimenti finirai per esaurire la memoria più velocemente!


Nella mia esperienza, l'utilizzo di arcpy.da.UpdateCursor è molto più veloce di arcpy.CalculateField_management. Ho scritto uno script che gira su 55.000.000 di funzionalità di costruzione, era circa 5 volte più lento con lo strumento CalculateField.
offermann

Il punto è impostare quattro sottoprocessi e scavalcare la memoria in quanto questo è il vero punto di presa qui. Come ho delineato nel secondo paragrafo è possibile dividere i sottoprocessi per righe, ma ciò richiede un po 'più di gestione rispetto a una singola selezione.
MappaGnosis,

1

La logica di manipolazione dei dati può essere scritta come un'istruzione UPDATE SQL utilizzando un'espressione CASE, che è possibile eseguire utilizzando GDAL / OGR, ad esempio tramite OSGeo4W con gdal-filegdbinstallato.

Ecco il flusso di lavoro, che utilizza osgeo.ogrinvece di 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)

Su una tabella simile con poco più di 1 milione di record, questa query ha richiesto 18 minuti. Quindi potrebbero essere necessarie da 4 a 5 ore per elaborare 16 milioni di record.


Sfortunatamente lo script fa parte di uno script più grande scritto usando arcpyma apprezzo la risposta. Sto lentamente cercando di usare di più GDAL.
cptpython,

1

L'aggiornamento del codice nella sezione # 2 della tua domanda non mostra come stai ricollegando il .csvfile alla tabella originale nel tuo geodatabase di file. Dici che l'esecuzione della tua sceneggiatura ha richiesto ~ 5 minuti. Questo sembra corretto se hai esportato il .csvfile solo senza eseguire alcun join. Quando proverai a riportare il .csvfile su ArcGIS, colpirai i problemi di prestazioni.

1) Non è possibile eseguire i join direttamente dalla .csvtabella di geodatabase, poiché il .csvfile non ha un OID (avere un campo calcolato con valori univoci non sarà di aiuto in quanto sarà comunque necessario convertire il .csvfile in una tabella di geodatabase). Quindi, alcuni minuti per lo Table To Tablestrumento GP (è possibile utilizzare l'area di in_memorylavoro per creare una tabella temporanea lì, sarà leggermente più veloce).

2) Dopo aver caricato il file .csvin una tabella di geodatabase, si vorrebbe creare un indice sul campo in cui eseguire il join (nel tuo caso, il valore sorgente objectiddal .csvfile. Ciò richiederebbe alcuni minuti su una tabella di 16 mln di righe.

3) Quindi dovrai utilizzare gli strumenti GP Add Joino Join Field. Né si esibirà bene sui tuoi tavoli di grandi dimensioni.

4) Successivamente, è necessario eseguire lo Calculate Fieldstrumento GP per calcolare i campi appena aggiunti. Molti minuti vanno qui; ancora di più, il calcolo dei campi impiega più tempo quando i campi che partecipano al calcolo provengono da una tabella unita.

In una parola, non otterrai nulla vicino a 5 minuti che dici. Se ce la farai tra un'ora, sarei impressionato.

Per evitare di gestire l'elaborazione di set di dati di grandi dimensioni all'interno di ArcGIS, suggerisco di inserire i dati al di fuori di ArcGIS in un pandasframe di dati e di eseguire tutti i calcoli lì. Al termine, basta riscrivere le righe del frame di dati in una nuova tabella di geodatabase con da.InsertCursor(oppure è possibile troncare la tabella esistente e scrivere le righe in quella di origine).

Il codice completo che ho scritto per fare il benchmark è di seguito:

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

Di seguito è riportato l'output dell'IO di debug (il numero riportato è il numero di righe in una tabella utilizzata) con informazioni sui tempi di esecuzione per le singole funzioni:

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

L'inserimento di una riga con da.InsertCursorrichiede un tempo costante, ovvero se l'inserimento di una riga richiede, ad esempio, 0,1 secondi, l'inserimento di 100 righe richiederà 10 secondi. Purtroppo, il 95% + del tempo totale di esecuzione viene impiegato per leggere la tabella del geodatabase e quindi reinserire le righe nel geodatabase.

Lo stesso vale per la creazione di un pandasframe di dati da un da.SearchCursorgeneratore e per il calcolo dei campi. Poiché il numero di righe nella tabella del geodatabase di origine raddoppia, aumenta anche il tempo di esecuzione dello script sopra. Naturalmente, è ancora necessario utilizzare Python a 64 bit poiché durante l'esecuzione, alcune strutture di dati più grandi verranno gestite in memoria.


In realtà, stavo per fare un'altra domanda che avrebbe parlato dei limiti del metodo che ho usato, perché mi sono imbattuto nei problemi che hai affrontato sopra, quindi grazie! Quello che sto cercando di realizzare: combina quattro raster e quindi esegui un'istruzione if-else basata sulle colonne e scrivi gli output in una nuova colonna e infine esegui Lookupper creare raster in base ai valori nella nuova colonna. Il mio metodo prevedeva molti passaggi non necessari e un flusso di lavoro inefficiente, avrei dovuto menzionarlo nella mia domanda originale. Vivere e imparare. Proverò la tua sceneggiatura alla fine di questa settimana.
cptpython,
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.