Tenho um conjunto de dados de cerca de 20.000 registros que representam cidades globais com população > 20.000. Estimei o raio que descreve mais ou menos o tamanho da cidade. Não é exatamente preciso, mas para meus propósitos será o suficiente.
que estou carregando-o no meu objeto dataframe Panda. Abaixo está o exemplo
name_city,country_code,latitude,longitude,geohash,estimated_radius,population
Vitry-sur-Seine,FR,48.78716,2.40332,u09tw9qjc3v3,1000,81001
Vincennes,FR,48.8486,2.43769,u09tzkx5dr13,500,45923
Villeneuve-Saint-Georges,FR,48.73219,2.44925,u09tpxrmxdth,500,30881
Villejuif,FR,48.7939,2.35992,u09ttdwmn45z,500,48048
Vigneux-sur-Seine,FR,48.70291,2.41357,u09tnfje022n,500,26692
Versailles,FR,48.80359,2.13424,u09t8s6je2sd,1000,85416
Vélizy-Villacoublay,FR,48.78198,2.19395,u09t9bmxdspt,500,21741
Vanves,FR,48.82345,2.29025,u09tu059nwwp,500,26068
Thiais,FR,48.76496,2.3961,u09tqt2u3pmt,500,29724
Sèvres,FR,48.82292,2.21757,u09tdryy15un,500,23724
Sceaux,FR,48.77644,2.29026,u09tkp7xqgmw,500,21511
Saint-Mandé,FR,48.83864,2.41579,u09tyfz1eyre,500,21261
Saint-Cloud,FR,48.84598,2.20289,u09tfhhh7n9u,500,28839
Paris,FR,48.85341,2.3488,u09tvmqrep8n,12000,2138551
Orly,FR,48.74792,2.39253,u09tq6q1jyzt,500,20528
Montrouge,FR,48.8162,2.31393,u09tswsyyrpr,500,38708
Montreuil,FR,48.86415,2.44322,u09tzx7n71ub,2000,111240
Montgeron,FR,48.70543,2.45039,u09tpf83dnpn,500,22843
Meudon,FR,48.81381,2.235,u09tdy73p38y,500,44652
Massy,FR,48.72692,2.28301,u09t5yqqvupx,500,38768
Malakoff,FR,48.81999,2.29998,u09tsr6v13tr,500,29420
Maisons-Alfort,FR,48.81171,2.43945,u09txtbkg61z,1000,53964
Longjumeau,FR,48.69307,2.29431,u09th0q9tq1s,500,20771
Le Plessis-Robinson,FR,48.78889,2.27078,u09te9txch23,500,22510
Le Kremlin-Bicêtre,FR,48.81471,2.36073,u09ttwrn2crz,500,27867
Le Chesnay,FR,48.8222,2.12213,u09t8rc3cjwz,500,29154
La Celle-Saint-Cloud,FR,48.85029,2.14523,u09tbufje6p6,500,21539
Ivry-sur-Seine,FR,48.81568,2.38487,u09twq8egqrc,1000,57897
Issy-les-Moulineaux,FR,48.82104,2.27718,u09tezd5njkr,1000,61447
Fresnes,FR,48.75568,2.32241,u09tkgenkj6r,500,24803
Fontenay-aux-Roses,FR,48.79325,2.29275,u09ts4t92cn3,500,24680
Clamart,FR,48.80299,2.26692,u09tes6dp0dn,1000,51400
Choisy-le-Roi,FR,48.76846,2.41874,u09trn12bez7,500,35590
Chevilly-Larue,FR,48.76476,2.3503,u09tmmr7mfns,500,20125
Châtillon,FR,48.8024,2.29346,u09tshnn96xx,500,32383
Châtenay-Malabry,FR,48.76507,2.26655,u09t7t6mn7yj,500,32715
Charenton-le-Pont,FR,48.82209,2.41217,u09twzu3r9hq,500,30910
Cachan,FR,48.79632,2.33661,u09tt5j7nvqd,500,26540
Bagnolet,FR,48.86667,2.41667,u09tyzzubrxb,500,33504
Bagneux,FR,48.79565,2.30796,u09tsdbx727w,500,38900
Athis-Mons,FR,48.70522,2.39147,u09tn6t2mr16,500,31225
Alfortville,FR,48.80575,2.4204,u09txhf6p7jp,500,37290
Quinze-Vingts,FR,48.84656,2.37439,u09tyh0zz6c8,500,26265
Croulebarbe,FR,48.81003,2.35403,u09tttd5hc5f,500,20062
Gare,FR,48.83337,2.37513,u09ty1cdbxcq,1000,75580
Maison Blanche,FR,48.82586,2.3508,u09tv2rz1xgx,1000,64302
Abaixo está a representação visual da amostra de dados:
Meu objetivo é encontrar um algoritmo eficiente que remova os círculos que se cruzam e mantenha apenas aquele com o maior population
.
Minha abordagem inicial foi determinar quais círculos estão se cruzando usando a fórmula de Haversine. O problema era que, para verificar cada registro em busca de interseções com outros, ele precisava percorrer todo o conjunto de dados. A complexidade de tempo disso era muito alta.
Minha segunda abordagem foi segregar o conjunto de dados por código de país e executar as comparações por blocos:
def _remove_intersecting_circles_for_country(df_country):
"""Helper function to remove intersections within a single country."""
indices_to_remove = set()
for i in range(len(df_country)):
for j in range(i + 1, len(df_country)):
distance = haversine(df_country['latitude'].iloc[i], df_country['longitude'].iloc[i],
df_country['latitude'].iloc[j], df_country['longitude'].iloc[j])
if distance < df_country['estimated_radius'].iloc[i] + df_country['estimated_radius'].iloc[j]:
if df_country['population'].iloc[i] < df_country['population'].iloc[j]:
indices_to_remove.add(df_country.index[i])
else:
indices_to_remove.add(df_country.index[j])
return indices_to_remove
all_indices_to_remove = set()
for country_code in df['country_code'].unique():
df_country = df[df['country_code'] == country_code]
indices_to_remove = _remove_intersecting_circles_for_country(df_country)
all_indices_to_remove.update(indices_to_remove)
new_df = df.drop(index=all_indices_to_remove)
return new_df
Isso melhorou significativamente o desempenho porque para verificar cada registro, precisamos apenas verificar todos os registros com o mesmo country_code
. Mas isso ainda faz muitas comparações desnecessárias
Depois de ter os círculos como polígonos, determinar as interseções entre os polígonos será muito rápido se você usar um índice espacial para fazer isso.
Então, você pode:
Resultado (vermelho: cidades originais, azul: cidades mantidas):
Código:
Da representação visual, a primeira coisa que me veio à mente foi usar uma abordagem de processamento de imagem com contornos mais externos (e mapear coordenadas geográficas para coordenadas de pixel). Não tenho certeza se é adequado para conjuntos de dados muito grandes com espaçamentos muito grandes entre cidades (mas por que alguém deveria ver se Lyon se sobrepõe a Paris?), mas nas amostras fornecidas perto de Paris, é vezes mais rápido do que a solução atual.
Estamos em O(n log(n)) + O(n) para complexidade de tempo comparado com o O(n^2 atual). No entanto, a principal compensação pode ser a criação de imagem com resolução suficiente. Seu processamento de contorno deve ser razoável considerando o OpenCV C++ otimizado sob o capô
Com o csv fornecido e o espaçamento da cidade, funcionou bem até (128, 128) na resolução da imagem. Na minha máquina, trabalhar em uma imagem (1024, 1024) ainda era mais rápido no tempo de execução do que
_remove_intersecting_circles_for_country
Uma maneira comum de reduzir a complexidade é sobrepor uma grade em cima do seu mapa. Por exemplo, sobreponha uma grade de quadrados que tenham 100 milhas de lado. Crie uma estrutura de dados que lhe dirá:
Supondo que nenhuma cidade tenha mais de 100 milhas de raio, uma cidade só pode cruzar cidades cujos centros são:
Na ilustração abaixo, a grade marcada com '0' é a grade que contém a cidade que você está examinando no momento. As únicas cidades com as quais ela pode possivelmente cruzar são aquelas em posições de grade marcadas com '1' ou '2'. Novamente, assumindo que o tamanho da sua grade é maior do que o tamanho de qualquer cidade individual.
Isso reduz muito o número de cidades que você precisa olhar quando estiver tentando determinar sobreposições. Em vez de 20.000 cidades, você terá... uma dúzia? Cem?
É um pequeno gasto adicional construir essa grade para que você possa agrupar seus itens, mas o benefício no desempenho é enorme.
Pode fazer sentido, quando você estiver construindo essa estrutura de dados, incluir para cada quadrado da grade não apenas quais cidades estão centralizadas nele, mas também quais cidades o invadem. Então , se "Smallville" estiver no quadrado da grade que você rotular, por exemplo, C2, e sua circunferência invadir C3, então C2 deve conter uma entrada que diga, na verdade, "Smallville está localizada em C2." C3 tem uma entrada que diz, "Smallville invade o quadrado da grade C3."
Essa informação é bem fácil de computar em tempo de execução quando você precisa dela, no entanto. Talvez mantê-la por perto seja desnecessário.
Atualizar
Pensando bem, como você tem a lat/lon para cada cidade, você já tem a maior parte do que precisa para construir a grade. Faça sua grade com um grau de latitude por um grau de longitude. No equador, isso lhe dá quadrados de aproximadamente 69 milhas (111 km) quadrados. Isso se comprime conforme você se aproxima dos polos, mas não deve importar. Para determinar a qual grade uma cidade pertence, você apenas corta as partes decimais da sua lat/lon e indexa na grade. Você pode precisar de alguma normalização dos números (ou seja, converter de -180...180 para 0...360).
De qualquer forma, isso permite reduzir o problema a um tamanho muito mais administrável.