Перегляньте 16 мільйонів записів за допомогою ArcPy?


13

У мене є таблиця з 8 колонками і ~ 16,7 мільйона записів. Мені потрібно запустити набір рівнянь if-else на стовпці. Я написав сценарій за допомогою модуля UpdateCursor, але після кількох мільйонів записів у нього не вистачає пам'яті. Мені було цікаво, чи є кращий спосіб опрацювати ці 16,7 мільйона записів.

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"

ОНОВЛЕННЯ №1

Я запустив той самий сценарій на комп’ютері з 40 Гб оперативної пам’яті (у початкового комп'ютера було всього 12 ГБ оперативної пам’яті). Він успішно завершився через ~ 16 годин. Я вважаю, що 16 годин занадто довго, але я ніколи не працював з таким великим набором даних, тому не знаю, що очікувати. Єдиним новим доповненням до цього сценарію є arcpy.env.parallelProcessingFactor = "100%". Я намагаюсь два запропоновані методи (1) робити 1 мільйон записів партіями та (2) за допомогою SearchCursor та записувати висновки в csv. Я незабаром звітую про прогрес.

ОНОВЛЕННЯ №2

Оновлення SearchCursor та CSV працювали чудово! У мене немає точних термінів виконання, я оновлю посаду, коли буду завтра, але скажу, що приблизний час роботи становить ~ 5-6 хвилин, що досить вражає. Я цього не очікував. Я ділюсь своїм неполірованим кодом, будь-які коментарі та покращення вітаються:

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"

ОНОВЛЕННЯ №3 Остаточне оновлення. Загальний час виконання сценарію становить ~ 199,6 секунди / 3,2 хвилини.


1
Ви використовуєте 64-бітну (або Фон, або Сервер, або Про)?
Хібма

Забув згадати. Я бігаю 10,4 x64 у фоновому режимі.
cptpython

Адвокати чортів - чи намагалися ви запускати його на передньому плані чи з IDLE, коли дивитесь на свій сценарій, вам не потрібно відкривати ArcMap?
Hornbydd

запускайте його як окремий сценарій або якщо ви знаєте SQL, завантажте файл форми на PostgreSQL і зробіть це там
ziggy

1
Я розумію, що він є відкритим кодом, але процес затвердження займає ~ 1-2 тижні, і це чутливий час, тому я не думаю, що це можливо в цьому випадку.
cptpython

Відповіді:


4

Ви можете записати Objectid та результат обчислення (cate_2) у файл csv. Потім приєднайте файл csv до оригінального файлу, заповніть поле, щоб зберегти результат. Таким чином ви не оновлюєте таблицю за допомогою курсору DA. Ви можете використовувати курсор пошуку.


Я думав те ж саме, є обговорення тут , і вони говорять про ще більші масиви даних.
Hornbydd

Спасибі, klewis. Це звучить перспективно. Я спробую це разом із пропозицією FelixIP та цікавою дискусією, хоча мені доведеться запустити це кілька десятків разів.
cptpython

Працювали блискуче! Я оновив питання останнім сценарієм. Спасибі!
cptpython

2

Вибачте, якщо я продовжую відроджувати цю стару тему. Ідея полягала в тому, щоб виконати операції if-else на растровому поєднанні, а потім використовувати нове поле в Lookup для створення нового растру. Я ускладнив цю проблему, експортувавши дані у таблицю та запровадив неефективний робочий процес, про який звертався @Alex Tereshenkov. Усвідомивши очевидне, я зібрав дані в 17 запитів (по 1 мільйон кожен), як це було запропоновано @FelixIP. На кожну партію в середньому знадобилося ~ 1,5 хвилини, а загальний час роботи - 23,3 хвилини. Цей метод позбавляє потреби в приєднанні, і я думаю, що цей метод найкраще виконує завдання. Ось переглянений сценарій для подальшого використання:

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

Тож, щоб переконатися, що я правильно це розумію. У своєму початковому дописі ви сказали, що коли ви працювали на комп'ютері з 40 Гб оперативної пам’яті, це займало ~ 16 годин. Але тепер ви розділили його на 17 партій, і це зайняло ~ 23 хвилин. Це правильно?
ianbroad

Правильно. Перший запуск займав ~ 16 годин з 40 ГБ оперативної пам’яті, а другий запуск потребував ~ 23 хв. + Інший ~ 15 хв., Щоб виконати Lookupта експортувати растр заново визначеними категоріями.
cptpython

Просто примітка, arcpy.env.parallelProcessingFactor = "100%"яка не впливає на ваш сценарій. Я не бачу в ньому ніяких інструментів, які б підтримували це середовище.
Хібма

Ти прав. Я відредагую код.
cptpython

1

Ви можете спробувати перейти на використання CalculateField_management . Це дозволяє уникнути циклічного використання за допомогою курсорів, і, виглядаючи ваші параметри для значення категорії, ви можете встановити це, оскільки чотири підпроцеси породжуються послідовно. По мірі завершення кожного підпроцесу його пам'ять звільняється перед початком наступної. Ви берете невеликий (мілісекунд) хіт нересту кожного підпроцесу.

Або, якщо ви хочете зберегти свій поточний підхід, мати підпроцес, який приймає x-рядки за один раз. У вас є основний процес для керування ним, і, як і раніше, ви постійно змінюєте свою пам'ять щоразу, коли вона закінчується. Бонус від цього таким чином (особливо через автономний процес python) полягає в тому, що ви можете більше використовувати всі ваші ядра як нерестуючі підпроцеси у багатопотоковості python, яку ви обійшли GIL. Це можливо завдяки ArcPy та підходу, який я використовував у минулому, щоб робити масові дані. Очевидно, зберігайте свої шматки даних, інакше у вас швидше закінчиться пам'ять!


На мій досвід, використання arcpy.da.UpdateCursor набагато швидше, ніж arcpy.CalculateField_management. Я написав сценарій, який працює на 55 000 000 функцій будівлі, він був приблизно в 5 разів повільніше за допомогою інструмента CalculateField.
Offermann

Сенс полягає в налаштуванні чотирьох підпроцесів і очищенні пам’яті, оскільки це справжній прищик. Як я окреслюю у другому абзаці, ви можете розділити підпроцеси на рядки, але це потребує трохи більше управління, ніж один вибір.
MappaGnosis

1

Логіка маніпулювання даними може бути записана як оператор UPDATE SQL, використовуючи вираз CASE, який можна виконати за допомогою GDAL / OGR, наприклад, через OSGeo4W із gdal-filegdbвстановленим.

Ось робочий процес, який використовує osgeo.ogrзамість 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)

На подібній таблиці з трохи більше 1 мільйона записів цей запит зайняв 18 хвилин. Тому обробка 16 мільйонів записів може зайняти від 4 до 5 годин.


На жаль, сценарій є частиною більшого сценарію, написаного з використанням, arcpyале я ціную відповідь. Я повільно намагаюся більше використовувати GDAL.
cptpython

1

Оновлення коду у розділі №2 у вашому запитанні не показує, як ви приєднуєте .csvфайл до вихідної таблиці у вашій базі даних про геодані. Ви кажете, що ваш сценарій запустив ~ 5 хв. Це звучить справедливо, якщо ви експортували .csvфайл лише без приєднання. Коли ви спробуєте повернути .csvфайл до ArcGIS, ви торкнетеся проблеми з продуктивністю.

1) Ви не можете приєднуватись безпосередньо .csvдо таблиці геоданих, оскільки у .csvфайлу немає OID (поле, обчислене унікальними значеннями, не допоможе, оскільки вам все одно знадобиться перетворити .csvфайл у таблицю бази даних геоданих). Отже, кілька хвилин для Table To Tableінструмента GP (ви можете використовувати in_memoryробочу область для створення тамтешньої таблиці там буде трохи швидше).

2) Після завантаження .csvв таблицю бази даних геоданих, ви хочете створити індекс на полі, яке б ви зробили приєднанням (у вашому випадку, вихідне значення objectidз .csvфайлу. Це займе кілька хвилин у таблиці 16-мільйонних рядків.

3) Тоді вам потрібно буде використовувати Add Joinабо Join Fieldінструменти GP, або GP. На ваших великих столах вони не будуть працювати добре.

4) Після цього вам потрібно зробити Calculate Fieldінструмент GP для обчислення нещодавно приєднаних полів. Сюди йде багато хвилин; навіть більше, обчислення поля займає більше часу, коли поля, які беруть участь у обчисленні, надходять із об'єднаної таблиці.

Одним словом, ви не отримаєте нічого близького до 5хв. Якщо ви зробите це через годину, я був би вражений.

Щоб уникнути обробки великих наборів даних в ArcGIS, я пропоную взяти ваші дані за межами ArcGIS у pandasкадр даних і зробити всі ваші розрахунки там. Коли ви закінчите, просто запишіть рядки кадру даних назад у нову таблицю баз даних геоданих da.InsertCursor(або ви можете скоротити існуючу таблицю і записати рядки у вихідну).

Повний код, який я написав для порівняння, наведено нижче:

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

Нижче наведено вихід з IO налагодження (число, про яке повідомляється, кількість рядків у використаній таблиці) з інформацією про час виконання окремих функцій:

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

Вставлення рядка з da.InsertCursorзаймає постійний час, тобто якщо для вставки 1 рядка потрібно, скажімо, 0,1 секунди, для вставки 100 рядків знадобиться 10 секунд. На жаль, 95% + загального часу виконання витрачається на читання таблиці геоданих, а потім на вставлення рядків назад в базу даних геоданих.

Це ж стосується виготовлення pandasкадру даних з da.SearchCursorгенератора та для обчислення полів. Оскільки кількість рядків у вашій вихідній таблиці геоданих збільшується вдвічі, так і час виконання сценарію вище. Звичайно, вам все одно потрібно використовувати 64-бітний Python, оскільки під час виконання деякі більші структури даних будуть оброблятися в пам'яті.


Насправді я збирався задати ще одне запитання, яке б говорило про обмеження використовуваного нами методу, тому що я зіткнувся з проблемами, про які ви зверталися вище, тому дякую! Що я намагаюся досягти: об'єднати чотири растри, а потім виконати оператор if-else на основі стовпців і виписати результати у новий стовпчик і, нарешті, виконати, Lookupщоб створити растр на основі значень у новому стовпці. У мого методу було багато непотрібних кроків і неефективний робочий процес, я повинен був це згадати в своєму первісному запитанні. Живи й учись. Я спробую твій сценарій пізніше цього тижня.
cptpython
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.