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.