Ruby: проверка массива на наличие уникальных значений, а также возврат минимального расстояния (формула гаверсинуса)

Мне трудно обдумать это.

У меня есть два CSV, которые составляют оптоволоконную сеть: один для широты, один для долготы. Они были взяты из файла KMZ, и оба CSV-файла состоят из 170 тыс. строк каждый из-за плохо построенного KMZ.

У меня есть CSV потенциальных клиентов, которых я хочу сравнить с оптоволоконной сетью. Если минимальное расстояние (рассчитанное по формуле Хаверсина) меньше 5280 футов, оно будет напечатано в выходной CSV-файл.

Я успешно справлялся с этим в прошлом, когда не было так много пар широта/долгота: 20 тысяч в прошлом, но теперь у нас есть 170 тысяч. Выходной CSV-файл становится огромным, как вы можете себе представить: 3 миллиона строк и больше.

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

require 'csv'
require 'haversine'

#this could be put into one file, works as is
fib_lat = CSV.read("swfl_fiber_lat.csv")
fib_long = CSV.read("swfl_fiber_long.csv")

#use zip to read both arrays at the same time
fib_coords = fib_lat.map(&:last).zip(fib_long.map(&:last))

#multiple column CSV with customer data, headers turned on
customers = CSV.read("swfl_1a_geocoded.csv", headers:true)

CSV.open('swfl-output-data-within-1mile.csv','w', :write_headers=> true, :headers => ['First Name','Last Name','Latitude','Longitude','Feet to Fiber','Address','City','State','Zip','County','Company','Title Code Description','PrimarySIC6 Description','Business Status Code Description','Phone Number','Tollfree Phonenumber','EmployeeSize Location Description','Sales Volume Location Decode','Telecommunications Expense','Email Address']) do |csv_object|
    fib_coords.each do |fib_lat, fib_long|
        customers.each do |cust|       
            if (Haversine.distance(cust[2].to_f, cust[3].to_f, fib_lat.to_f, fib_long.to_f).to_feet < 5280)
                data_out = ["#{cust[0]},#{cust[1]},#{cust[2].to_f},#{cust[3].to_f}, #{Haversine.distance(cust[2].to_f, cust[3].to_f, fib_lat.to_f, fib_long.to_f).to_feet.round(2)},#{cust[5]},#{cust[6]},#{cust[7]},#{cust[8]},#{cust[9]},#{cust[10]},#{cust[11]},#{cust[12]},#{cust[13]},#{cust[14]},#{cust[15]},#{cust[16]},#{cust[17]},#{cust[18]}"]            
                csv_object << data_out
            end
        end
    end
end 

Я пытаюсь придумать способ вернуть клиента (возможно, используя .uniq arr#min и только минимальный адрес для каждого клиента, не запихивая это в выходной CSV. Затем, если действительно есть расстояние ниже 5280 и связанный с ним клиент, поместите только это в выходной массив CSV.

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

Любое понимание приветствуется.


person Nubtacular    schedule 28.02.2017    source источник


Ответы (1)


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

1) Я бы не читал весь CSV-файл клиентов в память за один раз, а перебирал customers CSV-файл, используя метод CSV::for_each. Загрузка всего CSV-файла, вероятно, использует довольно много памяти, которую лучше использовать для массива fib_coords. Это означает обратный порядок циклов customers и fib_coords.

2) Во-вторых, вы можете избежать поиска по всему массиву fib_coords. Если вы отсортируете по первому столбцу, чтобы он был в порядке широты, рассчитайте минимально возможную широту (customer.latitude - 5280ft), найдите первое потенциальное совпадение в fib_coords, используя bsearch, что намного быстрее, чем линейный поиск, и выполните цикл по fib_coords оттуда до широты в fib_coords вне допустимого диапазона ( > customer.latitude + 5280ft).

person Marc Rohloff    schedule 01.03.2017
comment
Основная проблема с производительностью заключается в наличии гигантского выходного CSV-файла. 3-4 миллиона записей и попытка открыть их в Excel - это кошмар (и большой размер файла, около 0,5 ГБ, если не больше). Затем я бы вставил все эти строки в таблицу MySQL и использовал MIN() для foot_to_fiber и сгруппировал по адресу. Отправляюсь спать здесь, но утром первым делом попробую ваш подход. - person Nubtacular; 01.03.2017
comment
Вы говорите, что у вас от 3 до 4 миллионов клиентов? Длина выходного файла не должна иметь значения, несмотря на обработку многих клиентов. Если вы пытаетесь выполнить дальнейшую обработку, то это звучит так, как будто база данных была бы хорошей идеей, хотя я бы рассмотрел что-то вроде Postgres, у которого есть некоторые расширения для обработки географических координат. Я также успешно использовал ElasticSearch для этого. - person Marc Rohloff; 01.03.2017
comment
Начальное swfl_1a_geocoded.csv составляет ~10 000 клиентов. Происходит то, что каждый клиент в вышеупомянутом csv проходит через КАЖДУЮ пару широта/длина в волокне, поэтому возвращает несколько результатов в выходном CSV. Таким образом, Джейн Доу по адресу XYZ может иметь 150 выходных результатов на расстоянии менее 5280 футов. Но меня волнует только Джейн Доу на XYZ, которая является минимальной из всех результатов до 5280 футов. Поэтому я пытаюсь избежать вывода дубликатов клиентов в выходной файл и делаю некоторую проверку стиля уникальности через Ruby. - person Nubtacular; 01.03.2017
comment
Как я уже сказал в своем пункте (2), я бы поместил петлю клиентов снаружи. Если пары широта/долгота отсортированы, вы можете перебрать их минимальный набор и найти тот, у которого минимальное расстояние, используя вариант стандартного минимального кода. - person Marc Rohloff; 01.03.2017