main.cpp 39.4 KB
Newer Older
André Anjos's avatar
André Anjos committed
1
2
3
4
/**
 * @author Andre Anjos <andre.anjos@idiap.ch>
 * @date Fri 25 Oct 16:54:55 2013
 *
André Anjos's avatar
André Anjos committed
5
 * @brief Bindings to bob::measure
André Anjos's avatar
André Anjos committed
6
7
8
9
10
 */

#ifdef NO_IMPORT_ARRAY
#undef NO_IMPORT_ARRAY
#endif
André Anjos's avatar
André Anjos committed
11
12
#include <bob.blitz/cppapi.h>
#include <bob.blitz/cleanup.h>
13
14
#include <bob.core/api.h>
#include <bob.io.base/api.h>
15
#include <bob.extension/documentation.h>
16

17
#include "cpp/error.h"
André Anjos's avatar
André Anjos committed
18

19
static int double1d_converter(PyObject* o, PyBlitzArrayObject** a) {
20
  if (PyBlitzArray_Converter(o, a) == 0) return 0;
21
22
23
  // in this case, *a is set to a new reference
  if ((*a)->type_num != NPY_FLOAT64 || (*a)->ndim != 1) {
    PyErr_Format(PyExc_TypeError, "cannot convert blitz::Array<%s,%" PY_FORMAT_SIZE_T "d> to a blitz::Array<double,1>", PyBlitzArray_TypenumAsString((*a)->type_num), (*a)->ndim);
24
    return 0;
25
  }
26
  return 1;
27
}
André Anjos's avatar
André Anjos committed
28

29
30
31
32
33
34
35
36
37
static int double2d_converter(PyObject* o, PyBlitzArrayObject** a) {
  if (PyBlitzArray_Converter(o, a) == 0) return 0;
  // in this case, *a is set to a new reference
  if ((*a)->type_num != NPY_FLOAT64 || (*a)->ndim != 2) {
    PyErr_Format(PyExc_TypeError, "cannot convert blitz::Array<%s,%" PY_FORMAT_SIZE_T "d> to a blitz::Array<double,2>", PyBlitzArray_TypenumAsString((*a)->type_num), (*a)->ndim);
    return 0;
  }
  return 1;
}
André Anjos's avatar
André Anjos committed
38

39

40
41
42
43
static auto epc_doc = bob::extension::FunctionDoc(
  "epc",
  "Calculates points of an Expected Performance Curve (EPC)",
  "Calculates the EPC curve given a set of positive and negative scores and a desired number of points. "
44
45
46
  "Returns a two-dimensional :py:class:`numpy.ndarray` of type float with the "
  "shape of ``(2, points)`` or ``(3, points)`` depending on the ``thresholds`` argument. "
  "The rows correspond to the X (cost), Y (weighted error rate on the test set given the min. threshold on the development set), and the thresholds which were used to calculate the error (if the ``thresholds`` argument was set to ``True``), respectively. "
47
48
49
50
51
52
53
54
  "Please note that, in order to calculate the EPC curve, one needs two sets of data comprising a development set and a test set. "
  "The minimum weighted error is calculated on the development set and then applied to the test set to evaluate the weighted error rate at that position.\n\n"
  "The EPC curve plots the HTER on the test set for various values of 'cost'. "
  "For each value of 'cost', a threshold is found that provides the minimum weighted error (see :py:func:`bob.measure.min_weighted_error_rate_threshold`) on the development set. "
  "Each threshold is consecutively applied to the test set and the resulting weighted error values are plotted in the EPC.\n\n"
  "The cost points in which the EPC curve are calculated are distributed uniformly in the range :math:`[0.0, 1.0]`.\n\n"
  ".. note:: It is more memory efficient, when sorted arrays of scores are provided and the ``is_sorted`` parameter is set to ``True``."
)
55
.add_prototype("dev_negatives, dev_positives, test_negatives, test_positives, n_points, [is_sorted], [thresholds]", "curve")
56
57
58
.add_parameter("dev_negatives, dev_positives, test_negatives, test_positives", "array_like(1D, float)", "The scores for negatives and positives of the development and test set")
.add_parameter("n_points", "int", "The number of weights for which the EPC curve should be computed")
.add_parameter("is_sorted", "bool", "[Default: ``False``] Set this to ``True`` if the scores are already sorted. If ``False``, scores will be sorted internally, which will require more memory")
59
60
.add_parameter("thresholds", "bool", "[Default: ``False``] If ``True`` the function returns an array with the shape of ``(3, points)`` where the third row contains the thresholds that were calculated on the development set.")
.add_return("curve", "array_like(2D, float)", "The EPC curve, with the first row containing the weights and the second row containing the weighted errors on the test set. If ``thresholds`` is ``True``, there is also a third row which contains the thresholds that were calculated on the development set.")
61
62
63
;
static PyObject* epc(PyObject*, PyObject* args, PyObject* kwds) {
BOB_TRY
64
  /* Parses input arguments in a single shot */
65
66
67
68
69
70
71
72
  char** kwlist = epc_doc.kwlist();

  PyBlitzArrayObject* dev_neg;
  PyBlitzArrayObject* dev_pos;
  PyBlitzArrayObject* test_neg;
  PyBlitzArrayObject* test_pos;
  Py_ssize_t n_points;
  PyObject* is_sorted = Py_False;
73
  PyObject* thresholds = Py_False;
74

75
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&O&O&n|OO",
76
77
78
79
80
        kwlist,
        &double1d_converter, &dev_neg,
        &double1d_converter, &dev_pos,
        &double1d_converter, &test_neg,
        &double1d_converter, &test_pos,
81
        &n_points,
82
83
        &is_sorted,
        &thresholds
84
85
        )) return 0;

86
87
88
89
90
91
  //protects acquired resources through this scope
  auto dev_neg_ = make_safe(dev_neg);
  auto dev_pos_ = make_safe(dev_pos);
  auto test_neg_ = make_safe(test_neg);
  auto test_pos_ = make_safe(test_pos);

André Anjos's avatar
André Anjos committed
92
  auto result = bob::measure::epc(
93
94
95
96
      *PyBlitzArrayCxx_AsBlitz<double,1>(dev_neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(dev_pos),
      *PyBlitzArrayCxx_AsBlitz<double,1>(test_neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(test_pos),
97
      n_points, PyObject_IsTrue(is_sorted), PyObject_IsTrue(thresholds));
André Anjos's avatar
André Anjos committed
98

99
100
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("epc", 0)
101
}
André Anjos's avatar
André Anjos committed
102

103
104
105
106
107
108
109
static auto det_doc = bob::extension::FunctionDoc(
  "det",
  "Calculates points of an Detection Error-Tradeoff (DET) curve",
  "Calculates the DET curve given a set of negative and positive scores and a desired number of points. Returns a two-dimensional array of doubles that express on its rows:\n\n"
  "[0]  X axis values in the normal deviate scale for the false-accepts\n\n"
  "[1]  Y axis values in the normal deviate scale for the false-rejections\n\n"
  "You can plot the results using your preferred tool to first create a plot using rows 0 and 1 from the returned value and then replace the X/Y axis annotation using a pre-determined set of tickmarks as recommended by NIST. "
110
  "The derivative scales are computed with the :py:func:`bob.measure.ppndf` function."
111
112
113
114
115
116
)
.add_prototype("negatives, positives, n_points", "curve")
.add_parameter("negatives, positives", "array_like(1D, float)", "The list of negative and positive scores to compute the DET for")
.add_parameter("n_points", "int", "The number of points on the DET curve, for which the DET should be evaluated")
.add_return("curve", "array_like(2D, float)", "The DET curve, with the FAR in the first and the FRR in the second row")
;
André Anjos's avatar
André Anjos committed
117
static PyObject* det(PyObject*, PyObject* args, PyObject* kwds) {
118
119
BOB_TRY
  char** kwlist = det_doc.kwlist();
André Anjos's avatar
André Anjos committed
120

121
122
123
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
124
125
126
127
128
129
130
131

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&n",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
        &n_points
        )) return 0;

132
133
134
135
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
136
137
138
139
140
  auto result = bob::measure::det(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      n_points);

141
142
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("det", 0)
André Anjos's avatar
André Anjos committed
143
144
}

145
146
147
148
149
150
151
152
153
154
155
static auto ppndf_doc = bob::extension::FunctionDoc(
  "ppndf",
  "Returns the Deviate Scale equivalent of a false rejection/acceptance ratio",
  "The algorithm that calculates the deviate scale is based on function ppndf() from the NIST package DETware version 2.1, freely available on the internet. "
  "Please consult it for more details. "
  "By 20.04.2011, you could find such package `here <http://www.itl.nist.gov/iad/mig/tools/>`_."
)
.add_prototype("value", "ppndf")
.add_parameter("value", "float", "The value (usually FAR or FRR) for which the ppndf should be calculated")
.add_return("ppndf", "float", "The derivative scale of the given value")
;
André Anjos's avatar
André Anjos committed
156
static PyObject* ppndf(PyObject*, PyObject* args, PyObject* kwds) {
157
158
159
BOB_TRY
  char** kwlist = ppndf_doc.kwlist();
  double v;
André Anjos's avatar
André Anjos committed
160
161
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "d", kwlist, &v)) return 0;

162
163
  return Py_BuildValue("d", bob::measure::ppndf(v));
BOB_CATCH_FUNCTION("ppndf", 0)
André Anjos's avatar
André Anjos committed
164
165
}

166
167
168
169
170
171
172
173
174
175
static auto roc_doc = bob::extension::FunctionDoc(
  "roc",
  "Calculates points of an Receiver Operating Characteristic (ROC)",
  "Calculates the ROC curve given a set of negative and positive scores and a desired number of points. "
)
.add_prototype("negatives, positives, n_points", "curve")
.add_parameter("negatives, positives", "array_like(1D, float)", "The negative and positive scores, for which the ROC curve should be calculated")
.add_parameter("n_points", "int", "The number of points, in which the ROC curve are calculated, which are distributed uniformly in the range ``[min(negatives, positives), max(negatives, positives)]``")
.add_return("curve", "array_like(2D, float)", "A two-dimensional array of doubles that express the X (FAR) and Y (FRR) coordinates in this order")
;
André Anjos's avatar
André Anjos committed
176
static PyObject* roc(PyObject*, PyObject* args, PyObject* kwds) {
177
178
BOB_TRY
  static char** kwlist = roc_doc.kwlist();
André Anjos's avatar
André Anjos committed
179

180
181
182
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
183
184
185
186
187
188
189
190

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&n",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
        &n_points
        )) return 0;

191
192
193
194
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
195
196
197
198
199
  auto result = bob::measure::roc(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      n_points);

200
201
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("roc", 0)
André Anjos's avatar
André Anjos committed
202
203
}

204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
static auto farfrr_doc = bob::extension::FunctionDoc(
  "farfrr",
  "Calculates the false-acceptance (FA) ratio and the false-rejection (FR) ratio for the given positive and negative scores and a score threshold",
  "``positives`` holds the score information for samples that are labeled to belong to a certain class (a.k.a., 'signal' or 'client'). "
  "``negatives`` holds the score information for samples that are labeled **not** to belong to the class (a.k.a., 'noise' or 'impostor'). "
  "It is expected that 'positive' scores are, at least by design, greater than 'negative' scores. "
  "So, every 'positive' value that falls bellow the threshold is considered a false-rejection (FR). "
  "`negative` samples that fall above the threshold are considered a false-accept (FA).\n\n"
  "Positives that fall on the threshold (exactly) are considered correctly classified. "
  "Negatives that fall on the threshold (exactly) are considered **incorrectly** classified. "
  "This equivalent to setting the comparison like this pseudo-code:\n\n"
  "  ``foreach (positive as K) if K < threshold: falseRejectionCount += 1``\n\n"
  "  ``foreach (negative as K) if K >= threshold: falseAcceptCount += 1``\n\n"
  "The output is in form of a tuple of two double-precision real numbers. "
  "The numbers range from 0 to 1. "
  "The first element of the pair is the false-accept ratio (FAR), the second element the false-rejection ratio (FRR).\n\n"
  "The ``threshold`` value does not necessarily have to fall in the range covered by the input scores (negatives and positives altogether), but if it does not, the output will be either (1.0, 0.0) or (0.0, 1.0), depending on the side the threshold falls.\n\n"
  "It is possible that scores are inverted in the negative/positive sense. "
  "In some setups the designer may have setup the system so 'positive' samples have a smaller score than the 'negative' ones. "
  "In this case, make sure you normalize the scores so positive samples have greater scores before feeding them into this method."
)
.add_prototype("negatives, positives, threshold", "far, frr")
.add_parameter("negatives", "array_like(1D, float)", "The scores for comparisons of objects of different classes")
.add_parameter("positives", "array_like(1D, float)", "The scores for comparisons of objects of the same class")
.add_parameter("threshold", "float", "The threshold to separate correctly and incorrectly classified scores")
.add_return("far", "float", "The False Accept Rate (FAR) for the given threshold")
.add_return("frr", "float", "The False Reject Rate (FRR) for the given threshold")
;
André Anjos's avatar
André Anjos committed
232
static PyObject* farfrr(PyObject*, PyObject* args, PyObject* kwds) {
233
234
BOB_TRY
  char** kwlist = farfrr_doc.kwlist();
André Anjos's avatar
André Anjos committed
235

236
237
238
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
239
240
241
242
243
244
245
246

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&d",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
        &threshold
        )) return 0;

247
248
249
250
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
251
252
253
254
255
  auto result = bob::measure::farfrr(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold);

256
257
  return Py_BuildValue("dd", result.first, result.second);
BOB_CATCH_FUNCTION("farfrr", 0)
André Anjos's avatar
André Anjos committed
258
259
}

260
261
262
263
264
265
266
267
268
269
270
271
static auto eer_threshold_doc = bob::extension::FunctionDoc(
  "eer_threshold",
  "Calculates the threshold that is as close as possible to the equal-error-rate (EER) for the given input data",
  "The EER should be the point where the FAR equals the FRR. "
  "Graphically, this would be equivalent to the intersection between the ROC (or DET) curves and the identity.\n\n"
  ".. note::\n\n"
  "   The scores will be sorted internally, requiring the scores to be copied.\n"
  "   To avoid this copy, you can sort both sets of scores externally in ascendant order, and set the ``is_sorted`` parameter to ``True``"
)
.add_prototype("negatives, positives, [is_sorted]", "threshold")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the threshold")
.add_parameter("is_sorted", "bool", "[Default: ``False``] Are both sets of scores already in ascendantly sorted order?")
272
.add_return("threshold", "float", "The threshold (i.e., as used in :py:func:`bob.measure.farfrr`) where FAR and FRR are as close as possible")
273
;
André Anjos's avatar
André Anjos committed
274
static PyObject* eer_threshold(PyObject*, PyObject* args, PyObject* kwds) {
275
276
BOB_TRY
  char** kwlist = eer_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
277

278
279
280
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
281

282
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|O",
André Anjos's avatar
André Anjos committed
283
284
        kwlist,
        &double1d_converter, &neg,
285
286
        &double1d_converter, &pos,
        &is_sorted
André Anjos's avatar
André Anjos committed
287
288
        )) return 0;

289
290
291
292
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
293
294
  double result = bob::measure::eerThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
295
296
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
297

298
299
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("eer_threshold", 0)
André Anjos's avatar
André Anjos committed
300
301
}

302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
static auto min_weighted_error_rate_threshold_doc = bob::extension::FunctionDoc(
  "min_weighted_error_rate_threshold",
  "Calculates the threshold that minimizes the error rate for the given input data",
  "The ``cost`` parameter determines the relative importance between false-accepts and false-rejections. "
  "This number should be between 0 and 1 and will be clipped to those extremes. "
  "The value to minimize becomes: :math:`ER_{cost} = cost * FAR + (1-cost) * FRR`. "
  "The higher the cost, the higher the importance given to **not** making mistakes classifying negatives/noise/impostors.\n\n"
  ".. note:: "
  "The scores will be sorted internally, requiring the scores to be copied. "
  "To avoid this copy, you can sort both sets of scores externally in ascendant order, and set the ``is_sorted`` parameter to ``True``"
)
.add_prototype("negatives, positives, cost, [is_sorted]", "threshold")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the threshold")
.add_parameter("cost", "float", "The relative cost over FAR with respect to FRR in the threshold calculation")
.add_parameter("is_sorted", "bool", "[Default: ``False``] Are both sets of scores already in ascendantly sorted order?")
.add_return("threshold", "float", "The threshold for which the weighted error rate is minimal")
;
André Anjos's avatar
André Anjos committed
319
static PyObject* min_weighted_error_rate_threshold(PyObject*, PyObject* args, PyObject* kwds) {
320
321
BOB_TRY
  char** kwlist = min_weighted_error_rate_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
322

323
324
325
326
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double cost;
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
327

328
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&d|O",
André Anjos's avatar
André Anjos committed
329
330
331
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
332
333
        &cost,
        &is_sorted
André Anjos's avatar
André Anjos committed
334
335
        )) return 0;

336
337
338
339
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
340
341
342
  double result = bob::measure::minWeightedErrorRateThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
343
344
      cost,
      PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
345

346
347
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("min_weighted_error_rate_threshold", 0)
André Anjos's avatar
André Anjos committed
348
349
}

350
351
static auto min_hter_threshold_doc = bob::extension::FunctionDoc(
  "min_hter_threshold",
352
  "Calculates the :py:func:`bob.measure.min_weighted_error_rate_threshold` with ``cost=0.5``"
353
354
355
356
357
358
)
.add_prototype("negatives, positives, [is_sorted]", "threshold")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the threshold")
.add_parameter("is_sorted", "bool", "[Default: ``False``] Are both sets of scores already in ascendantly sorted order?")
.add_return("threshold", "float", "The threshold for which the weighted error rate is minimal")
;
André Anjos's avatar
André Anjos committed
359
static PyObject* min_hter_threshold(PyObject*, PyObject* args, PyObject* kwds) {
360
361
BOB_TRY
  char** kwlist = min_hter_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
362

363
364
365
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
366

367
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|O",
André Anjos's avatar
André Anjos committed
368
369
        kwlist,
        &double1d_converter, &neg,
370
371
        &double1d_converter, &pos,
        &is_sorted
André Anjos's avatar
André Anjos committed
372
373
        )) return 0;

374
375
376
377
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
378
379
  double result = bob::measure::minHterThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
380
381
382
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      PyObject_IsTrue(is_sorted)
      );
André Anjos's avatar
André Anjos committed
383

384
385
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("min_hter_threshold", 0)
André Anjos's avatar
André Anjos committed
386
387
}

388
389
390
391
392
393
394
395
396
397
static auto precision_recall_doc = bob::extension::FunctionDoc(
  "precision_recall",
  "Calculates the precision and recall (sensitiveness) values given negative and positive scores and a threshold",
  "Precision and recall are computed as:\n\n"
  ".. math::\n\n"
  "   \\mathrm{precision} = \\frac{tp}{tp + fp}\n\n"
  "   \\mathrm{recall} = \\frac{tp}{tp + fn}\n\n"
  "where :math:`tp` are the true positives, :math:`fp` are the false positives and :math:`fn` are the false negatives.\n\n"
  "``positives`` holds the score information for samples that are labeled to belong to a certain class (a.k.a., 'signal' or 'client'). "
  "``negatives`` holds the score information for samples that are labeled **not** to belong to the class (a.k.a., 'noise' or 'impostor'). "
398
  "For more precise details about how the method considers error rates, see :py:func:`bob.measure.farfrr`."
399
400
401
402
403
404
405
)
.add_prototype("negatives, positives, threshold", "precision, recall")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the measurements")
.add_parameter("threshold", "float", "The threshold to compute the measures for")
.add_return("precision", "float", "The precision value for the given negatives and positives")
.add_return("recall", "float", "The recall value for the given negatives and positives")
;
André Anjos's avatar
André Anjos committed
406
static PyObject* precision_recall(PyObject*, PyObject* args, PyObject* kwds) {
407
408
BOB_TRY
  static char** kwlist = precision_recall_doc.kwlist();
André Anjos's avatar
André Anjos committed
409

410
411
412
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
413
414
415
416
417
418
419
420

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&d",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
        &threshold
        )) return 0;

421
422
423
424
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
425
426
427
428
429
  auto result = bob::measure::precision_recall(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold);

430
431
  return Py_BuildValue("dd", result.first, result.second);
BOB_CATCH_FUNCTION("precision_recall", 0)
André Anjos's avatar
André Anjos committed
432
433
}

434
435
436
static auto f_score_doc = bob::extension::FunctionDoc(
  "f_score",
  "This method computes the F-score of the accuracy of the classification",
437
  "The F-score is a weighted mean of precision and recall measurements, see :py:func:`bob.measure.precision_recall`. "
438
439
440
441
442
443
444
445
446
447
448
449
  "It is computed as:\n\n"
  ".. math::\n\n"
  "    \\mathrm{f-score} = (1 + w^2)\\frac{\\mathrm{precision}\\cdot{}\\mathrm{recall}}{w^2\\cdot{}\\mathrm{precision} + \\mathrm{recall}}\n\n"
  "The weight :math:`w` needs to be non-negative real value. "
  "In case the weight parameter is 1 (the default), the F-score is called F1 score and is a harmonic mean between precision and recall values."
)
.add_prototype("negatives, positives, threshold, [weight]", "f_score")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the precision and recall")
.add_parameter("threshold", "float", "The threshold to compute the precision and recall for")
.add_parameter("weight", "float", "[Default: ``1``] The weight :math:`w` between precision and recall")
.add_return("f_score", "float", "The computed f-score for the given scores and the given threshold")
;
André Anjos's avatar
André Anjos committed
450
static PyObject* f_score(PyObject*, PyObject* args, PyObject* kwds) {
451
452
BOB_TRY
  static char** kwlist = f_score_doc.kwlist();
André Anjos's avatar
André Anjos committed
453

454
455
456
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
457
458
459
460
461
462
463
464
465
  double weight = 1.0;

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&d|d",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
        &threshold, &weight
        )) return 0;

466
467
468
469
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
470
471
472
473
474
  auto result = bob::measure::f_score(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold, weight);

475
476
  return Py_BuildValue("d",result);
BOB_CATCH_FUNCTION("f_score", 0)
André Anjos's avatar
André Anjos committed
477
478
}

479
480
481
482
483
484
485
486
487
488
489
static auto correctly_classified_negatives_doc = bob::extension::FunctionDoc(
  "correctly_classified_negatives",
  "This method returns an array composed of booleans that pin-point, which negatives where correctly classified for the given threshold",
  "The pseudo-code for this function is:\n\n"
  "  ``foreach (k in negatives) if negatives[k] < threshold: classified[k] = true else: classified[k] = false``"
)
.add_prototype("negatives, threshold", "classified")
.add_parameter("negatives", "array_like(1D, float)", "The scores generated by comparing objects of different classes")
.add_parameter("threshold", "float", "The threshold, for which scores should be considered to be correctly classified")
.add_return("classified", "array_like(1D, bool)", "The decision for each of the ``negatives``")
;
André Anjos's avatar
André Anjos committed
490
static PyObject* correctly_classified_negatives(PyObject*, PyObject* args, PyObject* kwds) {
491
492
BOB_TRY
  static char** kwlist = correctly_classified_negatives_doc.kwlist();
André Anjos's avatar
André Anjos committed
493

494
495
  PyBlitzArrayObject* neg;
  double threshold;
André Anjos's avatar
André Anjos committed
496
497
498
499
500
501
502

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&d",
        kwlist,
        &double1d_converter, &neg,
        &threshold
        )) return 0;

503
504
505
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);

André Anjos's avatar
André Anjos committed
506
507
508
509
  auto result = bob::measure::correctlyClassifiedNegatives(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      threshold);

510
511
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("correctly_classified_negatives", 0)
André Anjos's avatar
André Anjos committed
512
513
}

514
515
516
517
518
519
520
521
522
523
524
static auto correctly_classified_positives_doc = bob::extension::FunctionDoc(
  "correctly_classified_positives",
  "This method returns an array composed of booleans that pin-point, which positives where correctly classified for the given threshold",
  "The pseudo-code for this function is:\n\n"
  "  ``foreach (k in positives) if positives[k] >= threshold: classified[k] = true else: classified[k] = false``"
)
.add_prototype("positives, threshold", "classified")
.add_parameter("positives", "array_like(1D, float)", "The scores generated by comparing objects of the same classes")
.add_parameter("threshold", "float", "The threshold, for which scores should be considered to be correctly classified")
.add_return("classified", "array_like(1D, bool)", "The decision for each of the ``positives``")
;
André Anjos's avatar
André Anjos committed
525
static PyObject* correctly_classified_positives(PyObject*, PyObject* args, PyObject* kwds) {
526
527
BOB_TRY
  static char** kwlist = correctly_classified_positives_doc.kwlist();
André Anjos's avatar
André Anjos committed
528

529
530
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
531
532
533
534
535
536
537

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&d",
        kwlist,
        &double1d_converter, &pos,
        &threshold
        )) return 0;

538
539
540
  //protects acquired resources through this scope
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
541
542
543
544
  auto result = bob::measure::correctlyClassifiedPositives(
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold);

545
546
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("correctly_classified_positives", 0)
André Anjos's avatar
André Anjos committed
547
548
}

549
550
551
552
553
554
555
556
557
558
static auto precision_recall_curve_doc = bob::extension::FunctionDoc(
  "precision_recall_curve",
  "Calculates the precision-recall curve given a set of positive and negative scores and a number of desired points" ,
  "The points in which the curve is calculated are distributed uniformly in the range ``[min(negatives, positives), max(negatives, positives)]``"
)
.add_prototype("negatives, positives, n_points", "curve")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the measurements")
.add_parameter("n_points", "int", "The number of thresholds for which precision and recall should be evaluated")
.add_return("curve", "array_like(2D, float)", "2D array of floats that express the X (precision) and Y (recall) coordinates")
;
André Anjos's avatar
André Anjos committed
559
static PyObject* precision_recall_curve(PyObject*, PyObject* args, PyObject* kwds) {
560
561
BOB_TRY
  char** kwlist = precision_recall_curve_doc.kwlist();
André Anjos's avatar
André Anjos committed
562

563
564
565
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
566
567
568
569
570
571
572
573

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&n",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
        &n_points
        )) return 0;

574
575
576
577
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
578
579
580
581
582
  auto result = bob::measure::precision_recall_curve(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      n_points);

583
584
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("precision_recall_curve", 0)
André Anjos's avatar
André Anjos committed
585
586
}

587
588
589
590
591
592
593
594
595
596
597
598
599
600
static auto far_threshold_doc = bob::extension::FunctionDoc(
  "far_threshold",
  "Computes the threshold such that the real FAR is **at least** the requested ``far_value``",
  ".. note::\n\n"
  "   The scores will be sorted internally, requiring the scores to be copied.\n"
  "   To avoid this copy, you can sort the ``negatives`` scores externally in ascendant order, and set the ``is_sorted`` parameter to ``True``"
)
.add_prototype("negatives, positives, [far_value], [is_sorted]", "threshold")
.add_parameter("negatives", "array_like(1D, float)", "The set of negative scores to compute the FAR threshold")
.add_parameter("positives", "array_like(1D, float)", "Ignored, but needs to be specified -- may be given as ``[]``")
.add_parameter("far_value", "float", "[Default: ``0.001``] The FAR value, for which the threshold should be computed")
.add_parameter("is_sorted", "bool", "[Default: ``False``] Set this to ``True`` if the ``negatives`` are already sorted in ascending order. If ``False``, scores will be sorted internally, which will require more memory")
.add_return("threshold", "float", "The threshold such that the real FAR is at least ``far_value``")
;
André Anjos's avatar
André Anjos committed
601
static PyObject* far_threshold(PyObject*, PyObject* args, PyObject* kwds) {
602
603
BOB_TRY
  static char** kwlist = far_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
604

605
606
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
607
  double far_value = 0.001;
608
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
609

610
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|dO",
André Anjos's avatar
André Anjos committed
611
612
613
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
614
615
        &far_value,
        is_sorted
André Anjos's avatar
André Anjos committed
616
617
        )) return 0;

618
619
620
621
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
622
623
624
  auto result = bob::measure::farThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
625
626
627
      far_value,
      PyObject_IsTrue(is_sorted)
      );
André Anjos's avatar
André Anjos committed
628

629
630
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("far_threshold", 0)
André Anjos's avatar
André Anjos committed
631
632
}

633
634
635
636
637
638
639
640
641
642
643
644
645
646
static auto frr_threshold_doc = bob::extension::FunctionDoc(
  "frr_threshold",
  "Computes the threshold such that the real FRR is **at least** the requested ``frr_value``",
  ".. note::\n\n"
  "   The scores will be sorted internally, requiring the scores to be copied.\n"
  "   To avoid this copy, you can sort the ``positives`` scores externally in ascendant order, and set the ``is_sorted`` parameter to ``True``"
)
.add_prototype("negatives, positives, [frr_value], [is_sorted]", "threshold")
.add_parameter("negatives", "array_like(1D, float)", "Ignored, but needs to be specified -- may be given as ``[]``")
.add_parameter("positives", "array_like(1D, float)", "The set of positive scores to compute the FRR threshold")
.add_parameter("frr_value", "float", "[Default: ``0.001``] The FRR value, for which the threshold should be computed")
.add_parameter("is_sorted", "bool", "[Default: ``False``] Set this to ``True`` if the ``positives`` are already sorted in ascendant order. If ``False``, scores will be sorted internally, which will require more memory")
.add_return("threshold", "float", "The threshold such that the real FRR is at least ``frr_value``")
;
André Anjos's avatar
André Anjos committed
647
static PyObject* frr_threshold(PyObject*, PyObject* args, PyObject* kwds) {
648
649
BOB_TRY
  char** kwlist = frr_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
650

651
652
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
653
  double frr_value = 0.001;
654
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
655

656
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|dO",
André Anjos's avatar
André Anjos committed
657
658
659
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
660
661
        &frr_value,
        &is_sorted
André Anjos's avatar
André Anjos committed
662
663
        )) return 0;

664
665
666
667
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
668
669
670
  auto result = bob::measure::frrThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
671
672
673
      frr_value,
      PyObject_IsTrue(is_sorted)
      );
André Anjos's avatar
André Anjos committed
674

675
676
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("frr_threshold", 0)
André Anjos's avatar
André Anjos committed
677
678
}

679
680
681
682
683
684
685
686
687
static auto eer_rocch_doc = bob::extension::FunctionDoc(
  "eer_rocch",
  "Calculates the equal-error-rate (EER) given the input data, on the ROC Convex Hull (ROCCH)",
  "It replicates the EER calculation from the Bosaris toolkit (https://sites.google.com/site/bosaristoolkit/)."
)
.add_prototype("negatives, positives", "threshold")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the threshold")
.add_return("threshold", "float", "The threshold for the equal error rate")
;
André Anjos's avatar
André Anjos committed
688
static PyObject* eer_rocch(PyObject*, PyObject* args, PyObject* kwds) {
689
BOB_TRY
André Anjos's avatar
André Anjos committed
690
  /* Parses input arguments in a single shot */
691
  char** kwlist = eer_rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
692

693
694
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
695
696
697
698
699
700
701

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos
        )) return 0;

702
703
704
705
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
706
707
708
709
710
  auto result = bob::measure::eerRocch(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos)
      );

711
712
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("eer_rocch", 0)
André Anjos's avatar
André Anjos committed
713
714
}

715
716
717
718
719
720
721
722
static auto rocch_doc = bob::extension::FunctionDoc(
  "rocch",
  "Calculates the ROC Convex Hull (ROCCH) curve given a set of positive and negative scores"
)
.add_prototype("negatives, positives", "curve")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the curve")
.add_return("curve", "array_like(2D, float)", "The ROC curve, with the first row containing the FAR, and the second row containing the FRR")
;
André Anjos's avatar
André Anjos committed
723
static PyObject* rocch(PyObject*, PyObject* args, PyObject* kwds) {
724
BOB_TRY
André Anjos's avatar
André Anjos committed
725
  /* Parses input arguments in a single shot */
726
  char** kwlist = rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
727

728
729
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
730
731
732
733
734
735
736

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&",
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos
        )) return 0;

737
738
739
740
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
741
742
743
744
745
  auto result = bob::measure::rocch(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos)
      );

746
747
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("rocch", 0)
André Anjos's avatar
André Anjos committed
748
749
}

750
751
752
753
754
755
756
757
758
static auto rocch2eer_doc = bob::extension::FunctionDoc(
  "rocch2eer",
  "Calculates the threshold that is as close as possible to the equal-error-rate (EER) given the input data"
)
.add_prototype("pmiss_pfa", "threshold")
// I don't know, what the pmiss_pfa parameter is, so I leave out its documentation (a .. todo:: will be generated automatically)
//.add_parameter("pmiss_pfa", "array_like(2D, float)", "???")
.add_return("threshold", "float", "The computed threshold, at which the EER can be obtained")
;
André Anjos's avatar
André Anjos committed
759
static PyObject* rocch2eer(PyObject*, PyObject* args, PyObject* kwds) {
760
761
BOB_TRY
  static char** kwlist = rocch2eer_doc.kwlist();
André Anjos's avatar
André Anjos committed
762

763
  PyBlitzArrayObject* p;
André Anjos's avatar
André Anjos committed
764
765
766
767
768
769

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&",
        kwlist,
        &double2d_converter, &p
        )) return 0;

770
  auto p_ = make_safe(p);
André Anjos's avatar
André Anjos committed
771

772
  auto result = bob::measure::rocch2eer(*PyBlitzArrayCxx_AsBlitz<double,2>(p));
André Anjos's avatar
André Anjos committed
773

774
775
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("rocch2eer", 0)
André Anjos's avatar
André Anjos committed
776
777
}

778
779
780
781
782
783
784
785
786
787
788
789
790
static auto roc_for_far_doc = bob::extension::FunctionDoc(
  "roc_for_far",
  "Calculates the ROC curve for a given set of positive and negative scores and the FAR values, for which the FRR should be computed",
  ".. note::\n\n"
  "   The scores will be sorted internally, requiring the scores to be copied.\n"
  "   To avoid this copy, you can sort both sets of scores externally in ascendant order, and set the ``is_sorted`` parameter to ``True``"
)
.add_prototype("negatives, positives, far_list, [is_sorted]", "curve")
.add_parameter("negatives, positives", "array_like(1D, float)", "The set of negative and positive scores to compute the curve")
.add_parameter("far_list", "array_like(1D, float)", "A list of FAR values, for which the FRR values should be computed")
.add_parameter("is_sorted", "bool", "[Default: ``False``] Set this to ``True`` if both sets of scores are already sorted in ascending order. If ``False``, scores will be sorted internally, which will require more memory")
.add_return("curve", "array_like(2D, float)", "The ROC curve, which holds a copy of the given FAR values in row 0, and the corresponding FRR values in row 1")
;
791
static PyObject* roc_for_far(PyObject*, PyObject* args, PyObject* kwds) {
792
BOB_TRY
793
  /* Parses input arguments in a single shot */
794
795
796
797
798
799
800
801
  char** kwlist = roc_for_far_doc.kwlist();

  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  PyBlitzArrayObject* far;
  PyObject* is_sorted = Py_False;

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&O&|O",
802
803
804
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
805
806
        &double1d_converter, &far,
        &is_sorted
807
808
        )) return 0;

809
810
811
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);
812
  auto far_ = make_safe(far);
813

814
815
816
  auto result = bob::measure::roc_for_far(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
817
818
      *PyBlitzArrayCxx_AsBlitz<double,1>(far),
      PyObject_IsTrue(is_sorted)
819
820
      );

821
822
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("roc_for_far", 0)
André Anjos's avatar
André Anjos committed
823
824
}

André Anjos's avatar
André Anjos committed
825
static PyMethodDef module_methods[] = {
826
    {
827
      epc_doc.name(),
828
829
      (PyCFunction)epc,
      METH_VARARGS|METH_KEYWORDS,
830
      epc_doc.doc()
831
    },
André Anjos's avatar
André Anjos committed
832
    {
833
      det_doc.name(),
André Anjos's avatar
André Anjos committed
834
835
      (PyCFunction)det,
      METH_VARARGS|METH_KEYWORDS,
836
      det_doc.doc()
André Anjos's avatar
André Anjos committed
837
838
    },
    {
839
      ppndf_doc.name(),
André Anjos's avatar
André Anjos committed
840
841
      (PyCFunction)ppndf,
      METH_VARARGS|METH_KEYWORDS,
842
      ppndf_doc.doc()
André Anjos's avatar
André Anjos committed
843
844
    },
    {
845
      roc_doc.name(),
André Anjos's avatar
André Anjos committed
846
847
      (PyCFunction)roc,
      METH_VARARGS|METH_KEYWORDS,
848
      roc_doc.doc()
André Anjos's avatar
André Anjos committed
849
850
    },
    {
851
      farfrr_doc.name(),
André Anjos's avatar
André Anjos committed
852
853
      (PyCFunction)farfrr,
      METH_VARARGS|METH_KEYWORDS,
854
      farfrr_doc.doc()
André Anjos's avatar
André Anjos committed
855
856
    },
    {
857
      eer_threshold_doc.name(),
André Anjos's avatar
André Anjos committed
858
859
      (PyCFunction)eer_threshold,
      METH_VARARGS|METH_KEYWORDS,
860
      eer_threshold_doc.doc()
André Anjos's avatar
André Anjos committed
861
862
    },
    {
863
      min_weighted_error_rate_threshold_doc.name(),
André Anjos's avatar
André Anjos committed
864
865
      (PyCFunction)min_weighted_error_rate_threshold,
      METH_VARARGS|METH_KEYWORDS,
866
      min_weighted_error_rate_threshold_doc.doc()
André Anjos's avatar
André Anjos committed
867
868
    },
    {
869
      min_hter_threshold_doc.name(),
André Anjos's avatar
André Anjos committed
870
871
      (PyCFunction)min_hter_threshold,
      METH_VARARGS|METH_KEYWORDS,
872
      min_hter_threshold_doc.doc()
André Anjos's avatar
André Anjos committed
873
874
    },
    {
875
      precision_recall_doc.name(),
André Anjos's avatar
André Anjos committed
876
877
      (PyCFunction)precision_recall,
      METH_VARARGS|METH_KEYWORDS,
878
      precision_recall_doc.doc()
André Anjos's avatar
André Anjos committed
879
880
    },
    {
881
      f_score_doc.name(),
André Anjos's avatar
André Anjos committed
882
883
      (PyCFunction)f_score,
      METH_VARARGS|METH_KEYWORDS,
884
      f_score_doc.doc()
André Anjos's avatar
André Anjos committed
885
886
    },
    {
887
      correctly_classified_negatives_doc.name(),
André Anjos's avatar
André Anjos committed
888
889
      (PyCFunction)correctly_classified_negatives,
      METH_VARARGS|METH_KEYWORDS,
890
      correctly_classified_negatives_doc.doc()
André Anjos's avatar
André Anjos committed
891
892
    },
    {
893
      correctly_classified_positives_doc.name(),
André Anjos's avatar
André Anjos committed
894
895
      (PyCFunction)correctly_classified_positives,
      METH_VARARGS|METH_KEYWORDS,
896
      correctly_classified_positives_doc.doc()
André Anjos's avatar
André Anjos committed
897
898
    },
    {
899
      precision_recall_curve_doc.name(),
André Anjos's avatar
André Anjos committed
900
901
      (PyCFunction)precision_recall_curve,
      METH_VARARGS|METH_KEYWORDS,
902
      precision_recall_curve_doc.doc()
André Anjos's avatar
André Anjos committed
903
904
    },
    {
905
      far_threshold_doc.name(),
André Anjos's avatar
André Anjos committed
906
907
      (PyCFunction)far_threshold,
      METH_VARARGS|METH_KEYWORDS,
908
      far_threshold_doc.doc()
André Anjos's avatar
André Anjos committed
909
910
    },
    {
911
      frr_threshold_doc.name(),
André Anjos's avatar
André Anjos committed
912
913
      (PyCFunction)frr_threshold,
      METH_VARARGS|METH_KEYWORDS,
914
      frr_threshold_doc.doc()
André Anjos's avatar
André Anjos committed
915
    },
André Anjos's avatar
André Anjos committed
916
    {
917
      eer_rocch_doc.name(),
André Anjos's avatar
André Anjos committed
918
919
      (PyCFunction)eer_rocch,
      METH_VARARGS|METH_KEYWORDS,
920
      eer_rocch_doc.doc()
André Anjos's avatar
André Anjos committed
921
922
    },
    {
923
      rocch_doc.name(),
André Anjos's avatar
André Anjos committed
924
925
      (PyCFunction)rocch,
      METH_VARARGS|METH_KEYWORDS,
926
      rocch_doc.doc()
André Anjos's avatar
André Anjos committed
927
928
    },
    {
929
      rocch2eer_doc.name(),
André Anjos's avatar
André Anjos committed
930
931
      (PyCFunction)rocch2eer,
      METH_VARARGS|METH_KEYWORDS,
932
      rocch2eer_doc.doc()
André Anjos's avatar
André Anjos committed
933
    },
934
    {
935
      roc_for_far_doc.name(),
936
937
      (PyCFunction)roc_for_far,
      METH_VARARGS|METH_KEYWORDS,
938
      roc_for_far_doc.doc()
939
    },
940
941
    {0}  /* Sentinel */
};
André Anjos's avatar
André Anjos committed
942

André Anjos's avatar
André Anjos committed
943
944
945
946
947
PyDoc_STRVAR(module_docstr, "Bob metrics and performance figures");

#if PY_VERSION_HEX >= 0x03000000
static PyModuleDef module_definition = {
  PyModuleDef_HEAD_INIT,
André Anjos's avatar
André Anjos committed
948
  BOB_EXT_MODULE_NAME,
André Anjos's avatar
André Anjos committed
949
950
  module_docstr,
  -1,
André Anjos's avatar
André Anjos committed
951
  module_methods,
André Anjos's avatar
André Anjos committed
952
953
954
955
  0, 0, 0, 0
};
#endif

André Anjos's avatar
André Anjos committed
956
static PyObject* create_module (void) {
André Anjos's avatar
André Anjos committed
957

André Anjos's avatar
André Anjos committed
958
959
# if PY_VERSION_HEX >= 0x03000000
  PyObject* m = PyModule_Create(&module_definition);
960
961
  auto m_ = make_xsafe(m);
  const char* ret = "O";
André Anjos's avatar
André Anjos committed
962
# else
André Anjos's avatar
André Anjos committed
963
  PyObject* m = Py_InitModule3(BOB_EXT_MODULE_NAME, module_methods, module_docstr);
964
  const char* ret = "N";
André Anjos's avatar
André Anjos committed
965
# endif
André Anjos's avatar
André Anjos committed
966
  if (!m) return 0;
André Anjos's avatar
André Anjos committed
967

André Anjos's avatar
André Anjos committed
968
969
  /* imports bob.blitz C-API + dependencies */
  if (import_bob_blitz() < 0) return 0;
970
971
  if (import_bob_core_logging() < 0) return 0;
  if (import_bob_io_base() < 0) return 0;
André Anjos's avatar
André Anjos committed
972

973
  return Py_BuildValue(ret, m);
André Anjos's avatar
André Anjos committed
974
975
}

André Anjos's avatar
André Anjos committed
976
PyMODINIT_FUNC BOB_EXT_ENTRY_NAME (void) {
André Anjos's avatar
André Anjos committed
977
# if PY_VERSION_HEX >= 0x03000000
André Anjos's avatar
André Anjos committed
978
  return
André Anjos's avatar
André Anjos committed
979
# endif
André Anjos's avatar
André Anjos committed
980
    create_module();
André Anjos's avatar
André Anjos committed
981
}