Existe uma maneira mais rápida em python de encontrar o menor número em um campo?


10

Usando o arcgis desktop 10.3.1 Eu tenho um script que usa um cursor de pesquisa para acrescentar valores a uma lista e, em seguida, use min () para encontrar o menor número inteiro. A variável é então usada em um script. A classe Feature tem 200.000 linhas e o script leva muito tempo para ser concluído. Existe uma maneira de fazer isso mais rápido? No momento, acho que faria apenas à mão, em vez de escrever um script devido ao tempo que leva.

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

Eu acho que é um não-Python maneira mais rápida de fazer isso que você parecia estar funcionando em pelo gis.stackexchange.com/q/197873/115
— PolyGeo

Alguma razão para você não estar usando arcpy.Statistics_analysis? desktop.arcgis.com/en/arcmap/10.3/tools/analysis-toolbox/...
— Berend

Sim. Eu tenho que começar em algum lugar e só raramente tenho que fazer alguma programação com o arcpy. É fantástico que tantas pessoas possam sugerir tantas abordagens. Esta é a melhor maneira de aprender coisas novas.
— Robert Buckley

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

Respostas:


15

Eu posso ver várias coisas que podem estar causando um atraso no seu script. O que provavelmente está sendo muito lento é a arcpy.CalculateField_management()função. Você deve usar um cursor, por várias magnitudes mais rápido. Além disso, você disse que está usando o ArcGIS Desktop 10.3.1, mas está usando os cursores antigos do estilo ArcGIS 10.0, que também são muito mais lentos.

A operação min (), mesmo em uma lista de 200K, será bastante rápida. Você pode verificar isso executando este pequeno trecho; isso acontece em um piscar de olhos:

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

Veja se isso é mais rápido:

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)

EDITAR:

Fiz alguns testes de tempo e, como suspeitava, a calculadora de campo demorou quase o dobro do tempo do cursor do novo estilo. Curiosamente, o cursor do estilo antigo era ~ 3x mais lento que a calculadora de campo. Criei 200.000 pontos aleatórios e usei os mesmos nomes de campo.

Uma função decoradora foi usada para cronometrar cada função (pode haver uma pequena sobrecarga na configuração e desmontagem das funções, portanto, talvez o módulo timeit seja um pouco mais preciso para testar trechos).

Aqui estão os resultados:

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

E aqui está o código que eu usei (dividi tudo em funções individuais para usar o timeitdecorador):

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)

E, finalmente, é isso que foi a impressão real do meu console.

>>> 
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: Acabei de publicar alguns testes atualizados, encontrei uma pequena falha na minha timeitfunção.


r [0] = (r [0] - valor) / 20.0 TypeError: tipo (s) de operando não suportado para -: 'NoneType' e 'int'
— Robert Buckley

Isso significa apenas que você tem alguns valores nulos no seu "XKoordInt". Veja minha edição, tudo que você precisa fazer é pular nulos.
— crmackey

2
Tenha cuidado com range. O ArcGIS ainda usa Python 2.7, então retorna a list. Mas no 3.x, rangeé seu próprio tipo especial de objeto que pode ter otimizações. Um teste mais confiável seria min(list(range(200000))), o que garantiria que você estivesse trabalhando com uma lista simples. Considere também usar o timeitmódulo para teste de desempenho.
— jpmc26

Você provavelmente poderia ganhar mais tempo usando conjuntos em vez de listas. Dessa forma, você não está armazenando valores duplicados e está pesquisando apenas valores únicos.
— Fezter

@Fezter Depende da distribuição. Teria que haver duplicatas exatas suficientes para compensar o custo de misturar todos os valores e verificar se cada um está no conjunto durante a construção. Por exemplo, se apenas 1% for duplicado, provavelmente não vale o custo. Observe também que, se o valor for ponto flutuante, é improvável que haja muitas duplicatas exatas.
— perfil completo de jpmc26

1

Como aponta @crmackey, a parte lenta provavelmente se deve ao método de cálculo do campo. Como alternativa às outras soluções adequadas, e supondo que você esteja usando um geodatabase para armazenar seus dados, você pode usar o comando Order By sql para classificar em ordem crescente antes de executar o cursor de atualização.

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)

Nesse caso, a cláusula where remove os nulos antes de fazer a consulta ou você pode usar o outro exemplo que verifica Nenhum antes da atualização.


Agradável! Usar a ordem como ascendente e agarrando o primeiro registro será definitivamente mais rápido do que obter todos os valores e depois encontrar o min(). Também incluirei isso em meus testes de velocidade para mostrar o ganho de desempenho.
— crmackey

Ficarei curioso para ver onde ele se classifica. Eu não ficaria surpreso se as operações extras do sql o tornassem lento.
— dslamb

2
referências de tempo foram adicionadas, veja minha edição. E eu acho que você estava correto, o sql parecia acrescentar uma sobrecarga extra, mas executou o cursor que percorre a lista inteira por 0.56segundos, o que não é tanto um ganho de desempenho como eu esperava.
— crmackey

1

Você também pode usar numpy em casos como esse, embora consuma mais memória.

Você ainda terá um gargalo ao carregar os dados em uma matriz numpy e depois novamente na fonte de dados, mas eu descobri que a diferença de desempenho é melhor (a favor da numpy) com fontes de dados maiores, especialmente se você precisar de vários estatísticas / cálculos .:

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

Por que não classificar a tabela em ascensão e usar um cursor de pesquisa para obter o valor da primeira linha? http://pro.arcgis.com/en/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

Eu colocaria a expressãoSearchCursor em um gerador (ie min()), tanto para velocidade quanto para sucessão. Em seguida, incorpore o valor mínimo da expressão do gerador em um datipo UpdateCursor. Algo como o seguinte:

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)

Não deve SearchCursorser fechado quando você terminar com isso?
— perfil completo de jpmc26

1
@ jpmc26 Um cursor pode ser liberado com a conclusão do cursor. Origem (cursores e bloqueio): pro.arcgis.com/en/pro-app/arcpy/get-started/… . Outro exemplo da Esri (consulte o exemplo 2): pro.arcgis.com/en/pro-app/arcpy/data-access/…
— Aaron

0

No seu loop, você tem duas referências de função que são reavaliadas para cada iteração.

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

Deve ser mais rápido (mas um pouco mais complexo) ter as referências fora do loop:

getvalue = row.getValue
append = ListVal.append

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

Isso não iria diminuir a velocidade? Na verdade, você está criando uma nova referência separada para o append()método interno do listtipo de dados. Eu não acho que é aí que o gargalo dele está acontecendo. Aposto que a função de campo de cálculo é a culpada. Isso pode ser verificado cronometrando a calculadora de campo contra um novo cursor de estilo.
— crmackey

1
na verdade, eu também estaria interessado nos horários :) Mas é uma substituição fácil no código original e, portanto, verificada rapidamente.
— Matte

Eu sei que fiz alguns testes de benchmark há algum tempo atrás em cursores vs calculadora de campo. Vou fazer outro teste e relatar minhas descobertas na minha resposta. Eu acho que também seria bom mostrar a velocidade do cursor antigo vs novo também.
— crmackey
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.