Source code for methcomp.glucose

import matplotlib.pyplot as plt
import matplotlib
import numpy as np
from shapely.geometry import Polygon, Point
try:
     import importlib.resources as pkg_resources
     from importlib.resources import path

     def path_func(pkg, file):
         with path(pkg, file) as p:
             return p

except ImportError:
    # Try backported to PY<37 `importlib_resources`.
    import importlib_resources as pkg_resources

    def path_func(pkg, file):
        return pkg_resources.files(pkg).joinpath(file)

from . import static

__all__ = ["clarke", "parkes", "seg",
           "clarkezones", "parkeszones", "segscores"]


class _Clarke(object):
    """Internal class for drawing a Clarke-Error grid plotting"""

    def __init__(self, reference, test, units,
                 x_title, y_title, graph_title,
                 xlim, ylim,
                 color_grid, color_gridlabels, color_points,
                 grid, percentage,
                 point_kws, grid_kws):
        # variables assignment
        self.reference: np.array = np.asarray(reference)
        self.test: np.array = np.asarray(test)
        self.units = units
        self.graph_title: str = graph_title
        self.x_title: str = x_title
        self.y_title: str = y_title
        self.xlim: list = xlim
        self.ylim: list = ylim
        self.color_grid: str = color_grid
        self.color_gridlabels: str = color_gridlabels
        self.color_points: str = color_points
        self.grid: bool = grid
        self.percentage: bool = percentage
        self.point_kws = {} if point_kws is None else point_kws.copy()
        self.grid_kws = {} if grid_kws is None else grid_kws.copy()

        self._check_params()
        self._derive_params()

    def _check_params(self):
        if len(self.reference) != len(self.test):
            raise ValueError('Length of reference and test values are not equal')

        if self.units not in ['mmol', 'mg/dl', 'mgdl']:
            raise ValueError('The provided units should be one of the following: mmol, mgdl or mg/dl.')

        if any([x is not None and not isinstance(x, str) for x in [self.x_title, self.y_title]]):
            raise ValueError('Axes labels arguments should be provided as a str.')


    def _derive_params(self):
        if self.x_title is None:
            _unit = 'mmol/L' if 'mmol' else 'mg/dL'
            self.x_title = 'Reference glucose concentration ({})'.format(_unit)

        if self.y_title is None:
            _unit = 'mmol/L' if 'mmol' else 'mg/dL'
            self.y_title = 'Predicted glucose concentration ({})'.format(_unit)

        self.xlim = self.xlim or [0, 400]
        self.ylim = self.ylim or [0, 400]

    def _calc_error_zone(self):
        # ref, pred
        ref = self.reference
        pred = self.test

        # calculate conversion factor if needed
        n = 18 if self.units == 'mmol' else 1

        # we initialize an array with ones
        # this in fact very smart because all the non-matching values will automatically
        # end up in zone B (which is 1)!
        _zones = np.ones(len(ref))

        # absolute relative error = abs(bias)/reference*100
        bias = pred - ref
        are = abs(bias) / ref * 100
        eq1 = (7 / 5) * (ref - 130 / n)
        eq2 = ref + 110 / n

        # zone E: (ref <= 70 and test >= 180) or (ref >=180 and test <=70)
        zone_e = ((ref<= 70 / n) & (pred >= 180 / n)) | ((ref >= 180 / n) & (pred <= 70 / n))
        _zones[zone_e] = 4

        # zone D: ref < 70 and (test > 70 and test < 180) or
        #   ref > 240 and (test > 70 and test < 180)
        test_d = (pred >= 70 / n) & (pred < 180 / n)  # error corrected >=70 instead of >70
        zone_d = ((ref < 70 / n) & test_d) | ((ref > 240 / n) & test_d)
        _zones[zone_d] = 3

        # zone C: (ref >= 130 and ref <= 180 and test < eq1) or
        #   (ref > 70 and ref > 180 and ref > eq2)
        zone_c = ((ref >= 130 / n) & (ref <= 180 / n) & (pred < eq1)) | ((ref > 70 / n) & (pred > 180 / n) & (pred > eq2))
        _zones[zone_c] = 2

        # zone A: are <= 20  or (ref < 58.3 and test < 70)
        zone_a = (are <= 20) | ((ref < 70 / n) & (pred < 70 / n))
        _zones[zone_a] = 0

        return [int(i) for i in _zones]

    def plot(self, ax):
        _gridlines = [
            ([0, 400], [0, 400], ':'),
            ([0, 175 / 3], [70, 70], '-'),
            ([175 / 3, 400 / 1.2], [70, 400], '-'),
            ([70, 70], [84, 400], '-'),
            ([0, 70], [180, 180], '-'),
            ([70, 290], [180, 400], '-'),
            ([70, 70], [0, 56], '-'),
            ([70, 400], [56, 320], '-'),
            ([180, 180], [0, 70], '-'),
            ([180, 400], [70, 70], '-'),
            ([240, 240], [70, 180], '-'),
            ([240, 400], [180, 180], '-'),
            ([130, 180], [0, 70], '-')
        ]

        colors = ['#196600', '#7FFF00', '#FF7B00', '#FF5700', '#FF0000']

        _gridlabels = [
            (30, 15, "A", colors[0]),
            (370, 260, "B", colors[1]),
            (280, 370, "B", colors[1]),
            (160, 370, "C", colors[2]),
            (160, 15, "C", colors[2]),
            (30, 140, "D", colors[3]),
            (370, 120, "D", colors[3]),
            (30, 370, "E", colors[4]),
            (370, 15, "E", colors[4]),
        ]

        # calculate conversion factor if needed
        n = 18 if self.units == 'mmol' else 1

        # plot individual points
        if self.color_points == 'auto':
            ax.scatter(self.reference,
                       self.test,
                       marker='o',
                       alpha=0.6,
                       c=[colors[i] for i in self._calc_error_zone()], s=8,
                       **self.point_kws)
        else:
            ax.scatter(self.reference,
                       self.test, marker='o',
                       color=self.color_points,
                       alpha=0.6,
                       s=8,
                       **self.point_kws)

        # plot grid lines
        if self.grid:
            for g in _gridlines:
                ax.plot(np.array(g[0])/n,
                        np.array(g[1])/n,
                        g[2], color=self.color_grid,
                        **self.grid_kws)

            if self.percentage:
                zones = [['A', 'B', 'C', 'D', 'E'][i] for i in self._calc_error_zone()]

                for l in _gridlabels:
                    ax.text(l[0] / n, l[1] / n, l[2],
                            fontsize=12,
                            fontweight='bold',
                            color=l[3] if self.color_gridlabels == 'auto' else self.color_gridlabels)
                    ax.text(l[0] / n + (8 / n),
                            l[1] / n + (8 / n),
                            "{:.1f}".format((zones.count(l[2]) / len(zones)) * 100),
                            fontsize=9,
                            fontweight='bold',
                            color=l[3] if self.color_gridlabels == 'auto' else self.color_gridlabels)

            else:
                for l in _gridlabels:
                    ax.text(l[0] / n, l[1] / n, l[2],
                            fontsize=12,
                            fontweight='bold',
                            color=l[3] if self.color_gridlabels == 'auto' else self.color_gridlabels)

        # limits and ticks
        ax.set_xlim(self.xlim[0]/n, self.xlim[1]/n)
        ax.set_ylim(self.ylim[0]/n, self.ylim[1]/n)

        # graph labels
        ax.set_ylabel(self.y_title)
        ax.set_xlabel(self.x_title)
        if self.graph_title is not None:
            ax.set_title(self.graph_title)

[docs]def clarke(reference, test, units, x_label=None, y_label=None, title=None, xlim=None, ylim=None, color_grid='#000000', color_gridlabels='auto', color_points='auto', grid=True, percentage=False, point_kws=None, grid_kws=None, square=False, ax=None): """Provide a glucose error grid analyses as designed by Clarke. This is an Axis-level function which will draw the Clarke-error grid plot. onto the current active Axis object unless ``ax`` is provided. Parameters ---------- reference, test : array, or list Glucose values obtained from the reference and predicted methods, preferably provided in a np.array. units : str The SI units which the glucose values are provided in. Options: 'mmol', 'mgdl' or 'mg/dl'. x_label : str, optional The label which is added to the X-axis. If None is provided, a standard label will be added. y_label : str, optional The label which is added to the Y-axis. If None is provided, a standard label will be added. title : str, optional Title of the Clarke error grid plot. If None is provided, no title will be plotted. xlim : list, optional Minimum and maximum limits for X-axis. Should be provided as list or tuple. If not set, matplotlib will decide its own bounds. ylim : list, optional Minimum and maximum limits for Y-axis. Should be provided as list or tuple. If not set, matplotlib will decide its own bounds. color_grid : str, optional Color of the Clarke error grid lines. color_gridlabels : str, optional Color of the gridlabels (A, B, C, ..) that will be plotted. If set to 'auto', it will plot the points according to their zones. color_points : str, optional Color of the individual differences that will be plotted. If set to 'auto', it will plot the points according to their zones. grid : bool, optional Enable the grid lines of the Parkes error. Defaults to True. percentage : bool, optional If True, percentage of the zones will be depicted in the plot. point_kws : dict of key, value mappings, optional Additional keyword arguments for `plt.scatter`. grid_kws : dict of key, value mappings, optional Additional keyword arguments for the grid with `plt.plot`. square : bool, optional If True, set the Axes aspect to "equal" so each cell will be square-shaped. ax : matplotlib Axes, optional Axes in which to draw the plot, otherwise use the currently-active Axes. Returns ------- ax : matplotlib Axes Axes object with the Clarke-error grid plot. See Also ------- Clarke, W. L., Cox, D., et al. Diabetes Care, vol. 10, no. 5, 1987, pp. 622–628. """ plotter: _Clarke = _Clarke(reference, test, units, x_label, y_label, title, xlim, ylim, color_grid, color_gridlabels, color_points, grid, percentage, point_kws, grid_kws) # Draw the plot and return the Axes if ax is None: ax = plt.gca() if square: ax.set_aspect('equal') plotter.plot(ax) return ax
[docs]def clarkezones(reference, test, units, numeric=False): """Provides the error zones as depicted by the Clarke error grid analysis for each point in the reference and test datasets. Parameters ---------- reference, test : array, or list Glucose values obtained from the reference and predicted methods, preferably provided in a np.array. units : str The SI units which the glucose values are provided in. Options: 'mmol', 'mgdl' or 'mg/dl'. numeric : bool, optional If this is set to true, returns integers (0 to 4) instead of characters for each of the zones. Returns ------- clarkezones : list Returns a list depecting the zones for each of the reference and test values. """ # obtain zones from a Clarke reference object _zones = _Clarke(reference, test, units, None, None, None, None, None, '#000000', 'auto', 'auto', True, False, None, None)._calc_error_zone() if numeric: return _zones else: labels = ['A', 'B', 'C', 'D', 'E'] return [labels[i] for i in _zones]
class _Parkes(object): """Internal class for drawing a Parkes consensus error grid plot""" def __init__(self, type, reference, test, units, x_title, y_title, graph_title, xlim, ylim, color_grid, color_gridlabels, color_points, grid, percentage, point_kws, grid_kws): # variables assignment self.type: int = type self.reference: np.array = np.asarray(reference) self.test: np.array = np.asarray(test) self.units = units self.graph_title: str = graph_title self.x_title: str = x_title self.y_title: str = y_title self.xlim: list = xlim self.ylim: list = ylim self.color_grid: str = color_grid self.color_gridlabels: str = color_gridlabels self.color_points: str = color_points self.grid: bool = grid self.percentage: bool = percentage self.point_kws = {} if point_kws is None else point_kws.copy() self.grid_kws = {} if grid_kws is None else grid_kws.copy() self._check_params() self._derive_params() def _check_params(self): if self.type != 1 and self.type != 2: raise ValueError('Type of Diabetes should either be 1 or 2.') if len(self.reference) != len(self.test): raise ValueError('Length of reference and test values are not equal') if self.units not in ['mmol', 'mg/dl', 'mgdl']: raise ValueError('The provided units should be one of the following: mmol, mgdl or mg/dl.') if any([x is not None and not isinstance(x, str) for x in [self.x_title, self.y_title]]): raise ValueError('Axes labels arguments should be provided as a str.') def _derive_params(self): if self.x_title is None: _unit = 'mmol/L' if 'mmol' else 'mg/dL' self.x_title = 'Reference glucose concentration ({})'.format(_unit) if self.y_title is None: _unit = 'mmol/L' if 'mmol' else 'mg/dL' self.y_title = 'Predicted glucose concentration ({})'.format(_unit) def _coef(self, x, y, xend, yend): if xend == x: raise ValueError('Vertical line - function inapplicable') return (yend - y) / (xend - x) def _endy(self, startx, starty, maxx, coef): return (maxx - startx) * coef + starty def _endx(self, startx, starty, maxy, coef): return (maxy - starty) / coef + startx def _calc_error_zone(self): # ref, pred ref = self.reference pred = self.test # calculate conversion factor if needed n = 18 if self.units == 'mmol' else 1 maxX = max(max(ref) + 20 / n, 550 / n) maxY = max([*(np.array(pred) + 20 / n), maxX, 550 / n]) # we initialize an array with ones # this in fact very smart because all the non-matching values will automatically # end up in zone A (which is zero) _zones = np.zeros(len(ref)) if self.type == 1: ce = self._coef(35, 155, 50, 550) cdu = self._coef(80, 215, 125, 550) cdl = self._coef(250, 40, 550, 150) ccu = self._coef(70, 110, 260, 550) ccl = self._coef(260, 130, 550, 250) cbu = self._coef(280, 380, 430, 550) cbl = self._coef(385, 300, 550, 450) limitE1 = Polygon([(x, y) for x, y in zip([0, 35 / n, self._endx(35 / n, 155 / n, maxY, ce), 0, 0], [150 / n, 155 / n, maxY, maxY, 150 / n])]) limitD1L = Polygon([(x, y) for x, y in zip([250 / n, 250 / n, maxX, maxX, 250 / n], [0, 40 / n, self._endy(410 / n, 110 / n, maxX, cdl), 0, 0])]) limitD1U= Polygon([(x, y) for x, y in zip([0, 25 / n, 50 / n, 80 / n, self._endx(80 / n, 215 / n, maxY, cdu), 0, 0], [100 / n, 100 / n, 125 / n, 215 / n, maxY, maxY, 100 / n])]) limitC1L = Polygon([(x, y) for x, y in zip([120 / n, 120 / n, 260 / n, maxX, maxX, 120 / n], [0, 30 / n, 130 / n, self._endy(260 / n, 130 / n, maxX, ccl), 0, 0])]) limitC1U = Polygon([(x, y) for x, y in zip([0, 30 / n, 50 / n, 70 / n, self._endx(70 / n, 110 / n, maxY, ccu), 0, 0], [60 / n, 60 / n, 80 / n, 110 / n, maxY, maxY, 60 / n])]) limitB1L = Polygon([(x, y) for x, y in zip([50 / n, 50 / n, 170 / n, 385 / n, maxX, maxX, 50 / n], [0, 30 / n, 145 / n, 300 / n, self._endy(385 / n, 300 / n, maxX, cbl), 0, 0])]) limitB1U = Polygon([(x, y) for x, y in zip([0, 30 / n, 140 / n, 280 / n, self._endx(280 / n, 380 / n, maxY, cbu), 0, 0], [50 / n, 50 / n, 170 / n, 380 / n, maxY, maxY, 50 / n])]) for i, points in enumerate(zip(ref, pred)): for f, r in zip([limitB1L, limitB1U, limitC1L, limitC1U, limitD1L, limitD1U, limitE1], [1, 1, 2, 2, 3, 3, 4]): if f.contains(Point(points[0], points[1])): _zones[i] = r return [int(i) for i in _zones] elif self.type == 2: ce = self._coef(35, 200, 50, 550) cdu = self._coef(35, 90, 125, 550) cdl = self._coef(410, 110, 550, 160) ccu = self._coef(30, 60, 280, 550) ccl = self._coef(260, 130, 550, 250) cbu = self._coef(230, 330, 440, 550) cbl = self._coef(330, 230, 550, 450) limitE2 = Polygon([(x, y) for x, y in zip([0, 35 / n, self._endx(35 / n, 200 / n, maxY, ce), 0, 0], # x limits E upper [200 / n, 200 / n, maxY, maxY, 200 / n])]) # y limits E upper limitD2L = Polygon([(x, y) for x, y in zip([250 / n, 250 / n, 410 / n, maxX, maxX, 250 / n], # x limits D lower [0, 40 / n, 110 / n, self._endy(410 / n, 110 / n, maxX, cdl), 0, 0])]) # y limits D lower limitD2U = Polygon([(x, y) for x, y in zip([0, 25 / n, 35 / n, self._endx(35 / n, 90 / n, maxY, cdu), 0, 0], # x limits D upper [80 / n, 80 / n, 90 / n, maxY, maxY, 80 / n])]) # y limits D upper limitC2L = Polygon([(x, y) for x, y in zip([90 / n, 260 / n, maxX, maxX, 90 / n], # x limits C lower [0, 130 / n, self._endy(260 / n, 130 / n, maxX, ccl), 0, 0])]) # y limits C lower limitC2U = Polygon([(x, y) for x, y in zip([0, 30 / n, self._endx(30 / n, 60 / n, maxY, ccu), 0, 0], # x limits C upper [60 / n, 60 / n, maxY, maxY, 60 / n])]) # y limits C upper limitB2L = Polygon([(x, y) for x, y in zip([50 / n, 50 / n, 90 / n, 330 / n, maxX, maxX, 50 / n], # x limits B lower [0, 30 / n, 80 / n, 230 / n, self._endy(330 / n, 230 / n, maxX, cbl), 0, 0])]) # y limits B lower limitB2U = Polygon([(x, y) for x, y in zip([0, 30 / n, 230 / n, self._endx(230 / n, 330 / n, maxY, cbu), 0, 0], # x limits B upper [50 / n, 50 / n, 330 / n, maxY, maxY, 50 / n])]) # y limits B upper for i, points in enumerate(zip(ref, pred)): for f, r in zip([limitB2L, limitB2U, limitC2L, limitC2U, limitD2L, limitD2U, limitE2], [1, 1, 2, 2, 3, 3, 4]): if f.contains(Point(points[0], points[1])): _zones[i] = r return [int(i) for i in _zones] def plot(self, ax): # ref, pred ref = self.reference pred = self.test # calculate conversion factor if needed n = 18 if self.units == 'mmol' else 1 maxX = self.xlim or max(max(ref) + 20 / n, 550 / n) maxY = self.ylim or max([*(np.array(pred) + 20 / n), maxX, 550 / n]) if self.type == 1: ce = self._coef(35, 155, 50, 550) cdu = self._coef(80, 215, 125, 550) cdl = self._coef(250, 40, 550, 150) ccu = self._coef(70, 110, 260, 550) ccl = self._coef(260, 130, 550, 250) cbu = self._coef(280, 380, 430, 550) cbl = self._coef(385, 300, 550, 450) _gridlines = [ ([0, min(maxX, maxY)], [0, min(maxX, maxY)], ':'), ([0, 30 / n], [50 / n, 50 / n], '-'), ([30 / n, 140 / n], [50 / n, 170 / n], '-'), ([140 /n, 280 / n], [170 / n, 380 / n], '-'), ([280 / n, self._endx(280 / n, 380 / n, maxY, cbu)], [380 / n, maxY], '-'), ([50 / n, 50 / n], [0 / n, 30 / n], '-'), ([50 / n, 170 / n], [30 / n, 145 / n], '-'), ([170 / n, 385 / n], [145 / n, 300 / n], '-'), ([385 / n, maxX], [300 / n, self._endy(385 / n, 300 / n, maxX, cbl)], '-'), ([0 / n, 30 / n], [60 / n, 60 / n], '-'), ([30 / n, 50 / n], [60 / n, 80 / n], '-'), ([50 / n, 70 / n], [80 / n, 110 / n], '-'), ([70 / n, self._endx(70/n, 110/n, maxY, ccu)], [110 / n, maxY], '-'), ([120 / n, 120 / n], [0 / n, 30 / n], '-'), ([120 / n, 260 / n], [30 / n, 130 / n], '-'), ([260 / n, maxX], [130 / n, self._endy(260 / n, 130 / n, maxX, ccl)], '-'), ([0 / n, 25 / n], [100 / n, 100 / n], '-'), ([25 / n, 50 / n], [100 / n, 125 / n], '-'), ([50 / n, 80 / n], [125 / n, 215 / n], '-'), ([80 / n, self._endx(80 / n, 215 / n, maxY, cdu)], [215 / n, maxY], '-'), ([250 / n, 250 / n], [0 / n, 40 / n], '-'), ([250 / n, maxX], [40 / n, self._endy(410 / n, 110 / n, maxX, cdl)], '-'), ([0 / n, 35 / n], [150 / n, 155 / n], '-'), ([35 / n, self._endx(35 / n, 155 / n, maxY, ce)], [155 / n, maxY], '-'), ] elif self.type == 2: ce = self._coef(35, 200, 50, 550) cdu = self._coef(35, 90, 125, 550) cdl = self._coef(410, 110, 550, 160) ccu = self._coef(30, 60, 280, 550) ccl = self._coef(260, 130, 550, 250) cbu = self._coef(230, 330, 440, 550) cbl = self._coef(330, 230, 550, 450) _gridlines = [ ([0, min(maxX, maxY)], [0, min(maxX, maxY)], ':'), ([0, 30/n], [50/n, 50/n], '-'), ([30 / n, 230 / n], [50 / n, 330 / n], '-'), ([230 / n, self._endx(230/n, 330/n, maxY, cbu)], [330/n, maxY], '-'), ([50/n, 50/n], [0/n, 30/n], '-'), ([50/n, 90/n], [30/n, 80/n], '-'), ([90/n, 330/n], [80/n, 230/n], '-'), ([330/n, maxX], [230/n, self._endy(330/n, 230/n, maxX, cbl)], '-'), ([0/n, 30/n], [60/n, 60/n], '-'), ([30/n, self._endx(30/n, 60/n, maxY, ccu)], [60/n, maxY], '-'), ([90/n, 260/n], [0/n, 130/n], '-'), ([260/n, maxX], [130/n, self._endy(260/n, 130/n, maxX, ccl)], '-'), ([0/n, 25/n], [80/n, 80/n], '-'), ([25/n, 35/n], [80/n, 90/n], '-'), ([35/n, self._endx(35/n, 90/n, maxY, cdu)], [90/n, maxY], '-'), ([250/n, 250/n], [0/n, 40/n], '-'), ([250/n, 410/n], [40/n, 110/n], '-'), ([410/n, maxX], [110/n, self._endy(410/n, 110/n, maxX, cdl)], '-'), ([0/n, 35/n], [200/n, 200/n], '-'), ([35/n, self._endx(35/n, 200/n, maxY, ce)], [200/n, maxY], '-'), ] colors = ['#196600', '#7FFF00', '#FF7B00', '#FF5700', '#FF0000'] _gridlabels = [ (600, 600, "A", colors[0]), (360, 600, "B", colors[1]), (600, 355, "B", colors[1]), (165, 600, "C", colors[2]), (600, 215, "C", colors[2]), (600, 50, "D", colors[3]), (75, 600, "D", colors[3]), (5, 600, "E", colors[4]) ] # plot individual points if self.color_points == 'auto': ax.scatter(self.reference, self.test, marker='o', alpha=0.6, c=[colors[i] for i in self._calc_error_zone()], s=8, **self.point_kws) else: ax.scatter(self.reference, self.test, marker='o', color=self.color_points, alpha=0.6, s=8, **self.point_kws) # plot grid lines if self.grid: for g in _gridlines: ax.plot(np.array(g[0]), np.array(g[1]), g[2], color=self.color_grid, **self.grid_kws) if self.percentage: zones = [['A', 'B', 'C', 'D', 'E'][i] for i in self._calc_error_zone()] for l in _gridlabels: ax.text(l[0] / n, l[1] / n, l[2], fontsize=12, fontweight='bold', color=l[3] if self.color_gridlabels == 'auto' else self.color_gridlabels) ax.text(l[0] / n + (18 / n), l[1] / n + (18 / n), "{:.1f}".format((zones.count(l[2])/len(zones))*100), fontsize=9, fontweight='bold', color=l[3] if self.color_gridlabels == 'auto' else self.color_gridlabels) else: for l in _gridlabels: ax.text(l[0] / n, l[1] / n, l[2], fontsize=12, fontweight='bold', color=l[3] if self.color_gridlabels == 'auto' else self.color_gridlabels) # limits and ticks _ticks = [70, 100, 150, 180, 240, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000] ax.set_xticks([round(x/n, 1) for x in _ticks]) ax.set_yticks([round(x/n, 1) for x in _ticks]) ax.set_xlim(0, maxX) ax.set_ylim(0, maxY) # graph labels ax.set_ylabel(self.y_title) ax.set_xlabel(self.x_title) if self.graph_title is not None: ax.set_title(self.graph_title)
[docs]def parkes(type, reference, test, units, x_label=None, y_label=None, title=None, xlim=None, ylim=None, color_grid='#000000', color_gridlabels='auto', color_points='auto', grid=True, percentage=False, point_kws=None, grid_kws=None, square=False, ax=None): """Provide a glucose error grid analyses as designed by Parkes. This is an Axis-level function which will draw the Parke-error grid plot. onto the current active Axis object unless ``ax`` is provided. Parameters ---------- type : int Parkes error grid differ for each type of diabetes. This should be either 1 or 2 corresponding to the type of diabetes. reference, test : array, or list Glucose values obtained from the reference and predicted methods, preferably provided in a np.array. units : str The SI units which the glucose values are provided in. Options: 'mmol', 'mgdl' or 'mg/dl'. x_label : str, optional The label which is added to the X-axis. If None is provided, a standard label will be added. y_label : str, optional The label which is added to the Y-axis. If None is provided, a standard label will be added. title : str, optional Title of the Parkes-error grid plot. If None is provided, no title will be plotted. xlim : list, optional Minimum and maximum limits for X-axis. Should be provided as list or tuple. If not set, matplotlib will decide its own bounds. ylim : list, optional Minimum and maximum limits for Y-axis. Should be provided as list or tuple. If not set, matplotlib will decide its own bounds. color_grid : str, optional Color of the Clarke error grid lines. Defaults to #000000 which represents the black color. color_gridlabels : str, optional Color of the grid labels (A, B, C, ..) that will be plotted. Defaults to 'auto' which colors the points according to their relative zones. color_points : str, optional Color of the individual differences that will be plotted. Defaults to 'auto' which colors the points according to their relative zones. grid : bool, optional Enable the grid lines of the Parkes error. Defaults to True. percentage : bool, optional If True, percentage of the zones will be depicted in the plot. square : bool, optional If True, set the Axes aspect to "equal" so each cell will be square-shaped. point_kws : dict of key, value mappings, optional Additional keyword arguments for `plt.scatter`. grid_kws : dict of key, value mappings, optional Additional keyword arguments for the grid with `plt.plot`. ax : matplotlib Axes, optional Axes in which to draw the plot, otherwise use the currently-active Axes. Returns ------- ax : matplotlib Axes Axes object with the Parkes error grid plot. See Also ------- Parkes, J. L., Slatin S. L. et al. Diabetes Care, vol. 23, no. 8, 2000, pp. 1143-1148. Pfutzner, A., Klonoff D. C., et al. J Diabetes Sci Technol, vol. 7, no. 5, 2013, pp. 1275-1281. """ plotter: _Parkes = _Parkes(type, reference, test, units, x_label, y_label, title, xlim, ylim, color_grid, color_gridlabels, color_points, grid, percentage, point_kws, grid_kws) # Draw the plot and return the Axes if ax is None: ax = plt.gca() if square: ax.set_aspect('equal') plotter.plot(ax) return ax
[docs]def parkeszones(type, reference, test, units, numeric=False): """Provides the error zones as depicted by the Parkes error grid analysis for each point in the reference and test datasets. Parameters ---------- type : int Parkes error grid differ for each type of diabetes. This should be either 1 or 2 corresponding to the type of diabetes. reference, test : array, or list Glucose values obtained from the reference and predicted methods, preferably provided in a np.array. units : str The SI units which the glucose values are provided in. Options: 'mmol', 'mgdl' or 'mg/dl'. numeric : bool, optional If this is set to true, returns integers (0 to 4) instead of characters for each of the zones. Returns ------- parkeszones : list Returns a list depicting the zones for each of the reference and test values. """ # obtain zones from a Clarke reference object _zones = _Parkes(type, reference, test, units, None, None, None, None, None, True, False, '#000000', 'auto', 'auto', None, None)._calc_error_zone() if numeric: return _zones else: labels = ['A', 'B', 'C', 'D', 'E'] return [labels[i] for i in _zones]
class _SEG(object): """Internal class for drawing a surveillance error grid error grid plot""" def __init__(self, reference, test, units, x_title, y_title, graph_title, xlim, ylim, color_points, percentage, point_kws): # variables assignment self.reference: np.array = np.asarray(reference) self.test: np.array = np.asarray(test) self.units = units self.graph_title: str = graph_title self.x_title: str = x_title self.y_title: str = y_title self.xlim: list = xlim self.ylim: list = ylim self.color_points: str = color_points self.percentage: bool = percentage self.point_kws = {} if point_kws is None else point_kws.copy() self._check_params() self._derive_params() def _check_params(self): if len(self.reference) != len(self.test): raise ValueError('Length of reference and test values are not equal') if self.units not in ['mmol', 'mg/dl', 'mgdl']: raise ValueError('The provided units should be one of the following: mmol, mgdl or mg/dl.') if any([x is not None and not isinstance(x, str) for x in [self.x_title, self.y_title]]): raise ValueError('Axes labels arguments should be provided as a str.') def _derive_params(self): if self.x_title is None: _unit = 'mmol/L' if 'mmol' else 'mg/dL' self.x_title = 'Reference glucose concentration ({})'.format(_unit) if self.y_title is None: _unit = 'mmol/L' if 'mmol' else 'mg/dL' self.y_title = 'Predicted glucose concentration ({})'.format(_unit) def _calc_error_score(self): n = 18 if self.units == 'mmol' else 1 ref = self.reference * n pred = self.test * n _zones = [] data = np.loadtxt(pkg_resources.open_text(static, 'seg.csv')) _zones = np.array([data.T[int(p), int(t)] for p, t in zip(pred, ref)]) return _zones def plot(self, ax): # ref, pred ref = self.reference pred = self.test _data = np.loadtxt(pkg_resources.open_text(static, 'seg.csv')) # calculate conversion factor if needed n = 18 if self.units == 'mmol' else 1 maxX = self.xlim or 600 maxY = self.ylim or 600 # Define colormaps _colors = [(0, 165 / 256, 0), (0, 255 / 256, 0), (255 / 256, 255 /256, 0), (255 / 256, 0, 0), (128 / 256, 0, 0)] _nodes = [0.0, 0.4375, 1.0625, 2.7500, 4.000] cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", list(zip([x/4 for x in _nodes], _colors))) # Plot color axes grid_path = str(path_func(static, 'seg600.png')) cax = ax.imshow(np.flipud(np.array(plt.imread(grid_path))), origin='lower', cmap=cmap, vmin=0, vmax=4) # Plot color bar cbar = plt.colorbar(cax, ticks=[0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], orientation='vertical', fraction=0.15, aspect=6) cbar.ax.tick_params(labelsize=8) cbar.ax.yaxis.set_label_position('left') cbar.ax.set_ylabel('Risk score') # Separators for s in [0, 0.5, 1.5, 2.5, 3.5, 4]: cbar.ax.plot([6, 6.5], [s]*2, '-', color='black', lw=1, alpha=1, clip_on=False) # Labels for l in [(0.25, 'None'), (1.0, 'Slight'), (2.0, 'Moderate'), (3.0, 'High'), (3.75, 'Extreme')]: cbar.ax.text(6.2, l[0]-0.008, l[1], ha='left', va='center', rotation=0, fontsize=10) if self.percentage: seg_scores = self._calc_error_score() _zones_sub = [[] for _ in range(8)] edges = list(np.arange(0, 4.5, 0.5)) for x in range(len(edges)-1): _zones_sub[x] = np.array(seg_scores[(seg_scores >= edges[x]) & (seg_scores < edges[x+1])]) perc_zones = [(len(x)/len(seg_scores))*100 for x in _zones_sub] for i, x in enumerate(perc_zones): cbar.ax.plot([0, 5], [(i*.5) + .5]*2, '--', color='grey', lw=1, alpha=.6) if x > 0: _str = "<0.01%" if round(x, 2) == 0 else "{:.2f}%".format(x) cbar.ax.text(2, (i*.5)+.25, _str, ha='center', va='center', fontsize=9) ax.scatter(self.reference*n, self.test*n, marker='o', color=self.color_points, alpha=0.6, s=8, **self.point_kws) # limits and ticks _ticks = [0, 90, 180, 270, 360, 450, 540] ax.set_xticks([round(x, 1) for x in _ticks]) ax.set_yticks([round(x, 1) for x in _ticks]) ax.set_xticklabels([round(x/n, 1) for x in _ticks]) ax.set_yticklabels([round(x / n, 1) for x in _ticks]) ax.set_xlim(0, maxX) ax.set_ylim(0, maxY) # graph labels ax.set_ylabel(self.y_title) ax.set_xlabel(self.x_title) if self.graph_title is not None: ax.set_title(self.graph_title)
[docs]def seg(reference, test, units, x_label=None, y_label=None, title=None, xlim=None, ylim=None, color_points='white', percentage=False, point_kws=None, square=False, ax=None): """Provide a glucose error grid analyses as designed by the surveillance error grid. This is an Axis-level function which will draw the surveillance error grid plot. onto the current active Axis object unless ``ax`` is provided. Parameters ---------- reference, test : array, or list Glucose values obtained from the reference and predicted methods, preferably provided in a np.array. units : str The SI units which the glucose values are provided in. Options: 'mmol', 'mgdl' or 'mg/dl'. x_label : str, optional The label which is added to the X-axis. If None is provided, a standard label will be added. y_label : str, optional The label which is added to the Y-axis. If None is provided, a standard label will be added. title : str, optional Title of the plot. If None is provided, no title will be plotted. xlim : list, optional Minimum and maximum limits for X-axis. Should be provided as list or tuple. If not set, matplotlib will decide its own bounds. ylim : list, optional Minimum and maximum limits for Y-axis. Should be provided as list or tuple. If not set, matplotlib will decide its own bounds. color_points : str, optional Color of the individual differences that will be plotted. Defaults to 'white'. percentage : bool, optional If True, percentage of the zones will be depicted in the plot. point_kws : dict of key, value mappings, optional Additional keyword arguments for `plt.scatter`. square : bool, optional If True, set the Axes aspect to "equal" so each cell will be square-shaped. ax : matplotlib Axes, optional Axes in which to draw the plot, otherwise use the currently-active Axes. Returns ------- ax : matplotlib Axes Axes object with the Surveillance error grid plot. See Also ------- Klonoff, D. C., Lias, C., et al. J Diabetes Sci Technol, vol. 8, no. 4, 2014, pp 658-672. Kovatchev, B. P., Wakeman, C. A., et al. J Diabetes Sci Technol, vol 8, no. 4, 2014, pp. 673-684. """ plotter: _SEG = _SEG(reference, test, units, x_label, y_label, title, xlim, ylim, color_points, percentage, point_kws) # Draw the plot and return the Axes if ax is None: ax = plt.gca() if square: ax.set_aspect('equal') plotter.plot(ax) return ax
[docs]def segscores(reference, test, units): """Provides the raw error values as depicted by the surveillance error grid analysis for each point in the reference and test datasets. Parameters ---------- reference, test : array, or list Glucose values obtained from the reference and predicted methods, preferably provided in a np.array. units : str The SI units which the glucose values are provided in. Options: 'mmol', 'mgdl' or 'mg/dl'. Returns ------- segscores : list Returns a list with a SEG score for each test, reference pair. """ # obtain zones from a Clarke reference object _zones = _SEG(reference, test, units, None, None, None, None, None, '#000000', None, None)._calc_error_score() return _zones