Belle II Software  release-08-01-10
classification.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 
4 
11 
12 import numpy as np
13 import collections
14 import numbers
15 import copy
16 
17 from tracking.validation import scores, statistics
18 
19 from tracking.validation.plot import ValidationPlot, compose_axis_label
20 from tracking.validation.fom import ValidationFiguresOfMerit
21 from tracking.validation.tolerate_missing_key_formatter import TolerateMissingKeyFormatter
22 
23 formatter = TolerateMissingKeyFormatter()
24 
25 
26 class ClassificationAnalysis(object):
27  """Perform truth-classification analysis"""
28 
29  def __init__(
30  self,
31  contact,
32  quantity_name,
33  cut_direction=None,
34  cut=None,
35  lower_bound=None,
36  upper_bound=None,
37  outlier_z_score=None,
38  allow_discrete=None,
39  unit=None
40  ):
41  """Compare an estimated quantity to the truths by generating standardized validation plots."""
42 
43 
44  self._contact_contact = contact
45 
46  self.quantity_namequantity_name = quantity_name
47 
48 
49  self.plotsplots = collections.OrderedDict()
50 
51  self.fomfom = None
52 
53 
54  self.cut_directioncut_direction = cut_direction
55 
56  self.cutcut = cut
57 
58 
59  self.lower_boundlower_bound = lower_bound
60 
61  self.upper_boundupper_bound = upper_bound
62 
63  self.outlier_z_scoreoutlier_z_score = outlier_z_score
64 
65  self.allow_discreteallow_discrete = allow_discrete
66 
67  self.unitunit = unit
68 
69  def analyse(
70  self,
71  estimates,
72  truths,
73  auxiliaries={}
74  ):
75  """Compares the concrete estimate to the truth and efficiency, purity and background rejection
76  as figure of merit and plots the selection as a stacked plot over the truths.
77 
78  Parameters
79  ----------
80  estimates : array_like
81  Selection variable to compare to the truths
82  truths : array_like
83  Binary true class values.
84  """
85 
86  quantity_name = self.quantity_namequantity_name
87  axis_label = compose_axis_label(quantity_name, self.unitunit)
88 
89  plot_name = "{quantity_name}_{subplot_name}"
90  plot_name = formatter.format(plot_name, quantity_name=quantity_name)
91 
92  signals = truths != 0
93 
94  # Some different things become presentable depending on the estimates
95  estimate_is_binary = statistics.is_binary_series(estimates)
96 
97  if estimate_is_binary:
98  binary_estimates = estimates != 0
99  cut_value = 0.5
100  cut_direction = -1 # reject low values
101 
102  elif self.cutcut is not None:
103  if isinstance(self.cutcut, numbers.Number):
104  cut_value = self.cutcut
105  cut_direction = self.cut_directioncut_direction
106  cut_classifier = CutClassifier(cut_direction=cut_direction, cut_value=cut_value)
107 
108  else:
109  cut_classifier = self.cutcut
110  cut_classifier = cut_classifier.clone()
111 
112  cut_classifier.fit(estimates, truths)
113  binary_estimates = cut_classifier.predict(estimates) != 0
114  cut_direction = cut_classifier.cut_direction
115  cut_value = cut_classifier.cut_value
116 
117  if not isinstance(self.cutcut, numbers.Number):
118  print(formatter.format(plot_name, subplot_name="cut_classifier"), "summary")
119  cut_classifier.describe(estimates, truths)
120 
121  else:
122  cut_value = None
123  cut_direction = self.cut_directioncut_direction
124 
125  lower_bound = self.lower_boundlower_bound
126  upper_bound = self.upper_boundupper_bound
127 
128  # Stacked histogram
129  signal_bkg_histogram_name = formatter.format(plot_name, subplot_name="signal_bkg_histogram")
130  signal_bkg_histogram = ValidationPlot(signal_bkg_histogram_name)
131  signal_bkg_histogram.hist(
132  estimates,
133  stackby=truths,
134  lower_bound=lower_bound,
135  upper_bound=upper_bound,
136  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
137  allow_discrete=self.allow_discreteallow_discrete,
138  )
139  signal_bkg_histogram.xlabel = axis_label
140 
141  if lower_bound is None:
142  lower_bound = signal_bkg_histogram.lower_bound
143 
144  if upper_bound is None:
145  upper_bound = signal_bkg_histogram.upper_bound
146 
147  self.plotsplots['signal_bkg'] = signal_bkg_histogram
148 
149  # Purity profile
150  purity_profile_name = formatter.format(plot_name, subplot_name="purity_profile")
151 
152  purity_profile = ValidationPlot(purity_profile_name)
153  purity_profile.profile(
154  estimates,
155  truths,
156  lower_bound=lower_bound,
157  upper_bound=upper_bound,
158  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
159  allow_discrete=self.allow_discreteallow_discrete,
160  )
161 
162  purity_profile.xlabel = axis_label
163  purity_profile.ylabel = 'purity'
164  self.plotsplots["purity"] = purity_profile
165 
166  # Try to guess the cur direction form the correlation
167  if cut_direction is None:
168  purity_grapherrors = ValidationPlot.convert_tprofile_to_tgrapherrors(purity_profile.plot)
169  correlation = purity_grapherrors.GetCorrelationFactor()
170  if correlation > 0.1:
171  print("Determined cut direction", -1)
172  cut_direction = -1 # reject low values
173  elif correlation < -0.1:
174  print("Determined cut direction", 1)
175  cut_direction = +1 # reject high values
176 
177  # Figures of merit
178  if cut_value is not None:
179  fom_name = formatter.format(plot_name, subplot_name="classification_figures_of_merits")
180  fom_description = "Efficiency, purity and background rejection of the classifiction with {quantity_name}".format(
181  quantity_name=quantity_name
182  )
183 
184  fom_check = "Check that the classifcation quality stays stable."
185 
186  fom_title = "Summary of the classification quality with {quantity_name}".format(
187  quantity_name=quantity_name
188  )
189 
190  classification_fom = ValidationFiguresOfMerit(
191  name=fom_name,
192  title=fom_title,
193  description=fom_description,
194  check=fom_check,
195  contact=self.contactcontactcontactcontact,
196  )
197 
198  efficiency = scores.efficiency(truths, binary_estimates)
199  purity = scores.purity(truths, binary_estimates)
200  background_rejection = scores.background_rejection(truths, binary_estimates)
201 
202  classification_fom['cut_value'] = cut_value
203  classification_fom['cut_direction'] = cut_direction
204  classification_fom['efficiency'] = efficiency
205  classification_fom['purity'] = purity
206  classification_fom['background_rejection'] = background_rejection
207 
208  self.fomfom = classification_fom
209  # Auxiliary hists
210  for aux_name, aux_values in auxiliaries.items():
211  if statistics.is_single_value_series(aux_values) or aux_name == quantity_name:
212  continue
213 
214  aux_axis_label = compose_axis_label(aux_name)
215 
216  # Signal + bkg distribution over estimate and auxiliary variable #
217  # ############################################################## #
218  signal_bkg_aux_hist2d_name = formatter.format(plot_name, subplot_name=aux_name + '_signal_bkg_aux2d')
219  signal_bkg_aux_hist2d = ValidationPlot(signal_bkg_aux_hist2d_name)
220  signal_bkg_aux_hist2d.hist2d(
221  aux_values,
222  estimates,
223  stackby=truths,
224  lower_bound=(None, lower_bound),
225  upper_bound=(None, upper_bound),
226  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
227  allow_discrete=self.allow_discreteallow_discrete,
228  )
229 
230  aux_lower_bound = signal_bkg_aux_hist2d.lower_bound[0]
231  aux_upper_bound = signal_bkg_aux_hist2d.upper_bound[0]
232 
233  signal_bkg_aux_hist2d.xlabel = aux_axis_label
234  signal_bkg_aux_hist2d.ylabel = axis_label
235  self.plotsplots[signal_bkg_aux_hist2d_name] = signal_bkg_aux_hist2d
236 
237  # Figures of merit as function of the auxiliary variables
238  if cut_value is not None:
239 
240  # Auxiliary purity profile #
241  # ######################## #
242  aux_purity_profile_name = formatter.format(plot_name, subplot_name=aux_name + "_aux_purity_profile")
243  aux_purity_profile = ValidationPlot(aux_purity_profile_name)
244  aux_purity_profile.profile(
245  aux_values[binary_estimates],
246  truths[binary_estimates],
247  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
248  allow_discrete=self.allow_discreteallow_discrete,
249  lower_bound=aux_lower_bound,
250  upper_bound=aux_upper_bound,
251  )
252 
253  aux_purity_profile.xlabel = aux_axis_label
254  aux_purity_profile.ylabel = 'purity'
255  self.plotsplots[aux_purity_profile_name] = aux_purity_profile
256 
257  # Auxiliary efficiency profile #
258  # ############################ #
259  aux_efficiency_profile_name = formatter.format(plot_name, subplot_name=aux_name + "_aux_efficiency_profile")
260  aux_efficiency_profile = ValidationPlot(aux_efficiency_profile_name)
261  aux_efficiency_profile.profile(
262  aux_values[signals],
263  binary_estimates[signals],
264  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
265  allow_discrete=self.allow_discreteallow_discrete,
266  lower_bound=aux_lower_bound,
267  upper_bound=aux_upper_bound,
268  )
269 
270  aux_efficiency_profile.xlabel = aux_axis_label
271  aux_efficiency_profile.ylabel = 'efficiency'
272  self.plotsplots[aux_efficiency_profile_name] = aux_efficiency_profile
273 
274  # Auxiliary bkg rejection profile #
275  # ############################### #
276  aux_bkg_rejection_profile_name = formatter.format(plot_name, subplot_name=aux_name + "_aux_bkg_rejection_profile")
277  aux_bkg_rejection_profile = ValidationPlot(aux_bkg_rejection_profile_name)
278  aux_bkg_rejection_profile.profile(
279  aux_values[~signals],
280  ~binary_estimates[~signals],
281  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
282  allow_discrete=self.allow_discreteallow_discrete,
283  lower_bound=aux_lower_bound,
284  upper_bound=aux_upper_bound,
285  )
286 
287  aux_bkg_rejection_profile.xlabel = aux_axis_label
288  aux_bkg_rejection_profile.ylabel = 'bkg rejection'
289  self.plotsplots[aux_bkg_rejection_profile_name] = aux_bkg_rejection_profile
290 
291  cut_abs = False
292  if cut_direction is None:
293  purity_grapherrors = ValidationPlot.convert_tprofile_to_tgrapherrors(purity_profile.plot,
294  abs_x=True)
295  correlation = purity_grapherrors.GetCorrelationFactor()
296  if correlation > 0.1:
297  print("Determined absolute cut direction", -1)
298  cut_direction = -1 # reject low values
299  cut_abs = True
300  elif correlation < -0.1:
301  print("Determined absolute cut direction", 1)
302  cut_direction = +1 # reject high values
303  cut_abs = True
304 
305  if cut_abs:
306  estimates = np.abs(estimates)
307  cut_x_label = "cut " + compose_axis_label("abs(" + quantity_name + ")", self.unitunit)
308  lower_bound = 0
309  else:
310  cut_x_label = "cut " + axis_label
311 
312  # Quantile plots
313  if not estimate_is_binary and cut_direction is not None:
314  # Signal estimate quantiles over auxiliary variable #
315  # ################################################# #
316  if cut_direction > 0:
317  quantiles = [0.5, 0.90, 0.99]
318  else:
319  quantiles = [0.01, 0.10, 0.5]
320 
321  for aux_name, aux_values in auxiliaries.items():
322  if statistics.is_single_value_series(aux_values) or aux_name == quantity_name:
323  continue
324 
325  aux_axis_label = compose_axis_label(aux_name)
326 
327  signal_quantile_aux_profile_name = formatter.format(plot_name, subplot_name=aux_name + '_signal_quantiles_aux2d')
328  signal_quantile_aux_profile = ValidationPlot(signal_quantile_aux_profile_name)
329  signal_quantile_aux_profile.hist2d(
330  aux_values[signals],
331  estimates[signals],
332  quantiles=quantiles,
333  bins=('flat', None),
334  lower_bound=(None, lower_bound),
335  upper_bound=(None, upper_bound),
336  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
337  allow_discrete=self.allow_discreteallow_discrete,
338  )
339  signal_quantile_aux_profile.xlabel = aux_axis_label
340  signal_quantile_aux_profile.ylabel = cut_x_label
341  self.plotsplots[signal_quantile_aux_profile_name] = signal_quantile_aux_profile
342 
343  # ROC plots
344  if not estimate_is_binary and cut_direction is not None:
345  n_data = len(estimates)
346  n_signals = scores.signal_amount(truths, estimates)
347  n_bkgs = n_data - n_signals
348 
349  # work around for numpy sorting nan values as high but we want it as low depending on the cut direction
350  if cut_direction < 0: # reject low
351  sorting_indices = np.argsort(-estimates)
352  else:
353  sorting_indices = np.argsort(estimates)
354 
355  sorted_truths = truths[sorting_indices]
356  sorted_estimates = estimates[sorting_indices]
357 
358  sorted_n_accepted_signals = np.cumsum(sorted_truths, dtype=float)
359  sorted_efficiencies = sorted_n_accepted_signals / n_signals
360 
361  sorted_n_rejected_signals = n_signals - sorted_n_accepted_signals
362  sorted_n_rejects = np.arange(len(estimates) + 1, 1, -1)
363  sorted_n_rejected_bkgs = sorted_n_rejects - sorted_n_rejected_signals
364  sorted_bkg_rejections = sorted_n_rejected_bkgs / n_bkgs
365 
366  # Efficiency by cut value #
367  # ####################### #
368  efficiency_by_cut_profile_name = formatter.format(plot_name, subplot_name="efficiency_by_cut")
369 
370  efficiency_by_cut_profile = ValidationPlot(efficiency_by_cut_profile_name)
371  efficiency_by_cut_profile.profile(
372  sorted_estimates,
373  sorted_efficiencies,
374  lower_bound=lower_bound,
375  upper_bound=upper_bound,
376  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
377  allow_discrete=self.allow_discreteallow_discrete,
378  )
379 
380  efficiency_by_cut_profile.xlabel = cut_x_label
381  efficiency_by_cut_profile.ylabel = "efficiency"
382 
383  self.plotsplots["efficiency_by_cut"] = efficiency_by_cut_profile
384 
385  # Background rejection over cut value #
386  # ################################### #
387  bkg_rejection_by_cut_profile_name = formatter.format(plot_name, subplot_name="bkg_rejection_by_cut")
388  bkg_rejection_by_cut_profile = ValidationPlot(bkg_rejection_by_cut_profile_name)
389  bkg_rejection_by_cut_profile.profile(
390  sorted_estimates,
391  sorted_bkg_rejections,
392  lower_bound=lower_bound,
393  upper_bound=upper_bound,
394  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
395  allow_discrete=self.allow_discreteallow_discrete,
396  )
397 
398  bkg_rejection_by_cut_profile.xlabel = cut_x_label
399  bkg_rejection_by_cut_profile.ylabel = "background rejection"
400 
401  self.plotsplots["bkg_rejection_by_cut"] = bkg_rejection_by_cut_profile
402 
403  # Purity over efficiency #
404  # ###################### #
405  purity_over_efficiency_profile_name = formatter.format(plot_name, subplot_name="purity_over_efficiency")
406  purity_over_efficiency_profile = ValidationPlot(purity_over_efficiency_profile_name)
407  purity_over_efficiency_profile.profile(
408  sorted_efficiencies,
409  sorted_truths,
410  cumulation_direction=1,
411  lower_bound=0,
412  upper_bound=1
413  )
414  purity_over_efficiency_profile.xlabel = 'efficiency'
415  purity_over_efficiency_profile.ylabel = 'purity'
416 
417  self.plotsplots["purity_over_efficiency"] = purity_over_efficiency_profile
418 
419  # Cut over efficiency #
420  # ################### #
421  cut_over_efficiency_profile_name = formatter.format(plot_name, subplot_name="cut_over_efficiency")
422  cut_over_efficiency_profile = ValidationPlot(cut_over_efficiency_profile_name)
423  cut_over_efficiency_profile.profile(
424  sorted_efficiencies,
425  sorted_estimates,
426  lower_bound=0,
427  upper_bound=1,
428  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
429  allow_discrete=self.allow_discreteallow_discrete,
430  )
431  cut_over_efficiency_profile.set_minimum(lower_bound)
432  cut_over_efficiency_profile.set_maximum(upper_bound)
433  cut_over_efficiency_profile.xlabel = 'efficiency'
434  cut_over_efficiency_profile.ylabel = cut_x_label
435 
436  self.plotsplots["cut_over_efficiency"] = cut_over_efficiency_profile
437 
438  # Cut over bkg_rejection #
439  # ###################### #
440  cut_over_bkg_rejection_profile_name = formatter.format(plot_name, subplot_name="cut_over_bkg_rejection")
441  cut_over_bkg_rejection_profile = ValidationPlot(cut_over_bkg_rejection_profile_name)
442  cut_over_bkg_rejection_profile.profile(
443  sorted_bkg_rejections,
444  sorted_estimates,
445  lower_bound=0,
446  upper_bound=1,
447  outlier_z_score=self.outlier_z_scoreoutlier_z_score,
448  allow_discrete=self.allow_discreteallow_discrete,
449  )
450  cut_over_bkg_rejection_profile.set_minimum(lower_bound)
451  cut_over_bkg_rejection_profile.set_maximum(upper_bound)
452  cut_over_bkg_rejection_profile.xlabel = 'bkg_rejection'
453  cut_over_bkg_rejection_profile.ylabel = cut_x_label
454 
455  self.plotsplots["cut_over_bkg_rejection"] = cut_over_bkg_rejection_profile
456 
457  # Efficiency over background rejection #
458  # #################################### #
459  efficiency_over_bkg_rejection_profile_name = formatter.format(plot_name, subplot_name="efficiency_over_bkg_rejection")
460  efficiency_over_bkg_rejection_profile = ValidationPlot(efficiency_over_bkg_rejection_profile_name)
461  efficiency_over_bkg_rejection_profile.profile(
462  sorted_bkg_rejections,
463  sorted_efficiencies,
464  lower_bound=0,
465  upper_bound=1
466  )
467 
468  efficiency_over_bkg_rejection_profile.xlabel = "bkg rejection"
469  efficiency_over_bkg_rejection_profile.ylabel = "efficiency"
470 
471  self.plotsplots["efficiency_over_bkg_rejection"] = efficiency_over_bkg_rejection_profile
472 
473 
475 
476  @property
477  def contact(self):
478  """Get the name of the contact person"""
479  return self._contact_contact
480 
481  @contact.setter
482  def contact(self, contact):
483  """Set the name of the contact person"""
484  self._contact_contact = contact
485 
486  for plot in list(self.plotsplots.values()):
487  plot.contact = contact
488 
489  if self.fomfom:
490  self.fomfom.contact = contact
491 
492  def write(self, tdirectory=None):
493  """Write the plots to the ROOT TDirectory"""
494  for plot in list(self.plotsplots.values()):
495  plot.write(tdirectory)
496 
497  if self.fomfom:
498  self.fomfom.write(tdirectory)
499 
500 
501 class CutClassifier(object):
502 
503  """Simple classifier cutting on a single variable"""
504 
505  def __init__(self, cut_direction=1, cut_value=np.nan):
506  """Constructor"""
507 
508  self.cut_direction_cut_direction_ = cut_direction
509 
510  self.cut_value_cut_value_ = cut_value
511 
512  @property
513  def cut_direction(self):
514  """Get the value of the cut direction"""
515  return self.cut_direction_cut_direction_
516 
517  @property
518  def cut_value(self):
519  """Get the value of the cut threshold"""
520  return self.cut_value_cut_value_
521 
522  def clone(self):
523  """Return a clone of this object"""
524  return copy.copy(self)
525 
526  def determine_cut_value(self, estimates, truths):
527  """Get the value of the cut threshold"""
528  return self.cut_value_cut_value_ # do not change cut value from constructed one
529 
530  def fit(self, estimates, truths):
531  """Fit to determine the cut threshold"""
532  self.cut_value_cut_value_ = self.determine_cut_valuedetermine_cut_value(estimates, truths)
533  return self
534 
535  def predict(self, estimates):
536  """Select estimates that satisfy the cut"""
537  if self.cut_value_cut_value_ is None:
538  raise ValueError("Cut value not set. Forgot to fit?")
539 
540  if self.cut_direction_cut_direction_ < 0:
541  binary_estimates = estimates >= self.cut_value_cut_value_
542  else:
543  binary_estimates = estimates <= self.cut_value_cut_value_
544 
545  return binary_estimates
546 
547  def describe(self, estimates, truths):
548  """Describe the cut selection and its efficiency, purity and background rejection"""
549  if self.cut_direction_cut_direction_ < 0:
550  print("Cut accepts >= ", self.cut_value_cut_value_, 'with')
551  else:
552  print("Cut accepts <= ", self.cut_value_cut_value_, 'with')
553 
554  binary_estimates = self.predictpredict(estimates)
555 
556  efficiency = scores.efficiency(truths, binary_estimates)
557  purity = scores.purity(truths, binary_estimates)
558  background_rejection = scores.background_rejection(truths, binary_estimates)
559 
560  print("efficiency", efficiency)
561  print("purity", purity)
562  print("background_rejection", background_rejection)
563 
564 
565 def cut_at_background_rejection(background_rejection=0.5, cut_direction=1):
566  return CutAtBackgroundRejectionClassifier(background_rejection, cut_direction)
567 
568 
570  """Apply cut on the background rejection"""
571 
572  def __init__(self, background_rejection=0.5, cut_direction=1):
573  """Constructor"""
574  super(CutAtBackgroundRejectionClassifier, self).__init__(cut_direction=cut_direction, cut_value=np.nan)
575 
576  self.background_rejectionbackground_rejection = background_rejection
577 
578  def determine_cut_value(self, estimates, truths):
579  """Find the cut value that satisfies the desired background-rejection level"""
580  n_data = len(estimates)
581  n_signals = scores.signal_amount(truths, estimates)
582  n_bkgs = n_data - n_signals
583 
584  sorting_indices = np.argsort(estimates)
585  if self.cut_direction_cut_direction_ < 0: # reject low
586  # Keep a reference to keep the content alive
587  original_sorting_indices = sorting_indices # noqa
588  sorting_indices = sorting_indices[::-1]
589 
590  sorted_truths = truths[sorting_indices]
591  sorted_estimates = estimates[sorting_indices]
592 
593  sorted_n_accepted_signals = np.cumsum(sorted_truths, dtype=float)
594  # sorted_efficiencies = sorted_n_accepted_signals / n_signals
595 
596  sorted_n_rejected_signals = n_signals - sorted_n_accepted_signals
597  sorted_n_rejects = np.arange(len(estimates) + 1, 1, -1)
598  sorted_n_rejected_bkgs = sorted_n_rejects - sorted_n_rejected_signals
599  sorted_bkg_rejections = sorted_n_rejected_bkgs / n_bkgs
600 
601  cut_index, = np.searchsorted(sorted_bkg_rejections[::-1], (self.background_rejectionbackground_rejection,), side='right')
602 
603  cut_value = sorted_estimates[-cut_index - 1]
604  return cut_value
def analyse(self, estimates, truths, auxiliaries={})
cut
cached value of the threshold in the truth-classification analysis
quantity_name
cached name of the quantity in the truth-classification analysis
upper_bound
cached upper bound for this truth-classification analysis
outlier_z_score
cached Z-score (for outlier detection) for this truth-classification analysis
plots
cached dictionary of plots in the truth-classification analysis
allow_discrete
cached discrete-value flag for this truth-classification analysis
unit
cached measurement unit for this truth-classification analysis
_contact
cached contact person of the truth-classification analysis
cut_direction
cached value of the cut direction (< or >) in the truth-classification analysis
def __init__(self, contact, quantity_name, cut_direction=None, cut=None, lower_bound=None, upper_bound=None, outlier_z_score=None, allow_discrete=None, unit=None)
lower_bound
cached lower bound for this truth-classification analysis
fom
cached value of the figure of merit in the truth-classification analysis
background_rejection
cachec copy of the background-rejection threshold
def __init__(self, background_rejection=0.5, cut_direction=1)
def determine_cut_value(self, estimates, truths)
cut_direction_
cached copy of the cut direction (< or >)
cut_value_
cached copy of the cut threshold
def __init__(self, cut_direction=1, cut_value=np.nan)