@@ -163,27 +163,50 @@ class LinesAverage(PlotAverage):
163163 _lines : List [Tuple [List [float ], List [float ]]]
164164 _x_step_size : float
165165
166+ x_values : ndarray # X values of the mean
167+ mean_values : ndarray # Y values of the mean. You can plot the mean like plt.plot(x_values, mean_values)
168+ standard_deviation_values : ndarray # Standard deviation in the mean. Useful for plt.fill_between.
169+ counts_in_standard_deviation_values : ndarray # Counts used for calculating the standard deviation
170+
166171 def __init__ (self , * lines : Tuple [List [float ], List [float ]], x_step_size : float = 1 ):
167172 """Creates the moving average. Each line is ([x1, x2, ...], [y1, y2, ...]), with the x in order from low to high."""
168173 self ._lines = list (lines )
169174 self ._x_step_size = x_step_size
170175 if x_step_size <= 0 :
171176 raise ValueError (f"Illegal step size: { x_step_size } " )
172177
173- def _get_min_max_x (self , min_x : Optional [float ] = None , max_x : Optional [float ] = None ) -> Tuple [float , float ]:
178+ # Calculate error bounds
179+ min_x , max_x = self ._get_min_max_x ()
180+
181+ x_moving_average = list ()
182+ y_moving_average = list ()
183+ y_moving_average_standard_deviation = list ()
184+ y_moving_average_counts = list ()
185+ for x in numpy .arange (min_x + 0.01 , max_x - 0.01 , self ._x_step_size ):
186+ y_values = self ._get_y_values_at (x )
187+ if len (y_values ) <= 1 :
188+ continue
189+
190+ y_mean = numpy .nanmean (y_values )
191+ y_std = numpy .nanstd (y_values , ddof = 1 )
192+
193+ x_moving_average .append (x )
194+ y_moving_average .append (y_mean )
195+ y_moving_average_standard_deviation .append (y_std )
196+ y_moving_average_counts .append (len (y_values ))
197+ self .x_values = numpy .array (x_moving_average , dtype = numpy .float32 )
198+ self .mean_values = numpy .array (y_moving_average , dtype = numpy .float32 )
199+ self .standard_deviation_values = numpy .array (y_moving_average_standard_deviation , dtype = numpy .float32 )
200+ self .counts_in_standard_deviation_values = numpy .array (y_moving_average_counts , dtype = numpy .uint16 )
201+
202+ def _get_min_max_x (self ) -> Tuple [float , float ]:
174203 """Gets the lowest and highest x values used in the lines. Returns (0, 1) if no lines are available."""
175204 found_min_x = None
176205 found_max_x = None
177206 for line_x , _ in self ._lines :
178207 found_min_x = min_none (line_x [0 ], found_min_x )
179208 found_max_x = max_none (line_x [- 1 ], found_max_x )
180209
181- # Let min_x and max_x override the found values
182- if min_x is not None and found_min_x is not None and min_x > found_min_x :
183- found_min_x = min_x
184- if max_x is not None and found_max_x is not None and max_x < found_max_x :
185- found_max_x = max_x
186-
187210 if found_min_x is None :
188211 return 0 , 1
189212 if found_max_x == found_min_x :
@@ -222,32 +245,26 @@ def count_values_at_min_max_x(self) -> Tuple[int, int]:
222245
223246 def plot (self , axes : Axes , * , color : Color = Color (0 , 0 , 255 ), linewidth = 2 , error_opacity = 0.8 ,
224247 standard_error : bool = False , label = "Average" , min_x : Optional [float ] = None , max_x : Optional [float ] = None ):
225- min_x , max_x = self ._get_min_max_x (min_x , max_x )
226-
227- # Calculate error bounds
228- x_error_values = list ()
229- y_error_values_min = list ()
230- y_error_values_mean = list ()
231- y_error_values_max = list ()
232- for x in numpy .arange (min_x + 0.01 , max_x - 0.01 , self ._x_step_size ):
233- y_values = self ._get_y_values_at (x )
234- if len (y_values ) <= 1 :
235- continue
236-
237- y_mean = numpy .nanmean (y_values )
238- y_error = numpy .nanstd (y_values , ddof = 1 )
239- if standard_error :
240- y_error /= numpy .sqrt (len (y_values ))
241-
242- x_error_values .append (x )
243- y_error_values_min .append (y_mean - y_error )
244- y_error_values_mean .append (y_mean )
245- y_error_values_max .append (y_mean + y_error )
248+ # Find which x values to plot
249+ if min_x is None :
250+ min_x = self .x_values [0 ]
251+ if max_x is None :
252+ max_x = self .x_values [- 1 ]
253+ x_mask = (self .x_values >= min_x ) & (self .x_values <= max_x )
254+
255+ # Calculate means and error bars
256+ x_values = self .x_values [x_mask ]
257+ mean_values = self .mean_values [x_mask ]
258+ y_error_values = self .standard_deviation_values [x_mask ]
259+ if standard_error :
260+ y_error_values /= numpy .sqrt (self .counts_in_standard_deviation_values [x_mask ])
261+ y_error_values_min = mean_values - y_error_values
262+ y_error_values_max = mean_values + y_error_values
246263
247264 # Plot
248- if len (x_error_values ) > 0 :
249- axes .plot (x_error_values , y_error_values_mean , color = color .to_rgb_floats (), linewidth = linewidth , label = label )
250- axes .fill_between (x_error_values , y_error_values_min ,
265+ if len (x_values ) > 0 :
266+ axes .plot (x_values , mean_values , color = color .to_rgb_floats (), linewidth = linewidth , label = label )
267+ axes .fill_between (x_values , y_error_values_min ,
251268 y_error_values_max , color = color .to_rgb_floats (), alpha = 1 - error_opacity , linewidth = 0 )
252269
253270 def get_x_positions_and_means (self ) -> Tuple [ndarray , ndarray ]:
0 commit comments