Географическое расстояние по группе — применение функции к каждой паре строк

Я хочу рассчитать среднее географическое расстояние между несколькими домами в провинции.

Предположим, у меня есть следующие данные.

df1 <- data.frame(province = c(1, 1, 1, 2, 2, 2),
              house = c(1, 2, 3, 4, 5, 6),
              lat = c(-76.6, -76.5, -76.4, -75.4, -80.9, -85.7), 
              lon = c(39.2, 39.1, 39.3, 60.8, 53.3, 40.2))

Используя библиотеку geosphere, я могу найти расстояние между двумя домами. Например:

library(geosphere)
distm(c(df1$lon[1], df1$lat[1]), c(df1$lon[2], df1$lat[2]), fun = distHaversine)

#11429.1

Как рассчитать расстояние между всеми домами в провинции и получить среднее расстояние по провинции?

Исходный набор данных содержит миллионы наблюдений на каждую провинцию, поэтому производительность здесь также является проблемой.

sapply(split(df1, df1$province), dist)
M-- 11.04.2019 00:08

@M-M кажется, что это даст неверный результат?

Oka 11.04.2019 00:46

@Oka, он вычисляет евклидово расстояние. Посмотрите на тело функции и настройте его, чтобы получить желаемый результат. Это было бы лучшим с точки зрения производительности.

M-- 11.04.2019 00:51

@M-M согласен с производительностью, но не уверен, что dist может дать расстояние "гаверсинус", аналогичное тому, которое используется в вопросе.

Oka 11.04.2019 01:13

дист не может. Вам нужно использовать его код и написать свою собственную функцию.

M-- 11.04.2019 01:29

@M-M Как бы вы написали эту функцию?

wake_wake 11.04.2019 03:14

Посмотрите на functionbody(dist)

M-- 11.04.2019 03:20

Я склоняюсь к присуждению награды @Alexis. wake_wake, пожалуйста, дайте мне знать, что вы думаете, если хотите, к концу дня.

M-- 18.04.2019 18:03

Я только что принял ответ @Alexis. Это самое быстрое решение моей реальной проблемы.

wake_wake 18.04.2019 18:17

Мне также очень понравился @dww за то, что он думал не только о заданном вопросе, но и подходил к реальной проблеме.

wake_wake 18.04.2019 18:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
10
883
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Мои 10 центов. Ты сможешь:

# subset the province
df1 <- df1[which(df1$province==1),]

# get all combinations
all <- combn(df1$house, 2, FUN = NULL, simplify = TRUE)

# run your function and get distances for all combinations
distances <- c()
for(col in 1:ncol(all)) {
  a <- all[1, col]
  b <- all[2, col]
  dist <- distm(c(df1$lon[a], df1$lat[a]), c(df1$lon[b], df1$lat[b]), fun = distHaversine)
  distances <- c(distances, dist)
  }

# calculate mean:
mean(distances)
# [1] 15379.21

Это дает вам среднее значение для провинции, которое вы можете сравнить с результатами других методов. Например sapply который упоминался в комментариях:

df1 <- df1[which(df1$province==1),]
mean(sapply(split(df1, df1$province), dist))
# [1] 1.349036

Как видите, это дает разные результаты, потому что функция dist может вычислять расстояния разного типа (например, евклидовы) и не может вычислять гаверсинус или другие «геодезические» расстояния. В пакете geodist есть варианты, которые могут приблизить вас к sapply:

library(geodist)
library(magrittr)

# defining the data
df1 <- data.frame(province = c(1, 1, 1, 2, 2, 2),
                  house = c(1, 2, 3, 4, 5, 6),
                  lat = c(-76.6, -76.5, -76.4, -75.4, -80.9, -85.7), 
                  lon = c(39.2, 39.1, 39.3, 60.8, 53.3, 40.2))

# defining the function 
give_distance <- function(resultofsplit){
  distances <- c()
  for (i in 1:length(resultofsplit)){
    sdf <- resultofsplit
    sdf <- sdf[[i]]
    sdf <- sdf[c("lon", "lat", "province", "house")]

    sdf2 <- as.matrix(sdf)
    sdf3 <- geodist(x=sdf2, measure = "haversine")
    sdf4 <- unique(as.vector(sdf3))
    sdf4 <- sdf4[sdf4 != 0]        # this is to remove the 0-distances 
    mean_dist <- mean(sdf4)
    distances <- c(distances, mean_dist)
    }  
    return(distances)
}

split(df1, df1$province) %>% give_distance()
#[1]  15379.21 793612.04

Например. функция даст вам средние значения расстояния для каждой провинции. Теперь мне не удалось заставить give_distance работать с sapply, но это уже должно быть более эффективно.

В отношении этого нить векторизованное решение вашей проблемы будет выглядеть следующим образом:

toCheck <- sapply(split(df1, df1$province), function(x){
                                            combn(rownames(x), 2, simplify = FALSE)})

names(toCheck) <- sapply(toCheck, paste, collapse = " - ")


sapply(toCheck, function(x){
               distm(df1[x[1],c("lon","lat")], df1[x[2],c("lon","lat")], 
                     fun = distHaversine)
                           })


  #    1 - 2      1 - 3      2 - 3      4 - 5      4 - 6      5 - 6 
  # 11429.10   22415.04   12293.48  634549.20 1188925.65  557361.28 

Это работает, если количество записей для каждой провинции одинаково. Если это не так, то вторая часть для присвоения соответствующих имен toCheck и того, как мы их используем в конце, должна быть изменена по мере изменения структуры списка toCheck. Однако он не заботится о порядке набора данных.


для вашего фактического набора данных toCheck станет вложенным списком, поэтому вам нужно настроить функцию, как показано ниже; Я не сделал toCheck имена чистыми для этого решения. (df2 можно найти в конце ответа).

df2 <- df2[order(df2$province),] #sorting may even improve performance
names(toCheck) <- paste("province", unique(df2$province))

toCheck <- sapply(split(df2, df2$province), function(x){
                                            combn(rownames(x), 2, simplify = FALSE)})

sapply(toCheck, function(x){ sapply(x, function(y){
  distm(df2[y[1],c("lon","lat")], df2[y[2],c("lon","lat")], fun = distHaversine)
})})

# $`province 1`
# [1]   11429.10   22415.04 1001964.84   12293.48 1013117.36 1024209.46
# 
# $`province 2`
# [1]  634549.2 1188925.7  557361.3
# 
# $`province 3`
# [1] 590083.2
# 
# $`province 4`
# [1] 557361.28 547589.19  11163.92

Вы также можете получить mean() для каждой провинции. Кроме того, если вам нужно, не должно быть сложно переименовать элементы вложенных списков, чтобы вы могли сказать, что каждое расстояние соответствует какому дому.

df2 <- data.frame(province = c(1, 1, 1, 2, 2, 2, 1, 3, 3, 4,4,4),
                  house = c(1, 2, 3, 4, 5, 6, 7, 10, 9, 8, 11, 12),
                  lat = c(-76.6, -76.5, -76.4, -75.4, -80.9, -85.7, -85.6, -76.4, -75.4, -80.9, -85.7, -85.6), 
                  lon = c(39.2, 39.1, 39.3, 60.8, 53.3, 40.2, 40.1, 39.3, 60.8, 53.3, 40.2, 40.1))

Интересно! Количество записей на провинцию сильно меняется, поэтому его действительно нужно немного подправить.

wake_wake 12.04.2019 18:11

@wake_wake Я обновляю свой ответ. Я не уверен, что это будет пример предсказания ветвления, но я все равно отсортировал фрейм данных. Если кто-то не предложит решение оптимизированныйdata.table, это должно быть лучшим с точки зрения производительности. Удачи.

M-- 12.04.2019 20:05

Это отлично работает для фиктивных данных и (части) исходных данных. Тем не менее, в какой-то момент toCheck становится настолько большим, что исчерпывает память моей локальной машины на 128 ГБ.

wake_wake 12.04.2019 23:11

@wake_wake при обработке больших данных имеет свои проблемы и методы, которые не входят в рамки этого вопроса. Лучше всего, не пачкая рук решениями более высокого уровня, было бы разбить ваши данные на более мелкие фрагменты, выполнить решение, описанное выше, очистить память и перейти к следующему фрагменту.

M-- 12.04.2019 23:14

Решение:

lapply(split(df1, df1$province), function(df){
  df <- Expand.Grid(df[, c("lat", "lon")], df[, c("lat", "lon")])
  mean(distHaversine(df[, 1:2], df[, 3:4]))
})

где Expand.Grid() взято из https://stackoverflow.com/a/30085602/3502164.

Объяснение:

1. Производительность

Я бы не стал использовать distm(), так как он преобразует функцию векторизованныйdistHaversine() в невекторизованную distm(). Если вы посмотрите на исходный код, вы увидите:

function (x, y, fun = distHaversine) 
{
   [...]
   for (i in 1:n) {
        dm[i, ] = fun(x[i, ], y)
    }
    return(dm)
}

В то время как distHaversine() отправляет «целый объект» в C, distm() отправляет данные «по строкам» в distHaversine() и, следовательно, заставляет distHaversine() делать то же самое при выполнении кода в C. Поэтому distm() не следует использовать. С точки зрения производительности я вижу больше вреда от использования функции-оболочки distm(), чем преимуществ.

2. Объяснение кода в «решении»:

а) Разделение на группы:

Вы хотите проанализировать данные по группе: провинция. Разделить на группы можно: split(df1, df1$province).

б) Группировка «сгустков столбцов»

Вы хотите найти все уникальные комбинации широты и долготы. Первое предположение может быть expand.grid(), но это не работает для нескольких столбцов. К счастью, мистер Флик позаботился об этом Функция expand.grid для data.frames в R.

Тогда у вас есть data.frame() всех возможных комбинаций, и вам просто нужно использовать mean(distHaversine(...)).

Ответ принят как подходящий

Моя первоначальная идея состояла в том, чтобы просмотреть исходный код distHaversine и воспроизвести его в функции, которую я буду использовать с proxy. Это будет работать следующим образом (обратите внимание, что lon должен быть первым столбцом):

library(geosphere)
library(dplyr)
library(proxy)

df1 <- data.frame(province = as.integer(c(1, 1, 1, 2, 2, 2)),
                  house = as.integer(c(1, 2, 3, 4, 5, 6)),
                  lat = c(-76.6, -76.5, -76.4, -75.4, -80.9, -85.7), 
                  lon = c(39.2, 39.1, 39.3, 60.8, 53.3, 40.2))

custom_haversine <- function(x, y) {
  toRad <- pi / 180

  diff <- (y - x) * toRad
  dLon <- diff[1L]
  dLat <- diff[2L]

  a <- sin(dLat / 2) ^ 2 + cos(x[2L] * toRad) * cos(y[2L] * toRad) * sin(dLon / 2) ^ 2
  a <- min(a, 1)
  # return
  2 * atan2(sqrt(a), sqrt(1 - a)) * 6378137
}

pr_DB$set_entry(FUN=custom_haversine, names = "haversine", loop=TRUE, distance=TRUE)

average_dist <- df1 %>%
  select(-house) %>%
  group_by(province) %>%
  group_map(~ data.frame(avg=mean(proxy::dist(.x[ , c("lon", "lat")], method = "haversine"))))

Однако, если вы ожидаете миллионы строк на провинцию, proxy вероятно не сможет выделить промежуточные (нижние треугольные) матрицы. Поэтому я перенес код на C++ и в качестве бонуса добавил многопоточность:

РЕДАКТИРОВАТЬ: оказывается, помощник s2d был далек от оптимального, в этой версии теперь используются формулы, заданные здесь.

РЕДАКТИРОВАТЬ2: Я только что узнал о RcppThread, и его можно использовать для обнаружения пользовательского прерывания.

// [[Rcpp::plugins(cpp11)]]
// [[Rcpp::depends(RcppParallel,RcppThread)]]

#include <cstddef> // size_t
#include <math.h> // sin, cos, sqrt, atan2, pow
#include <vector>

#include <RcppThread.h>
#include <Rcpp.h>
#include <RcppParallel.h>

using namespace std;
using namespace Rcpp;
using namespace RcppParallel;

// single to double indices for lower triangular of matrices without diagonal
void s2d(const size_t id, const size_t nrow, size_t& i, size_t& j) {
  j = nrow - 2 - static_cast<size_t>(sqrt(-8 * id + 4 * nrow * (nrow - 1) - 7) / 2 - 0.5);
  i = id + j + 1 - nrow * (nrow - 1) / 2 + (nrow - j) * ((nrow - j) - 1) / 2;
}

class HaversineCalculator : public Worker
{
public:
  HaversineCalculator(const NumericVector& lon,
                      const NumericVector& lat,
                      double& avg,
                      const int n)
    : lon_(lon)
    , lat_(lat)
    , avg_(avg)
    , n_(n)
    , cos_lat_(lon.length())
  {
    // terms for distance calculation
    for (size_t i = 0; i < cos_lat_.size(); i++) {
      cos_lat_[i] = cos(lat_[i] * 3.1415926535897 / 180);
    }
  }

  void operator()(size_t begin, size_t end) {
    // for Kahan summation
    double sum = 0;
    double c = 0;

    double to_rad = 3.1415926535897 / 180;

    size_t i, j;
    for (size_t ind = begin; ind < end; ind++) {
      if (RcppThread::isInterrupted(ind % static_cast<int>(1e5) == 0)) return;

      s2d(ind, lon_.length(), i, j);

      // haversine distance
      double d_lon = (lon_[j] - lon_[i]) * to_rad;
      double d_lat = (lat_[j] - lat_[i]) * to_rad;
      double d_hav = pow(sin(d_lat / 2), 2) + cos_lat_[i] * cos_lat_[j] * pow(sin(d_lon / 2), 2);
      if (d_hav > 1) d_hav = 1;
      d_hav = 2 * atan2(sqrt(d_hav), sqrt(1 - d_hav)) * 6378137;

      // the average part
      d_hav /= n_;

      // Kahan sum step
      double y = d_hav - c;
      double t = sum + y;
      c = (t - sum) - y;
      sum = t;
    }

    mutex_.lock();
    avg_ += sum;
    mutex_.unlock();
  }

private:
  const RVector<double> lon_;
  const RVector<double> lat_;
  double& avg_;
  const int n_;
  tthread::mutex mutex_;
  vector<double> cos_lat_;
};

// [[Rcpp::export]]
double avg_haversine(const DataFrame& input, const int nthreads) {
  NumericVector lon = input["lon"];
  NumericVector lat = input["lat"];

  double avg = 0;
  int size = lon.length() * (lon.length() - 1) / 2;
  HaversineCalculator hc(lon, lat, avg, size);

  int grain = size / nthreads / 10;
  RcppParallel::parallelFor(0, size, hc, grain);
  RcppThread::checkUserInterrupt();

  return avg;
}

Этот код не будет выделять промежуточную матрицу, он просто рассчитает расстояние для каждой пары того, что будет нижним треугольником, и в конце аккумулирует значения для среднего значения. См. здесь для части суммирования Кахана.

Если вы сохраните этот код, скажем, в haversine.cpp, то вы можете сделать следующее:

library(dplyr)
library(Rcpp)
library(RcppParallel)
library(RcppThread)

sourceCpp("haversine.cpp")

df1 %>%
  group_by(province) %>%
  group_map(~ data.frame(avg=avg_haversine(.x, parallel::detectCores())))
# A tibble: 2 x 2
# Groups:   province [2]
  province     avg
     <int>   <dbl>
1        1  15379.
2        2 793612.

Вот еще проверка на вменяемость:

pr_DB$set_entry(FUN=geosphere::distHaversine, names = "distHaversine", loop=TRUE, distance=TRUE)

df1 %>%
  select(-house) %>%
  group_by(province) %>%
  group_map(~ data.frame(avg=mean(proxy::dist(.x[ , c("lon", "lat")], method = "distHaversine"))))

Однако предостережение:

df <- data.frame(lon=runif (1e3, -90, 90), lat=runif (1e3, -90, 90))

system.time(proxy::dist(df, method = "distHaversine"))
   user  system elapsed 
 34.353   0.005  34.394

system.time(proxy::dist(df, method = "haversine"))
   user  system elapsed 
  0.789   0.020   0.809

system.time(avg_haversine(df, 4L))
   user  system elapsed 
  0.054   0.000   0.014

df <- data.frame(lon=runif (1e5, -90, 90), lat=runif (1e5, -90, 90))

system.time(avg_haversine(df, 4L))
   user  system elapsed 
 73.861   0.238  19.670

Вам, вероятно, придется подождать довольно долго, если у вас есть миллионы строк...

Я также должен упомянуть, что невозможно обнаружить прерывание пользователя внутри потоков, созданных через RcppParallel, поэтому, если вы начинаете расчет, вы должны либо дождаться его завершения, либо или полностью перезапустите R/RStudio. См. РЕДАКТИРОВАТЬ2 выше.


Относительно сложности

В зависимости от ваших фактических данных и количества ядер вашего компьютера, вы вполне можете в конечном итоге ждать несколько дней, пока расчет не будет завершен. Эта задача имеет квадратичную сложность (по провинции, так сказать). Эта строка:

int size = lon.length() * (lon.length() - 1) / 2;

означает количество (гаверсинус) вычислений расстояния, которые должны быть выполнены. Таким образом, если количество строк увеличивается в n раз, количество вычислений увеличивается в n^2 / 2 раз, грубо говоря.

Это невозможно оптимизировать; вы не можете рассчитать среднее N чисел без фактического вычисления каждого числа, и вам будет трудно найти что-то более быстрое, чем многопоточный код C++, так что вам придется либо подождать, или кинуть больше ядер на проблему, либо с одной машиной, либо со многими машинами, работающими вместе. Иначе эту проблему не решить.

это намного быстрее, чем мое решение, когда количество строк увеличивается по величине (1e3, 1e4,...)

Etienne Kintzler 15.04.2019 22:16

@Alexis запускает это на сервере, я получаю сообщение об ошибке could not find function "group_map", в то время как у меня есть dplyr и все остальные прикрепленные пакеты. Есть идеи, почему это всплывает?

wake_wake 19.04.2019 19:01

@ Алексис Ах, dplyr устарела. Не могу обновиться до новой версии, потому что для этого требуется обновленная версия BH, которая, похоже, недоступна для Microsoft R Open. Любые идеи о том, как справиться с этим препятствием?

wake_wake 19.04.2019 19:34

@wake_wake Сейчас я далеко от своего компьютера, но раньше можно было использовать do, поэтому я предполагаю, что на последнем шаге вы могли бы вместо этого использовать что-то вроде do(data.frame(province=.$province[1L], avg=avg_haversine(., parallel::detectCores()))).

Alexis 19.04.2019 20:38

@Алексис Спасибо! Я только что исправил это, обновив 10 пакетов вручную. Но ваш комментарий полезен для тех, кто пока застрял со старым dplyr. Спасибо!

wake_wake 19.04.2019 20:40

@wake_wake Я нашел способ обнаружить прерывание пользователя, если вы сочтете это полезным. Смотрите редактирование.

Alexis 27.04.2019 23:26

Учитывая, что ваши данные содержат миллионы строк, это звучит как проблема «XY». т.е. ответ, который вам действительно нужен, не является ответом на вопрос, который вы задали.

Приведу аналогию: если вы хотите узнать среднюю высоту деревьев в лесу, вы не будете измерять каждое дерево. Вы просто измеряете достаточно большую выборку, чтобы убедиться, что ваша оценка имеет достаточно высокую вероятность быть настолько близкой к истинному среднему, насколько вам нужно.

Выполнение расчета методом грубой силы с использованием расстояния от каждого дома до каждого второго дома не только потребует чрезмерных ресурсов (даже с оптимизированным кодом), но и даст гораздо больше знаков после запятой, чем вам может понадобиться, или оправдано точностью данных (Координаты GPS обычно точны в лучшем случае с точностью до нескольких метров).

Поэтому я бы порекомендовал выполнять расчеты с размером выборки, который настолько велик, насколько это необходимо для уровня точности, которого требует ваша задача. Например, в следующем примере будет получена оценка для двух миллионов строк с точностью до 4 значащих цифр всего за несколько секунд. Вы можете повысить точность, увеличив размер выборки, но, учитывая неопределенность самих GPS-координат, я сомневаюсь, что это оправдано.

sample.size=1e6    
lapply(split(df1[3:4], df1$province), 
  function(x) {
    s1 = x[sample(nrow(x), sample.size, T), ]
    s2 = x[sample(nrow(x), sample.size, T), ]
    mean(distHaversine(s1, s2))
  })

Некоторые большие данные для тестирования:

N=1e6
df1 <- data.frame(
  province = c(rep(1,N),rep(2,N)),
  house = 1:(2*N),
  lat = c(rnorm(N,-76), rnorm(N,-85)), 
  lon = c(rnorm(N,39), rnorm(N,-55,2)))

Чтобы получить представление о точности этого метода, мы можем использовать начальную загрузку. Для следующей демонстрации я использую всего 100 000 строк данных, чтобы мы могли выполнить 1000 итераций начальной загрузки за короткое время:

N=1e5
df1 <- data.frame(lat = rnorm(N,-76,0.1), lon = rnorm(N,39,0.1))

dist.f = function(i) {
    s1 = df1[sample(N, replace = T), ]
    s2 = df1[sample(N, replace = T), ]
    mean(distHaversine(s1, s2))
    }

boot.dist = sapply(1:1000, dist.f)
mean(boot.dist)
# [1] 17580.63
sd(boot.dist)
# [1] 29.39302

hist(boot.dist, 20) 

т.е. для этих тестовых данных среднее расстояние составляет 17 580 +/- 29 м. Это коэффициент вариации 0,1%, что, вероятно, достаточно точно для большинства целей. Как я уже сказал, вы можете добиться большей точности, увеличив размер выборки, если вам это действительно нужно.

Это хороший момент. Что вы думаете о группировании домов на основе их координации, а не «неконтролируемой» выборки?

M-- 15.04.2019 04:37

Я не уверен, что вы подразумеваете под «кластеризацией по координации». Но если цель состоит в том, чтобы оценить среднее расстояние для всей популяции, вам следует произвести случайную выборку для всей популяции. Если вы заинтересованы в дезагрегировании результатов по какой-либо категоризации, тогда сделайте выборку в интересующих категориях.

dww 15.04.2019 14:02

Скажем, вычисление среднего значения широты/долготы для соседних домов!

M-- 15.04.2019 23:07

Я не думаю, что это сработает, так как в кластерах будет разное количество домов, что сбивает расчеты. Я по-прежнему считаю, что выборка всего населения является лучшей. Я добавил в ответ некоторые статистические данные, чтобы лучше понять, насколько хорошо работает этот метод.

dww 16.04.2019 00:56

Можете ли вы уточнить это другое количество домов? Я просто не могу понять это, так как я уже шел по этому пути для исследований O / D, и это доказало свою эффективность.

M-- 16.04.2019 01:00

Представьте себе простой пример — два дома стоят рядом друг с другом (скажем, в метре друг от друга) и один дом в сотне километров. Если вы сначала сгруппируете, а затем возьмете среднее расстояние между кластерами, вы получите 100 км в качестве среднего. Но истинный ответ должен быть (100+100+1)/3 или 67 км. Таким образом, проблема заключается не только в том, что кластеры имеют разные номера, но и в том, что кластеризация игнорирует расстояние между домами в кластерах при получении среднего значения.

dww 16.04.2019 01:10

Ваш простой пример ошибочен. Давайте используем его для решения случайной выборки. Вы берете только два дома, расположенных близко друг к другу, и вычисляете расстояние равным 1. Вы просто не можете уменьшить размер набора данных, чтобы представить или отвергнуть достоверность решений.

M-- 16.04.2019 15:35

Здесь не место для длительного обсуждения кластеризации (которая не является частью исходного вопроса). Если вы хотите узнать об этом подробнее, возможно, было бы лучше начать новый вопрос о перекрестной проверке. Однако я хотел бы отметить здесь, что мой пример не имеет недостатков и работает с методом выборки - вам просто нужно иметь достаточно большую выборку. Если вы проверите 3 дома 1 миллион раз с заменой (аналогично моему примеру), вы наверняка получите ответ, очень близкий к истинному среднему.

dww 16.04.2019 15:41

Вы можете использовать векторизованную версию расстояния гаверсинуса, например:

dist_haversine_for_dfs <- function (df_x, df_y, lat, r = 6378137) 
{
  if (!all(c("lat", "lon") %in% names(df_x))) {
    stop("parameter df_x does not have column 'lat' and 'lon'")
  }
  if (!all(c("lat", "lon") %in% names(df_y))) {
    stop("parameter df_x does not have column 'lat' and 'lon'")
  }
  toRad <- pi/180
  df_x <- df_x * toRad
  df_y <- df_y * toRad
  dLat <- df_y[["lat"]] - df_x[["lat"]]
  dLon <- df_y[["lon"]] - df_x[["lon"]]
  a <- sin(dLat/2) * sin(dLat/2) + cos(df_x[["lat"]]) * cos(df_y[["lat"]]) * 
    sin(dLon/2) * sin(dLon/2)
  a <- pmin(a, 1)
  dist <- 2 * atan2(sqrt(a), sqrt(1 - a)) * r
  return(dist)
}

Затем с помощью data.table и пакета arrangements (для более быстрой генерации комбинаций) можно сделать следующее:

library(data.table)
dt <- data.table(df1)
ids <- dt[, {
  comb_mat <- arrangements::combinations(x = house, k = 2)
  list(house_x = comb_mat[, 1],
       house_y = comb_mat[, 2])}, by = province]

jdt <- cbind(ids, 
             dt[ids$house_x, .(lon_x=lon, lat_x=lat)], 
             dt[ids$house_y, .(lon_y=lon, lat_y=lat)])

jdt[, dist := dist_haversine_for_dfs(df_x = jdt[, .(lon = lon.x, lat = lat.x)],
                                     df_y = jdt[, .(lon = lon.y, lat = lat.y)])]

jdt[, .(mean_dist = mean(dist)), by = province]

который выводит

   province mean_dist
1:        1  15379.21
2:        2 793612.04

Ниже я добавляю решение с использованием пакета пространственного риска. Основные функции этого пакета написаны на C++ (Rcpp) и поэтому работают очень быстро.

library(data.table)
library(tidyverse)
library(spatialrisk)
library(optiRum)

# Expand grid
grid <- function(x){
  df <- x[, lat, lon]
  optiRum::CJ.dt(df, df)
}

Поскольку каждый элемент вывода представляет собой фрейм данных, purrr::map_dfr используется для связывания их вместе:

data.table(df1) %>%
  split(.$province) %>%
  map_dfr(grid, .id = "province") %>%
  mutate(distm = spatialrisk::haversine(lat, lon, i.lat, i.lon)) %>%
  filter(distm > 0) %>%
  group_by(province) %>%
  summarize(distm_mean = mean(distm))

Выход:

  province distm_mean
  <chr>         <dbl>
1 1            15379.
2 2           793612.

Другие вопросы по теме