Open In Colab

Mickaël Tits CETIC [email protected]

Chapitre 4 - Un exemple concret: analysons quelques bien immobiliers...

Le dataset simulé contient des maisons à vendre. Les informations sur ces maisons sont leur adresse, le site de référence (Immoweb ou Immovlan), le prix de vente, la surface du bien, et le nombre de pièces.

  • Quelle est le "meilleur" site de référence ? (immoweb ou immovlan ?)
  • Les maisons sont-elles plus chères sur un des sites (pour savoir si une plateforme est plus utilisée pour les biens de luxe) ?
  • Quelle maison a le meilleur ratio rooms/price et surface/price ?
  • Quelle ville est la plus chère ?
In [6]:
#Un inventaire de biens immobiliers

houses_dataset = {"address":["Rue de Fer 26, 5000 Namur",
                             "Rue de Bruxelles 42, 5000 Namur",
                      "Porte de Namur 25, Bruxelles",
                      "Rue de L'Eglise 42, Charleroi",
                      "Rue Saint-ghislain 30, 6224 Fleurus",
                     "Rue de la Closière 20, Fleurus",
                     "Rue de la Closière 20, Fleurus",
                     "Rue de Fer 25, 5000 Namur",
                     "Rue du Luxembourg 15, 1000 Bruxelles",
                     "NaN",
                     "Rue de Bruxelles 42, 5000 Namur",
                     "Rue de la Loi 50, Bruxelles"],
          "website":["immoweb","immoweb","immoweb","immoweb","immoweb","immoweb","immoweb","immovlan","immovlan","immovlan","immovlan","immovlan"],
           "price": [400000,
                     350000,
                     400000,
                     150000,
                     "330000",
                     230000,
                     230000,
                     0,
                     -100,
                     "cent mille",
                     350000,
                     700000],
           "surface":[150,
                      200,
                      120,
                      150,
                      320,
                      175,
                      170,
                      170,
                      100,
                      100,
                      200,
                      220],
                 "rooms":[4,5,3,5,5,2,3,3,"two",0,4,3]}

print(houses_dataset)

#Note: Pour corser l'exercice, on peut orthographier des addresses identiques différemment (avec ou sans code postal, Rue de la Closière = Rue des Closières,  Boulevard = Bd, Saint = St, ...), pour rendre le nettoyage de données plus compliqué (ce qui est plus proche de la réalité)
{'address': ['Rue de Fer 26, 5000 Namur', 'Rue de Bruxelles 42, 5000 Namur', 'Porte de Namur 25, Bruxelles', "Rue de L'Eglise 42, Charleroi", 'Rue Saint-ghislain 30, 6224 Fleurus', 'Rue de la Closière 20, Fleurus', 'Rue de la Closière 20, Fleurus', 'Rue de Fer 25, 5000 Namur', 'Rue du Luxembourg 15, 1000 Bruxelles', 'NaN', 'Rue de Bruxelles 42, 5000 Namur', 'Rue de la Loi 50, Bruxelles'], 'website': ['immoweb', 'immoweb', 'immoweb', 'immoweb', 'immoweb', 'immoweb', 'immoweb', 'immovlan', 'immovlan', 'immovlan', 'immovlan', 'immovlan'], 'price': [400000, 350000, 400000, 150000, '330000', 230000, 230000, 0, -100, 'cent mille', 350000, 700000], 'surface': [150, 200, 120, 150, 320, 175, 170, 170, 100, 100, 200, 220], 'rooms': [4, 5, 3, 5, 5, 2, 3, 3, 'two', 0, 4, 3]}

Représentation des données

Pour mettre en pratique le principe d'encapsulation, nous allons créer une classe représantant un bien immobilier. Le code est ainsi plus modulaire et pourrait facilement être réutilisé dans un autre projet.

Néanmoins, on pourrait tout à fait se passer d'utiliser une classe, et simplament appliquer des fonctions sur les éléments du dictionnaire ou d'une autre représentation des données. Nous verrons en l'occurrence une manière plus pratique de représenter un dataset: le DataFrame.

In [0]:
class House:
  
  def __init__(self, address, website, price, surface, rooms):
    
    self.address = address
    self.website = website
    self.price = price
    self.surface = surface
    self.rooms = rooms
    
    #On vérifie la validité des données, et on les rend valides si possible, sinon on renvoie False (pas valide)
    self.is_valid = self.validate()
    
  def price_m2(self):
        
    return self.price/self.surface
  
  
  def price_rooms(self):
    
    return self.price/self.rooms  
  
  
  def get_street(self):
    
    address = self.address
    
    #Le nom de la rue est la partie avant le premier nombre (le numéro de la rue).
    for i in range(len(address)):
      c = address[i]
      if c in "0123456789":
        #c est un nombre, on peut sortir de la boucle
        break
    
    #On extrait le nom de la rue de l'adresse
    street = address[:i-1]
    
    return street
  
  
  def get_city(self):
    
    address = self.address
    
    #Le nom de la rue est la partie après le dernier nombre, ou une virgule si aucun code postal n'est renseigné. On cherche donc un nombre en commençant par la fin ( range(len(address),0,-1) )
    for i in range(len(address)-1,0,-1):
      c = address[i]
      if c in "0123456789,":
        #c est un nombre (ou une virgule), on peut sortir de la boucle
        break
    
    #On extrait le nom de la ville
    city = address[i+2:]
    
    return city  
  
  
  def validate(self):
    
    """
    Vérifie si l'objet est valide
    """    
    
    #Si le prix, la surface, ou le nombre de chambre ne sont pas des int, on essaye de les convertir, sinon pas valide
    try:
      self.price = int(self.price)
      self.surface = int(self.surface)
      self.rooms = int(self.rooms)
    except:
      return False
    
    #Si le prix <= 0, pas valide
    if self.price <= 0:
      return False
    
    #si l'addresse n'est pas un string ou si c'est "NaN", pas valide
    adr = self.address
    if (type(adr) is not str) or (adr == "NaN"):
      return False
    
    #si rooms n'est pas un entier, ou si c'est <= 0, pas valide
    if type(self.rooms) is not int or self.rooms <= 0:
      return False
    
    #si surface n'est pas un entier, ou si c'est <= 0, pas valide
    if type(self.surface) is not int or self.surface <= 0:
      return False    
    
    #Si aucun des "return" précédent n'a été réalisé, c'est que l'objet est valide
    return True
  
  
  def display(self):
    
    if self.is_valid:
      print("%s: %s, %d €, %d m2, %d rooms, %d €/m2, %d €/room (%s)" % (self.get_street(), self.get_city(), self.price, self.surface, self.rooms, self.price_m2(), self.price_rooms(), self.website) )
    else:
      print("L'élément n'est pas un élément valide:", self.address)
    

Créons maintenant une liste d'objets de type House:

In [0]:
#Nombre de maisons
n = len(houses_dataset["address"])

#Pour chaque élément du dataset, on crée un objet de type House avec ses propres attributs
houses = [House( houses_dataset["address"][i], houses_dataset["website"][i], houses_dataset["price"][i], houses_dataset["surface"][i], houses_dataset["rooms"][i]  ) for i in range(n)]

Correction des données

Avant de pouvoir concrètement analyser les données, il est important de traiter le dataset. En effet, des données réelles contiennent souvent des données invalides, manquantes ou non-pertinentes. Dans cet exemple, nous allons corriger ou éliminer les données invalides, et les doublons.

In [9]:
for item in houses:
  item.display()
  
#On ne garde que les éléments valides
valid_houses = [h for h in houses if h.is_valid]
Rue de Fer: Namur, 400000 €, 150 m2, 4 rooms, 2666 €/m2, 100000 €/room (immoweb)
Rue de Bruxelles: Namur, 350000 €, 200 m2, 5 rooms, 1750 €/m2, 70000 €/room (immoweb)
Porte de Namur: Bruxelles, 400000 €, 120 m2, 3 rooms, 3333 €/m2, 133333 €/room (immoweb)
Rue de L'Eglise: Charleroi, 150000 €, 150 m2, 5 rooms, 1000 €/m2, 30000 €/room (immoweb)
Rue Saint-ghislain: Fleurus, 330000 €, 320 m2, 5 rooms, 1031 €/m2, 66000 €/room (immoweb)
Rue de la Closière: Fleurus, 230000 €, 175 m2, 2 rooms, 1314 €/m2, 115000 €/room (immoweb)
Rue de la Closière: Fleurus, 230000 €, 170 m2, 3 rooms, 1352 €/m2, 76666 €/room (immoweb)
L'élément n'est pas un élément valide: Rue de Fer 25, 5000 Namur
L'élément n'est pas un élément valide: Rue du Luxembourg 15, 1000 Bruxelles
L'élément n'est pas un élément valide: NaN
Rue de Bruxelles: Namur, 350000 €, 200 m2, 4 rooms, 1750 €/m2, 87500 €/room (immovlan)
Rue de la Loi: Bruxelles, 700000 €, 220 m2, 3 rooms, 3181 €/m2, 233333 €/room (immovlan)
In [10]:
#Pour supprimer les doublons, on peut convertir la liste en dictionnaire d'objets House, en prenant comme clé l'adresse. En effet, dans un dictionnaire les clés sont uniques!
unique_houses_dict = {h.address:h for h in valid_houses}

print("Affichage des valeurs du dictionnaire:")
for key in unique_houses_dict:
  unique_houses_dict[key].display()


#On peut reconvertir les valeurs du dictionnaire en liste:
unique_houses = list(unique_houses_dict.values())

print("Affichage de la liste:")
for item in unique_houses:
  item.display()
Affichage des valeurs du dictionnaire:
Rue de Fer: Namur, 400000 €, 150 m2, 4 rooms, 2666 €/m2, 100000 €/room (immoweb)
Rue de Bruxelles: Namur, 350000 €, 200 m2, 4 rooms, 1750 €/m2, 87500 €/room (immovlan)
Porte de Namur: Bruxelles, 400000 €, 120 m2, 3 rooms, 3333 €/m2, 133333 €/room (immoweb)
Rue de L'Eglise: Charleroi, 150000 €, 150 m2, 5 rooms, 1000 €/m2, 30000 €/room (immoweb)
Rue Saint-ghislain: Fleurus, 330000 €, 320 m2, 5 rooms, 1031 €/m2, 66000 €/room (immoweb)
Rue de la Closière: Fleurus, 230000 €, 170 m2, 3 rooms, 1352 €/m2, 76666 €/room (immoweb)
Rue de la Loi: Bruxelles, 700000 €, 220 m2, 3 rooms, 3181 €/m2, 233333 €/room (immovlan)
Affichage de la liste:
Rue de Fer: Namur, 400000 €, 150 m2, 4 rooms, 2666 €/m2, 100000 €/room (immoweb)
Rue de Bruxelles: Namur, 350000 €, 200 m2, 4 rooms, 1750 €/m2, 87500 €/room (immovlan)
Porte de Namur: Bruxelles, 400000 €, 120 m2, 3 rooms, 3333 €/m2, 133333 €/room (immoweb)
Rue de L'Eglise: Charleroi, 150000 €, 150 m2, 5 rooms, 1000 €/m2, 30000 €/room (immoweb)
Rue Saint-ghislain: Fleurus, 330000 €, 320 m2, 5 rooms, 1031 €/m2, 66000 €/room (immoweb)
Rue de la Closière: Fleurus, 230000 €, 170 m2, 3 rooms, 1352 €/m2, 76666 €/room (immoweb)
Rue de la Loi: Bruxelles, 700000 €, 220 m2, 3 rooms, 3181 €/m2, 233333 €/room (immovlan)

Exploration des données

Quelle est la maison la moins chère au mètre carré ?

In [11]:
price_per_m2 = [h.price_m2() for h in unique_houses]
#Minimum
min_price = min(price_per_m2)
#Indice correspondant à ce prix
best = price_per_m2.index(min_price)
#Maison correspondante
best_house = unique_houses[best]

best_house.display()
Rue de L'Eglise: Charleroi, 150000 €, 150 m2, 5 rooms, 1000 €/m2, 30000 €/room (immoweb)

Quel est le "meilleur" site web immobilier ?

In [12]:
#Attention, si on retire la doublons, on crée un biais, puisqu'on a retiré des maisons valides d'une plateforme

#si on garde les éléments non-valides:
immovlan = [h for h in houses if h.website == "immovlan"]
immoweb = [h for h in houses if h.website == "immoweb"]

print("Avec les non-valide - immovlan:", len(immovlan), "immoweb:", len(immoweb))

#Si on ne garde que les éléments valides
immovlan = [h for h in valid_houses if h.website == "immovlan"]
immoweb = [h for h in valid_houses if h.website == "immoweb"]

print("Elements valides - immovlan:", len(immovlan), "immoweb:", len(immoweb))
Avec les non-valide - immovlan: 5 immoweb: 7
Elements valides - immovlan: 2 immoweb: 7

Si le dataset était réel et suffisamment grand pour extraire des statistiques fiables, on en déduirait peut-être que immoweb est plus gros et de meilleure qualité.

Quelle est la plateforme la plus branchée maisons de luxe ?

In [13]:
def meanprice(house_list):
  
  prices = [h.price for h in house_list]
  return int(sum(prices)/len(prices))

price_immovlan = meanprice(immovlan)
price_immoweb = meanprice(immoweb)

print("immovlan:", price_immovlan)
print("immoweb:", price_immoweb)
immovlan: 525000
immoweb: 298571

Si le dataset était réel et suffisamment grand pour extraire des statistiques fiables, on en déduirait peut-être que immovlan propose des maisons généralement plus chères que immoweb.

Quelle est la ville la plus chère ?

In [14]:
#Il faut d'abord extraire la liste des villes
cities = [h.get_city() for h in unique_houses]

#Pour extraire simplement les villes unique, on peut utiliser une autre collection d'objets pas encore vue: le "set". C'est une collection non-ordonnée d'éléments uniques. 
#Remarque: ca n'aurait pas fonctionné sur la classe House puisqu'on ne sait pas directement utiliser d'opérateur de test d'égalité sur les objets de type House
cities = list(set(cities))
print(cities)

#Pour chaque ville, on extrait la liste des maisons. On obtient donc une liste de listes!
cities_houses = [[h for h in unique_houses if h.get_city() == c] for c in cities]

#On calcule ensuite le prix moyen pour chaque liste:
cities_prices = [meanprice(l) for l in cities_houses]

print(cities_prices)
['Charleroi', 'Fleurus', 'Bruxelles', 'Namur']
[150000, 280000, 550000, 375000]

Si le dataset était réel et suffisamment grand pour extraire des statistiques fiables, on en déduirait peut-être que Bruxelles est la ville la plus chère.

Dans le Chapitre suivant, nous allons découvrir différentes librairies Python, qui nous permettront d'analyser bien plus efficacement un ensemble de données. C'est par ici: Chapitre 5: Les librairies Python pour l'analyse de données

Bonus - Exemple 2: un peu nutrition et de Natural Language Processing (NLP)

Dans cet exemple, nous avons une collection de denrées alimentaires, et quelques informations sur ces aliments. Chaque aliment a le même type de propriété (un nom, un poids, des valeurs énergétiques).

In [15]:
#extract singular versions of words in text (defaults plurals and exceptions are used, but you can override default rules)
def singulize(string, pluriels = None, exceptions = None):
  
  """
  Extract singular versions of words in text (defaults plurals and exceptions are used, but you can override default rules)
  Default plurals: {'ois':'ois','s':'','eaux':'eau','aux':'al','x':''}
  Default exceptions: {'os':'os','chacals':'chacal','souris':'souris','rabais':'rabais','prix':'prix', 'taux':'taux','rhinoceros':'rhinoceros','jus':'jus','noix':'noix','mais':'mais'}

  """

  #singulize (french ad-hoc method...)
  #règles du pluriel
  if pluriels is None:
      pluriels = {'ois':'ois','s':'','eaux':'eau','aux':'al','x':''}
  #exceptions
  if exceptions is None:
      exceptions = {'os':'os','chacals':'chacal','souris':'souris','rabais':'rabais','prix':'prix', 'taux':'taux','rhinoceros':'rhinoceros','jus':'jus','noix':'noix','mais':'mais', 'chips':'chips'}

  #Une fonction dans une fonction! Pourquoi diable ? Elle n'est pas appelée ailleurs que dans la fonction singulize. Par soucis de clarté, on la déclare donc uniquement dans la portée de cette fonction.
  def singulize_word(word):

      if word in exceptions:

          return exceptions[word]

      isplural = [word.endswith(k) for k in pluriels]

      if any(isplural):

          #take first key
          keyid = isplural.index(True)
          key = list(pluriels)[keyid]

          #to replace last occurence, reverse all strings and replace first occurrence
          return word[::-1].replace(key[::-1], pluriels[key][::-1], 1)[::-1]

      return word

  return ' '.join([singulize_word(word) for word in string.split()])



#remove stopwords from text (defaults stopwords are used but you can override default rules)
def remove_stopwords(string, stopwords = None):
      
  """
  Remove stopwords from text (defaults stopwords are used but you can override default rules)
  Default stopwords: ['de','du','le','les','aux','la','des', 'a', 'une', 'un', 'au','d','l']
  """
  #remove stopwords
  if stopwords is None:
      stopwords = ['de','du','le','les','aux','la','des', 'a', 'à', 'une', 'un', 'au','d','l','et']

  string = string.replace("d'","")
  string = string.replace("l'","")        
  return ' '.join([word for word in string.split() if word not in (stopwords)])



#Un petit test
sentence1 = "cake à la banane noix de cajou"
sentence2 = "cakes aux bananes et aux noix de cajou"

s1 = singulize( remove_stopwords(sentence1) )
s2 = singulize( remove_stopwords(sentence2) )



print(s1)
print(s2)

if s1 == s2:
  print("Les phrases sont identiques")
  

  
cake banane noix cajou
cake banane noix cajou
Les phrases sont identiques
In [16]:
# Pour éviter de redéfinir à chaque fois toutes les règles de traitement (pluriels, stopwords, exceptions) quand on appelle une fonction, on peut à la place créer une classe et définir les règles comme des attributs de cette classe.


class NLP:
  
  def __init__(self, plurals, stopwords, exceptions):
    
    self.pluriels = plurals
    self.stopwords = stopwords
    self.exceptions = exceptions
    
  def singulize(self, string):
    
    """
    Extract singular versions of words in text
    """    
    
    #Une fonction dans une fonction! Pourquoi diable ? Elle n'est pas appelée ailleurs que dans la fonction singulize. Par soucis de clarté, on la déclare donc uniquement dans la portée de cette fonction.
    def singulize_word(word):

        if word in self.exceptions:

            return self.exceptions[word]

        isplural = [word.endswith(k) for k in self.pluriels]

        if any(isplural):

            #take first key
            keyid = isplural.index(True)
            key = list(self.pluriels)[keyid]

            #to replace last occurence, reverse all strings and replace first occurrence
            return word[::-1].replace(key[::-1], self.pluriels[key][::-1], 1)[::-1]

        return word

    return ' '.join([singulize_word(word) for word in string.split()])
  
  def remove_stopwords(self, string):

    """
    Remove stopwords from text
    """

    string = string.replace("d'","")
    string = string.replace("l'","")        
    return ' '.join([word for word in string.split() if word not in (stopwords)])
  
  def simplify(self, string):
    
    string = self.remove_stopwords(string)
    return self.singulize(string)


#Définition unique des règles  
pluriels = {'ois':'ois','s':'','eaux':'eau','aux':'al','x':''}
exceptions = {'os':'os','chacals':'chacal','souris':'souris','rabais':'rabais','prix':'prix', 'taux':'taux','rhinoceros':'rhinoceros','jus':'jus','noix':'noix','mais':'mais', 'chips':'chips'}
stopwords = ['de','du','le','les','aux','la','des', 'a', 'à', 'une', 'un', 'au','d','l','et']

textprocessor = NLP(pluriels, stopwords, exceptions)

print(textprocessor.pluriels)


#Test (le résultat devrait être le même)
s1 = textprocessor.simplify(sentence1)
s2 = textprocessor.simplify(sentence2)

print(s1)
print(s2)

if s1 == s2:
  print("Les phrases sont identiques")
{'ois': 'ois', 's': '', 'eaux': 'eau', 'aux': 'al', 'x': ''}
cake banane noix cajou
cake banane noix cajou
Les phrases sont identiques
In [0]:
class Food:
  
  def __init__(self, nom, poids, calories_per_100g):
    
    self.nom = nom
    self.poids = poids
    self.cal = calories_per_100g
    
    self.simplify_name()

  def simplify_name(self):
    
    self.nom = singulize( remove_stopwords(self.nom) )
    self.nom = self.nom.capitalize()
    
  def totcal(self):
    
    return self.poids*self.cal/100
  
  def display(self):
    
    print("%s: %d cal/100g, %d g, calories totales: %d" % (self.nom, self.cal, self.poids, self.totcal()) )
In [0]:
#Un inventaire d'aliments

dataset = {"noms":["des pommes","chips",sentence1, sentence2, "pomme"], 
           "poids": [150, 120, 75, 80, 145], 
           "calories_per_100g":[52, 536, 320, 310, 53]}
In [19]:
n = len(dataset["noms"])

#Je peut utiliser la classe Food pour représenter mes aliments de manière structurée:
aliments = [Food(dataset["noms"][i], dataset["poids"][i], dataset["calories_per_100g"][i]) for i in range(0,n)]

for item in aliments:
  item.display()
Pomme: 52 cal/100g, 150 g, calories totales: 78
Chips: 536 cal/100g, 120 g, calories totales: 643
Cake banane noix cajou: 320 cal/100g, 75 g, calories totales: 240
Cake banane noix cajou: 310 cal/100g, 80 g, calories totales: 248
Pomme: 53 cal/100g, 145 g, calories totales: 76
In [20]:
#Lors de la création d'un aliment, son nom est déjà traité (voir la méthode __init__). La liste contient donc deux aliments qui ont le même nom: "Pomme"
#retirons les duplicatas
#On peut pour cela passer par un dictionnaire: chaque clé doit être unique. On utilise donc le nom de l'aliment comme clé:
aliments_dict = {item.nom:item for item in aliments}
unique_aliments = list(aliments_dict.values())

for item in unique_aliments:
  item.display()


def find_lightest(foods):
  
  """
  Arguments d'entrée:
  foods: liste d'aliments (objets de la classe Food)
  
  Résultats:
  un tuple contanent le meilleur aliment, et son indice dans la liste
  """

  allcals = [item.totcal() for item in aliments]
  mincals = min(allcals)
  best = allcals.index(mincals)
  
  return (aliments[best], best)

meilleur_aliment = find_lightest(aliments)[0]

print("Aliment le plus léger:", meilleur_aliment.nom)
Pomme: 53 cal/100g, 145 g, calories totales: 76
Chips: 536 cal/100g, 120 g, calories totales: 643
Cake banane noix cajou: 310 cal/100g, 80 g, calories totales: 248
Aliment le plus léger: Pomme
In [0]: