D'après la liste des projets, allocations de recherche (ADR) et des acteurs (laboratoires, écoles doctorales, partenaires socio-économiques...), nous allons calculer la structure du réseau des partenariats de l'ARC5.
Nous procédons d'abord à l'import des données :
Ensuite, nous convertissons les données de réseaux :
Enfin, nous exportons ces données sous plusieurs formes
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import csv
import os
import json
import itertools
from collections import Counter
from slugify import slugify
import networkx as nx
from networkx.readwrite import write_gpickle
data_dir = os.getcwd()
fichier_projets = "../final/ARC5-Final - Projets (tous).csv"
fichier_partenaires = "../final/ARC5-Final - Partenariats (OK).csv"
fichier_nodes = "../final/ARC5-Final - Noms (tous).csv"
# parsing helpers
project_types = {
"ADR" : "Thèse",
"projet" : "Projet de recherche",
"postdoc" : "Recherche post-doctorale"
}
L'ensemble de fonctions ci-dessous est utilisé pour créer et montrer les différentes données:
from IPython.display import display, Markdown, Latex
def show_table( title, array):
"""print a table using markdown"""
md_table = ""
display(Markdown("### "+title))
md_table += "| Ecole Doctorale | Nombre de thèses |\n"
md_table += "| --- | --- |\n"
for c in Counter(array).most_common():
md_table +="| %s | %s | \n"%(c[0], c[1])
display(Markdown(md_table))
def get_slug(name, type):
""" get a clean string ID from name and type"""
return "%s-%s"%(slugify( name.decode('utf-8') ),type.decode('utf-8'))
def get_project(name, type):
""" Retrieve a project based on his name and type"""
slug = get_slug(name, type)
try :
return G.node[slug]
except KeyError:
n=stored_projects[slug]
node = create_node(n["name"], n["type"], n["start"], n["end"], orga=n["orga"])
return G.node[slug]
def create_node(name, type, start, end, orga=None, info=None) :
"""create the node at the right format in the main graph"""
slug = get_slug(name, type)
try :
if start > G.node[slug]["start"] : start = G.node[slug]["start"]
if end > G.node[slug]["end"] : start = G.node[slug]["end"]
except:
start = start
end = end
node = {}
node["id"] = slug
node["type"] = type
node["orga"] = orga # cluster or ARC ?
node["name"] = name
node["start"] = start
node["end"] = end
if info :
node["info"]=info
G.add_node(node["id"], node)
return node["id"]
def merge_edge_data(Graph, e, data):
"""
merge data properly :prevent data within existing edges to be erased
"""
try :
Graph.edge[e[0]][e[1]]
except KeyError:
Graph.add_edge(e[0], e[1])
try:
Graph.edge[e[0]][e[1]]["edge_types"].append(data)
except KeyError:
Graph.edge[e[0]][e[1]]["edge_types"] = [data]
def save_graph(graph, path):
"""save pickle graph for later use"""
print "graph saved at : %s"%path
write_gpickle(graph, path)
print fichier_nodes
partenaires_types= {}
G = nx.Graph()
with open( os.path.join(data_dir, fichier_nodes), "r") as f :
reader = csv.DictReader(f)
for line in reader :
start = int(line["Début"])
end = int(line["Fin"])
info = {
"ville" : line["Ville"],
"lien" : line["Lien"]
}
node = create_node(line["Nom"], line["Type"], start, end, info=info)
partenaires_types[slugify(line["Nom"].decode('utf-8'))] = line["Type"]
print "%s nodes"%len(G.nodes())
print "%s edges"%len(G.edges())
show_table( "Types de nodes dans le fichier d'origine ", [n[1]["type"] for n in G.nodes(data=True)])
../final/ARC5-Final - Noms (tous).csv 279 nodes 0 edges
Ecole Doctorale | Nombre de thèses |
---|---|
laboratoire | 62 |
médiation | 52 |
patrimoine | 45 |
création | 37 |
localité | 36 |
etablissement | 14 |
enseignement | 11 |
ecole-doctorale | 10 |
économique | 6 |
cst | 6 |
stored_projects={}
print fichier_projets
with open( os.path.join(data_dir, fichier_projets), "r") as f :
reader = csv.DictReader(f)
for line in reader :
if line["Nom Projet"] and line["Orga"] != "13" and line["Orga"] != "14":
start = int(line["Année"])
end = start+3
# create project
projet = create_node(line["Nom Projet"], line["Type"], start, end, orga=line["Orga"])
# porteur de projet
porteur = create_node(line["Porteurs (nom)"], "personne", start, end)
# get existing
etablissement = G.node[get_slug(line["Etablissement"], "etablissement")]["id"]
laboratoire = G.node[get_slug(line["Labo"], "laboratoire")]["id"]
# TODOs : ville !
# ville = G.node[get_slug(line["Ville"], "localite")]
edges = []
edges.append((projet, etablissement))
edges.append((projet, laboratoire))
edges.append((projet, porteur))
edges.append((laboratoire, porteur))
# edges.append((etablissement, ville))
# edges.append((laboratoire, ville))
for e in edges :
merge_edge_data(G, e, { "type" : line["Type"], "name" : line["Nom Projet"] })
elif line["Orga"] == "13" or line["Orga"] == "14":
start = int(line["Année"])
end = start+3
stored_projects[get_slug(line["Nom Projet"], line["Type"])] = { "name" : line["Nom Projet"], "type": line["Type"], "start" : start, "end" : end, "orga" : line["Orga"] }
print "%s nodes"%len(G.nodes())
print "%s edges"%len(G.edges())
show_table( "Types de nodes ", [n[1]["type"] for n in G.nodes(data=True)])
show_table( "Projets par organisation ", [n[1]["orga"] for n in G.nodes(data=True)])
../final/ARC5-Final - Projets (tous).csv 482 nodes 438 edges
Ecole Doctorale | Nombre de thèses |
---|---|
personne | 91 |
laboratoire | 62 |
ADR | 57 |
projet | 54 |
médiation | 52 |
patrimoine | 45 |
création | 37 |
localité | 36 |
etablissement | 14 |
enseignement | 11 |
ecole-doctorale | 10 |
économique | 6 |
cst | 6 |
postdoc | 1 |
Ecole Doctorale | Nombre de thèses |
---|---|
None | 370 |
ARC5 | 112 |
Nous procédons maintenant à l'import du fichier contenant les partenariats. Chaque ligne contient un partenariat, organisé comme suit :
Structure | Projet | début | fin | type de projet |
---|---|---|---|---|
Académie de Savoie | Chaînes Éditoriales Patrimoniales : Corpus Électroniques et Papier (CEP2) | 2013 | 2015 | projet |
Académie de Savoie | CLELIA 2 : du fonds de manuscrits de Stendhal à d’autres corpus rhône-alpins, valorisation d’une mémoire culturelle collective par l’édition électronique. | 2012 | 2014 | projet |
Acrimed 69 | Le passage au numérique des médias locaux entre mutations médiatiques et mutations territoriales : du bouleversement des pratiques professionnelles à la reconfiguration des identités locales | 2014 | 2017 | ADR |
print fichier_partenaires
with open( os.path.join(data_dir, fichier_partenaires), "r") as f :
reader = csv.DictReader(f)
for i, line in enumerate(reader):
if line["Projet"] and line["Structure"] :
start = int(line["début"])
end = int(line["fin"])
type = partenaires_types[slugify(line["Structure"].decode('utf-8'))]
partenaire = G.node[ get_slug( line["Structure"], type)]
# TODO : ville
# ville = create_node(line["Ville"], "ville", start, end)
# get project (only those with partners)
projet = get_project(line["Projet"], line["Type"])
e = (partenaire["id"], projet["id"])
merge_edge_data(G, e, { "type" : projet["type"], "name" : projet["name"] })
print "%s nodes"%len(G.nodes())
print "%s edges"%len(G.edges())
show_table( "Types de nodes après avoir ajouté les partenariats", [n[1]["type"] for n in G.nodes(data=True)])
../final/ARC5-Final - Partenariats (OK).csv 491 nodes 713 edges
Ecole Doctorale | Nombre de thèses |
---|---|
personne | 91 |
ADR | 65 |
laboratoire | 62 |
projet | 55 |
médiation | 52 |
patrimoine | 45 |
création | 37 |
localité | 36 |
etablissement | 14 |
enseignement | 11 |
ecole-doctorale | 10 |
économique | 6 |
cst | 6 |
postdoc | 1 |
Plutôt que de conserver les personnes (et leurs noms) dans le graphe, nous allons désormais les transformer en liens entre les organisations. Chaque personne ayant des liens entre deux organisations créera donc un lien entre elles puis sera supprimée du graphe.
print "before : %s nodes / %s edges"%(len(G.nodes()),len(G.edges()))
G_without_people = G.copy()
# get all persons in the graph
persons = [node[0] for node in G_without_people.nodes(data=True) if node[1]["type"] == "personne"]
for person in persons:
# edges for a single person
person_edges = G_without_people.edges(person)
# get all nodes linked by a single person
list_of_person_nodes = []; map(list_of_person_nodes.extend, map(list,person_edges))
assert len(list_of_person_nodes) == len(person_edges)*2 # make sure we have all nodes
clean_nodes = [n for n in list_of_person_nodes if n != person]
if len(person_edges) > 2 : # if have less than degree of 1 then remove node
# get data from the node to add to the edge
data = G_without_people.node[person]
# create new edges between all those
new_edges = list(itertools.combinations(clean_nodes, 2))
# create new edges with merge data info
for e in new_edges:
merge_edge_data(G_without_people, e, { "type" : "personne", "name" : None })
# remove person from the graph
G_without_people.remove_node(person)
print "after : %s nodes / %s edges"%(len(G_without_people.nodes()),len(G_without_people.edges()))
show_table( "Types de nodes (sans les personnes) ", [n[1]["type"] for n in G_without_people.nodes(data=True)])
# save graph without people inside
save_graph(G_without_people, '../final/ARC5-nx-with-projects.pickle')
before : 491 nodes / 713 edges after : 400 nodes / 560 edges
Ecole Doctorale | Nombre de thèses |
---|---|
ADR | 65 |
laboratoire | 62 |
projet | 55 |
médiation | 52 |
patrimoine | 45 |
création | 37 |
localité | 36 |
etablissement | 14 |
enseignement | 11 |
ecole-doctorale | 10 |
économique | 6 |
cst | 6 |
postdoc | 1 |
graph saved at : ../final/ARC5-nx-with-projects.pickle
De la même façon, les projets et allocations de recherches (ADR) vont désormais être convertis en liens dans le graphe. Les liens ainsi créés vont relier les différentes organisations ayant pris par au projet, puis les projets (ou ADRs) seront supprimés du graphe.
Les titres des projets et ADRs seront stockés dans les liens, afin de pouvoir être consultable ensuite.
print "before : %s nodes / %s edges"%(len(G_without_people.nodes()),len(G_without_people.edges()))
G_without_people_and_projects = G_without_people.copy()
# get all projects in the graph
projects = [node[0] for node in G_without_people_and_projects.nodes(data=True) if node[1]["type"] == "projet" or node[1]["type"] == "ADR" or node[1]["type"] == "postdoc" ]
for project in projects:
# edges for a single person
project_edges = G_without_people_and_projects.edges(project)
# get all nodes linked by a single person
list_of_project_nodes = []; map(list_of_project_nodes.extend, map(list, project_edges))
assert len(list_of_project_nodes) == len(project_edges)*2 # make sure we have all nodes
clean_nodes = [n for n in list_of_project_nodes if n != project]
if len(project_edges) > 2 : # if have less than degree of 1 then remove node
# get data from the node to add to the edge
data = G_without_people_and_projects.node[project]
# create new edges between all those
new_edges = list(itertools.combinations(clean_nodes, 2))
# parse text properly
# merge data into edge info
for e in new_edges:
proj=G.node[project]
notes = { "type" : proj["type"], "name" : proj["name"]}
merge_edge_data(G_without_people_and_projects, e, notes)
# remove person from the graph
G_without_people_and_projects.remove_node(project)
print "after : %s nodes / %s edges"%(len(G_without_people_and_projects.nodes()),len(G_without_people_and_projects.edges()))
show_table( "Types de nodes (sans personnes ni projets) ", [n[1]["type"] for n in G_without_people_and_projects.nodes(data=True)])
# save graph without projects
save_graph(G_without_people_and_projects, '../final/ARC5-nx-without-projects.pickle')
before : 400 nodes / 560 edges after : 279 nodes / 1021 edges
Ecole Doctorale | Nombre de thèses |
---|---|
laboratoire | 62 |
médiation | 52 |
patrimoine | 45 |
création | 37 |
localité | 36 |
etablissement | 14 |
enseignement | 11 |
ecole-doctorale | 10 |
économique | 6 |
cst | 6 |
graph saved at : ../final/ARC5-nx-without-projects.pickle
Dans le graph final, nous supprimons les noeuds ayant un degré nul (ceux qui n'ont aucune connection), car il n'apporte que très peu d'information. Egalement, nous attribuons aux liens un poids égal au nombre de projets, personnel ou ADRs en commun.
Une dernière étape constite à convertir les données stockées dans les liens (liste de projets et ADRs) en une forme agréable à lire qui pourra ensuite être affichée dans l'interface de navigation du logiciel Topogram.
# create the graph
nodes = []
for n in G_without_people_and_projects.nodes(data=True):
if G_without_people_and_projects.degree(n[0]) > 0: # ignore singletons
node = n[1]
node["id"] = n[0]
node["group"] = n[1]["type"]
# add website and city
node["additionalInfo"] = "**Ville** : %s \n\n "%node["info"]["ville"]
node["additionalInfo"] += "[Consulter le site](%s)"%node["info"]["lien"]
nodes.append(node)
print "%s nodes"%len(nodes)
edges = []
for i, e in enumerate(G_without_people_and_projects.edges(data=True)):
edge = e[2]
# calculate edge weight
edge["weight"] = len(edge["edge_types"])
notes = ""
team = 0
for t in edge["edge_types"]:
if t["type"] == "ADR" or t["type"] == "projet" or t["type"] == "postdoc" :
notes = notes + "* **%s** : %s \n"%(project_types[t["type"]], t["name"])
elif t["type"] == "personne":
team = team + 1
if team != 0 :
notes = "* Membres d'équipe en commun \n" + notes
edge["additionalInfo"] = notes
edge["source"] = e[0]
edge["target"] = e[1]
edges.append(edge)
print "%s edges"%len(edges)
210 nodes 1021 edges
Maintenant que toutes nos données ont été traitées et formattées correctement, nous pouvons créer ou mettre à jour la visualisation de notre graphe, rendu disponible en ligne grâce au logiciel Topogram.
Pour écrire la carte depuis ce script, nous utilisons le client API Python qui nous permet de manipuler les graphes présents dans le service Topogram depuis une machine tierce. La mise à jour se fait donc de façon programmatique, après voir supprimé le contenu existant de la carte.
from topogram_client import TopogramAPIClient
import logging
import datetime
now=datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
# passwords
TOPOGRAM_URL = "https://app.topogram.io" # http://localhost:3000
USER = "***"
PASSWORD = "***"
# connect to the topogram instance
topogram = TopogramAPIClient(TOPOGRAM_URL)
# topogram.create_user(USER, PASSWORD)
topogram.user_login(USER, PASSWORD)
r = topogram.create_topogram("ARC 5 - Collaborations Culture / Recherche en Rhône-Alpes")
print r["message"]
topogram_ID = r["data"][0]["_id"]
# get and backup existing nodes and edges
existing_nodes = topogram.get_nodes(topogram_ID)["data"]
url = slugify(TOPOGRAM_URL.decode('utf-8'))
with open('data/ARC5-%s-nodes-%s.json'%(url,now), 'w') as outfile:
json.dump(existing_nodes, outfile)
existing_edges = topogram.get_edges(topogram_ID)["data"]
with open('data/ARC5-%s-edges-%s.json'%(url,now), 'w') as outfile:
json.dump(existing_edges, outfile)
print "%s existing edges, %s existing nodes"%(len(existing_edges), len(existing_nodes))
# clear existing graph
topogram.delete_nodes([n["_id"] for n in existing_nodes])
print "nodes deleted"
topogram.delete_edges([n["_id"] for n in existing_edges])
print "edges deleted"
r = topogram.create_nodes(topogram_ID, nodes)
print "%s nodes created."%len(r["data"])
r = topogram.create_edges(topogram_ID, edges)
print "%s edges created."%len(r["data"])
print "done. Topogram is online at %s/topograms/%s/view"%(TOPOGRAM_URL, topogram_ID)
A topogram with the same name already exists 1024 existing edges, 210 existing nodes nodes deleted edges deleted 210 nodes created. 1021 edges created. done. Topogram is online at https://app.topogram.io/topograms/3Fep7oZAFjqBnHLQR/view