-
Notifications
You must be signed in to change notification settings - Fork 53
Add Sankey diagram visualization functions #989
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
Merged
Merged
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
8c6df3e
sankey plots and their tests
d59b90d
pyproject.toml dependency added and AnnData from typehints removed
dcbdaf9
small fix
3a9c5f8
backend option matplotlib in addition to bokeh
1474da9
Merge branch 'main' into enhancement/issue-232-clean
sueoglu f672487
sankey added to init.py
542a9fc
sankey_time_plot test more complex with more timeseries and more states
14abe6e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 5900d21
backend argument in plotting functions removed
2326aa4
merge conflict resolve
c9e6b01
fix
cae40f6
fix
abcb9ba
deleted outcommented code and formatting fixed
3698734
formatting fix
bfa6949
comments addressed other than the one regarding the blobs for test an…
9457560
example and test blobs will be replaced except that done
b5c361f
fix
f15f8a3
Merge branch 'main' into enhancement/issue-232-clean
sueoglu ca28b80
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 472f33b
fix
55fdde2
plot_index.md plots renamed
c9dbcad
submodule conflict
c076666
Merge branch 'main' into enhancement/issue-232-clean
Zethson ccd7c84
decorator for hv backend and sanke_diagram_time singledispatched supp…
6059424
Merge branch 'enhancement/issue-232-clean' of github.com:theislab/ehr…
b26a7a2
data for sankey time example & test with int casting
9edf320
small fix
51bd8af
fail early if not binned, error cases added
5f20e2d
error cases refined
44fb400
last fixes
3feaa93
Merge branch 'main' into enhancement/issue-232-clean
Zethson 737170a
use to_dense from fau
a01313e
Merge branch 'enhancement/issue-232-clean' of github.com:theislab/ehr…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from functools import singledispatch | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| import holoviews as hv | ||
| import numpy as np | ||
| import pandas as pd | ||
| from holoviews import opts | ||
|
|
||
| from ehrapy._compat import _raise_array_type_not_implemented, choose_hv_backend | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Sequence | ||
|
|
||
| from ehrdata import EHRData | ||
|
|
||
|
|
||
| @choose_hv_backend() | ||
| def sankey_diagram( | ||
| edata: EHRData, | ||
| *, | ||
| columns: Sequence[str], | ||
| node_width: int | float = 20, | ||
| node_padding: int | float = 10, | ||
| node_color: str = None, | ||
| label_position: str | None = "right", | ||
| show_values: bool = True, | ||
| title: str | None = None, | ||
| width: int | None = 600, | ||
| height: int | None = 400, | ||
| **kwargs, | ||
|
sueoglu marked this conversation as resolved.
|
||
| ) -> hv.Sankey: | ||
| """Create a Sankey diagram of relationships across the flat observation table. | ||
|
|
||
| Args: | ||
| edata : Central data object. | ||
|
sueoglu marked this conversation as resolved.
Outdated
|
||
| columns : Column names from `edata.obs` to visualize | ||
| node_width : Width of the nodes in the Sankey diagram. | ||
| node_padding : Padding between nodes in the Sankey diagram. | ||
| node_color : Color of the nodes. If None, default coloring is used. | ||
| edge_color : Color of the edges. If None, default coloring is used. | ||
| label_position : Position of the labels on the nodes. Options are 'left', 'right', 'top', 'bottom', or 'center'. | ||
| show_values : Whether to display the values on the edges. | ||
| title : Title of the Sankey diagram. | ||
| width : Width of the Sankey diagram. | ||
| height : Height of the Sankey diagram. | ||
| **kwargs: Additional styling options passed to :class:`holoviews.element.sankey.Sankey`. | ||
|
|
||
|
sueoglu marked this conversation as resolved.
|
||
| Examples: | ||
| >>> import ehrdata as ed | ||
|
sueoglu marked this conversation as resolved.
|
||
| >>> edata = ed.dt.diabetes_130_fairlearn(columns_obs_only=["gender", "race"]) | ||
| >>> ep.pl.sankey_diagram(edata, columns=["gender", "race"]) | ||
|
sueoglu marked this conversation as resolved.
|
||
| """ | ||
| if hv.Store.current_backend is None: | ||
|
sueoglu marked this conversation as resolved.
Outdated
|
||
| raise RuntimeError( | ||
| "No holoviews backend selected. " | ||
| ":func:`holoviews.extension` with ``matplotlib`` or ``bokeh`` must be called before using this function." | ||
| ) | ||
| df = edata.obs[columns] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a series and not a Pandas DataFrame or am I wrong? |
||
|
|
||
| # Build links between consecutive columns | ||
| sources, targets, values = [], [], [] | ||
| source_levels, target_levels = [], [] | ||
| for i in range(len(columns) - 1): | ||
| col_from, col_to = columns[i], columns[i + 1] | ||
| flows = df.groupby([col_from, col_to]).size().reset_index(name="count") | ||
| sources.extend(col_from + ": " + flows[col_from].astype("string")) | ||
| targets.extend(col_to + ": " + flows[col_to].astype("string")) | ||
| values.extend(flows["count"].to_numpy()) | ||
| source_levels.extend([col_from] * len(flows)) | ||
| target_levels.extend([col_to] * len(flows)) | ||
|
|
||
| sankey_df = pd.DataFrame( | ||
| { | ||
| "source": sources, | ||
| "target": targets, | ||
| "value": values, | ||
| "source_level": source_levels, | ||
| "target_level": target_levels, | ||
| } | ||
| ) | ||
|
|
||
| sankey = hv.Sankey(sankey_df, kdims=["source", "target"], vdims=["value"]) | ||
|
|
||
| opts_dict: dict[str, Any] = {} | ||
|
|
||
| if hv.Store.current_backend == "bokeh": | ||
| if width is not None: | ||
| opts_dict["width"] = width | ||
| if height is not None: | ||
| opts_dict["height"] = height | ||
|
|
||
| if node_width is not None: | ||
| opts_dict["node_width"] = node_width | ||
| if node_padding is not None: | ||
| opts_dict["node_padding"] = node_padding | ||
| if title is not None: | ||
| opts_dict["title"] = title | ||
| if node_color is not None: | ||
| opts_dict["node_color"] = node_color | ||
| if label_position is not None: | ||
| opts_dict["label_position"] = label_position | ||
| if show_values is not None: | ||
| opts_dict["show_values"] = show_values | ||
|
|
||
| opts_dict.update(kwargs) | ||
|
|
||
| sankey = sankey.opts(**opts_dict) | ||
| return sankey | ||
|
|
||
|
|
||
| @singledispatch | ||
|
sueoglu marked this conversation as resolved.
Outdated
|
||
| def _generate_sankey(mtx, time: list[Any], state_labels: dict[int, str] | None = None): | ||
| _raise_array_type_not_implemented(mtx, type(mtx)) | ||
|
|
||
|
|
||
| @_generate_sankey.register(np.ndarray) | ||
| def _(mtx: np.ndarray, time: list[Any], state_labels: dict[int, str] | None = None) -> pd.DataFrame: | ||
| if state_labels is None: | ||
| unique_states = np.unique(mtx) | ||
| if np.issubdtype(unique_states.dtype, np.floating): | ||
| unique_states = unique_states[~np.isnan(unique_states)] | ||
|
|
||
| state_labels = {int(state): str(state) for state in unique_states} | ||
|
|
||
| state_values = sorted(state_labels.keys()) | ||
| state_names = [state_labels[val] for val in state_values] | ||
|
|
||
| sources, targets, values = [], [], [] | ||
| for t in range(len(time) - 1): | ||
| for s_from_idx, s_from_val in enumerate(state_values): | ||
| for s_to_idx, s_to_val in enumerate(state_values): | ||
| count = np.sum((mtx[:, t] == s_from_val) & (mtx[:, t + 1] == s_to_val)) | ||
| if count > 0: | ||
| source_label = f"{state_names[s_from_idx]} ({time[t]})" | ||
| target_label = f"{state_names[s_to_idx]} ({time[t + 1]})" | ||
| sources.append(source_label) | ||
| targets.append(target_label) | ||
| values.append(int(count)) | ||
|
|
||
| return pd.DataFrame({"source": sources, "target": targets, "value": values}) | ||
|
|
||
|
|
||
|
sueoglu marked this conversation as resolved.
|
||
| @choose_hv_backend() | ||
| def sankey_diagram_time( | ||
| edata: EHRData, | ||
| *, | ||
| columns: Sequence[str], | ||
| layer: str, | ||
| state_labels: dict[int, str] | None = None, | ||
| node_width: int | float = 20, | ||
| node_padding: int | float = 10, | ||
| node_color: str = None, | ||
| label_position: str | None = "right", | ||
| show_values: bool = True, | ||
| title: str | None = None, | ||
| width: int | None = 600, | ||
| height: int | None = 400, | ||
| **kwargs, | ||
|
sueoglu marked this conversation as resolved.
|
||
| ) -> hv.Sankey: | ||
| """Create a Sankey diagram showing patient state transitions over time. | ||
|
|
||
| Each node represents a state at a specific time point, and flows show the | ||
| number of patients transitioning between states. | ||
|
sueoglu marked this conversation as resolved.
|
||
| Visualizes how patients transition between different states | ||
| (e.g. disease severity, treatment status) across consecutive time points. | ||
|
|
||
| Args: | ||
| edata: Central data object. | ||
| columns: Variable name from `edata.var_names` to visualize | ||
| layer: Name of the layer in `edata.layers` containing the feature data to visualize. | ||
| state_labels: Mapping from numeric state values to readable labels. | ||
| If None, state values will be displayed as strings of their numeric codes (e.g., "0", "1", "2"). | ||
| node_width : Width of the nodes in the Sankey diagram. | ||
|
sueoglu marked this conversation as resolved.
Outdated
|
||
| node_padding : Padding between nodes in the Sankey diagram. | ||
| node_color : Color of the nodes. If None, default coloring is used. | ||
| edge_color : Color of the edges. If None, default coloring is used. | ||
| label_position : Position of the labels on the nodes. Options are 'left', 'right', 'outer', or 'inner'. | ||
| show_values : Whether to display the values on the edges. | ||
| title : Title of the Sankey diagram. | ||
| width : Width of the Sankey diagram. | ||
| height : Height of the Sankey diagram. | ||
| **kwargs: Additional styling options passed to :class:`holoviews.element.sankey.Sankey`. | ||
|
|
||
| Examples: | ||
| >>> import ehrdata as ed | ||
| >>> edata = ed.dt.ehrdata_blobs(base_timepoints=5, n_variables=1, n_observations=5, random_state=59) | ||
| >>> edata.layers["tem_data"] = edata.layers["tem_data"].astype(int) | ||
| >>> state_labels = {-2: "no", -3: "mild", -4: "moderate", -5: "severe", -6: "critical"} | ||
| >>> plot = sankey_diagram_time( | ||
|
sueoglu marked this conversation as resolved.
Outdated
sueoglu marked this conversation as resolved.
Outdated
|
||
| ... edata, | ||
| ... columns=["feature_0"], | ||
| ... layer="tem_data", | ||
| ... state_labels=state_labels, | ||
| ... ) | ||
|
|
||
| .. image:: /_static/docstring_previews/sankey_time.png | ||
| """ | ||
| flare_data = edata[:, edata.var_names.isin(columns), :].layers[layer][:, 0, :] | ||
| time_steps = edata.tem.index.tolist() | ||
|
|
||
| sankey_df = _generate_sankey(flare_data, time=time_steps, state_labels=state_labels) | ||
|
|
||
| sankey = hv.Sankey(sankey_df, kdims=["source", "target"], vdims=["value"]) | ||
|
|
||
| opts_dict: dict[str, Any] = {} | ||
|
|
||
| if hv.Store.current_backend == "bokeh": | ||
| if width is not None: | ||
| opts_dict["width"] = width | ||
| if height is not None: | ||
| opts_dict["height"] = height | ||
|
|
||
| if node_width is not None: | ||
| opts_dict["node_width"] = node_width | ||
| if node_padding is not None: | ||
| opts_dict["node_padding"] = node_padding | ||
| if title is not None: | ||
| opts_dict["title"] = title | ||
| if node_color is not None: | ||
| opts_dict["node_color"] = node_color | ||
| if label_position is not None: | ||
| opts_dict["label_position"] = label_position | ||
| if show_values is not None: | ||
| opts_dict["show_values"] = show_values | ||
|
|
||
| opts_dict.update(kwargs) | ||
|
|
||
| sankey = sankey.opts(**opts_dict) | ||
|
|
||
| return sankey | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.