【问题标题】:Detecting and counting when Pandas DataFrame with time-indexed float column exceeds a threshold for longer than 5-minutes当具有时间索引浮点列的 Pandas DataFrame 超过阈值超过 5 分钟时检测和计数
【发布时间】:2021-07-13 16:30:00
【问题描述】:

我有一个简单的 DataFrame,其中包含心率(和相关列),由唯一且单调递增的日期时间值索引,以一分钟的间隔采样(在移除传感器的地方有一些中断)。这是一个示例:

print(data)
                               End Time    HR  Min HR  Max HR
Start Time                                                   
2020-10-14 18:27:00 2020-10-14 18:27:59  87.0    84.0    89.0
2020-10-14 18:28:00 2020-10-14 18:28:59  89.0    85.0    94.0
2020-10-14 18:29:00 2020-10-14 18:29:59  87.0    84.0    90.0
2020-10-14 18:30:00 2020-10-14 18:30:59  91.0    87.0    96.0
2020-10-14 18:31:00 2020-10-14 18:31:59  95.0    91.0   100.0
...                                 ...   ...     ...     ...
2021-07-08 22:26:00 2021-07-08 22:26:59  73.0    70.0    76.0
2021-07-08 22:27:00 2021-07-08 22:27:59  76.0    74.0    79.0
2021-07-08 22:28:00 2021-07-08 22:28:59  71.0    70.0    74.0
2021-07-08 22:29:00 2021-07-08 22:29:59  71.0    69.0    74.0
2021-07-08 22:30:00 2021-07-08 22:30:59  74.0    72.0    78.0

[373234 rows x 4 columns]

我想检测“峰值”,即心率超过阈值 5 分钟或更长时间。所以,需要明确的是,当一个峰值持续 10 分钟时,它仍然是一个峰值,而不是两个。

当然,我可以按阈值过滤:

maybe_peaks = data[data['HR']>= threshold])]
print(maybe_peaks)

                               End Time     HR  Min HR  Max HR
Start Time                                                    
2020-10-16 12:14:00 2020-10-16 12:14:59  104.0    95.0   108.0
2020-10-16 12:15:00 2020-10-16 12:15:59  111.0   106.0   115.0
2020-10-16 12:16:00 2020-10-16 12:16:59  132.0   105.0   157.0
2020-10-16 12:17:00 2020-10-16 12:17:59  126.0   106.0   159.0
2020-10-16 12:18:00 2020-10-16 12:18:59  109.0   108.0   111.0
...                                 ...    ...     ...     ...
2021-07-04 12:58:00 2021-07-04 12:58:59  103.0    97.0   116.0
2021-07-06 13:38:00 2021-07-06 13:38:59  106.0   103.0   108.0
2021-07-06 13:39:00 2021-07-06 13:39:59  104.0   102.0   109.0
2021-07-06 17:02:00 2021-07-06 17:02:59  121.0    98.0   135.0
2021-07-07 19:58:00 2021-07-07 19:58:59  110.0   105.0   116.0

[12940 rows x 4 columns]

但是问题就变成了:如何进一步过滤掉超过阈值的时间短于 5 分钟的行。

当然,我可以做这种 C 风格的逐行循环,但我觉得这既不是最有效也不是最优雅的方式。

最终,我想在 x 轴上构建一个日历日,以及这些峰值事件沿 y 的频率/计数的图。

任何提示/方向将不胜感激。

这里有一些示例数据可以提供帮助:

from pandas import Timestamp
test_dat = [[Timestamp('2021-06-25 12:00:00'), Timestamp('2021-06-25 12:00:59'), 99.0, 95.0, 105.0], [Timestamp('2021-06-25 12:01:00'), Timestamp('2021-06-25 12:01:59'), 96.0, 91.0, 102.0], [Timestamp('2021-06-25 12:02:00'), Timestamp('2021-06-25 12:02:59'), 100.0, 96.0, 105.0], [Timestamp('2021-06-25 12:03:00'), Timestamp('2021-06-25 12:03:59'), 96.0, 91.0, 100.0], [Timestamp('2021-06-25 12:04:00'), Timestamp('2021-06-25 12:04:59'), 93.0, 88.0, 102.0], [Timestamp('2021-06-25 12:05:00'), Timestamp('2021-06-25 12:05:59'), 105.0, 99.0, 110.0], [Timestamp('2021-06-25 12:06:00'), Timestamp('2021-06-25 12:06:59'), 102.0, 97.0, 109.0], [Timestamp('2021-06-25 12:07:00'), Timestamp('2021-06-25 12:07:59'), 96.0, 87.0, 102.0], [Timestamp('2021-06-25 12:08:00'), Timestamp('2021-06-25 12:08:59'), 96.0, 93.0, 101.0], [Timestamp('2021-06-25 12:09:00'), Timestamp('2021-06-25 12:09:59'), 96.0, 90.0, 106.0], [Timestamp('2021-06-25 12:10:00'), Timestamp('2021-06-25 12:10:59'), 100.0, 95.0, 110.0], [Timestamp('2021-06-25 12:11:00'), Timestamp('2021-06-25 12:11:59'), 100.0, 95.0, 113.0], [Timestamp('2021-06-25 12:12:00'), Timestamp('2021-06-25 12:12:59'), 98.0, 91.0, 103.0], [Timestamp('2021-06-25 12:13:00'), Timestamp('2021-06-25 12:13:59'), 101.0, 97.0, 108.0], [Timestamp('2021-06-25 12:14:00'), Timestamp('2021-06-25 12:14:59'), 98.0, 91.0, 102.0], [Timestamp('2021-06-25 12:15:00'), Timestamp('2021-06-25 12:15:59'), 100.0, 93.0, 110.0], [Timestamp('2021-06-25 12:16:00'), Timestamp('2021-06-25 12:16:59'), 96.0, 89.0, 104.0], [Timestamp('2021-06-25 12:17:00'), Timestamp('2021-06-25 12:17:59'), 98.0, 95.0, 104.0], [Timestamp('2021-06-25 12:18:00'), Timestamp('2021-06-25 12:18:59'), 95.0, 93.0, 99.0], [Timestamp('2021-06-25 12:19:00'), Timestamp('2021-06-25 12:19:59'), 94.0, 84.0, 104.0], [Timestamp('2021-06-25 12:20:00'), Timestamp('2021-06-25 12:20:59'), 94.0, 90.0, 99.0], [Timestamp('2021-06-25 12:21:00'), Timestamp('2021-06-25 12:21:59'), 98.0, 95.0, 100.0], [Timestamp('2021-06-25 12:22:00'), Timestamp('2021-06-25 12:22:59'), 98.0, 97.0, 102.0], [Timestamp('2021-06-25 12:23:00'), Timestamp('2021-06-25 12:23:59'), 98.0, 96.0, 102.0], [Timestamp('2021-06-25 12:24:00'), Timestamp('2021-06-25 12:24:59'), 98.0, 96.0, 100.0], [Timestamp('2021-06-25 12:25:00'), Timestamp('2021-06-25 12:25:59'), 96.0, 95.0, 100.0], [Timestamp('2021-06-25 12:26:00'), Timestamp('2021-06-25 12:26:59'), 102.0, 98.0, 105.0], [Timestamp('2021-06-25 12:27:00'), Timestamp('2021-06-25 12:27:59'), 97.0, 92.0, 103.0], [Timestamp('2021-06-25 12:28:00'), Timestamp('2021-06-25 12:28:59'), 92.0, 87.0, 99.0], [Timestamp('2021-06-25 12:29:00'), Timestamp('2021-06-25 12:29:59'), 96.0, 94.0, 99.0], [Timestamp('2021-06-25 12:30:00'), Timestamp('2021-06-25 12:30:59'), 97.0, 93.0, 100.0], [Timestamp('2021-06-25 12:31:00'), Timestamp('2021-06-25 12:31:59'), 101.0, 97.0, 103.0], [Timestamp('2021-06-25 12:32:00'), Timestamp('2021-06-25 12:32:59'), 99.0, 95.0, 103.0], [Timestamp('2021-06-25 12:33:00'), Timestamp('2021-06-25 12:33:59'), 101.0, 93.0, 105.0], [Timestamp('2021-06-25 12:34:00'), Timestamp('2021-06-25 12:34:59'), 98.0, 96.0, 101.0], [Timestamp('2021-06-25 12:35:00'), Timestamp('2021-06-25 12:35:59'), 100.0, 93.0, 105.0], [Timestamp('2021-06-25 12:36:00'), Timestamp('2021-06-25 12:36:59'), 103.0, 101.0, 108.0], [Timestamp('2021-06-25 12:37:00'), Timestamp('2021-06-25 12:37:59'), 105.0, 101.0, 111.0], [Timestamp('2021-06-25 12:38:00'), Timestamp('2021-06-25 12:38:59'), 106.0, 103.0, 114.0], [Timestamp('2021-06-25 12:39:00'), Timestamp('2021-06-25 12:39:59'), 107.0, 104.0, 109.0], [Timestamp('2021-06-25 12:40:00'), Timestamp('2021-06-25 12:40:59'), 101.0, 95.0, 109.0], [Timestamp('2021-06-25 12:41:00'), Timestamp('2021-06-25 12:41:59'), 99.0, 96.0, 103.0], [Timestamp('2021-06-25 12:42:00'), Timestamp('2021-06-25 12:42:59'), 99.0, 96.0, 105.0], [Timestamp('2021-06-25 12:43:00'), Timestamp('2021-06-25 12:43:59'), 96.0, 95.0, 98.0], [Timestamp('2021-06-25 12:44:00'), Timestamp('2021-06-25 12:44:59'), 96.0, 94.0, 99.0], [Timestamp('2021-06-25 12:45:00'), Timestamp('2021-06-25 12:45:59'), 102.0, 96.0, 110.0], [Timestamp('2021-06-25 12:46:00'), Timestamp('2021-06-25 12:46:59'), 105.0, 102.0, 109.0], [Timestamp('2021-06-25 12:47:00'), Timestamp('2021-06-25 12:47:59'), 104.0, 100.0, 108.0], [Timestamp('2021-06-25 12:48:00'), Timestamp('2021-06-25 12:48:59'), 100.0, 98.0, 103.0], [Timestamp('2021-06-25 12:49:00'), Timestamp('2021-06-25 12:49:59'), 103.0, 99.0, 110.0], [Timestamp('2021-06-25 12:50:00'), Timestamp('2021-06-25 12:50:59'), 106.0, 99.0, 111.0], [Timestamp('2021-06-25 12:51:00'), Timestamp('2021-06-25 12:51:59'), 100.0, 95.0, 104.0], [Timestamp('2021-06-25 12:52:00'), Timestamp('2021-06-25 12:52:59'), 108.0, 102.0, 113.0], [Timestamp('2021-06-25 12:53:00'), Timestamp('2021-06-25 12:53:59'), 113.0, 106.0, 116.0], [Timestamp('2021-06-25 12:54:00'), Timestamp('2021-06-25 12:54:59'), 109.0, 105.0, 113.0], [Timestamp('2021-06-25 12:55:00'), Timestamp('2021-06-25 12:55:59'), 103.0, 101.0, 110.0], [Timestamp('2021-06-25 12:56:00'), Timestamp('2021-06-25 12:56:59'), 104.0, 94.0, 109.0], [Timestamp('2021-06-25 12:57:00'), Timestamp('2021-06-25 12:57:59'), 93.0, 82.0, 107.0], [Timestamp('2021-06-25 12:58:00'), Timestamp('2021-06-25 12:58:59'), 99.0, 94.0, 104.0], [Timestamp('2021-06-25 12:59:00'), Timestamp('2021-06-25 12:59:59'), 98.0, 92.0, 103.0], [Timestamp('2021-06-25 13:00:00'), Timestamp('2021-06-25 13:00:59'), 98.0, 95.0, 102.0]]
df = pd.DataFrame(test_dat, columns=['Start Time', 'End Time', 'HR', 'Min HR', 'Max HR']).set_index('Start Time')

编辑:没关系,但我使用的阈值是 103 BPM。 (这是从统计量计算出来的,而不是一个幻数。)

【问题讨论】:

  • 您想如何处理移除传感器的中断?计算峰值时是否需要考虑缺失的区间?
  • @ShubhamSharma 我认为这不是问题。我想在高峰期可能会取下或戴上传感器。但在这种情况下,如果峰值已经 5 分钟长,则将被计算在内。如果不是,则不应计算它——与中断无关。
  • 如果heart rate > threshold 超过5 min,我们应该把它分成5 分钟的间隔吗?例如,假设 HR 保持在阈值以上 12 分钟,那么在这种情况下,我们应该计算 1 条连续记录还是 2 条 5 min 连续记录?
  • @ShubhamSharma 好问题!我将编辑我的问题以专门解决它。简短的回答:超过 5 分钟仍然是一个高峰。因此,峰值被定义为心率超过阈值 5 分钟或更长时间。所以,超过 5 分钟仍然是同一个峰值。

标签: python-3.x pandas dataframe datetime


【解决方案1】:

让我们分步进行

df = df.asfreq('1T').reset_index()

th = 96
th_mask = df['HR'].gt(th)
streaks = (~th_mask).cumsum() # continuous streaks

peaks = df[th_mask].groupby(streaks)['Start Time']\
                   .agg(start_time='first', streak_count='count')

peaks = peaks.query('streak_count >= 5')  # hr > th for at least 5 min
peaks_freq_per_day = peaks.groupby(peaks['start_time'].dt.date)['streak_count'].count()

解释

  • Reindex 1 min 频率上的数据帧,以便在没有可用传感器数据时考虑行。
  • 创建一个布尔值th_mask 以识别心率大于给定阈值th 的行
>>> th_mask

0      True
1     False
2      True
3     False
4     False
      ...  
56     True
57    False
58     True
59     True
60     True
Name: HR, Length: 61, dtype: bool
  • th_mask 上使用cumsum 识别心率保持高于阈值th 的连续条纹
>>> streaks

0      0
1      1
2      1
3      2
4      3
      ..
56    15
57    16
58    16
59    16
60    16
Name: HR, Length: 61, dtype: int64
  • 对连续条纹上的数据帧进行分组,并使用firstcount 聚合Start Time
>>> peaks
            start_time  streak_count
HR                                
0  2021-06-25 12:00:00           1
1  2021-06-25 12:02:00           1
3  2021-06-25 12:05:00           2
6  2021-06-25 12:10:00           6
7  2021-06-25 12:17:00           1
10 2021-06-25 12:21:00           4
11 2021-06-25 12:26:00           2
13 2021-06-25 12:30:00          13
15 2021-06-25 12:45:00          12
16 2021-06-25 12:58:00           3
  • 过滤peaks 以仅选择心率保持在阈值以上至少5min 的行
>>> peaks
            start_time  streak_count
HR                                
6  2021-06-25 12:10:00           6
13 2021-06-25 12:30:00          13
15 2021-06-25 12:45:00          12
  • 现在,我们可以通过将 peaks 按每日频率分组并使用 count 聚合来确定每个日历日的峰值频率。
>>> peaks_freq_per_day

start_time
2021-06-25    3
Name: peak_count, dtype: int64

【讨论】:

  • 我稍后会解决这个问题,但我收到以下错误:----> 1 df = data.asfreq('1T').reset_index() ValueError: cannot reindex从重复的轴
  • 我认为您的初始数据帧在index 中包含重复值,即Start time,我们必须先删除索引中的重复值,然后您可以尝试asfreq。要删除重复项,您可以尝试data = data[~data.index.duplicated()]
  • 很好的复制副本!传感器中的一个错误在使用的第一天导致了一些重复。我已经在数据清理器中处理了它,并且代码运行了。我将验证结果并接受您的回答。非常感谢!
  • 好的,谢谢!抱歉耽搁了。我还没有手动验证,但我有足够的信心根据你的解释和我最初的嗅探测试来接受你的答案。再次感谢您。
  • @Timtro 不用担心!感谢您接受答案。如果您遇到任何问题,请随时告诉我,我会尽力帮助您 =)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-09-27
  • 1970-01-01
  • 2014-12-15
  • 1970-01-01
  • 2020-07-07
相关资源
最近更新 更多