Как я могу определить, какая кривая является ближайшей к данному набору точек? - PullRequest
1 голос
/ 24 мая 2019

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

Я бы хотел установить расстояние от каждой точки данных до каждой точки накривая, с sqrt(x^2+y^2) - sqrt(x_1^2 + y_1^2), сложите их для каждой точки на кривой.Проблема в том, что на кривой есть несколько тысяч точек, и для оценки требуется всего несколько десятков точек данных, поэтому я не могу просто поместить их в столбцы рядом друг с другом.

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

Редактировать: Спасибо за комментарии.@Alexander: я попробовал функцию векторизации, как показано ниже, с образцом набора данных.Я на самом деле использую контуры, которые содержат несколько тысяч точек данных, и набор данных для сравнения составляет более 100, поэтому я хотел бы иметь возможность автоматизировать как можно больше.В настоящее время я могу создать измерение расстояния от первой точки данных по моему контуру, но в идеале я бы также хотел прокрутить j.Когда я пытаюсь это сделать, появляется ошибка:

import numpy as np
from numpy import vectorize
import pandas as pd
from pandas import DataFrame

df1 = {'X1':['1', '2', '2', '3'], 'Y1':['2', '5', '7', '9']}
df1 = DataFrame(df1, columns=['X1', 'Y1'])
df2 = {'X2':['3', '5', '6'], 'Y2':['10', '15', '16']}
df2 = DataFrame(df2, columns=['X2', 'Y2'])
df1=df1.astype(float)
df2=df2.astype(float)
Distance=pd.DataFrame()

i = range(0, len(df1))
j = range(0, len(df2))

def myfunc(x1, y1, x2, y2):
    return np.sqrt((x2-x1)**2+np.sqrt(y2-y1)**2)

vfunc=np.vectorize(myfunc)
Distance['Distance of Datapoint j to Contour']=vfunc(df1.iloc[i]   ['X1'], df1.iloc[i]['Y1'], df2.iloc[0]['X2'], df2.iloc[0]['Y2'])
Distance['Distance of Datapoint j to Contour']=vfunc(df1.iloc[i] ['X1'], df1.iloc[i]['Y1'], df2.iloc[1]['X2'], df2.iloc[1]['Y2'])
Distance

Ответы [ 2 ]

1 голос
/ 25 мая 2019

Общая идея

"Кривая" на самом деле является многоугольником с множеством точек.Там определенно есть несколько библиотек для расчета расстояния между полигоном и точкой.Но обычно это будет что-то вроде:

  1. Рассчитать «приблизительное расстояние» до целого многоугольника, например, до ограничительной рамки многоугольника (от точки до 4 отрезков линии) или до центра ограничительной рамки.
  2. рассчитать расстояния до линий многоугольника.Если у вас слишком много точек, то в качестве дополнительного шага «разрешение» многоугольника может быть уменьшено.
  3. Наименьшее найденное расстояние - это расстояние от точки до многоугольника.
  4. повторите для каждой точки икаждый полигон

Существующие решения

Некоторые библиотеки уже могут это сделать:

Пример:


# get distance from points of 1 dataset to all the points of another dataset

from scipy.spatial import distance

d = distance.cdist(df1.to_numpy(), df2.to_numpy(), 'euclidean')

print(d)
# Results will be a matrix of all possible distances:
# [[ D(Point_df1_0, Point_df2_0), D(Point_df1_0, Point_df2_1), D(Point_df1_0, Point_df2_2)]
#  [ D(Point_df1_1, Point_df2_0), D(Point_df1_1, Point_df2_1), D(Point_df1_1, Point_df2_2)]
#  [ D(Point_df1_3, Point_df2_0), D(Point_df1_2, Point_df2_1), D(Point_df1_2, Point_df2_2)]
#  [ D(Point_df1_3, Point_df2_0), D(Point_df1_3, Point_df2_1), D(Point_df1_3, Point_df2_2)]]

[[ 8.24621125 13.60147051 14.86606875]
 [ 5.09901951 10.44030651 11.70469991]
 [ 3.16227766  8.54400375  9.8488578 ]
 [ 1.          6.32455532  7.61577311]]

Что делать дальше, зависит от вас.Например, в качестве показателя «общего расстояния между кривыми» вы можете:

  • Выбрать наименьшие значения в каждой строке и каждом столбце (если вы пропустите некоторые столбцы / строки, то вы можете получить кандидата, который"соответствует только части контура) и вычислите их медиану: np.median(np.hstack([np.amin(d, axis) for axis in range(len(d.shape))])).
  • Или вы можете рассчитать среднее значение:

    • для всех расстояний: np.median(d)
    • «наименьших 2/3 расстояний»: np.median(d[d<np.percentile(d, 66, interpolation='higher')])
    • «наименьших расстояний, которые охватывают, по крайней мере, каждый ряд и каждый столбец»:
for min_value in np.sort(d, None):
    chosen_indices = d<=min_value
    if np.all(np.hstack([np.amax(chosen_indices, axis) for axis in range(len(chosen_indices.shape))])):
        break

similarity = np.median(d[chosen_indices])

Альтернативный подход

Альтернативный подходБыло бы полезно использовать некоторую библиотеку «geometry» для сравнения областей вогнутых оболочек:

  • Создание вогнутых оболочек для контуров и для «точек данных-кандидатов» (не просто, но возможно: с использованиемстройные , с использованием Concaveman ).Но если вы уверены, что ваши контуры уже упорядочены и без перекрывающихся сегментов, то вы можете напрямую строить полигоны из этих точек без необходимости вогнутой оболочки.

  • Использовать «площадь пересечения« минус »«необщая зона» как показатель сходства (для этого можно использовать shapely * ):

Этот подход может быть лучше, чем обработка расстояний в некоторых ситуациях, например:

  • Вы хотите предпочесть «меньшее количество точек, охватывающих всю область», чем «огромное количество очень близких точек, которые покрывают только половинуarea "
  • Это более очевидный способ сравнения кандидатов с разным количеством баллов

Но у него тоже есть свои недостатки (просто нарисуйте несколько примеров на бумаге и поэкспериментируйте, чтобы их найти)

Другие идеи:

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

    • построить линейное кольцо из ваших точек и затем использовать contour.buffer(some_distance). Таким образом, вы игнорируете «внутреннюю область» контура и сравниваете только сам контур (с допуском some_distance). Расстояние между центроидами (или удвоенное значение) может использоваться как значение для some_distance
    • Вы можете строить полигоны / линии из сегментов, используя ops.polygonize
  • вместо использования intersection.area - symmetric_difference.area вы можете:

    • Привязка одного объекта к другому, а затем сравнение привязанного объекта с исходным
  • Перед сравнением реальных объектов вы можете сравнить "более простые" версии объектов, чтобы отфильтровать очевидные несоответствия:

1 голос
/ 25 мая 2019

Для расстояния вам нужно изменить формулу на

def getDistance(x, y, x_i, y_i):
    return sqrt((x_i -x)^2 + (y_i - y)^2)

с (x, y) как точкой данных, а (x_i, y_i) с точкой.

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

import numpy as np # Universal abbreviation for the module

datapoints = np.random.rand(3,2) # Returns a vector with randomized entries of size 3x2 (Imagine it as 3 sets of x- and y-values

contour1 = np.random.rand(1000, 2) # Other than the size (which is 1000x2) no different than datapoints
contour2 = np.random.rand(1000, 2)
contour3 = np.random.rand(1000, 2)

def squareDistanceUnvectorized(datapoint, contour):
    retVal = 0.
    print("Using datapoint with values x:{}, y:{}".format(datapoint[0], datapoint[1]))

    lengthOfContour = np.size(contour, 0) # This gets you the number of lines in the vector

    for pointID in range(lengthOfContour):
        squaredXDiff = np.square(contour[pointID,0] - datapoint[0])
        squaredYDiff = np.square(contour[pointID,1] - datapoint[1])
        retVal += np.sqrt(squaredXDiff + squaredYDiff)

    retVal = retVal / lengthOfContour # As we want the average, we are dividing the sum by the element count
    return retVal

if __name__ == "__main__":
    noOfDatapoints = np.size(datapoints,0)
    contID = 0
    for currentDPID in range(noOfDatapoints):
        dist1 = squareDistanceUnvectorized(datapoints[currentDPID,:], contour1)
        dist2 = squareDistanceUnvectorized(datapoints[currentDPID,:], contour2)
        dist3 = squareDistanceUnvectorized(datapoints[currentDPID,:], contour3)
        if dist1 > dist2 and dist1 > dist3:
            contID = 1
        elif dist2 > dist1 and dist2 > dist3:
            contID = 2
        elif dist3 > dist1 and dist3 > dist2:
            contID = 3
        else:
            contID = 0
        if contID == 0:
            print("Datapoint {} is inbetween two contours".format(currentDPID))
        else:
            print("Datapoint {} is closest to contour {}".format(currentDPID, contID))

Хорошо, теперь переходим к земле векторов.

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

import numpy as np
import pandas as pd

# Generate 1000 points (2-dim Vector) with random values between 0 and 1. Make them strings afterwards.
# This is the first contour
random2Ddata1 = np.random.rand(1000,2)
listOfX1      = [str(x) for x in random2Ddata1[:,0]]
listOfY1      = [str(y) for y in random2Ddata1[:,1]]

# Do the same for a second contour, except that we de-center this 255 units into the first dimension
random2Ddata2 = np.random.rand(1000,2)+[255,0]
listOfX2      = [str(x) for x in random2Ddata2[:,0]]
listOfY2      = [str(y) for y in random2Ddata2[:,1]]

# After this step, our 'contours' are basically two blobs of datapoints whose centers are approx. 255 units apart.

# Generate a set of 4 datapoints and make them a Pandas-DataFrame
datapoints = {'X': ['0.5', '0', '255.5', '0'], 'Y': ['0.5', '0', '0.5', '-254.5']}
datapoints = pd.DataFrame(datapoints, columns=['X', 'Y'])

# Do the same for the two contours
contour1    = {'Xf': listOfX1, 'Yf': listOfY1}
contour1    = pd.DataFrame(contour1,  columns=['Xf', 'Yf'])

contour2    = {'Xf': listOfX2, 'Yf': listOfY2}
contour2    = pd.DataFrame(contour2,  columns=['Xf', 'Yf'])

# We do now have 4 datapoints.
# - The first datapoint is basically where we expect the mean of the first contour to be.
#   Contour 1 consists of 1000 points with x, y- values between 0 and 1
# - The second datapoint is at the origin. Its distances should be similar to the once of the first datapoint
# - The third datapoint would be the result of shifting the first datapoint 255 units into the positive first dimension
# - The fourth datapoint would be the result of shifting the first datapoint 255 units into the negative second dimension

# Transformation into numpy array
# First the x and y values of the data points
dpArray = ((datapoints.values).T).astype(np.float)
c1Array = ((contour1.values).T).astype(np.float)
c2Array = ((contour2.values).T).astype(np.float)

# This did the following:
# - Transform the datapoints and contours into numpy arrays
# - Transpose them afterwards so that if we want all x values, we can write var[0,:] instead of var[:,0].
#   A personal preference, maybe
# - Convert all the values into floats.

# Now, we iterate through the contours. If you have a lot of them, putting them into a list beforehand would do the job
for contourid, contour in enumerate([c1Array, c2Array]):
    # Now for the datapoints
    for _index, _value in enumerate(dpArray[0,:]):
        # The next two lines do vectorization magic.
        # First, we square the difference between one dpArray entry and the contour x values.
        # You might notice that contour[0,:] returns an 1x1000 vector while dpArray[0,_index] is an 1x1 float value.
        # This works because dpArray[0,_index] is broadcasted to fit the size of contour[0,:].
        dx       = np.square(dpArray[0,_index] - contour[0,:])
        # The same happens for dpArray[1,_index] and contour[1,:]
        dy       = np.square(dpArray[1,_index] - contour[1,:])
        # Now, we take (for one datapoint and one contour) the mean value and print it.
        # You could write it into an array or do basically anything with it that you can imagine
        distance = np.mean(np.sqrt(dx+dy))
        print("Mean distance between contour {} and datapoint {}: {}".format(contourid+1, _index+1, distance))

# But you want to be able to call this... so here we go, generating a function out of it!
def getDistanceFromDatapointsToListOfContoursFindBetterName(datapoints, listOfContourDataFrames):
    """ Takes a DataFrame with points and a list of different contours to return the average distance for each combination"""
    dpArray = ((datapoints.values).T).astype(np.float)

    listOfContours = []
    for item in listOfContourDataFrames:
        listOfContours.append(((item.values).T).astype(np.float))

    retVal  = np.zeros((np.size(dpArray,1), len(listOfContours)))
    for contourid, contour in enumerate(listOfContours):
        for _index, _value in enumerate(dpArray[0,:]):
            dx       = np.square(dpArray[0,_index] - contour[0,:])
            dy       = np.square(dpArray[1,_index] - contour[1,:])
            distance = np.mean(np.sqrt(dx+dy))
            print("Mean distance between contour {} and datapoint {}: {}".format(contourid+1, _index+1, distance))
            retVal[_index, contourid] = distance

    return retVal

# And just to see that it is, indeed, returning the same results, run it once
getDistanceFromDatapointsToListOfContoursFindBetterName(datapoints, [contour1, contour2])

...