#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 изменяется от 0 до 19). Здесь у вас нет проблем с поиском пиков и их выделением.
- Данные, построенные как функция
x=df['Timestamp']
— это выглядит хаотично, потому что ваш фрейм данных не упорядочен по времени! - Отсортированный фрейм данных, отображаемый как функция метки времени, используемый
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 !