Imports – we need to add the root path for our package
from pathlib import Path
import sys
repo_path = Path("__file__").absolute().parent.parent.resolve()
assert repo_path.exists()
sys.path.insert(0, str(repo_path))
from bioevents import event_handling, hypnogram
import matplotlib.pyplot as plt
import numpy as np
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
Cell In[2], line 1
----> 1 from bioevents import event_handling, hypnogram
2 import matplotlib.pyplot as plt
3 import numpy as np
File ~/checkouts/readthedocs.org/user_builds/bioevents/checkouts/stable/bioevents/hypnogram.py:8
6 import matplotlib.pyplot as plt
7 import numpy as np
----> 8 from pydantic import (
9 BaseModel,
10 NonNegativeFloat,
11 NonNegativeInt,
12 confloat,
13 model_validator,
14 )
16 from bioevents import event_handling
18 WAKE_PERSISTENCE_THRESHOLD_IN_MINUTES = 2
ImportError: cannot import name 'model_validator' from 'pydantic' (/home/docs/checkouts/readthedocs.org/user_builds/bioevents/envs/stable/lib/python3.9/site-packages/pydantic/__init__.cpython-39-x86_64-linux-gnu.so)
The Event class
help(event_handling.Event.__init__)
Define three events that happened between timestamps 20 and 100, 120 and 126, and 125 and 130, respectively
event_a = event_handling.Event(on=20, off=100)
event_b = event_handling.Event(on=120, off=126)
event_c = event_handling.Event(on=125, off=130)
Find the duration of each event
print(event_a.duration)
print(event_b.duration)
print(event_c.duration)
Overlap analysis
The Event object can be directly compared to another Event via the “overlaps” function. This configurable behavior will form the basis of more complex time series analyses we will do at a higher level.
help(event_handling.Event.overlaps)
print(f"{event_a} overlaps {event_b}: {event_a.overlaps(event_b)}")
print(f"{event_b} overlaps {event_c}: {event_b.overlaps(event_c)}")
As noted in the docstring, the “overlaps” function is governed by the “OverlapTolerances” class. By default, any contemporaneity of one or more samples is considered a viable overlap. For example, this is consistent with "Seizure Detection", Mark L. Scheuer et al., 2021
event_a.tolerances
We could be a little more strict, by dictating that the onsets of any “overlapping” Events must be within “diff_on” samples of event_b:
event_b.tolerances.diff_on = 3
print(f"{event_b} overlaps {event_c}: {event_b.overlaps(event_c)}")
The EventSeries class
Import and Export
Place these events in an EventSeries, starting at timestamp 0 and ending after a duration of 200. We can immediately plot the event series.
events = event_handling.EventSeries([event_a, event_b], duration=200)
plt.subplots(figsize=(10,1))
events.plot()
We can also export our events as a Dataframe
events.as_dataframe()
… or as a boolean array
events.as_bools()
An EventSeries object may also be created from a boolean array. Notably, this can be a more efficient way to store long boolean event time series data, because we capture only the transitions as Events.
my_bool_array = np.random.random(20) > .5
my_bool_array[-1] = my_bool_array[-2] # make sure we have at least two contiguous timestamps at the end
print(f"As a raw boolean array: {my_bool_array}\n")
events2 = event_handling.EventSeries.from_bools(my_bool_array)
print(f"As an EventSeries object: {events2}")
List operations
Because the EventSeries class inherits from the list class, it’s easy to perform list operations with Event objects.
print(len(events))
print(events[-1].duration)
last_event = events.pop()
print(events)
events.append(last_event)
print(events)
The “resolve_events” functionality of the EventSeries class efficiently sorts and combines overlapping events within the same time series, ensuring there are no duplicates.
events.append(event_handling.Event(122, 127))
events.append(event_handling.Event(120, 123))
events.append(event_handling.Event(0,8))
events.append(event_handling.Event(20,30))
events.append(event_handling.Event(25,75))
print(f"After resolution: {events}")
There are also some list functions available to us that work on two or more EventSeries objects
Below, we’ll simulate another EventSeries and combine it with our original one via the “+” list operator
def simulate_event_series(series_duration, event_count, event_max_length):
bools = np.zeros(series_duration, dtype=bool)
ons = np.random.randint(series_duration - event_max_length, size=event_count)
durs = np.random.randint(event_max_length, size=event_count)
for on, dur in zip(ons, durs):
bools[on: on + dur] = True
bools[-1] = bools[-2] # make sure we have at least two contiguous timestamps at the end
return event_handling.EventSeries.from_bools(bools)
fig, axs = plt.subplots(3, 1, figsize=(10,3), sharex=True)
plt.axes(axs[0])
events.plot()
plt.title("Events")
plt.axes(axs[1])
np.random.seed(2)
events2 = simulate_event_series(int(events.duration), 3, 20)
events2.plot()
plt.title("Events 2")
plt.axes(axs[2])
boolean_intersection = events + events2
boolean_intersection.plot()
plt.title("Added")
plt.tight_layout()
Logical operators
print(events)
print(events2)
events & events2
events | events2
events[0] == event_handling.Event(0, 8)
Statistics
By leveraging the aforementioned “overlaps” functionality of the Event class, we can start performing quantitative analytics directly on a pair of EventSeries objects. For instance, we can generate an agreement table between two EventSeries.
events.compute_agreement(events2, normalize=False)
We can manipulate the Event tolerances to be much more strict…
tol = event_handling.OverlapTolerances(ratio_on=.1)
events.set_tolerances(tol)
events.compute_agreement(events2, normalize=False)
We can generate an epoch-wise confusion matrix
events.epoch_confusion_matrix(events2, normalize=None)
We can also generate an event-wise confusion matrix, although the TN is always 0, given that it’s difficult to define a truly-identified non-event:
events.event_confusion_matrix(events2, normalize=None)
The Hypnogram class
The EventStack class enables handling of multiple contemporaneous EventSeries objects which pertain to different conditions. This is achieved by subclassing the ‘dict’ class.
A good example of an EventStack is the Hypnogram:
from tests import utils
hypno = utils.simulate_hypnogram(series_duration=200, num_cycles=5, avg_duration=5, seed=3)
fig = hypno.plot()
plt.title("Hypnogram")
Now let’s compare two hypnograms
true = hypno.as_array()
pred = [hypnogram.SleepStages(c) for c in np.roll(true, 2)]
other = hypnogram.Hypnogram.from_array(pred)
fig = hypno.plot()
fig = other.plot()
On a per-epoch basis…
hypno.epoch_confusion_matrix(other)
Or on a per-event basis…
tol = event_handling.OverlapTolerances(ratio_on=.2, ratio_off=.2)
hypno.set_tolerances(tol)
hypno.event_confusion_matrix(other)