Entrepôts de données, année 2019

Mise en forme normale.

Dans ce cours, l’utilisation du terminal est très fortement recommandé.

Pour simplifier, vous pouvez télécharger une version du listing de données encore plus courte que la précédente, afin de pouvoir faire des testes plus rapidement ici.

Vous aurez besoin dans la suite de documentation sur la gestion des fichiers en Python. Vous trouverez cette documentation ici.

Attention. Vous ne pourrez pas faire ce TD sans avoir une indexation des lignes en binaire. Voir la correction du TD 2.

Mise en forme normal analytique

De nombreuses formes normales existent pour les bases de données OLTP. C’est également le cas pour les bases de données OLAP.

Le schema en étoile (ou flocon de neige) va décomposer une table en une table principale contenant des clefs extérieurs en une tables de faits et une table pour chaque dimension d’analyses.

Une analyse en cube consiste alors en une simple jointure entre deux tables de dimensions. Ces jointures peuvent par ailleurs être pré-calculés quand elles sont souvents utilisées à l’aide de vues matérialisée. Nous verrons ça dans le cours suivant.

Mise sous forme normal en étoile du document scihub en Python

Corrections

Nous allons mettre la table en forme normale en étoile. Pour ce faire, nous allons créer une table pour chaque dimension d’analyse. Une table pour la position géographique, une table pour l’url et une table date.

Le code est téléchargeable à l’adresse ici.

Quelques fonction pratiques

Les fonctions suivantes ont pour objectif de convertire en binaire les entier et réciproquement.

from datetime import datetime
def _convert_octet(entier):
    """
    Fonction qui encode sur 3 octets
    des nombres entiers.
    """
    return int.to_bytes(entier, 3, "little")

def _convert_int(octets):
    """
    Fonction qui décode des entiers
    codés sur 3 octets.
    """
    return int.from_bytes(octets, "little")

def _convert_triplet(octets):
    """
    Fonction qui décode des triplets
    d'entiers encodés sur 3 octets
    """
    a = _convert_int(octets[:3])
    b = _convert_int(octets[3:6])
    c = _convert_int(octets[6:])
    return (a, b, c)

La fonction principale

La fonction suivante va automatiquement extraire la table des faits depuis le fichier scihub et sauvegarder les informations dans des tables dimensionnelles

def extraire_faits():
    format_tmps = "%Y-%m-%d %H:%M:%S"
    # format utilisé dans les logs pour l'encodage
    # des valeurs temporelles.
    dates = {} # Un dictionnaire par fait
    locs = {}
    urls = {}
    count_date = 0 # Un compteur par fait
    count_loc = 0
    count_url = 0
    with open("scihub.shorter.shorter.tab") as f, open("fact_table", "wb") as g:
        for line in f:
            dec = line.rstrip().split("\t") # rstrip enlève les espaces en trop
            # et retours à la ligne
            tps = int(datetime.strptime(dec[0], format_tmps).timestamp())
            #converti la date en une date numérique
            url = dec[1]
            loc = tuple(dec[4:6]) # tuple permet de rendre dec[4:6] hashable et
            # donc lui permettre d'être mis dans un dictionnaire.
            # On stock les inforamtions dans des ensembles

            if tps not in dates:
                dates[tps] = count_date
                # si tps n'a jamais été vu, on l'ajoute au dictionnaire.
                # L'identifiant de tps est count_date.
                count_date += 1
                # On incrémente count_date pour la prochaine date.
            #On fait pareil pour les autres faits.
            if loc not in locs:
                locs[loc] = count_loc
                count_loc += 1
            if url not in urls:
                urls[url] = count_url
                count_url += 1

            tps_oct  = _convert_octet(dates[tps]) # convertis l'identifiant de
            # tps en binaires. Pareil pour les autres dimensions.
            locs_oct = _convert_octet(locs[loc])
            urls_oct = _convert_octet(urls[url])
            g.write(tps_oct+locs_oct+urls_oct) #Ajoute à la table des faits les
            #informations en binaires.

    # Pour chaque table de faits, on doit maintenant faire
    # correspondre à chaque identifiant sa valeur.
    # Pour ce faire, on va créer un fichier binaire contenant l'indexes des
    # lignes et un fichier texte contenants la liste des faits.

    sauvegarde_faits(dates, "dates")
    sauvegarde_faits(locs, "locs")
    sauvegarde_faits(urls, "urls")
def sauvegarde_faits(faits, nom):
    """
    Fonction permettant de sauvegarder
    et indexer des faits dans un fichier
    "nom" et un fichier "nom_idx" contenant
    son index de ligne.

    "faits" est un dictionnaire qui a une valeur associe
    son index.
    """
    liste_faits = list(faits.keys()) #la liste des faits.
    # Cette liste n'est pas trié par ordre d'apparition.
    liste_faits.sort(key=lambda e:faits[e])
    # On trie par ordre croissant de l'index du fait.
    pos = 0 # position courrante dans le fichier texte.
    with open(nom, "w") as f, open(nom+"_idx", "wb") as g:
        for fait in liste_faits:
            f.write(str(fait)+"\n") #crée une ligne avec le fait.
            g.write(_convert_octet(pos)) #Indique dans le fichier d'index
            #l'adresse de la ligne
            pos += len(str(fait).encode()) + 1 # met à jour la position de l'adresse
            # suivante.

Fonction en lecture

La fonction suivante va lire des faits à l’aide des fichiers d’indexes.

def lire_un_fait(fait_id, nom):
    """
    Fonction qui récupère un fait dans une table de fait.
    """
    with open(nom+"_idx", "rb") as f:
        f.seek(fait_id*3)
        adresse = _convert_int(f.read(3))

    with open(nom) as g:
        g.seek(adresse)
        txt = g.read(1000) # lit 1000 caractères.
        while "\n" not in txt:
            txt += g.read(1000)
        return txt.split("\n")[0]


def lire_ligne_faits(numero_ligne):
    """
    Fonction qui lit une table des faits.
    """
    with open("fact_table","rb") as f:
        f.seek(numero_ligne*3)
        oct_tuple = f.read(9)
        tps_id, loc_id, url_id = _convert_triplet(oct_tuple)
        # On a récupérer les id, on peut récupérer les faits.
        tps = lire_un_fait(tps_id, "dates")
        loc = lire_un_fait(loc_id, "locs")
        url = lire_un_fait(locs_id, "urls")
    # on convertit les faits aux bons types.
    return (int(tps), eval(loc), url)
def tableaux_croises(fait1, fait2):
    l = ["dates", "locs", "urls"]
    id1 = l.index(fait1) # index du premier fait
    id2 = l.index(fait2) # index du second fait
    id3 = set([0, 1, 2]).difference([id1, id2]).pop()
    # index du fait qui reste.
    with open("fact_table", "rb") as f:
        x = f.read() # charge la table en mémoire
        n = len(x) // 9
        couples = {}
        for i in range(n):
            triplet = _convert_triplet(x[9*i:9*(i+1)])
            couple = (triplet[id1], triplet[id2])
            if couple in couples:
                couples[couple].append(triplet[id3])
            else:
                couples[couple] = [triplet[id3]]
    return couples
def tableau_projete(fait1, fait2):
    l = ["dates", "locs", "urls"]
    id1 = l.index(fait1) # index du premier fait
    id2 = l.index(fait2) # index du fait projeté
    with open("fact_table", "rb") as f:
        x = f.read() # charge la table en mémoire
        n = len(x) // 9
        vals = {}
        for i in range(n):
            triplet = _convert_triplet(x[9*i:9*(i+1)])
    return vals

Compiled the: dim. 07 janv. 2024 23:19:22 CET