Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plot_hypnogram display could be clearer #106

Closed
remrama opened this issue Nov 27, 2022 · 1 comment · Fixed by #108
Closed

plot_hypnogram display could be clearer #106

remrama opened this issue Nov 27, 2022 · 1 comment · Fixed by #108
Assignees
Labels
enhancement 🚧 New feature or request

Comments

@remrama
Copy link
Collaborator

remrama commented Nov 27, 2022

I'm somewhat confused about how hypnograms are represented with yasa.plot_hypnogram, and even somewhat suspicious that the plotted hypnogram is shifted (left) by 1 epoch. I suppose there are multiple ways to view a graph like this, especially how much interpretation you might put on vertical lines. I think the hypnogram includes stages that represent a chunk of time (e.g., epoch 1 with standard 1/30 sf represents the window between 0-30 seconds), whereas the current plotting with plt.step doesn't handle that well.

Examples below, but first I'll jump ahead to my conclusion: Maybe a combined use of ax.hlines (for the lines) and ax.stairs (for the fill) would make for a better and more accurate visualization?

Examples

Using some super-short simple hypnograms...

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yasa

sf = 1/30

5-min hypnograms aren't 5 minutes

This example shows plots of 2 different hypnograms that should be 5 minutes long. They only differ by whether it takes 30 or 60 seconds to fall asleep. Note the difference in the first Wake epoch(s) between the two graphs. In the first graph, it should be 60 seconds long but it's 30. In the second graph, it should be 30 seconds long but it's... 0?

#### Draw 2 different plots,
#### 1 with 30-sec SOL, another with 60-sec SOL

hypno_str_a = ["W", "W", "N1", "N1", "N2", "N2", "N3", "N3", "R", "R"]
hypno_str_b = ["W", "N1", "N1", "N2", "N2", "N3", "N3", "R", "R", "W"]
hypno_int_a = yasa.hypno_str_to_int(hypno_str_a)
hypno_int_b = yasa.hypno_str_to_int(hypno_str_b)

# Some transformations usually done within yasa.plot_hypnogram
hypno_yvals_a = pd.Series(hypno_int_a).map({-2: -2, -1: -1, 0: 0, 1: 2, 2: 3, 3: 4, 4: 1}).mul(-1).values
hypno_yvals_b = pd.Series(hypno_int_b).map({-2: -2, -1: -1, 0: 0, 1: 2, 2: 3, 3: 4, 4: 1}).mul(-1).values
t_hyp = np.arange(hypno_yvals_a) / (sf * 60)

fig, axes = plt.subplots(nrows=2, figsize=(4, 6), sharex=True, sharey=True)
axes[0].step(t_hyp, hypno_yvals_a, color="black", lw=2)
axes[1].step(t_hyp, hypno_yvals_b, color="black", lw=2)
axes[0].set_title("5-min hypnogram with 30-sec SOL:\n" + " - ".join(hypno_str_a))
axes[1].set_title("5-min hypnogram with 30-sec SOL:\n" + " - ".join(hypno_str_b))

# Titles/labels/limits
axes[1].set_xlim(-0.5, 5.5)
axes[1].set_ylim(-4.5, 0.5)
axes[1].set_yticks([0, -1, -2, -3, -4])
axes[1].set_yticklabels(["W", "R", "N1", "N2", "N3"])
fig.supxlabel("Time [mins]")
plt.tight_layout()

example_5min_SOL

Zoom in on 1-min: flat line despite 2 stages

This time using YASA to make sure my code wasn't different from the underlying function.

#### Silly 2-epoch example to highlight the problem.
yasa.plot_hypnogram([0, 1], sf)
plt.suptitle("1-min hypnogram: W - N1")
plt.tight_layout()

example_1min

Alternatives

I like the use of plt.stairs() because of the explicit passing of bin edges. Also the fill option is nice, though you can see that in a case where a hypnogram ends on non-wake, it will automatically jump up to 0. This can be changed with parameters! Just pointing it out as something to be careful about with defaults. I actually like plt.hlines() because it avoids the whole vertical line thing.

# Make a super-simple super-short hypnogram.
# (5-minutes, decreasing through 2 30-s epochs of each stage.)
hypno_str = ["W", "W", "N1", "N1", "N2", "N2", "N3", "N3", "R", "R"]
hypno_int = yasa.hypno_str_to_int(hypno_str)

# Some transformations usually done within yasa.plot_hypnogram
hypno_yvals = pd.Series(hypno_int
    ).map({-2: -2, -1: -1, 0: 0, 1: 2, 2: 3, 3: 4, 4: 1}
    ).mul(-1).values

n_epochs = hypno_yvals.size
# x-values for plt.step, same size as hypno
t_hyp = np.arange(n_epochs) / (sf * 60)
# x-values for plt.stairs and plt.hlines, 1 longer than hypno
bins = np.arange(n_epochs + 1) / (sf * 60)

# Draw 3 different plots of the same data
fig, axes = plt.subplots(nrows=3, figsize=(4, 6), sharex=True, sharey=True)

axes[0].step(t_hyp, hypno_yvals, color="black", lw=2)
axes[0].set_title("ax.step()")

axes[1].stairs(hypno_yvals, bins, color="gainsboro", fill=True)
axes[1].stairs(hypno_yvals, bins, lw=2, color="black")
axes[1].set_title("ax.stairs(fill=True)")

axes[2].hlines(hypno_yvals, xmin=bins[:-1], xmax=bins[1:], lw=2, color="black")
axes[2].set_title("ax.hlines()")

# Titles/labels/limits
axes[2].set_xlim(-0.5, 5.5)
axes[2].set_ylim(-4.5, 0.5)
axes[2].set_yticks([0, -1, -2, -3, -4])
axes[2].set_yticklabels(["W", "R", "N1", "N2", "N3"])
fig.suptitle("5-min hypnogram:\n" + " - ".join(hypno_str))
fig.supxlabel("Time [mins]")
plt.tight_layout()

example_5min_custom

YASA's function

Again just showing that my code is providing the same output as YASA. This should match the first panel of previous image, where there should be 60 seconds of Wake to start, but there's 30.

yasa.plot_hypnogram(hypno_int, sf)
plt.suptitle("5-min hypnogram:\n" + " - ".join(hypno_str))
plt.tight_layout()

example_5min_yasa

@raphaelvallat
Copy link
Owner

Hi @remrama!

Thanks for the super detailed PR, loved it! Let's go with plt.stairs, it is indeed much better than the current plt.step. I think plt.hlines gets messy with longer hypnograms. I like the fill parameter too, definitely would like to add this as a customization parameter.

hypno_str = np.tile(["W", "W", "N1", "N2", "N2", "N2", "N3", "N3", "N2", "R", "R"], 10)

image

@raphaelvallat raphaelvallat added the enhancement 🚧 New feature or request label Nov 29, 2022
@remrama remrama mentioned this issue Dec 1, 2022
@raphaelvallat raphaelvallat linked a pull request Dec 1, 2022 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement 🚧 New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants