﻿

#########################################################################################

class Metrics:
    """Commonly used evaluation metrics"""
    
    header_template = '{:8} {:8} {:8} {:8} {:8} {:8} {:8}'
    line_template = '{:8.4f} {:8.4f} {:8.4f} {:8.2f} {:8.2f} {:8.0f} {:8.0f}'

    def __init__(self):
        """Initialize the empty metrics object"""
        # True positive used in recall calculation
        self.tp_std = 0.0
        # True positive used in precision calculation
        self.tp_test = 0.0
        # Number of standard objects
        self.n_std = 0
        # Number of test objects
        self.n_test = 0
        # Precision
        self.precision = 1.0
        # Recall
        self.recall = 1.0
        # F1
        self.f1 = 1.0

    def recalculate(self):
        self.precision = self.tp_test / self.n_test if self.n_test > 0 else 1.0
        self.recall = self.tp_std / self.n_std if self.n_std > 0 else 1.0

        if self.n_std + self.n_test == 0:
            self.f1 = 1.0
        else:
            denominator = self.precision + self.recall
            self.f1  = (
                (2 * self.precision * self.recall / denominator)
                    if denominator > 0 else 0.0
            )

        isValid = lambda x: x >= 0 and x <= 1
        assert(isValid(self.precision))
        assert(isValid(self.recall))
        assert(isValid(self.f1))

    def add(self, other):
        self.tp_std += other.tp_std
        self.tp_test += other.tp_test
        self.n_std += other.n_std
        self.n_test += other.n_test
        self.recalculate()

    def toLine(self):
        """Returns a line for the stats table"""
        return Metrics.line_template.format(
            self.precision, self.recall, self.f1,
            self.tp_std, self.tp_test, self.n_std, self.n_test)

    @classmethod
    def header(cls):
        """Returns a header for the stats table"""
        return Metrics.header_template.format(
            'P', 'R', 'F1', 'TP1', 'TP2', 'In Std.', 'In Test.')

    @classmethod
    def createSimple(cls, tp, n_std, n_test):
        """Calculate metrics with single TruePositive value"""
        m = cls()

        m.tp_std = tp
        m.tp_test = tp
        m.n_std = n_std
        m.n_test = n_test

        m.recalculate()

        return m
    
    @classmethod
    def create(cls, tp_std, tp_test, n_std, n_test):
        """Calculate metrics with separate TruePositive values"""
        m = cls()

        m.tp_std = tp_std
        m.tp_test = tp_test
        m.n_std = n_std
        m.n_test = n_test

        m.recalculate()

        return m