Commit 92c41731 authored by Jonathan Juhl's avatar Jonathan Juhl
Browse files


parent 2c376be1
import numpy as np
from scipy import interpolate
from scipy.signal import argrelextrema
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
import warnings
from typing import Tuple, Optional, Iterable
class KneeLocator(object):
def __init__(
x: Iterable[float],
y: Iterable[float],
S: float = 1.0,
curve: str = "concave",
direction: str = "increasing",
interp_method: str = "interp1d",
online: bool = False,
Once instantiated, this class attempts to find the point of maximum
curvature on a line. The knee is accessible via the `.knee` attribute.
:param x: x values.
:param y: y values.
:param S: Sensitivity, original paper suggests default of 1.0
:param curve: If 'concave', algorithm will detect knees. If 'convex', it
will detect elbows.
:param direction: one of {"increasing", "decreasing"}
:param interp_method: one of {"interp1d", "polynomial"}
:param online: Will correct old knee points if True, will return first knee if False
# Step 0: Raw Input
self.x = np.array(x)
self.y = np.array(y)
self.curve = curve
self.direction = direction
self.N = len(self.x)
self.S = S
self.all_knees = set()
self.all_norm_knees = set()
self.all_knees_y = []
self.all_norm_knees_y = [] = online
# Step 1: fit a smooth line
if interp_method == "interp1d":
uspline = interpolate.interp1d(self.x, self.y)
self.Ds_y = uspline(self.x)
elif interp_method == "polynomial":
pn_model = PolynomialFeatures(7)
xpn = pn_model.fit_transform(self.x.reshape(-1, 1))
regr_model = LinearRegression(), self.y)
self.Ds_y = regr_model.predict(
pn_model.fit_transform(self.x.reshape(-1, 1))
"{} is an invalid interp_method parameter, use either 'interp1d' or 'polynomial'".format(
# Step 2: normalize values
self.x_normalized = self.__normalize(self.x)
self.y_normalized = self.__normalize(self.Ds_y)
# Step 3: Calculate the Difference curve
self.x_normalized, self.y_normalized = self.transform_xy(
self.x_normalized, self.y_normalized, self.direction, self.curve
# normalized difference curve
self.y_difference = self.y_normalized - self.x_normalized
self.x_difference = self.x_normalized.copy()
# Step 4: Identify local maxima/minima
# local maxima
self.maxima_indices = argrelextrema(self.y_difference, np.greater_equal)[0]
self.x_difference_maxima = self.x_difference[self.maxima_indices]
self.y_difference_maxima = self.y_difference[self.maxima_indices]
# local minima
self.minima_indices = argrelextrema(self.y_difference, np.less_equal)[0]
self.x_difference_minima = self.x_difference[self.minima_indices]
self.y_difference_minima = self.y_difference[self.minima_indices]
# Step 5: Calculate thresholds
self.Tmx = self.y_difference_maxima - (
self.S * np.abs(np.diff(self.x_normalized).mean())
# Step 6: find knee
self.knee, self.norm_knee = self.find_knee()
# Step 7: If we have a knee, extract data about it
self.knee_y = self.norm_knee_y = None
if self.knee:
self.knee_y = self.y[self.x == self.knee][0]
self.norm_knee_y = self.y_normalized[self.x_normalized == self.norm_knee][0]
def __normalize(a: Iterable[float]) -> Iterable[float]:
"""normalize an array
:param a: The array to normalize
return (a - min(a)) / (max(a) - min(a))
def transform_xy(
x: Iterable[float], y: Iterable[float], direction: str, curve: str
) -> Tuple[Iterable[float], Iterable[float]]:
"""transform x and y to concave, increasing based on given direction and curve"""
# convert elbows to knees
if curve == "convex":
x = x.max() - x
y = y.max() - y
# flip decreasing functions to increasing
if direction == "decreasing":
y = np.flip(y, axis=0)
if curve == "convex":
x = np.flip(x, axis=0)
y = np.flip(y, axis=0)
return x, y
def find_knee(self,):
"""This function finds and sets the knee value and the normalized knee value. """
if not self.maxima_indices.size:
"No local maxima found in the difference curve\n"
"The line is probably not polynomial, try plotting\n"
"the difference curve with plt.plot(knee.x_difference, knee.y_difference)\n"
"Also check that you aren't mistakenly setting the curve argument",
return None, None
# placeholder for which threshold region i is located in.
maxima_threshold_index = 0
minima_threshold_index = 0
# traverse the difference curve
for i, x in enumerate(self.x_difference):
# skip points on the curve before the the first local maxima
if i < self.maxima_indices[0]:
j = i + 1
# reached the end of the curve
if x == 1.0:
# if we're at a local max, increment the maxima threshold index and continue
if (self.maxima_indices == i).any():
threshold = self.Tmx[maxima_threshold_index]
threshold_index = i
maxima_threshold_index += 1
# values in difference curve are at or after a local minimum
if (self.minima_indices == i).any():
threshold = 0.0
minima_threshold_index += 1
if self.y_difference[j] < threshold:
if self.curve == "convex":
if self.direction == "decreasing":
knee = self.x[threshold_index]
norm_knee = self.x_normalized[threshold_index]
knee = self.x[-(threshold_index + 1)]
norm_knee = self.x_normalized[-(threshold_index + 1)]
elif self.curve == "concave":
if self.direction == "decreasing":
knee = self.x[-(threshold_index + 1)]
norm_knee = self.x_normalized[-(threshold_index + 1)]
knee = self.x[threshold_index]
norm_knee = self.x_normalized[threshold_index]
# add the y value at the knee
y_at_knee = self.y[self.x == knee][0]
y_norm_at_knee = self.y_normalized[self.x_normalized == norm_knee][0]
if knee not in self.all_knees:
# now add the knee
# if detecting in offline mode, return the first knee found
if is False:
return knee, norm_knee
if self.all_knees == set():
warnings.warn("No knee/elbow found")
return None, None
return knee, norm_knee
def plot_knee_normalized(self, figsize: Optional[Tuple[int, int]] = None):
"""Plot the normalized curve, the difference curve (x_difference, y_normalized) and the knee, if it exists.
:param figsize: Optional[Tuple[int, int]
The figure size of the plot. Example (12, 8)
:return: NoReturn
import matplotlib.pyplot as plt
if figsize is None:
figsize = (6, 6)
plt.title("Normalized Knee Point")
plt.plot(self.x_normalized, self.y_normalized, "b", label="normalized curve")
plt.plot(self.x_difference, self.y_difference, "r", label="difference curve")
np.arange(self.x_normalized.min(), self.x_normalized.max() + 0.1, 0.1)
np.arange(self.y_difference.min(), self.y_normalized.max() + 0.1, 0.1)
def plot_knee(self, figsize: Optional[Tuple[int, int]] = None):
Plot the curve and the knee, if it exists
:param figsize: Optional[Tuple[int, int]
The figure size of the plot. Example (12, 8)
:return: NoReturn
import matplotlib.pyplot as plt
if figsize is None:
figsize = (6, 6)
plt.title("Knee Point")
plt.plot(self.x, self.y, "b", label="data")
self.knee, plt.ylim()[0], plt.ylim()[1], linestyles="--", label="knee/elbow"
# Niceties for users working with elbows rather than knees
def elbow(self):
return self.knee
def norm_elbow(self):
return self.norm_knee
def elbow_y(self):
return self.knee_y
def norm_elbow_y(self):
return self.norm_knee_y
def all_elbows(self):
return self.all_knees
def all_norm_elbows(self):
return self.all_norm_knees
def all_elbows_y(self):
return self.all_knees_y
def all_norm_elbows_y(self):
return self.all_norm_knees_y
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment