class Arbre_C45(Arbre_ID3):
    """
        Un arbre C4.5 hérite d'un arbre ID3 mais se construit différemment
    """

    def __init__(self, chemin_données="", chemin_elagage=""):
        """
            chemin_données est l'emplacement du fichier contenant les données
            chemin_elagage est l'emplacement d'un autre set permettant
            d'élaguer l'arbre
        """
        #initialisation de l'ensemble avec le fichier dans chemin_données
        self.ensemble = Ensemble(chemin_données)
        #initialisation du nœud principal de l'arbre
        self.arbre = None
        self.chemin_elagage = chemin_elagage

    def construire(self):
        """
            retourne l'arbre au complet
        """
        #si le set est corrompu (attributs manquants), on le restaure
        self.ensemble.restaurer_valeurs_manquantes()
        self.arbre = self.__construire_arbre(self.ensemble)

    def __construire_arbre(self, ensemble):
        if not isinstance(ensemble, Ensemble):
            raise TypeError("ensemble doit être un Ensemble et non un {}" \
                            .format(type(ensemble)))
        #si la liste est vide
        if len(ensemble) == 0:
            raise ValueError("la liste d'exemples ne peut être vide !")
        #testons si tous les exemples ont la même étiquette
        if ensemble.entropie() == 0:
            #on retourne l'étiquette en question
            return Feuille(ensemble.liste_exemples[0].etiquette)
        #s'il ne reste d'attribut à tester 
        if len(ensemble.liste_attributs) == 0:
            max, etiquette_finale = 0, ""
            #on teste toutes les étiquettes possibles de l'ensemble
            for etiquette in ensemble.etiquettes_possibles():
                sous_ensemble = ensemble.sous_ensemble_etiquette(etiquette)
                #si c'est la plus fréquente, c'est celle qu'on choisit
                if len(sous_ensemble) > max:
                    max, etiquette_finale = len(sous_ensemble), etiquette
            #et on la retourne dans une feuille
            return Feuille(etiquette_finale)

        #ne pas oublier de sauver les valeurs pour pouvoir les restituer au cas
        #où l'attribut discrétisé n'est pas choisi
        sauvegarde_valeurs = ensemble.sauvegarder_valeurs_discretes()
        #pour chaque valeur à discrétiser
        for attribut, valeurs in sauvegarde_valeurs:
            #on discrétise
            ensemble.discretiser(attribut)
        #on récupère l'attribut optimal
        #ATTENTION : préciser ID3=False pour utiliser le ratio de gain
        a_tester = ensemble.attribut_optimal(ID3=False)
        #pour chaque attribut sauvegardé
        for attribut, valeurs in sauvegarde_valeurs:
            #si ce n'est pas l'attribut choisi
            if attribut != a_tester:
                #on remet les anciennes valeurs continues
                for i in range(len(valeurs)):
                    ensemble.liste_exemples[i].dict_attributs[attribut] = \
                                                                    valeurs[i]
        #si on arrive ici, on retourne d'office un nœud et pas une feuille
        noeud = Noeud(a_tester)
        #pour chaque valeur que peut prendre l'attribut à tester
        for valeur in ensemble.valeurs_possibles_attribut(a_tester):
            #on crée un sous-ensemble
            sous_ensemble = ensemble.sous_ensemble_attribut(a_tester, valeur)
            #et on en crée un nouveau nœud
            noeud.enfants[valeur] = self.__construire_arbre(sous_ensemble)
        #on retourne le nœud que l'on vient de créer
        return noeud

    def etiqueter(self, exemple):
        #on initialise le nœud actuel avec le haut de l'arbre
        noeud_actuel = self.arbre
        #tant que l'on est sur un nœud et pas sur une feuille
        while isinstance(noeud_actuel, Noeud):
            #valeur == valeur de l'exemple à étiqueter pour l'attribut du nœud
            valeur = exemple.dict_attributs[noeud_actuel.attribut_teste]
            #si valeur représente un nombre
            try:
                valeur = float(valeur)
            #si ça ne marche pas, tout va bien : c'est une valeur discrète
            except:
                pass
            #si c'est une valeur continue, on la transforme en intervalle
            else:
                for intervalle in noeud_actuel.enfants:
                    if valeur < intervalle[1] and valeur >= intervalle[0]:
                        valeur = intervalle
                        break
            finally:
                #mais il faut bien faire avancer le nœud
                noeud_actuel = noeud_actuel.enfants[valeur]
        #une fois l'exploration terminée, on étiquette l'exemple
        exemple.etiquette = noeud_actuel.etiquette

    def taux_erreur(self, ensemble):
        """
            renvoie un nombre dans [0, 1] correspondant à la proportion
            d'exemples dans ensemble qui se font étiqueter correctement
            avec l'arbre tel quel
        """
        compteur_etiquetages_incorrects = 0
        #pour chaque exemple
        for exemple in ensemble.liste_exemples:
            #on garde son étiquette
            etiquette = exemple.etiquette
            self.etiqueter(exemple)
            #et on la compare à l'étiquette donnée par l'arbre
            if etiquette != exemple.etiquette:
                #si elles sont différentes, on augmente la proportion
                #d'étiquetages incorrects et on rétablit la bonne étiquette
                compteur_etiquetages_incorrects += 1
                exemple.etiquette = etiquette
        return compteur_etiquetages_incorrects/len(ensemble)

    def elaguer(self):
        """
            modifie l'arbre en élaguant suivant l'ensemble de travail
            d'élagage donné dans self.chemin_elagage
        """
        #ATTENTION : si le chemin n'a pas été donné, on n'élague pas !
        if self.chemin_elagage != "":
            self.arbre = self.__elaguer_noeud(self.arbre,
                                              Ensemble(self.chemin_elagage))

    def __elaguer_noeud(self, noeud, ensemble_elagage):
        """
            élague le noeud passé en paramètre et le retourne
        """
        #si on est sur une feuille, on ne va pas plus loin
        if isinstance(noeud, Feuille):
            return noeud
        min_erreur, etiquette_gardee = 1.0, ""
        proportion_initiale = self.taux_erreur(ensemble_elagage)
        sauvegarde = self.arbre
        #pour chaque étiquette
        for etiquette in ensemble_elagage.etiquettes_possibles():
            self.arbre = Feuille(etiquette)
            #on calcule le taux d'erreur si on remplace le nœud par
            #l'étiquette en question
            taux_erreur_actuel = self.taux_erreur(ensemble_elagage)
            #on sauvegarde le meilleur taux
            if taux_erreur_actuel < min_erreur:
                min_erreur, etiquette_gardee = taux_erreur_actuel, etiquette
        #s'il existe un taux avantageux on élague à cet endroit
        if min_erreur <= proportion_initiale:
            return Feuille(etiquette_gardee)
        else:
            self.arbre = sauvegarde
        #on n'oublie pas de restaurer la valeur du sommet de l'arbre !
        sauvegarde, self.arbre = self.arbre, sauvegarde
        #on teste chaque enfant pour voir s'il est élagable
        for enfant in noeud.enfants:
            sous_ensemble = ensemble_elagage.sous_ensemble_attribut(
                                                    noeud.attribut_teste,
                                                    enfant)
            #s'il l'est, on l'élague
            if len(sous_ensemble) != 0:
                noeud.enfants[enfant] = self.__elaguer_noeud(
                                                    noeud.enfants[enfant],
                                                    sous_ensemble)
        #et au final, on renvoie le nœud aux enfants peut-être élagués 
        return noeud