1. Introduction

Tous les quatre ans depuis 1896, environ 10'000 athlètes se retrouvent quelque part dans le monde pour participer au sommet du sport mondial dans plus de 55 disciplines et tout cela sous la bannière de leur pays. Lors de chaque édition chaque pays cherche donc évidemment à ramener le plus de médailles et c'est alors que les analystes entre en jeux afin de prédire combien de médailles chaque pays va gagner. Étant un fervent auditeur des jeux Olympiques (JO) je me suis donc demandé comment ces analystes faisaient pour estimer ces chiffres et c'est ainsi que j'ai découvert que l'on pouvait utiliser de l'intelligence artificielle pour cela.

Mon projet à donc pour but de créer un modèle capable de prédire le nombre de médailles qu'un pays va gagner au JO d'été puis de créer un site web qui permettra à des utilisateurs de créer leur propre pays et de voir combien de médailles ce dernier gagnerait aux prochaines JO (Paris 2024).

Pour arriver à cela il faudra donc commencer par créer une base de données qui contiendra les différentes statistiques qui m'intéressent. Il faudra ensuite créer une modèle (ce sera un réseau neuronal, expliqué plus tard) puis l'entraîner avec notre base de données. Finalement, il s'agira de créer un site web afin qu'un utilisateur et le réseau neuronal puissent interagir. Les plus grandes difficultés dans ce projet seront la création du modèle et celle de la base donnée car je n'ai quasiment aucune connaissance sur le sujet.

2. Matériel et méthodes

2.1 Matériel

  • Un ordinateur avec une connexion Internet
  • L'environment VSCode pour coder

2.2 Méthode

2.2.1 La base de donnée:

La première étape pour construire un modèle de prédiction est une base de donnée qui contient des statistiques qui vont nous permettrent d'estimer le nombre de médailles qu'un pays va gagner.

2.2.1.1 Médailles:

La première étapes est de récupérer combien de médailles un pays à gagné à une édition. Heureusement pour moi, Wikipedia à déjà une page pour toutes les éditions qui comporte à chaque fois une liste de tous les pays ayant gagné une médaille. On va donc construire un parser Wikipédia(programme qui permet de récupérer des données d'une page web) afin de récupérer les données puis on va ensuite les modifier à notre guise. Pour cette partie j'ai trouvé un tutoriel sur Internet que j'ai modifié pour mes besoins.

On va tout d'abord commencer par importer les librairies nécessaires:

 import pandas as pd # librarie pour l'analyse de données
 import requests
 from bs4 import BeautifulSoup # librairie pour parser des documents HTML

On va maintenant donner URL à chercher au programme. Après avoir regarder quelques pages des éditions de JO, je me suis rendu compte que tous suivaient la même structure j'ai donc utilisé une variable "year" qu'il suffit de changer pour que l'URL se mette à jour automatiquement (j'utilise ici un f string)

 year = 1960
 wikiurl = f"https://en.wikipedia.org/wiki/{year}_Summer_Olympics_medal_table"
 table_class="wikitable sortable jquery-tablesorter"
 response=requests.get(wikiurl) 
 # permet de savoir si le site web peut être légalement "scraper" (si la réponse est 200 c'est bon)
 print(response.status_code)

Une fois la page uploadé, on va chercher le tableau de médaille grâce à la balise <table> dans le tableau html

 soup = BeautifulSoup(response.text, 'html.parser')
 indiatable=soup.find('table',{'class':"wikitable"}) # permet de chercher un tableau dans la page wikipedia

Maintenant la page récupérée on va pouvoir la convertir en data frame afin de la modifier avec panda

 data=pd.read_html(str(indiatable)) # permet de convertir la liste html en data frame
 data=pd.DataFrame(data0) # on rajoute l'index et on déclare la data frame (ici data)
 print(data.head(5))

Dans les pages Wikipedia pour les éditions plus anciennes, la colonne avec le nom des pays s'appelle Nation, tandis que sur les nouvelles c'est NOC. On change donc le nom sur toutes à NOC pour plus de clarté. On va également enlever toutes les colonnes qui ne nous intéressent pas, soit le classement général du nombres de médailles gagnées et le détails du nombre de chaque type de médailles.

 data = data.rename(columns={"Nation": "NOC"}) # on renomme la colonne
 data = data.drop("Gold", "Silver", "Bronze", "Rank", axis=1) # on enlève les colonnes
 print(data.head(5))

Actuellement l'index de notre dataframe (la table des matières) est simplement une liste allant commençant par 0 et en incrément de 1 pour chaque ligne. Comme on veut par la suite être capable de chercher un pays par son nom plutôt que son index arbitraire dans la data frame on va changer l'index par la colonne NOC. On va également rajouter une colonne "Participated" qui nous servira par la suite (sa valeur actuelle sera 0 pour toutes les lignes).

 data = data.set_index('NOC') #on change l'index de la data frame 
 data'Participated' = 0 # on rajoute une colonne pour le moment seulement avec des zéros
 print(data.head(5))

On va maintenant modifier notre index car pour le moment les noms de pays ressembles à cela: Italy\xa0(ITA)*, on veut se débarrasser du code du pays (ITA) mais également de l'étoile qui indique le pays organisateur. Ainsi, on pourra plus facilement comparé les pays du tableau avec les pays qui nous intéressent. Afin de faire cela, on crée une "array" avec les valeurs de notre index puis on va découper les noms des pays à l'endroit où on détecte le \xa0. Il faut ensuite chercher le pays avec le "*" puis l'enlever. Avec ça notre index est maintenant prêt est on peut remplacer l'ancien dans la data frame.

 country_table = list(data.index.values)
 print(country_table)
 country_cleaned = country.split("\xa0")[0 for country in country_table]
 print(country_cleaned)
 country_cleaned = s.replace('*', '') for s in country... 
 # on enlève les "*" qui viennent après le pays qui organise les jeux olympiques 
 print(country_cleaned)
 data = data.set_index(country_cleaned)

Maintenant que la dataframe issu du tableau Wikipedia est correctement formaté il va falloir s'attaquer aux deux cas de figure problématiques:

  1. Les tableaux Wikipedia ne contiennent que les pays qui ont gagnée une médaille, ainsi certains pays qui nous intéressent ne sont pas dans ce tableaux car ils n'ont gagné aucune médaille à cette édition des JO.
  2. Les tableaux Wikipedia contiennent également des pays qui ne nous intéressent pas et qu'il faut donc enlever de la data frame.

Afin de résoudre ces problèmes j'ai donc pris l'approche suivante. J'ai commencé par créé une array avec la liste de pays qui m'intéressait. Pour cela j'ai utilisé un petit programme trouvé sur Internet (3) qui permet de faire d'une liste de mots une array en séparant là où il y avait une espace. Le seul défaut de ce programme est qu'il découpe aussi les pays à noms composés (ex: "New", "Zealand" ) mais comme ce sont des exceptions le problème est vite réglé à la main.

 a_file = open("country.txt", "r")
 list_of_lists = 
 for line in a_file:
   stripped_line = line.strip()
   line_list = stripped_line.split()
   list_of_lists.append(line_list)
 a_file.close()
 print(list_of_lists)

La liste étant prête on va pouvoir faire la chose suivante: On va prendre chaque élément de la liste des pays du tableau Wikipedia (country_cleaned) puis on va regarder si il se trouve dans la liste des pays qui nous intéressent (olympic_interested). Si c'est le cas on changera la valeur de ce pays dans la colonne "Participated" de 0 à 1, sinon on le laisse à 0.

 olympic_interested = ['Algeria', 'Argentina', 'Australia', 'Austria', 'Bahamas', \\
 'Belgium', 'Bermuda', 'Brazil', 'Canada', 'Chile', 'Chinese Taipei', 'Colombia', \\
 'Denmark',  'Egypt', 'Finland',  'France',  'Ghana', 'Great Britain',  'Greece', \\
 'Guyana',  'Hong Kong',  'Iceland',  'India',  'Ireland',  'Israel',  'Italy',  'Jamaica', \\
 'Japan',  'Kenya', 'South Korea', 'Lebanon', 'Liechtenstein',  'Luxembourg'  , \\
 'Malaysia'  ,  'Mexico'  ,  'Monaco'  ,  'Morocco'  ,  'Myanmar'  ,  'Netherlands'  , \\
 'New Zealand'  ,  'Nigeria'  ,  'Norway'  ,  'Pakistan'  ,  'Panama'  ,  'Peru'  , \\
 'Philippines'  ,  'Portugal'  ,  'Puerto Rico'  ,  'Romania'  ,  'Singapore'  ,  'Spain'  , \\
 'Sri Lanka'  ,  'Sweden'  ,  'Switzerland'  ,  'Thailand'  ,  'Trinidad and Tobago'  , \\
 'Tunisia'  ,  'Turkey'  ,  'Uganda'  ,  'United States'  ,  'Uruguay'  , 'Venezuela']
 
 for country in country_cleaned: # on fait toute la liste des pays qui participent aux JO
   print(country)
   if any(country in s for s in olympic_interested): # si on détecte une string qui correspond
       print("found")
       data.loccountry, 'Participated' = 1 # on change la valeur à 1
   else:
       print("not found")
       data.loccountry, 'Participated' = 0
 print(data)

Maintenant que l'on sait si les pays dans le tableau Wikipedia nous intéresse (1 = intéressant, 0 = on peut l'enlever) on va pouvoir enlever les pays qui ont 0 dans la colonne "Participated". Une fois le tri fais on peut enlever la colonne et convertir le tableau (qui ne contient maintenant que une colonne "Total" et l'index qui est le nom du pays) vers excel pour la suite.

 data.drop(data(data['Participated' == 0 )].index, inplace=True) # on enlève les lignes qui contiennent des 0
 print(data)
 data = data.drop("Participated", axis=1) # on enlève la colonne "Participated"
 print(data)
 data.to_excel(f"Olympic_{year}.xlsx") Participated # on convertit la data frame en tableau excel
2.2.1.2 Facteurs:

Après avoir fait quelques recherches sur Internet j'ai découvert cet article qui détaille comment des chercheurs de l'université de Dartmouth avait réussit à prédire le nombre de médailles par pays aux JO de Londres avec 98% d'efficacité. Ces derniers utilisaient 4 facteurs différents: La population (2), le PIB par habitant (3), combien de médailles le pays avait gagné l'année d'avant et finalement si le pays organisait les JO. À ces quatre facteurs j'ai également rajouté un index qui mesure la liberté d'expression (4)car, comme expliqué dans l'article, les pays communistes ont une tendance à investir plus fortement dans le sport comme c'est une fierté national que des démocraties.
Toutes ces données sont relevées par la banque mondiale pour tous les pays depuis 1960, ce qui élimine donc directement toutes les éditions de 1896 à 1956. J'ai du ensuite, grâce à un tableau de participation aux JO d'été sur Wikipedia déterminé quel pays avaient participé à toutes les éditions entre 1960 et 2020 (la saisie des données est ainsi plus facile). Autres certains cas spécifiques (pays qui gagnent leur indépendance, etc...) Trois grandes exceptions en sont ressorti: Les JO de 1976 et 1980 qui ont été boycottées et l'édition de 1960 car trop peux de pays y participaient. Après ce tri manuel je me suis donc retrouvé avec 13 éditions auxquelles 52 pays ont participé, ce qui fait donc 676 points de données différents.

2.2.2 Le réseau neuronal:

Maintenant que notre base de donnée est terminée, on va pourvoir l'implémenter dans notre réseaux neuronal.

Comme pour la base de donnée, la première étape va être d'importer les librairies nécessaires.

 import keras #pour le réseau neuronal
 import numpy as np # pour modifier des données 
 import pandas as pd 
 import sklearn # pour le réseau neuronal
 from sklearn import preprocessing
 from sklearn.preprocessing import StandardScaler

On va maintenant créer une classe pour notre réseau neuronal (cela nous servira par la suite pour le site web) puis on va modifier le type de données dans la colonne "Freedom of expression". En effet, les chiffres dedans oscillent entre 0 et 1 car c'est la mesure de la Banque Mondiale, ainsi le type de donnée est float32.

 class Prediction_Model():
   def init(self):
       df = pd.read_csv("cleaned_data.csv")
       Freedom_of_expression = df"Freedom of expression"
       Freedom_of_expression = np.asarray(Freedom_of_expression).astype('float32') 
       #on change le type de donnée dans cette colonne (int --> float32)

On va maintenant définir notre axe x et notre axe y (les colonnes)

 x = df.drop(columns="Medals") # on garde toutes les colonnes sauf celle des médailles
 print(x)
 y = df"Medals" # on garde seulement la colonne des médailles

On va maintenant utiliser une méthode qui se nomme le "scaling". Les réseaux neuronaux on souvent du mal à gérer les prédictions lorsque les facteurs qu'ils utilisent on des tailles très différentes, ces qui est notre cas (Freedom of Expression: entre 0 et 1, Population: plusieurs dizaines de millions à quelques centaines de milliers). Cette méthode consiste donc à prendre tous les facteurs que l'on utilise et de les mettrent sur la même échelle afin de mieux répartir leurs importances.

 scaler = StandardScaler().fit(x)
 x = scaler.transform(x) # on "scale" les facteurs x

Maintenant que nos données sont prêtes on peut s'attaquer au réseau neuronal. Un réseau neuronal marche de la façon suivante:

Figure 1 - Un réseau neuronal typique

Chaque neurone reçois des valeurs des neurones derrière lui dont il fait la somme puis multiplie ce chiffre par un poids qu'il s'assigne lui même arbitrairement (simplement un autre chiffre). Le neurone individuel vérifie ensuite si le chiffre qu'il à obtenu après cette opération dépasse un certain palier et si c'est le cas il propage l'information à tous les neurones devant lui. Le réseau neuronal lui, se compose en 3 partie: la couche "input" (5 neurones) qui reçois les données initiales, les couches cachées (5 neurones) (dans notre cas il n'y en a qu'une) qui permettent de complexifier le modèle, et finalement la couché "output" (1 neurones) qui donne la prédiction finale. À chaque prédiction, le système donne des poids arbitraires à chaque neurone puis, dépendant de la précision de la prédiction, ajuste plus ou moins les poids associés aux neurones (c'est l'activation "relu").

Ainsi, pour notre première couche on veut 5 neurones, car on a 5 facteurs, et une fonction d'activation car il faudra changer les poids. Pour la deuxième couche 5 neurones aussi (arbitraire) et la fonction d'activation, puis finalement un neurone, car on n'a que une seule valeur à prédire, pour la dernière couche (pas besoin de fonction d'activation car pas d'information à propager devant).

 self.model = keras.Sequential()
 self.model.add(keras.layers.Dense(5, activation="relu", input_shape=(5,))) #couche input
 self.model.add(keras.layers.Dense(5, activation="relu")) # couche cachée
 self.model.add(keras.layers.Dense(1)) # couche output

Notre réseau neuronal est maintenant prêt à être entraîner. On compile donc le modèle et on le fit sur notre data.

 self.model.compile(loss='mean_squared_error', optimizer='adam', metrics='accuracy') 
 # loss permet de savoir comment notre système s'adapte aux résultats, 
 # optimizer permet de changer les poids comme relu et metrics permet de mesurer la performance du système
 self.model.fit(x, y, epochs=5) # on entraîne notre système 5 fois sur la même base de donnée/dataset

Le réseaux neuronal étant entraîné et prêt on peut maintenant lui donner de nouvelles valeurs pour un pays hypothétique, et il devrait pouvoir nous dire combien de médailles ce pays gagnerait aux JO d'été. Cette partie de code sera surtout utile pour la partie site web.

  def model_result(self, population, gdppercap, foe, medalsyearbefore): 
       prediction_data = np.array(population, gdppercap, foe, 0, meda...) 
       # on donne les valeurs avec lesquelles on veut que le modèle fasse une prédiction
       prediction = self.model.predict(prediction_data.reshape(1, 5), batch_size=1) 
       # le modèle retourne une prédiction
       return prediction

2.2.3 Le site web:

Le site web sera relativement rudimentaire avec simplement deux pages:

  1. La première aura des menus qui permettront de sélectionner les valeurs voulues pour le pays hypothétiques.
  2. La deuxième contiendra un lien vers le site d'OCI contenant les billets des projets.
2.2.3.1 Première page:

On va commencer par déclarer un menu qui contient la page d'accueil et la page de renseignement ("Learn More")

 <body>
   <section class="header"> # section pour associé un style sur la feuille CSS
     <div class="navbar">
       <a href="index.html">Home</a> #les liens derrière les boutons du menu
       <a href="learn_more.html">Learn More</a> #les liens derrière les boutons du menu
     </div>

On va maintenant créer une boite de texte avec notre titre et on referme notre section.

 <div class="text-box">
       <h1>
         This website is <br />able to predict how many medals a country will
         win at the Paris Olympics
       </h1>
       <p>Try it out just down below !</p>
     </div>
    </section>

On crée la prochaine section puis on rajoute notre texte.

 <section class="form">
     <div class="text-box-2">
       <h1>
         Input the values for your country and press "predict" to see how many
         medals they would win in Paris !
       </h1>
     </div>

On va maintenant rajouter nos formulaires pour récupérer nos valeurs (en spécifiant les minimums et maximums afin de s'éviter des erreurs) On rajoute également les cookies pour l'encryption. <div class="text-box-3">

       <form action="" method="Post"> 
         {% csrf_token %}  # on ajoute les cookies pour l'encryption
         <label for="population">Population (ex: 2534334):</label><br /> #on spécifie l'ID du forme pour plus tard
         <input
           type="number"
           id="population"
           name="population"
           min="0"
         /><br />
         <label for="foe"
           >Freedom of Expression (between 0 and 100 with 100 being a perfect
           democracy, ex: 89):</label
         ><br />
         <input type="number" id="foe" name="foe" min="0" max="100" /><br />
         <label for="medalsyearbefore"
           >Number of medals the country has won the year before (ex:
           23):</label
         ><br />
         <input
           type="number"
           id="medalsyearbefore"
           name="medalsyearbefore"
           min="0"
         /><br />
         <label for="gdppercap">GDP per capita (ex: 20158):</label><br />
         <input type="number" id="gdppercap" name="gdppercap" min="0" /><br />
         <input type="submit" value="predict" />
       </form>
     </div>
   </section>

On va finalement rajouter la phrase qui 's'affichera lorsque le bouton "predict est pressée"

   {%if user_fill_form%}
   <section class="result">
     <div class="text-box-4">
       <h1>
         Your country would win prediction medals at the 2024 Paris Summer
         Olympics
       </h1>
     </div>
   </section>
   {%endif%}

Finalement pour raccorder les différentes parties ensemble, on va rajouter ce code dans le document views.py: Si le bouton est pressé alors le

 def homepage(request):
   context = {"user_fill_form": False}
   if request.method == "POST":
       population = int(request.POST"population") # on récupère chaque variable du forme
       foe = int(request.POST"foe")
       medalsyearbefore = int(request.POST"medalsyearbefore")
       gdppercap = int(request.POST"gdppercap")
       print(type(population))
       pm = Prediction_Model()
       result = pm.model_result(
           population, foe, medalsyearbefore, gdppercap) 
       # on prédit le résultat avec la def Model prediction dans neural_netwrok_2.ipynb
       context = {"user_fill_form": True, "prediction": result}
       return render(request, "index.html", context)
   return render(request, "index.html", context) # on renvoie la prédiction vers la page

3. Résultats

Dans l'ensemble je suis très satisfait avec le résultat dans ce projet, même si le modèle n'est que juste à environ 30%, il marche et le site web aussi. On peut donc dire que dans l'ensemble le projet a atteint ses objectifs. Je reste pourtant critique de mon projet et selon moi, il y'a une marge d'amélioration dans chaque partie de ce projet.

3.1 La base de donnée:

Pour la base donnée, j'ai réussi à automatiser la plus grande partie du processus en récupérant les médailles automatiquement. J'aurais pu toutefois utilisées une array avec les f-strings pour ne pas avoir à changer les années manuellement. Du côté des facteurs je n'ai pas utilisé pandas alors que cela aurait pu me sauver plusieurs heures, c'est donc là que je verrais quelque chose à changer pour la prochaine fois. Dans l'ensemble j'ai quand même sauvé un nombre important d'heure car je n'avais pas besoin de saisir toutes les données à la main. Je considère donc que cette partie est un succès mais qu'elle pourrait être encore plus poussée.

3.2 Le réseau neuronal:

Pour le réseau neuronal, le résultat est quelque peu décevant étant donné qu'en utilisant les même données je ne soit que arrivé à une précision de 30% tandis qu'il serait possible de monter jusqu'à 98%. Je ne suis pourtant pas complètement dépité du résultat étant donné que le sujet de l'intelligence artificielle est extrêmement vaste est je ne connais qu'une petite partie de toute les possibilités que ce domaine offre. Je pense donc qu'une précision de 30% pour le premier réseau neuronal de ma vie est raisonnable mais qu'avec plus de temps à disposition je pourrais optimiser ce modèle et le rendre bien plus précis.

3.3 Le site web:

Finalement pour le site web, je suis relativement satisfait avec le résultat. Le site web fonctionne et il utilise de nombreuses choses disponibles avec HTML (lien, boutons, menu, forme, images, etc...). Je dois cependant admettre qu'il n'est pas esthétiquement très poussé et que beaucoup pourrait encore être ajouté. Comme pour la base de données je pense que cette partie est succès mais qu'elle pourrait être , comme toujours, plus jolie et optimisée.

4. Discussion

Je pense que ce projet pourrait être largement optimiser mais qu'il a tout de même atteint ses objectifs de base.

4.1 La base de donnée:

Les plus grandes possibilités d'améliorations seraient la création de la base de données. Tout d'abord je pourrais faire usage de f-strings afin de me faciliter la tâche et ne pas à avoir à entrer manuellement toutes les années qui m'intéressent et deuxièmement je pourrais étendre cette utilisation de Pandas aux tableaux avec les facteurs ce que je n'ai pas fait dans ce projet faute de temps.

4.2 Le réseau neuronal:

Je pourrais ensuite améliorer la précision de mon modèle. Il semble existe un nombre gigantesque de façon de raffiner un modèle avec lesquelles je en suis pour le moment pas familière mais que je pourrais éventuellement implémenter dans un projet futur.

4.3 Le site web:

Finalement le site web pourrait plus complexe, je pourrais rajouter plus de pages et faire plus de graphismes. Je pense que cette partie n'est pas en dehors de mes capacités pour le moment mais je n'est pas plus m'y attarder.

5. Conclusion

Pour conclure, je pense que mon projet a atteint ses objectifs et de plus il m'a permis de découvrir tout un pan de la programmation qui m'intéresse tout particulièrement. Je pense que la plus grosse difficulté aura été d'apprendre à utiliser pandas qui est une librairie pourtant très complète et qui offre d'innombrables possibilités. Si j'avais encore un mois je pourrais optimiser et automatiser la saisie des données dans la database, augmenter la précision de mon modèle et rendre mon site plus esthétique et complexe.

Je pense tout de même que ce projet m'a largement permis d'améliorer mais connaissances en Python, ainsi qu'en HTML, CSS et dans des librairies spécialisés pour la data science comme Pandas.

Pour terminer sur une note plus légère, j'ai beaucoup apprécié ce projet car c'est la première fois que j'ai pu coder des réseaux neuronaux et de l'intelligence artificielle. C'est en effet un thème qui m'intéresse depuis longtemps mais n'ayant jamais eu le niveau jusqu'à maintenant j'étais content de finalement pouvoir tester cette technologie !

Références

  1. https://medium.com/analytics-vidhya...
  2. https://www.weforum.org/agenda/2016/07/this-model-predicts-the-final-olympic-medal-table-using-nothing-but-economics
  3. https://www.adamsmith.haus/python/a...
  4. https://www.kaggle.com/datasets/ali...
  5. https://ourworldindata.org/democrac...
  6. https://www.kaggle.com/datasets/ali...
  7. https://www.kaggle.com/datasets/zgr...
  8. https://www.youtube.com/watch?v=Q93...
  9. https://scikit-learn.org/stable/mod...