Python: связанные компоненты на сфере
Я бился головой об этом уже некоторое время. Моя проблема очень проста для объяснения:
У меня есть данные, содержащие долготы и широты. Для простоты предположим, что это координаты городов. Я хочу разделить эти координаты города на группы, чтобы все города в пределах группы находились на заданном "максимальном расстоянии" от ближайшего соседа. У всех городов в группе должен быть хотя бы один сосед в пределах этого ограничения расстояния. Поэтому минимальное расстояние между этими разделенными группами больше, чем "максимальное расстояние", упомянутое выше.
Насколько я понимаю, это проблема кластеризации (например, минимальное связующее дерево). Расстояние на сфере может быть рассчитано с помощью расстояния хаверсин, но я не могу обдумать, как это реализовать... мое ограничение заключается в том, что я могу использовать только numpy, scipy и scikit-learn.
Я надеюсь, что кто-то может помочь, спасибо
3 ответа
Исходя из графического вывода в том виде, в каком оно содержится в вашем ответе, я считаю, что ваши кластеры преждевременно завершаются. Это мой подход к проблеме; код ужасен, потому что на самом деле я просто хотел продемонстрировать концепцию, и у меня нет времени думать о самом элегантном способе проиллюстрировать это. Кроме того, это не просто так, потому что тогда я мог бы украсть мою старую функцию вычисления расстояния, чтобы сэкономить мне время. Надеюсь, что концепция достаточно ясна, и вы увидите, как ее можно сделать быстрее и чище, например, не многократно перестраивая available_locations
и, возможно, не повторное сканирование элементов в кластере из предыдущей итерации.
Изменить: Иллюстрированное поведение:
1) Всегда сходится на одном и том же решении для каждого DISTANCE_CAP
независимо от всей рандомизации при инициализации и прогрессии решения 2) Модификация DISTANCE_CAP
может привести к кластерам в одном месте или гигантской капле
import math
from random import choice, shuffle
DISTANCE_CAP = 20
def crow_flies(lat1, lon1, lat2, lon2):
dx1,dy1 = (lat1/180)*3.141593,(lon1/180)*3.141593
dx2,dy2 = (lat2/180)*3.141593,(lon2/180)*3.141593
dlat,dlon = abs(dx2-dx1),abs(dy2-dy1)
a = (math.sin(dlat/2))**2 + (math.cos(dx1) * math.cos(dx2)
* (math.sin(dlon/2))**2)
c = 2*(math.atan2(math.sqrt(a),math.sqrt(1-a)))
km = 6373 * c
return km
# Aim: separate these back out
manchester = [[53.486286, -2.251476, 1],
[53.483586, -2.254534, 2],
[53.475158, -2.248011, 3],
[53.397161, -2.509189, 4]]
stoke = [[53.037375, -2.262903, 5],
[53.031031, -2.199587, 6]]
birmingham = [[52.443368, -1.975714, 7],
[52.429641, -1.902849, 8],
[52.483326, -1.817483, 9]]
# Mix them all together
combined_list = [item for item in manchester]
for item in stoke:
combined_list.append(item)
for item in birmingham:
combined_list.append(item)
shuffle(combined_list)
# Build a matrix:
matrix = {}
for item in combined_list:
for pair_item in combined_list:
if item[2] != pair_item[2]:
distance = crow_flies(item[0], item[1], pair_item[0], pair_item[1])
matrix[(item[2], pair_item[2])] = distance
# pick a random starting location
available_locations = [combined_list[x][2] for x in range(len(combined_list))]
start_loc = choice(available_locations)
available_locations = [a for a in available_locations if a != start_loc]
all_clusters = []
single_cluster = []
single_cluster.append(start_loc)
# RECURSIVELY add items to our cluster until it cannot get larger, then start a
# new one
cluster_got_bigger = True
while available_locations:
if cluster_got_bigger == True:
cluster_got_bigger = False
for loc in single_cluster:
for item in available_locations:
distance = matrix[(loc, item)]
if distance < DISTANCE_CAP:
single_cluster.append(item)
available_locations = [a for a in available_locations if a != item]
cluster_got_bigger = True
if cluster_got_bigger == False:
all_clusters.append(single_cluster)
single_cluster = []
new_seed = choice(available_locations)
single_cluster.append(new_seed)
available_locations = [a for a in available_locations if a != new_seed]
cluster_got_bigger = True
if not available_locations:
all_clusters.append(single_cluster)
print all_clusters
Итак, я применил метод грубой силы, чтобы решить эту проблему. Я не уверен на 100%, верны ли результаты во всех случаях, хотя... если бы у некоторых из вас было время проверить это, это было бы очень признательно.
import numpy as np
import matplotlib.pyplot as plt
# -------------------------------------------------------------------
def distance_sphere(lon1, lat1, lon2, lat2):
# Calculate distance on sphere
return np.degrees(np.arccos(np.sin(np.radians(lat1)) * np.sin(np.radians(lat2)) +
np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) *
np.cos(np.radians(lon1 - lon2))))
# -------------------------------------------------------------------
def distance_euclid(lon1, lat1, lon2, lat2):
# Calculate distance
return np.sqrt((lon1 - lon2)**2 + (lat1 - lat2)**2)
# -------------------------------------------------------------------
# Maximum allowed distance in degrees
max_distance = 10
# Generate city coordinates
lon_all = np.random.random(100) * 100
lat_all = np.random.random(100) * 100
# Start with as many groups as cities
group = np.arange(len(lon_all))
# Loop over all city coordinates
for lon, lat in zip(lon_all, lat_all):
# Calculate distance to all other cities
dis = distance_euclid(lon1=lon, lat1=lat, lon2=lon_all, lat2=lat_all)
# Get index of those which are within the given limits
idx = np.where(dis <= max_distance)[0]
# If there is no other city, we continue
if len(idx) == 0:
continue
# Set common group for all cities within the limits
for i in idx:
group[group == group[i]] = min(group[idx])
# Rewrite labels starting with 0
for old, new in zip(set(group), range(len(set(group)))):
idx = [i for i, j in enumerate(group) if j == old]
group[idx] = new
# -------------------------------------------------------------------
# Plot results
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=[10, 10])
for g, lon, lat in zip(group, lon_all, lat_all):
ax.annotate(str(g), xy=(lon, lat), xycoords="data", size=12, ha="center", va="center")
circ = plt.Circle((lon, lat), radius=max_distance/2, lw=0, color="gray")
ax.add_patch(circ)
ax.set_xlim(-10, 110)
ax.set_ylim(-10, 110)
plt.show()
Может мой ответ запоздал. Но быстрое решение состоит в том, чтобы построить сетевую структуру данных из ваших городов и получить связанные компоненты вашего графа:
- Каждый город является узлом
- Существует граница между двумя городами, если их расстояние между ними меньше некоторого порога.
Наконец, используйте какой-нибудь сетевой модуль Python (например , NetworkX).
Код будет примерно таким:
import networkx as nx
graph = nx.Graph()
# Add all vertices (cities) to the graph
for i, city in enumerate(cities):
graph.add_vertex(i)
# Add edges between cities that lie under a distance threshold
for i, city_one in enumerate(cities):
for j, city_two in enumerate(cities):
if j > i:
link_exists = calculate_distance(city_one, city_two) < threshold
if link_exists:
graph.add_edge(i,j)
# A list of sets, each set has the indices of cities
components = [c for c in sorted(nx.connected_components(G), reverse=False)]
Предполагается , что calculate_distance и пороговое значение известны, первое — это функция, а второе — пороговое значение расстояния.