Commit initial

This commit is contained in:
Matt Marcha 2018-11-18 17:04:03 +01:00
commit 235d5f6305
5 changed files with 313 additions and 0 deletions

View file

@ -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)

57
Model/Criteria.py Executable file
View file

@ -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))

98
Model/DecisionTree.py Normal file
View file

@ -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]

44
Model/Table.py Normal file
View file

@ -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

74
main.py Executable file
View file

@ -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...")