Построить график scipy.signal.find_peaks с данными datetime

#python #pandas #matplotlib #plot #scipy

#python #панды #matplotlib #построить #scipy

Вопрос:

Я хотел бы использовать scipy.signal.find_peaks для поиска пиков для Value in df , как показано ниже.

df:

   index Timestamp               Value   Id
0   36  2020-11-08 23:30:40.370 45.5    15
1   47  2020-11-13 04:52:29.410 44.5    15
2   67  2020-12-01 22:17:50.300 42.5    20
3   129 2020-11-24 00:57:11.950 43.0    103
4   176 2020-12-03 01:40:16.250 42.0    87
5   246 2020-11-12 07:32:54.000 43.5    103
6   281 2020-11-30 21:13:07.630 45.5    15
7   335 2020-11-30 20:43:11.050 43.5    15
8   370 2020-11-09 06:04:19.630 45.0    15
9   375 2020-11-22 21:21:33.150 44.0    115
10  384 2020-11-23 22:04:44.580 40.5    20
11  408 2020-11-16 03:26:10.150 46.0    15
12  428 2020-12-07 02:04:42.890 46.5    15
13  437 2020-11-26 00:10:34.660 47.0    15
14  482 2020-11-26 04:14:23.180 46.0    15
15  500 2020-12-06 19:40:30.580 46.0    15
16  528 2020-11-26 02:17:27.110 47.5    15
17  585 2020-11-25 18:13:17.450 43.0    15
18  641 2020-11-26 20:02:13.170 46.0    15
19  647 2020-11-11 21:36:09.530 41.0    112
 

воспроизводимый пример:

 from pandas import Timestamp
df = pd.DataFrame({'index': {0: 36,
  1: 47,
  2: 67,
  3: 129,
  4: 176,
  5: 246,
  6: 281,
  7: 335,
  8: 370,
  9: 375,
  10: 384,
  11: 408,
  12: 428,
  13: 437,
  14: 482,
  15: 500,
  16: 528,
  17: 585,
  18: 641,
  19: 647},
 'Timestamp': {0: Timestamp('2020-11-08 23:30:40.370000'),
  1: Timestamp('2020-11-13 04:52:29.410000'),
  2: Timestamp('2020-12-01 22:17:50.300000'),
  3: Timestamp('2020-11-24 00:57:11.950000'),
  4: Timestamp('2020-12-03 01:40:16.250000'),
  5: Timestamp('2020-11-12 07:32:54'),
  6: Timestamp('2020-11-30 21:13:07.630000'),
  7: Timestamp('2020-11-30 20:43:11.050000'),
  8: Timestamp('2020-11-09 06:04:19.630000'),
  9: Timestamp('2020-11-22 21:21:33.150000'),
  10: Timestamp('2020-11-23 22:04:44.580000'),
  11: Timestamp('2020-11-16 03:26:10.150000'),
  12: Timestamp('2020-11-07 02:04:42.890000'),
  13: Timestamp('2020-11-26 00:10:34.660000'),
  14: Timestamp('2020-11-26 04:14:23.180000'),
  15: Timestamp('2020-12-06 19:40:30.580000'),
  16: Timestamp('2020-12-26 02:17:27.110000'),
  17: Timestamp('2020-11-25 18:13:17.450000'),
  18: Timestamp('2020-11-26 20:02:13.170000'),
  19: Timestamp('2020-11-11 21:36:09.530000')},
 'Value': {0: 45.5,
  1: 44.5,
  2: 42.5,
  3: 43.0,
  4: 42.0,
  5: 43.5,
  6: 45.5,
  7: 43.5,
  8: 45.0,
  9: 44.0,
  10: 40.5,
  11: 46.0,
  12: 46.5,
  13: 47.0,
  14: 46.0,
  15: 46.0,
  16: 47.5,
  17: 43.0,
  18: 46.0,
  19: 41.0},
 'Id': {0: 15,
  1: 15,
  2: 20,
  3: 103,
  4: 87,
  5: 103,
  6: 15,
  7: 15,
  8: 15,
  9: 115,
  10: 20,
  11: 15,
  12: 15,
  13: 15,
  14: 15,
  15: 15,
  16: 15,
  17: 15,
  18: 15,
  19: 112}})
 

Используя приведенный ниже код:

 import matplotlib.pyplot as plt
from scipy.misc import electrocardiogram
from scipy.signal import find_peaks

x = df['Value'].values
peaks, properties = find_peaks(x, prominence=0.1, width=1)
properties["prominences"], properties["widths"]

plt.figure(figsize=(15,12))
plt.plot(x)
plt.plot(peaks, x[peaks], "x")
plt.vlines(x=peaks, ymin=x[peaks] - properties["prominences"],
           ymax = x[peaks], color = "C1")
plt.hlines(y=properties["width_heights"], xmin=properties["left_ips"],
           xmax=properties["right_ips"], color = "C1")
plt.show()
 

и ниже приведен вывод, в котором рассматривается только Value столбец.
введите описание изображения здесь

Как я могу сделать Timestamp горизонтальную ось?


Редактировать:

Я попытался создать Timestamp индекс и соответствующим образом изменил оси x и y:

 
import matplotlib.pyplot as plt
from scipy.misc import electrocardiogram
from scipy.signal import find_peaks

z = df
z.set_index('Timestamp', inplace=True)
z.index.to_pydatetime()
peaks, properties = find_peaks(z.Value, prominence=0.1, width=1)
properties["prominences"], properties["widths"]

plt.figure(figsize=(15,12))
plt.plot_date(z.index, z.Value)
plt.plot_date(z.index[peaks], z.Value[peaks], "x")
plt.vlines(x=z.index[peaks], ymin=z.Value[peaks] - properties["prominences"],
           ymax = z.index[peaks], color = "C1")
plt.hlines(y=properties["width_heights"], xmin=properties["left_ips"],
           xmax=properties["right_ips"], color = "C1")
plt.show()
 

который вернул:
введите описание изображения здесь

Что могло пойти не так?


Редактировать 2: используя решение @Asmus для большего набора данных, я заметил, что график полностью изменился, когда я изменил prominence и width . Например, на приведенном ниже графике я использовал prominence== 5 и width==0.0001157 , Value > 30 потому что меня интересуют пики выше 30 для Value , и они имеют видимость около 5 и ширину 0,0001157, что составляет 10 секунд как часть дня.

введите описание изображения здесь

Тогда, если я изменил prominence значение на 10, это выглядит так: введите описание изображения здесь

Оба выглядят совсем не так, как исходные данные, как показано ниже: введите описание изображения здесь

Почему это происходит?

Комментарии:

1. Ваш df не отсортирован Timestamp , следовательно, пики, которые вы нашли до сих пор, действительны только в «индексном пространстве», так сказать. В противном случае вы должны иметь возможность просто преобразовать найденные индексы во временные метки df.loc[indices,'Timestamp'] и нанести все на правильную ось: plt.plot(df['Timestamp'],x) , и так далее.

2. @Asmus Могу я спросить, что indices это такое? Должен ли я сбрасывать Timestamp как индекс? Не могли бы вы показать мне какой-нибудь код?

3. Я снова добавил свой ответ, объясняя, почему вам нужно выполнить повторную выборку. Возможно, в вашем конкретном случае вы могли бы попробовать использовать более точный аргумент интерполяции, например 'min' , см. Мое Обновление ниже.

Ответ №1:

Что касается find_peaks() и индексов:

Хорошо, итак, если мы посмотрим на документацию find_peaks() , мы увидим, что это

принимает одномерный массив и находит все локальные максимумы путем простого сравнения соседних значений

и возвращает

Индексы пиков в x, которые удовлетворяют всем заданным условиям.

Итак, например, запуск:

 import numpy as np
x = np.array([4,5,6,7,6,5,5])
idx, properties = find_peaks(x)
print(idx, x[idx])
 

выдает: [3] (индекс) и [7] как значение.


Что касается упорядочения данных:

В вашем случае вы пытаетесь сопоставить данные как функцию от дат, т. Е. Сначала нам нужно убедиться, что ваши данные упорядочены правильно — если вы запустите это:

 x = df['Timestamp'].values
y = df['Value'].values
idx, properties = find_peaks(y, prominence=0.1, width=1)

fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10,3))

# that is your original plot:
axes[0].plot(y)
axes[0].plot(idx,y[idx],"x")
axes[0].set_title("unsorted, x = indices")

# here, I simply use the "correct" data as x-axis
axes[1].plot(x,y)
axes[1].plot(x[idx], y[idx], "x")
axes[1].set_title("unsorted, x = dates")

# and now I also sort the data:
df = df.sort_values(by="Timestamp")
x = df['Timestamp'].values
y = df['Value'].values
idx, properties = find_peaks(y, prominence=0.1, width=1)
axes[2].plot(x,y)
axes[2].plot(x[idx], y[idx], "x")
axes[2].set_title("sorted, x = dates")

# some nicer formatting:
for ax in axes:
    ax.grid()
fig.autofmt_xdate()
plt.tight_layout()
plt.show()
 

Вы увидите это:Различия в оси x

То есть (слева направо):

  1. Данные в том виде, в каком вы их построили, в зависимости от индекса (т.Е. x изменяется от 0 до 19). Здесь у вас нет проблем с поиском пиков и их выделением.
  2. Данные, построенные как функция x=df['Timestamp'] — это выглядит хаотично, потому что ваш фрейм данных не упорядочен по времени!
  3. Отсортированный фрейм данных, отображаемый как функция метки времени, используемый x[idx], y[idx] для выделения местоположений пиков.

Что касается строк и строк на оси даты

Теперь вы сможете добавлять вертикальные линии без особых проблем с:

 axes[0].vlines(x=x[idx], ymin=y[idx] - properties["prominences"],
           ymax = y[idx], color = "C1")
 

Но в случае горизонтальных линий проблема становится такой, что properties выглядит так:

 {
    'prominences': array([5., 5.]), 
    'left_bases': array([3, 8]), 
    'right_bases': array([ 8, 17]), 
    'widths': array([3.14285714, 3.225]), 
    'width_heights': array([43.5, 44.5]), 
    'left_ips': array([ 4., 10.375]), 
    'right_ips': array([ 7.14285714, 13.6])
}
 

Где явно «неясно», для matplotlib чего, например, a width of 3.14285714 означает в терминах дат, по крайней мере, без надлежащего преобразования в даты.


Редактировать: Как работать с отсутствующими данными, чтобы исправить hlines

Прежде всего, вам нужно убедиться, что все даты в диапазоне дат имеют действительные данные, чтобы вы могли напрямую интерпретировать возвращаемые значения find_peaks() как относительные даты (то есть, если он найдет пик с индексом «2», вы сможете напрямую преобразовать это в [start_date 2дней]).

 # print(df.head()) ## <-- see the unordered df
# first, make sure that the DataFrame is sorted by date
df = df.sort_values(by='Timestamp').set_index('Timestamp')
# print(df.head()) ## <-- it is now ordered, but there are missing dates
# and then resample it on a daily basis, 
# using mean() to average multiple entries per day:
df = df.resample('D').mean().reset_index()
# print(df.head()) ## <-- it is sorted and every day is present, but still some 'Value' are missing

x = df['Timestamp']
# since we have missing data, interpolate the values linearly:
y = df['Value'].interpolate()

# now find the peaks
idx, properties = find_peaks(y, prominence=0.1, width=1)
# note that since we interpolated the data on a daily basis,
# "idx" is now equivalent to "days since the first date"!

# introduce some shorthands:
l = properties["left_ips"]
r = properties["right_ips"]
p = properties["prominences"]
w = properties["widths"]
wh = properties["width_heights"]

peak_x = x.iloc[idx] 
peak_y = y.iloc[idx]

def to_date(x):
    """
    takes the first Timestamp of the df as a start date
    and then converts a given relative date x (in days)  
    back into a "normal" date
    
    Note how this only works since we resampled the df on a daily basis!
    """
    _start = df.loc[0,'Timestamp']
    return pd.to_datetime(_start)   pd.to_timedelta(x, unit='D')

fig,ax = plt.subplots(figsize=(10,3))    

ax.plot(x,y,c="b",zorder=0)
ax.scatter(x[df['Value'].isna()],y[df['Value'].isna()],
    edgecolors="r",facecolors="none",marker='o',zorder=1,label="interpolated!") # to highlight interpolated dates    
ax.scatter(peak_x, peak_y, color="C1", marker="x", s=200,zorder=2)
ax.vlines(x=peak_x, ymin=peak_y - p, ymax = peak_y, color = "C1")

ax.hlines(y=wh, xmin=to_date(l), xmax=to_date(r), color = "C1")

ax.legend()
fig.autofmt_xdate()
ax.grid()
plt.tight_layout()
plt.show()
 

Дает:

Окончательное решение, пики в масштабе даты и времени

Примечание: я подчеркнул интерполированные точки данных (дополнительные красные кружки, которых нет в вашем исходном фрейме данных.) и помните, что некоторые Values из них были усреднены resample('D').mean() выше, поэтому вам нужно проверить, действительно ли это соответствует вашим потребностям!


Последняя демонстрация:

Хорошо, итак, чтобы подчеркнуть, что я имел в виду, давайте используем сокращенный пример:

 ts = pd.to_datetime(["2020-01-01", "2020-02-01", "2020-02-02", "2020-02-03", "2020-02-03 12:45:00", "2020-03-01", "2020-04-01"])
ys = [0, 1, 4, 3, 2.7, 2, 1]
df = pd.DataFrame({'Timestamp':ts, 'Value':ys})
print(df)

idx, properties = find_peaks(df['Value'], prominence=0.1, width=0.1)
# introduce some shorthands:
l = properties["left_ips"]
r = properties["right_ips"]
p = properties["prominences"]
w = properties["widths"]
wh = properties["width_heights"]

peak_x = df['Timestamp'][idx].values
peak_y = df['Value'][idx].values

print(f"Found a peak at {idx} (index), i.e. at {peak_x} (Timestamp) with height {peak_y} (Value)")
print(f"Half of the peak maximum can be found at {wh} (Value) and has a width of {w} (Index)!")
print(f"The width starts at {l} (index) and goes to {r} (index) == ?? (INTERPOLATION OF INDEX REQUIRED!)")

fig,ax = plt.subplots(figsize=(10,3))    
ax.plot(df['Timestamp'],df['Value'], marker="o")

ax.scatter(peak_x, peak_y, marker="x", s=20**2,color="r")

ax.vlines(x=peak_x, ymin=peak_y - p, ymax = peak_y, color = "C1")
ax.axhline(wh,color="g",linestyle="dashed")

fig.autofmt_xdate()
ax.grid()
plt.tight_layout()
plt.show()
 

Здесь фрейм данных выглядит следующим образом:

             Timestamp  Value
0 2020-01-01 00:00:00    0.0
1 2020-02-01 00:00:00    1.0
2 2020-02-02 00:00:00    4.0 # <— clearly a peak here at index [2]
3 2020-02-03 00:00:00    3.0
4 2020-02-03 12:45:00    2.7
5 2020-03-01 00:00:00    2.0
6 2020-04-01 00:00:00    1.0
 

Здесь мы явно находим пик at [2] (index units) , то есть at ['2020-02-02T00:00:00.000000000'] (Timestamp units) с высотой [4.] (Value units) .
Половина максимального максимума может быть найдена на [2.5] (Value) и имеет ширину [2.78571429] (Index) !
Строка ширины должна начинаться с [1.5] (index) и переходить к [4.28571429] (index) == ??

Я думаю, это ясно демонстрирует, что необходимо выполнить какую-то интерполяцию, чтобы выявить, что 4.285… означает ось масштаба времени (что вам нужно для hlines() работы).

Самый простой способ сделать это — ввести данные с регулярным интервалом find_peaks() , чтобы вы могли легко преобразовать данный индекс обратно в дату. Выполняете ли вы интерполяцию ежедневно, как я делал выше, или на минутной или секундной основе, зависит от вас. Просто измените эти две строки на одно из допустимых смещений даты:

 df = df.resample('min').mean().reset_index()

# and, within def to_date(x):
return pd.to_datetime(_start)   pd.to_timedelta(x, unit='min')
 

Комментарии:

1. Привет, Асмус, спасибо за потрясающий ответ. Могу ли я узнать, почему нам нужно выполнить повторную выборку? Можем ли мы просто использовать исходные данные? Я обновил вопрос для получения более подробной информации.

2. @nilsinelabore похоже, вы неправильно поняли, как find_peaks() это работает: вы буквально только предоставляете ему массив df["Value"] в качестве входных данных ( параметры), он совершенно не знает об оси x! Выбираете ли вы ось x как «datetime» (т. Е. 2020-12-01 ) или индексы (т. Е. [0,1,2] ) Или что-то еще, он просто пытается найти пики в заданных значениях y !