main.cpp 38.7 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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. "
  "Returns a two-dimensional :py:class:`numpy.ndarray` of type float that express the X (cost) and Y (weighted error rare on the test set given the min. threshold on the development set) coordinates in this order. "
  "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``."
)
.add_prototype("dev_negatives, dev_positives, test_negatives, test_positives, n_points, is_sorted", "curve")
.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")
.add_return("curve", "array_like(2D, float)", "The EPC curve, with the first row containing the weights, and the second row containing the weighted thresholds on the test set")
;
static PyObject* epc(PyObject*, PyObject* args, PyObject* kwds) {
BOB_TRY
61
  /* Parses input arguments in a single shot */
62
63
64
65
66
67
68
69
70
71
  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;

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&O&O&n|O",
72
73
74
75
76
        kwlist,
        &double1d_converter, &dev_neg,
        &double1d_converter, &dev_pos,
        &double1d_converter, &test_neg,
        &double1d_converter, &test_pos,
77
78
        &n_points,
        &is_sorted
79
80
        )) return 0;

81
82
83
84
85
86
  //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
87
  auto result = bob::measure::epc(
88
89
90
91
      *PyBlitzArrayCxx_AsBlitz<double,1>(dev_neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(dev_pos),
      *PyBlitzArrayCxx_AsBlitz<double,1>(test_neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(test_pos),
92
      n_points, PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
93

94
95
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("epc", 0)
96
}
André Anjos's avatar
André Anjos committed
97

98
99
100
101
102
103
104
105
106
107
108
109
110
111
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. "
  "The derivative scales are computed with the :py:func:`ppndf` function."
)
.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
112
static PyObject* det(PyObject*, PyObject* args, PyObject* kwds) {
113
114
BOB_TRY
  char** kwlist = det_doc.kwlist();
André Anjos's avatar
André Anjos committed
115

116
117
118
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
119
120
121
122
123
124
125
126

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

127
128
129
130
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
131
132
133
134
135
  auto result = bob::measure::det(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      n_points);

136
137
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("det", 0)
André Anjos's avatar
André Anjos committed
138
139
}

140
141
142
143
144
145
146
147
148
149
150
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
151
static PyObject* ppndf(PyObject*, PyObject* args, PyObject* kwds) {
152
153
154
BOB_TRY
  char** kwlist = ppndf_doc.kwlist();
  double v;
André Anjos's avatar
André Anjos committed
155
156
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "d", kwlist, &v)) return 0;

157
158
  return Py_BuildValue("d", bob::measure::ppndf(v));
BOB_CATCH_FUNCTION("ppndf", 0)
André Anjos's avatar
André Anjos committed
159
160
}

161
162
163
164
165
166
167
168
169
170
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
171
static PyObject* roc(PyObject*, PyObject* args, PyObject* kwds) {
172
173
BOB_TRY
  static char** kwlist = roc_doc.kwlist();
André Anjos's avatar
André Anjos committed
174

175
176
177
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
178
179
180
181
182
183
184
185

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

186
187
188
189
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
190
191
192
193
194
  auto result = bob::measure::roc(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      n_points);

195
196
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("roc", 0)
André Anjos's avatar
André Anjos committed
197
198
}

199
200
201
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
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
227
static PyObject* farfrr(PyObject*, PyObject* args, PyObject* kwds) {
228
229
BOB_TRY
  char** kwlist = farfrr_doc.kwlist();
André Anjos's avatar
André Anjos committed
230

231
232
233
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
234
235
236
237
238
239
240
241

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

242
243
244
245
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
246
247
248
249
250
  auto result = bob::measure::farfrr(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold);

251
252
  return Py_BuildValue("dd", result.first, result.second);
BOB_CATCH_FUNCTION("farfrr", 0)
André Anjos's avatar
André Anjos committed
253
254
}

255
256
257
258
259
260
261
262
263
264
265
266
267
268
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?")
.add_return("threshold", "float", "The threshold (i.e., as used in :py:func:`farfrr`) where FAR and FRR are as close as possible")
;
André Anjos's avatar
André Anjos committed
269
static PyObject* eer_threshold(PyObject*, PyObject* args, PyObject* kwds) {
270
271
BOB_TRY
  char** kwlist = eer_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
272

273
274
275
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
276

277
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|O",
André Anjos's avatar
André Anjos committed
278
279
        kwlist,
        &double1d_converter, &neg,
280
281
        &double1d_converter, &pos,
        &is_sorted
André Anjos's avatar
André Anjos committed
282
283
        )) return 0;

284
285
286
287
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
288
289
  double result = bob::measure::eerThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
290
291
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
292

293
294
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("eer_threshold", 0)
André Anjos's avatar
André Anjos committed
295
296
}

297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
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
314
static PyObject* min_weighted_error_rate_threshold(PyObject*, PyObject* args, PyObject* kwds) {
315
316
BOB_TRY
  char** kwlist = min_weighted_error_rate_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
317

318
319
320
321
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double cost;
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
322

323
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&d|O",
André Anjos's avatar
André Anjos committed
324
325
326
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
327
328
        &cost,
        &is_sorted
André Anjos's avatar
André Anjos committed
329
330
        )) return 0;

331
332
333
334
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
335
336
337
  double result = bob::measure::minWeightedErrorRateThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
338
339
      cost,
      PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
340

341
342
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("min_weighted_error_rate_threshold", 0)
André Anjos's avatar
André Anjos committed
343
344
}

345
346
347
348
349
350
351
352
353
static auto min_hter_threshold_doc = bob::extension::FunctionDoc(
  "min_hter_threshold",
  "Calculates the :py:func:`min_weighted_error_rate_threshold` with ``cost=0.5``"
)
.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
354
static PyObject* min_hter_threshold(PyObject*, PyObject* args, PyObject* kwds) {
355
356
BOB_TRY
  char** kwlist = min_hter_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
357

358
359
360
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
361

362
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|O",
André Anjos's avatar
André Anjos committed
363
364
        kwlist,
        &double1d_converter, &neg,
365
366
        &double1d_converter, &pos,
        &is_sorted
André Anjos's avatar
André Anjos committed
367
368
        )) return 0;

369
370
371
372
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
373
374
  double result = bob::measure::minHterThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
375
376
377
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      PyObject_IsTrue(is_sorted)
      );
André Anjos's avatar
André Anjos committed
378

379
380
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("min_hter_threshold", 0)
André Anjos's avatar
André Anjos committed
381
382
}

383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
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'). "
  "For more precise details about how the method considers error rates, see :py:func:`farfrr`."
)
.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
401
static PyObject* precision_recall(PyObject*, PyObject* args, PyObject* kwds) {
402
403
BOB_TRY
  static char** kwlist = precision_recall_doc.kwlist();
André Anjos's avatar
André Anjos committed
404

405
406
407
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
408
409
410
411
412
413
414
415

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

416
417
418
419
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
420
421
422
423
424
  auto result = bob::measure::precision_recall(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold);

425
426
  return Py_BuildValue("dd", result.first, result.second);
BOB_CATCH_FUNCTION("precision_recall", 0)
André Anjos's avatar
André Anjos committed
427
428
}

429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
static auto f_score_doc = bob::extension::FunctionDoc(
  "f_score",
  "This method computes the F-score of the accuracy of the classification",
  "The F-score is a weighted mean of precision and recall measurements, see :py:func:`precision_recall`. "
  "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
445
static PyObject* f_score(PyObject*, PyObject* args, PyObject* kwds) {
446
447
BOB_TRY
  static char** kwlist = f_score_doc.kwlist();
André Anjos's avatar
André Anjos committed
448

449
450
451
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
452
453
454
455
456
457
458
459
460
  double weight = 1.0;

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

461
462
463
464
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
465
466
467
468
469
  auto result = bob::measure::f_score(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold, weight);

470
471
  return Py_BuildValue("d",result);
BOB_CATCH_FUNCTION("f_score", 0)
André Anjos's avatar
André Anjos committed
472
473
}

474
475
476
477
478
479
480
481
482
483
484
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
485
static PyObject* correctly_classified_negatives(PyObject*, PyObject* args, PyObject* kwds) {
486
487
BOB_TRY
  static char** kwlist = correctly_classified_negatives_doc.kwlist();
André Anjos's avatar
André Anjos committed
488

489
490
  PyBlitzArrayObject* neg;
  double threshold;
André Anjos's avatar
André Anjos committed
491
492
493
494
495
496
497

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

498
499
500
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);

André Anjos's avatar
André Anjos committed
501
502
503
504
  auto result = bob::measure::correctlyClassifiedNegatives(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      threshold);

505
506
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("correctly_classified_negatives", 0)
André Anjos's avatar
André Anjos committed
507
508
}

509
510
511
512
513
514
515
516
517
518
519
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
520
static PyObject* correctly_classified_positives(PyObject*, PyObject* args, PyObject* kwds) {
521
522
BOB_TRY
  static char** kwlist = correctly_classified_positives_doc.kwlist();
André Anjos's avatar
André Anjos committed
523

524
525
  PyBlitzArrayObject* pos;
  double threshold;
André Anjos's avatar
André Anjos committed
526
527
528
529
530
531
532

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

533
534
535
  //protects acquired resources through this scope
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
536
537
538
539
  auto result = bob::measure::correctlyClassifiedPositives(
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      threshold);

540
541
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("correctly_classified_positives", 0)
André Anjos's avatar
André Anjos committed
542
543
}

544
545
546
547
548
549
550
551
552
553
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
554
static PyObject* precision_recall_curve(PyObject*, PyObject* args, PyObject* kwds) {
555
556
BOB_TRY
  char** kwlist = precision_recall_curve_doc.kwlist();
André Anjos's avatar
André Anjos committed
557

558
559
560
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
561
562
563
564
565
566
567
568

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

569
570
571
572
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
573
574
575
576
577
  auto result = bob::measure::precision_recall_curve(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
      n_points);

578
579
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("precision_recall_curve", 0)
André Anjos's avatar
André Anjos committed
580
581
}

582
583
584
585
586
587
588
589
590
591
592
593
594
595
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
596
static PyObject* far_threshold(PyObject*, PyObject* args, PyObject* kwds) {
597
598
BOB_TRY
  static char** kwlist = far_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
599

600
601
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
602
  double far_value = 0.001;
603
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
604

605
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|dO",
André Anjos's avatar
André Anjos committed
606
607
608
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
609
610
        &far_value,
        is_sorted
André Anjos's avatar
André Anjos committed
611
612
        )) return 0;

613
614
615
616
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
617
618
619
  auto result = bob::measure::farThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
620
621
622
      far_value,
      PyObject_IsTrue(is_sorted)
      );
André Anjos's avatar
André Anjos committed
623

624
625
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("far_threshold", 0)
André Anjos's avatar
André Anjos committed
626
627
}

628
629
630
631
632
633
634
635
636
637
638
639
640
641
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
642
static PyObject* frr_threshold(PyObject*, PyObject* args, PyObject* kwds) {
643
644
BOB_TRY
  char** kwlist = frr_threshold_doc.kwlist();
André Anjos's avatar
André Anjos committed
645

646
647
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
648
  double frr_value = 0.001;
649
  PyObject* is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
650

651
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|dO",
André Anjos's avatar
André Anjos committed
652
653
654
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
655
656
        &frr_value,
        &is_sorted
André Anjos's avatar
André Anjos committed
657
658
        )) return 0;

659
660
661
662
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
663
664
665
  auto result = bob::measure::frrThreshold(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
666
667
668
      frr_value,
      PyObject_IsTrue(is_sorted)
      );
André Anjos's avatar
André Anjos committed
669

670
671
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("frr_threshold", 0)
André Anjos's avatar
André Anjos committed
672
673
}

674
675
676
677
678
679
680
681
682
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
683
static PyObject* eer_rocch(PyObject*, PyObject* args, PyObject* kwds) {
684
BOB_TRY
André Anjos's avatar
André Anjos committed
685
  /* Parses input arguments in a single shot */
686
  char** kwlist = eer_rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
687

688
689
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
690
691
692
693
694
695
696

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

697
698
699
700
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
701
702
703
704
705
  auto result = bob::measure::eerRocch(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos)
      );

706
707
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("eer_rocch", 0)
André Anjos's avatar
André Anjos committed
708
709
}

710
711
712
713
714
715
716
717
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
718
static PyObject* rocch(PyObject*, PyObject* args, PyObject* kwds) {
719
BOB_TRY
André Anjos's avatar
André Anjos committed
720
  /* Parses input arguments in a single shot */
721
  char** kwlist = rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
722

723
724
  PyBlitzArrayObject* neg;
  PyBlitzArrayObject* pos;
André Anjos's avatar
André Anjos committed
725
726
727
728
729
730
731

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

732
733
734
735
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
736
737
738
739
740
  auto result = bob::measure::rocch(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos)
      );

741
742
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("rocch", 0)
André Anjos's avatar
André Anjos committed
743
744
}

745
746
747
748
749
750
751
752
753
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
754
static PyObject* rocch2eer(PyObject*, PyObject* args, PyObject* kwds) {
755
756
BOB_TRY
  static char** kwlist = rocch2eer_doc.kwlist();
André Anjos's avatar
André Anjos committed
757

758
  PyBlitzArrayObject* p;
André Anjos's avatar
André Anjos committed
759
760
761
762
763
764

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

765
  auto p_ = make_safe(p);
André Anjos's avatar
André Anjos committed
766

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

769
770
  return Py_BuildValue("d", result);
BOB_CATCH_FUNCTION("rocch2eer", 0)
André Anjos's avatar
André Anjos committed
771
772
}

773
774
775
776
777
778
779
780
781
782
783
784
785
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")
;
786
static PyObject* roc_for_far(PyObject*, PyObject* args, PyObject* kwds) {
787
BOB_TRY
788
  /* Parses input arguments in a single shot */
789
790
791
792
793
794
795
796
  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",
797
798
799
        kwlist,
        &double1d_converter, &neg,
        &double1d_converter, &pos,
800
801
        &double1d_converter, &far,
        &is_sorted
802
803
        )) return 0;

804
805
806
  //protects acquired resources through this scope
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);
807
  auto far_ = make_safe(far);
808

809
810
811
  auto result = bob::measure::roc_for_far(
      *PyBlitzArrayCxx_AsBlitz<double,1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double,1>(pos),
812
813
      *PyBlitzArrayCxx_AsBlitz<double,1>(far),
      PyObject_IsTrue(is_sorted)
814
815
      );

816
817
  return PyBlitzArrayCxx_AsNumpy(result);
BOB_CATCH_FUNCTION("roc_for_far", 0)
André Anjos's avatar
André Anjos committed
818
819
}

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

André Anjos's avatar
André Anjos committed
938
939
940
941
942
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
943
  BOB_EXT_MODULE_NAME,
André Anjos's avatar
André Anjos committed
944
945
  module_docstr,
  -1,
André Anjos's avatar
André Anjos committed
946
  module_methods,
André Anjos's avatar
André Anjos committed
947
948
949
950
  0, 0, 0, 0
};
#endif

André Anjos's avatar
André Anjos committed
951
static PyObject* create_module (void) {
André Anjos's avatar
André Anjos committed
952

André Anjos's avatar
André Anjos committed
953
954
955
# if PY_VERSION_HEX >= 0x03000000
  PyObject* m = PyModule_Create(&module_definition);
# else
André Anjos's avatar
André Anjos committed
956
  PyObject* m = Py_InitModule3(BOB_EXT_MODULE_NAME, module_methods, module_docstr);
André Anjos's avatar
André Anjos committed
957
# endif
André Anjos's avatar
André Anjos committed
958
959
  if (!m) return 0;
  auto m_ = make_safe(m); ///< protects against early returns
André Anjos's avatar
André Anjos committed
960

André Anjos's avatar
André Anjos committed
961
962
  /* imports bob.blitz C-API + dependencies */
  if (import_bob_blitz() < 0) return 0;
963
964
  if (import_bob_core_logging() < 0) return 0;
  if (import_bob_io_base() < 0) return 0;
André Anjos's avatar
André Anjos committed
965

966
  return Py_BuildValue("O", m);
André Anjos's avatar
André Anjos committed
967
968
}

André Anjos's avatar
André Anjos committed
969
PyMODINIT_FUNC BOB_EXT_ENTRY_NAME (void) {
André Anjos's avatar
André Anjos committed
970
# if PY_VERSION_HEX >= 0x03000000
André Anjos's avatar
André Anjos committed
971
  return
André Anjos's avatar
André Anjos committed
972
# endif
André Anjos's avatar
André Anjos committed
973
    create_module();
André Anjos's avatar
André Anjos committed
974
}