commit 235d5f63052be30e358a9efc4534b5564f48b106 Author: Matt Marcha Date: Sun Nov 18 17:04:03 2018 +0100 Commit initial diff --git a/Controller/EntityDetermination.py b/Controller/EntityDetermination.py new file mode 100644 index 0000000..38b6551 --- /dev/null +++ b/Controller/EntityDetermination.py @@ -0,0 +1,40 @@ +# -*-coding:utf8 -* + +from anytree.search import find + +class EntityDetermination: + """Détermine si le critère de recherche d'un arbre est true ou false pour une entité""" + def __init__(self, tree, entity): + self.entity = entity + self.reference = tree.reference + + + + # On démarre les recherches à partir du noeud initial + self._find_next_node(tree.nodes[0]) + + def _find_next_node(self, current_node): + """Identifie la valeur de l'entité pour le critère courant et retourne le suivant, ou le résultat""" + # On identifie la valeur de l'entité pour le critère courant + criteria_value = self.entity[current_node.name] + + # Pour ce critère, on récupère le noeud enfant + node = find(current_node, lambda node: node.name == criteria_value and node.parent == current_node) + + # Si cette valeur n'est pas référencée, on est bien incapable de déterminer quoi que ce soit + if node == None: + self._print_result("inconnue. Que voulez-vous, même la technologie a ses limites...") + else: + next_node = node.children[0] + + # Si le gamin de ce noeud est true ou false : on a un résultat + if next_node.name in ("True", "False"): + self._print_result(next_node.name) + # sinon on recommence + else: + self._find_next_node(next_node) + + def _print_result(self, result): + """Affiche le résultat""" + print("Notre ami.e", self.entity["Nom"], "est elle de type", self.reference, "?", "\nLa réponse est", + result) diff --git a/Model/Criteria.py b/Model/Criteria.py new file mode 100755 index 0000000..44df8b5 --- /dev/null +++ b/Model/Criteria.py @@ -0,0 +1,57 @@ +# -*-coding:utf8 -* + +from math import log + + +class Criteria : + """Classe Critère - valeurs d'un critère pour l'éléments recherché" + Attributs : + @string name = nom du critère + @dict{@dict{@int} values = totaux des correspondances des différentes valeurs pour ce critère + + """ + + def __init__(self, criteriaList, referenceList): + """Constructeur - liste les valeurs possibles du critère """ + # Definition du nom et suppression des header de la liste + self.name = criteriaList[0] + self.referenceList = referenceList.copy() + self.criteriaList = criteriaList.copy() + del self.criteriaList[0] + del self.referenceList[0] + + # Définition des différentes valeurs + self.values = {} + for key, value in enumerate(self.criteriaList): + # Ajout de la valeur si nouvelle + if value not in self.values.keys(): + self.values[value] = {"True": 0, "False": 0} + # Et incrément de la valeur du critère correspondant à l'élément de référence recherché + if self.referenceList[key] == "O" or self.referenceList[key] is True or self.referenceList[key] == "Oui": + self.values[value]["True"] += 1 + elif self.referenceList[key] == "N" or self.referenceList[key] is False or self.referenceList[key] == "Non": + self.values[value]["False"] += 1 + + def get_entropy(self): + """Calcule et retourne l'entropie du critère""" + # On récupère les totaux + totals = {"all": 0} + for key, value in self.values.items(): + totals[key] = value["True"] + value["False"] + totals["all"] += totals[key] + + # Maintenant qu'on a tout ce qu'il nous faut, on peut lancer le calcul ! + entropy = 0 + for key, value in self.values.items(): + entropy += totals[key]/totals["all"] \ + * ((self._entropy_frag(value["True"]/totals[key])) + + (self._entropy_frag(value["False"]/totals[key]))) + + return entropy + + @staticmethod + def _entropy_frag(prob): + """Un morceau du calcul d'entropie, permet d'alléger la formule""" + if prob == 0: + return 0 + return -1 * prob * (log(prob)/log(2)) diff --git a/Model/DecisionTree.py b/Model/DecisionTree.py new file mode 100644 index 0000000..2033e55 --- /dev/null +++ b/Model/DecisionTree.py @@ -0,0 +1,98 @@ +# -*-coding:utf8 -* + +from Model.Criteria import Criteria +from Model.Table import Table +from anytree import Node + +class DecisionTree: + """Classe DecisionTree permettant de contruire un arbre de décision + + Atributs : + @Table table # tableau de données + @String reference # le critère à déterminer, pour lequel on construit notre arbre + @List(@Node) # Liste des noeuds + """ + + def __init__(self, data, reference): + self.table = Table(data) + self.nodes = list() + self.reference = reference + + # On récupère l'identité du premier noeud et on le définit + first_criteria = self._criteria_next_node(self.table) + root_node = self._add_node(first_criteria.name, None) + # Puis on lance la machine pour avancer dans l'arbre + self._create_branch(self.table, first_criteria, root_node) + + + def _criteria_next_node(self, table): + """Détermine le critère en prochain noeud à partir d'un tableau""" + + # On établit la liste des critères + criteria_list = table.get_criteria_list(self.reference) + + # initialisation des variables + maxEntropy = 1.1 + nextNode = Criteria + + # On détemrine l'entropie de chaque critère + for crit in criteria_list: + criteria = Criteria(crit, table.get_column(table.table[0].index(self.reference))) + # Si c'est la plus petite jusqu'alors, c'est ce critère qui sera le prochain noeud + if criteria.get_entropy() < maxEntropy: + nextNode = criteria + maxEntropy = criteria.get_entropy() + + return nextNode + + def _create_branch(self, current_table, criteria, node): + """détermine les branches partant d'un critère positionné en noeud""" + + for value, counts in criteria.values.items(): + # on replace le noeud parent correctement et on ajoute le critère en tant que noeud + parent_node = node + parent_node = self._add_node(value, parent_node) + # Si un total est à 0, ou qu'il n'y plus que 3 colonne dans le tableau on est en fin de branhce ! + # Ou aussi qu'il n'y a qu'une seule option ! + if 0 in counts.values() or len(current_table.table[0]) == 3: + result = None + # On définit le résultat en fonction de la valeur la plus importante + if counts["False"] > counts["True"]: + result = "False" + else: # Par défaut (en cas d'égalité notamment), on prend true + result = "True" + #On ajoute le noeud final + self._add_node(result, parent_node) + + # sinon il faut créer un nouveau tableau à partir de l'ancien + else: + new_table = Table(current_table.remove_criteria(criteria.name, value)) + + # Et continuer à avancer dans l'arbre + next_criteria = self._criteria_next_node(new_table) + + # Si le critère suivant n'a plus qu'une entrée possible, on est également en fin de branche ! + if len(next_criteria.values) < 2: + # On l'ajoute donc en noeud final, selon sa valeur la plus importante + for values in next_criteria.values.values(): + result = max(values, key=values.get) + self._add_node(result, parent_node) + + # Sinon, le critère est un nouveau noeud et on continue + else: + new_parent = self._add_node(next_criteria.name, parent_node) + self._create_branch(new_table, next_criteria, new_parent) + + def _add_node(self, node, parent): + """Crée un nouveau noeud dans l'arbre. Renvoie ce nouveau noeud""" + self.nodes.append(Node(node, parent=parent)) + return self.nodes[-1] + + + + + + + + + diff --git a/Model/Table.py b/Model/Table.py new file mode 100644 index 0000000..963b39c --- /dev/null +++ b/Model/Table.py @@ -0,0 +1,44 @@ +# -*-coding:utf8 -* + + +class Table: + + """Classe Tableau. Instancie un tableau de données manipulable""" + + def __init__(self, table): + self.table = table + + def get_column(self, key): + """retourne toutes les valeurs d'une colonne sous la forme d'une liste""" + column = list() + for i, entry in enumerate(self.table): + column.append(entry[key]) + return column + + def remove_criteria(self, criteria, value): + """Génère un nouveau tableau à partir d'un autre + en enelvant les données liées à un critère et en ne gardant que la valeur de ce critère""" + newTable = [] + + # récupère l'indice du critère à virer + index = self.table[0].index(criteria) + + # on vire allègrement cet indice de toutes les entrées du tableau qui matchent et on les ajoute au nouveau + for entry in self.table: + if entry[index] == value or entry[index] == criteria: + newEntry = entry.copy() + del newEntry[index] + newTable.append(newEntry) + + return newTable + + def get_criteria_list(self, reference=""): + """Retourne la liste des critères à analyser dans un tableau, + en ignorant une colonne de référence si spécifiée""" + # On parcourt la ligne d'entête pour récupérer les colonnes à scanner + toScan = [] + for i, header in enumerate(self.table[0]): + # on ne prend pas la colonne nom qui est inutile, ni la colonne de référence + if i != 0 and header != reference: + toScan.append(self.get_column(i)) + return toScan diff --git a/main.py b/main.py new file mode 100755 index 0000000..100f6f1 --- /dev/null +++ b/main.py @@ -0,0 +1,74 @@ +# -*-coding:utf8 -* + +from anytree import RenderTree +from Model.DecisionTree import DecisionTree +from Controller.EntityDetermination import EntityDetermination + +# Données initiales +data = [ + ["Nom", "Cape", "Argent", "Tech", "Pouvoir", "Héro"], + ["Spiderman", "N", "N", "N", "O", "O"], + ["Poutine", "N", "O", "O", "?", "N"], + ["Batman", "O", "O", "O", "N", "O"], + ["Jocker", "N", "O", "O", "N", "N"], + ["Rorschach", "N", "N", "N", "?", "O"], + ["Deadpool", "N", "N", "O", "O", "O"], + ["Merckel", "N", "O", "O", "N", "N"], + ["D'Artagnan", "O", "N", "N", "N", "N"], + ["César", "O", "O", "O", "N", "N"], + ["Tesla", "N", "N", "O", "?", "O"], + ["Edison", "N", "O", "O", "N", "N"], + ["Homer Simpson", "N", "N", "N", "N", "N"], + ["Sherlock Holmes", "N", "O", "N", "?", "O"], + ["Moriarty", "N", "O", "O", "?", "N"] + ] + +""" Tableau de data secondaire. Utile pour faire des tests sur la première partie du cours +dataGolf = [ +["Jour", "Climat", "Température", "Humidité", "Vent", "Golf"], +["1 ", "Pluie ", "+", "+", "Non ", "N"], +["2 ", "Pluie ", "+", "+", "Oui ", "N"], +["3 ", "Nuage ", "+", "+", "Non ", "O"], +["4 ", "Soleil", "~", "+", "Non ", "O"], +["5 ", "Soleil", "-", "~", "Non ", "O"], +["6 ", "Soleil", "-", "~", "Oui ", "N"], +["7 ", "Nuage ", "-", "~", "Oui ", "O"], +["8 ", "Pluie ", "~", "+", "Non ", "N"], +["9 ", "Pluie ", "-", "~", "Non ", "O"], +["10 ", "Soleil", "~", "~", "Non ", "O"], +["11 ", "Pluie ", "~", "~", "Oui ", "O"], +["12 ", "Nuage ", "~", "+", "Oui ", "O"], +["13 ", "Nuage ", "+", "~", "Non ", "O"], +["14 ", "Soleil", "~", "+", "Oui ", "N"] +] +""" +# On détermine notre critère de référence : celui qu'on va chercher à déterminer +# On peut aussi directment passer une chaine de caractère si jamais +# elle doit juste correspondre à un header du tableau (ex : Héro) +# Le critère doit aussi contenir des valeurs "propres" : oui/non, o/n, True/False... +reference = data[0][5] + +# Création de l'arbre et affichage (ça fait toujours plaisir après s'être saoûlé à le construire) + +tree = DecisionTree(data, reference) +print("ooooh, le bel arbre de décision ! \n", RenderTree(tree.nodes[0]).by_attr("name"), + "\n==============================================================================\n") + +# Nouvelle entité : est-ce un héro ? +# Idéalement il faudrait rentrer les infos à la mano via +# un prompt interactif qui demande selon l'arbre décisionnel, +# pis ajouter les entités ainsi récoltées à la base +# pour affiner les résultats, +# mais bon j'ai un mémoire à faire alors c'est en dur dans le code +entity = { + "Nom": "Moustache", + "Cape": "N", + "Argent": "N", + "Tech": "O", + "Pouvoir": "?", + "Héro": "?" +} + +EntityDetermination(tree, entity) + +input("\n\nAppuyez sur Entrée pour fermer le programme...")