Aller au contenu

Un jeu de données sur le diabète

On dispose d'un fichier csv présentant 8 colonnes de diverses mesures (insuline, âge, ...).
Le séparateur de données est la virgule.

La 9è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.

Exercice 1

Un nouveau patient est caractérisé par les mesures suivantes (il s'agit, dans l'ordre, des données correspondant aux 8 premières colonnes):

nouveauPatient = [1, 89, 67, 24, 80, 23, 0.6, 65]

En réutilisant (et modifiant) le code écrit pour la base iris, ce nouveau patient devra-t-il suivre un traitement ? (testez diverses valeurs de k dans votre application du principe k-NN)

Code

On doit nécessairement apporter quelques modifications au dernier code utilisé pour les iris.

On a cherché à nouveau à faire ces modifications de façon à ce qu'elles soient le plus adaptables à un potentiel nouveau fichier de données.

import csv
from math import sqrt


# nom du fichier de données
# défini ici en constante préalable afin que toute modification ultérieure
# puisse autant que possible se limiter aux premières lignes de ce code
BASE_DE_DONNEES = 'diabete'

# on définit une constante correspondant au nombre de données (nombre de mesures)
NB_DONNEES = 8



# dictionnaire avec clefs = classes de la base de données
# Il est ici aussi créé en début de fichier pour mettre en évidence
# les parties à modifier lorsqu'on change de base de données
# (on pourrait automatiser ici la construction 
# de ce dictionnaire en fonction de la base de données)
# On rappelle que c'est dans la fonction "classification"
# que l'on utilise ce dictionnaire.
DICO = {'0': 0, '1':0}



with open(BASE_DE_DONNEES + '.csv', newline='') as fichierDonnees:
    lecture = csv.reader(fichierDonnees, delimiter=',')    
    # transformation de lecture en liste:
    lecture = list(lecture)


# on élimine la ligne de légende:
lecture.pop(0) 
# on transforme les NB_DONNEES premières données en flottant:
for i in range(len(lecture)): 
    for j  in range(NB_DONNEES):
        lecture[i][j] =  float(lecture[i][j])





def distance(donnee1, donnee2):
    """
    donnee1 -- élément de la base de données ou liste de mesures d'une donnée uniquement
    donnee2 -- élément de la base de données ou liste de mesures d'une donnée uniquement


    renvoie la distance euclidienne entre les deux données
    """
    # les écarts sur chaque coordonnée:
    ecarts = [donnee1[j] - donnee2[j] for j in range(NB_DONNEES)]
    # somme des carrés des écarts:
    sommeCarresEcarts = sum([ecart**2 for ecart in ecarts])
    return sqrt(sommeCarresEcarts)






def kPlusProchesVoisins(nouvelle_donnee, k):
    """
    nouvelle_donnee -- liste de NB_DONNEES mesures d'une nouvelle donnée  
    k -- entier naturel, compris entre 1 et le nombre de données dans le fichier de données

    renvoie la liste des k plus proches voisins 
    (pris dans les données du fichier .csv) de nouvelle_donnee.

    On rappelle que les données du fichier csv ont été récupérées dans une liste
    nommée lecture.
    """
    # on crée la liste des distances entre donnee et les données du fichier:
    L = [distance(nouvelle_donnee, donnee) for donnee in lecture]
    # on ajoute la donnée "indice":
    M = [(L[i], i) for i in range(len(L))]
    # on trie M suivant les distances, ordre croissant:
    M.sort(key= lambda x: x[0])
    # On récupère uniquement les indices des k premiers éléments:
    I = [ M[i][1] for i in range(k)]
    # on renvoie la liste des données correspondantes:
    return [lecture[k] for k in I]


def classification(nouvelle_donnee, k):
    """
    nouvelle_donnee -- liste de NB_DONNEES mesures d'une nouvelle donnée  
    k -- k>0

    renvoie la classe la plus fréquente 
    parmi les k plus proches voisins de nouvelle_donnee.
    """
    # remise à 0 du dictionnaire des classes:
    for clef in DICO.keys():
        DICO[clef] = 0
    # liste des voisins:
    ppv = kPlusProchesVoisins(nouvelle_donnee, k)
    # mise à jour compteurs:
    for donnee in ppv:         
        DICO[donnee[-1]] += 1  # donnee[-1] est  la classe de la donnée (classe lue dans le fichier csv)

    # on cherche maintenant le maximum des valeurs du dico:
    effectifs_classe = [(clef, valeur) for clef, valeur in DICO.items()]
    maxi = max(effectifs_classe, key= lambda x: x[1])
    return maxi[0]



if __name__ == '__main__':


    nouveauPatient = [1, 89, 67, 24, 80, 23, 0.6, 65]
    k = 2
    ppv = kPlusProchesVoisins(nouveauPatient, k)
    print(ppv)
    print("Patient 1 : ",classification(nouveauPatient, k))

    print()


    # on teste un patient déjà dans la base (il s'agit de la ligne 1 des données):
    nouveauPatient = [6, 148, 72, 35, 0, 33.6, 0.627, 50]
    k = 1 
    ppv = kPlusProchesVoisins(nouveauPatient, k)
    print(ppv)
    print("Patient 2 : ",classification(nouveauPatient, k))
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!

Exercice 2

On aimerait avoir une idée de la fiabilité de la méthode.

Pour cela, on découpe les données en deux listes:

  • une liste BD qui contiendra une certaine proportion des données du fichier. Par exemple 90% des données.
  • une liste 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.

Écrire une telle fonction et faîtes des essais.

Un code
import csv
from math import sqrt
from random import shuffle 

# nom du fichier de données
# défini ici en constante préalable afin que toute modification ultérieure
# puisse autant que possible se limiter aux premières lignes de ce code
BASE_DE_DONNEES = 'diabete'

# on définit une constante correspondant au nombre de données (nombre de mesures)
NB_DONNEES = 8



# dictionnaire avec clefs = classes de la base de données
# Il est ici aussi créé en début de fichier pour mettre en évidence
# les parties à modifier lorsqu'on change de base de données
# (on pourrait automatiser ici la construction de ce dictionnaire en fonction de la base de données)
DICO = {'0': 0, '1':0}






with open(BASE_DE_DONNEES + '.csv', newline='') as fichierDonnees:
    lecture = csv.reader(fichierDonnees, delimiter=',')    
    # transformation de lecture en liste:
    lecture = list(lecture)



# on élimine la ligne de légende:
lecture.pop(0) 
# on transforme les NB_DONNEES premières données en flottant:
for i in range(len(lecture)): 
    for j  in range(NB_DONNEES):
        lecture[i][j] =  float(lecture[i][j])


# on commence par mélanger la liste  de données
# cela permettrait de tester des variantes sur la base 
# et éviterait des biais dus à une base au préalable triée.
shuffle(lecture) 

# proportion 90% base, 10% tests:
p = int(0.9 * len(lecture)) 
# base de données:
BD = [lecture[i] for i in range(p)]
# éléments de tests:
TEST = [lecture[i] for i in range(p+1, len(lecture))]



def distance(donnee1, donnee2):
    """
    donnee1 -- élément de la base de données ou liste de mesures d'une donnée uniquement
    donnee2 -- élément de la base de données ou liste de mesures d'une donnée uniquement


    renvoie la distance euclidienne entre les deux données
    """
    # les écarts sur chaque coordonnée:
    ecarts = [donnee1[j] - donnee2[j] for j in range(NB_DONNEES)]
    # somme des carrés des écarts:
    sommeCarresEcarts = sum([ecart**2 for ecart in ecarts])
    return sqrt(sommeCarresEcarts)






def kPlusProchesVoisins(nouvelle_donnee, k, base_de_donnees):
    """
    nouvelle_donnee -- liste de NB_DONNEES mesures d'une nouvelle donnée  
    k -- entier naturel, compris entre 1 et le nombre de données dans le fichier de données
    base_de_donnees -- la base de données (qui ne sera plus ici tout le fichier de données 
    mais limitée à une partie de ce fichier, le reste servant de valeurs de tests).

    renvoie la liste des k plus proches voisins 
    (pris dans les données du fichier .csv) de nouvelle_donnee.

    On rappelle que les données du fichier csv ont été récupérées dans une liste
    nommée lecture.
    """
    # on crée la liste des distances entre donnee et les données du fichier:
    L = [distance(nouvelle_donnee, donnee) for donnee in base_de_donnees]
    # on ajoute la donnée "indice":
    M = [(L[i], i) for i in range(len(L))]
    # on trie M suivant les distances, ordre croissant:
    M.sort(key= lambda x: x[0])
    # On récupère uniquement les indices des k premiers éléments:
    I = [ M[i][1] for i in range(k)]
    # on renvoie la liste des données correspondantes:
    return [base_de_donnees[k] for k in I]


def classification(nouvelle_donnee, k):
    """
    nouvelle_donnee -- liste de NB_DONNEES mesures d'une nouvelle donnée  
    k -- k>0

    renvoie la classe la plus fréquente 
    parmi les k plus proches voisins de nouvelle_donnee.
    """
    # remise à 0 du dictionnaire des classes:
    for clef in DICO.keys():
        DICO[clef] = 0
    # liste des voisins:
    ppv = kPlusProchesVoisins(nouvelle_donnee, k, BD)
    # mise à jour compteurs:
    for donnee in ppv:         
        DICO[donnee[-1]] += 1  # donnee[-1] est  la classe de la donnée (classe lue dans le fichier csv)

    # on cherche maintenant le maximum des valeurs du dico:
    effectifs_classe = [(clef, valeur) for clef, valeur in DICO.items()]
    maxi = max(effectifs_classe, key= lambda x: x[1])
    return maxi[0]

def tests(k):
    """
    k -- entier > 0

    Renvoie le pourcentage de bonnes réponses 
    lorsqu'on teste les données de la liste TEST
    avec le reste des données pour base.
    """
    nb_succes = 0 # nombre de bonnes réponses
    for test in TEST:
        reponse = classification(test, k)
        if reponse == test[-1]: 
            nb_succes += 1
    return nb_succes/len(TEST)


if __name__ == '__main__':

    k = 10
    print(tests(k))

En début de script, on a ajouté un mélange des données de telle sorte que la partie servant de base soit modifiée à chaque lancement. 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.