#
# This file is part of the chi repository
# (https://github.com/DavAug/chi/) which is released under the
# BSD 3-clause license. See accompanying LICENSE.md for copyright notice and
# full license details.
#
import numpy as np
import pandas as pd
import plotly.colors
import plotly.graph_objects as go
from chi import plots
[docs]
class PDPredictivePlot(plots.SingleFigure):
"""
A figure class that visualises the predictions of a predictive
pharmacodynamic model.
Extends :class:`SingleFigure`.
Parameters
----------
updatemenu
Boolean flag that enables or disables interactive buttons, such as a
logarithmic scale switch for the y-axis.
"""
def __init__(self, updatemenu=True):
super(PDPredictivePlot, self).__init__(updatemenu)
def _add_data_trace(self, _id, times, measurements, color):
"""
Adds scatter plot of an indiviudals pharamcodynamics to figure.
"""
self._fig.add_trace(
go.Scatter(
x=times,
y=measurements,
name="ID: %d" % _id,
showlegend=True,
mode="markers",
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))))
def _add_prediction_scatter_trace(self, times, samples):
"""
Adds scatter plot of samples from the predictive model.
"""
# Get colour (light blueish)
color = plotly.colors.qualitative.Pastel2[1]
# Add trace
self._fig.add_trace(
go.Scatter(
x=times,
y=samples,
name="Predicted samples",
showlegend=True,
mode="markers",
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))))
def _add_prediction_bulk_prob_trace(self, data):
"""
Adds the bulk probabilities as two line plots (one for upper and lower
limit) and shaded area to the figure.
"""
# Construct times that go from min to max and back to min
# (Important for shading with 'toself')
times = data['Time'].unique()
times = np.hstack([times, times[::-1]])
# Get unique bulk probabilities and sort in descending order
bulk_probs = data['Bulk probability'].unique()
bulk_probs[::-1].sort()
# Get colors (shift start a little bit, because 0th level is too light)
n_traces = len(bulk_probs)
shift = 2
colors = plotly.colors.sequential.Blues[shift:shift+n_traces]
# Add traces
for trace_id, bulk_prob in enumerate(bulk_probs):
# Get relevant upper and lower percentiles
mask = data['Bulk probability'] == bulk_prob
reduced_data = data[mask]
upper = reduced_data['Upper'].to_numpy()
lower = reduced_data['Lower'].to_numpy()
values = np.hstack([upper, lower[::-1]])
# Add trace
self._fig.add_trace(go.Scatter(
x=times,
y=values,
line=dict(width=1, color=colors[trace_id]),
fill='toself',
legendgroup='Model prediction',
name='Predictive model',
text="%s Bulk" % bulk_prob,
hoverinfo='text',
showlegend=True if trace_id == n_traces-1 else False))
def _compute_bulk_probs(self, data, bulk_probs, time_key, sample_key):
"""
Computes the upper and lower percentiles from the predictive model
samples, corresponding to the provided bulk probabilities.
"""
# Create container for perecentiles
container = pd.DataFrame(columns=[
'Time', 'Upper', 'Lower', 'Bulk probability'])
# Translate bulk probabilities into percentiles
percentiles = []
for bulk_prob in bulk_probs:
lower = 0.5 - bulk_prob / 2
upper = 0.5 + bulk_prob / 2
percentiles.append([bulk_prob, lower, upper])
# Get unique times
unique_times = data[time_key].unique()
# Fill container with percentiles for each time
for time in unique_times:
# Mask relevant data
mask = data[time_key] == time
reduced_data = data[mask]
# Get percentiles
percentile_df = reduced_data[sample_key].rank(
pct=True)
for item in percentiles:
bulk_prob, lower, upper = item
# Get biomarker value corresponding to percentiles
mask = percentile_df <= lower
biom_lower = reduced_data[mask][sample_key].max()
mask = percentile_df >= upper
biom_upper = reduced_data[mask][sample_key].min()
# Append percentiles to container
container = pd.concat([container, pd.DataFrame({
'Time': [time],
'Lower': [biom_lower],
'Upper': [biom_upper],
'Bulk probability': [str(bulk_prob)]})])
return container
[docs]
def add_data(
self, data, observable=None, id_key='ID', time_key='Time',
obs_key='Observable', value_key='Value'):
"""
Adds pharmacodynamic time series data of (multiple) individuals to
the figure.
Expects a :class:`pandas.DataFrame` with an ID, a time, an
observable and a value column, and adds a scatter plot of the
measured time series to the figure. Each individual receives a
unique colour.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD data in form of
an ID, time, and observable column.
observable
The predicted observable. This argument is used to determine the
relevant rows in the dataframe. If ``None``, the first observable
in the observable column is selected.
id_key
Key label of the :class:`DataFrame` which specifies the ID column.
The ID refers to the identity of an individual. Defaults to
``'ID'``.
time_key
Key label of the :class:`DataFrame` which specifies the time
column. Defaults to ``'Time'``.
obs_key
Key label of the :class:`DataFrame` which specifies the
observable column. Defaults to ``'Observable'``.
value_key
Key label of the :class:`DataFrame` which specifies the column of
the measured values. Defaults to ``'Value'``.
"""
# Check input format
if not isinstance(data, pd.DataFrame):
raise TypeError(
'Data has to be pandas.DataFrame.')
for key in [id_key, time_key, obs_key, value_key]:
if key not in data.keys():
raise ValueError(
'Data does not have the key <' + str(key) + '>.')
# Default to first bimoarker, if observable is not specified
biom_types = data[obs_key].unique()
if observable is None:
observable = biom_types[0]
if observable not in biom_types:
raise ValueError(
'The observable could not be found in the observable column.')
# Mask data for observable
mask = data[obs_key] == observable
data = data[mask]
# Get a colour scheme
colors = plotly.colors.qualitative.Plotly
n_colors = len(colors)
# Fill figure with scatter plots of individual data
ids = data[id_key].unique()
for index, _id in enumerate(ids):
# Get individual data
mask = data[id_key] == _id
times = data[time_key][mask]
measurements = data[value_key][mask]
color = colors[index % n_colors]
# Create Scatter plot
self._add_data_trace(_id, times, measurements, color)
[docs]
def add_prediction(
self, data, observable=None, bulk_probs=[0.9], time_key='Time',
obs_key='Observable', value_key='Value'):
r"""
Adds the prediction to the figure.
Expects a :class:`pandas.DataFrame` with a time, an observable and a
value column. The time column determines the times of the
measurements and the value column the measured value.
The observable column determines the observable.
A list of bulk probabilities ``bulk_probs`` can be specified, which are
then added as area to the figure. The corresponding upper and lower
percentiles are estimated from the ranks of the provided
samples.
.. warning::
For low sample sizes the illustrated bulk probabilities may deviate
significantly from the theoretical bulk probabilities. The upper
and lower limit are determined from the rank of the samples for
each time point.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD simulation in
form of a time and observable column.
observable
The predicted observable. This argument is used to determine the
relevant rows in the dataframe. If ``None``, the first observable
in the observable column is selected.
bulk_probs
A list of bulk probabilities that are illustrated in the
figure. If ``None`` the samples are illustrated as a scatter plot.
time_key
Key label of the :class:`pandas.DataFrame` which specifies the time
column. Defaults to ``'Time'``.
obs_key
Key label of the :class:`pandas.DataFrame` which specifies the
observable column. Defaults to ``'Observable'``.
value_key
Key label of the :class:`pandas.DataFrame` which specifies the
value column. Defaults to ``'Value'``.
"""
# Check input format
if not isinstance(data, pd.DataFrame):
raise TypeError(
'Data has to be pandas.DataFrame.')
for key in [time_key, obs_key, value_key]:
if key not in data.keys():
raise ValueError(
'Data does not have the key <' + str(key) + '>.')
# Default to first bimoarker, if observable is not specified
biom_types = data[obs_key].dropna().unique()
if observable is None:
observable = biom_types[0]
if observable not in biom_types:
raise ValueError(
'The observable could not be found in the observable column.')
# Mask data for observable
mask = data[obs_key] == observable
data = data[mask]
# Add samples as scatter plot if no bulk probabilites are provided, and
# terminate method
if bulk_probs is None:
times = data[time_key]
samples = data[value_key]
self._add_prediction_scatter_trace(times, samples)
return None
# Not more than 7 bulk probabilities are allowed (Purely aesthetic
# criterion)
if len(bulk_probs) > 7:
raise ValueError(
'At most 7 different bulk probabilities can be illustrated at '
'the same time.')
# Make sure that bulk probabilities are between 0 and 1
bulk_probs = [float(probability) for probability in bulk_probs]
for probability in bulk_probs:
if (probability < 0) or (probability > 1):
raise ValueError(
'The provided bulk probabilities have to between 0 and 1.')
# Add bulk probabilities to figure
percentile_df = self._compute_bulk_probs(
data, bulk_probs, time_key, value_key)
self._add_prediction_bulk_prob_trace(percentile_df)
[docs]
class PKPredictivePlot(plots.SingleSubplotFigure):
"""
A figure class that visualises the predictions of a predictive
pharmacokinetic model.
Extends :class:`SingleSubplotFigure`.
Parameters
----------
updatemenu
Boolean flag that enables or disables interactive buttons, such as a
logarithmic scale switch for the y-axis.
"""
def __init__(self, updatemenu=True):
super(PKPredictivePlot, self).__init__()
self._create_template_figure(
rows=2, cols=1, shared_x=True, row_heights=[0.2, 0.8])
# Define legend name of prediction
self._prediction_name = 'Predictive model'
if updatemenu:
self._add_updatemenu()
def _add_dose_trace(
self, _id, times, doses, durations, color,
is_prediction=False):
"""
Adds scatter plot of dose events to figure.
"""
# Convert durations to strings
durations = [
'Dose duration: ' + str(duration) for duration in durations]
name = "ID: %s" % str(_id)
if is_prediction is True:
name = 'Predictive model'
# Add scatter plot of dose events
self._fig.add_trace(
go.Scatter(
x=times,
y=doses,
name=name,
legendgroup=name,
showlegend=False,
mode="markers",
text=durations,
hoverinfo='text',
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))),
row=1,
col=1)
def _add_biom_trace(self, _id, times, measurements, color):
"""
Adds scatter plot of an indiviudals pharamcokinetics to figure.
"""
self._fig.add_trace(
go.Scatter(
x=times,
y=measurements,
name="ID: %s" % str(_id),
legendgroup="ID: %s" % str(_id),
showlegend=True,
mode="markers",
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))),
row=2,
col=1)
def _add_updatemenu(self):
"""
Adds a button to the figure that switches the biomarker scale from
linear to logarithmic.
"""
self._fig.update_layout(
updatemenus=[
dict(
type="buttons",
direction="left",
buttons=list([
dict(
args=[{"yaxis2.type": "linear"}],
label="Linear y-scale",
method="relayout"
),
dict(
args=[{"yaxis2.type": "log"}],
label="Log y-scale",
method="relayout"
)
]),
pad={"r": 0, "t": -10},
showactive=True,
x=0.0,
xanchor="left",
y=1.15,
yanchor="top"
)
]
)
def _add_prediction_scatter_trace(self, times, samples):
"""
Adds scatter plot of samples from the predictive model.
"""
# Get colour (light blueish)
color = plotly.colors.qualitative.Pastel2[1]
# Add trace
self._fig.add_trace(
go.Scatter(
x=times,
y=samples,
name="Predicted samples",
showlegend=True,
mode="markers",
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))))
def _add_prediction_bulk_prob_trace(self, data, colors):
"""
Adds the bulk probabilities as two line plots (one for upper and lower
limit) and shaded area to the figure.
"""
# Construct times that go from min to max and back to min
# (Important for shading with 'toself')
times = data['Time'].unique()
times = np.hstack([times, times[::-1]])
# Get unique bulk probabilities and sort in descending order
bulk_probs = data['Bulk probability'].unique()
bulk_probs[::-1].sort()
# Add traces
n_traces = len(bulk_probs)
for trace_id, bulk_prob in enumerate(bulk_probs):
# Get relevant upper and lower percentiles
mask = data['Bulk probability'] == bulk_prob
reduced_data = data[mask]
upper = reduced_data['Upper'].to_numpy()
lower = reduced_data['Lower'].to_numpy()
values = np.hstack([upper, lower[::-1]])
# Add trace
self._fig.add_trace(go.Scatter(
x=times,
y=values,
line=dict(width=1, color=colors[trace_id]),
fill='toself',
legendgroup=self._prediction_name,
name=self._prediction_name,
text="%s Bulk" % bulk_prob,
hoverinfo='text',
showlegend=True if trace_id == n_traces-1 else False),
row=2,
col=1)
def _compute_bulk_probs(self, data, bulk_probs, time_key, sample_key):
"""
Computes the upper and lower percentiles from the predictive model
samples, corresponding to the provided bulk probabilities.
"""
# Create container for perecentiles
container = pd.DataFrame(columns=[
'Time', 'Upper', 'Lower', 'Bulk probability'])
# Translate bulk probabilities into percentiles
percentiles = []
for bulk_prob in bulk_probs:
lower = 0.5 - bulk_prob / 2
upper = 0.5 + bulk_prob / 2
percentiles.append([bulk_prob, lower, upper])
# Get unique times
unique_times = data[time_key].unique()
# Fill container with percentiles for each time
for time in unique_times:
# Mask relevant data
mask = data[time_key] == time
reduced_data = data[mask]
# Get percentiles
percentile_df = reduced_data[sample_key].rank(
pct=True)
for item in percentiles:
bulk_prob, lower, upper = item
# Get biomarker value corresponding to percentiles
mask = percentile_df <= lower
biom_lower = reduced_data[mask][sample_key].max()
mask = percentile_df >= upper
biom_upper = reduced_data[mask][sample_key].min()
# Append percentiles to container
container = pd.concat([container, pd.DataFrame({
'Time': [time],
'Lower': [biom_lower],
'Upper': [biom_upper],
'Bulk probability': [str(bulk_prob)]})])
return container
[docs]
def add_data(
self, data, observable=None, id_key='ID', time_key='Time',
obs_key='Observable', value_key='Value', dose_key='Dose',
dose_duration_key='Duration'):
"""
Adds pharmacokinetic time series data of (multiple) individuals to
the figure.
Expects a :class:`pandas.DataFrame` with an ID, a time, an
observable and a value column, and adds a scatter plot of the
measuremed time series to the figure. The dataframe is also expected
to have information about the administered dose via a dose and a
dose duration column. Each individual receives a unique colour.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD data in form of
an ID, time, and observable column.
observable
The measured observable. This argument is used to determine the
relevant rows in the dataframe. If ``None``, the first observable
in the observable column is selected.
id_key
Key label of the :class:`DataFrame` which specifies the ID column.
The ID refers to the identity of an individual. Defaults to
``'ID'``.
time_key
Key label of the :class:`DataFrame` which specifies the time
column. Defaults to ``'Time'``.
obs_key
Key label of the :class:`DataFrame` which specifies the
observable column. Defaults to ``'Observable'``.
value_key
Key label of the :class:`DataFrame` which specifies the column of
the measured values. Defaults to ``'Value'``.
dose_key
Key label of the :class:`DataFrame` which specifies the dose
column. Defaults to ``'Dose'``.
dose_duration_key
Key label of the :class:`DataFrame` which specifies the dose
duration column. Defaults to ``'Duration'``.
"""
# Check input format
if not isinstance(data, pd.DataFrame):
raise TypeError(
'Data has to be pandas.DataFrame.')
keys = [
id_key, time_key, obs_key, value_key, dose_key, dose_duration_key]
for key in keys:
if key not in data.keys():
raise ValueError(
'Data does not have the key <' + str(key) + '>.')
# Default to first bimoarker, if observable is not specified
biom_types = data[obs_key].dropna().unique()
if observable is None:
observable = biom_types[0]
if observable not in biom_types:
raise ValueError(
'The observable could not be found in the observable column.')
# Get dose information
mask = data[dose_key].notnull()
dose_data = data[mask][[id_key, time_key, dose_key, dose_duration_key]]
# Mask data for observable
mask = data[obs_key] == observable
data = data[mask][[id_key, time_key, value_key]]
# Set axis labels to dataframe keys
self.set_axis_labels(time_key, obs_key, dose_key)
# Get a colour scheme
colors = plotly.colors.qualitative.Plotly
n_colors = len(colors)
# Fill figure with scatter plots of individual data
ids = data[id_key].unique()
for index, _id in enumerate(ids):
# Get doses applied to individual
mask = dose_data[id_key] == _id
dose_times = dose_data[time_key][mask]
doses = dose_data[dose_key][mask]
durations = dose_data[dose_duration_key][mask]
# Get observable measurements
mask = data[id_key] == _id
times = data[time_key][mask]
measurements = data[value_key][mask]
# Get a color for the individual
color = colors[index % n_colors]
# Create scatter plot of dose events
self._add_dose_trace(_id, dose_times, doses, durations, color)
# Create Scatter plot
self._add_biom_trace(_id, times, measurements, color)
[docs]
def add_prediction(
self, data, observable=None, bulk_probs=[0.9], time_key='Time',
obs_key='Observable', value_key='Value', dose_key='Dose',
dose_duration_key='Duration'):
r"""
Adds the prediction for the observable pharmacokinetic observable
values to the figure.
Expects a :class:`pandas.DataFrame` with a time, an observable and a
value column. The time column determines the time of the observable
measurement and the sample column the corresponding observable
measurement. The observable column determines the observable type. The
dataframe is also expected to have information about the administered
dose via a dose and a dose duration column.
A list of bulk probabilities ``bulk_probs`` can be specified, which are
then added as area to the figure. The corresponding upper and lower
percentiles are estimated from the ranks of the provided
samples.
.. warning::
For low sample sizes the illustrated bulk probabilities may deviate
significantly from the theoretical bulk probabilities. The upper
and lower limit are determined from the rank of the samples for
each time point.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD simulation in
form of a time and observable column.
observable
The predicted observable. This argument is used to determine the
relevant rows in the dataframe. If ``None``, the first observable
in the observable column is selected.
bulk_probs
A list of bulk probabilities that are illustrated in the
figure. If ``None`` the samples are illustrated as a scatter plot.
time_key
Key label of the :class:`pandas.DataFrame` which specifies the time
column. Defaults to ``'Time'``.
obs_key
Key label of the :class:`pandas.DataFrame` which specifies the
observable column. Defaults to ``'Observable'``.
value_key
Key label of the :class:`pandas.DataFrame` which specifies the
value column. Defaults to ``'Value'``.
dose_key
Key label of the :class:`DataFrame` which specifies the dose
column. Defaults to ``'Dose'``.
dose_duration_key
Key label of the :class:`DataFrame` which specifies the dose
duration column. Defaults to ``'Duration'``.
"""
# Check input format
if not isinstance(data, pd.DataFrame):
raise TypeError(
'Data has to be pandas.DataFrame.')
keys = [
time_key, obs_key, value_key, dose_key, dose_duration_key]
for key in keys:
if key not in data.keys():
raise ValueError(
'Data does not have the key <' + str(key) + '>.')
# Default to first bimoarker, if observable is not specified
biom_types = data[obs_key].dropna().unique()
if observable is None:
observable = biom_types[0]
if observable not in biom_types:
raise ValueError(
'The observable could not be found in the observable column.')
# Get dose information
mask = data[dose_key].notnull()
dose_data = data[mask][[time_key, dose_key, dose_duration_key]]
# Mask data for observable
mask = data[obs_key] == observable
data = data[mask][[time_key, value_key]]
# Set axis labels to dataframe keys
self.set_axis_labels(time_key, obs_key, dose_key)
# Add samples as scatter plot if no bulk probabilites are provided, and
# terminate method
if bulk_probs is None:
times = data[time_key]
samples = data[value_key]
self._add_prediction_scatter_trace(times, samples)
return None
# Not more than 7 bulk probabilities are allowed (Purely aesthetic
# criterion)
if len(bulk_probs) > 7:
raise ValueError(
'At most 7 different bulk probabilities can be illustrated at '
'the same time.')
# Make sure that bulk probabilities are between 0 and 1
bulk_probs = [float(probability) for probability in bulk_probs]
for probability in bulk_probs:
if (probability < 0) or (probability > 1):
raise ValueError(
'The provided bulk probabilities have to between 0 and 1.')
# Define colour scheme
shift = 2
colors = plotly.colors.sequential.Blues[shift:]
# Create scatter plot of dose events
self._add_dose_trace(
_id=None,
times=dose_data[time_key],
doses=dose_data[dose_key],
durations=dose_data[dose_duration_key],
color=colors[0],
is_prediction=True)
# Add bulk probabilities to figure
percentile_df = self._compute_bulk_probs(
data, bulk_probs, time_key, value_key)
self._add_prediction_bulk_prob_trace(percentile_df, colors)
[docs]
def set_axis_labels(self, time_label, biom_label, dose_label):
"""
Sets the label of the time axis, the biomarker axis, and the dose axis.
"""
self._fig.update_xaxes(title=time_label, row=2)
self._fig.update_yaxes(title=dose_label, row=1)
self._fig.update_yaxes(title=biom_label, row=2)
[docs]
class PDTimeSeriesPlot(plots.SingleFigure):
"""
A figure class that visualises measurements of a pharmacodynamic
observables across multiple individuals.
Measurements of a pharmacodynamic observables over time are visualised as a
scatter plot.
Extends :class:`SingleFigure`.
Parameters
----------
updatemenu
Boolean flag that enables or disables interactive buttons, such as a
logarithmic scale switch for the y-axis.
"""
def __init__(self, updatemenu=True):
super(PDTimeSeriesPlot, self).__init__(updatemenu)
def _add_data_trace(self, _id, times, measurements, color):
"""
Adds scatter plot of an indiviudals pharamcodynamics to figure.
"""
self._fig.add_trace(
go.Scatter(
x=times,
y=measurements,
name="ID: %d" % _id,
showlegend=True,
mode="markers",
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))))
def _add_simulation_trace(self, times, biomarker):
"""
Adds scatter plot of an indiviudals pharamcodynamics to figure.
"""
self._fig.add_trace(
go.Scatter(
x=times,
y=biomarker,
name="Model",
showlegend=True,
mode="lines",
line=dict(color='black')))
[docs]
def add_data(
self, data, observable=None, id_key='ID', time_key='Time',
obs_key='Observable', value_key='Value'):
"""
Adds pharmacodynamic time series data of (multiple) individuals to
the figure.
Expects a :class:`pandas.DataFrame` with an ID, a time, an
observable and a value column, and adds a scatter plot of the
measuremed time series to the figure. Each individual receives a
unique colour.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD data in form of
an ID, time, and observable column.
observable
The measured bimoarker. This argument is used to determine the
relevant rows in the dataframe. If ``None``, the first observable
in the observable column is selected.
id_key
Key label of the :class:`DataFrame` which specifies the ID column.
The ID refers to the identity of an individual. Defaults to
``'ID'``.
time_key
Key label of the :class:`DataFrame` which specifies the time
column. Defaults to ``'Time'``.
obs_key
Key label of the :class:`DataFrame` which specifies the
observable column. Defaults to ``'Observable'``.
value_key
Key label of the :class:`DataFrame` which specifies the column of
the measured values. Defaults to ``'Value'``.
"""
# Check input format
if not isinstance(data, pd.DataFrame):
raise TypeError(
'Data has to be pandas.DataFrame.')
for key in [id_key, time_key, obs_key, value_key]:
if key not in data.keys():
raise ValueError(
'Data does not have the key <' + str(key) + '>.')
# Default to first bimoarker, if observable is not specified
biom_types = data[obs_key].dropna().unique()
if observable is None:
observable = biom_types[0]
if observable not in biom_types:
raise ValueError(
'The observable could not be found in the observable column.')
# Mask data for observable
mask = data[obs_key] == observable
data = data[mask]
# Get a colour scheme
colors = plotly.colors.qualitative.Plotly
n_colors = len(colors)
# Fill figure with scatter plots of individual data
ids = data[id_key].unique()
for index, _id in enumerate(ids):
# Get individual data
mask = data[id_key] == _id
times = data[time_key][mask]
measurements = data[value_key][mask]
color = colors[index % n_colors]
# Create Scatter plot
self._add_data_trace(_id, times, measurements, color)
[docs]
def add_simulation(self, data, time_key='Time', value_key='Value'):
"""
Adds a pharmacodynamic time series simulation to the figure.
Expects a :class:`pandas.DataFrame` with a time and a value
column, and adds a line plot of the simulated time series to the
figure.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD simulation in
form of a time and value column.
time_key
Key label of the :class:`DataFrame` which specifies the time
column. Defaults to ``'Time'``.
value_key
Key label of the :class:`DataFrame` which specifies the
value column. Defaults to ``'Value'``.
"""
# Check input format
if not isinstance(data, pd.DataFrame):
raise TypeError(
'Data has to be pandas.DataFrame.')
for key in [time_key, value_key]:
if key not in data.keys():
raise ValueError(
'Data does not have the key <' + str(key) + '>.')
times = data[time_key]
values = data[value_key]
self._add_simulation_trace(times, values)
[docs]
class PKTimeSeriesPlot(plots.SingleSubplotFigure):
"""
A figure class that visualises measurements of a pharmacokinetic observable
across multiple individuals.
Measurements of a pharmacokinetic observable over time are visualised as a
scatter plot.
Extends :class:`SingleSubplotFigure`.
Parameters
----------
updatemenu
Boolean flag that enables or disables interactive buttons, such as a
logarithmic scale switch for the y-axis.
"""
def __init__(self, updatemenu=True):
super(PKTimeSeriesPlot, self).__init__()
self._create_template_figure(
rows=2, cols=1, shared_x=True, row_heights=[0.2, 0.8])
if updatemenu:
self._add_updatemenu()
def _add_dose_trace(self, _id, times, doses, durations, color):
"""
Adds scatter plot of an indiviudals pharamcodynamics to figure.
"""
# Convert durations to strings
durations = [
'Dose duration: ' + str(duration) for duration in durations]
# Add scatter plot of dose events
self._fig.add_trace(
go.Scatter(
x=times,
y=doses,
name="ID: %s" % str(_id),
legendgroup="ID: %s" % str(_id),
showlegend=False,
mode="markers",
text=durations,
hoverinfo='text',
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))),
row=1,
col=1)
def _add_biom_trace(self, _id, times, measurements, color):
"""
Adds scatter plot of an indiviudals pharamcodynamics to figure.
"""
self._fig.add_trace(
go.Scatter(
x=times,
y=measurements,
name="ID: %s" % str(_id),
legendgroup="ID: %s" % str(_id),
showlegend=True,
mode="markers",
marker=dict(
symbol='circle',
color=color,
opacity=0.7,
line=dict(color='black', width=1))),
row=2,
col=1)
def _add_updatemenu(self):
"""
Adds a button to the figure that switches the biomarker scale from
linear to logarithmic.
"""
self._fig.update_layout(
updatemenus=[
dict(
type="buttons",
direction="left",
buttons=list([
dict(
args=[{"yaxis2.type": "linear"}],
label="Linear y-scale",
method="relayout"
),
dict(
args=[{"yaxis2.type": "log"}],
label="Log y-scale",
method="relayout"
)
]),
pad={"r": 0, "t": -10},
showactive=True,
x=0.0,
xanchor="left",
y=1.15,
yanchor="top"
)
]
)
[docs]
def add_data(
self, data, observable=None, id_key='ID', time_key='Time',
obs_key='Observable', value_key='Value', dose_key='Dose',
dose_duration_key='Duration'):
"""
Adds pharmacokinetic time series data of (multiple) individuals to
the figure.
Expects a :class:`pandas.DataFrame` with an ID, a time, an
observable and a value column, and adds a scatter plot of the
measuremed time series to the figure. The dataframe is also expected
to have information about the administered dose via a dose and a
dose duration column. Each individual receives a unique colour.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD data in form of
an ID, time, observable and value column.
observable
The measured bimoarker. This argument is used to determine the
relevant rows in the dataframe. If ``None``, the first observable
in the observable column is selected.
id_key
Key label of the :class:`DataFrame` which specifies the ID column.
The ID refers to the identity of an individual. Defaults to
``'ID'``.
time_key
Key label of the :class:`DataFrame` which specifies the time
column. Defaults to ``'Time'``.
obs_key
Key label of the :class:`DataFrame` which specifies the
observable column. Defaults to ``'Observable'``.
value_key
Key label of the :class:`DataFrame` which specifies the column of
the measured values. Defaults to ``'Value'``.
dose_key
Key label of the :class:`DataFrame` which specifies the dose
column. Defaults to ``'Dose'``.
dose_duration_key
Key label of the :class:`DataFrame` which specifies the dose
duration column. Defaults to ``'Duration'``.
"""
# Check input format
if not isinstance(data, pd.DataFrame):
raise TypeError(
'Data has to be pandas.DataFrame.')
keys = [
id_key, time_key, obs_key, value_key, dose_key, dose_duration_key]
for key in keys:
if key not in data.keys():
raise ValueError(
'Data does not have the key <' + str(key) + '>.')
# Default to first bimoarker, if observable is not specified
biom_types = data[obs_key].dropna().unique()
if observable is None:
observable = biom_types[0]
if observable not in biom_types:
raise ValueError(
'The observable could not be found in the observable column.')
# Get dose information
mask = data[dose_key].notnull()
dose_data = data[mask][[id_key, time_key, dose_key, dose_duration_key]]
# Mask data for observable
mask = data[obs_key] == observable
data = data[mask][[id_key, time_key, value_key]]
# Set axis labels to dataframe keys
self.set_axis_labels(time_key, obs_key, dose_key)
# Get a colour scheme
colors = plotly.colors.qualitative.Plotly
n_colors = len(colors)
# Fill figure with scatter plots of individual data
ids = data[id_key].unique()
for index, _id in enumerate(ids):
# Get doses applied to individual
mask = dose_data[id_key] == _id
dose_times = dose_data[time_key][mask]
doses = dose_data[dose_key][mask]
durations = dose_data[dose_duration_key][mask]
# Get observable measurements
mask = data[id_key] == _id
times = data[time_key][mask]
measurements = data[value_key][mask]
# Get a color for the individual
color = colors[index % n_colors]
# Create scatter plot of dose events
self._add_dose_trace(_id, dose_times, doses, durations, color)
# Create Scatter plot
self._add_biom_trace(_id, times, measurements, color)
[docs]
def add_simulation(
self, data, time_key='Time', value_key='Value',
dose_key='Dose'):
"""
Adds a pharmacokinetic time series simulation to the figure.
Expects a :class:`pandas.DataFrame` with a time, a value,
and a dose column. A line plot of the biomarker time series, as well
as the dosing regimen is added to the figure.
Parameters
----------
data
A :class:`pandas.DataFrame` with the time series PD simulation in
form of a time and a value column.
time_key
Key label of the :class:`DataFrame` which specifies the time
column. Defaults to ``'Time'``.
value_key
Key label of the :class:`DataFrame` which specifies the simulated
values column. Defaults to ``'Value'``.
"""
raise NotImplementedError
[docs]
def set_axis_labels(self, time_label, biom_label, dose_label):
"""
Sets the label of the time axis, the biomarker axis, and the dose axis.
"""
self._fig.update_xaxes(title=time_label, row=2)
self._fig.update_yaxes(title=dose_label, row=1)
self._fig.update_yaxes(title=biom_label, row=2)