Повышение эффективности по сравнению с итерацией по двум большим информационным рамкам Pandas. - PullRequest
0 голосов
/ 27 ноября 2018

У меня есть два ОГРОМНЫХ кадра данных Pandas со значениями на основе местоположения, и мне нужно обновить df1 ['count'], указав количество записей из df2, которые меньше 1000 м от каждой точки в df1.

Вот пример моих данных, импортированных в Pandas

df1 =       lat      long    valA   count
        0   123.456  986.54  1      0
        1   223.456  886.54  2      0
        2   323.456  786.54  3      0
        3   423.456  686.54  2      0
        4   523.456  586.54  1      0

df2 =       lat      long    valB
        0   123.456  986.54  1
        1   223.456  886.54  2
        2   323.456  786.54  3
        3   423.456  686.54  2
        4   523.456  586.54  1

В действительности, df1 имеет ~ 10 миллионов строк, а df2 имеет ~ 1 миллион

Я создалрабочий вложенный цикл FOR с использованием метода Pandas DF.itertuples (), который отлично работает для небольших наборов тестовых данных (df1 = 1k строк и df2 = 100 строк занимает около часа), но полный набор данных экспоненциально больше, изаймет годы, чтобы завершить на основе моих расчетов.Вот мой рабочий код ...

import pandas as pd
import geopy.distance as gpd

file1 = 'C:\\path\\file1.csv'    
file2 = 'C:\\path\\file2.csv' 

df1 = pd.read_csv(file1)
df2 = pd.read_csv(file2)

df1.sort_values(['long', 'lat']), inplace=True) 
df2.sort_values(['long', 'lat']), inplace=True)

for irow in df1.itertuples():    
     count = 0
     indexLst = []        
     Location1 = (irow[1], irow[2])    

     for jrow in df2.itertuples():  
          Location2 = (jrow[1], jrow[2])                                      
          if gpd.distance(Location1, Location2).kilometers < 1:
             count += 1
             indexLst.append(jrow[0])    
     if count > 0:                  #only update DF if a match is found
         df1.at[irow[0],'count'] = (count)      
         df2.drop(indexLst, inplace=True)       #drop rows already counted from df2 to speed up next iteration

 #save updated df1 to new csv file
 outFileName = 'combined.csv'
 df1.to_csv(outFileName, sep=',', index=False)

Каждая точка в df2 должна быть посчитана только один раз, так как точки в df1 расположены равномерно.С этой целью я добавил опущенную статистику для удаления строк из df2 после их подсчета в надежде улучшить время итерации.Я также попытался сначала создать оператор слияния / объединения вместо вложенных циклов, но безуспешно.

На этом этапе любая помощь в повышении эффективности здесь очень ценится!

Редактировать: Цель состоит в том, чтобы обновить столбец 'count' в df1 (как показано ниже) с количеством очков отdf2, которые <1 км, и вывод в новый файл.</p>

df1 =       lat      long    valA   count
        0   123.456  986.54  1      3
        1   223.456  886.54  2      1
        2   323.456  786.54  3      9
        3   423.456  686.54  2      2
        4   523.456  586.54  1      5

Ответы [ 2 ]

0 голосов
/ 27 ноября 2018

В последнее время я делал нечто подобное, но не с lat, lon, и мне нужно было только найти ближайшую точку и ее расстояние.Для этого я использовал пакет scipy.spatial.cKDTree .Это было довольно быстро. cKDTree

Я думаю, что в вашем случае вы могли бы использовать функцию query_ball_point () .

from scipy import spatial
import pandas as pd

file1 = 'C:\\path\\file1.csv'    
file2 = 'C:\\path\\file2.csv' 

df1 = pd.read_csv(file1)
df2 = pd.read_csv(file2)
# Build the index
tree = spatial.cKDTree(df1[['long', 'lat']])
# Then query the index

Вы должны попробовать.

0 голосов
/ 27 ноября 2018

Делая подобные вещи часто, я нашел пару лучших практик:

1) Старайтесь использовать как можно больше numpy и numba

2) Попробуйте использовать рычагимаксимально возможное распараллеливание

3) Пропустите циклы для векторизованного кода (здесь мы используем цикл с numba для усиления распараллеливания).

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

import numpy as np
from geopy import distance

origin = (np.random.uniform(-90,90), np.random.uniform(-180,180))
dest = (np.random.uniform(-90,90), np.random.uniform(-180,180))

%timeit distance.distance(origin, dest)

216 мкс ± 363 нс на цикл(среднее ± стандартное отклонение из 7 запусков, по 1000 циклов в каждом)

Это означает, что на этом временном интервале для вычисления расстояний 10 миллионов x 1 миллион потребуется приблизительно 2160000000 секунд или 600 тысяч часов.Даже параллелизм только очень поможет.

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

from numba import jit, prange, vectorize

@vectorize
def haversine(s_lat,s_lng,e_lat,e_lng):

    # approximate radius of earth in km
    R = 6373.0

    s_lat = s_lat*np.pi/180.0                      
    s_lng = np.deg2rad(s_lng)     
    e_lat = np.deg2rad(e_lat)                       
    e_lng = np.deg2rad(e_lng)  

    d = np.sin((e_lat - s_lat)/2)**2 + np.cos(s_lat)*np.cos(e_lat) * np.sin((e_lng - s_lng)/2)**2

    return 2 * R * np.arcsin(np.sqrt(d))

%timeit haversine(origin[0], origin[0], dest[1], dest[1])

1,85 мкс ± 53,9 нс на цикл (среднее ± стандартное отклонение из 7 циклов, по 100000 циклов в каждом)

Это уже 100-кратное улучшение.Но мы можем сделать лучше.Возможно, вы заметили @vectorize декоратор, который я добавил из numba.Это позволяет ранее скалярной функции Хаверсайна векторизоваться и принимать векторы в качестве входных данных.Мы воспользуемся этим на следующем шаге:

@jit(nopython=True, parallel=True)
def get_nearby_count(coords, coords2, max_dist):
    '''
    Input: `coords`: List of coordinates, lat-lngs in an n x 2 array
           `coords2`: Second list of coordinates, lat-lngs in an k x 2 array
           `max_dist`: Max distance to be considered nearby
    Output: Array of length n with a count of coords nearby coords2
    '''
    # initialize
    n = coords.shape[0]
    k = coords2.shape[0]
    output = np.zeros(n)

    # prange is a parallel loop when operations are independent
    for i in prange(n):
        # comparing a point in coords to the arrays in coords2
        x, y = coords[i]
        # returns an array of length k
        dist = haversine(x, y, coords2[:,0], coords2[:,1])
        # sum the boolean of distances less than the max allowable
        output[i] = np.sum(dist < max_dist)

    return output

Надеемся, теперь у вас будет массив, равный длине первого набора координат (10 миллионов в вашем случае).Затем вы можете просто назначить это для вашего фрейма данных в качестве количества!

Время теста 100 000 x 10 000:

n = 100_000
k = 10_000

coords1 = np.zeros((n, 2))
coords2 = np.zeros((k, 2))

coords1[:,0] = np.random.uniform(-90, 90, n)
coords1[:,1] = np.random.uniform(-180, 180, n)
coords2[:,0] = np.random.uniform(-90, 90, k)
coords2[:,1] = np.random.uniform(-180, 180, k)

%timeit get_nearby_count(coords1, coords2, 1.0)

2,45 с ± 73,2 мс на цикл (среднее ± стандартное отклонение от7 прогонов, 1 цикл каждый)

К сожалению, это все еще означает, что вы будете смотреть на что-то около 20 000 с лишним секунд.И это было на машине с 80 ядрами (с использованием 76ish, на основе top использования).

Это лучшее, что я могу сделать на данный момент, удачи (также, первое сообщение, спасибо за то, что вдохновили меня внести свой вклад!)

PS: Вы можете также изучить массивы Dask и функцию map_block (), чтобы распараллелить эту функцию (вместо того, чтобы полагаться на prange).То, как вы разбиваете данные, может повлиять на общее время выполнения.

PPS: 1 000 000 x 100 000 (в 100 раз меньше, чем у вашего полного набора): 3 мин 27 с (207 секунд), поэтому масштабирование выглядит линейным и немного прощающим.

PPPS: реализовано спростой фильтр разности широт:

@jit(nopython=True, parallel=True)
def get_nearby_count_vlat(coords, coords2, max_dist):
    '''
    Input: `coords`: List of coordinates, lat-lngs in an n x 2 array
           `coords2`: List of port coordinates, lat-lngs in an k x 2 array
           `max_dist`: Max distance to be considered nearby
    Output: Array of length n with a count of coords nearby coords2
    '''
    # initialize
    n = coords.shape[0]
    k = coords2.shape[0]
    coords2_abs = np.abs(coords2)
    output = np.zeros(n)

    # prange is a parallel loop when operations are independent
    for i in prange(n):
        # comparing a point in coords to the arrays in coords2
        point = coords[i]
        # subsetting coords2 to reduce haversine calc time. Value .02 is from playing with Gmaps and will need to change for max_dist > 1.0
        coords2_filtered = coords2[np.abs(point[0] - coords2[:,0]) < .02]
        # in case of no matches
        if coords2_filtered.shape[0] == 0: continue
        # returns an array of length k
        dist = haversine(point[0], point[1], coords2_filtered[:,0], coords2_filtered[:,1])
        # sum the boolean of distances less than the max allowable
        output[i] = np.sum(dist < max_dist)

    return output
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...