# -*- coding: utf-8 -*-
# This package is a translation of a part of the BOSARIS toolkit.
# The authors thank Niko Brummer and Agnitio for allowing them to
# translate this code and provide the community with efficient structures
# and tools.
#
# The BOSARIS Toolkit is a collection of functions and classes in Matlab
# that can be used to calibrate, fuse and plot scores from speaker recognition
# (or other fields in which scores are used to test the hypothesis that two
# samples are from the same source) trials involving a model and a test segment.
# The toolkit was written at the BOSARIS2010 workshop which took place at the
# University of Technology in Brno, Czech Republic from 5 July to 6 August 2010.
# See the User Guide (available on the toolkit website)1 for a discussion of the
# theory behind the toolkit and descriptions of some of the algorithms used.
#
# The BOSARIS toolkit in MATLAB can be downloaded from `the website
# <https://sites.google.com/site/bosaristoolkit/>`_.
"""
This is the 'detplot' module
This module supplies tools for ploting DET curve.
It includes a class for creating a plot for displaying detection performance
with the axes scaled and labelled so that a normal Gaussian
distribution will plot as a straight line.
The y axis represents the miss probability.
The x axis represents the false alarm probability.
This file is a translation of the BOSARIS toolkit.
For more information, refers to the license provided with this package.
"""
import copy
import matplotlib
import numpy
import os
if "DISPLAY" not in os.environ:
matplotlib.use('PDF')
import matplotlib.pyplot as mpl
import scipy
from collections import namedtuple
import logging
from . import PlotWindow
from . import Scores
from . import Key
__author__ = "Anthony Larcher"
__maintainer__ = "Anthony Larcher"
__email__ = "anthony.larcher@univ-lemans.fr"
__status__ = "Production"
__docformat__ = 'reStructuredText'
__credits__ = ["Niko Brummer", "Edward de Villiers"]
colorStyle = [
((0, 0, 0), '-', 2), # black
((0, 0, 1.0), '--', 2), # blue
((0.8, 0.0, 0.0), '-.', 2), # red
((0, 0.6, 0.0), ':', 2), # green
((0.5, 0.0, 0.5), '-', 2), # magenta
((0.3, 0.3, 0.0), '--', 2), # orange
((0, 0, 0), ':', 2), # black
((0, 0, 1.0), ':', 2), # blue
((0.8, 0.0, 0.0), ':', 2), # red
((0, 0.6, 0.0), '-', 2), # green
((0.5, 0.0, 0.5), '-.', 2), # magenta
((0.3, 0.3, 0.0), '-', 2), # orange
]
grayStyle = [
((0, 0, 0), '-', 2), # black
((0, 0, 0), '--', 2), # black
((0, 0, 0), '-.', 2), # black
((0, 0, 0), ':', 2), # black
((0.3, 0.3, 0.3), '-', 2), # gray
((0.3, 0.3, 0.3), '--', 2), # gray
((0.3, 0.3, 0.3), '-.', 2), # gray
((0.3, 0.3, 0.3), ':', 2), # gray
((0.6, 0.6, 0.6), '-', 2), # lighter gray
((0.6, 0.6, 0.6), '--', 2), # lighter gray
((0.6, 0.6, 0.6), '-.', 2), # lighter gray
((0.6, 0.6, 0.6), ':', 2), # lighter gray
]
Box = namedtuple("Box", "left right top bottom")
def effective_prior(Ptar, cmiss, cfa):
"""This function adjusts a given prior probability of target p_targ,
to incorporate the effects of a cost of miss,
cmiss, and a cost of false-alarm, cfa.
In particular note:
EFFECTIVE_PRIOR(EFFECTIVE_PRIOR(p,cmiss,cfa),1,1)
= EFFECTIVE_PRIOR(p,cfa,cmiss)
The effective prior for the NIST SRE detection cost fuction,
with p_targ = 0.01, cmiss = 10, cfa = 1 is therefore:
EFFECTIVE_PRIOR(0.01,10,1) = 0.0917
:param Ptar: is the probability of a target trial
:param cmiss: is the cost of a miss
:param cfa: is the cost of a false alarm
:return: a prior
"""
p = Ptar * cmiss / (Ptar * cmiss + (1 - Ptar) * cfa)
return p
def logit_effective_prior(Ptar, cmiss, cfa):
"""This function adjusts a given prior probability of target p_targ,
to incorporate the effects of a cost of miss,
cmiss, and a cost of false-alarm, cfa.
In particular note:
EFFECTIVE_PRIOR(EFFECTIVE_PRIOR(p,cmiss,cfa),1,1)
= EFFECTIVE_PRIOR(p,cfa,cmiss)
The effective prior for the NIST SRE detection cost fuction,
with p_targ = 0.01, cmiss = 10, cfa = 1 is therefore:
EFFECTIVE_PRIOR(0.01,10,1) = 0.0917
:param Ptar: is the probability of a target trial
:param cmiss: is the cost of a miss
:param cfa: is the cost of a false alarm
:return: a prior
"""
p = Ptar * cmiss / (Ptar * cmiss + (1 - Ptar) * cfa)
return __logit__(p)
def __probit__(p):
"""Map from [0,1] to [-inf,inf] as used to make DET out of a ROC
:param p: the value to map
:return: probit(input)
"""
y = numpy.sqrt(2) * scipy.special.erfinv(2 * p - 1)
return y
def __logit__(p):
"""logit function.
This is a one-to-one mapping from probability to log-odds.
i.e. it maps the interval (0,1) to the real line.
The inverse function is given by SIGMOID.
log_odds = logit(p) = log(p/(1-p))
:param p: the input value
:return: logit(input)
"""
p = numpy.array(p)
lp = numpy.zeros(p.shape)
f0 = p == 0
f1 = p == 1
f = (p > 0) & (p < 1)
if lp.shape == ():
if f:
lp = numpy.log(p / (1 - p))
elif f0:
lp = -numpy.inf
elif f1:
lp = numpy.inf
else:
lp[f] = numpy.log(p[f] / (1 - p[f]))
lp[f0] = -numpy.inf
lp[f1] = numpy.inf
return lp
def __DETsort__(x, col=''):
"""DETsort Sort rows, the first in ascending, the remaining in descending
thereby postponing the false alarms on like scores.
based on SORTROWS
:param x: the array to sort
:param col: not used here
:return: a sorted vector of scores
"""
assert x.ndim > 1, 'x must be a 2D matrix'
if col == '':
list(range(1, x.shape[1]))
ndx = numpy.arange(x.shape[0])
# sort 2nd column ascending
ind = numpy.argsort(x[:, 1], kind='mergesort')
ndx = ndx[ind]
# reverse to descending order
ndx = ndx[::-1]
# now sort first column ascending
ind = numpy.argsort(x[ndx, 0], kind='mergesort')
ndx = ndx[ind]
sort_scores = x[ndx, :]
return sort_scores
def __compute_roc__(true_scores, false_scores):
"""Computes the (observed) miss/false_alarm probabilities
for a set of detection output scores.
true_scores (false_scores) are detection output scores for a set of
detection trials, given that the target hypothesis is true (false).
(By convention, the more positive the score,
the more likely is the target hypothesis.)
:param true_scores: a 1D array of target scores
:param false_scores: a 1D array of non-target scores
:return: a tuple of two vectors, Pmiss,Pfa
"""
num_true = true_scores.shape[0]
num_false = false_scores.shape[0]
assert num_true > 0, "Vector of target scores is empty"
assert num_false > 0, "Vector of nontarget scores is empty"
total = num_true + num_false
Pmiss = numpy.zeros((total + 1))
Pfa = numpy.zeros((total + 1))
scores = numpy.zeros((total, 2))
scores[:num_false, 0] = false_scores
scores[:num_false, 1] = 0
scores[num_false:, 0] = true_scores
scores[num_false:, 1] = 1
scores = __DETsort__(scores)
sumtrue = numpy.cumsum(scores[:, 1], axis=0)
sumfalse = num_false - (numpy.arange(1, total + 1) - sumtrue)
Pmiss[0] = 0
Pfa[0] = 1
Pmiss[1:] = sumtrue / num_true
Pfa[1:] = sumfalse / num_false
return Pmiss, Pfa
def __filter_roc__(pm, pfa):
"""Removes redundant points from the sequence of points (pfa,pm) so
that plotting an ROC or DET curve will be faster. The output ROC
curve will be identical to the one plotted from the input
vectors. All points internal to straight (horizontal or
vertical) sections on the ROC curve are removed i.e. only the
points at the start and end of line segments in the curve are
retained. Since the plotting code draws straight lines between
points, the resulting plot will be the same as the original.
:param pm: the vector of miss probabilities of the ROC Convex
:param pfa: the vector of false-alarm probabilities of the ROC Convex
:return: a tuple of two vectors, Pmiss, Pfa
"""
out = 0
new_pm = [pm[0]]
new_pfa = [pfa[0]]
for i in range(1, pm.shape[0]):
if (pm[i] == new_pm[out]) | (pfa[i] == new_pfa[out]):
pass
else:
# save previous point, because it is the last point before the
# change. On the next iteration, the current point will be saved.
out += 1
new_pm.append(pm[i - 1])
new_pfa.append(pfa[i - 1])
out += 1
new_pm.append(pm[-1])
new_pfa.append(pfa[-1])
pm = numpy.array(new_pm)
pfa = numpy.array(new_pfa)
return pm, pfa
def pavx(y):
"""PAV: Pool Adjacent Violators algorithm.
Non-paramtetric optimization subject to monotonicity.
ghat = pav(y)
fits a vector ghat with nondecreasing components to the
data vector y such that sum((y - ghat).^2) is minimal.
(Pool-adjacent-violators algorithm).
optional outputs:
width: width of pav bins, from left to right
(the number of bins is data dependent)
height: corresponding heights of bins (in increasing order)
Author: This code is a simplified version of the 'IsoMeans.m' code
made available by Lutz Duembgen at:
http://www.imsv.unibe.ch/~duembgen/software
:param y: input value
"""
assert y.ndim == 1, 'Argument should be a 1-D array'
assert y.shape[0] > 0, 'Input array is empty'
n = y.shape[0]
index = numpy.zeros(n, dtype=int)
length = numpy.zeros(n, dtype=int)
# An interval of indices is represented by its left endpoint
# ("index") and its length "length"
ghat = numpy.zeros(n)
ci = 0
index[ci] = 0
length[ci] = 1
ghat[ci] = y[0]
# ci is the number of the interval considered currently.
# ghat(ci) is the mean of y-values within this interval.
for j in range(1, n):
# a new index interval, {j}, is created:
ci += 1
index[ci] = j + 1
length[ci] = 1
ghat[ci] = y[j]
while (ci >= 1) & (ghat[numpy.max(ci - 1, 0)] >= ghat[ci]):
# pool adjacent violators:
nw = length[ci - 1] + length[ci]
ghat[ci - 1] = ghat[ci - 1] + (length[ci] / nw) * (ghat[ci] - ghat[ci - 1])
length[ci - 1] = nw
ci -= 1
height = copy.deepcopy(ghat[:ci + 1])
width = copy.deepcopy(length[:ci + 1])
# Now define ghat for all indices:
while n >= 0:
for j in range(int(index[ci]), int(n + 1)):
ghat[j - 1] = ghat[ci]
n = index[ci] - 1
ci -= 1
return ghat, width, height
def rocch2eer(pmiss, pfa):
"""Calculates the equal error rate (eer) from pmiss and pfa vectors.
Note: pmiss and pfa contain the coordinates of the vertices of the
ROC Convex Hull.
Use rocch.m to convert target and non-target scores to pmiss and
pfa values.
:param pmiss: the vector of miss probabilities
:param pfa: the vector of false-alarm probabilities
:return: the equal error rate
"""
eer = 0
for i in range(pfa.shape[0] - 1):
xx = pfa[i:i + 2]
yy = pmiss[i:i + 2]
# xx and yy should be sorted:
assert (xx[1] <= xx[0]) & (yy[0] <= yy[1]), \
'pmiss and pfa have to be sorted'
XY = numpy.column_stack((xx, yy))
dd = numpy.dot(numpy.array([1, -1]), XY)
if numpy.min(numpy.abs(dd)) == 0:
eerseg = 0
else:
# find line coefficients seg s.t. seg'[xx(i);yy(i)] = 1,
# when xx(i),yy(i) is on the line.
seg = numpy.linalg.solve(XY, numpy.array([[1], [1]]))
# candidate for EER, eer is highest candidate
eerseg = 1 / (numpy.sum(seg))
eer = max([eer, eerseg])
return eer
def rocch(tar_scores, nontar_scores):
"""ROCCH: ROC Convex Hull.
Note: pmiss and pfa contain the coordinates of the vertices of the
ROC Convex Hull.
For a demonstration that plots ROCCH against ROC for a few cases, just
type 'rocch' at the MATLAB command line.
:param tar_scores: vector of target scores
:param nontar_scores: vector of non-target scores
:return: a tupple of two vectors: Pmiss, Pfa
"""
Nt = tar_scores.shape[0]
Nn = nontar_scores.shape[0]
N = Nt + Nn
scores = numpy.concatenate((tar_scores, nontar_scores))
# Pideal is the ideal, but non-monotonic posterior
Pideal = numpy.concatenate((numpy.ones(Nt), numpy.zeros(Nn)))
#
# It is important here that scores that are the same
# (i.e. already in order) should NOT be swapped.rb
perturb = numpy.argsort(scores, kind='mergesort')
#
Pideal = Pideal[perturb]
Popt, width, foo = pavx(Pideal)
#
nbins = width.shape[0]
pmiss = numpy.zeros(nbins + 1)
pfa = numpy.zeros(nbins + 1)
#
# threshold leftmost: accept everything, miss nothing
left = 0 # 0 scores to left of threshold
fa = Nn
miss = 0
#
for i in range(nbins):
pmiss[i] = miss / Nt
pfa[i] = fa / Nn
left = int(left + width[i])
miss = numpy.sum(Pideal[:left])
fa = N - left - numpy.sum(Pideal[left:])
#
pmiss[nbins] = miss / Nt
pfa[nbins] = fa / Nn
#
return pmiss, pfa
def sigmoid(log_odds):
"""SIGMOID: Inverse of the logit function.
This is a one-to-one mapping from log odds to probability.
i.e. it maps the real line to the interval (0,1).
p = sigmoid(log_odds)
:param log_odds: the input value
:return: sigmoid(input)
"""
p = 1 / (1 + numpy.exp(-log_odds))
return p
def fast_minDCF(tar, non, plo, normalize=False):
"""Compute the minimum COST for given target and non-target scores
Note that minDCF is parametrized by plo:
minDCF(Ptar) = min_t Ptar * Pmiss(t) + (1-Ptar) * Pfa(t)
where t is the adjustable decision threshold and:
Ptar = sigmoid(plo) = 1./(1+exp(-plo))
If normalize == true, then the returned value is:
minDCF(Ptar) / min(Ptar,1-Ptar).
Pmiss: a vector with one value for every element of plo.
This is Pmiss(tmin), where tmin is the minimizing threshold
for minDCF, at every value of plo. Pmiss is not altered by
parameter 'normalize'.
Pfa: a vector with one value for every element of plo.
This is Pfa(tmin), where tmin is the minimizing threshold for
minDCF, at every value of plo. Pfa is not altered by
parameter 'normalize'.
Note, for the un-normalized case:
minDCF(plo) = sigmoid(plo).*Pfa(plo) + sigmoid(-plo).*Pmiss(plo)
:param tar: vector of target scores
:param non: vector of non-target scores
:param plo: vector of prior-log-odds: plo = logit(Ptar) = log(Ptar) - log(1-Ptar)
:param normalize: if true, return normalized minDCF, else un-normalized (optional, default = false)
:return: the minDCF value
:return: the miss probability for this point
:return: the false-alarm probability for this point
:return: the precision-recall break-even point: Where #FA == #miss
:return the equal error rate
"""
Pmiss, Pfa = rocch(tar, non)
Nmiss = Pmiss * tar.shape[0]
Nfa = Pfa * non.shape[0]
prbep = rocch2eer(Nmiss, Nfa)
eer = rocch2eer(Pmiss, Pfa)
Ptar = sigmoid(plo)
Pnon = sigmoid(-plo)
cdet = numpy.dot(numpy.array([[Ptar, Pnon]]), numpy.vstack((Pmiss, Pfa)))
ii = numpy.argmin(cdet, axis=1)
minDCF = cdet[0, ii][0]
Pmiss = Pmiss[ii]
Pfa = Pfa[ii]
if normalize:
minDCF = minDCF / min([Ptar, Pnon])
return minDCF, Pmiss[0], Pfa[0], prbep, eer
def plotseg(xx, yy, box, dps):
"""Prepare the plotting of a curve.
:param xx:
:param yy:
:param box:
:param dps:
"""
assert ((xx[1] <= xx[0]) & (yy[0] <= yy[1])), 'xx and yy should be sorted'
XY = numpy.column_stack((xx, yy))
dd = numpy.dot(numpy.array([1, -1]), XY)
if numpy.min(abs(dd)) == 0:
eer = 0
else:
# find line coefficients seg s.t. seg'[xx(i);yy(i)] = 1,
# when xx(i),yy(i) is on the line.
seg = numpy.linalg.solve(XY, numpy.array([[1], [1]]))
# candidate for EER, eer is highest candidate
eer = 1.0 / numpy.sum(seg)
# segment completely outside of box
if (xx[0] < box.left) | (xx[1] > box.right) | (yy[1] < box.bottom) | (yy[0] > box.top):
x = numpy.array([])
y = numpy.array([])
else:
if xx[1] < box.left:
xx[1] = box.left
yy[1] = (1 - seg[0] * box.left) / seg[1]
if xx[0] > box.right:
xx[0] = box.right
yy[0] = (1 - seg[0] * box.right) / seg[1]
if yy[0] < box.bottom:
yy[0] = box.bottom
xx[0] = (1 - seg[1] * box.bottom) / seg[0]
if yy[1] > box.top:
yy[1] = box.top
xx[1] = (1 - seg[1] * box.top) / seg[0]
dx = xx[1] - xx[0]
xdots = xx[0] + dx * numpy.arange(dps + 1) / dps
ydots = (1 - seg[0] * xdots) / seg[1]
x = __probit__(xdots)
y = __probit__(ydots)
return x, y, eer
def rocchdet(tar, non,
dcfweights=numpy.array([]),
pfa_min=5e-4,
pfa_max=0.5,
pmiss_min=5e-4,
pmiss_max=0.5,
dps=100,
normalize=False):
"""ROCCHDET: Computes ROC Convex Hull and then maps that to the DET axes.
The DET-curve is infinite, non-trivial limits (away from 0 and 1)
are mandatory.
:param tar: vector of target scores
:param non: vector of non-target scores
:param dcfweights: 2-vector, such that: DCF = [pmiss,pfa]*dcfweights(:) (Optional, provide only if mindcf is
desired, otherwise omit or use []
:param pfa_min: limit of DET-curve rectangle. Default is 0.0005
:param pfa_max: limit of DET-curve rectangle. Default is 0.5
:param pmiss_min: limit of DET-curve rectangle. Default is 0.0005
:param pmiss_max: limits of DET-curve rectangle. Default is 0.5
:param dps: number of returned (x,y) dots (arranged in a curve) in DET space, for every straight line-segment
(edge) of the ROC Convex Hull. Default is 100.
:param normalize: normalize the curve
:return: probit(Pfa)
:return: probit(Pmiss)
:return: ROCCH EER = max_p mindcf(dcfweights=[p,1-p]), which is also equal to the intersection of the ROCCH
with the line pfa = pmiss.
:return: the mindcf: Identical to result using traditional ROC, but computed by mimimizing over the ROCCH
vertices, rather than over all the ROC points.
"""
assert ((pfa_min > 0) & (pfa_max < 1) & (pmiss_min > 0) & (pmiss_max < 1)), 'limits must be strictly inside (0,1)'
assert((pfa_min < pfa_max) & (pmiss_min < pmiss_max)), 'pfa and pmiss min and max values are not consistent'
pmiss, pfa = rocch(tar, non)
mindcf = 0.0
if dcfweights.shape == (2,):
dcf = numpy.dot(dcfweights, numpy.vstack((pmiss, pfa)))
mindcf = numpy.min(dcf)
if normalize:
mindcf = mindcf/min(dcfweights)
# pfa is decreasing
# pmiss is increasing
box = Box(left=pfa_min, right=pfa_max, top=pmiss_max, bottom=pmiss_min)
x = []
y = []
eer = 0
for i in range(pfa.shape[0] - 1):
xx = pfa[i:i + 2]
yy = pmiss[i:i + 2]
xdots, ydots, eerseg = plotseg(xx, yy, box, dps)
x = x + xdots.tolist()
y = y + ydots.tolist()
eer = max(eer, eerseg)
return numpy.array(x), numpy.array(y), eer, mindcf
[docs]class DetPlot:
"""A class for creating a plot for displaying detection performance
with the axes scaled and labelled so that a normal Gaussian
distribution will plot as a straight line.
- The y axis represents the miss probability.
- The x axis represents the false alarm probability.
:attr __plotwindow__: PlotWindow object to plot into
:attr __title__: title of the plot
:attr __sys_name__: list of IDs of the systems
:attr __tar__: list of arrays of of target scores for each system
:attr __non__: list of arrays of the non-target scores for each system
:attr __figure__: figure to plot into
"""
def __init__(self, window_style='old', plot_title=''):
"""Initialize an empty DetPlot object"""
self.__plotwindow__ = PlotWindow(window_style)
self.__title__ = plot_title
self.__sys_name__ = []
self.__tar__ = []
self.__non__ = []
self.__figure__ = ''
self.title = ''
[docs] def set_title(self, title):
"""Modify the title of a DetPlot object
:param title: title of the plot to display
"""
self.title = title
[docs] def set_system(self, tar, non, sys_name=''):
"""Sets the scores to be plotted. This function must be called
before plots are made for a system, but it can be called several
times with different systems (with calls to plotting functions in
between) so that curves for different systems appear on the same plot.
:param tar: A vector of target scores.
:param non: A vector of non-target scores.
:param sys_name: A string describing the system. This string will
be prepended to the plot names in the legend.
You can pass an empty string to this argument or omit it.
"""
assert tar.ndim == 1, 'Vector of target scores should be 1-dimensional'
assert non.ndim == 1, 'Vector of nontarget scores should be 1-dimensional'
assert tar.shape[0] > 0, 'Vector of target scores is empty'
assert non.shape[0] > 0, 'Vector of nontarget scores is empty'
self.__sys_name__.append(sys_name)
self.__tar__.append(tar)
self.__non__.append(non)
[docs] def set_system_from_scores(self, scores, key, sys_name=''):
"""Sets the scores to be plotted. This function must be called
before plots are made for a system, but it can be called several
times with different systems (with calls to plotting functions in
between) so that curves for different systems appear on the same plot.
:param scores: A Scores object containing system scores.
:param key: A Key object for distinguishing target and non-target scores.
:param sys_name: A string describing the system. This string will be
prepended to the plot names in the legend. You can pass an
empty string to this argument or omit it.
"""
assert isinstance(scores, Scores), 'First argument should be a Score object'
assert isinstance(key, Key), 'Second argument should be a Key object'
assert scores.validate(), 'Wrong format of Scores'
assert key.validate(), 'Wrong format of Key'
tar, non = scores.get_tar_non(key)
self.set_system(tar, non, sys_name)
[docs] def plot_steppy_det(self, idx=0, style='color', plot_args=''):
"""Plots a DET curve.
:param idx: the idx of the curve to plot in case tar and non have
several dimensions
:param style: style of the curve, can be gray or color
:param plot_args: a cell array of arguments to be passed to plot
that control the appearance of the curve.
"""
Pmiss, Pfa = __compute_roc__(self.__tar__[idx], self.__non__[idx])
Pmiss, Pfa = __filter_roc__(Pmiss, Pfa)
x = __probit__(Pfa)
y = __probit__(Pmiss)
# In case the plotting arguments are not specified, they are initialized
# by using default values
if not (isinstance(plot_args, tuple) & (len(plot_args) == 3)):
if style == 'gray':
plot_args = grayStyle[idx]
else:
plot_args = colorStyle[idx]
fig = mpl.plot(x, y,
label=self.__sys_name__[idx],
color=plot_args[0],
linestyle=plot_args[1],
linewidth=plot_args[2])
mpl.legend()
if matplotlib.get_backend() == 'agg':
mpl.savefig(self.__title__ + '.pdf')
[docs] def plot_rocch_det(self, idx=0, style='color', target_prior=0.001, plot_args=''):
"""Plots a DET curve using the ROCCH.
:param idx: index of the figure to plot on
:param style: style of the DET-curve (see DetPlot description)
:param target_prior: prior of the target trials
:param plot_args: a list of arguments to be passed
to plot that control the appearance of the curve.
"""
# In case the plotting arguments are not specified, they are initialized
# by using default values
if not (isinstance(plot_args, tuple) & (len(plot_args) == 3)):
if style == 'gray':
plot_args = grayStyle[idx]
else:
plot_args = colorStyle[idx]
pfa_min = self.__plotwindow__.__pfa_limits__[0]
pfa_max = self.__plotwindow__.__pfa_limits__[1]
pmiss_min = self.__plotwindow__.__pmiss_limits__[0]
pmiss_max = self.__plotwindow__.__pmiss_limits__[1]
dps = 100 # dots per segment
tmp = numpy.array([sigmoid(__logit__(target_prior)), sigmoid(-__logit__(target_prior))])
x, y, eer, mindcf = rocchdet(self.__tar__[idx], self.__non__[idx],
tmp, pfa_min, pfa_max,
pmiss_min, pmiss_max, dps, normalize=True)
fig = mpl.plot(x, y,
label='{}; (eer; minDCF) = ({:.03}; {:.04})'.format(
self.__sys_name__[idx],
100. * eer,
100. * mindcf),
color=plot_args[0],
linestyle=plot_args[1],
linewidth=plot_args[2])
mpl.legend()
if matplotlib.get_backend() == 'agg':
mpl.savefig(self.__title__ + '.pdf')
[docs] def plot_mindcf_point(self, target_prior, idx=0, plot_args='ok', legend_string=''):
"""Places the mindcf point for the current system.
:param target_prior: The effective target prior.
:param idx: inde of the figure to plot in
:param plot_args: a list of arguments to be
passed to 'plot' that control the appearance of the curve.
:param legend_string: Optional. A string to describe this curve
in the legend.
"""
mindcf, pmiss, pfa, prbep, eer = fast_minDCF(self.__tar__[idx],
self.__non__[idx], __logit__(target_prior), True)
if (pfa < self.__plotwindow__.__pfa_limits__[0]) | (pfa > self.__plotwindow__.__pfa_limits__[1]):
logging.warning('pfa of %f is not between %f and %f mindcf point will not be plotted.', format(pfa),
self.__plotwindow__.__pfa_limits__[0],
self.__plotwindow__.__pfa_limits__[1])
elif (pmiss < self.__plotwindow__.__pmiss_limits__[0]) | (pmiss > self.__plotwindow__.__pmiss_limits__[1]):
logging.warning('pmiss of %f is not between %f and %f. The mindcf point will not be plotted.',
pmiss, self.__plotwindow__.__pmiss_limits__[0],
self.__plotwindow__.__pmiss_limits__[1])
else:
fig = mpl.plot(__probit__(pfa), __probit__(pmiss), plot_args)
if matplotlib.get_backend() == 'agg':
mpl.savefig(self.__title__ + '.pdf')
[docs] def plot_DR30_fa(self,
idx=0,
plot_args=((0, 0, 0), '--', 1),
legend_string=''):
"""Plots a vertical line indicating the Doddington 30 point for
false alarms. This is the point left of which the number of false
alarms is below 30, so that the estimate of the false alarm rate
is no longer good enough to satisfy Doddington's Rule of 30.
:param idx: index of the figure to plot in
:param plot_args: A cell array of arguments to be passed to 'plot'
that control the appearance of the curve.
:param legend_string: Optional. A string to describe this curve
in the legend.
"""
assert isinstance(plot_args, tuple) & (len(plot_args) == 3), 'Invalid plot_args'
pfa_min = self.__plotwindow__.__pfa_limits__[0]
pfa_max = self.__plotwindow__.__pfa_limits__[1]
pmiss_min = self.__plotwindow__.__pmiss_limits__[0]
pmiss_max = self.__plotwindow__.__pmiss_limits__[1]
pfaval = 30.0 / self.__non__[idx].shape[0]
if (pfaval < pfa_min) | (pfaval > pfa_max):
logging.warning('Pfa DR30 of %f is not between %f and %f Pfa DR30 line will not be plotted.',
pfaval, pfa_min, pfa_max)
else:
drx = __probit__(pfaval)
mpl.axvline(drx,
ymin=__probit__(pmiss_min),
ymax=__probit__(pmiss_max),
color=plot_args[0],
linestyle=plot_args[1],
linewidth=plot_args[2],
label=legend_string)
[docs] def plot_DR30_miss(self,
idx=0,
plot_args=((0, 0, 0), '--', 1),
legend_string=''):
"""Plots a horizontal line indicating the Doddington 30 point for
misses. This is the point above which the number of misses is
below 30, so that the estimate of the miss rate is no longer good
enough to satisfy Doddington's Rule of 30.
:param idx: index of the figure to plot in
:param plot_args: A cell array of arguments to be passed
to 'plot' that control the appearance of the curve.
:param legend_string: Optional. A string to describe this curve
in the legend.
"""
assert isinstance(plot_args, tuple) & (len(plot_args) == 3), 'Invalid plot_args'
pfa_min = self.__plotwindow__.__pfa_limits__[0]
pfa_max = self.__plotwindow__.__pfa_limits__[1]
pmiss_min = self.__plotwindow__.__pmiss_limits__[0]
pmiss_max = self.__plotwindow__.__pmiss_limits__[1]
pmissval = 30.0 / self.__tar__[idx].shape[0]
if (pmissval < pmiss_min) | (pmissval > pmiss_max):
logging.warning('Pmiss DR30 of is not between %f and %f Pfa DR30 line will not be plotted.',
pmissval, pmiss_min, pmiss_max)
else:
dry = __probit__(pmissval)
mpl.axhline(y=dry,
xmin=__probit__(pfa_min),
xmax=__probit__(pfa_max),
color=plot_args[0],
linestyle=plot_args[1],
linewidth=plot_args[2],
label=legend_string)
[docs] def plot_DR30_both(self,
idx=0,
plot_args_fa=((0, 0, 0), '--', 1),
plot_args_miss=((0, 0, 0), '--', 1),
legend_string=''):
"""Plots two lines indicating Doddington's Rule of 30 points: one
for false alarms and one for misses. See the documentation of
plot_DR30_fa and plot_DR30_miss for details.
:param idx: index of the figure to plot in
:param plot_args_fa: A tuple of arguments to be passed to 'plot' that control
the appearance of the DR30_fa point.
:param plot_args_miss: A tuple of arguments to be passed to 'plot' that control
the appearance of the DR30_miss point.
:param legend_string: Optional. A string to describe this curve
in the legend.
"""
self.plot_DR30_fa(idx, plot_args_fa, 'pfa DR30')
self.plot_DR30_miss(idx, plot_args_miss, 'pmiss DR30')
def display_legend(self):
# to complete
pass
def save_as_pdf(self, outfilename):
# to complete
pass
def add_legend_entry(self, lh, legend_string, append_name):
# to complete
pass