Vous parcourez 16 millions d'enregistrements à l'aide d'ArcPy?


13

J'ai un tableau avec 8 colonnes et ~ 16,7 millions d'enregistrements. J'ai besoin d'exécuter un ensemble d'équations if-else sur les colonnes. J'ai écrit un script à l'aide du module UpdateCursor, mais après quelques millions d'enregistrements, il manque de mémoire. Je me demandais s'il y avait une meilleure façon de traiter ces 16,7 millions d'enregistrements.

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"

MISE À JOUR # 1

J'ai exécuté le même script sur un ordinateur avec 40 Go de RAM (l'ordinateur d'origine n'avait que 12 Go de RAM). Il s'est terminé avec succès après ~ 16 heures. Je pense que 16 heures, c'est trop long, mais je n'ai jamais travaillé avec un si grand ensemble de données, donc je ne sais pas à quoi m'attendre. Le seul nouvel ajout à ce script est arcpy.env.parallelProcessingFactor = "100%". J'essaie deux méthodes suggérées (1) faire 1 million d'enregistrements par lots et (2) en utilisant SearchCursor et en écrivant des sorties sur csv. Je rendrai compte des progrès sous peu.

MISE À JOUR # 2

La mise à jour SearchCursor et CSV a parfaitement fonctionné! Je n'ai pas les temps d'exécution précis, je mettrai à jour le message quand je serai au bureau demain, mais je dirais que le temps d'exécution approximatif est de ~ 5-6 minutes, ce qui est assez impressionnant. Je ne m'y attendais pas. Je partage mon code non poli tous les commentaires et améliorations sont les bienvenus:

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"

MISE À JOUR # 3 Mise à jour finale. La durée totale d'exécution du script est de ~ 199,6 secondes / 3,2 minutes.


1
Utilisez-vous 64 bits (en arrière-plan ou serveur ou Pro)?
KHibma

Oublié de mentionner. J'utilise 10,4 x64 en arrière-plan.
cptpython

Avocat des Diables - avez-vous essayé de l'exécuter en premier plan ou depuis IDLE alors qu'en regardant votre script, vous n'avez pas besoin d'avoir ArcMap ouvert?
Hornbydd

exécutez-le en tant que script autonome ou si vous connaissez SQL, téléchargez le fichier de formes sur PostgreSQL et faites-le là
ziggy

1
Je comprends que c'est open source, mais son processus d'approbation prend environ 1 à 2 semaines, et cela est sensible au temps, donc je ne pense pas que ce soit faisable dans ce cas.
cptpython

Réponses:


4

Vous pouvez écrire l'Objectid et le résultat du calcul (cate_2) dans un fichier csv. Joignez ensuite le fichier csv à votre fichier d'origine, remplissez un champ pour conserver le résultat. De cette façon, vous ne mettez pas à jour la table à l'aide du curseur DA. Vous pouvez utiliser un curseur de recherche.


Je pensais la même chose car il y a une discussion ici et ils parlent d'ensembles de données encore plus grands.
Hornbydd

Merci, klewis. Cela semble prometteur. Je vais l'essayer avec la suggestion de FelixIP, et une discussion intéressante bien que je devrai l'exécuter quelques dizaines de fois.
cptpython

A fonctionné avec brio! J'ai mis à jour la question avec le dernier script. Merci!
cptpython

2

Toutes mes excuses, si je continue de faire revivre ce vieux fil. L'idée était d'exécuter les instructions if-else sur le raster de combinaison, puis d'utiliser le nouveau champ dans la recherche pour créer un nouveau raster. J'ai compliqué le problème en exportant les données sous forme de tableau et introduit un flux de travail inefficace qui a été résolu par @Alex Tereshenkov. Après avoir réalisé l'évidence, j'ai groupé les données en 17 requêtes (1 million chacune) comme suggéré par @FelixIP. Il a fallu environ 1,5 minute à chaque lot pour terminer et la durée totale de fonctionnement était d'environ 23,3 minutes. Cette méthode élimine le besoin de jointures et je pense que cette méthode accomplit le mieux la tâche. Voici un script révisé pour référence future:

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

Donc, juste pour être sûr de bien comprendre. Dans votre message d'origine, vous avez dit que lorsque vous exécutiez cela sur un ordinateur avec 40 Go de RAM, cela prenait environ 16 heures au total. Mais maintenant que vous l'avez divisé en 17 lots, cela a pris environ 23 minutes au total. Est-ce exact?
ianbroad

Correct. La première exécution a pris ~ 16 heures avec 40 Go de RAM et la deuxième exécution a pris ~ 23 minutes + 15 minutes supplémentaires pour effectuer Lookupet exporter le raster avec les catégories nouvellement définies.
cptpython

Juste une note qui arcpy.env.parallelProcessingFactor = "100%"n'a aucun effet sur votre script. Je ne vois aucun outil qui exploite cet environnement.
KHibma

Vous avez raison. Je vais modifier le code.
cptpython

1

Vous pouvez essayer de passer à l'aide de CalculateField_management . Cela évite de parcourir en boucle à l'aide de curseurs et, d'après l'apparence de vos options pour la valeur de catégorie, vous pouvez définir cela comme quatre sous-processus générés séquentiellement. Lorsque chaque sous-processus se termine, sa mémoire est libérée avant de démarrer le suivant. Vous prenez un petit coup (millisecondes) engendrant chaque sous-processus.

Ou, si vous souhaitez conserver votre approche actuelle, ayez un sous-processus qui prend x-lignes à la fois. Avoir un processus principal pour le piloter et, comme précédemment, vous continuez à fouiller votre mémoire chaque fois qu'elle se termine. L'avantage de le faire de cette façon (en particulier via un processus python autonome) est que vous pouvez utiliser davantage tous vos cœurs en tant que sous-processus de génération dans le multithreading de python que vous contournez le GIL. Cela est possible avec ArcPy et une approche que j'ai utilisée dans le passé pour effectuer des transferts de données massifs. Évidemment, gardez vos morceaux de données bas sinon vous finirez par manquer de mémoire plus rapidement!


D'après mon expérience, l'utilisation d'arcpy.da.UpdateCursor est bien plus rapide que arcpy.CalculateField_management. J'ai écrit un script qui s'exécute sur 55 000 000 fonctionnalités de construction, il était environ 5 fois plus lent avec l'outil CalculateField.
offermann

Le but est de mettre en place quatre sous-processus et de nettoyer la mémoire car c'est le véritable point de pincement ici. Comme je le souligne dans le deuxième paragraphe, vous pouvez diviser les sous-processus par lignes, mais cela prend un peu plus de gestion qu'une seule sélection.
MappaGnosis

1

La logique de manipulation des données peut être écrite sous la forme d'une instruction SQL UPDATE à l'aide d'une expression CASE, que vous pouvez exécuter à l'aide de GDAL / OGR, par exemple via OSGeo4W avec gdal-filegdbinstallé.

Voici le workflow, qui utilise à la osgeo.ogrplace de 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)

Sur une table similaire avec un peu plus d'un million d'enregistrements, cette requête a pris 18 minutes. Le traitement de 16 millions d'enregistrements peut donc prendre environ 4 à 5 heures.


Malheureusement, le script fait partie d'un script plus grand écrit en utilisant arcpymais j'apprécie la réponse. J'essaie lentement d'utiliser GDAL davantage.
cptpython

1

La mise à jour du code dans la section # 2 de votre question ne montre pas comment vous joignez le .csvfichier à la table d'origine dans votre géodatabase fichier. Vous dites que l'exécution de votre script a duré environ 5 minutes. Cela semble juste si vous avez uniquement exporté le .csvfichier sans faire de jointures. Lorsque vous tenterez de ramener le .csvfichier dans ArcGIS, vous rencontrerez les problèmes de performances.

1) Vous ne pouvez pas faire de jointures directement à partir .csvde la table de géodatabase, car le .csvfichier n'a pas d'OID (avoir un champ calculé avec des valeurs uniques n'aidera pas car vous devrez toujours convertir votre .csvfichier en table de géodatabase). Ainsi, plusieurs minutes pour l' Table To Tableoutil GP (vous pouvez utiliser l' in_memoryespace de travail pour y créer une table temporaire, ce sera légèrement plus rapide).

2) Une fois que vous avez chargé le .csvdans une table de géodatabase, vous souhaitez créer un index sur le champ sur lequel vous effectuez la jointure (dans votre cas, la objectidvaleur source du .csvfichier. Cela prendrait quelques minutes sur un tableau de 16 ml de lignes).

3) Ensuite, vous devez utiliser les outils Add Joinou Join FieldGP. Aucun ne fonctionnera bien sur vos grandes tables.

4) Ensuite, vous devez faire l' Calculate Fieldoutil GP pour calculer le ou les nouveaux champs joints. De nombreuses minutes passent ici; encore plus, le calcul de champ prend plus de temps lorsque les champs qui participent au calcul proviennent d'une table jointe.

En un mot, vous n'obtiendrez rien près de 5 minutes que vous mentionnez. Si vous arriviez dans une heure, je serais impressionné.

Pour éviter de traiter le traitement d'ensembles de données volumineux dans ArcGIS, je suggère de prendre vos données en dehors d'ArcGIS dans un bloc de pandasdonnées et de faire tous vos calculs là-bas. Lorsque vous avez terminé, réécrivez simplement les lignes du bloc de données dans une nouvelle table de géodatabase avec da.InsertCursor(ou vous pouvez tronquer votre table existante et écrire vos lignes dans la source).

Le code complet que j'ai écrit pour comparer ceci est ci-dessous:

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

Vous trouverez ci-dessous la sortie du Debug IO (le nombre signalé est le nombre de lignes dans une table utilisée) avec des informations sur le temps d'exécution pour les fonctions individuelles:

---------------------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'insertion d'une ligne avec da.InsertCursorprend un temps constant, c'est-à-dire que si insérer 1 ligne prend, disons, 0,1 seconde, l'insertion de 100 lignes prendra 10 secondes. Malheureusement, 95% + du temps d'exécution total est consacré à la lecture de la table de géodatabase, puis à la réinsertion des lignes dans la géodatabase.

Il en va de même pour la création d'une pandastrame de données à partir d'un da.SearchCursorgénérateur et pour le calcul du ou des champs. Comme le nombre de lignes de votre table de géodatabase source double, le temps d'exécution du script ci-dessus augmente également. Bien sûr, vous devez toujours utiliser le Python 64 bits car pendant l'exécution, certaines structures de données plus grandes seront traitées en mémoire.


En fait, j'allais poser une autre question qui parlerait des limites de la méthode que j'ai utilisée, car j'ai rencontré les problèmes que vous avez abordés ci-dessus, merci! Ce que j'essaie d'accomplir: combiner quatre rasters, puis effectuer une instruction if-else basée sur les colonnes et écrire les sorties dans une nouvelle colonne et enfin effectuer Lookuppour créer un raster basé sur les valeurs de la nouvelle colonne. Ma méthode comportait de nombreuses étapes inutiles et un flux de travail inefficace, j'aurais dû le mentionner dans ma question d'origine. Vivre et apprendre. Je vais cependant essayer votre script plus tard cette semaine.
cptpython
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.