Un jeu de données sur le diabète☘
Ces exercices doivent être utilisés pour vous entraîner à programmer. Ils sont généralement accompagnés d'aide et de leur solution pour vous permettre de progresser.
Avant de vous précipiter sur ces solutions dès la première difficulté, n'oubliez pas les conseils suivants :
- Avez-vous bien fait un schéma au brouillon pour visualiser le problème posé ?
- Avez-vous essayé de rédiger un algorithme en français, avec vos propres mots, avant de vous lancer dans la programmation sur machine ?
- Avez-vous utilisé des affichages intermédiaires, des
print()
, pour visualiser au fur et à mesure le contenu des variables ? - Avez-vous testé le programme avec les propositions de tests donnés dans l'exercice ?
- Avez-vous testé le programme avec de nouveaux tests, différents de ceux proposés ?
Rappels
- Pour exécuter ce programme, il suffit de le sauvegarder puis d'appuyer
sur la touche
[F5]
. - Le programme principal doit contenir un appel au module
doctest
:##----- Programme principal et tests -----## if __name__ == '__main__': import doctest doctest.testmod()
Introduction☘
Les exercices de cette page sont issus d'une situation commune, décrite ci-après. Toutes les fonctions devront être sauvegardées dans le même fichier.
Téléchargez ce fichier à compléter ProgG06.60.py
(clic droit -> [Enregistrer sous]) et enregistrez-le dans le dossier
[G06_Algo_KNN]
.
Le problème☘
On dispose d'une base de données sur le diabète, à télécharger présentant huit colonnes de diverses mesures (insuline, âge, ...). La neuvième colonne présente des 0 et des 1 :
- 1 signifie que la personne suit un traitement pour des problèmes liés au diabète,
- 0 qu'elle n'en suit pas.
Il s'agit de nos deux classes pour ce problème.
Partie A - Préparer la base de données☘
Un patient est caractérisé par neuf données. Utiliser un dictionnaire pour décrire ce patient risque d'être laborieux (surtout à la lecture des attributs).
Dans cette partie, vous allez importer les données et les stocker dans un
tableau de tableaux, où chaque sous-tableau sera de la forme
[a, b, c, d, e, f, g, h, i]
telle que a
représente la valeur du premier
attribut, b
celle du deuxième attribut, etc...
-
Complétez la définition de la fonction
importation()
en respectant ses spécifications. Seules les valeurs des attributs'Body mass index (weight in kg/(height in m)^2)'
et'Diabetes pedigree function'
doivent être flottantes, les autres doivent être entières.1 2 3 4 5
def importation(nom_fichier): """ nom_fichier - str, nom du fichier csv à importer Sortie: list - Tableau de tableaux représentant les données """
Une piste
Vous pouvez :
- soit effectuer une importation à l'aide de la fonction
importe_csv()
mais il faut alors faire attention de ne pas perturber l'ordre des attributs... - soit travailler directement sur le fichier CSV à partir des méthodes étudiées dans ce chapitre.
Une solution
Solution avec la fonction
importe_csv()
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
def importation(nom_fichier): """ nom_fichier - str, nom du fichier csv à importer Sortie: list - Tableau de tableaux représentant les données """ base = importe_csv(nom_fichier) result = [[] for _ in range(len(base))] info = base[0] for clef in info: print(clef) for i in range(len(base)): if clef != 'Diabetes pedigree function' and clef != 'Body mass index (weight in kg/(height in m)^2)': result[i].append(int(base[i][clef])) else: result[i].append(float(base[i][clef])) return result
Une autre solution
Solution avec un traitement direct du fichier :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
def importation(nom_fichier): """ nom_fichier - str, nom du fichier csv à importer Sortie: list - Tableau de tableaux représentant les données """ with open(nom_fichier, 'r', encoding='utf-8') as f_source: tab_lignes = f_source.readlines() tab_lignes.pop(0) result = [] for ligne in tab_lignes: ligne.rstrip('\n') # On enlève le saut en fin de ligne tab = ligne.split(',') # On transforme la ligne en tableau for i in range(len(tab)): # On transtype les éléments if i != 5 and i != 6: tab[i] = int(tab[i]) else: tab[i] = float(tab[i]) result.append(tab) return result
- soit effectuer une importation à l'aide de la fonction
-
Exécutez votre fonction en important la base
diabete.csv
puis vérifiez que les premiers tableaux de cette base sont ceux listés ci-dessous :1 2 3 4 5 6 7 8 9
>>> for tab in BASE_DE_DONNEES: ... print(tab) [6, 148, 72, 35, 0, 33.6, 0.627, 50, 1] [1, 85, 66, 29, 0, 26.6, 0.351, 31, 0] [8, 183, 64, 0, 0, 23.3, 0.672, 32, 1] [1, 89, 66, 23, 94, 28.1, 0.167, 21, 0] [0, 137, 40, 35, 168, 43.1, 2.288, 33, 1] [5, 116, 74, 0, 0, 25.6, 0.201, 30, 0]
Partie B - Classifier un nouveau patient☘
Un nouveau patient est caractérisé par les mesures suivantes :
1 |
|
Ce client devra-t-il suivre un traitement ?
-
Complétez la définition de la fonction
distance()
qui doit renvoyer la distance euclidienne entre deux données (représentées par des tableaux) sur lesNB_DONNEES
premiers éléments de chaque tableau.1 2 3 4 5
def distance(donnee1, donnee2): """ donnee1, donnee2 - list, tableaux dont on cherche la distance Sortie: float - Distance euclidienne entre les deux données """
Exemple de test
>>> donnee1 = [1, 121, 78, 39, 74, 39.0, 0.261, 28, 0] >>> donnee2 = [2, 127, 46, 21, 335, 34.4, 0.176, 22, 0] >>> distance(donnee1, donnee2) 263.7483027907478
Une piste
La distance distance euclidienne entre A(x, y, z, t) et B(a, b, c, d) est : \sqrt{(x-a)^2 + (y-b)^2 + (z-c)^2 + (t-d)^2}.
Une solution
1 2 3 4 5 6 7 8 9
def distance(donnee1, donnee2): """ donnee1, donnee2 - list, tableaux dont on cherche la distance Sortie: float - Distance euclidienne entre les deux données """ somme = 0 for i in range(NB_DONNEES): somme += (donnee1[i] - donnee2[i])**2 return sqrt( somme )
Remarque
Dans ce qui précède, on calcule des distances en utilisant des coordonnées qui ne sont pas comparables a priori (unités différentes, significations différentes, ...). Ce qui ne semble pas avoir beaucoup de sens a priori.
Nous avons en fait simplifié l'ensemble en passant sous-silence tout le travail de prétraitement des données qui est nécessaire mais trop technique pour nous concentrer seulement sur le principe, au risque de conclusions non pertinentes !
-
Complétez la définition de la fonction
k_plus_proches()
qui doit renvoyer le tableau desk
données de la base les plus proches de la donnée à classifier.1 2 3 4 5 6 7 8 9
def k_plus_proches(k, a_classer, BDD): """ k - int, entier strictement positif (nombre de voisins) a_classer - list, tableau contenant NB_DONNEES éléments BDD - list, Tableau de tableaux contenant (NB_DONNEES + 1) éléments Sortie: list - tableau des k tableaux contenus dans BDD les plus "proches" de a_classer """
Exemple de test
>>> nouveauPatient = [1, 89, 67, 24, 80, 23.0, 0.6, 65] >>> k_plus_proches(1, nouveauPatient, BASE_DE_DONNEES) [[7, 94, 64, 25, 79, 33.3, 0.738, 41, 0]] >>> k_plus_proches(3, nouveauPatient, BASE_DE_DONNEES) [[7, 94, 64, 25, 79, 33.3, 0.738, 41, 0], [5, 96, 74, 18, 67, 33.6, 0.997, 43, 0], [3, 89, 74, 16, 85, 30.4, 0.551, 38, 0]] >>> k_plus_proches(5, nouveauPatient, BASE_DE_DONNEES) [[7, 94, 64, 25, 79, 33.3, 0.738, 41, 0], [5, 96, 74, 18, 67, 33.6, 0.997, 43, 0], [3, 89, 74, 16, 85, 30.4, 0.551, 38, 0], [1, 95, 74, 21, 73, 25.9, 0.673, 36, 0], [7, 83, 78, 26, 71, 29.3, 0.767, 36, 0]]
Une piste
Vous pouvez creer un tableau vide, à remplir avec des couples constitués de chaque donnée et de sa distance avec la donnée
a_classer
.Triez ensuite ce tableau selon les distances puis renvoyez un tableau des
k
premières données (sans les distances).Une solution
On définit une fonction supplémentaire comme critère de tri.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
def critere_2nd_elt(couple): return couple[1] def k_plus_proches(k, a_classer, BDD): """ k - int, entier strictement positif (nombre de voisins) a_classer - list, tableau contenant NB_DONNEES éléments BDD - list, Tableau de tableaux contenant (NB_DONNEES + 1) éléments Sortie: list - tableau des k tableaux contenus dans BDD les plus "proches" de a_classer """ tab_couples = [] for donnee in BDD: dist = distance(a_classer, donnee) tab_couples.append( (donnee, dist) ) tab_couples.sort(key=critere_2nd_elt) result = [] for i in range(k): result.append(tab_couples[i][0]) return result
-
Terminez cette parte en complétant la définition de la fonction
classification()
puis conclure en répondant à la question initiale : « ce client devra-t-il suivre un traitement ? ».1 2 3 4 5 6 7 8
def classification(k, a_classer, BDD): """ k - int, entier strictement positif (nombre de voisins) a_classer - list, tableau contenant NB_DONNEES éléments BDD - list, Tableau de tableaux contenant (NB_DONNEES + 1) éléments Sortie: str - classe de a_classer obtenue selon le principe k-NN """
Exemple de test
>>> nouveauPatient = [1, 89, 67, 24, 80, 23.0, 0.6, 65] >>> classification(1, nouveauPatient, BASE_DE_DONNEES) '0' >>> classification(3, nouveauPatient, BASE_DE_DONNEES) '0' >>> classification(5, nouveauPatient, BASE_DE_DONNEES) '0' >>> nouveauPatient = [6, 148, 72, 35, 0, 33.6, 0.627, 50] >>> classification(5, nouveauPatient, BASE_DE_DONNEES) '1' >>> classification(7, nouveauPatient, BASE_DE_DONNEES) '0' >>> classification(9, nouveauPatient, BASE_DE_DONNEES) '1'
Une piste
Faîtes appel au dictionnaore
DICO
défini dans les constantes pour décompter les classes parmi les voisins les plus proches.N'oubliez pas de réinitialiser le dictionnaire en fin d'appel de la fonction...
Une solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
def classification(k, a_classer, BDD): """ k - int, entier strictement positif (nombre de voisins) a_classer - list, tableau contenant NB_DONNEES éléments BDD - list, Tableau de tableaux contenant (NB_DONNEES + 1) éléments Sortie: str - classe de a_classer obtenue selon le principe k-NN """ kppv = k_plus_proches(k, a_classer, BDD) for donnee in kppv: DICO[str(donnee[-1])] += 1 max = 0 for clef in DICO.keys(): if DICO[clef] > max: max = DICO[clef] classe = clef # On ré-initialise le dictionnaire for clef in DICO.keys(): DICO[clef] = 0 return classe
Partie C - Fiabilité de la méthode☘
On aimerait avoir une idée de la fiabilité de la méthode.
Pour cela, on découpe les données en deux parties :
- une partie
BD
qui contiendra une certaine proportion des données du fichier. Par exemple 90 % des données. - une partie
TEST
qui contiendra le reste des données.
Comme on connaît la classe des individus de TEST
, on peut vérifier la
proportion de bonnes réponses par la méthode sur les éléments de TEST
en utilisant BD
comme jeu de données.
-
Complétez la définition de la fonction
extraction()
qui renvoie le couple (BD
,TEST
).
Vous utiliserez l'extraction aléatoire suivante : pour chaque donnée deBASE_DE_DONNEES
, cette donnée est placée une fois sur dix dans le tableauTEST
, sinon elle est placée dansBD
.1 2 3 4 5 6 7 8
def extraction(BDD): """ BDD - list, Tableau de tableaux contenant (NB_DONNEES + 1) éléments Sortie: tuple - couple de tableaux, le premier est constitué d'environ 90% des tableaux de BDD le second est constitué des tableaux restant """
Une solution
On fait appel à la fonction
randint()
du modulerandom
.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
def extraction(BDD): """ BDD - list, Tableau de tableaux contenant (NB_DONNEES + 1) éléments Sortie: tuple - couple de tableaux, le premier est constitué d'environ 90% des tableaux de BDD le second est constitué des tableaux restant """ BD = [] TEST = [] for donnee in BDD: if randint(1, 10) == 1: TEST.append(donnee) else: BD.append(donnee) return BD, TEST
Une autre solution
On mélange la
BASE_DE_DONNEES
à l'aide de la fonctionshuffle()
présente dans le modulerandom
puis on garde 90% des lignes à partir du début pour la tableBD
et les 10% restant pour la tableTEST
.A vous de l'implémenter...
-
Complétez la définition de la fonction
fiabilite()
qui renvoie le pourcentage des données deTEST
qui obtiennent une bonne classification quand on les compare avec leursk
plus proches voisins dans la baseBD
, oùk
est un paramètre de la fonction.1 2 3 4 5 6 7 8
def fiabilite(k, BD, TEST): """ k - int, entier strictement positif (nombre de voisins) BD, TEST - list, tableaux de tableaux contenant (NB_DONNEES+1) éléments Sortie: float - Renvoie le pourcentage de bonnes classifications lorsqu'on teste les données de la liste TEST avec le reste des données présentes dans BD """
Exemple de test
>>> BD, TEST = extraction(BASE_DE_DONNEES) >>> for k in range(1, 11): ... print(fiabilite(k, BD, TEST)) 0.6153846153846154 0.717948717948718 0.6282051282051282 0.7307692307692307 0.6923076923076923 0.7307692307692307 0.7051282051282052 0.7564102564102564 0.7307692307692307 0.7307692307692307
Une solution
Attention au type des données renvoyé par la fonction
classification()
.1 2 3 4 5 6 7 8 9 10 11 12 13 14
def fiabilite(k, BD, TEST): """ k - int, entier strictement positif (nombre de voisins) BD, TEST - list, tableaux de tableaux contenant (NB_DONNEES+1) éléments Sortie: float - Renvoie le pourcentage de bonnes classifications lorsqu'on teste les données de la liste TEST avec le reste des données présentes dans BD """ nb_succes = 0 # nombre de bonnes réponses for test in TEST: reponse = classification(k, test, BD) if reponse == str(test[-1]): nb_succes += 1 return nb_succes/len(TEST)
Conclusion
Le mélange des données permet que la partie extraite servant de base de référence soit modifiée à chaque exécution de la fonction
extraction()
. On obtient des taux de succès entre 60% et 90% en faisant quelques essais avec k = 10.A vous de faire des essais avec d'autres valeurs de k.