[{"cw_etype": "BlogEntry", "eid": 25653872, "title": "AFPYRo du 16 mars 2023", "content": "Les AFPYRo reprennent et le prochain aura lieu dans nos locaux parisiens entre Denfert Rochereau et la Place d'Italie !\r\n\r\nUn AFPYRo est un \u00e9v\u00e9nement organis\u00e9 par l\u2019[AFPy](https://www.afpy.org/) \u2212 Association Francophone Python\u00a0\u2212 pour regrouper des personnes souhaitant discuter du langage de programmation Python dans un cadre convivial.  Apr\u00e8s une ou deux pr\u00e9sentations (vous pouvez [proposer la v\u00f4tre](https://discuss.afpy.org/t/pour-les-parisiens-afpyro-chez-logilab-le-16-mars/1384)), nous \u00e9changerons autour de quelques pizzas.\r\n\r\nLe prochain AFPYRo sera donc \u00e0 Logilab, au 104 Boulevard Auguste Blanqui 75013 Paris, le 16 mars 2023 de 19h \u00e0 21h et nous offrirons les pizzas.\r\n\r\nN\u2019h\u00e9sitez pas \u00e0 [vous inscrire](https://discuss.afpy.org/t/pour-les-parisiens-afpyro-chez-logilab-le-16-mars/1384) et \u00e0 passer nous voir\u00a0!", "content_format": "text/markdown", "heading": "Les AFPYRo reprennent et le prochain aura lieu dans nos locaux parisiens entre Denfert Rochereau et la Place d'Italie !", "word_count": 101, "creation_date": "2023/02/27 17:09:05", "modification_date": "2023/02/27 17:09:52", "cwuri": "https://www.logilab.fr/25653872"}, {"cw_etype": "BlogEntry", "eid": 19793722, "title": "Pandas, Plotly et Jupyter : De l\u2019analyse de donn\u00e9es \u00e0 l\u2019application en ligne (2/3)", "content": "Temps de lecture estim\u00e9 10 minutes.\r\n\r\nDans un [article pr\u00e9c\u00e9dent](https://www.logilab.fr/blogentry/13252264) nous vous proposions une analyse de donn\u00e9es \u00e0 l\u2019aide de la biblioth\u00e8que Pandas. Nous y avions construit une s\u00e9rie de graphiques simples pour r\u00e9aliser cette analyse. Dans cet \u00e9pisode, nous allons aborder les *widgets* qui vont nous permettre de rendre ces graphiques dynamiques.\r\n\r\nIl est conseill\u00e9 d\u2019avoir lu l\u2019article pr\u00e9c\u00e9dent qui d\u00e9taille la structure des donn\u00e9es utilis\u00e9es.\r\n\r\n## Qu\u2019est-ce qu\u2019un *widget* ?\r\n\r\nDans un calepin jupyter, le code peut facilement \u00eatre \u00e9dit\u00e9 et rejou\u00e9. Il est donc assez simple d\u2019effectuer des changements. Il est toutefois possible que les utilisateurs finaux de l\u2019application ne sachent pas programmer ou simplement qu\u2019on pr\u00e9f\u00e8re avoir un moyen simple d'interagir (sans avoir \u00e0 relire le code Python et \u00e0 le modifier). Dans de tels cas, les *widgets* constituent une bonne solution.\r\n\r\nLes *widgets* sont des objets qui sont rendus dynamiquement dans les calepins Jupyter, et avec lesquels il est possible d\u2019interagir.\r\n\r\nLa biblioth\u00e8que de base pour construire ces *widgets* est [`ipywidgets`](https://ipywidgets.readthedocs.io/en/latest/).\r\n\r\nDans l\u2019exemple ci-dessous, la biblioth\u00e8que est import\u00e9e puis un curseur glissant est construit.\r\n\r\n```ipython\r\n>>> import ipywidgets as ipw\r\n>>> ipw.IntSlider(min=0, max=20, step=2)\r\n```\r\n\r\n\u00c0 l\u2019ex\u00e9cution de la cellule Jupyter, le *widget* est affich\u00e9.\r\n\r\n![](https://www.logilab.fr/file/19793781/raw/e852cfbe7238489c4ae60251c.png)\r\n\r\nLa connexion entre le *widget* affich\u00e9 dans la page Web et l\u2019objet python a \u00e9t\u00e9 automatiquement d\u00e9finie. Cela signifie que si l\u2019objet python est modifi\u00e9, le rendu du *widget* est modifi\u00e9 et vice-versa. Dans le cas pr\u00e9sent, l\u2019attribut ``value`` du *widget* vaut ``6``.\r\n\r\n\u00c0 titre d\u2019exemple, on peut construire un curseur glissant comme ceci :\r\n\r\n```ipython\r\n>>> slider = ipw.IntSlider(min=0, max=20, step=2)\r\n>>> slider\r\n```\r\n\r\npuis modifier dynamiquement la valeur de cet objet. Le rendu sera alors mis \u00e0 jour.\r\n\r\n```ipython\r\n>>> from time import sleep\r\n>>> for i in range(0, 22, 2):\r\n...    sleep(1)\r\n...    slider.value = i\r\n```\r\n\r\n![](https://www.logilab.fr/file/19793850/raw/e852cfbe7238489c4ae602520.20.15.gif)\r\n\r\nLes *widgets* deviennent tr\u00e8s int\u00e9ressants d\u00e8s lors que l\u2019on associe des fonctions python \u00e0 des \u00e9v\u00e8nements. Dans l\u2019exemple ci-dessous, nous avons d\u00e9fini deux *widgets* de type \u201ccurseur glissant\u201d et un *widget* d\u2019affichage. Nous voulons afficher dans ce dernier *widget* la somme des deux curseurs.\r\n\r\nOn construit un *widget* de type Bouton, et on associe le clic sur ce bouton \u00e0 l\u2019appel de la fonction `compute_add` qui somme les valeurs des deux curseurs et met \u00e0 jour l\u2019affichage.\r\n\r\n```ipython\r\n>>> from IPython.display import clear_output\r\n>>> sld1 = ipw.IntSlider(min=0, max=20)\r\n>>> sld2 = ipw.IntSlider(min=0, max=20)\r\n>>>\r\n>>> out = ipw.Output()\r\n>>> with out:\r\n...    print(\"0 + 0 = 0\")\r\n...\r\n>>> def compute_add(evt):\r\n...    with out:\r\n...       clear_output()\r\n...       res = sld1.value + sld2.value\r\n...       print(f\"{sld1.value} + {sld2.value} = {res}\")\r\n...\r\n>>> btn = ipw.Button(description=\"Sum\")\r\n>>> btn.on_click(compute_add)\r\n>>> ipw.HBox([ipw.VBox([sld1, sld2, btn]), out])\r\n```\r\n\r\nLe rendu est alors le suivant : \r\n\r\n![](https://www.logilab.fr/file/19793893/raw/e852cfbe7238489c4ae602520.20.15.gif)\r\n\r\n\r\n## Utiliser un *widget* pour s\u00e9lectionner les donn\u00e9es \u00e0 afficher\r\n\r\nDans l\u2019\u00e9pisode pr\u00e9c\u00e9dent, nous avions \u00e9crit une fonction pour charger toutes les donn\u00e9es des licenci\u00e9s inscrits dans les f\u00e9d\u00e9rations sportives pour les ann\u00e9es 2012 \u00e0 2019. La fonction est la suivante : \r\n\r\n```ipython\r\n>>> from pathlib import Path\r\n>>> import pandas as pd\r\n>>> DATA_DIR = Path().resolve() / \"data\"\r\n>>> def load_data():\r\n...    year_dfs = []\r\n...    for year in range(2012, 2019):\r\n...       fname = f\"sport_license_holders_{year}.csv\"\r\n...       yr_df = pd.read_csv(\r\n...          DATA_DIR / fname,\r\n...          dtype={\"dep_code\": str},\r\n...          index_col=[\"dep_code\", \"dep_name\", \"fed_code\", \"fed_name\", \"gender\", \"age\"],\r\n...       )\r\n...       yr_df.rename(columns={\"lic_holders\": str(year)}, inplace=True)\r\n...       year_dfs.append(yr_df)\r\n...    data = pd.concat(year_dfs, axis=1)\r\n...    return data\r\n...\r\n>>> d = load_data()\r\n```\r\n\r\n![](https://www.logilab.fr/file/19793929/raw/e852cfbe7238489c4ae60251c.png)\r\n\r\nLe `DataFrame` r\u00e9sultant contient plus de 1.6 millions de lignes et 7 colonnes. Nous pouvons maintenant \u00e9crire une fonction tr\u00e8s simple qui affiche l\u2019\u00e9volution du nombre de licenci\u00e9s de 2012 \u00e0 2019 pour les f\u00e9d\u00e9rations qui sont donn\u00e9es en param\u00e8tre.\r\n\r\n\r\n```ipython\r\n>>> pd.options.plotting.backend = \"plotly\"  # Choose Plotly as the plotting back-end\r\n>>> def plot_license_holders_evolution_by_sport(data, fed_codes):\r\n...    data_sports = data.groupby(level=[\"fed_code\", \"fed_name\"]).sum()\r\n...    sel_data_sports = data_sports.loc[list(fed_codes)]\r\n...    sel_data_sports = sel_data_sports.droplevel(0)\r\n...    sel_data_sports.index.name = \"Federations\"\r\n...    fig = sel_data_sports.transpose().plot(title=\"Sport license holders\")\r\n...    fig.update_layout(xaxis_title=\"year\", yaxis_title=\"number of license holders\")\r\n...    return fig\r\n...\r\n>>> plot_license_holders_evolution_by_sport(d, [109, 115, 242, 117])\r\n```\r\n\r\n![](https://www.logilab.fr/file/19793957/raw/e852cfbe7238489c4ae60251c.png)\r\n\r\n\r\nNous souhaitons utiliser un *widget* proposant de s\u00e9lectionner une ou plusieurs disciplines, puis afficher le graphique correspondant lorsque la s\u00e9lection est valid\u00e9e.\r\n\r\nLa premi\u00e8re chose que nous r\u00e9alisons est un dictionnaire contenant en clef le nom des f\u00e9d\u00e9rations sportives et en valeur leur num\u00e9ro associ\u00e9. Ce dictionnaire pourra \u00eatre fourni \u00e0 un *widget* de type `SelectMultiple`.\r\n\r\nNous utilisons le code suivant pour obtenir le dictionnaire de correspondance :\r\n\r\n```ipython\r\n>>> def extract_federation_names_codes(data):\r\n...    codes = data.index.get_level_values(\r\n...        \"fed_code\"\r\n...    )  # Extract all the values from the level ``fed_codes`` of the index\r\n...    names = data.index.get_level_values(\r\n...        \"fed_name\"\r\n...    )  # Extract all the values from the level ``fed_names`` of the index\r\n...    dic = {name: code for code, name in zip(codes, names)}\r\n...    return dic\r\n...\r\n```\r\n\r\nEt finalement, la fonction suivante permet de construire l\u2019interface souhait\u00e9e :\r\n\r\n```ipython\r\n>>> from IPython.display import display\r\n>>> def build_gui(data):\r\n...    fed_values = extract_federation_names_codes(data)\r\n...    fed_wdg = ipw.SelectMultiple(\r\n...        options=fed_values, description=\"Sport federations\", rows=20\r\n...    )\r\n...    plt_btn = ipw.Button(description=\"Plot\")\r\n...    out_wdg = ipw.Output()\r\n...    # Define the hook function that will be called each time the button is clicked\r\n...    def refresh_plot(evt):\r\n...        fed_codes = fed_wdg.value\r\n...        with out_wdg:\r\n...            clear_output()\r\n...            display(plot_license_holders_evolution_by_sport(data, fed_codes))\r\n...\r\n...    plt_btn.on_click(refresh_plot)\r\n...    gui_wdg = ipw.HBox([ipw.VBox([fed_wdg, plt_btn]), out_wdg])\r\n...    return gui_wdg\r\n...\r\n>>> build_gui(d)\r\n```\r\n\r\n![](https://www.logilab.fr/file/19793992/raw/e852cfbe7238489c4ae602520.20.15.gif)\r\n\r\nNous venons ainsi de faire une fonction qui construit une interface utilisateur, compos\u00e9e d\u2019un *widget* permettant de faire une s\u00e9lection multiple. Lorsque la s\u00e9lection est valid\u00e9e, la fonction d\u2019affichage du graphique est rappel\u00e9e, mettant ainsi le composant \u00e0 jour. Le d\u00e9veloppement de cette interface utilisateur est bien plus simple que ce que nous aurions eu \u00e0 faire avec d'autres solutions comme Qt, Tkinter ou m\u00eame Flask + Javascript.\r\n\r\nOn voit que cela permet \u00e0 tous les utilisateurs de faire leur propre analyse sans avoir \u00e0 changer une seule ligne de code.\r\n\r\nDans le prochain \u00e9pisode, nous pr\u00e9senterons *Voila* qui permet de transformer un calepin Jupyter en une petite application Web, utilisable sans aucune connaissance de Python. Nous utiliserons \u00e9galement *jupyter-flex* pour obtenir une jolie application Web dot\u00e9e de bulles d\u2019aides, d\u2019onglets et d\u2019un menu lat\u00e9ral.", "content_format": "text/markdown", "heading": "Dans un article pr\u00e9d\u00e9cent nous vous proposions une analyse de donn\u00e9es \u00e0 l\u2019aide de la biblioth\u00e8que Pandas. Nous y avions construit une s\u00e9rie de graphiques simples pour r\u00e9aliser cette analyse. Dans cet \u00e9pisode, nous allons aborder les widgets qui vont nous permettre de rendre ces graphiques dynamiques", "word_count": 860, "creation_date": "2022/06/27 12:33:43", "modification_date": "2022/06/27 13:11:13", "cwuri": "https://www.logilab.fr/19793722"}, {"cw_etype": "BlogEntry", "eid": 16367140, "title": "Resourcecode", "content": "400 mots - Temps de lecture 2 min\r\n\r\n![](https://www.logilab.fr/file/16368263/raw/748bab1cd694a1a4fa0703996_200px.png)\r\n\r\nLe 10 mars 2022 a eu lieu le lancement de la \u00ab boite-\u00e0-outils Resourcecode \u00bb devant plus d\u2019une centaine de partenaires du projet. Logilab est fi\u00e8re d\u2019avoir pu participer \u00e0 ce projet.\r\n\r\n[Resourcecode](http://resourcecode.info/) est un projet visant \u00e0 soutenir les investissements et la croissance dans le secteur de l\u2019\u00e9nergie houlomotrice et mar\u00e9omotrice par la cr\u00e9ation d\u2019une bo\u00eete \u00e0 outils int\u00e9gr\u00e9e de donn\u00e9es marines.\r\n\r\nConcr\u00e8tement, des donn\u00e9es d\u00e9crivant l\u2019\u00e9tat de la mer (vitesse du vent, hauteur des vagues, direction du courant, etc) sont enregistr\u00e9es par des bou\u00e9es de l\u2019[IFREMER](https://ifremer.fr/) (*Institut Fran\u00e7ais de Recherche pour l'Exploitation de la Mer*) et de ses partenaires. Des donn\u00e9es de 1994 \u00e0 2020 sont disponibles pour des milliers de points de l\u2019oc\u00e9an Atlantique et de la mer du Nord avec une r\u00e9solution temporelle de l\u2019ordre de l\u2019heure. Une fois ces donn\u00e9es enregistr\u00e9es, elles peuvent \u00eatre interpol\u00e9es sur les points d\u2019un maillage triangulaire.\r\n\r\n\r\n\r\nLogilab a remport\u00e9 un appel d\u2019offre, d\u00e9pos\u00e9 par l\u2019Ifremer, dans le cadre de ce projet. Nous avons eu la charge de r\u00e9aliser :\r\n\r\n- une application web [resourcecode.ifremer.fr](https://resourcecode.ifremer.fr) permettant la visualisation des points o\u00f9 les donn\u00e9es sont accessibles et proposant des outils statiques ou interactifs bas\u00e9s sur des calepins [Jupyter](jupyter.org/) afin d\u2019\u00e9tudier la mer au point consid\u00e9r\u00e9.\r\n- produire une biblioth\u00e8que python [resourcecode](https://pypi.org/project/resourcecode) permettant de t\u00e9l\u00e9charger localement les donn\u00e9es d\u2019un point sous forme de `DataFrame` Pandas. L'int\u00e9gration continue de la forge GitLab de l'IFREMER g\u00e9n\u00e8re avec Sphinx la [documentation](https://resourcecode.gitlab-pages.ifremer.fr/resourcecode/) de cette biblioth\u00e8que.\r\n- int\u00e9grer \u00e0 cette biblioth\u00e8que des codes de calculs \u00e9crits par l\u2019IFREMER et ses partenaires (codes \u00e9crits en R, MATLAB ou Python)\r\n- mettre en place une architecture permettant \u00e0 l\u2019IFREMER et ses partenaires de construire des nouveaux outils (statiques ou interactifs). Ces outils sont d\u00e9velopp\u00e9s et maintenus par l\u2019IFREMER et ses partenaires, et *automatiquement* int\u00e9gr\u00e9 \u00e0 l\u2019application web. Ils sont d\u00e9velopp\u00e9s sur l\u2019instance [GitLab de l\u2019Ifremer](https://gitlab.ifremer.fr/resourcecode/tools).\r\n\r\nLors de cet \u00e9v\u00e9nement de lancement de Resourcecode, une d\u00e9monstration en direct a pu \u00eatre effectu\u00e9e aupr\u00e8s du public\u00a0: la biblioth\u00e8que a \u00e9t\u00e9 install\u00e9e et un d\u00e9p\u00f4t de code contenant un calepin Jupyter a \u00e9t\u00e9 clon\u00e9 puis ex\u00e9cut\u00e9. Cela a permis de d\u00e9montrer la facilit\u00e9 d'utilisation de cet outil, ainsi que la r\u00e9p\u00e9tabilit\u00e9 offerte par ce type d\u2019architecture, qui correspond aux attentes actuelles en mati\u00e8re de science ouverte (*Open Science*).\r\n\r\n![](https://www.logilab.fr/file/16367129/raw/748bab1cd694a1a4fa0703995.png)", "content_format": "text/markdown", "heading": "Le 10 mars 2022, l'IFREMER a pr\u00e9sent\u00e9 les r\u00e9sultats du projet http://resourcecode.info/ devant plus d\u2019une centaine de parties prenantes.", "word_count": 378, "creation_date": "2022/04/12 14:57:13", "modification_date": "2022/04/12 15:29:50", "cwuri": "https://www.logilab.fr/16367140"}, {"cw_etype": "BlogEntry", "eid": 13252264, "title": "Pandas, Plotly et Jupyter : De l'analyse de donn\u00e9es \u00e0 l'application en ligne (1/3)", "content": "_Temps de lecture estim\u00e9 10 minutes._\r\n\r\nNous proposons une s\u00e9rie de quelques articles o\u00f9 nous allons utiliser la\r\nbiblioth\u00e8que Pandas pour analyser les licences sportives en France. En chemin,\r\nnous r\u00e9aliserons une interface utilisateur avec des widgets.\r\n\r\nCette s\u00e9rie sera d\u00e9coup\u00e9e en trois articles. Dans le premier, nous allons\r\nexplorer le jeu de donn\u00e9es \u00e0 notre disposition en utilisant la biblioth\u00e8que\r\nPandas. Dans le second, nous introduirons *Jupyter* et les *ipywidgets* qui\r\nnous permettront de faire une interface utilisateur. Nous terminerons la s\u00e9rie\r\nen pr\u00e9sentant *Voil\u00e0* ainsi que le th\u00e8me *jupyter-flex*.\r\n\r\n# *Pandas, jupyter, ipywidgets, voil\u00e0* ? De quoi parle-t-on ?\r\n\r\n- [Pandas](https://pandas.pydata.org/) est une biblioth\u00e8que Python tr\u00e8s connue,\r\n  qui permet d\u2019analyser et  d\u2019\u00e9tudier des jeux de donn\u00e9es. Elle est con\u00e7ue pour\r\n  traiter des jeux de donn\u00e9es tabulaires (ceux pouvant \u00eatre lus par un tableur).\r\n  Les donn\u00e9es peuvent \u00eatre de diff\u00e9rents types (nombres, dates, cha\u00eenes de\r\n  caract\u00e8res, etc). Pandas est, comme nous le verrons, tr\u00e8s efficace. Les\r\n  fonctions co\u00fbteuses de Pandas sont g\u00e9n\u00e9ralement \u00e9crites en C, et Python est\r\n  utilis\u00e9 pour manipuler et appeler ces fonctions.\r\n- [Jupyter](https://jupyter.org/) est une plateforme, utilisable dans un\r\n  navigateur web qui permet   d\u2019ex\u00e9cuter des calepins (*notebooks*). Un calepin\r\n  est un fichier   qui combine des cellules de diff\u00e9rents types : du code\r\n  ex\u00e9cutable, du texte,   des visualisations, etc.\r\n- Les [Ipywidgets](https://ipywidgets.readthedocs.io/en/latest/) sont des\r\n  \u00e9l\u00e9ments graphiques interactifs que l\u2019on peut ajouter \u00e0 des calepins\r\n  *Jupyter*. Ils vont nous permettre de proposer aux utilisateurs de choisir un\r\n  fichier, choisir un \u00e9l\u00e9ment dans une liste, cliquer sur un bouton, etc.\r\n  Chacune des actions de l\u2019utilisateur peut \u00eatre associ\u00e9e \u00e0 une fonction Python,\r\n  et donc rendre le calepin interactif.\r\n- [Voil\u00e0](https://github.com/voila-dashboards/voila) est une application qui\r\n  permet d\u2019ex\u00e9cuter des calepins, mais sans afficher le code source \u2212 qui est\r\n  visible par d\u00e9faut dans *Jupyter*. L\u2019\u00e9norme int\u00e9r\u00eat \u00e0 cela est qu\u2019un calepin\r\n  *Jupyter* devient alors une application web \u00e0 part enti\u00e8re, utilisable dans le\r\n  navigateur, et seuls les \u00e9l\u00e9ments indispensables \u00e0 son utilisation sont\r\n  visibles.\r\n\r\nApr\u00e8s cette petite phase de pr\u00e9sentation, d\u00e9couvrons les donn\u00e9es que nous allons\r\nmanipuler aujourd\u2019hui.\r\n\r\n# Pr\u00e9sentation des donn\u00e9es\r\n\r\nDans cette s\u00e9rie d\u2019articles nous utilisons des donn\u00e9es issues de\r\n[https://data.gouv.fr](https://data.gouv.fr). Il s\u2019agit du nombre de licences\r\nsportives, par sexe, par cat\u00e9gorie d\u2019\u00e2ges, par municipalit\u00e9 pour les ann\u00e9es 2012\r\n\u00e0 2018. Les donn\u00e9es brutes peuvent \u00eatre t\u00e9l\u00e9charg\u00e9es\r\n[ici](https://www.data.gouv.fr/fr/datasets/donnees-geocodees-issues-du-recensement-des-licences-et-clubs-aupres-des-federations-sportives-agreees-par-le-ministere-charge-des-sports/).\r\n\r\nNous avons r\u00e9alis\u00e9 une op\u00e9ration de nettoyage sur ces donn\u00e9es, afin de nous\r\nassurer d\u2019avoir une structure coh\u00e9rente pour chaque ann\u00e9e. Nous avons \u00e9galement\r\nremplac\u00e9 les municipalit\u00e9s par leur d\u00e9partement, ce qui permet d\u2019all\u00e9ger les\r\ndonn\u00e9es \u00e0 manipuler. Au final, nous obtenons six fichiers csv, un par ann\u00e9e,\r\ndont la structure est la suivante :\r\n\r\n```csv\r\ndep_code,dep_name,fed_code,fed_name,gender,age,lic_holders\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,00-04,0\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,05-09,75\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,10-14,251\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,15-19,130\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,20-29,39\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,30-44,105\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,45-59,105\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,60-74,23\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,F,75+,0\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,M,00-04,0\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,M,05-09,106\r\n01,Ain,101,F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme,M,10-14,278\r\n[\u2026]\r\n```\r\n\r\n| Nom de colonne | Description                                                                                                              |\r\n| --------:       | --------                                                                                                                 |\r\n| `dep_code`     | Code (unique) du d\u00e9partement                                                                                             |\r\n| `dep_name`     | Nom du d\u00e9partement                                                                                                       |\r\n| `fed_code`     | Code (unique) de la f\u00e9d\u00e9ration sportive                                                                                  |\r\n| `fed_name`     | Nom de la f\u00e9d\u00e9ration sportive                                                                                            |\r\n| `gender`       | Genre (peut \u00eatre `M` ou `F`)                                                                                             |\r\n| `age`          | La tranche d\u2019\u00e2ge consid\u00e9r\u00e9e (peut \u00eatre `00-04`, `05-09`, `10-14`, `15-19`, `20-29`, `30-44`, `44-59`, `60-74`, `75+`) |\r\n| `lic_holders`  | Le nombre de licenci\u00e9s dans le d\u00e9partement, enregistr\u00e9s dans cette f\u00e9d\u00e9ration, de ce genre et de cette tranche d\u2019\u00e2ge.  |\r\n\r\n\r\n# Chargement de donn\u00e9es pour une ann\u00e9e\r\n\r\nPandas offre un nombre important de fonctions permettant de charger des donn\u00e9es\r\ndepuis diff\u00e9rents formats: CSV, Excel, tableaux HTML, JSON, bases SQL, HDF5, etc.\r\nNous allons utiliser la fonction `read_csv`. Cette fonction utilise les \u00e9l\u00e9ments\r\nde la premi\u00e8re ligne comme noms de colonnes. Pandas essaie \u00e9galement de d\u00e9tecter\r\nles types de colonnes \u00e0 utiliser (nombre, date, cha\u00eene de caract\u00e8res) en se\r\nbasant sur les premiers \u00e9l\u00e9ments lus. Nous sp\u00e9cifions donc \u00e0 Pandas que la\r\ncolonne `dep_code` est de type `str`, pour prendre en compte les d\u00e9partements\r\nCorse (`2A` et `2B`), sans quoi Pandas \u00e9mettra un avertissement.\r\n\r\n```python\r\nfrom pathlib import Path\r\nimport pandas as pd\r\n\r\nDATA_DIR = Path().resolve() / \"data\"  # en supposant que les donn\u00e9es sont dans le dossier data\r\n\r\nd2012 = pd.read_csv(\r\n    DATA_DIR / \"sport_license_holders_2012.csv\", dtype={\"dep_code\": str}\r\n)\r\n```\r\n\r\nNous obtenons alors la *DataFrame* suivante :\r\n\r\n![](https://www.logilab.fr/file/13252396/raw/748bab1cd694a1a4fa0703927.png)\r\n\r\n# Premi\u00e8res analyses\r\n\r\n\u00c0 partir de l\u00e0, nous pouvons commencer \u00e0 \u00e9tudier le jeu de donn\u00e9es. Par exemple,\r\nen demandant le nom de chaque f\u00e9d\u00e9ration :\r\n\r\n```python\r\n>>> d2012[\"fed_name\"].unique()\r\narray([\"F\u00e9d\u00e9ration Fran\u00e7aise d'athl\u00e9tisme\",\r\n       \"F\u00e9d\u00e9ration Fran\u00e7aise des soci\u00e9t\u00e9s d'aviron\",\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de badminton',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de basketball',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de boxe',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de cano\u00eb-kayak',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de cyclisme',\r\n       \"F\u00e9d\u00e9ration Fran\u00e7aise d'\u00e9quitation\",\r\n       \"F\u00e9d\u00e9ration Fran\u00e7aise d'escrime\",\r\n       [\u2026],\r\n       'F\u00e9d\u00e9ration fran\u00e7aise de pentathlon moderne',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de javelot tir sur cible',\r\n       'F\u00e9d\u00e9ration Flying Disc France', 'F\u00e9d\u00e9ration Fran\u00e7aise Maccabi',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de la course camarguaise',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de la course landaise',\r\n       'F\u00e9d\u00e9ration Fran\u00e7aise de ballon au poing'], dtype=object)\r\n```\r\n\r\nNous pouvons facilement compter le nombre total, toutes cat\u00e9gories\r\nconfondues, de licenci\u00e9s :\r\n\r\n```python\r\n>>> d2012[\"lic_holders\"].sum()\r\n12356101\r\n```\r\n\r\nUne des forces de Pandas r\u00e9side dans la possibilit\u00e9 de cr\u00e9er des filtres, ou des\r\ngroupes simplement. Par exemple, pour compter le nombre de licenci\u00e9s hommes, nous\r\npouvons cr\u00e9er un masque (`True` si le genre est `M` et `False` sinon), puis\r\nappliquer ce masque \u00e0 notre *DataFrame*.\r\n\r\n\r\n```python\r\n>>> mask_male = d2012[\"gender\"] == \"M\"\r\n>>> d2012[mask_male][\"lic_holders\"].sum()\r\n7806235\r\n```\r\n\r\nAinsi, en 2012, il y avait 7 806 235 licenci\u00e9s masculins de sport en France.\r\n\r\nCombien y a-t-il de licenci\u00e9s, en 2012, par tranche d\u2019\u00e2ge ? Pour r\u00e9pondre \u00e0\r\ncette question, nous utilisons la m\u00e9thode `groupby` de Pandas, en donnant le nom\r\nde la colonne sur laquelle nous souhaitons faire le groupe :\r\n\r\n```python\r\n>>> d2012.groupby(\"age\")[\"lic_holders\"].sum()\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252442/raw/748bab1cd694a1a4fa0703921.png)\r\n\r\n\r\nCette m\u00e9thode permet de constituer des groupes, selon une cl\u00e9 (g\u00e9n\u00e9ralement le\r\nnom d\u2019une ou plusieurs colonnes), puis d\u2019appliquer sur chaque groupe partageant\r\nla m\u00eame cl\u00e9 une fonction d\u2019agr\u00e9gation. Dans notre exemple, la cl\u00e9 de chaque\r\ngroupe est l\u2019\u00e2ge, et la fonction d\u2019agr\u00e9gation la somme sur la colonne\r\n`lic_holders`.\r\n\r\nNous pouvons effectuer le m\u00eame type de calcul, mais en groupant cette fois-ci\r\nsur le genre et l\u2019\u00e2ge, ce qui donne le r\u00e9sultat suivant :\r\n\r\n```python\r\n>>> d2012.groupby([\"gender\", \"age\"])[\"lic_holders\"].sum()\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252463/raw/748bab1cd694a1a4fa0703920.png)\r\n\r\n\r\nLes deux r\u00e9sultats que nous venons d\u2019obtenir sont ce qu\u2019on appelle des `Series`.\r\nC\u2019est-\u00e0-dire, des *DataFrames* mais constitu\u00e9es d\u2019une seule colonne.\r\nOn observe que les groupes sont directement constitu\u00e9s par l\u2019index.  Dans le cas\r\nd\u2019un `groupby()` avec une seule colonne, nous avons un *index simple* et dans le\r\ncas o\u00f9 plusieurs colonnes sont utilis\u00e9es, nous obtenons ce qu\u2019on appelle un\r\n*index multiple* ou un *index hi\u00e9rarchique*. Nous allons \u00e9tudier cela un peu\r\nplus en profondeur dans la suite.\r\n\r\n\r\n# Cr\u00e9er un index sur mesure\r\n\r\nDans la *DataFrame* que nous avons charg\u00e9e, de tr\u00e8s nombreuses donn\u00e9es sont\r\nr\u00e9p\u00e9t\u00e9es et ne sont utilis\u00e9es que pour d\u00e9finir des groupes (`dep_code`,\r\n`dep_name`, `gender`, `age` etc). Nous allons mettre toutes ces donn\u00e9es dans\r\nl\u2019index de la *DataFrame*. Cela permet d\u2019avoir dans l\u2019index les donn\u00e9es de\r\nchaque groupe, et dans la *DataFrame* les donn\u00e9es desdits groupes (ici le nombre\r\nde licenci\u00e9s sportifs).\r\n\r\nPour ce faire, nous utilisons la m\u00e9thode `set_index` :\r\n\r\n```python\r\n>>> d2012.set_index(\r\n   [\"dep_code\", \"dep_name\", \"fed_code\", \"fed_name\", \"gender\", \"age\"], inplace=True\r\n)\r\n>>> d2012\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252487/raw/748bab1cd694a1a4fa070391f.png)\r\n\r\n\r\nNous avons ainsi une *DataFrame* \u00e0 une seule colonne, et avec un index \u00e0 six\r\nniveaux. Nous pouvons toujours grouper par genre et par \u00e2ge, en utilisant le\r\nmot-cl\u00e9 `level`, indiquant qu\u2019il faut grouper en utilisant l\u2019index :\r\n\r\n```python\r\n>>> d2012.groupby(level=[\"gender\", \"age\"]).sum()\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252506/raw/748bab1cd694a1a4fa070391e.png)\r\n\r\n\r\n# Dans quels d\u00e9partements la course camarguaise est-elle pratiqu\u00e9e ?\r\n\r\nLa course camarguaise est un sport traditionnel dans lequel les participants\r\ntentent d'attraper des attributs prim\u00e9s fix\u00e9s au frontal et aux cornes d'un\r\nb\u0153uf. Pour savoir dans quels d\u00e9partements ce sport est le plus pratiqu\u00e9, nous\r\nallons :\r\n\r\n1. Filtrer sur l\u2019index pour n\u2019avoir que les enregistrements correspondant \u00e0 ce\r\n   sport (le code de la f\u00e9d\u00e9ration est 215) ;\r\n2. Grouper par code et nom de d\u00e9partement, et compter le nombre de licenci\u00e9s ;\r\n3. Afficher les groupes tri\u00e9s par ordre d\u00e9croissant de licenci\u00e9s.\r\n\r\n```python\r\n>>> d2012_camarg = d2012.xs(\r\n    215, level=\"fed_code\"\r\n)  # Only keep the rows with index equal to 215 at level ``fed_code``\r\n>>> d2012_camarg_depts = d2012_camarg.groupby(\r\n    [\"dep_code\", \"dep_name\"]\r\n).sum()  # Group the data by department (only keep departments with non-null values)\r\n>>> d2012_camarg_depts.sort_values(\r\n    by=\"lic_holders\", ascending=False\r\n)  # Sort the data in decreasing order\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252526/raw/748bab1cd694a1a4fa070391d.png)\r\n\r\n\r\nSans trop de surprise, on observe que c\u2019est le Gard (o\u00f9 est la Camargue), les\r\nBouches-du-Rh\u00f4ne, l\u2019H\u00e9rault et le Vaucluse (d\u00e9partements qui entourent le Gard)\r\nqui ont le plus de licenci\u00e9s dans ce sport.\r\n\r\n# Quels sont les sports les plus pratiqu\u00e9s par les femmes ?\r\n\r\nNous allons :\r\n\r\n1. S\u00e9lectionner les enregistrements correspondant \u00e0 `gender = 'F'` ;\r\n2. Grouper par f\u00e9d\u00e9ration et compter le nombre de licenci\u00e9es ;\r\n3. Afficher les dix sports avec le plus de licenci\u00e9es.\r\n\r\n```python\r\n>>> d2012_females_top_10 = d2012.xs(\"F\", level=\"gender\")\r\n    .groupby([\"fed_code\", \"fed_name\"])\r\n    .sum()\r\n    .nlargest(10, \"lic_holders\")\r\n>>> d2012_females_top_10\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252554/raw/748bab1cd694a1a4fa070391c.png)\r\n\r\nPandas permet \u00e9galement de faire des graphiques. Par d\u00e9faut c\u2019est la\r\nbiblioth\u00e8que [matplotlib](https://matplotlib.org/) qui est utilis\u00e9e. Nous\r\npouvons par exemple utiliser un diagramme en b\u00e2tons pour afficher le top 10 des\r\nsports pratiqu\u00e9s par les femmes :\r\n\r\n```python\r\n>>> d2012_females_top_10.plot(\r\n    kind=\"bar\",\r\n    legend=False,\r\n    xlabel=\"Sport federation\",\r\n    ylabel=\"Number of license holders\",\r\n    color=\"#CC0066\",\r\n    title=\"Female sport license holders in 2012 (top 10)\",\r\n)\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252574/raw/748bab1cd694a1a4fa070391a.png)\r\n\r\n# Charger les donn\u00e9es pour toutes les ann\u00e9es\r\n\r\nDans la section pr\u00e9c\u00e9dente, nous avons charg\u00e9 uniquement les donn\u00e9es de l\u2019ann\u00e9e\r\n2012. Mais nous avons bien plus de donn\u00e9es que cela. Nous allons donc charger\r\nchaque fichier, puis renommer la colonne `lic_holders` en fonction de l\u2019ann\u00e9e en\r\ncours. Nous aurons ainsi une *DataFrame*, avec en colonne le nombre de licenci\u00e9s\r\npar ann\u00e9e, et en index les diff\u00e9rents groupes.\r\n\r\nNous allons faire une liste `years_dfs` qui va contenir toutes les *DataFrames*,\r\nune par ann\u00e9e, puis nous allons simplement les concat\u00e9ner. Cela donne donc :\r\n\r\n```python\r\n>>> years_dfs = []\r\n>>> for year in range(2012, 2019):\r\n...    fname = f\"sport_license_holders_{year}.csv\"\r\n...    yr_df = pd.read_csv(\r\n...        DATA_DIR / fname,\r\n...        dtype={\"dep_code\": str},\r\n...        index_col=[\"dep_code\", \"dep_name\", \"fed_code\", \"fed_name\", \"gender\", \"age\"],\r\n...    )\r\n...    yr_df.rename(columns={\"lic_holders\": str(year)}, inplace=True)\r\n...    year_dfs.append(yr_df)\r\n>>>\r\n```\r\n\r\nOn concat\u00e8ne toutes les *DataFrames*, en fonction de l\u2019index (`axis=1`) :\r\n\r\n```python\r\n>>> data = pd.concat(years_df, axis=1)\r\n>>> data\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252596/raw/748bab1cd694a1a4fa070391b.png)\r\n\r\nNous avons ainsi une *DataFrame* avec plus de 1.6 million de lignes, et 7\r\ncolonnes.\r\n\r\nOn peut maintenant afficher, par exemple, les 10 sports les plus pratiqu\u00e9s en\r\nfonction des ann\u00e9es :\r\n\r\n```python\r\n>>> data_sport = data.groupby(level=[\"fed_code\", \"fed_name\"]).sum()\r\n>>> data_sport.nlargest(10, \"2012\")\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252618/raw/748bab1cd694a1a4fa0703928.png)\r\n\r\nNous avons ainsi le nombre de licenci\u00e9s, par f\u00e9d\u00e9ration et par ann\u00e9e pour les\r\n10 plus grosses f\u00e9d\u00e9rations de 2012. Le tri est effectu\u00e9 par rapport aux donn\u00e9es\r\nde 2012.\r\n\r\nOn notera qu\u2019en 2018 il y a 0 licenci\u00e9 de Karat\u00e9. Cela est probablement une\r\nerreur dans les donn\u00e9es, ce qui peut arriver.\r\n\r\n# Tracer l'\u00e9volution du nombre de licenci\u00e9s avec Plotly\r\n\r\nNous pouvons maintenant suivre l\u2019\u00e9volution du nombre de licenci\u00e9s dans certaines\r\ndisciplines. Nous s\u00e9lectionnons les sports dont le code de f\u00e9d\u00e9ration est 109,\r\n115, 242, 117.\r\n\r\n```python\r\n>>> sel_data_sports = data_sports.loc[\r\n...    [109, 115, 242, 117]\r\n... ] # Select the rows whose value at the first level of the index (``fed_code``)\r\n... # is one of the list items\r\n>>> # Drop the first level of the index (``fed_code``)\r\n>>> sel_data_sports = sel_data_sports.droplevel(0)\r\n>>> # Will be used as the title of the legend\r\n>>> sel_data_sports.index.name = \"Federations\"\r\n>>> sel_data_sports.transpose().plot(\r\n...    title=\"Sport license holders\", xlabel=\"year\", ylabel=\"number of license holders\"\r\n...)  # Transpose to have the years as the index (will be the X axis)\r\n```\r\n\r\n![](https://www.logilab.fr/file/13252640/raw/748bab1cd694a1a4fa0703929.png)\r\n\r\nComme nous le disions, par d\u00e9faut Pandas utilise la biblioth\u00e8que *matplotlib*\r\npour g\u00e9n\u00e9rer les graphiques. La figure produite ici est statique, elle peut\r\nfacilement \u00eatre ins\u00e9r\u00e9e dans un rapport par exemple, mais cela pr\u00e9sente des\r\ndifficult\u00e9s lors de la phase d\u2019exploration.\r\n\r\nDepuis quelque temps maintenant, Pandas est compatible avec plusieurs\r\nbiblioth\u00e8ques de visualisation. Il y a notamment\r\n[Plotly](https://plotly.com/python/), qui permet de faire des graphiques\r\ninteractifs utilisables dans le navigateur web.\r\n\r\nPour utiliser *Plotly*, il est n\u00e9cessaire de changer la biblioth\u00e8que utilis\u00e9e\r\npar d\u00e9faut.\r\n\r\n```python\r\n# Choose Plotly as the plotting back-end\r\n# this has to be done only once, usually at the begining of the code\r\n>>> pd.options.plotting.backend = \"plotly\"\r\n```\r\n\r\nUne fois *Plotly* configur\u00e9e, nous pouvons retracer le graphique, comme\r\npr\u00e9c\u00e9demment :\r\n\r\n```python\r\n>>> fig = sel_data_sports.transpose().plot(title=\"Sport license holders\")\r\n>>> fig.update_layout(xaxis_title=\"year\", yaxis_title=\"number of license holders\")\r\n>>> fig\r\n```\r\n\r\nDans un environnement *Jupyter*, la figure produite est celle-ci, et il est\r\npossible de s\u00e9lectionner/d\u00e9s\u00e9lectionner les courbes \u00e0 afficher :\r\n\r\n![](https://www.logilab.fr/file/13252662/raw/748bab1cd694a1a4fa070392a.png)\r\n\r\n# Quelle est la prochaine \u00e9tape ?\r\n\r\nNous avons dans ce premier article, charg\u00e9 avec Pandas des donn\u00e9es textuelles au\r\nformat CSV.\r\nNous avons vu comment et pourquoi utiliser un index multiple. Cela nous a permis\r\nde calculer quelques statistiques simples sur des groupes d\u2019individus. Nous\r\navons \u00e9galement produit des visualisations avec *matplotlib* et avec *Plotly*.\r\n\r\nDans le prochain article, nous utiliserons des widgets *Jupyter* pour manipuler\r\ndynamiquement les donn\u00e9es \u00e0 afficher sur les graphiques.", "content_format": "text/markdown", "heading": "Nous proposons une s\u00e9rie de quelques articles o\u00f9 nous allons analyser les licences sportives en France \u00e0 l\u2019aide de Pandas, et nous r\u00e9aliserons une interface utilisateur avec des widgets.", "word_count": 1957, "creation_date": "2022/02/04 16:51:12", "modification_date": "2022/02/04 17:07:00", "cwuri": "https://www.logilab.fr/13252264"}]