Allez vers la [[etapes|page précédente - étapes du projet]] ====== traitements statistiques ====== Nous allons dorénavant réaliser différents traitements statistiques et étudier notre population d'économiste et de juriste. Nous allons concentrer notre étude sur les données présentes sur Wikidata, car cette base de données fournie suffisamment d'individu (env. 130 000) pour réaliser une étude complète. Mais bien sûr, il faudrait aussi ajouter les données présentes sur BnF Data et DBpedia pour avoir une étude plus large. La totalité des analyses statistiques est réalisée sur des carnets jupyterlab. ====== Étude des propriétés ====== La première étape nécessaire à la réalisation de l'étude statistique est d'évaluer les propriétés qui sont intéressantes à traiter. Le niveau d'intérêt qu'elles peuvent avoir est double. D'une part, elles doivent être présente dans une proportion suffisamment conséquente de la population étudiée et d'autre part les individus de la population doivent partager assez de propriétés afin d'étudier ses dernières conjointement. ==== Requête SPARQL ==== Pour cela, il est tout d'abord préférable d'importer les propriétés que l'on souhaite étudier comme montrer dans l'[[etapes_fusion|étape précédente]] afin de pouvoir extraire facilement les propriétés. Ensuite, il s'agit de créer une requête qui va prendre l'ensemble de ses propriétés pour chaque individu. Il faut dont faire en sorte que les valeurs de propriétés ne se répètent pas (étant donné qu'une personne peut avoir plusieurs valeurs pour une propriété. ex: une personne qui a fréquenté plusieurs établissements). Pour cela, nous avons créer un requête de la façon suivante: PREFIX wd: PREFIX wdt: PREFIX rdfs: PREFIX rdf: PREFIX xsd: SELECT ?person ?nationality ?birthDate ?gender ?placeOfBirth ?placeOfDeath ?educatedAt ?doctoralAdvisor ?doctoralStudent ?occupation ?positionHeld ?employer WHERE {SELECT ?person ?nationality (ROUND(AVG(?birthDate_AVG)) AS ?birthDate) ?gender ?placeOfBirth ?placeOfDeath ?educatedAt ?doctoralAdvisor ?doctoralStudent ?occupation ?positionHeld ?employer WHERE {SELECT DISTINCT ?person (YEAR(xsd:dateTime(?bD)) AS ?birthDate_AVG) (GROUP_CONCAT(DISTINCT(?gend); SEPARATOR = "|") AS ?gender) (GROUP_CONCAT(DISTINCT(?birthPlace); SEPARATOR = "|") AS ?placeOfBirth) (GROUP_CONCAT(DISTINCT(?deathPlace); SEPARATOR = "|") AS ?placeOfDeath)(GROUP_CONCAT(DISTINCT(?nat); SEPARATOR = "|") AS ?nationality) (GROUP_CONCAT(DISTINCT(?posHeld);SEPARATOR = "|") AS ?positionHeld) (GROUP_CONCAT(DISTINCT(?docAdv); SEPARATOR = "|") AS ?doctoralAdvisor) (GROUP_CONCAT(DISTINCT(?docStu); SEPARATOR = "|") AS ?doctoralStudent) (GROUP_CONCAT(DISTINCT(?empl);SEPARATOR = "|") AS ?employer) (GROUP_CONCAT(DISTINCT(?educAt);SEPARATOR = "|") AS ?educatedAt) (GROUP_CONCAT(DISTINCT(?occ);SEPARATOR = "|") AS ?occupation) WHERE { {?person wdt:P569 ?bD} OPTIONAL {?person wdt:P69 ?edAt . ?edAt rdfs:label ?educAt} OPTIONAL {?person wdt:P27 ?na . ?na rdfs:label ?nat} OPTIONAL {?person wdt:P21 ?g . ?g rdfs:label ?gend} OPTIONAL {?person wdt:P19 ?pB . ?pB rdfs:label ?birthPlace} OPTIONAL {?person wdt:20 ?pD. ?pD rdfs:label ?deathPlace} OPTIONAL {?person wdt:P184 ?dA . ?dA rdfs:label ?docAdv} OPTIONAL {?person wdt:P106 ?o . ?o rdfs:label ?occ} OPTIONAL {?person wdt:P39 ?pH. ?pH rdfs:label ?posHeld} OPTIONAL {?person wdt:P108 ?em . ?em rdfs:label ?empl} OPTIONAL {?person wdt:P185 ?dS . ?dS rdfs:label ?docStu} } GROUP BY ?person ?bD ORDER BY ?person } GROUP BY ?person ?nationality ?placeOfBirth ?placeOfDeath ?educatedAt ?doctoralAdvisor ?doctoralStudent ?occupation ?positionHeld ?employer ?gender ORDER BY ?person } #LIMIT 100 C'est une requête qui semble assez complexe, mais on peut la comprendre assez facilement si on la découpe. Tout d'abord, le cœur de la requête sont les propriétés et nous cherchons à toutes les exporter. Dans ce cas, il est préférable d'utiliser la clause OPTIONAL afin de ne pas enlever des personnes dont certaines propriétés manqueraient. Ensuite, pour chaque propriété, il faut les importer avec leur étiquette donc il faut utiliser la propriété "rdfs:label". Puis, dans le SELECT, il faut utiliser pour chaque propriété, la clause GROUP CONCAT qui permet de concaténer les différentes valeurs dans une même propriété pour un même individu (ex: les différents établissements d'un individu). Il est préférable d'ajouter à l'intérieur la clause DISTINCT sinon chaque valeur est répétée le nombre de fois où il y a d'autres propriétés. Pour en apprendre davantage sur la façon d'utiliser le GROUP CONCAT, référez-vous à cette [[https://docs.cambridgesemantics.com/anzograph/v2.3/userdoc/group-concat.htm|ressource]]. Ensuite, dans ce même SELECT, nous avons utilisé la clause YEAR pour la date de naissance afin de ne garder que l'année, puisque c'est avant tout elle qui nous intéressent (et qu'elle nous permettra de voir les évolutions des différentes propriétés dans le temps). On finit la sous-requête avec une clause GROUP BY pour indiquer que c'est par les personnes que les autres propriétés doivent être regroupées (nécessaire si l'on souhaite utiliser un GROUP CONCAT). Dans la clause SELECT supérieure, nous avons utilisé la clause AVG pour l'année afin de prendre la moyenne des années si plusieurs années sont mentionnées. Nous avons ajouté une clause ROUND pour que l'année soit arrondie et reste une date. Il faut cette fois encore utiliser un GROUP BY mais avec cette fois toutes les propriétés sauf celle dont nous avons changé le format (la date de naissance ici). Le dernier niveau de SELECT permet de s'assurer que les propriétés sont bien avec l'étiquette souhaité et qu'il n'y a pas de clause. ====Étude des propriétés==== Après avoir réalisé cette étape préliminaire, nous pouvons importer les données dans Jupyterlab afin de pouvoir réaliser à proprement parler les traitements statistiques. Nous n'allons pas présenter ici, l'ensemble du script, mais seulement les parties. L'ensemble de la méthode se trouve dans le [[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Sounding_properties_Wikidata.ipynb|carnet]] dédié. Pour commencer, il est nécessaire de connaître le nombre de valeurs dans chaque propriété. Pour cela, il faut couper et compter les valeurs des propriétés : df_wiki['len_nationality'] = df_wiki['nationality'].apply(lambda x : len(x.split('|')) if x else 0) La fonction découpe dans chaque cellule les valeurs concaténées (le découpage est fait par rapport à un caractère prédéfini, ici "|" que l'on avait utilisé pour séparer les concaténations dans la requête SPARQL). Cette étape doit être réalisée pour toutes les propriétés hormis la date de naissance comme chaque personne n'a qu'une seule date. Il est mieux d'ensuite changer l'ordre du tableau de données pour que chaque propriété soit associée sa longueur. df_wiki = df_wiki[[ 'birthDate', 'nationality', 'len_nationality','gender','len_gender','placeOfBirth','len_placeOfBirth', \ 'educatedAt', 'len_educatedAt', 'doctoralAdvisor', 'len_doctoralAdvisor', 'doctoralStudent', 'len_doctoralStudent',\ 'occupation', 'len_occupation', 'positionHeld', 'len_positionHeld', 'employer', 'len_employer']] Afin de ne pas réimporter les données à chaque fois, il est préférable de les exporter au format CSV. Et les réimporter par la suite. Une fois fait, il est possible de calculer le nombre de valeurs pour chaque propriété en les additionnant. properties_persons = [] for i,r in list(persons.iterrows()): ## iterrows permets d'avoir une liste de pairs. cf . https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.iterrows.html pour avoir de la do properties = [] if r['len_placeOfBirth'] > 0: properties.append('placeOfBirth') if r['len_gender'] > 0: properties.append('gender') if r['len_nationality'] > 0: properties.append('nationality') if r['len_educatedAt'] > 0: properties.append('educatedAt') if r['len_occupation'] > 0: properties.append('occupation') if r['len_positionHeld'] > 0: properties.append('positionHeld') if r['len_employer'] > 0: properties.append('employer') properties_persons.append([i, properties, len(properties)]) Pour chaque propriété, si la longueur est supérieure alors cela ajoute l'étiquette de la propriété, et ce, pour chaque personne. On peut ensuite créer un //dataframe// de "properties_persons" et calculer la distribution des effectifs des personnes et des propriétés. frequencies_properties_per_person = df_properties_persons.groupby('frequency').count() À partir de cela, une première visualisation de la distribution peut être faite. {{ :projets_individuels:distribution_proprietes_par_personne.png.png?direc&700 | distribution du nombre de propriétés par personne}} Cette première visualisation permet déjà d'avoir pas mal d'information sur les propriétés. Cela permet notamment de voir que la majorité des personnes ont entre 4 et 6 propriétés. La forme de cloche de la représentation le montre bien. Cela montre aussi qu'extrêmement peu de personnes n'ont qu'une seule propriété, de même que peu de personnes ont toutes les propriétés. Il est aussi possible d'étudier la distribution des propriétés avec une méthode très similaire à la précédente. freq_persons = 0 nationality = 0 gender = 0 placeOfBirth = 0 educatedAt = 0 occupation = 0 positionHeld = 0 employer = 0 for i,r in list(persons.iterrows()): freq_persons += 1 if r['len_nationality'] > 0: nationality += 1 if r['len_placeOfBirth'] > 0: placeOfBirth += 1 if r['len_gender'] > 0: gender += 1 if r['len_educatedAt'] > 0: educatedAt += 1 if r['len_occupation'] > 0: occupation += 1 if r['len_positionHeld'] > 0: positionHeld += 1 if r['len_employer'] > 0: employer += 1 Pour chaque personne, le code vérifie si une propriété est rentrée ou non et cela calcule leur nombre. On peut ensuite déterminer la distribution du nombre de propriétés disponibles. Ensuite, on met les propriétés et leur valeur en série : frequencies_properties = pd.Series({"freq_persons": freq_persons, "nationality": nationality, "placeOfBirth":placeOfBirth, "gender": gender, "educatedAt":educatedAt,\ "occupation":occupation, "positionHeld":positionHeld, "employer":employer }) frequencies_properties On peut aussi l'obtenir sous forme de visualisation : {{ :besson_sylvain:number_individuals_per_property.png?direct |nombre d'individus par propriété}} Cette visualisation nous renseigne que c'est la propriété "genre" qui est la plus renseignée, sans doute, car c'est l'une des plus simples à obtenir. Les propriétés "nationalités", "lieu de naissance", "lieu d'étude" et "professions" sont aussi bien représentées. =====Étude des propriétés combinées===== ====Réseaux de propriétés==== À partir de ces analyses préliminaires, une analyse plus fine des propriétés est possible. Cette analyse peut être faite en montrant les réseaux que les propriétés ont entre elles. Ce réseau tend à montrer à quel point chaque propriété est entré avec tel autre. Pour cela, nous montrerons ici que les étapes essentielles pour aller voir les étapes intermédiaires, il faudra regarder dans le notebook présent sur Github. La première étape est de créer les arrêtes du réseau entre les propriétés (c'est-à-dire à quel point chaque propriété est relié à tel autre). properties_edges = [] for pp in properties_persons: local_properties = pp[1] len_prop = len(local_properties) # print(len_prop) if len_prop > 1: local_edges = [] i = 0 while i < (len_prop-1): i_len = i + 1 while i_len < (len_prop): local_edges.append((local_properties[i], local_properties[i_len])) i_len += 1 i += 1 # print(local_edges) properties_edges += local_edges Puis il faut créer le graphe à proprement parler. Pour cela, il faut utiliser sur python la librairie [[https://networkx.org/documentation/stable/tutorial.html|networkx]] qui permet de faire de l'analyse de réseaux. import networkx as nx # cf. https://networkx.org/documentation/stable/reference/classes/multigraph.html graph_properties = nx.MultiGraph() graph_properties.add_edges_from(properties_edges) print(nx.info(graph_properties)) Ensuite on créer un dictionnaire pour les effectifs et un dictionnaire pour les degrées de chaque propriété. frequency = dict([(i, {'frequency': v}) for i,v in frequencies_properties.items()]) print(frequency) degree = dict([(d[0], {'degree': d[1]}) for d in nx.degree(graph_properties)]) print(degree) À partir de cela, il est possible donner les effectifs et les degrées aux nœuds. nx.set_node_attributes(graph_properties, frequency) nx.set_node_attributes(graph_properties, degree) Ensuite on peut calculer les poids entre chaque propriétés. Pour cela, il faut créer une liste qui compte le poids qui existe entre deux propriétés. De cette liste, on crée un dictionnaire: # Use the list of the previous command and create a dictionary of the weights lll = [(l[0][0], l[0][1], {'weight' : l[1]}) for l in list(Counter([(u,v) for u,v in graph_properties.edges()]).items())] Ensuite, on peut créer un graphe avec les poids mais il faut dans le même temps convertir ce qui est jusqu'à présent un multigraphe en un graphe simple. weigth_graph_properties = nx.Graph() weigth_graph_properties.add_edges_from(lll) wgp = nx.Graph() for u,v,data in graph_properties.edges.data(): w = data['weight'] if 'weight' in data else 1 if wgp.has_edge(u,v): wgp[u][v]['weight'] += w else: wgp.add_edge(u, v, weight=w) Un moyen de vérifier que le graphe n'a de problème et notamment des //selfloops// éventuelles: print(nx.number_of_selfloops(weigth_graph_properties)) print(nx.is_weighted(weigth_graph_properties)) Enfin, on peut ajouter les effectifs et les degrées à ce graphe simple. nx.set_node_attributes(weigth_graph_properties, dict([(n, {'frequency': data['frequency']}) for n, data in graph_properties.nodes.data()])) nx.set_node_attributes(weigth_graph_properties, dict([(n, {'degree': data['degree']}) for n, data in graph_properties.nodes.data()])) Une fois que le graphe est totalement créé, il est possible de le visualiser. Cela se présente de la façon suivante: {{ :projets_individuels:weigth_graph_properties_without_doctorate_bis.png?direct&900 |Graphe des propriétés}} ==== Comparer les effectifs des autres caractéristiques pour chaque classe ==== Tout d'abord, il faut choisir les propriétés que l'on souhaite observer plus précisément. Pour cela, les précédentes analyses peuvent être d'une grande aide. Dans notre casn nous avons retenu le "lieu de naissance, le "genre", la "nationalité", la le "lieu d'étude", la "profession", l'"employeur" et le "poste". On commence par les mettre sous forme de série : observed_variables = {"educatedAt":"len_educatedAt","placeOfBirth": "len_placeOfBirth", "gender":"len_gender","nationality":"len_nationality", "employer":"len_employer", "occupation":"len_occupation", "positionHeld":"len_positionHeld", } for i,v in observed_variables.items(): print(i,v) Dans ce filtre, si une propriété a une valeur supérieure à zéro, alors la fonction vérifie si les autres propriétés sont rentrées. Cela se présente de la façon suivante : plot_id = 1 plot_left = 1 for ix,v in observed_variables.items(): freq_persons = 0 nationality = 0 placeOfBirth = 0 gender = 0 educatedAt = 0 occupation = 0 positionHeld = 0 employer = 0 for i,r in list(persons.iterrows()): if r[v] > 0: freq_persons += 1 if r['len_nationality'] > 0: nationality += 1 if r['len_placeOfBirth'] > 0: placeOfBirth += 1 if r['len_gender'] > 0: gender += 1 if r['len_educatedAt'] > 0: educatedAt += 1 if r['len_occupation'] > 0: occupation += 1 if r['len_positionHeld'] > 0: positionHeld += 1 if r['len_employer'] > 0: employer += 1 frequencies_properties = pd.Series({"nationality": nationality, \ "placeOfBirth":placeOfBirth, \ "gender":gender, \ "educatedAt":educatedAt,\ "occupation":occupation, \ "positionHeld":positionHeld, \ "employer":employer }) x = [l for l in frequencies_properties.index] y = [l for l in frequencies_properties] plt.subplot(3, 3, plot_id) plt.title(ix) plt.bar(x, y) plt.gcf().set_size_inches(12, 8) plt.xticks(rotation=45) plt.ylabel('frequency') plot_id += 1 plt.subplots_adjust(#left=0.1, bottom=0.1, right=1.6, top=2, # wspace=0.4, hspace=0.4 ) plt.show() La suite du code permet de créer une visualisation pour chaque propriété. Pour réaliser des sous-graphiques, cette [[https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subplot.html|page]] montre comment les réaliser. {{ :projets_individuels:frequencies_property_others_properties.png?direct |Distribution des autres propriétés par rapport à chaque propriété}} Dans ces représentations, il y a quelques éléments qui faut faire attention pour bien les comprendre. Tous d'abord, chaque propriété est représentée dans sa visualisation. Et étant donné qu'elle est comparée à elle-même, elle a nécessairement le maximum d'effectif. Ensuite, dans toutes les visualisations l'échelle n'est pas la même, car cela dépend de l'effectif de la propriété (ex: la propriété "genre" a quasiment 140 000 individus alors que "employer" n'en a que 25 000). C'est un biais visuel qui faut prendre en compte lors d'une analyse. Une autre façon de faire qui permet d'être plus précis dans l'analyse de filtrer avec les différentes variables que l'on souhaite. Par exemple, il est possible de filtrer les propriétés "profession", "lieu d'étude" et "nationalité" et de vérifier le nombre de fois où ces trois propriétés apparaissaient ensembles. La fonction se présente de la façon suivante : freq_persons = 0 nationality = 0 placeOfBirth = 0 gender = 0 educatedAt = 0 occupation = 0 positionHeld = 0 employer = 0 for i,r in list(persons.iterrows()): # if r['len_occupation'] > 0 and r['len_educatedAt'] > 0 and r['len_nationality'] > 0 and r['len_gender']: # if r['len_occupation'] > 0 and r['len_positionHeld'] > 0 and \ # r['len_educatedAt'] > 0 and r['len_gender'] and r['len_nationality'] \ # and r['len_employer'] : if r['len_doctoralAdvisor'] > 0 and r['len_doctoralStudent'] > 0 and r['len_educatedAt'] > 0: # if r['len_employer'] > 0 and r['len_positionHeld'] > 0 : freq_persons += 1 if r['len_nationality'] > 0: nationality += 1 if r['len_placeOfBirth'] > 0: placeOfBirth += 1 if r['len_gender'] > 0: gender += 1 if r['len_educatedAt'] > 0: educatedAt += 1 if r['len_occupation'] > 0: occupation += 1 if r['len_positionHeld'] > 0: positionHeld += 1 if r['len_employer'] > 0: employer += 1 if r['len_doctoralAdvisor'] > 0: doctoralAdvisor += 1 if r['len_doctoralStudent'] > 0: doctoralStudent += 1 frequencies_properties_filter = pd.Series({ "nationality":nationality, \ "placeOfBirth":placeOfBirth, \ "gender":gender, \ "educatedAt":educatedAt,\ "occupation":occupation, \ "positionHeld":positionHeld, \ "employer":employer, \ }) frequencies_properties_filter {{ :projets_individuels:properties_combination_occupation_educatedat_nationality_gender_.png?direct&700 | Distribution avec les propriétés combinées: "profession", "lieu d'étude", "nationalité et "genre"}} ====== traitements statistiques ====== Nous allons dorénavant réaliser différents traitements statistiques et étudier notre population d'économiste et de juriste. Nous allons concentrer notre étude sur les données présentes sur Wikidata, car cette base de données fournie suffisamment d'individu (env. 130 000) pour réaliser une étude complète. Mais bien sûr, il faudrait aussi ajouter les données présentes sur BnF Data et DBpedia pour avoir une étude plus large. ===== Étude des propriétés ===== La première étape nécessaire à la réalisation de l'étude statistique est d'évaluer les propriétés qui sont intéressantes à traiter. Le niveau d'intérêt qu'elles peuvent avoir est double. D'une part, elles doivent être présente dans une proportion suffisamment conséquente de la population étudiée et d'autre part les individus de la population doivent partager assez de propriétés afin d'étudier ses dernières conjointement. ==== Requête SPARQL ==== Pour cela, il est tout d'abord préférable d'importer les propriétés que l'on souhaite étudier comme montrer dans l'[[etapes_fusion|étape précédente]] afin de pouvoir extraire facilement les propriétés. Ensuite, il s'agit de créer une requête qui va prendre l'ensemble de ses propriétés pour chaque individu. Il faut dont faire en sorte que les valeurs de propriétés ne se répètent pas (étant donné qu'une personne peut avoir plusieurs valeurs pour une propriété. ex: une personne qui a fréquenté plusieurs établissements). Pour cela, nous avons créer un requête de la façon suivante: PREFIX wd: PREFIX wdt: PREFIX rdfs: PREFIX rdf: PREFIX xsd: SELECT ?person ?nationality ?birthDate ?gender ?placeOfBirth ?placeOfDeath ?educatedAt ?doctoralAdvisor ?doctoralStudent ?occupation ?positionHeld ?employer WHERE {SELECT ?person ?nationality (ROUND(AVG(?birthDate_AVG)) AS ?birthDate) ?gender ?placeOfBirth ?placeOfDeath ?educatedAt ?doctoralAdvisor ?doctoralStudent ?occupation ?positionHeld ?employer WHERE {SELECT DISTINCT ?person (YEAR(xsd:dateTime(?bD)) AS ?birthDate_AVG) (GROUP_CONCAT(DISTINCT(?gend); SEPARATOR = "|") AS ?gender) (GROUP_CONCAT(DISTINCT(?birthPlace); SEPARATOR = "|") AS ?placeOfBirth) (GROUP_CONCAT(DISTINCT(?deathPlace); SEPARATOR = "|") AS ?placeOfDeath)(GROUP_CONCAT(DISTINCT(?nat); SEPARATOR = "|") AS ?nationality) (GROUP_CONCAT(DISTINCT(?posHeld);SEPARATOR = "|") AS ?positionHeld) (GROUP_CONCAT(DISTINCT(?docAdv); SEPARATOR = "|") AS ?doctoralAdvisor) (GROUP_CONCAT(DISTINCT(?docStu); SEPARATOR = "|") AS ?doctoralStudent) (GROUP_CONCAT(DISTINCT(?empl);SEPARATOR = "|") AS ?employer) (GROUP_CONCAT(DISTINCT(?educAt);SEPARATOR = "|") AS ?educatedAt) (GROUP_CONCAT(DISTINCT(?occ);SEPARATOR = "|") AS ?occupation) WHERE { {?person wdt:P569 ?bD} OPTIONAL {?person wdt:P69 ?edAt . ?edAt rdfs:label ?educAt} OPTIONAL {?person wdt:P27 ?na . ?na rdfs:label ?nat} OPTIONAL {?person wdt:P21 ?g . ?g rdfs:label ?gend} OPTIONAL {?person wdt:P19 ?pB . ?pB rdfs:label ?birthPlace} OPTIONAL {?person wdt:20 ?pD. ?pD rdfs:label ?deathPlace} OPTIONAL {?person wdt:P184 ?dA . ?dA rdfs:label ?docAdv} OPTIONAL {?person wdt:P106 ?o . ?o rdfs:label ?occ} OPTIONAL {?person wdt:P39 ?pH. ?pH rdfs:label ?posHeld} OPTIONAL {?person wdt:P108 ?em . ?em rdfs:label ?empl} OPTIONAL {?person wdt:P185 ?dS . ?dS rdfs:label ?docStu} } GROUP BY ?person ?bD ORDER BY ?person } GROUP BY ?person ?nationality ?placeOfBirth ?placeOfDeath ?educatedAt ?doctoralAdvisor ?doctoralStudent ?occupation ?positionHeld ?employer ?gender ORDER BY ?person } #LIMIT 100 C'est une requête qui semble assez complexe, mais on peut la comprendre assez facilement si on la découpe. Tout d'abord, le cœur de la requête est les propriétés et nous cherchons à toutes les exporter. Dans ce cas, il est préférable d'utiliser la clause OPTIONAL afin de ne pas enlever des personnes dont certaines propriétés manqueraient. Ensuite, pour chaque propriété, il faut les importer avec leur étiquette donc il faut utiliser la propriété "rdfs:label". Puis, dans le SELECT, il faut utiliser pour chaque propriété, la clause GROUP CONCAT qui permet de concaténer les différentes valeurs dans une même propriété pour un même individu (ex: les différents établissements d'un individu). Il est préférable d'ajouter à l'intérieur la clause DISTINCT sinon chaque valeur est répétée le nombre de fois où il y a d'autres propriétés. Pour en apprendre davantage sur la façon d'utiliser le GROUP CONCAT, référez-vous à cette [[https://docs.cambridgesemantics.com/anzograph/v2.3/userdoc/group-concat.htm|ressource]]. Ensuite, dans ce même SELECT, nous avons utilisé la clause YEAR pour la date de naissance afin de ne garder que l'année, puisque c'est avant tout elle qui nous intéressent (et qu'elle nous permettra de voir les évolutions des différentes propriétés dans le temps). ON finit la sous-requête avec une clause GROUP BY pour indiquer que c'est par personnes que les autres propriétés doivent être regroupées (nécessaire si l'on souhaite utiliser un GROUP CONCAT). Dans la clause SELECT supérieure, nous avons utilisé la clause AVG pour l'année afin de prendre la moyenne des années si plusieurs années sont mentionnées. Nous avons ajouté une clause ROUND pour que l'année soit arrondie et reste une date. Il faut cette fois encore utiliser un GROUP BY mais avec cette fois toutes les propriétés sauf celle dont nous avons changé le format (la date de naissance ici) Le dernier niveau de SELECT permet de s'assurer que les propriétés sont bien avec l'étiquette que l'on souhaite et qu'il n'y ait pas de clause. ====Étude des propriétés==== Après avoir réalisé cette étape préliminaire, nous pouvons importer les données dans python afin de pouvoir réaliser à proprement parler les traitements statistiques. Nous n'allons pas présenter ici, l'ensemble du script, mais seulement les parties. L'ensemble de la méthode se trouve dans le [[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Sounding_properties_Wikidata.ipynb|carnet]] dédié. Pour commencer, il est nécessaire de connaître le nombre de valeurs dans chaque propriété. Pour cela, il faut couper et compter les valeurs des propriétés : df_wiki['len_nationality'] = df_wiki['nationality'].apply(lambda x : len(x.split('|')) if x else 0) La fonction découpe dans chaque cellule les valeurs concaténées (le découpage est fait par rapport à un caractère prédéfini, ici "|" que l'on avait utilisé pour séparer les concaténations dans la requête SPARQL). Cette étape doit être réalisée pour toutes les propriétés hormis la date de naissance comme chaque personne n'a qu'une seule date. Il est mieux d'ensuite changer l'ordre du tableau de données pour que chaque propriété soit associée sa longueur. df_wiki = df_wiki[[ 'birthDate', 'nationality', 'len_nationality','gender','len_gender','placeOfBirth','len_placeOfBirth', \ 'educatedAt', 'len_educatedAt', 'doctoralAdvisor', 'len_doctoralAdvisor', 'doctoralStudent', 'len_doctoralStudent',\ 'occupation', 'len_occupation', 'positionHeld', 'len_positionHeld', 'employer', 'len_employer']] Afin de ne pas réimporter les données à chaque fois, il est préférable de les exporter au format CSV. Et les réimporter par la suite. Une fois fait, il est possible de calculer le nombre de valeurs pour chaque propriété en les additionnant. properties_persons = [] for i,r in list(persons.iterrows()): ## iterrows permets d'avoir une liste de pairs. cf . https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.iterrows.html pour avoir de la do properties = [] if r['len_placeOfBirth'] > 0: properties.append('placeOfBirth') if r['len_gender'] > 0: properties.append('gender') if r['len_nationality'] > 0: properties.append('nationality') if r['len_educatedAt'] > 0: properties.append('educatedAt') if r['len_occupation'] > 0: properties.append('occupation') if r['len_positionHeld'] > 0: properties.append('positionHeld') if r['len_employer'] > 0: properties.append('employer') properties_persons.append([i, properties, len(properties)]) Pour chaque propriété, si la longueur est supérieure alors cela ajoute l'étiquette de la propriété, et ce, pour chaque personne. On peut ensuite créer un //dataframe// de "properties_persons". On peut ensuite calculer la distribution des effectifs des personnes et des propriétés. frequencies_properties_per_person = df_properties_persons.groupby('frequency').count() À partir de cela, une première visualisation de la distribution peut être faite. {{ :projets_individuels:distribution_proprietes_par_personne.png.png?direc&700 | distribution du nombre de propriétés par personne}} Cette première visualisation permet déjà d'avoir pas mal d'information sur les propriétés. Cela permet notamment de voir que la majorité des personnes ont entre 4 et 6 propriétés. La forme de cloche de la représentation le montre bien. Cela montre aussi qu'extrêmement peu de personnes n'ont qu'une seule propriété, de même que peu de personnes ont toutes les propriétés. Il est aussi possible d'étudier la distribution des propriétés avec une méthode très similaire à la précédente. freq_persons = 0 nationality = 0 gender = 0 placeOfBirth = 0 educatedAt = 0 occupation = 0 positionHeld = 0 employer = 0 for i,r in list(persons.iterrows()): freq_persons += 1 if r['len_nationality'] > 0: nationality += 1 if r['len_placeOfBirth'] > 0: placeOfBirth += 1 if r['len_gender'] > 0: gender += 1 if r['len_educatedAt'] > 0: educatedAt += 1 if r['len_occupation'] > 0: occupation += 1 if r['len_positionHeld'] > 0: positionHeld += 1 if r['len_employer'] > 0: employer += 1 Pour chaque personne, le code vérifie si une propriété est rentrée ou non et cela calcule leur nombre. On peut ensuite déterminer la distribution du nombre de propriétés disponibles. Ensuite, on met les propriétés et leur valeur en série : frequencies_properties = pd.Series({"freq_persons": freq_persons, "nationality": nationality, "placeOfBirth":placeOfBirth, "gender": gender, "educatedAt":educatedAt,\ "occupation":occupation, "positionHeld":positionHeld, "employer":employer }) frequencies_properties On peut aussi l'obtenir sous forme de visualisation : {{ :projets_individuels:frequency_per_available_property_without_doctorate.png?direct&700 | Distribution des propriétés disponibles}} Cette visualisation nous renseigne que c'est la propriété "genre" qui est la plus renseignée, sans doute, car c'est l'une des plus simples à obtenir. On peut remarquer aussi que la propriété "profession" est peu utilisée alors que c'est par cette même propriété que nous avons définie notre population. Mais cela s'explique, car ce sont seulement ceux avec une étiquette en anglais qui sortent. =====Étude des propriétés combinées===== ====Réseaux de propriétés==== À partir de ces analyses préliminaires, une analyse plus fine des propriétés est possible. Cette analyse peut être faite en montrant les réseaux que les propriétés ont entre elles. Ce réseau tend à montrer à quel point chaque propriété est entré avec tel autre. Pour cela, nous montrerons ici que les étapes essentielles pour aller voir les étapes intermédiaires, il faudra regarder dans le notebook présent sur Github. La première étape est de créer les arrêtes du réseau entre les propriétés (c'est-à-dire à quel point chaque propriété est relié à tel autre). properties_edges = [] for pp in properties_persons: local_properties = pp[1] len_prop = len(local_properties) # print(len_prop) if len_prop > 1: local_edges = [] i = 0 while i < (len_prop-1): i_len = i + 1 while i_len < (len_prop): local_edges.append((local_properties[i], local_properties[i_len])) i_len += 1 i += 1 # print(local_edges) properties_edges += local_edges Puis il faut créer le graphe à proprement parler. Pour cela, il faut utiliser sur python la librairie [[https://networkx.org/documentation/stable/tutorial.html|networkx]] qui permet de faire de l'analyse de réseaux. import networkx as nx # cf. https://networkx.org/documentation/stable/reference/classes/multigraph.html graph_properties = nx.MultiGraph() graph_properties.add_edges_from(properties_edges) print(nx.info(graph_properties)) Ensuite on créer un dictionnaire pour les effectifs et un dictionnaire pour les degrées de chaque propriété. frequency = dict([(i, {'frequency': v}) for i,v in frequencies_properties.items()]) print(frequency) degree = dict([(d[0], {'degree': d[1]}) for d in nx.degree(graph_properties)]) print(degree) À partir de cela, il est possible donner les effectifs et les degrées aux nœuds. nx.set_node_attributes(graph_properties, frequency) nx.set_node_attributes(graph_properties, degree) Ensuite on peut calculer les poids entre chaque propriétés. Pour cela, il faut créer une liste qui compte le poids qui existe entre deux propriétés. De cette liste, on crée un dictionnaire: # Use the list of the previous command and create a dictionary of the weights lll = [(l[0][0], l[0][1], {'weight' : l[1]}) for l in list(Counter([(u,v) for u,v in graph_properties.edges()]).items())] Ensuite, on peut créer un graphe avec les poids mais il faut dans le même temps convertir ce qui est jusqu'à présent un multigraphe en un graphe simple. weigth_graph_properties = nx.Graph() weigth_graph_properties.add_edges_from(lll) wgp = nx.Graph() for u,v,data in graph_properties.edges.data(): w = data['weight'] if 'weight' in data else 1 if wgp.has_edge(u,v): wgp[u][v]['weight'] += w else: wgp.add_edge(u, v, weight=w) Un moyen de vérifier que le graphe n'a de problème et notamment des //selfloops// éventuelles: print(nx.number_of_selfloops(weigth_graph_properties)) print(nx.is_weighted(weigth_graph_properties)) Enfin, on peut ajouter les effectifs et les degrées à ce graphe simple. nx.set_node_attributes(weigth_graph_properties, dict([(n, {'frequency': data['frequency']}) for n, data in graph_properties.nodes.data()])) nx.set_node_attributes(weigth_graph_properties, dict([(n, {'degree': data['degree']}) for n, data in graph_properties.nodes.data()])) Une fois que le graphe est totalement créé, il est possible de le visualiser. Cela se présente de la façon suivante: {{ :besson_sylvain:weigth_graph_properties.png?direct | Graphe des propriétés}} ==== Comparer les effectifs des autres caractéristiques pour chaque classe ==== Tout d'abord, il faut choisir les propriétés que l'on souhaite observer plus précisément. Pour cela, les précédentes analyses peuvent être d'une grande aide. Dans notre cas, nous avons retenu le "lieu de naissance, le "genre", la "nationalité", le "lieu d'étude", la "profession", l'"employeur" et le "poste". On commence par les mettre sous forme de série : observed_variables = {"educatedAt":"len_educatedAt","placeOfBirth": "len_placeOfBirth", "gender":"len_gender","nationality":"len_nationality", "employer":"len_employer", "occupation":"len_occupation", "positionHeld":"len_positionHeld", } for i,v in observed_variables.items(): print(i,v) Dans ce filtre, si une propriété a une valeur supérieure à zéro, alors la fonction vérifie si les autres propriétés sont rentrées. Cela se présente de la façon suivante : plot_id = 1 plot_left = 1 for ix,v in observed_variables.items(): freq_persons = 0 nationality = 0 placeOfBirth = 0 gender = 0 educatedAt = 0 occupation = 0 positionHeld = 0 employer = 0 for i,r in list(persons.iterrows()): if r[v] > 0: freq_persons += 1 if r['len_nationality'] > 0: nationality += 1 if r['len_placeOfBirth'] > 0: placeOfBirth += 1 if r['len_gender'] > 0: gender += 1 if r['len_educatedAt'] > 0: educatedAt += 1 if r['len_occupation'] > 0: occupation += 1 if r['len_positionHeld'] > 0: positionHeld += 1 if r['len_employer'] > 0: employer += 1 frequencies_properties = pd.Series({"nationality": nationality, \ "placeOfBirth":placeOfBirth, \ "gender":gender, \ "educatedAt":educatedAt,\ "occupation":occupation, \ "positionHeld":positionHeld, \ "employer":employer }) x = [l for l in frequencies_properties.index] y = [l for l in frequencies_properties] plt.subplot(3, 3, plot_id) plt.title(ix) plt.bar(x, y) plt.gcf().set_size_inches(12, 8) plt.xticks(rotation=45) plt.ylabel('frequency') plot_id += 1 plt.subplots_adjust(#left=0.1, bottom=0.1, right=1.6, top=2, # wspace=0.4, hspace=0.4 ) plt.show() La suite du code permet de créer une visualisation pour chaque propriété. Pour réaliser des sous-graphiques, cette [[https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subplot.html|page]] montre comment les réaliser. {{ :projets_individuels:frequencies_property_others_properties.png?direct |Distribution des autres propriétés par rapport à chaque propriété}} Dans ces représentations, il y a quelques éléments qui faut faire attention pour bien les comprendre. Tous d'abord, chaque propriété est représentée dans sa visualisation. Et étant donné qu'elle est comparée à elle-même, elle a nécessairement le maximum d'effectif. Ensuite, dans toutes les visualisations l'échelle n'est pas la même, car cela dépend de l'effectif de la propriété (ex: la propriété "genre" a quasiment 140 000 individus alors que "employer" n'en a que 25 000). C'est un biais visuel qui faut prendre en compte lors d'une analyse. Une autre façon de faire qui permet d'être plus précis dans l'analyse est de filtrer avec les différentes variables que l'on souhaite. Par exemple, il est possible de filtrer les propriétés "profession", "lieu d'étude" et "nationalité" et de vérifier le nombre de fois où ces trois propriétés apparaissaient ensembles. La fonction se présente de la façon suivante : freq_persons = 0 nationality = 0 placeOfBirth = 0 gender = 0 educatedAt = 0 occupation = 0 positionHeld = 0 employer = 0 for i,r in list(persons.iterrows()): # if r['len_occupation'] > 0 and r['len_educatedAt'] > 0 and r['len_nationality'] > 0 and r['len_gender']: # if r['len_occupation'] > 0 and r['len_positionHeld'] > 0 and \ # r['len_educatedAt'] > 0 and r['len_gender'] and r['len_nationality'] \ # and r['len_employer'] : if r['len_doctoralAdvisor'] > 0 and r['len_doctoralStudent'] > 0 and r['len_educatedAt'] > 0: # if r['len_employer'] > 0 and r['len_positionHeld'] > 0 : freq_persons += 1 if r['len_nationality'] > 0: nationality += 1 if r['len_placeOfBirth'] > 0: placeOfBirth += 1 if r['len_gender'] > 0: gender += 1 if r['len_educatedAt'] > 0: educatedAt += 1 if r['len_occupation'] > 0: occupation += 1 if r['len_positionHeld'] > 0: positionHeld += 1 if r['len_employer'] > 0: employer += 1 if r['len_doctoralAdvisor'] > 0: doctoralAdvisor += 1 if r['len_doctoralStudent'] > 0: doctoralStudent += 1 frequencies_properties_filter = pd.Series({ "nationality":nationality, \ "placeOfBirth":placeOfBirth, \ "gender":gender, \ "educatedAt":educatedAt,\ "occupation":occupation, \ "positionHeld":positionHeld, \ "employer":employer, \ }) frequencies_properties_filter {{ :projets_individuels:properties_combination_occupation_educatedat_nationality_gender_.png?direct&700 | Distribution avec les propriétés combinées: "profession", "lieu d'étude", "nationalité et "genre"}} ==== Filtrer les propriétés par individu ==== Il est possible aussi de filtrer les propriétés par individu. Un moyen de le faire est d'ajouter une dimension temporelle en utilisant les dates de naissance. Il est tout d'abord nécessaire de réaliser le même principe que pour déterminer le nombre de propriété par personne en ajoutant le filtre des propriétés que l'on souhaite étudier. Ensuite il faut créer un //dataframe// à partir de ce filtrer. Puis fusionner avec le jeu de données avec les dates de naissances des personnes. Ensuite, il faut créer une liste avec l'étendu d'années que l'on souhaite étudié (ici 20 ans): y20_list = pd.RangeIndex(start=1801, stop=2010, step=20).to_list() y20_list Puis on agrège les individus suivant la génération dans laquelles ils se trouvent: merged['generation'] = pd.cut(merged['birthDate'],y20_list, right=False) merged['generation'] = merged['generation'].apply(lambda row : str(int(row.left))+'-'+str(int(row.right))) De ce découpage, on peut calculer la fréquence par génération: gb_generation = merged.groupby('generation').size() gb_generation Enfin on peut créer une représentation de cette répartition dans dans le temps des propriétés filtrées: {{ :projets_individuels:frequency_birth_generation_period_occ_edat_nat_gend.png?direct |}} \\ \\ -------------------------------------- Il est possible ensuite de faire plusieurs analyses qualitatives en prenant en compte l'analyse de propriétés que nous avons faite plus haut afin de les mener à bien et appliquer certains filtres. Pour n'allons pas détailler ici l'ensemble de la méthode pour réaliser ces analyses mais il est possible de se reporter aux carnets correspondants. =====Étude des nationalités===== Ensuite, il est possible de faire une **[[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Analysis_nationalities_wikidata.ipynb|étude des nationalités]]**. La totalité de la méthode est expliquée dans le carnet dédié. L'étude des nationalités est réalisée à partir de la propriété “pays de citoyenneté” (P27) de Wikidata. Le problème est que ces pays évoluent dans le temps (les frontières évoluent, il peut avoir des changements de régime, etc.). Il est d'autant plus difficile que leur nombre est trop grand pour être bien étudier. La solution est d’agréger les pays par continent ou bien par région. La solution est d’agréger les pays par continent ou bien par région. Il est ensuite possible de réaliser une bivariée avec d'autres propriétés (genre, autres professions et type) et l'évolution dans le temps. ------------------------------------- =====Étude des professions===== De la même façon il est possible de faire une **[[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Analysis_occupations.ipynb|analyse des professions]]**. Les professions sont celles que les individus de la population effectuent en plus d'être économiste, juriste ou les deux. Il est possible ensuite de voir les différences entre les économistes et les juristes. Il est aussi possible de voir l'évolution dans le temps, ainsi que les différences de genre. ---------------------------------------- =====Analyse des correspondances multiples===== L'analyse des correspondances multiples est une méthode permettant d'étudier plusieurs variables qualitatives en même temps. Cela consiste à encoder les variables de façon à ce qu'elles soient interprétées comme des variables quantitatives. C'est-à-dire que chaque modalité d'une variable est replacé par des 0 et des 1. Il est ensuite possible de voir les proximités qu'il peut avoir entre les modalités et les proximités entre les individus. Pour réaliser l'ACM sur Python, vous pouvez consulter ce **[[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Analysis_MCA.ipynb|carnet]]**. ---------------------------------------- =====Analyse par les cartes===== Une autre méthode pour étudier les individus est la réalisation de carte. Il est possible de réaliser ces cartes sur Python. Deux méthodes sont développées. La première utilise les coordonnées géographiques et groupe sur une carte les individus avec les mêmes coordonnées. Il est ensuite possible d'ajouter de l'interactivité en affichant ces cartes sur une page HTML. L'autre méthode utilise les polygones (forme géométrique avec un nombre de sommet variable) en dessinant les contours des continents et ensuite en les affichant sur une carte. L'ensemble de la méhode est disponible dans ce **[[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Analysis_maps.ipynb|carnet]]**. ---------------------------------------- =====Analyse de réseaux===== Enfin, l'ultime méthode que nous avons pu réalisé est l'analyse de réseaux. Dans notre étude, l'analyse de réseaux permet de voir des liens entre des individus et entre des organisations. Un lien entre deux individus est crée lorsqu'ils ont été dans le même établissement dans un temps rapproché. De même, deux organisations sont liées lorsqu'un individus a été au sein des deux organisations. Pour cette analyse de réseau nous avons eu besoin de séparer en deux fichiers (sinon la taille autorisée par Github était dépassée). Dans la **[[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Analysis_network_educatedAt_part1.ipynb|première partie]]** se trouve la façon de créer les relations du réseau par une base de données SQLite. La **[[https://github.com/Semantic-Data-for-Humanities/Economists_Jurists/blob/main/Notebooks/Analysis_network_educatedAt_Script_part2.ipynb|seconde partie]]** est consacrée à l'analyse de réseaux à proprement parler.