@@ -28,29 +28,31 @@ class PiecewiseSystemDiscovery(object):
2828 def __init__ (
2929 self ,
3030 timecourse : Timecourse ,
31- num_change_point : int = 2 ,
32- min_normalized_reduction : float = 0.1 ,
31+ max_change_point : int = 2 ,
32+ min_fractional_reduction : float = 0.1 ,
3333 min_segment_length : int = 100 ,
3434 change_point_threshold : float = 0.1 ,
3535 predict_kernel_bandwidth : float = 0.5 ,
36- ** kwargs : Any ,
36+ ** sd_kwargs : Any ,
3737 ) -> None :
3838 """_summary_
3939
4040 Args:
4141 timecourse (Timecourse): _description_
42- num_change_point (int, optional): _description_. Defaults to 2.
43- min_normalized_reduction (float, optional): _description_. Defaults to 0.1.
42+ max_change_point (int, optional): _description_. Defaults to 2.
43+ min_fractional_reduction (float, optional): _description_. Defaults to 0.1.
4444 min_segment_length (int, optional): _description_. Defaults to 100.
4545 change_point_threshold (float, optional): _description_. Defaults to 0.1.
46+ predict_kernel_bandwidth (float, optional): _description_. Defaults to 0.5.
47+ **kwargs: Arguments for SystemDiscovery constructor (e.g. fit_kernel_bandwidth, model_name).
4648 """
4749 self .timecourse = timecourse
48- self .num_change_point = num_change_point
49- self .min_normalized_reduction = min_normalized_reduction
50+ self .max_change_point = max_change_point
51+ self .min_fractional_reduction = min_fractional_reduction
5052 self .min_segment_length = min_segment_length
5153 self .change_point_threshold = change_point_threshold
5254 self .predict_kernel_bandwidth = predict_kernel_bandwidth
53- self ._kwargs = kwargs
55+ self ._sd_kwargs = sd_kwargs
5456
5557 self ._segment_models : List [SystemDiscovery ] = []
5658 self ._segment_boundaries : List [Tuple [float , float ]] = []
@@ -126,34 +128,21 @@ def _detectChangePoints(self, signal_arr: np.ndarray, num_point: int) -> List[in
126128 Returns a sorted (by time) list of accepted interior split indices.
127129 """
128130 detector = ChangePointDetector (signal_arr )
129- detector .fit (max_change_point = self .num_change_point ,
130- min_fractional_reduction = 0.0 ,
131- min_segment_length = 2 )
132- if len (detector .subsequences ) == 0 :
133- split_indices = []
134- else :
135- split_indices = [info .splice_start for info in detector .subsequences ]
136- split_indices .remove (0 ) # Remove the first index if it's 0, as we don't want to split at the very beginning
131+ # self.min_fraction_reduction gates the SSE reduction a split must achieve
132+ # (as a fraction of the total adjusted sum of squares), not the raw
133+ # signal value at the split point: the split index itself often falls
134+ # on a low-signal point right after a spike, since the detector
135+ # chooses the boundary that best separates two segments, not the
136+ # point with the largest signal value.
137+ detector .fit (max_change_point = self .max_change_point ,
138+ min_fractional_reduction = self .min_fractional_reduction ,
139+ min_segment_length = self .min_segment_length )
140+
141+ # Map signal index k to timecourse split index k + 1
142+ split_indices = [info .splice_start + 1 for info in detector .subsequences
143+ if info .splice_start > 0 ]
144+
137145 return sorted (split_indices )
138- # candidate_index_arr = np.arange(1, num_point)
139- # order_arr = np.argsort(-signal_arr, kind="stable")
140- # accepted: List[int] = []
141- # for rank in order_arr:
142- # signal_value = signal_arr[rank]
143- # if signal_value < self.change_point_threshold:
144- # break
145- # split_idx = int(candidate_index_arr[rank])
146- # pos = bisect.bisect_left(accepted, split_idx)
147- # left_bound = accepted[pos - 1] if pos > 0 else 0
148- # right_bound = accepted[pos] if pos < len(accepted) else num_point
149- # if (split_idx - left_bound) < self.min_segment_length:
150- # continue
151- # if (right_bound - split_idx) < self.min_segment_length:
152- # continue
153- # accepted.insert(pos, split_idx)
154- # if len(accepted) == self.num_change_point:
155- # break
156- # return accepted
157146
158147 def fit (self ) -> "PiecewiseSystemDiscovery" :
159148 """fit() steps 1-4: detect change points, fit per-segment models."""
@@ -174,7 +163,7 @@ def fit(self) -> "PiecewiseSystemDiscovery":
174163 end_time = time_arr [hi ] if hi < num_point else time_arr [- 1 ]
175164 self ._segment_boundaries .append ((float (time_arr [lo ]), float (end_time )))
176165 self ._segment_lengths .append (hi - lo )
177- model = SystemDiscovery (segment_df , ** self ._kwargs ).fit ()
166+ model = SystemDiscovery (segment_df , ** self ._sd_kwargs ).fit ()
178167 self ._segment_models .append (model )
179168
180169 self ._is_fitted = True
@@ -201,7 +190,7 @@ def dynamicFit(self) -> "PiecewiseSystemDiscovery":
201190 jacobian_collection_arr = self .timecourse .jacobian_collection_arr
202191 num_point = raw_df .shape [0 ]
203192 time_arr = raw_df .index .to_numpy (dtype = float )
204- num_seg = self .num_change_point + 1
193+ num_seg = self .max_change_point + 1
205194 mseg = self .min_segment_length
206195
207196 signal_arr = np .nan_to_num (
@@ -264,7 +253,7 @@ def dynamicFit(self) -> "PiecewiseSystemDiscovery":
264253 end_time = time_arr [hi ] if hi < num_point else time_arr [- 1 ]
265254 self ._segment_boundaries .append ((float (time_arr [lo ]), float (end_time )))
266255 self ._segment_lengths .append (hi - lo )
267- model = SystemDiscovery (segment_df , ** self ._kwargs ).fit ()
256+ model = SystemDiscovery (segment_df , ** self ._sd_kwargs ).fit ()
268257 self ._segment_models .append (model )
269258
270259 self ._is_fitted = True
@@ -367,7 +356,7 @@ def plotPiecewise(self, num_true_point: int = 20, **kwargs: Any) -> PlotOptions:
367356 species_names = self ._segment_models [0 ].species_names
368357 num_skip = max (1 , len (time_arr ) // num_true_point )
369358
370- baseline = SystemDiscovery (timecourse_df , ** self ._kwargs ).fit ()
359+ baseline = SystemDiscovery (timecourse_df , ** self ._sd_kwargs ).fit ()
371360 try :
372361 baseline_pred_df = baseline .predict ()
373362 except Exception :
@@ -405,7 +394,7 @@ def _draw(po: PlotOptions, pred_df: pd.DataFrame | None, title: str,
405394 po .apply ()
406395
407396 _draw (PlotOptions (fig = fig , ax = ax_top , ** kwargs ), baseline_pred_df , "0 change points" )
408- _draw (plot_options , psd_pred_df , f"{ self .num_change_point } change point(s)" ,
397+ _draw (plot_options , psd_pred_df , f"{ self .max_change_point } change point(s)" ,
409398 vlines = change_point_times )
410399 fig .suptitle ("Actual vs Predicted" , fontsize = 13 , fontweight = "bold" )
411400 fig .tight_layout ()
0 commit comments