在肘部曲线中如何找到曲线开始上升的点?

数据挖掘 Python 图表 离群值 matplotlib
2021-10-05 10:38:45

我正在计算我的数据的距离度量。然后将结果按升序排序。距离大于特定阈值的样本将被标记为异常值并被丢弃。下面是所有距离值的图。

图形

从图中可以明显看出,在某个点之后,图上升得非常快,甚至数据点也变得稀疏。我需要计算发生这种情况的那个点并将该点标记为阈值。

2个回答

TL;博士

使用下面的两个函数来获取肘部的索引:

elbow_index = find_elbow(data, get_data_radiant(data))


编辑:我将下面的所有代码都放入了一个名为的 python 包现在,您可以简单地这样做:

from kneebow.rotor import Rotor

rotor = Rotor()
rotor.fit_rotate(data)
elbow_index = rotor.get_elbow_index()


长答案

如果这条曲线代表所有曲线(例如单峰曲线和连续曲线),那么一种快速而肮脏的方法是将其旋转到一定程度并简单地取最小值。

旋转可以通过与旋转矩阵相乘来完成

(cosθsinθsinθcosθ)

在哪里 θ 是以弧度为单位的所需角度。

在 python 中,您可以使用以下函数执行此操作:

def find_elbow(data, theta):

    # make rotation matrix
    co = np.cos(theta)
    si = np.sin(theta)
    rotation_matrix = np.array(((co, -si), (si, co)))

    # rotate data vector
    rotated_vector = data.dot(rotation_matrix)

    # return index of elbow
    return np.where(rotated_vector == rotated_vector.min())[0][0]

请注意,这theta是以弧度为单位的角度。你可以通过 来计算np.radians(angle)

重要提示:要记住的一件事是 x 轴和 y 轴可能有不同的比例。因此,在您的情节中,看起来 45° 旋转就足够了,但实际上并非如此。因此,您可以使用以下函数来计算您应该使用哪个辐射。它将数据中从最小值到最大值的斜率转换为弧度:

def get_data_radiant(data):
  return np.arctan2(data[:, 1].max() - data[:, 1].min(), 
                    data[:, 0].max() - data[:, 0].min())

如果您想获得角度,请运行np.rad2deg(get_data_radiant(data)).


例子

如何使用

让我们在与您类似的样本数据上测试该方法:

# Let's define our sample data:
data = np.array([
    [1, 1],
    [2, 2],
    [3, 3],
    [4, 4],
    [5, 5],
    [6, 6],
    [7, 7],
    [8, 8],
    [9, 16],
    [10, 32],
    [11, 64],
    [12, 128],
    [13, 256],
    [14, 512]
])
# y is linear until (8,8) and increases exponentially afterwards

plt.scatter(data[:, 0], data[:, 1])

绘制数据为我们提供了下图:

图1

现在,让我们尝试通过组合上面的函数来找到肘部:

elbow_index = find_elbow(data, get_data_radiant(data))

print(elbow_index)        # 10
print(data[elbow_index])  # array([11, 64])

详细

总而言之,我们只是计算了从最小值到最大值的斜率,然后以斜率为零的方式旋转绘图。随后,我们取数据的最小值得到肘部。

我们可以通过以下方式获得旋转角度:

angle = np.rad2deg(get_data_radiant(data))
print(angle)  # 88.543

左边的图有一条橙色线的斜率。轴的刻度使它看起来像是 45° 的角度,而实际上,它的角度是 88.5°!矢量旋转后,数据看起来像右边的图。从这些数据中,我们取了第 11 个数据点的最小值。

图 2 图 3

缺点

请注意,这种方法有一个缺点:轴的比例越不相等,它会越多地选择有利于较大轴的点。

在使用此方法之前,您可以尝试使用 scikit-learnMinMaxScaler来扩展您的数据,以减少这种影响。如果您使用膝盖弓包,默认情况下会缩放数据。

我复制了用户 rafaelvalle 的代码

# Find Elbow Point, Trade off Point
def elbow_point(data):
    curve = data
    nPoints = len(curve)
    allCoord = np.vstack((range(nPoints), curve)).T
    np.array([range(nPoints), curve])
    
    firstPoint = allCoord[0]
    lineVec = allCoord[-1] - allCoord[0]
    lineVecNorm = lineVec / np.sqrt(np.sum(lineVec**2))
    vecFromFirst = allCoord - firstPoint
    scalarProduct = np.sum(vecFromFirst * np.matlib.repmat(lineVecNorm, nPoints, 1), axis=1)
    vecFromFirstParallel = np.outer(scalarProduct, lineVecNorm)
    vecToLine = vecFromFirst - vecFromFirstParallel
    distToLine = np.sqrt(np.sum(vecToLine ** 2, axis=1))
    idxOfBestPoint = np.argmax(distToLine)

    return idxOfBestPoint