﻿# -*- coding: utf-8 -*-
"""knn_iris.ipynb
# Exercice guidé sur la classification d'iris - Apprentissage automatique supervisé

"""

from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()

"""## Description des données
`iris` obtenu dans le script précédent est un dictionnaire tel que :
- `iris['data']` est une matrice `numpy` dont chaque ligne contient les caractéristiques d'un iris : longueur, largeur de la sépale et de la pétale.  
- `iris['target']` est un vecteur contenant les variétés d'iris : setosa (0), versicolor (1) ou virginica (2).
## Prise en main du jeu de données
"""

np.shape(iris.data) # il X2_ a 150 iris, chacun aX2_ant 4 caractéristiques
"""*   Affichage des caractéristiques du 1er iris du jeu de données:"""
iris.data[0] # pour obtenir les caracteristiques du 1er iris
"""*   Affichage de l'étiquette (ou label ou classe) du 1er iris :"""
iris.target[0] # l'étiquette du 1er iris : 0 don setosa

## Tracé de graphiques
import matplotlib.pyplot as plt

X1_setosa =[]
X2_setosa = []
X1_versicolor= []
X2_versicolor= []
X1_virginica = []
X2_virginica = []

# Création des listes avec les 2 1eres caracteristiques de chaque classe (0,1,2):
for i in range( len(iris.target) ): # recupération de chaque iris
  liris = iris.data[i]
  classe = iris.target[i]

  if classe == 0 :
    X1_setosa.append(liris[0])
    X2_setosa.append(liris[1])
  elif classe ==1:
    X1_versicolor.append(liris[0])
    X2_versicolor.append(liris[1])
  else : 
    X1_virginica.append(liris[0])
    X2_virginica.append(liris[1])

# Tracé du graphique:
plt.title('Espèces d iris')
plt.xlabel('Longueur de sepale')  # titre de l'axe des abscisses
plt.ylabel('Largeur de sepale')  # titre de l'axe des ordonnées

"""  #version avec plot
plt.plot(X1_setosa,X2_setosa, marker='o', color='g', markersize=3, linestyle='', label="Setosa")
plt.plot(X1_versicolor,X2_versicolor, marker='o', color='r', markersize=3, linestyle='', label="Versicolor")
plt.plot(X1_virginica,X2_virginica, marker='o', color='b', markersize=3, linestyle='', label="Virginica")
plt.legend()
plt.show()
"""
#version avec plot
plt.scatter(X1_setosa,X2_setosa, color='g', label='setosa')
plt.scatter(X1_versicolor,X2_versicolor, color='r', label='versicolor')
plt.scatter(X1_virginica,X2_virginica, color='b', label='Virginica')
plt.legend()
plt.show()

"""Afficher les """

X_setosa = [ [],[],[],[] ]
X_versicolor= [ [],[],[],[] ]
X_virginica= [ [],[],[],[] ]

# Création des listes avec les 2 1eres caracteristiques de chaque classe (0,1,2):
for i in range( len(iris.target) ): # recupération de chaque iris
  liris = iris.data[i]
  classe = iris.target[i]

  if classe == 0 :
    for j in range (4) :
      X_setosa[j].append(liris[j])
  elif classe ==1:
    for j in range (4) :
      X_versicolor[j].append(liris[j])
  else : 
    for j in range (4) :
      X_virginica[j].append(liris[j])

# Tracé des graphiques:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,7))
fig.suptitle('Espèces d iris')

ax1.set_xlabel('Longueur de sepale')  # titre de l'axe des abscisses
ax1.set_ylabel('Largeur de sepale')  # titre de l'axe des ordonnées
"""
ax1.plot(X_setosa[0],X_setosa[1], marker='o', color='g', markersize=3, linestyle='', label="Setosa")
ax1.plot(X_versicolor[0],X_versicolor[1], marker='o', color='r', markersize=3, linestyle='', label="Versicolor")
ax1.plot(X_virginica[0],X_virginica[1], marker='o', color='b', markersize=3, linestyle='', label="Virginica")
"""
ax1.scatter(X_setosa[0],X_setosa[1], color='g', label='setosa')
ax1.scatter(X_versicolor[0],X_versicolor[1],  color='r', label='versicolor')
ax1.scatter(X_virginica[0],X_virginica[1], color='b', label='Virginica')

ax1.legend()

ax2.set_xlabel('Longueur de petale')  # titre de l'axe des abscisses
ax2.set_ylabel('Largeur de petale')  # titre de l'axe des ordonnées
ax2.scatter(X_setosa[2],X_setosa[3], color='g', label='setosa')
ax2.scatter(X_versicolor[2],X_versicolor[3],  color='r', label='versicolor')
ax2.scatter(X_virginica[2],X_virginica[3], color='b', label='Virginica')
"""
ax2.plot(X_setosa[2],X_setosa[3], marker='o', color='g', markersize=3, linestyle='', label="Setosa")
ax2.plot(X_versicolor[2],X_versicolor[3], marker='o', color='r', markersize=3, linestyle='', label="Versicolor")
ax2.plot(X_virginica[2],X_virginica[3], marker='o', color='b', markersize=3, linestyle='', label="Virginica")
"""
ax2.legend()

"""## Classification


### Séparation données d'entraînement / test
"""

def separer(X, Y, p):
    Xtrain, Xtest, Ytrain, Ytest = [], [], [], []
    for i in range(len(X)):
        if i <= p * len(X) :
            Xtrain.append(X[i])
            Ytrain.append(Y[i])
        else:
            Xtest.append(X[i])
            Ytest.append(Y[i])
    return np.array(Xtrain), np.array(Xtest), np.array(Ytrain), np.array(Ytest)
  
Xtrain, Xtest, Ytrain, Ytest = separer(iris.data, iris.target, .8) # cette séparation n'est pas deale avec notre jeu de données car les classes 0 sont toutes au début, puis les classes 1 ensemble, puis les classes 2.

# 2e version + adaptée au jeu de données iris:
# on met p*10 (p correspondant au %) iris sur 10 dans Xtrain , Ytrain, et 1/5 dans Xtest, Ytest dans l'ordre ou ils sont stockés
def separer2(X, Y, p):
    nb = p*10
    cptr = 0
    Xtrain, Xtest, Ytrain, Ytest = [], [], [], []
    for i in range(len(X)):
      if cptr <nb  :
            Xtrain.append(X[i])
            Ytrain.append(Y[i])
            cptr +=1
      else:
            Xtest.append(X[i])
            Ytest.append(Y[i])
            cptr +=1
            if cptr == 10 :            
              cptr = 0
      
    return np.array(Xtrain), np.array(Xtest), np.array(Ytrain), np.array(Ytest)
  
Xtrain, Xtest, Ytrain, Ytest = separer(iris.data, iris.target, .8)
Xtrain, Xtest, Ytrain, Ytest = separer2(iris.data, iris.target, .8)

"""

Afficher les caractéristiques (longueur, largeur de sépale et de pétale) de la première fleur des données d'entraînement, ainsi que sa variété :"""

print( Xtrain[0] ) # caractéristiques
print( Ytrain[0] ) # variété

"""### Algorithme des $k$ plus proches voisins
#### Calcul de distances : distance euclidienne
"""
def d(x, y):
    return ((x[0] - y[0])**2 + (x[1] - y[1])**2 + (x[2] - y[2])**2 + (x[3] - y[3])**2)**0.5

def d( x, y ) : # calcul de distances pour des tailles de vecteurs quelconques 
  s=0
  for i in range (0, len(x)):
    s = s + (x[i] - y[i])**2 
  return s**0.5

def d2(x, y):
    z = x - y
    return z.T.dot(z)


def voisins(x, X, k, dist):

    listeDistanceIndices=[] # liste de listes[distance, indice]
    for i in range ( len(X)) :
      distance = dist( x, X[i])
      listeDistanceIndices.append([distance, i])   
    listeDistanceIndices.sort()
    lesVoisins = [] # liste qui ne contiendra que les indices
    for i  in range( k ):
      lesVoisins.append( listeDistanceIndices[i][1] )
    return lesVoisins

k=9
x = Xtest[0]
V = voisins(x, Xtrain, k, d)


# variante en utilisant la fonction sorted
def voisins(x, X, k, dist): # renvoie les k plus proches voisins de x dans X
    indices = sorted(range(len(X)), key=lambda i: dist(x, X[i]))
    return indices[:k]

x = Xtest[0]
V = voisins(x, Xtrain, k, d)

"""*   Fonction `majoritaire(L)` qui détermine et renvoie la classe majoritaire de la liste L, passée en paramètre."""
def majoritaire(L):
  compte = {} # compte[e] = nombre d'occurrences de e dans L
  for e in L:
        if e in compte:
            compte[e] += 1
        else:
            compte[e] = 1
  kmax = L[0]
  for k in compte:
        if compte[k] > compte[kmax]:
            kmax = k
  return kmax
# test de la fonction en affichant la classe majoritaire du jeu de données
print( majoritaire( iris.target) ) 

# version 2 en utilisant la fonction get des dictionnaires
def majoritaire(L): # renvoie la classe qui apparaît le plus souvent dans L
    compte = {}
    for e in L:
        compte[e] = compte.get(e, 0) + 1
    return max(compte, key=compte.get)

"""*   A l'aide des fonctions précédentes, écrire la fonction `knn(x, X, Y, k, dist)` qui retourne la classe prédite pour x par l'algorithme des k plus proches voisins."""

def knn(x, X, Y, k, dist): # renvoie la classe prédite pour x par l'algorithme des k plus proches voisins
    # détermination des indices des k plus proches voisins
    lesVoisins = voisins(x, X, k, dist)

    # détermination des classes des k plus proches voisins
    lesClasses = [ Y[i] for i in lesVoisins ] 

    # determination de la classe majoritaire des k voisins obtenues qui sera la classe de x
    laClasseMajoritaire = majoritaire( lesClasses )
    return laClasseMajoritaire

def predict(i, k, d): # renvoie la classe prédite pour X_test[i]
    return knn( Xtest[i], Xtrain, Ytrain, k, d )

print("Classe prédite pour X_test[0] :", predict(0, 3, d2))
print("Classe réelle pour X_test[0] :", Ytest[0])

"""Ecrire la fonction `affichePrediction( x, classe_x , Xtrain , Ytrain)` qui affiche `x` et les `k` plus proches voisins de `x` ainsi que la classe prédite :"""

def affichePrediction( x, k , Xtrain , Ytrain ):

  X_setosa = [ [],[],[],[] ]
  X_versicolor= [ [],[],[],[] ]
  X_virginica= [ [],[],[],[] ]

  classe_x = knn(x, Xtrain, Ytrain , k, d)

  coul = ['g','r','b']
  neighbors_i = voisins(x, Xtrain, 7, d2) 
  neighbors = Xtrain[neighbors_i]

  # Création des listes avec les 2 1eres caracteristiques de chaque classe (0,1,2):
  for i in range( len(Ytrain) ): # recupération de chaque iris
    liris  = Xtrain[i]
    classe = Ytrain[i]

    if classe == 0 :
      for j in range (4) :
        X_setosa[j].append(liris[j])
    elif classe ==1:
      for j in range (4) :
        X_versicolor[j].append(liris[j])
    else : 
      for j in range (4) :
        X_virginica[j].append(liris[j])

  # Tracé des graphiques:
  fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,7))
  fig.suptitle('Espèces d iris')

  ax1.set_xlabel('Longueur de sepale')  # titre de l'axe des abscisses
  ax1.set_ylabel('Largeur de sepale')  # titre de l'axe des ordonnées
  ax1.scatter(X_setosa[0],X_setosa[1], color='g', label='setosa')
  ax1.scatter(X_versicolor[0],X_versicolor[1],  color='r', label='versicolor')
  ax1.scatter(X_virginica[0],X_virginica[1], color='b', label='Virginica')
  
  for i in range(len( neighbors )):
    couleur = coul[ Ytrain[neighbors_i[i]] ]
    ax1.scatter(neighbors[i, 0], neighbors[i, 1], s=120,  color=couleur,  marker='X')

  ax1.scatter(x[0],x[1], s=200,color=coul[classe_x] )
  ax1.legend()
 
  ax2.set_xlabel('Longueur de petale')  # titre de l'axe des abscisses
  ax2.set_ylabel('Largeur de petale')  # titre de l'axe des ordonnées
  ax2.scatter(X_setosa[2],X_setosa[3], color='g', label='setosa')
  ax2.scatter(X_versicolor[2],X_versicolor[3],  color='r', label='versicolor')
  ax2.scatter(X_virginica[2],X_virginica[3], color='b', label='Virginica')
  
  for i in range(len( neighbors )):
    couleur = coul[ Ytrain[neighbors_i[i]] ]
    ax2.scatter(neighbors[i, 2], neighbors[i, 3],s=120,  color=couleur,  marker='X')

  ax2.scatter(x[2],x[3], s=200,color=coul[classe_x] )
  ax2.legend()

  ax2.legend()
  #plt.show()

affichePrediction( x, k, Xtrain , Ytrain )

"""## Évaluation du modèle

Ecrire la fonction `precision(Xtest, Ytest, Xtrain, Ytrain, k, dist)` qui calculela précision càd le nombre de prédictions correctes sur le nombre de prédictions totales
"""

def precision(Xtest, Ytest, Xtrain, Ytrain, k, dist):

   # print (Xtest, Ytest)
    n = len(Xtest)
    p = 0
    for i in range(n):
        x = Xtest[i]
        classe_avec_knn= knn(x, Xtrain, Ytrain, k, dist)
        # print(classe)
        if classe_avec_knn == Ytest[i]: # la classe obtenue correspond-elle a la classe predite?
            p += 1
        # else:
        # 	print(f"Erreur pour X_test[{i}] : {iris.target_names[predict(i, k)]} au lieu de {iris.target_names[Y_test[i]]}")
    return p/n

precision(Xtest, Ytest, Xtrain, Ytrain, k, d)



def plot_precision(kmax, Xtest, Ytest, Xtrain, Ytrain, d):
    plt.figure()
    plt.plot(range(1, kmax), [precision(Xtest, Ytest, Xtrain, Ytrain,k, d) for k in range(1, kmax)])
    plt.xlabel("k")
    plt.ylabel("Précision")
    plt.show()
plot_precision(50, Xtest, Ytest, Xtrain, Ytrain, d2)
"

plot_precision(15, Xtest, Ytest, Xtrain, Ytrain, d2)



precision(Xtest, Ytest, Xtrain, Ytrain, k, d2)

"""Creer et afficher la matrice de confusion pour $k = 9$ :"""

k=9
M = np.zeros((3, 3), dtype=int)
for i in range(len(Xtest)):
  x = Xtest[i]
  cl = knn(x, Xtrain, Ytrain, k, d)
  M[cl][Ytest[i]] += 1
print(M)



"""## Autres distances

Essais d'autres distances pour comparer la précision :

### Distance de Manhattan
Créer la fonction d1 qui calcule la distance de Manhattan pour les vecteurs x et y.
"""

def dist_m(x, X2_):
    return sum(abs(x[i] - X2_[i]) for i in range(len(x)))

"""Calculer la précision en utilisant d1."""

precision(Xtest, Ytest, Xtrain, Ytrain, k, dist_m )

precision(Xtest, Ytest, Xtrain, Ytrain, k, dist_m) # on obtient la même précision qu'avec la distance euclidienne

"""### Distance de Thebychev"""

def dist_t(x, y):
    return max(abs(x[i] -y[i]) for i in range(len(x)))

precision(Xtest, Ytest, Xtrain, Ytrain, k, dist_t)

precision(Xtest, Ytest, Xtrain, Ytrain,k, dist_t)

"""**Conclusion** : l'utilisation d'autres distances n'a pas permis d'améliorer la précision.
