import math as math
class DataCentral:
    def __init__(self, data,description):
        if not isinstance(data,list):
            raise TypeError("Data is not instance of list")
        if not isinstance(description, str):
             raise TypeError("Description is not instance of string")
        if not data:
            raise ValueError("Data list is empty")
        if len(data) < 2:
            raise ValueError("Data list must contain at least two elements")
        self.data = data
        self.description = description
        self.n = len(data)
        self.amplitude = math.ceil((max(data) - min(data))/math.ceil((1+3.3*math.log10(self.n))))
        self.intervalos = math.ceil((1+3.3*math.log10(self.n)))

    def mean(self, grouped, decimal_places):    
        #para datos no agrupados
        if not grouped:
            return round((sum(self.data) / self.n), decimal_places)
        
        # Para datos agrupados
        self.data.sort()
        min_val = min(self.data)
        intervalos = [min_val]
        
        # Crear límites de intervalos
        for i in range(self.intervalos):
            intervalos.append(intervalos[i] + self.amplitude)
        
        sumatory = 0
        
        # Procesar cada intervalo (hay exactamente self.intervalos intervalos)
        for i in range(self.intervalos):
            lim_inf = intervalos[i]
            lim_sup = intervalos[i + 1]
            marca_clase = (lim_inf + lim_sup) / 2
            frecuencia = 0
            
            # Contar frecuencia
            for value in self.data:
                # Último intervalo incluye ambos límites
                if i == self.intervalos - 1:
                    if lim_inf <= value <= lim_sup:
                        frecuencia += 1
                else:
                    if lim_inf <= value < lim_sup:
                        frecuencia += 1
            
            sumatory += frecuencia * marca_clase
        
        return round(sumatory / self.n, decimal_places)          

    def median(self, grouped):
        #la mediana siempre necesita de una lista ordenada de datos
        self.data.sort()
        #Para datos no agrupados
        if not grouped:
            mitad = self.n // 2
            if self.n % 2 == 0:
                # n par: promedio de los dos valores centrales
                return (self.data[mitad - 1] + self.data[mitad]) / 2
            else:
                # n impar: valor central
                return self.data[mitad]

        #Para datos agrupados
        
        intervalos = [self.data[0]]  
        # Crear límites de intervalos
        for i in range(self.intervalos):
            intervalos.append(intervalos[i] + self.amplitude)   
        frecuencias = []
        # Contar frecuencias
        for i in range(self.intervalos):
            lim_inf = intervalos[i]
            lim_sup = intervalos[i + 1]
            frecuencia = 0
            for value in self.data:
                # Último intervalo incluye ambos límites
                if i == self.intervalos - 1:
                    if lim_inf <= value <= lim_sup:
                        frecuencia += 1
                else:
                    if lim_inf <= value < lim_sup:
                        frecuencia += 1
            frecuencias.append(frecuencia)  
        # Calcular la mediana
        suma_frecuencia = 0
        clase_mediana = 0
        for i, freq in enumerate(frecuencias):
            suma_frecuencia += freq
            if suma_frecuencia >= (self.n / 2):
                clase_mediana = i
                break
        L = intervalos[clase_mediana]
        F = sum(frecuencias[:clase_mediana])
        f_m = frecuencias[clase_mediana]
        h = self.amplitude
        mediana = L + (((self.n/2)- F) / f_m) * h           

        return mediana 

    def mode(self, grouped):
        #Para datos no agrupados
        if not grouped:
            frecuencia = {}
            for value in self.data:
                if value in frecuencia:
                    frecuencia[value] += 1
                else:
                    frecuencia[value] = 1
            max_freq = max(frecuencia.values())
            modos = [key for key, freq in frecuencia.items() if freq == max_freq]
            return modos

        #Para datos agrupados
        min_val = min(self.data)
        intervalos = [min_val]  
        # Crear límites de intervalos
        for i in range(self.intervalos):
            intervalos.append(intervalos[i] + self.amplitude)   
        frecuencias = []
        # Contar frecuencias
        for i in range(self.intervalos):
            lim_inf = intervalos[i]
            lim_sup = intervalos[i + 1]
            frecuencia = 0
            for value in self.data:
                # Último intervalo incluye ambos límites
                if i == self.intervalos - 1:
                    if lim_inf <= value <= lim_sup:
                        frecuencia += 1
                else:
                    if lim_inf <= value < lim_sup:
                        frecuencia += 1
            frecuencias.append(frecuencia)  
        # Calcular la moda
        moda_index = frecuencias.index(max(frecuencias))
        L = intervalos[moda_index]
        f_m = frecuencias[moda_index]
        f_1 = frecuencias[moda_index - 1] if moda_index > 0 else 0
        f_2 = frecuencias[moda_index + 1] if moda_index < len(frecuencias) - 1 else 0
        h = self.amplitude
        moda = L + ((f_m - f_1) / ((f_m - f_1) + (f_m - f_2))) * h          

        return moda        

    def variance(self, grouped, decimal_places):
        #calculo de la varianza para datos no agrupados y agrupados
        mean = self.mean(True,decimal_places) if grouped else self.mean(False,decimal_places)
        variance = sum((x - mean) ** 2 for x in self.data) / self.n
        return round(variance, decimal_places)

    def standard_deviation(self, grouped, decimal_places):
        #calculo de la desviacion estandar para datos no agrupados y agrupados
        variance = self.variance(True,decimal_places) if grouped else self.variance(False,decimal_places)
        return round(math.sqrt(variance), decimal_places)           
    
    def coefficient_of_variation(self, grouped, decimal_places):
        #calculo del coeficiente de variacion para datos no agrupados y agrupados
        mean = self.mean(True, decimal_places) if grouped else self.mean(False, decimal_places)
        std_dev = self.standard_deviation(True,decimal_places) if grouped else self.standard_deviation(False,decimal_places)
        cv = (std_dev / mean) * 100
        return round(cv, decimal_places)
    
    def mean_deviation(self, grouped,decimal_places):
        #calculo de la desviacion media para datos no agrupados
        mean = self.mean(True, decimal_places) if grouped else self.mean(False, decimal_places)
        mean_dev = sum(abs(x - mean) for x in self.data) / self.n
        return round(mean_dev, decimal_places)

    def median_deviation(self, grouped,decimal_places):
        #calculo de la desviacion mediana para datos no agrupados
        median = self.median(True) if grouped else self.median(False)
        median_dev = sum(abs(x - median) for x in self.data) / self.n
        return round(median_dev, decimal_places)
            

