Как найти наиболее близкие места для списка мест более эффективным способом?

1

Ищите помощь с алгоритмом для локальной машины или кластера (Python, R, JavaScript, любые языки).

У меня есть список местоположений с координатами.

# R script
n <- 10
set.seed(1)
index <- paste0("id_",c(1:n))
lat <- runif(n, 32.0, 41)
lon <- runif(n, 84, 112)*(-1)
values <- as.integer(runif(n, 50, 100))
df <- data.frame(index, lat, lon, values, stringsAsFactors = FALSE)
names(df) <- c('loc_id','lat','lon', 'value')

   loc_id      lat        lon value
1    id_1 34.38958  -89.76729    96
2    id_2 35.34912  -88.94359    60
3    id_3 37.15568 -103.23664    82
4    id_4 40.17387  -94.75490    56
5    id_5 33.81514 -105.55556    63
6    id_6 40.08551  -97.93558    69
7    id_7 40.50208 -104.09332    50
8    id_8 37.94718 -111.77337    69
9    id_9 37.66203  -94.64099    93
10  id_10 32.55608 -105.76847    67

Мне нужно найти 3 шкафа для каждого места в таблице.

Это мой код в R:

# R script
require(dplyr)
require(geosphere)

start.time <- Sys.time()
d1 <- df
sample <- 999999999999
distances <- list("init1" = sample, "init2" = sample, "init3" = sample)
d1$distances <- apply(d1, 1, function(x){distances})

n_rows = nrow(d1)
for (i in 1:(n_rows-1)) {
  # current location
  dot1 <- c(d1$lon[i], d1$lat[i])
  for (k in (i+1):n_rows) {
    # next location
    dot2 <- c(d1$lon[k], d1$lat[k])
    # distance between locations
    meters_between <- as.integer(distm(dot1, dot2, fun = distHaversine))

    # updating current location distances
    distances <- d1$distances[[i]]
    distances[d1$loc_id[k]] <- meters_between
    d1$distances[[i]] <- distances[order(unlist(distances), decreasing=FALSE)][1:3]

    # updating next location distances
    distances <- d1$distances[[k]]
    distances[d1$loc_id[i]] <- meters_between
    d1$distances[[k]] <- distances[order(unlist(distances), decreasing=FALSE)][1:3]
  }
}

Но это занимает слишком много времени:

# [1] "For 10 rows and 45 iterations takes 0.124729156494141 sec. Average sec 0.00277175903320313 per row."
# [1] "For 100 rows and 4950 iterations takes 2.54944682121277 sec. Average sec 0.000515039761861165 per row."
# [1] "For 200 rows and 19900 iterations takes 10.1178169250488 sec. Average sec 0.000508433011308986 per row."
# [1] "For 500 rows and 124750 iterations takes 73.7151870727539 sec. Average sec 0.000590903303188408 per row."

Я сделал то же самое в Python:

# Python script
import pandas as pd 
import numpy as np

n = 10
np.random.seed(1)
data_m = np.random.uniform(0, 5, 5)
data = {'loc_id':range(1, n+1), 
        'lat':np.random.uniform(32, 41, n),
        'lon':np.random.uniform(84, 112, n)*(-1),
        'values':np.random.randint(50, 100, n)}
df = pd.DataFrame(data)[['loc_id', 'lat', 'lon', 'values']]
df['loc_id'] = df['loc_id'].apply(lambda x: 'id_{0}'.format(x))
df = df.reset_index().drop('index', axis = 1).set_index('loc_id')

from geopy.distance import distance
from datetime import datetime 

start_time = datetime.now() 

sample = 999999999999
df['distances'] = np.nan
df['distances'] = df['distances'].apply(lambda x: [{'init1': sample}, {'init2': sample}, {'init3': sample}])

n_rows = len(df)

rows_done = 0
for i, row_i in df.head(n_rows-1).iterrows():
    dot1 = (row_i['lat'], row_i['lon'])
    rows_done = rows_done + 1
    for k, row_k in df.tail(n_rows-rows_done).iterrows():
        dot2 = (row_k['lat'], row_k['lon'])
        meters_between = int(distance(dot1,dot2).meters)
        distances = df.at[i, 'distances']
        distances.append({k: meters_between})
        distances_sorted = sorted(distances, key=lambda x: x[next(iter(x))])[:3]  
        df.at[i, 'distances'] = distances_sorted
        distances = df.at[k, 'distances']
        distances.append({i: meters_between})
        distances_sorted = sorted(distances, key=lambda x: x[next(iter(x))])[:3]
        df.at[k, 'distances'] = distances_sorted

print df

Почти такая же производительность.

Кто-нибудь знает, есть ли лучший подход? В моей задаче это нужно сделать для 90000 мест. Даже подумал о Hadoop/MpRc/Spark, но понятия не имею, как это сделать в распределенном режиме.

Я рад услышать любые идеи или предложения.

  • 0
    Кажется, что какой-то алгоритм k-ближайшего соседа должен быть в состоянии сделать это быстро, хотя я не знаю достаточно о широте / долготе, чтобы точно знать, как указать правильную меру расстояния или какие реализации knn рассчитали бы ее.
Теги:
latitude-longitude

3 ответа

4

Если евклидово расстояние хорошо, то nn2 использует kd-деревья и код C, поэтому он должен быть быстрым:

library(RANN)
nn2(df[2:3], k = 4)

Это заняло в среднем 0,06-0,11 секунды на моем не очень быстром ноутбуке для обработки n = 10 000 строк и в общей сложности от 1,00 до 1,25 секунды для 90 000 строк.

  • 0
    Я действительно впечатлен ... Никогда не слышал об этой библиотеке. Расчет приблизительный, но я считаю, что могу рассчитать 6 ближайших, пересчитать для них формулу Хаверсина, отсортировать и оставить первые 3 местоположения. Спасибо Г. Гротендик. Я скоро обновлю свой пост с вашим решением.
  • 0
    Проверьте: math.stackexchange.com/questions/29157/…
2

Я могу предложить решение python с scipy

from scipy.spatial import distance
from geopy.distance import vincenty
v=distance.cdist(df[['lat','lon']].values,df[['lat','lon']].values,lambda u, v: vincenty(u, v).kilometers)
np.sort(v,axis=1)[:,1:4]
Out[1033]: 
array([[384.09948155, 468.15944729, 545.41393271],
   [270.07677993, 397.21974571, 659.96238603],
   [384.09948155, 397.21974571, 619.616239  ],
   [203.07302273, 483.54687912, 741.21396029],
   [203.07302273, 444.49156394, 659.96238603],
   [437.31308598, 468.15944729, 494.91879983],
   [494.91879983, 695.91437812, 697.27399161],
   [270.07677993, 444.49156394, 483.54687912],
   [530.54946479, 626.29467739, 695.91437812],
   [437.31308598, 545.41393271, 697.27399161]])
  • 0
    Благодарю. Чистый код и в 8 раз лучшая производительность. Выборка из 1000 строк была сделана за 60 секунд вместо 500 секунд.
  • 0
    1000 строк - 499500 итераций (1 мин), 90000 строк - 4049955000 итераций (8108 мин). так грустно ... Я считаю, что я должен кластеризовать набор данных. И параллельно выполняйте вычисления внутри каждого кластера.
Показать ещё 3 комментария
0

Вот как решить эту проблему с помощью C++ и моей библиотеки GeographicLib (версия 1.47 или новее). Это использует истинные эллипсоидальные геодезические расстояния и дерево точек видимости для оптимизации поиска ближайших соседей.

#include <exception>
#include <vector>
#include <fstream>
#include <string>

#include <GeographicLib/NearestNeighbor.hpp>
#include <GeographicLib/Geodesic.hpp>

using namespace std;
using namespace GeographicLib;

// A structure to hold a geographic coordinate.
struct pos {
  string id;
  double lat, lon;
  pos(const string& _id = "", double _lat = 0, double _lon = 0) :
    id(_id), lat(_lat), lon(_lon) {}
};

// A class to compute the distance between 2 positions.
class DistanceCalculator {
private:
  Geodesic _geod;
public:
  explicit DistanceCalculator(const Geodesic& geod) : _geod(geod) {}
  double operator() (const pos& a, const pos& b) const {
    double d;
    _geod.Inverse(a.lat, a.lon, b.lat, b.lon, d);
    if ( !(d >= 0) )
      // Catch illegal positions which result in d = NaN
      throw GeographicErr("distance doesn't satisfy d >= 0");
    return d;
  }
};

int main() {
  try {
    // Read in pts
    vector<pos> pts;
    string id;
    double lat, lon;
    {
      ifstream is("pts.txt");   // lines of "id lat lon"
      if (!is.good())
        throw GeographicErr("pts.txt not readable");
      while (is >> id >> lon >> lat)
        pts.push_back(pos(id, lat, lon));
      if (pts.size() == 0)
        throw GeographicErr("need at least one location");
    }

    // Define a distance function object
    DistanceCalculator distance(Geodesic::WGS84());

    // Create NearestNeighbor object
    NearestNeighbor<double, pos, DistanceCalculator>
      ptsset(pts, distance);

    vector<int> ind;
    int n = 3;                  // Find 3 nearest neighbors
    for (unsigned i = 0; i < pts.size(); ++i) {
      ptsset.Search(pts, distance, pts[i], ind,
                    n, numeric_limits<double>::max(),
                    // exclude the point itself
                    0.0);
      if (ind.size() != n)
          throw GeographicErr("unexpected number of results");
      cout << pts[i].id;
      for (unsigned j = 0; j < ind.size(); ++j)
        cout << " " << pts[ind[j]].id;
      cout << "\n";
    }
    int setupcost, numsearches, searchcost, mincost, maxcost;
    double mean, sd;
    ptsset.Statistics(setupcost, numsearches, searchcost,
                      mincost, maxcost, mean, sd);
    long long
      totcost = setupcost + searchcost,
      exhaustivecost = ((pts.size() - 1) * pts.size())/2;
    cerr
      << "Number of distance calculations = " << totcost << "\n"
      << "With an exhaustive search = " << exhaustivecost << "\n"
      << "Ratio = " << double(totcost) / exhaustivecost << "\n"
      << "Efficiency improvement = "
      << 100 * (1 - double(totcost) / exhaustivecost) << "%\n";

  }
  catch (const exception& e) {
    cerr << "Caught exception: " << e.what() << "\n";
    return 1;
  }
}

Это читает в наборе точек (в форме "id lat lon") для pts.txt, помещает их в дерево VP. Затем для каждой точки он просматривает 3 ближайших соседа и печатает идентификатор и идентификатор соседей (ранжированных по расстоянию).

Скомпилируйте это, например,

g++ -O3 -o nearest nearest.cpp -lGeographic

Если pts.txt содержит 90000 точек, то вычисление завершается примерно через 6 секунд (или 70 мкс на точку) на моем домашнем компьютере после выполнения 3380000 дистанционных вычислений. Это примерно в 1200 раз эффективнее расчета грубой силы (все вычисления расстояний N (N - 1)/2).

Вы можете ускорить это (в несколько раз), используя грубое приближение к расстоянию (например, сферическое или евклидово); просто измените класс DistanceCalculator соответствующим образом. Например, эта версия DistanceCalculator возвращает сферическое расстояние в градусах:

// A class to compute the spherical distance between 2 positions.
class DistanceCalculator {
public:
  explicit DistanceCalculator(const Geodesic& /*geod*/) {}
  double operator() (const pos& a, const pos& b) const {
    double sphia, cphia, sphib, cphib, somgab, comgab;
    Math::sincosd(a.lat, sphia, cphia);
    Math::sincosd(b.lat, sphib, cphib);
    Math::sincosd(Math::AngDiff(a.lon, b.lon), somgab, comgab);
    return Math::atan2d(Math::hypot(cphia * sphib - sphia * cphib * comgab,
                                    cphib * somgab),
                        sphia * sphib + cphia * cphib * comgab);
  }
};

Но теперь у вас есть дополнительное бремя обеспечения достаточности аппроксимации. Я рекомендую просто использовать правильное геодезическое расстояние в первую очередь.

Детали реализации деревьев VP в GeographicLib приведены здесь.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню