Skip to content

Commit 8a2ebbb

Browse files
Updates to MultipleLinearPredictor
1 parent fe8d18c commit 8a2ebbb

5 files changed

Lines changed: 514 additions & 58 deletions

File tree

notebooks/score_analysis.ipynb

Lines changed: 55 additions & 54 deletions
Large diffs are not rendered by default.

src/linear_predictor.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,19 @@ def makeFromBiomodels(cls,
103103

104104
def _getJacobian(self) -> np.ndarray:
105105
"""Return the Jacobian matrix selected by jacobian_selection."""
106+
if self.jacobian_selection == cn.JAC_FITTED:
107+
if self.trajectory.model.num_species > self.trajectory.num_point:
108+
print(
109+
f"Cannot use JAC_FITTED: num_species "
110+
f"({self.trajectory.model.num_species}) > num_point "
111+
f"({self.trajectory.num_point}).")
112+
return self.trajectory.jacobian_median_arr
113+
else:
114+
return self._fitJacobian()
106115
if self.jacobian_selection == cn.JAC_MEDIAN:
107116
return self.trajectory.jacobian_median_arr
108117
if self.jacobian_selection == cn.JAC_FIRST:
109118
return self.trajectory.jacobian_collection_arr[0]
110-
if self.jacobian_selection == cn.JAC_FITTED:
111-
return self._fitJacobian()
112119
raise ValueError(
113120
f"Unknown jacobian_selection: {self.jacobian_selection!r}. "
114121
f"Must be {cn.JAC_MEDIAN!r}, {cn.JAC_FIRST!r}, or "

src/multiple_linear_predictor.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Piece-wise linear prediction using a TrajectoryCollection."""
2+
3+
import src.constants as cn # type: ignore
4+
from src.linear_predictor import LinearPredictor # type: ignore
5+
from src.plot_options import PlotOptions # type: ignore
6+
from src.score import Score # type: ignore
7+
from src.trajectory_collection import TrajectoryCollection # type: ignore
8+
9+
import numpy as np # type: ignore
10+
import pandas as pd # type: ignore
11+
12+
13+
class MultipleLinearPredictor(object):
14+
"""Piece-wise linear predictor across a TrajectoryCollection.
15+
16+
Each Trajectory is predicted independently with LinearPredictor; results
17+
are concatenated into a single timecourse. Boundary timepoints shared
18+
between adjacent segments appear once in the output (the duplicate first
19+
row of each interior segment is dropped).
20+
"""
21+
22+
def __init__(self,
23+
trajectory_collection: TrajectoryCollection,
24+
jacobian_selection: str = cn.JAC_FITTED,
25+
num_step: int = -1) -> None:
26+
"""
27+
Parameters
28+
----------
29+
trajectory_collection : TrajectoryCollection
30+
jacobian_selection : str
31+
Passed to each LinearPredictor.
32+
num_step : int
33+
Number of steps ahead for windowed prediction.
34+
"""
35+
self.trajectory_collection = trajectory_collection
36+
self.jacobian_selection = jacobian_selection
37+
self.num_step = num_step
38+
39+
def _makeTimecourse(self) -> pd.DataFrame:
40+
"""Concatenate actual timecourses, dropping duplicate boundary rows.
41+
42+
Returns
43+
-------
44+
pd.DataFrame
45+
Time-indexed DataFrame with one row per unique timepoint.
46+
"""
47+
dfs = []
48+
for i, traj in enumerate(self.trajectory_collection.trajectories):
49+
tc_df = traj.timecourse_df
50+
if i > 0:
51+
tc_df = tc_df.iloc[1:]
52+
dfs.append(tc_df)
53+
return pd.concat(dfs)
54+
55+
def predict(self) -> pd.DataFrame:
56+
"""Predict concentrations across all segments.
57+
58+
Each segment is predicted by an independent LinearPredictor. Duplicate
59+
boundary rows are dropped to produce a single time-indexed DataFrame.
60+
61+
Returns
62+
-------
63+
pd.DataFrame
64+
Time-indexed DataFrame with columns matching the model's species names.
65+
"""
66+
dfs = []
67+
for i, traj in enumerate(self.trajectory_collection.trajectories):
68+
lp = LinearPredictor(traj,
69+
jacobian_selection=self.jacobian_selection,
70+
num_step=self.num_step)
71+
pred_df = lp.predict()
72+
if i > 0:
73+
pred_df = pred_df.iloc[1:]
74+
dfs.append(pred_df)
75+
return pd.concat(dfs)
76+
77+
def score(self, description: str = "") -> pd.DataFrame:
78+
"""Score the piece-wise prediction against the actual timecourse.
79+
80+
Parameters
81+
----------
82+
description : str
83+
Label stored in the 'description' column of the returned DataFrame.
84+
85+
Returns
86+
-------
87+
pd.DataFrame
88+
One row per aggregation level (model + one per species).
89+
"""
90+
prediction_df = self.predict()
91+
actual_df = self.trajectory_collection.makeTimecourse()
92+
scorer = Score()
93+
score_infos = scorer.makeScoreInfo(description, actual_df, prediction_df)
94+
return pd.DataFrame([info.__dict__ for info in score_infos])
95+
96+
def plotPrediction(self, **kwargs) -> PlotOptions:
97+
"""Plot actual and predicted timecourses with segment boundary lines.
98+
99+
Actual values are solid lines; predictions are dashed. Vertical dashed
100+
lines mark boundaries between adjacent segments.
101+
102+
Parameters
103+
----------
104+
**kwargs
105+
Passed to PlotOptions.
106+
107+
Returns
108+
-------
109+
PlotOptions
110+
"""
111+
prediction_df = self.predict()
112+
actual_df = self.trajectory_collection.makeTimecourse()
113+
if "title" not in kwargs:
114+
scorer = Score()
115+
score_infos = scorer.makeScoreInfo("", actual_df, prediction_df)
116+
p95 = score_infos[0].p95
117+
model_name = self.trajectory_collection.model.model_name
118+
n_seg = len(self.trajectory_collection.trajectories)
119+
kwargs["title"] = f"{model_name} n_seg={n_seg}, p95={p95:.2f}"
120+
plot_options = PlotOptions(**kwargs)
121+
ax = plot_options.ax
122+
for i, name in enumerate(self.trajectory_collection.model.species_names):
123+
color = f"C{i}"
124+
ax.plot(actual_df.index, actual_df[name], # type: ignore
125+
color=color, label=f"{name} (actual)")
126+
ax.plot(prediction_df.index, prediction_df[name], # type: ignore
127+
color=color, linestyle="--", label=f"{name} (predicted)")
128+
for traj in self.trajectory_collection.trajectories[:-1]:
129+
ax.axvline(x=traj.end_time, color="black", # type: ignore
130+
linestyle="--", linewidth=0.8)
131+
plot_options.apply()
132+
return plot_options
133+
134+
@property
135+
def cost(self) -> float:
136+
"""Mean squared relative prediction error, aggregated across species.
137+
138+
Computed on the concatenated timecourse with the first row dropped.
139+
Returns the median of per-species mean squared relative errors.
140+
141+
Returns
142+
-------
143+
float
144+
"""
145+
actual_arr = self.trajectory_collection.makeTimecourse().values[1:]
146+
predicted_arr = self.predict().values[1:]
147+
with np.errstate(divide='ignore', invalid='ignore'):
148+
rel_arr = np.where(
149+
actual_arr == 0,
150+
np.nan,
151+
(predicted_arr - actual_arr) / actual_arr,
152+
)
153+
species_costs = np.nanmean(rel_arr ** 2, axis=0)
154+
return float(np.nanmedian(species_costs))

0 commit comments

Comments
 (0)