Notebook réalisé par Pierre-Alexandre Aranega et Charles Cros dans le cadre du projet Python pour l'économiste (ENSAE 2A - 2019-2020).
Le 3 décembre 2019, l'OCDE a publié les résultats de l'enquête PISA (Programme for International Student Assessment) menée en 2018 dans 79 pays, sur des élèves de 15 ans, et répétée tous les trois ans depuis l'an 2000. Ces résultats sont très commentés et sont considérés comme un indicateur pertinent de l'évolution du niveau scolaire des élèves, de manière absolue et de manière relative (en comparaison avec les pays voisins). Quelques articles sur la question :
Suivant les articles ou les publications officielles, il est difficile de savoir s'il faut ou non se réjouir de ces résultats. Les commentaires ne sont jamais neutres : certains s'inquiètent d'une nouvelle baisse des résultats français ; d'autres relativisent, observent que cette baisse est tout de même moins forte que la précédente, et que la France reste au-dessus de la moyenne des pays de l'OCDE...
L'objectif de ce projet est de :
Pour information, toutes les publications de l'OCDE sont disponibles à cette adresse. L'enquête PISA couvre de très nombreux sujets, divers et passionnants, qui ne se limitent pas à la seule question de la performance. Soucieux de rester concis, on ne pourra évidemment pas tous les aborder ici.
from jyquickhelper import add_notebook_menu
add_notebook_menu()
import numpy as np
import pandas as pd
import urllib
import bs4
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
import statsmodels.api as sm
from sklearn.cluster import KMeans
Les données sont issues de la page Wikipedia Programme for International Student Assessment.
url_wikipedia = "https://en.wikipedia.org/wiki/Programme_for_International_Student_Assessment"
request_text_PISA = urllib.request.urlopen(url_wikipedia).read()
page_PISA = bs4.BeautifulSoup(request_text_PISA, "lxml")
Cette section se concentre sur le paragraphe PISA 2018 ranking summary qui donne les résultats de l'année 2018 dans un format particulier.
On aurait aimé disposer d'un id, d'une classe ou d'un titre permettant de cibler facilement le bon tableau. Faute de mieux, on se résout à utiliser une spécificité qui est la largeur de table, fixée à 240px pour les sous-tables qui nous intéressent ici.
# On se concentre sur les tables ayant la particularité de faire 240px de largeur...
tables_2018 = page_PISA.findAll("table", width="240")
tables_2018 = pd.read_html(str(tables_2018))
# On définit les headers manuellement afin d'harmoniser avec les intitulés de la partie suivante.
liste_matieres = ["Maths", "Science", "Reading"]
# On fusionne ces tables dans un dataframe
PISA_2018 = pd.DataFrame()
for id_table in range(0,3):
table = tables_2018[id_table].iloc[:,1:3]
table.columns = ["Country", "Score"]
table["Year"] = 2018
table["Subject"] = liste_matieres[id_table]
PISA_2018 = pd.concat([PISA_2018,table])
# Les résultats de la France en 2018
PISA_2018[PISA_2018["Country"]=="France"]
Country | Score | Year | Subject | |
---|---|---|---|---|
24 | France | 495 | 2018 | Maths |
23 | France | 493 | 2018 | Science |
22 | France | 493 | 2018 | Reading |
Cette section se concentre sur le paragraphe Rankings comparison 2003 - 2015 qui regroupe les résultats des années précédentes.
De même que ci-dessus, nous n'avons pas d'id unique pour se référer aux tableaux qui nous intéressent. On choisit de ne garder que les tables ayant au moins 9 colonnes, ce qui permet de filtrer les trois tableaux qui nous préoccupent de manière un peu brutale, mais rapide.
all_tables = page_PISA.findAll("table")
all_tables = pd.read_html(str(all_tables), header=[0,1], na_values = ["—", "‡"])
# On ne s'intéresse qu'aux trois tables ayant + de 9 colonnes.
PISA = PISA_2018
for table in all_tables:
if len(table.columns) > 8:
subject = table.columns[0][0]
table.columns = table.columns.droplevel(0)
# On supprime toutes les colonnes de "rang" qui nous sont inutiles.
for column in table.columns:
if table[column][0] == "Rank":
table = table.drop(columns=column)
# On supprime les 2 premières lignes : "moyenne OCDE" et sous-titre "Score" qui nous sont inutiles.
table = table.drop([0,1])
# On dé-pivote les années.
table = table.melt(id_vars="Country",var_name="Year",value_name="Score")
# On ajoute la matière en colonne.
table["Subject"] = subject
# On fusionne avec le tableau complet, qui avait commencé à être créé pour l'année 2018.
PISA = pd.concat([PISA,table], sort=True)
PISA[PISA["Country"]=="France"].head()
Country | Score | Subject | Year | |
---|---|---|---|---|
24 | France | 495 | Maths | 2018 |
23 | France | 493 | Science | 2018 |
22 | France | 493 | Reading | 2018 |
22 | France | 493 | Maths | 2015 |
95 | France | 495 | Maths | 2012 |
# Réinitialisation de l'index du dataframe, suite aux diverses concaténations / suppressions.
PISA = PISA.reset_index(drop=True)
# Suppression des scores N/A
PISA = PISA[pd.notnull(PISA["Score"])]
# Conversion de certains formats puis tri.
PISA["Year"]=PISA["Year"].astype(int)
PISA["Score"]=PISA["Score"].astype(int)
PISA = PISA.sort_values(by=["Year", "Subject", "Score"], ascending=False)
On commence avant tout par régler quelques cas particuliers qui pourraient poser problème lors des jointures à venir.
# On ignore le cas particulier de Buenos Aires, traitée séparément du reste de l'Argentine.
PISA = PISA[PISA["Country"]!="Argentina CABA[b]"]
# On harmonise le nom de certains pays qui varient selon les bases.
PISA.loc[PISA["Country"].str.contains("China"), "Country"] = "China"
PISA["Country"] = PISA["Country"].replace("Hong Kong, China", "Hong Kong")
PISA["Country"] = PISA["Country"].replace("Czech Republic", "Czechia")
PISA["Country"] = PISA["Country"].replace(["South Korea", "Korea"], "Korea, South")
On souhaite disposer, dans la suite de ce devoir, de la liste des pays membres de l'OCDE. Ils constitueront notre pool de comparaison au fil du temps.
url_wikipedia_OECD = "https://en.wikipedia.org/wiki/OECD"
request_text_OECD = urllib.request.urlopen(url_wikipedia_OECD).read()
page_OECD = bs4.BeautifulSoup(request_text_OECD, "lxml")
OECD_countries = page_OECD.findAll("table", class_="wikitable sortable")
OECD_countries = pd.read_html(str(OECD_countries), header=0)[0]["Country"]
print("Les", len(OECD_countries), "pays membres de l'OCDE sont :", OECD_countries.str.cat(sep=", "))
Les 36 pays membres de l'OCDE sont : Australia, Austria, Belgium, Canada, Chile, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Iceland, Ireland, Israel, Italy, Japan, South Korea, Latvia, Lithuania, Luxembourg, Mexico, Netherlands, New Zealand, Norway, Poland, Portugal, Slovakia, Slovenia, Spain, Sweden, Switzerland, Turkey, United Kingdom, United States
# Ajoutons cette information à la base principale
PISA["is_OECD"] = PISA.apply(lambda row : (row["Country"] in OECD_countries.values)*1, axis = 1 )
PISA.head()
Country | Score | Subject | Year | is_OECD | |
---|---|---|---|---|---|
78 | China | 590 | Science | 2018 | 0 |
79 | Singapore | 551 | Science | 2018 | 0 |
80 | Macau | 544 | Science | 2018 | 0 |
81 | Estonia | 530 | Science | 2018 | 1 |
82 | Japan | 529 | Science | 2018 | 1 |
On aura également besoin d'accéder au code ISO-3 (identifiant à trois lettres) de chaque pays, notamment pour l'utilisation du package servant à tracer une carte choroplèthe. Utilisons, pour varier, le site de la CIA. Le scrapping s'avère facilité : il n'y a qu'une seule table sur toute la page.
url_CIA_iso3codes = "https://www.cia.gov/library/publications/the-world-factbook/appendix/appendix-d.html"
request_text_CIA = urllib.request.urlopen(url_CIA_iso3codes).read()
page_iso3codes = bs4.BeautifulSoup(request_text_CIA, "lxml")
table_iso3codes = page_iso3codes.findAll("table")
table_iso3codes = pd.read_html(str(table_iso3codes), header=0, na_values="-")[0]
table_iso3codes = table_iso3codes[["entity", "iso 3166.1"]]
table_iso3codes.columns = ["Country", "iso3code"]
print(str(len(table_iso3codes["iso3code"].unique())) + " codes ISO3 ont bien été importés.")
250 codes ISO3 ont bien été importés.
# Ajoutons cette information à la base principale.
PISA = PISA.merge(table_iso3codes, on="Country", how="left")
PISA.head()
Country | Score | Subject | Year | is_OECD | iso3code | |
---|---|---|---|---|---|---|
0 | China | 590 | Science | 2018 | 0 | CHN |
1 | Singapore | 551 | Science | 2018 | 0 | SGP |
2 | Macau | 544 | Science | 2018 | 0 | MAC |
3 | Estonia | 530 | Science | 2018 | 1 | EST |
4 | Japan | 529 | Science | 2018 | 1 | JPN |
Étant donné que l'on va importer successivement plusieurs indicateurs depuis le site de la Banque Mondiale, on définit une fonction pour cela. L'organisme a le bon goût de proposer un API d'export au format Excel de tous ses indicateurs, avec toujours le même format.
def import_from_World_Bank(ref_indicator, name):
# Lecture depuis le site Banque Mondiale
url_WB = "http://api.worldbank.org/v2/en/indicator/"+ ref_indicator +"?downloadformat=excel"
table = pd.read_excel(url_WB, header=3)
# Remplissage des N/A avec la dernière valeur disponible.
sub_table = table[table.columns[4:]]
sub_table = sub_table.fillna(method="ffill", axis="columns")
sub_table = sub_table.fillna(method="bfill", axis="columns")
table[table.columns[4:]] = sub_table
# Dé-pivotage des colonnes d'année
table = table.melt(id_vars=table.columns[0:4], var_name="Year", value_name=name)
# Filtrage des colonnes qui nous intéressent (pays, année, valeur) puis renommage
table = table[table.columns[[1,4,5]]]
table.columns = ["iso3code", "Year", name]
# Changement de format puis tri par année
table["Year"] = table["Year"].astype(int)
table = table.sort_values(by="Year", ascending=False)
return table
table_GDP_capita = import_from_World_Bank("NY.GDP.PCAP.KD", "GDP_per_capita")
PISA = PISA.merge(table_GDP_capita, on=["iso3code", "Year"], how="left")
Source : Life expectancy at birth, total (years), United Nations
# Ajoutons cette information à la base principale.
table_life_expect = import_from_World_Bank("SP.DYN.LE00.IN", "life_expect")
PISA = PISA.merge(table_life_expect, on=["iso3code", "Year"], how="left")
# Ajoutons cette information à la base principale.
table_gov_exp = import_from_World_Bank("SE.XPD.TOTL.GD.ZS", "gov_exp")
PISA = PISA.merge(table_gov_exp, on=["iso3code", "Year"], how="left")
Source : Pupil-teacher ratio, primary, UNESCO Institute for Statistics (uis.unesco.org)
table_pupil_teacher_ratio = import_from_World_Bank("SE.PRM.ENRL.TC.ZS", "pupil_teacher_ratio")
PISA = PISA.merge(table_pupil_teacher_ratio, on=["iso3code", "Year"], how="left")
# Version définitive de la table, qui sera utilisée dans la suite de ce devoir.
PISA.head()
Country | Score | Subject | Year | is_OECD | iso3code | GDP_per_capita | life_expect | gov_exp | pupil_teacher_ratio | |
---|---|---|---|---|---|---|---|---|---|---|
0 | China | 590 | Science | 2018 | 0 | CHN | 7752.559525 | 76.470000 | 1.88804 | 16.42675 |
1 | Singapore | 551 | Science | 2018 | 0 | SGP | 58247.872640 | 82.895122 | 2.89769 | 14.69428 |
2 | Macau | 544 | Science | 2018 | 0 | MAC | 58641.627664 | 83.989000 | 2.71071 | 13.49843 |
3 | Estonia | 530 | Science | 2018 | 1 | EST | 19954.130111 | 77.641463 | 5.17316 | 11.31153 |
4 | Japan | 529 | Science | 2018 | 1 | JPN | 48919.798942 | 84.099756 | 3.59059 | 15.66096 |
De manière alternative, on propose une version "pivotée", qui pourra nous faciliter la tâche dans la suite.
PISA_pivote = PISA
# Avant de pivoter, on remplace les N/A par des fausses valeurs (seule solution trouvée pour éviter leur disparition)
PISA_pivote = PISA_pivote.fillna("foo")
PISA_pivote = PISA_pivote.pivot_table(columns="Subject", values="Score",
index=["Country", "Year", "is_OECD", "iso3code", "GDP_per_capita",
"life_expect", "gov_exp", "pupil_teacher_ratio"])
PISA_pivote = PISA_pivote.reset_index(drop=False).rename_axis(None, axis=1)
PISA_pivote = PISA_pivote.replace("foo", np.nan)
# On lui ajoute une colonne contenant le score moyen par pays et par année.
PISA_pivote["Mean_score"] = PISA_pivote.apply(
lambda row: np.round(np.nanmean([row["Maths"], row["Reading"], row["Science"]]),1), 1)
PISA_pivote = PISA_pivote.sort_values(by=["Year", "Country"], ascending=False)
PISA_pivote.head()
Country | Year | is_OECD | iso3code | GDP_per_capita | life_expect | gov_exp | pupil_teacher_ratio | Maths | Reading | Science | Mean_score | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
394 | Uruguay | 2018 | 0 | URY | 14617.464002 | 77.632000 | 4.86891 | 11.01930 | 418.0 | 427.0 | 426.0 | 423.7 |
388 | United States | 2018 | 1 | USA | 54579.016837 | 78.539024 | 4.96174 | 14.19857 | 478.0 | 505.0 | 502.0 | 495.0 |
381 | United Kingdom | 2018 | 1 | GBR | 43324.592970 | 81.156098 | 5.48697 | 15.13275 | 502.0 | 504.0 | 505.0 | 503.7 |
374 | United Arab Emirates | 2018 | 0 | ARE | 40782.443624 | 77.647000 | NaN | 24.52278 | 435.0 | 432.0 | 434.0 | 433.7 |
371 | Ukraine | 2018 | 0 | UKR | 3110.194646 | 71.780976 | 5.41400 | 12.98011 | 453.0 | 466.0 | 469.0 | 462.7 |
On exporte à toutes fins utiles les deux versions de notre base de données au format csv.
PISA_pivote.to_csv("PISA_pivote.csv")
PISA.to_csv("PISA.csv")
Specific_year = 2018
Ranking_specific_year = PISA_pivote[PISA_pivote["Year"]==Specific_year]
print("Top 5 des meilleurs scores PISA en 2018 :")
Ranking_specific_year[['Country', 'Maths', 'Reading', 'Science', 'Mean_score']].sort_values(by="Mean_score",ascending=False).reset_index(drop=True).head()
Top 5 des meilleurs scores PISA en 2018 :
Country | Maths | Reading | Science | Mean_score | |
---|---|---|---|---|---|
0 | China | 591.0 | 555.0 | 590.0 | 578.7 |
1 | Singapore | 569.0 | 549.0 | 551.0 | 556.3 |
2 | Macau | 558.0 | 525.0 | 544.0 | 542.3 |
3 | Hong Kong | 551.0 | 524.0 | 517.0 | 530.7 |
4 | Estonia | 523.0 | 523.0 | 530.0 | 525.3 |
Remarques sur ce top 5 :
Intéressons-nous désormais aux 5 pays obtenant les moins bons scores en 2018.
print("Top 5 des pires scores PISA en 2018 :")
Ranking_specific_year[['Country', 'Maths', 'Reading', 'Science', 'Mean_score']].sort_values(by="Mean_score",ascending=True).reset_index(drop=True).head()
Top 5 des pires scores PISA en 2018 :
Country | Maths | Reading | Science | Mean_score | |
---|---|---|---|---|---|
0 | Dominican Republic | 325.0 | 342.0 | 336.0 | 334.3 |
1 | Philippines | 353.0 | 340.0 | 357.0 | 350.0 |
2 | Kosovo | 366.0 | 353.0 | 365.0 | 361.3 |
3 | Panama | 353.0 | 377.0 | 365.0 | 365.0 |
4 | Morocco | 368.0 | 359.0 | 377.0 | 368.0 |
On observe que les pays obtenant les pires scores moyens ont quasiment tous (sauf le Liban, qui stagne) perdu des places par rapport à l'édition 2015.
On va se demander à quoi ressemble un "bon" ou un "mauvais" score PISA en 2018 en étudiant la répartition des scores parmi les pays participants.
np.round(Ranking_specific_year[["Maths", "Reading", "Science"]].describe(),1).transpose()
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
Maths | 78.0 | 458.6 | 56.4 | 325.0 | 417.2 | 468.0 | 500.0 | 591.0 |
Reading | 77.0 | 453.1 | 53.1 | 340.0 | 412.0 | 466.0 | 498.0 | 555.0 |
Science | 78.0 | 457.9 | 51.9 | 336.0 | 417.5 | 468.0 | 498.5 | 590.0 |
On remarque que, pour l'année 2018 :
Intéressons-nous désormais à la distribution des scores.
sns.set()
fig, axs = plt.subplots(1, 2, figsize=(17,5))
sns.distplot(Ranking_specific_year["Maths"].dropna(), hist=False, label="Maths", ax = axs[0])
sns.distplot(Ranking_specific_year["Reading"].dropna(), hist=False, label="Reading", ax = axs[0])
sns.distplot(Ranking_specific_year["Science"].dropna(), hist=False, label="Science", ax = axs[0])
sns.distplot(Ranking_specific_year["Mean_score"].dropna(), hist=False, kde=False, rug=True, ax = axs[0])
axs[0].set_title("Fonction de densité des scores")
axs[0].set_xlabel("Score obtenu à l'épreuve")
sns.boxplot(x="Subject", y="Score", data=PISA[PISA["Year"]==Specific_year], palette="Set3", ax = axs[1])
axs[1].set_title("Répartition du score dans chaque discipline lors de l'édition 2018 du test PISA")
axs[1].set_ylabel("Score obtenu à l'épreuve")
plt.show()
On constate que :
Ranking_previous_test = PISA_pivote[PISA_pivote["Year"]==Specific_year - 3][["Country", "Mean_score"]]
Ranking_previous_test.columns = ["Country", "Mean_score_previous_test"]
Ranking_specific_year = Ranking_specific_year.merge(Ranking_previous_test, on="Country")
new_col = Ranking_specific_year.apply(lambda row : (row["Mean_score"]-row["Mean_score_previous_test"]), axis=1)
Ranking_specific_year["Evolution"] = new_col
fig = px.choropleth(Ranking_specific_year, locations="iso3code",
color="Evolution", hover_name="Country",
color_continuous_scale="Spectral", animation_frame="Year",
range_color = [-30,30],
title='Évolution du score obtenu en 2018 par rapport à celui obtenu en 2015 (moyenne sur les 3 disciplines)')
fig.show()
L'édition 2018 révèle une tendance préoccupante en Europe particulièrement où la majorité des pays voient leur score diminuer par rapport à l'édition 2015. Remarquons la présence de quelques valeurs extrêmes :
fig = px.choropleth(PISA_pivote, locations="iso3code",
color="Mean_score", hover_name="Country",
color_continuous_scale="Spectral", animation_frame="Year",
range_color = [350,550],
title='Score obtenu au test PISA par année et par pays (moyenne sur les 3 disciplines)')
print("Utiliser la barre de temps en bas de la carte pour faire défiler les années.")
fig.show()
Utiliser la barre de temps en bas de la carte pour faire défiler les années.
Rappel : pour la Chine, le test PISA ne couvre en fait que les villes de Beijing, Shanghai, Jiangsu et Zhejiang. On affiche la valeur sur tout le territoire par abus, mais celle-ci est donc à considérer avec précaution.
Quelques cas intéressants révélés par cette carte, en guise d'illustration :
PISA_France = PISA[PISA["Country"]=="France"]
sns.set()
fig, axs = plt.subplots(1, 1, figsize=(17,5))
sns.lineplot(x="Year", y="Score", hue="Subject", data=PISA_France, ax=axs, linewidth=2)
plt.xticks(range(2000,2021,3))
plt.title("Évolution des performances de la France en lecture, mathématiques et sciences depuis 2000.")
plt.show()
On constate, comme l'ont souligné de nombreux journaux, que la France voit ses performances diminuer régulièrement depuis 2012. Plus particulièrement : dans la dernière enquête PISA réalisée en 2018, la France a perdu 6 points en lecture et 2 en sciences, en comparaison avec la précédente étude réalisée en 2015. Seule consolation : elle en regagne 2 en mathématiques.
PISA_France_pivote = PISA_pivote[PISA_pivote["Country"]=="France"]
PISA_France_pivote[PISA_France_pivote["Year"].isin(range(2012,2019))][["Country", "Year", "Mean_score"]]
Country | Year | Mean_score | |
---|---|---|---|
115 | France | 2018 | 493.7 |
114 | France | 2015 | 495.7 |
113 | France | 2012 | 499.7 |
En moyenne, toutes disciplines confondes, elle a ainsi perdu 4 poids entre 2012 et 2015, puis encore 2 points entre 2015 et 2018.
Cela n'aurait pas de sens de comparer la France avec la performance moyenne de tous les pays participants aux tests PISA car de nouveaux pays (souvent, des pays aux performances inférieures à la moyenne) s'ajoutent à chaque édition. On choisit donc de comparer la France aux pays membres de l'OCDE, qui constituent un ensemble de pays relativement homogène, qui participent historiquement à ce test, et qui sont souvent pris pour groupe de référence.
# Calcul des scores moyens pour les pays de l'OCDE
PISA_OECD_aggregated = PISA[PISA["is_OECD"]==1].groupby(['Year', 'Subject', 'is_OECD']).agg("mean")
PISA_OECD_aggregated = PISA_OECD_aggregated.reset_index(drop=False)
PISA_OECD_aggregated["Country"] = "OECD mean"
PISA_France_vs_OECD = pd.concat([PISA_France, PISA_OECD_aggregated], sort = True)
# Affichage du graphique comparant la France avec les pays de l'OCDE
sns.set()
fig, axs = plt.subplots(1, 1, figsize=(17,5))
sns.lineplot(x="Year", y="Score", hue="Subject", style="Country", data=PISA_France_vs_OECD, ax=axs, linewidth=2)
plt.xticks(range(2000,2021,3))
plt.title("Évolution de la performance moyenne des pays de l'OCDE (pointillés) et de la France (trait plein) en lecture, mathématiques et sciences depuis 2000.")
plt.show()
Ce graphique conduit à relativiser, ou du moins à donner un contexte, à la contre-performance française. Certes, la performance de la France ces dernières années n'est pas fameuse. Mais si on la compare, discipline par discipline, la France est toujours au-dessus de la moyenne des pays de l'OCDE.
Ce graphique pousse donc à tirer des conclusions plus positives que précédemment : en fait, tout dépend comment on utilise le score PISA.
Le commentateur, suivant son objectif, ne manquera pas d'insister sur l'un ou l'autre des constats pour "faire parler les chiffres". Sans surprise, le communiqué du Ministère de l'Éducation Nationale, cité en introduction, se concentre plutôt sur le second point.
Le package Plotly nous permet de proposer des boxplots dynamiques, qui facilitent la comparaison de la répartition au fil des ans.
PISA_OECD = PISA[PISA["is_OECD"]==1].copy()
PISA_OECD = PISA_OECD[PISA_OECD["Year"]>=2006]
fig = px.box(PISA_OECD, x="Subject", y="Score", hover_name="Country", animation_frame="Year", points="all",
title="Évolution de la répartition des scores au sein de l'OCDE, par année et par compétence.")
fig.update_yaxes(range=[400, 600])
print("Utiliser la barre de temps en bas du graphique pour faire défiler les années.")
fig.show()
Utiliser la barre de temps en bas du graphique pour faire défiler les années.
Deux constats principaux surgissent de ces représentations lorsqu'on fait varier l'axe temps. Entre 2012 et 2018 :
Cette partie se concentre sur les tests réalisés après 2006, car toutes les épreuves n'existaient pas encore les années précédentes.
PISA_pivote_post2006 = PISA_pivote[PISA_pivote["Year"]>=2006].copy()
On est donc en présence, pour chaque pays, et pour chaque édition du test PISA, de trois variables : le score en maths, en lecture, et en sciences. On a pu observer, sur divers exemples, que la majorité des pays obtenaient des scores très similaires dans les trois domaines (un pays "bon" en maths est généralement "bon" en sciences et en lecture). De plus, on a étudié, à plusieurs reprises, le "score moyen" calculé par pays et par année. Cet indicateur est pratique parce qu'il facilite la comparaison, mais il peut néanmoins cacher des disparités. On voudrait donc savoir s'il existe des pays qui performent particulièrement mieux dans un domaine que dans un autre, ce que ne permettrait pas de voir un score moyen.
print("Matrice de corrélation entre les scores obtenus dans les trois compétences.")
np.round(PISA_pivote_post2006[["Maths", "Reading", "Science"]].corr(),3)
Matrice de corrélation entre les scores obtenus dans les trois compétences.
Maths | Reading | Science | |
---|---|---|---|
Maths | 1.000 | 0.949 | 0.974 |
Reading | 0.949 | 1.000 | 0.969 |
Science | 0.974 | 0.969 | 1.000 |
On constate sans surprise que les trois variables sont très fortement corrélées :
Obtenir un bon ou un mauvais score dans une compétence est donc fortement corrélé au fait d'obtenir un bon ou un mauvais score dans une autre compétence. Ce résultat est confirmé par les graphiques croisés ci-dessous, qui croisent le score dans une matière avec le score dans une autre matière.
sns.pairplot(PISA_pivote_post2006[["Maths", "Reading", "Science"]].dropna(), diag_kind="kde")
print("Score dans une matière en fonction du score dans une autre matière.")
Score dans une matière en fonction du score dans une autre matière.
On constate que certains pays s'éloignent quand même légèrement de la bissectrice sur les graphiques ci-dessus. Cela est particulièrement visible sur le graphique qui croise la performance en maths et en lecture, les deux composantes les moins corrélées. On voudrait pouvoir identifier ces pays. Pour cela, on va étudier l'écart [max(maths, reading, science) - min(maths, reading, science)], pour chaque pays et pour chaque année.
Ecart_max = PISA_pivote_post2006.apply(lambda row : max(row["Maths"],row["Reading"],row["Science"])
- min(row["Maths"],row["Reading"],row["Science"]), axis=1)
PISA_pivote_post2006["Ecart_max"] = Ecart_max
print("Top 5 des pays ayant le plus gros écart [max-min] en 2018")
PISA_pivote_post2006=PISA_pivote_post2006.sort_values(by=["Year","Ecart_max"], ascending=False)
np.round(PISA_pivote_post2006[["Country", "Ecart_max", "Maths", "Reading", "Science"]], 1).head(5)
Top 5 des pays ayant le plus gros écart [max-min] en 2018
Country | Ecart_max | Maths | Reading | Science | |
---|---|---|---|---|---|
207 | Lebanon | 40.0 | 393.0 | 353.0 | 384.0 |
189 | Kazakhstan | 36.0 | 423.0 | 387.0 | 397.0 |
65 | China | 36.0 | 591.0 | 555.0 | 590.0 |
58 | Chile | 35.0 | 417.0 | 452.0 | 444.0 |
253 | Netherlands | 34.0 | 519.0 | 485.0 | 503.0 |
On constate donc que, pour la seule année 2018, les pays participants affichent des écarts [max-min] allant jusqu'à 40 points. Par exemple, le Liban obtient un score 40 points plus élevé en maths qu'en lecture. À l'inverse, le Chili obtient un score 35 points plus élevé en lecture qu'en maths.
fig = px.box(PISA_pivote_post2006, x="Year", y="Ecart_max", hover_name="Country", points="all",
hover_data=["Maths", "Reading", "Science","is_OECD"],
title="Répartition de l'écart [max-min] calculé précédemment, par année.")
fig.show()
Exemple de lecture : en 2018, on constate que...
La visualisation 3D ci-dessous permet de synthétiser cette partie sur l'écart [max-min] en affichant, pour chaque pays : son score en maths, en lecture, en sciences. On peut ainsi isoler les pays qui "dévient" de l'axe x=y=z et repérer facilement les pays qui seraient meilleurs en lecture qu'en mathématiques, par exemple.
fig = px.scatter_3d(PISA_pivote_post2006, x="Maths", y="Reading", z="Science", color="Ecart_max",
hover_name="Country", hover_data=["Maths", "Reading", "Science", "Mean_score"],
color_continuous_scale="Picnic", animation_frame="Year", range_color = [0,40],
title="Visualisation 3D du score en maths, lecture, sciences, par pays et par année.")
fig.show()
Les considérations ci-dessus incitent donc à la vigilance quand on parle du "classement" PISA : il est pratique de comparer des scores moyens, comme nous l'avons fait, et comme il est courant de le faire par ailleurs. Les moyennes demeurent utiles et pertinentes, parce que les écarts restent généralement modérés : il n'y a pas de pays qui serait premier en mathématiques et dernier en lecture, par exemple... Toutefois, se limiter à des moyennes est une approche incomplète. Par exemple, les Pays-Bas obtiennent un score moyen supérieur de 10 points à celui de la France. On serait alors tentés d'en conclure que ce pays est "meilleur", "mieux classé", ou même que son système scolaire fonctionne mieux. Pourtant, ses résultats en lecture sont inférieurs de 8 points à ceux de la France (485 vs 493). On retiendra donc qu'il est indispensable d'adopter quelques précautions de langage quand on parle de "classement PISA" et que les moyennes ne sont qu'un agrégat qui cache des disparités.
Dans la partie II - A - 2. Comment sont réparties les performances en 2018 ?, on a représenté la densité des scores obtenus en maths, en lecture et en sciences en 2018. On a remarqué, en observant ces courbes, qu'elles s'apparentaient "à vue d'oeil", à la somme de deux courbes en cloche, ce qui laisse à penser qu'on pourrait scinder les pays participants en deux groupes relativement homogènes en termes de performance. Au moyen d'un algorithme k-means, on va chercher à classifier les pays participants au test PISA en deux catégories, pour chaque édition du test.
Les seules variables d'entrée utilisées pour former les clusters sont le score en mathématiques, en lecture et en science. Les différents indicateurs économiques qui seront étudiés plus tard n'entrent pas du tout en compte dans la formation des clusters.
sub_table = PISA_pivote[pd.notnull(PISA_pivote["Maths"]) & pd.notnull(PISA_pivote["Reading"])
& pd.notnull(PISA_pivote["Science"])].copy()
model=KMeans(n_clusters=2)
PISA_pivote_clusters = pd.DataFrame()
for Year in sub_table["Year"].unique():
# On calcule les clusters séparément pour chaque année du test PISA.
sub_table_year = sub_table[sub_table["Year"] == Year].copy()
model.fit(sub_table_year[["Maths", "Science", "Reading"]])
sub_table_year["Cluster"] = model.labels_
# Précaution pour s'assurer que les id de clusters soient rangés par niveau de performance moyen croissant.
Mean_by_cluster = sub_table_year.groupby("Cluster", as_index=False).agg("mean").sort_values(by="Mean_score")
i=1
for Cluster in Mean_by_cluster['Cluster']:
sub_table_year['Cluster'] = sub_table_year['Cluster'].replace(Cluster, 'Cluster '+str(i))
i+=1
PISA_pivote_clusters = pd.concat([PISA_pivote_clusters, sub_table_year])
fig = px.scatter_3d(PISA_pivote_clusters, x="Maths", y="Reading", z="Science", color="Cluster",
hover_name="Country", hover_data=["Maths", "Reading", "Science"], animation_frame="Year",
title="Performance en maths, lecture et sciences des pays composant les deux clusters, par année.",
symbol="is_OECD")
fig.show()
Cette sous-partie se concentre sur l'année 2018 en guise d'illustration.
PISA_pivote_2018_clusters = PISA_pivote_clusters[PISA_pivote_clusters["Year"]==2018].copy()
Cluster_1 = PISA_pivote_2018_clusters[PISA_pivote_2018_clusters["Cluster"]=='Cluster 1']
print("Le cluster 1 est composé de", len(Cluster_1["iso3code"]), "pays, dont le score moyen est compris entre", min(Cluster_1["Mean_score"]), "et", max(Cluster_1["Mean_score"]))
Cluster_2 = PISA_pivote_2018_clusters[PISA_pivote_2018_clusters["Cluster"]=='Cluster 2']
print("Le cluster 2 est composé de", len(Cluster_2["iso3code"]), "pays, dont le score moyen est compris entre", min(Cluster_2["Mean_score"]), "et", max(Cluster_2["Mean_score"]))
Le cluster 1 est composé de 34 pays, dont le score moyen est compris entre 334.3 et 442.3 Le cluster 2 est composé de 43 pays, dont le score moyen est compris entre 453.3 et 578.7
PISA_2018_clusters = PISA_pivote_2018_clusters.melt(id_vars=['Country', 'Year', 'is_OECD', 'iso3code', 'GDP_per_capita', 'life_expect', 'gov_exp', 'pupil_teacher_ratio', 'Cluster', 'Mean_score'], var_name='Subject', value_name='Score')
g = sns.FacetGrid(PISA_2018_clusters, row="Subject", col="Cluster", height=1.7, aspect=4)
g.map(sns.distplot, "Score", hist=False, rug=True);
On a bien réussi à décomposer les participants en deux catégories homogènes, dont les scores ont pour densité une courbe en cloche comme anticipé. Comparons les caractéristiques de ces deux clusters.
print("Comparaison des caractéristiques moyennes entre les deux clusters.")
np.round(PISA_pivote_2018_clusters.groupby(['Cluster', 'Year']).agg('mean'), 1)
Comparaison des caractéristiques moyennes entre les deux clusters.
is_OECD | GDP_per_capita | life_expect | gov_exp | pupil_teacher_ratio | Maths | Reading | Science | Mean_score | ||
---|---|---|---|---|---|---|---|---|---|---|
Cluster | Year | |||||||||
Cluster 1 | 2018 | 0.1 | 12324.5 | 76.0 | 4.2 | 17.3 | 404.2 | 401.0 | 407.4 | 404.2 |
Cluster 2 | 2018 | 0.7 | 39642.5 | 80.2 | 4.9 | 13.6 | 501.2 | 494.3 | 497.3 | 497.6 |
On observe donc l'existence de deux groupes, tous deux plutôt homogènes dans leur composition en termes de performance scolaire. Cette dichotomie semble assez intuitive puisqu'elle consiste à séparer :
Au passage, rappelons que tous les pays ne participent pas au test PISA : cette démarche est facultative, ce qui entraîne forcément un biais d'auto-sélection, et donc notre clusterisation ne permet pas de décrire l'état du monde mais seulement de caractériser un pool de pays bien particulier qui a choisi de participer aux tests PISA. Notons que les pays qui ne participent pas à l'enquête PISA (une centaine, tout de même) sont pour la plupart des pays en voie de développement, où les taux de scolarisation sont faibles. Si ceux-ci devaient être intégrés au test PISA, il serait probablement utile de considérer à minima un troisième cluster dont le score PISA moyen serait probablement plus faible que celle des deux clusters actuels.
Enfin, au vu de ce qui précède, on est tenté de penser que le score PISA serait...
La partie suivante s'intéresse à l'existence et à la pertinence de ces corrélations.
Au préalable, on décide de :
PISA_pivote_post2006 = PISA_pivote_post2006[PISA_pivote_post2006["Country"] != "China"].dropna().copy()
L'objectif de cette partie est d'étudier la corrélation entre le score moyen obtenu au score PISA et les indicateurs suivants :
Commençons par croiser le score PISA moyen obtenu par pays et par année avec ces différentes variables, une à une.
sns.set()
fig, axs = plt.subplots(1, 4, figsize=(16,4))
sns.regplot(x="gov_exp", y="Mean_score", data=PISA_pivote_post2006, ax=axs[0], ci=None)
sns.regplot(x="pupil_teacher_ratio", y="Mean_score", data=PISA_pivote_post2006, ax=axs[1], ci=None)
sns.regplot(x="life_expect", y="Mean_score", data=PISA_pivote_post2006, ax=axs[2], ci=None)
sns.regplot(x="GDP_per_capita", y="Mean_score", data=PISA_pivote_post2006, ax=axs[3], ci=None)
print("Score moyen obtenu au test PISA en fonction de différents indicateurs. \nChaque point représente le score moyen d'un pays pour une année donnée.")
plt.show()
Score moyen obtenu au test PISA en fonction de différents indicateurs. Chaque point représente le score moyen d'un pays pour une année donnée.
À vue d'oeil, on constate que :
PISA_pivote_post2006['log_GDP_per_capita'] = np.log(PISA_pivote_post2006['GDP_per_capita'])
fig = px.scatter(PISA_pivote_post2006, x="log_GDP_per_capita", y="Mean_score",
hover_data=["Maths", "Reading", "Science", "is_OECD"], hover_name="Country", trendline="ols",
title='Score PISA moyen en fonction du log(PIB par tête) ($ 2010), par pays et par année.')
fig.show()
results = px.get_trendline_results(fig)
results.px_fit_results[0].summary()
Dep. Variable: | y | R-squared: | 0.467 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.465 |
Method: | Least Squares | F-statistic: | 255.4 |
Date: | Tue, 04 Feb 2020 | Prob (F-statistic): | 9.81e-42 |
Time: | 15:19:30 | Log-Likelihood: | -1471.9 |
No. Observations: | 294 | AIC: | 2948. |
Df Residuals: | 292 | BIC: | 2955. |
Df Model: | 1 | ||
Covariance Type: | nonrobust |
coef | std err | t | P>|t| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
const | 114.3908 | 22.147 | 5.165 | 0.000 | 70.804 | 157.978 |
x1 | 35.6109 | 2.229 | 15.980 | 0.000 | 31.225 | 39.997 |
Omnibus: | 45.049 | Durbin-Watson: | 1.482 |
---|---|---|---|
Prob(Omnibus): | 0.000 | Jarque-Bera (JB): | 172.371 |
Skew: | -0.571 | Prob(JB): | 3.72e-38 |
Kurtosis: | 6.573 | Cond. No. | 105. |
Interprétation :
Le passage en logarithme permet donc bien d'obtenir un effet explicatif satisfaisant pour la variable PIB par tête.
On souhaite désormais estimer le modèle suivant : $Mean\_score = const + x_1 * log\_GDP\_per\_capita + x_2 * gov\_exp + x_3 * life\_expect + x_4 * pupil\_teacher\_ratio + x_5 * is\_OECD$
X = PISA_pivote_post2006[['log_GDP_per_capita', 'gov_exp', 'life_expect', 'pupil_teacher_ratio', 'is_OECD']]
X = sm.add_constant(X)
y = PISA_pivote_post2006['Mean_score']
model = sm.OLS(y, X)
results = model.fit()
results.summary()
/Users/pierre/anaconda3/lib/python3.7/site-packages/numpy/core/fromnumeric.py:2389: FutureWarning: Method .ptp is deprecated and will be removed in a future version. Use numpy.ptp instead.
Dep. Variable: | Mean_score | R-squared: | 0.527 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.519 |
Method: | Least Squares | F-statistic: | 64.22 |
Date: | Tue, 04 Feb 2020 | Prob (F-statistic): | 7.27e-45 |
Time: | 15:19:30 | Log-Likelihood: | -1454.1 |
No. Observations: | 294 | AIC: | 2920. |
Df Residuals: | 288 | BIC: | 2942. |
Df Model: | 5 | ||
Covariance Type: | nonrobust |
coef | std err | t | P>|t| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
const | 113.6410 | 50.927 | 2.231 | 0.026 | 13.404 | 213.878 |
log_GDP_per_capita | 19.9852 | 4.024 | 4.967 | 0.000 | 12.065 | 27.905 |
gov_exp | 0.2412 | 1.699 | 0.142 | 0.887 | -3.103 | 3.585 |
life_expect | 1.8157 | 0.892 | 2.035 | 0.043 | 0.059 | 3.572 |
pupil_teacher_ratio | -0.2575 | 0.515 | -0.500 | 0.618 | -1.272 | 0.757 |
is_OECD | 28.9419 | 5.384 | 5.375 | 0.000 | 18.345 | 39.539 |
Omnibus: | 16.176 | Durbin-Watson: | 2.097 |
---|---|---|---|
Prob(Omnibus): | 0.000 | Jarque-Bera (JB): | 42.988 |
Skew: | 0.083 | Prob(JB): | 4.63e-10 |
Kurtosis: | 4.866 | Cond. No. | 2.04e+03 |
Étude des p-valeurs :
Étude des coefficients significatifs : toutes choses égales par ailleurs...
Commentaire général sur la pertinence de cette régression : malgré son potentiel prédictif intéressant, celle-ci ne permet absolument pas d'identifier un effet causal en raison de différents biais.
En 2018, seuls 78 pays ont participé au test PISA. Ils sont donc une centaine à ne pas y avoir pris part. À partir de notre modèle ci-dessus, et malgré ses imperfections notables, on va essayer de prédire quel aurait été le score des pays qui n'ont pas pris part au test, pour chaque édition. Ré-utilisons les données scrappées dans la partie 1 afin de construire une table contenant les variables explicatives pour tous les pays du monde, y compris ceux qui n'ont pas participé.
world_indicators = table_GDP_capita
world_indicators['log_GDP_per_capita'] = np.log(world_indicators['GDP_per_capita'])
world_indicators = world_indicators.merge(table_life_expect, on=["iso3code", "Year"], how="left")
world_indicators = world_indicators.merge(table_gov_exp, on=["iso3code", "Year"], how="left")
world_indicators = world_indicators.merge(table_pupil_teacher_ratio, on=["iso3code", "Year"], how="left")
world_indicators = world_indicators.merge(table_iso3codes, on=["iso3code"], how="left")
world_indicators["is_OECD"] = world_indicators.apply(lambda row : (row["Country"] in OECD_countries.values)*1, axis = 1)
# On ne conserve que les lignes des années 2006, 2009, ..., 2018, pour lesquelles il n'y a pas d'indicateur manquant
world_indicators = world_indicators[world_indicators["Year"].isin(range(2006,2020,3))].dropna()
world_indicators.head()
iso3code | Year | GDP_per_capita | log_GDP_per_capita | life_expect | gov_exp | pupil_teacher_ratio | Country | is_OECD | |
---|---|---|---|---|---|---|---|---|---|
265 | GTM | 2018 | 3159.967477 | 8.058317 | 73.810000 | 2.79538 | 20.26228 | Guatemala | 0 |
266 | GNB | 2018 | 622.079825 | 6.433068 | 57.673000 | 2.13249 | 51.92515 | Guinea-Bissau | 0 |
267 | GNQ | 2018 | 10254.993251 | 9.235520 | 58.061000 | 2.18798 | 23.22706 | Equatorial Guinea | 0 |
268 | GRC | 2018 | 23558.083966 | 10.067224 | 81.387805 | 3.96396 | 9.38027 | Greece | 1 |
269 | GRD | 2018 | 9096.329959 | 9.115626 | 72.388000 | 3.17043 | 16.18269 | Grenada | 0 |
PISA_predict = world_indicators.copy()
# Si un test PISA a eu lieu cette année-là, on ajoute le score moyen réellement obtenu
PISA_sub_table = PISA_pivote[PISA_pivote["Country"]!="China"][["iso3code", "Year", "Mean_score"]]
PISA_predict = PISA_predict.merge(PISA_sub_table, on=["iso3code", "Year"], how="left")
# On crée une colonne qui prédit le score à partir des 5 indicateurs importés et du modèle ci-dessus.
X_predict = PISA_predict[['log_GDP_per_capita', 'gov_exp', 'life_expect', 'pupil_teacher_ratio', 'is_OECD']]
X_predict = sm.add_constant(X_predict)
PISA_predict["Mean_score_predict"] = results.predict(X_predict)
PISA_predict.head()
iso3code | Year | GDP_per_capita | log_GDP_per_capita | life_expect | gov_exp | pupil_teacher_ratio | Country | is_OECD | Mean_score | Mean_score_predict | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | GTM | 2018 | 3159.967477 | 8.058317 | 73.810000 | 2.79538 | 20.26228 | Guatemala | 0 | NaN | 404.159038 |
1 | GNB | 2018 | 622.079825 | 6.433068 | 57.673000 | 2.13249 | 51.92515 | Guinea-Bissau | 0 | NaN | 334.064981 |
2 | GNQ | 2018 | 10254.993251 | 9.235520 | 58.061000 | 2.18798 | 23.22706 | Equatorial Guinea | 0 | NaN | 398.180610 |
3 | GRC | 2018 | 23558.083966 | 10.067224 | 81.387805 | 3.96396 | 9.38027 | Greece | 1 | 453.3 | 490.092293 |
4 | GRD | 2018 | 9096.329959 | 9.115626 | 72.388000 | 3.17043 | 16.18269 | Grenada | 0 | NaN | 423.848677 |
fig = px.choropleth(PISA_predict, locations="iso3code",
color="Mean_score_predict", hover_name="Country", hover_data=["Mean_score"],
color_continuous_scale="Spectral", animation_frame="Year",
range_color = [350,550],
title='Résultats prédits par pays et par année à l\'aide du modèle ci-dessus')
fig.show()
# On calcule l'erreur de prédiction qui est égale à la valeur prédite moins la vraie valeur
PISA_predict["Prediction_error"] = PISA_predict["Mean_score_predict"] - PISA_predict["Mean_score"]
PISA_predict["Prediction_error"].describe()
count 296.000000 mean 0.110483 std 33.991564 min -124.232284 25% -18.653395 50% 3.071772 75% 20.968264 max 149.671062 Name: Prediction_error, dtype: float64
On constate que les erreurs de prédiction sont quasi-nulles en moyenne (par construction) mais très dispersées. Pour certains pays et certaines années, l'erreur est de l'ordre de 150 points, ce qui est énorme. Le graphique ci-dessous affiche pour chaque pays, pour chaque année, le score réellement obtenu en fonction du score prédit. Le gradient de couleur quantifie l'erreur de prédiction.
fig = px.scatter(PISA_predict, x="Mean_score", y="Mean_score_predict", hover_name="Country",
hover_data=["Year", "GDP_per_capita", "life_expect", "gov_exp", "pupil_teacher_ratio"],
color="Prediction_error", animation_frame="Year")
bissectrice = np.linspace(300,600)
fig.add_trace(go.Scatter(x=bissectrice, y=bissectrice, mode='lines', name='lines'))
fig.show()
Certains cas s'expliquent assez bien avec l'intuition. Par exemple, en 2018, le Qatar a un score bien plus faible que celui anticipé (quasiment 70 pts d'erreur). Notre prédiction est totalement faussée par un PIB / tête très élevé (63k €) qui ne rend pas compte des inégalités socio-économiques dans ce pays, par exemple.
Notre modèle est donc très imprécis et les résultats obtenus sont donc à considérer avec vigilance. On pouvait s'y attendre vu le R2 obtenu ci-dessus (51%). Une solution simple consisterait à augmenter le nombre de variables utilisé, tout en se laissant guider par l'intuition pour leur choix, en observant notamment les pays pour lesquels l'écart est trop faible et en essayant de trouver des indicateurs qui pourraient expliquer ces écarts (ex : mesure des inégalités, etc.). La difficulté toutefois, à laquelle nous avons dû faire face, consiste à trouver des indicateurs faciles d'accès et qui soient disponibles pour tous les pays.
À l'aide de données scrappées sur divers sites (Wikipedia, Banque Mondiale, CIA...), nous avons pu analyser les résultats de l'enquête PISA depuis sa création en 2000, en observant l'évolution de leur répartition au fil du temps, et en effectuant différents focus sur le cas de la France et de l'OCDE. Pour revenir sur le cas de la France, on a vu qu'il était possible de "faire parler" les chiffres de diverses manières : dans l'absolu, les performances baissent, mais en relatif cette tendance est conforme à celle de l'OCDE. Néanmoins, la question se pose de savoir s'il est toujours pertinent de se comparer aux pays de l'OCDE, un groupe dont la cohérence est discutable, qui comprend des pays comme le Chili ou le Mexique, dont la situation économique est très différente de celle de la France, et qui exclut en même temps des pays riches, dont la situation économique est plus proche de celle de la France, comme Singapour ou la Corée du Sud.
Dans la troisième partie, on a constaté que certains pays affichaient des performances très différentes en mathématiques / lecture / sciences et qu'il était donc utile de ne pas se limiter à la simple comparaison de scores moyens. On a ensuite pu observer qu'il était possible et assez naturel de diviser les pays participants en deux clusters à la performance homogène. On a remarqué que les pays composant le cluster le plus performant avaient des points communs : ils sont en moyenne beaucoup plus riches, ont une espérance de vie supérieure à la moyenne, ils investissent plus dans l'éducation, ils ont un ratio élèves / professeur à l'école primaire inférieur à la moyenne, et ils sont majoritairement membres de l'OCDE. Partant de ce résultat, on a souhaité approfondir la corrélation avec ces indicateurs en proposant un modèle fondé sur une régression multivariée. Enfin, à l'aide de ce modèle, on a proposé une prédiction du score PISA de tous les pays du monde, y compris tous ceux qui n'y ont jamais participé, par année. On a néanmoins été contraints d'observer que notre modèle était très imprécis, ce qui nous a poussé à émettre différentes critiques à son sujet, et à faire plusieurs suggestions permettant de l'améliorer.