Modélisation de réseaux sociaux, base de données orientées graphe

De Wiki du LAMA (UMR 5127)
Aller à la navigation Aller à la recherche

Qu'est-ce qu'un graphe ?

Un graphe est la donnée d’un ensemble de points qu’on appelle des sommets et d’un ensemble de ligne qu’on appelle des arêtes reliant certains sommets entre eux. On peut aussi noter qu’un graphe est dit complet lorsque deux sommets quelconques distincts sont toujours adjacents. Autrement dit, tous les sommets sont reliés deux à deux par une arête, comme sur la deuxième image par exemple.

Graphe.png Graphe complet.png


graphe de réseau "petit monde"

Pour ce projet, on cherche à modéliser un réseau social se présentant sous forme de graphe, un réseau social est un réseau "petit monde". Un réseau "petit monde" est un réseau dans lequel chaque individu peut être relié à n’importe quel autre individu par une courte chaîne de relations sociales. Le réseau « petit monde » est un modèle mathématique utilisé pour modéliser des réseaux réels, notamment des réseaux sociaux.

Ce concept reprend l’expérience de Stanley Milgram en 1967 avec son concept de « six degrés de séparation ». Les réseaux sociaux ont la propriété de petit monde qui est capturé par ce modèle : dans la majorité des cas, deux nœuds, c’est-à-dire deux personnes, peuvent être reliés par un très petit nombre d’amis intermédiaires.

Créations des graphes

Nous allons ensuite créer un graphe aléatoire à l'aide des algorithmes suivant:

Le premier algorithme permet de créer un graphe représentant l'invitation dans le réseau de nouveaux individus, le graphe sera donc représenté sous forme d'arbre. Le deuxième algorithme a pour but de renforcer les liens entre les individus au sein du réseau social de manière aléatoire représentant l'ajout des amis qui ont les mêmes centres d'intérêt, on choisit le coefficient rho d'arêtes aléatoires que l'on désire. Le troisième algorithme permet de renforcer une nouvelle fois les liens mais cette fois ci non pas de manière aléatoire mais avec des nœuds voisins, qui seraient représentés par des amis d'amis dans un réseau social.

Une fois les trois algorithmes réalisés, on obtient un graphe avec un coefficient d'arêtes aléatoires par rapport au rho choisi, ces graphes sont affichés sous forme de matrice. On peut ensuite visualiser le graphe sur neo4j grâce à la fonction create_graphe(G).


création d'un graphe aléatoire de n nœuds et de n liaisons

def graphe(nbNoeuds):
    G = { "V": [], "E": []}
    existe = True
    test = 0
    for i in range(1,nbNoeuds + 1):
        G["V"] = G["V"] + [i]

    for i in G["V"]:
        a = randint(1,i)
        test = 0
        existe = True
        while existe == True:
            test = 0
            compteur = 0 
            for j in G["E"]:
                compteur = compteur + 1 
                if j == [a,i] or j == [i,a] or i == a :
                    a = randint(1,i)
                else:
                    test = test + 1
            if test == compteur:
                existe = False
        G["E"] = G["E"] + [[i,a]]

    return G

Renforcement des liens avec rho % d'arêtes aléatoires

def renforcement_Graphe(G,rho,E):
    limite = rho*(E - (len(G["E"])))
    nU = []
    for t in G["V"]:
        nU = nU + [t]
    i = 0

    while i<limite and len(nU)>0:
        u = random.choice(nU)
        nV = []
        for a in G["V"]:
            nV = nV + [a]
        nV.remove(u)
        arêtecréée = False

        while len(nV)>0 and arêtecréée == False :
            compteur = 0
            test = 0
            v = random.choice(nV)
            nV.remove(v)
            for j in G["E"]:
                test = test + 1
                if j == [u,v] or j == [v,u]:
                    compteur = compteur 
                else:
                    compteur = compteur + 1
            if compteur == test:
                G["E"] = G["E"] + [[u,v]]
                i = i + 1
                arêtecréée = True
        if arêtecréée == False:
            nU.remove(u)
    return G


Renforcement des liaisons

def renforcement2(G,E):
    limite = E - len(G["E"])
    n = []
    for t in G["V"]:
        n = n + [t]
    i = 0
    voisins = []
    test = 0
    compteur = 0

    while i<limite and len(n)>0:
        u = random.choice(n)
        for k in range(len(G["E"])):
            for j in range(len(G["E"][k])):
                if G["E"][k][j] == u:
                    if j == 0:
                        voisins = voisins + [G["E"][k][1]]
                    else :
                        voisins = voisins + [G["E"][k][0]]           

        arêtecréée = False
        while len(voisins)>0 and arêtecréée == False:
            v = random.choice(voisins)
            voisins.remove(v)
            for w in voisins:
                test = 0
                compteur = 0
                for j in G["E"]:
                    test = test + 1
                    if j == [v,w] or j == [w,v]:
                        compteur = compteur 
                    else:
                        compteur = compteur + 1
                if compteur == test:
                    G["E"] = G["E"] + [[v,w]]
                    i = i + 1
                    arêtecréée = True
                
        if arêtecréée == False:
            n.remove(u)
    return G

Créer les graphes dans neo4j

def create_graphe(G):
    """ Creation du graph G dans neo4j """

    edges = G.get("E")
    vertices = G.get("V")
    
    with driver.session(database="neo4j") as session:
        #1- creation des sommets 
        for vertex in vertices :
            session.write_transaction(create_node, vertex)

        #2- creation des relations
        for edge in edges :
            session.write_transaction(create_relation, edge[0], edge[1])

    print("Graph created successfuly!")



def create_relation(tx, no1, no2):
    """ creattion d'une relation entre deux noeuds """

    cypher= """MATCH (n1:NODE), (n2:NODE)
            WHERE n1.no = $no1 AND n2.no = $no2
            MERGE (n1)-[r:RELATION]-(n2)
            RETURN n1,r,n2"""
    #2- execution de la requete 
    result = tx.run(cypher,no1=no1,no2=no2)
    #3- retour du res
    return result
    

def create_node(tx, no):
    """ tx tarnsaction appelee """
    #1 - requete cypher
    cypher = "CREATE (n:NODE) SET n.no = $no RETURN n"
    #2- execution de la requete 
    result = tx.run(cypher,no=no)
    #3- retour du res
    return result 

Analyse du graphe créé

Une fois le graphe créé il faut effectuer des tests dessus pour vérifier que le graphe obtenu correspond bien à un réseau social. Un réseau social est caractérisé par la taille moyenne des chemins ainsi que par son coefficient de clustering.


La taille moyenne des chemins d'un graphe est le nombre moyen des chemins pour relier un nœud à un autre.


Le coefficient de clustering d'un graphe aussi appelé coefficient d'agglomération est une mesure de regroupement des nœuds dans un réseau. Ce coefficient varie de 0 et 1, il représente la probabilité que deux nœuds soient connectés sachant qu'ils ont un voisin en commun, ce qu'on appel la transitivité. Lorsque le coefficient est à 0, le graphe forme un arbre alors que lorsque le coefficient est à 1, le graphe est un graphe complet, c'est à dire que tous les nœuds sont reliés les uns aux autres. On peut obtenir le coefficient de clustering grâce à la formule suivante : (nombre de triangles X 3 / nombre de triades connexes) Par exemple dans le cas du schéma ci-dessous, il y a deux triangles et dix triades connexes (123,231,312,345,453,534,135,134,234,235). On obtient donc un coefficient de clustering égal à 0.6 (6/10).

Clusteringcoef.jpg


On va donc utiliser des algorithmes pour obtenir ces différentes valeurs. On ne peut pas directement les appliquer, il faut donc utiliser l'algorithme de Floyd-Warshall. Floyd-Warshall prend en entrée une matrice d'adjacence et renvoi en sortie une matrice qui regroupe tous les plus courts chemins pour relier un nœud à un autre. Le principe de Floyd-Warshall est simple, il suffit de parcourir la matrice d'adjacence et de regarder si deux nœuds peuvent être reliés entre eux par l’intermédiaire d’un autre nœud, si oui on change la valeur dans la matrice par le nombre de nœuds parcourus pour atteindre l’objectif si cette valeur est inférieure à la valeur initiale. Une fois cet algorithme réalisé sur la matrice d'adjacence initiale, on obtient une matrice des plus courts chemins. On peut ensuite appliquer le coefficient de clustering et la taille moyenne des chemins sur cette matrice obtenue.


Dans le cas du graphe suivant, on a une matrice d'adjacence avec la taille des chemins et les "-" représentant les chemins impossibles, puis on obtient à partir de celle-ci la matrice de Floyd-Warshall qui permet de montrer des liens entre deux nœuds qui n'ont pas de lien direct.

Graphe2.png Matrices.png


Algorithme de Floyd-Warshall

def fw_floyd_warshall_k(FW,k):
    """
    intercale k entre chaque couple de sommet 
    """
    #1- matrice k-1
    M0 = FW["M"]
    P0 = FW["P"]

    #2 - matrices de sortie pour k
    M = []
    P = []
    
    nbl = len(M0)
    nbc = len(M0[0])

    #3- iteration sur la matrice d'adjacence k-1
    for i in range(0, nbl):
        Ml = []
        Pl = []
        for j in range(0, nbl):
           
            #4- chemin à minimiser  
            mij = M0[i][j]
            pij = P0[i][j]

            #5- nouveau chemin a considerer par le sommet k
            mik = M0[i][k]
            mkj = M0[k][j]
            
            #print("mij: " + str( mij ) +" mik: " + str(mik) + " mkj: " + str(mkj) )

            #6- si le chemin par k optimise le chemin a k-1 on le met
            # à jour dans la nouvelle matrice

            if fw_supp(mij, mik, mkj):
                Ml += [ mik + mkj ]
                Pl += [k]
            else :
                Ml += [mij]
                Pl += [pij]
                    
        M += [Ml]
        P += [Pl]
    
    return { "M":M, "P":P }

def fw_floyd_warshall(FW0):
    """
    Apllique l'algorithme de FW sur la matrice initiale pour trouver
    les plus courts chemin pour tous couple de sommets
    """
    
    M = FW0["M"]
    nbv = len(M)
    FW = FW0; 
    
    #intercale v entre chaque couple de sommet
    for k in range(0, nbv) :
        FW = fw_floyd_warshall_k(FW, k)
        
    return FW

Algorithmes d'analyse du graphe

Coefficient de clustering

 
def fw_neighbor(n, FW):
    """ Recupere les voisins a->b = 1 dans la matrice d'adjacence """
    M = FW["M"]
    pth = M[n]
    nbv = len(M)
    nei = []
         
    for v in range(0,  nbv):
        if pth[v] == 1 :
            nei += [v]

    return nei

def fw_nei_inter(nei, vnei):
    """
    intersection entre deux liste pour trouver
    la les voisins conecté entres eux
    """
    return [value for value in nei if value in vnei]
   

def fw_connected_neighbor(nei,FW):
    """
    calcul le nombre de voisin de la liste des voisins
    sont connectés entre eux
    """
    nbv = len(nei)
    M = FW["M"]
    cnei = 0 #nb de voisin connectes
    
    for i in range(0, nbv):

        #1- ieme sommet de la liste des voisins
        v = nei[i]

        #2- les voisin du sommet v 
        vnei = fw_neighbor(v, FW)

        #3-intersection entre la liste des voisins et le voisins de v
        vnei_in_nei = fw_nei_inter(nei, vnei)

        #4- nombre de voisin connectés entre eux
        cnei += len(vnei_in_nei)

    return cnei



def fw_clustering_local( v, FW ):
    """
    Rapport entre le nombre de lien entre le nombre de voisins conectés entre eux
    et le nombre max kv de connexion entre voisin du sommet v
    """
    nei = fw_neighbor(v, FW)
      
    #2- calcul le nombre de voisins connectés entre eux
    cn = fw_connected_neighbor(nei,FW)

    #3- calcul du coeff local 1 / ( kv * (kv - 1)) * cn
    kv = len( nei )
    if kv < 2 :
        cl = 0
    else :
        cl = ( 1 / ( kv * (kv - 1) / 2 ) ) * cn

    return cl
    

def fw_clustering_coeff(FW):

    #1- recupere les voisins
    M = FW["M"]
    nbv = len(M)
    SUM_CL=0 #clustering global 
    
    for v in range(0, nbv):
        cl = fw_clustering_local( v, FW )
        SUM_CL += cl
       
    #2- moyenne
    AV_CLG = SUM_CL / nbv
    return AV_CLG

Taille moyenne des chemins

def fw_average_path_length(FW):

    APL = 0
    SPL = 0 #sum path length  
    NBP = 0 #number path

    #1- recupere la matrice des plus courts chemins
    M = FW["M"]
    nbv=len(M) 
    nbp=0 #nombre de chemin 

    for i in range(0, nbv):
        for j in range(0, nbv):
            pl = M[i][j] #path length i = j  => 0
            if pl > 0 :
                SPL += pl
                NBP += 1

    
    APL = SPL /  NBP 
        
    return APL

Création des graphes grâce aux algorithmes

Avec tous les algorithmes décrits ci-dessus, on va créer un graphe puis analyser la taille moyenne du graphe obtenu ainsi que son coefficient de clustering.

On commence par créer un graphe de 30 nœuds ainsi que 30 liaisons à l'aide du premier algorithme de création du graphe. On obtient donc un graphe sous forme d'arbre, cela représente l'invitation dans le réseau social d'un nouvel individu, chaque individu rejoint le réseau grâce à l'invitation d'un personne. On obtient ensuite le coefficient de clustering et la taille moyenne des chemins grâces aux algorithmes ci-dessus. On peut voir que le coefficient de clustering est à 0 étant donné que le graphe est un arbre et qu'il n'y a aucun triangle qui se forme dans le graphe. La taille moyenne des chemins quand à elle est d'environ 2.7 ce qui est très faible mais c'est expliqué par le fait qu'il n'y ait pas beaucoup de personnes au sein du réseau donc le chemin pour rejoindre une personne est lui aussi faible. On peut également voir la matrice de Floyd-Warshall qui regroupe les plus courts chemins à coté du graphe.

FW.png


La deuxième étape de la création d'un réseau social consiste à renforcer les liaisons avec un pourcentage d'arêtes aléatoires. Dans notre cas, j'ai renforcé le graphe pour arriver à 50 arêtes mais j'ai mit un coefficient de 0.2, ce qui signifie qu'il y a seulement 20*0.2 arêtes supplémentaires qui vont êtres crées, soit 4 arêtes. Cette étape représente l'ajout en ami de personnes aux sein du réseau par rapport aux centres d'intérêts, ils sont représentés de manière aléatoire sur ce graphe. On peut donc voir que le coefficient de clustering a augmenté étant donné qu'on a rajouté des liaisons internes au graphe sans rajouter de nœuds. La taille moyenne des chemins quand à elle à légèrement diminué mais vu qu'elle était déjà très faible, on ne peut pas voir une grande différence.

FW2.png


La troisième étape quand à elle permet de renforcer les liens au sein du réseau social en fonction du nombre aléatoire d'arêtes ajoutées à l'étape précédente. Dans notre cas, on a ajouté 4 arêtes aléatoires, nous avons donc actuellement 34 arêtes et nous voulons atteindre 50 arêtes, nous allons donc rajouter 16 arêtes. Ces arêtes vont êtres créer entre voisins ce qui représente l'ajout d'amis d'amis via les recommandations dans un réseau social. Le coefficient de clustering augmente donc une nouvelle fois car il y a une augmentation des liaisons sans augmentation des nœuds et la taille moyenne des chemins reste similaire mais baisse légèrement.

FW3.png