C'est au moment où il fallait choisir un sujet pour notre projet de machine learning que nous avons reçu un email nous proposant de participer à un challenge open data sur le thème du cancer (le Challenge4Cancer). Notre engagement associatif (nous faisons tous les deux partie de Cheer Up!, association étudiante dont les membres visitent des jeunes atteints de cancer et les aident à réaliser des projets personnels) et notre conception de la data science comme une discipline qui peut avoir un impact positif sur la société nous a conduit à choisir ce challenge comme cadre pour notre projet.

C'est dans le cadre proposé par les organisateurs du challenge - la Paillasse et le laboratoire Roche - que nous avons étudiés les risques associés au cancer. Plus précisément, nous avons choisi d'axer nos recherches sur l'impact de la pollution et de certains facteurs comportementaux et sociaux sur la mortalité liée au cancer.

In [1]:
from IPython.display import Image
from IPython.core.display import HTML 
Image(url= "http://wiki.epidemium.cc/images/thumb/2/20/EPDM_visuel_Partenaires.jpeg/800px-EPDM_visuel_Partenaires.jpeg")
Out[1]:

Notre projet a la caractéristique d'avoir requis un temps de préparation considérable. Effectivement, nous avons dû tout d'abord effectuer un travail de recherche de bases de données pertinentes pour notre sujet (en tant que participants à un challenge Open Data, nous avions simplement décidé quel thème nous allions aborder avant d'avoir véritablement toutes les bases à notre disposition). Pour ce faire, nous avons utilisé des données récupérées sur différents sites Internet mais surtout celles proposées par le portail mis à notre disposition dans le cadre du challenge (http://data.epidemium.cc/dataset, plus de 20 000 data sets).

Plus précisément, voici les liens vers les datasets utilisés pour notre projet :

Abandonnées car trop peu pertinentes.

Nous avons des données sur la mortalité liée au cancer pour les deux sexes (plus précisément, le nombre de morts sur cinq ans dans chaque département français pour différents types de cancer, entre 1985 et 2009), c'est pourquoi nous avons créé trois bases : une pour les femmes, une pour les hommes et une pour les deux sexes (sachant que, bien entendu, chaque sexe est affecté par des cancers "généraux" et des cancers spécifiques, comme le cancer du sein). Ces trois bases et ces différents types de cancer pourront être utilisés par la suite pour affiner les modèles.

Du fait de la spécificité de notre sujet, nous avons formulé l'hypothèse que les facteurs de pollution affectaient la mortalité dûe au cancer mais sur le long terme. C'est pourquoi nous construisons un modèle de prédiction de la mortalité du cancer dans les départements français entre 2000 et 2009 à partir de données sur ces années ou sur les années précédentes.

Les données liées à la mortalité sont réparties dans des tableaux html sur plusieurs pages web du site de l'INVS. Les pages sont cependant indisponibles depuis quelques semaines. Voici malgré tout le programme qui a permis d'extraire les tableaux.

In [ ]:
from bs4 import BeautifulSoup
import csv
from urllib.request import urlopen

basicurl = 'http://webcancer.invs.sante.fr/mortalite_8408/print.php?return_url=%2Fmortalite_8408%2Fdonnees_localisation%2Fevolution%2Flocal.php%3Flocalisation%3DL{location}&data=tab_CI_02_GR_L{location}_S{gender}'
locationlist = ["0" + str(i) for i in range(1,10)] + [str(i) for i in range(10,27)] + ["99"]
validchars = ' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'


def souptocsv(basicurl, validchars, location, gender):
    try:
        soup = BeautifulSoup(urlopen(basicurl.format(location=location,gender=gender)))
        tables = soup.findAll('table') #cherche les tableaux dans la page
        table1 = tables[0]
        table2 = tables[1]
        rows = []
        for row in table1.find_all('tr'): #tr et td marquent les séparations
            rows.append([val.text for val in row.find_all('td')])

        title = gender + "".join([c for c in rows[1][0] if c in validchars])
        headers = [header.text for header in table2.find_all('th')]

        rows = []
        for row in table2.find_all('tr'):
            rows.append([val.text.encode('utf8') for val in row.find_all('td')])
        folder = 'Mortalite/'

        with open(folder + title + '.csv', 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerow(headers)
            writer.writerows(row for row in rows if row)
    except:
        print(location + gender)

for i in locationlist:
    souptocsv(basicurl, validchars, i, "1")
    souptocsv(basicurl, validchars, i, "2")

Les données météorologiques de Météo France ne sont accessibles que dans les bulletins météorologiques publiés en pdf. Les données mensuels correspondent aux années 1999 à 2015.

In [ ]:
#programme de téléchargement des données
def download_file(date):
    import urllib
    urllib.urlretrieve('https://donneespubliques.meteofrance.fr/donnees_libres/bulletins/BCM/{0}.pdf'.format(date), 'meteo/BCM{0}.pdf'.format(date))
    print("Completed")
    
#Nous bouclons sur les dates de publication des bulletins météo
date = 199901
while date < 201511:
    download_file(date)
    if date%100==12:
        date+=89
    else:
        date+=1

Les données se trouvent sur deux pages avec pour titre : "Résumé mensuel". Nous cherchons à savoir le numéro de ces pages pour chaque pdf.

In [ ]:
def fnPDF_FindText(xFile, xString):
    # xfile : chemin vers le pdf
    # xString : string à chercher
    import pyPdf, re
    pagenumber = set()
    pdfDoc = pyPdf.PdfFileReader(file(xFile, "rb"))
    for i in range(2, pdfDoc.getNumPages()):
        content = ""
        content += pdfDoc.getPage(i).extractText() + "\n"
        content1 = content.encode('ascii', 'ignore')
        ResSearch = re.search(xString, content1)
        if ResSearch is not None:
            pagenumber.add(i)
    return pagenumber

date = 199901
pagenumbers={}
while date < 201511:
    if date%100 == 1:
        print(str(date) + "**********************************************")
    try:
        pagenumbers[date]=fnPDF_FindText("meteo/BCM{0}.pdf".format(date), 'Rsum mensuel')
    except ValueError: #Des erreurs de lecture sont possibles
        print(date)
    if date%100==12:
        date+=89
    else:
        date+=1

Voici les deux principales fonctions qui convertissent une page pdf en html, puis parsent le html :

In [ ]:
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import HTMLConverter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfpage import PDFPage
from cStringIO import StringIO
import re
import csv
import time
from BeautifulSoup import BeautifulSoup


def convert_pdf_to_html(path,pagenos):
    rsrcmgr = PDFResourceManager()
    retstr = StringIO()
    codec = 'utf-8'
    laparams = LAParams()
    device = HTMLConverter(rsrcmgr, retstr, codec=codec, laparams=laparams)
    fp = file(path, 'rb')
    interpreter = PDFPageInterpreter(rsrcmgr, device)
    password = ""
    maxpages = 0
    caching = True
    for page in PDFPage.get_pages(fp, pagenos, maxpages=maxpages, password=password,caching=caching, check_extractable=True):
        interpreter.process_page(page)
    fp.close()
    device.close()
    string = retstr.getvalue()
    retstr.close()
    return string

def clean_html(alltext):
    data = []
    complementposition=1000
    soup = BeautifulSoup(alltext)
    divs = soup.findAll('div')
    splitdiv=""
    splitbr=""
    for div in divs:
        div_string = str(div)
        splitdiv+=div_string

        div_string = div_string.replace('<br />', '')
        div_list = div_string.split('\n')
        div_list = map(lambda x: x.strip('[').strip(']'),div_list)
        if checkcomplement(div_list[0]):
            complementposition = int(find_between(div_list[0],"top:", "px"))
        record = []
        for item in div_list:
            record.append(item.split(':', 1))
        data.append(record)
    return(data, complementposition)

headers = "STATIONS,TN,TX,TNN,D,TXX,D,H.RR,RMAX,D,INST,FXI,D".split(",")
headersdico={"STATIONS" : str, "TN" : float, "TX" : float, "TNN" : float, "D" : int,
            "TXX" : float, "H.RR" : float, "RMAX" : float, "INST" : int,
            "FXI" : int}

Le code de la plupart des sous-fonctions ne sera pas montré. Il varie en fonction des pdfs (entre 1999 et 2015, le format des pdfs a beaucoup évolué et ainsi l'interprétation par le module pdfminer varie énormément). L'essentiel du travail repose pourtant sur les ajustements à faire.

  • Dans beaucoup de pdfs, il y a des données manquantes, marquées comme des "*".
  • Parfois, ces données manquantes sont complétées dans un pdf ultérieur grâce à un tableau nommé "compléments d'information". Il faut ainsi ignorer les données situées en dessous du titre "compléments d'information".
  • Afin de reconnaître des données à récupérer dans le pdf, plusieurs stratégies ont été mises en place, selon les dates. On a établi une liste de caractères acceptées afin de former des floats, on a déterminé les positions verticales acceptées afin de délimiter le tableau. Enfin, la taille de la police servait également d'indice pour différencier les headers des données elles-mêmes.
  • Pour certaines données, des solutions n'ont pas été trouvées. La colonne INST est irrécupérable, car les données manquantes sont trop nombreuses et marquées par des espaces vides. La détection des espaces vides est possible. Cependant, pdfminer ne permet pas de faire la différence entre 1 et 2 valeurs manquantes consécutives.
In [ ]:
import csv

date = 199901

while date < 201511: #pour chaque pdf, nous travaillons sur la récupération des données
    path ="meteo/BCM{0}.pdf".format(date)
    outpath = "meteoout/BCM{0}.csv".format(date)
    
    with open(outpath, "a") as f:
        writer = csv.writer(f)
        f.write(','.join(headers) + '\n')
        for pagefrompdf in date_pages[date]: #pour chaque page retenue du pdf
            donnees = []
            alltext = convert_pdf_to_html(path, [pagefrompdf]) #on transforme la page en html
            data, complementposition=clean_html(alltext) #on nettoie une partie du code html, en repérant
                                                            #la position verticale du "complément d'informations" 
            data, headersposition = wordsearch(data, '>STATIONS') #on cherche la position du header.
            donneestation,fontsize=getstationsdata(data, headersposition, complementposition) # on récupère les
            donnees.append(donneestation)                                             # noms des stations
            
            donnees+=trouveur(data, fontsize, complementposition) #récupération des données
            try:
                writer.writerows(clean_data(donnees, date))
            except IndexError: #Les erreurs sont nombreuses. Connaître les dates des erreurs permet de faire
                                # des modifications ciblées sur des groupes de dates (avec pour hypothèse que
                                # pour des dates proches, le format des tables est semblable.)
                print(date)
    if date%100==12:
        date+=89
    else:
        date+=1

Dans un second temps, nous avons dû nettoyer les bases extraites. Là, le site Dataiku et le Data Science Studio nous ont été d'une aide précieuse (nous y avons un accès gratuit le temps du challenge). Grâce au DSS, nous avons pu centraliser en un seul endroit tous nos datasets et les nettoyer sans avoir recours à chaque fois à du code Python. En outre, on avait la possibilité de visualiser en un instant les changements que nous avions effectués sur les données et leur mise en forme.

Néanmoins, ce fut également une étape très chronophage pour nous. Effectivement, le nombre de bases à traiter était élevé (plus d'une cinquantaine) et nous avons dû aussi faire face à des problèmes de mise en forme des données. Par exemple, certaines bases contenaient des données sur les communes et non pas sur les départements ; nous avons donc dû réfléchir à la meilleure manière d'agréger ces données et d'en tirer des informations représentatives au niveau départemental. En outre, nous avons dû sélectionner parmi la masse d'informations dont nous disposions les variables les plus pertinentes. A la fin, nous n'avons retenu que 1 500 variables.

En outre, le DSS a parfois eu des bugs qui nous ont fait perdre du temps : pour certaines bases, quand on voulait supprimer une colonne, seul le nom était supprimé et tout le reste des colonnes se décalait. Il a donc aussi fallu porter une attention particulière à ces bugs.

Enfin, une bonne partie de notre temps a aussi été consacrée au nettoyage général des données.

Voici une image montrant une partie des contributions de Joseph Lam :

In [3]:
from IPython.display import Image
PATH = 'C:\\Users\\lamjo_000\\Dropbox\\ENSAE C4C\\France\\Bases to merge\\Bases_recuperees\\'

Image(filename= PATH + 'JF_contributions.png')
Out[3]:

Voici une image montrant une partie des contributions de Benoît Choffin :

In [4]:
Image(filename= PATH + 'BC_contributions.png')
Out[4]: