From a3eadd4d3a5ca431908773226b0f2af5f559aea9 Mon Sep 17 00:00:00 2001 From: Romain Avouac <43444134+avouacr@users.noreply.github.com> Date: Tue, 1 Nov 2022 18:51:14 +0100 Subject: [PATCH] =?UTF-8?q?Mod=C3=A8le=20de=20notebooks=20de=20correction?= =?UTF-8?q?=20ex=C3=A9cutables=20(#304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * dev scripts * dev tools * dev tools * split questions and answers to make nb executable * dev tools * pimp notebooks only for new changes * fix function name * dev tools * test new correction * tweak parameters for correction only in headers * fix quarto project file * fix mistakes * revert gitignore * fix mistake * fix mistake * fix mistake * fix mistake * Automated changes * Automated changes * fix counter * essaie créer équivalence classes entre box * Automated changes * Automated changes * test heading * tweak again * Automated changes * Automated changes * change css * Automated changes * Automated changes * improve app * css * echo false * Automated changes * Automated changes * style * fontaxesome * Automated changes * Automated changes * corrige css Co-authored-by: github-actions[bot] Co-authored-by: linogaliana --- assets/scss/custom.scss | 38 +- build/pimp_notebook.py | 11 +- build/tweak_headers_quarto.py | 30 ++ build/tweak_options_quarto.py | 2 +- .../manipulation/02b_pandas_TP/index.qmd | 345 +++++++++++------- dev/build-correction-nb.sh | 10 + dev/render-changes.sh | 13 + dev/render-chapter.sh | 14 + dev/set-up.sh | 23 ++ view.sh | 23 -- 10 files changed, 355 insertions(+), 154 deletions(-) create mode 100644 build/tweak_headers_quarto.py create mode 100644 dev/build-correction-nb.sh create mode 100644 dev/render-changes.sh create mode 100644 dev/render-chapter.sh create mode 100644 dev/set-up.sh delete mode 100644 view.sh diff --git a/assets/scss/custom.scss b/assets/scss/custom.scss index 3c3617dc3..d617e171d 100644 --- a/assets/scss/custom.scss +++ b/assets/scss/custom.scss @@ -87,6 +87,35 @@ https://github.com/jupyter/notebook/blob/9de5042e1058dc8aef7632f313e3e86c33390d3 CUSTOM BOXES --------------------*/ +.alert.alert-info, +.alert.alert-success { + /* padding: 1px 0px; */ + background-color: white; +} + +/* Box heading color */ +.alert-info > .alert-heading { + background-color: #e7f2fa; +} +.alert-success > .alert-heading { + background-color: #ecf8e8; +} + +/* Text color */ +.alert-success, +.alert-info { + color: rgba(0,0,0,.8); +} + +/* Space after header */ +.alert-success > .alert-heading { + margin-bottom: 15px !important; +} +.alert-info > .alert-heading { + margin-bottom: 15px !important; +} + +.alert-success, .box-exercise, .box-warning, .box-caution, @@ -96,6 +125,7 @@ https://github.com/jupyter/notebook/blob/9de5042e1058dc8aef7632f313e3e86c33390d3 .box-tip, .box-important, .box-note, +.alert-info, .box-attention{ margin-top: 1em; margin-bottom: 1em; @@ -132,13 +162,13 @@ https://github.com/jupyter/notebook/blob/9de5042e1058dc8aef7632f313e3e86c33390d3 .box-important { border-left:.2rem solid #007bff80; } -.box-note { +.alert-info { border-left:.2rem solid #007bff80; } .box-attention { border-left:.2rem solid #fd7e1480; } -.box-exercise { +.alert-success { border-left:.2rem solid #3fb618; } @@ -150,6 +180,7 @@ https://github.com/jupyter/notebook/blob/9de5042e1058dc8aef7632f313e3e86c33390d3 .box-header-hint, .box-header-tip, .box-header-important, +.alert-heading, .box-header-note, .box-header-attention{ /*background-color:#fdf3f2;*/ @@ -205,7 +236,8 @@ https://github.com/jupyter/notebook/blob/9de5042e1058dc8aef7632f313e3e86c33390d3 background-color:#fdf3f2 } -.box h3{ +.box h3, +.alert h3{ margin-top: 0rem; font-weight:600; font-size: 1.1rem; diff --git a/build/pimp_notebook.py b/build/pimp_notebook.py index 4ae15a6cf..8c8053e16 100644 --- a/build/pimp_notebook.py +++ b/build/pimp_notebook.py @@ -46,6 +46,11 @@ def change_box_markdown(fl): write_file(fl, tweak_md) if __name__ == '__main__': - list_files = glob.glob("./content/course/**/index.qmd", recursive=True) - print(list_files) - [change_box_markdown(fl) for fl in list_files if not fl.endswith("_index.md")] + with open('diff') as f: + lines = f.read().splitlines() + + list_files = [l for l in lines if l.endswith('.qmd')] + + for fl in list_files: + if not fl.endswith("_index.md"): + change_box_markdown(fl) diff --git a/build/tweak_headers_quarto.py b/build/tweak_headers_quarto.py new file mode 100644 index 000000000..c6f90258a --- /dev/null +++ b/build/tweak_headers_quarto.py @@ -0,0 +1,30 @@ +import glob +import re + + +with open('diff') as f: + lines = f.read().splitlines() + +list_files = [l for l in lines if l.endswith('.qmd')] + +def clean_overwrite_file(fl): + with open(fl, 'r') as file_in: + text_in = file_in.readlines() + + text_out = [] + c = 0 + for line in text_in: + # Delimit header + if "---" in line: + c += 1 + if c < 2: + line = re.sub("echo:\s?false", "echo: true", line) + text_out.append(line) + + with open(fl, 'w') as file_out: + file_out.writelines(text_out) + + +if __name__ == '__main__': + for fl in list_files: + clean_overwrite_file(fl) diff --git a/build/tweak_options_quarto.py b/build/tweak_options_quarto.py index 3f9d8790c..9a697bba8 100644 --- a/build/tweak_options_quarto.py +++ b/build/tweak_options_quarto.py @@ -21,4 +21,4 @@ def clean_overwrite_file(fl): write_file(fl, content) if __name__ == '__main__': - [clean_overwrite_file(fl) for fl in list_files] \ No newline at end of file + [clean_overwrite_file(fl) for fl in list_files] diff --git a/content/course/manipulation/02b_pandas_TP/index.qmd b/content/course/manipulation/02b_pandas_TP/index.qmd index 4e7baae88..a6b43cb4d 100644 --- a/content/course/manipulation/02b_pandas_TP/index.qmd +++ b/content/course/manipulation/02b_pandas_TP/index.qmd @@ -19,7 +19,7 @@ summary: | ce chapitre vise à illustrer les fonctionalités du package à partir de données d'émissions de gaz à effet de serre de l'[`Ademe`](https://data.ademe.fr/). -echo: false +echo: true output: false eval: false --- @@ -63,7 +63,10 @@ Pour faciliter l'import de données Insee, il est recommandé d'utiliser le pack de l'Insee disponibles sur le site web [insee.fr](https://www.insee.fr/fr/accueil) ou via des API. -[^1]: Toute contribution sur ce package, disponible sur [Github](https://github.com/InseeFrLab/Py-Insee-Data) est bienvenue ! +```{=html} +1. [^](#cite_ref-1) +``` +Toute contribution sur ce package, disponible sur [Github](https://github.com/InseeFrLab/Py-Insee-Data) est bienvenue ! Après avoir installé la librairie `pynsee` (voir l'[introduction à pandas](course/manipulation/02a_pandas_tutorial)), nous suivrons les conventions habituelles dans l'import des packages : @@ -100,7 +103,11 @@ Le code pour télécharger les données est le suivant : df_city = pynsee.download.download_file("FILOSOFI_COM_2016") ``` -{{% box status="note" title="Note" icon="fa fa-comment" %}} +::: {.cell .markdown} +```{=html} + +``` +::: -{{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} +::: {.cell .markdown} +```{=html} + +``` +::: ```{python} +#| echo: false # Question 1 df.head(10) df.tail(15) @@ -174,106 +191,130 @@ la première base est relativement complète, la seconde comporte beaucoup de va Autrement dit, si on désire exploiter `df_city`, il faut faire attention à la variable choisie. -{{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} +::: {.cell .markdown} +```{=html} + +``` +::: ```{python} +#| echo: false + +# Question 1 print("base df") print(df.dtypes) print("\nbase df_city") print(df_city.dtypes) + # Il faut changer les types de toutes les variables de df_city à l'exception des codes et libellés de commune. df_city[df_city.columns[2:len(df_city.columns)]] = df_city[df_city.columns[2:len(df_city.columns)]].apply(pd.to_numeric) print("\nbase df_city corrigée") print(df_city.dtypes) ``` - -Ensuite, on vérifie les dimensions des `DataFrames` et la structure de certaines variables clés. -En l'occurrence, les variables fondamentales pour lier nos données sont les variables communales. -Ici, on a deux variables géographiques: un code commune et un nom de commune. - -* Vérifier les dimensions des DataFrames - ```{python} +#| echo: false + +# Question 2 print(df.shape) print(df_city.shape) ``` -* Vérifier le nombre de valeurs uniques des variables géographiques dans chaque base. Les résultats apparaissent-ils cohérents ? - ```{python} +#| echo: false + +# Question 3 print(df[['INSEE commune', 'Commune']].nunique()) print(df_city[['CODGEO', 'LIBGEO']].nunique()) # Résultats dont l'ordre de grandeur est proche. Dans les deux cas, #(libelles) < #(code) ``` - -* Identifier dans `df_city` les noms de communes qui correspondent à plusieurs codes communes et sélectionner leurs codes. En d'autres termes, identifier les `CODGEO` tels qu'il existe des doublons de `LIBGEO` et les stocker dans un vecteur `x` (conseil: faire attention à l'index de `x`) - - ```{python} +#| echo: false + +# Question 4 x = df_city.groupby('LIBGEO').count()['CODGEO'] x = x[x>1] x = x.reset_index() x ``` -On se focalise temporairement sur les observations où le libellé comporte plus de deux codes communes différents - -* Regarder dans `df_city` ces observations - ```{python} +#| echo: false +# Question 5 df_city[df_city['LIBGEO'].isin(x['LIBGEO'])] ``` -* Pour mieux y voir, réordonner la base obtenue par order alphabétique ```{python} +#| echo: false +# Question 6 df_city[df_city['LIBGEO'].isin(x['LIBGEO'])].sort_values('LIBGEO') ``` -* Déterminer la taille moyenne (variable nombre de personnes: `NBPERSMENFISC16`) et quelques statistiques descriptives de ces données. -Comparer aux mêmes statistiques sur les données où libellés et codes communes coïncident - ```{python} +#| echo: false +# Question 7 print(df_city[df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe()) print(df_city[~df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe()) ``` -* Vérifier les grandes villes (plus de 100 000 personnes), -la proportion de villes pour lesquelles un même nom est associé à différents codes commune. - ```{python} +#| echo: false +# Question 8 df_big_city = df_city[df_city['NBPERSMENFISC16']>100000].copy() df_big_city['probleme'] = df_big_city['LIBGEO'].isin(x['LIBGEO']) df_big_city['probleme'].mean() df_big_city[df_big_city['probleme']] ``` -* Vérifier dans `df_city` les villes dont le libellé est égal à Montreuil. -Vérifier également celles qui contiennent le terme 'Saint-Denis' - - - ```{python} +#| echo: false +# Question 9 df_city[df_city.LIBGEO == 'Montreuil'] df_city[df_city.LIBGEO.str.contains('Saint-Denis')].head(10) ``` -{{% /box %}} - - Ce petit exercice permet de se rassurer car les libellés dupliqués sont en fait des noms de commune identiques mais qui ne sont pas dans le même département. Il ne s'agit donc pas d'observations dupliquées. @@ -285,16 +326,39 @@ Les indices sont des éléments spéciaux d'un DataFrame puisqu'ils permettent d Il est tout à fait possible d'utiliser plusieurs indices, par exemple si on a des niveaux imbriqués. -{{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} +::: {.cell .markdown} +```{=html} + +``` +::: + ```{python} +#| echo: false +# Question 1 display(df) df = df.set_index('INSEE commune') display(df) @@ -304,32 +368,24 @@ df_city = df_city.set_index('CODGEO') display(df_city) ``` -* Les deux premiers chiffres des codes communes sont le numéro de département. -Créer une variable de département `dep` dans `df` et dans `df_city` - ```{python} +#| echo: false +# Question 2 df['dep'] = df.index.str[:2] df_city['dep'] = df_city.index.str[:2] ``` -* Calculer les émissions totales par secteur pour chaque département. -Mettre en log ces résultats dans un objet `df_log`. -Garder 5 départements et produire un `barplot` - ```{python} +#| echo: false +# Question 3 df_log = df.groupby('dep').sum().apply(np.log) print(df_log.head()) df_log.sample(5).plot(kind = "bar") ``` -* Repartir de `df`. -Calculer les émissions totales par département et sortir la liste -des 10 principaux émetteurs de CO2 et des 5 départements les moins émetteurs. -Sans faire de *merge*, -regarder les caractéristiques de ces départements (population et niveau de vie) - - ```{python} +#| echo: false +# Question 4 ## Emissions totales par département (df) df_emissions = df.reset_index().set_index(['INSEE commune','dep']).sum(axis = 1).groupby('dep').sum() #df.reset_index().groupby('dep').sum().sum(axis = 1).head() #version simplifiee ? @@ -344,45 +400,48 @@ print(df_city[df_city['dep'].isin(petits_emetteurs.index)][['NBPERSMENFISC16','M # Les petits emetteurs sont en moyenne plus pauvres et moins peuplés que les gros emetteurs ``` -{{% /box %}} - - +::: {.cell .markdown} +```{=html} + +``` +::: ```{python} +#| echo: false +# Question 1 df_copy = df.copy() df_copy2 = df.copy() ``` -* Utiliser la variable `dep` comme indice pour `df_copy` et retirer tout index pour `df_copy2` - ```{python} +#| echo: false +# Question 2 df_copy = df_copy.set_index('dep') df_copy2 = df_copy2.reset_index() ``` -* Importer le module `timeit` et comparer le temps d'exécution de la somme par secteur, pour chaque département, des émissions de CO2 - ```{python} +#| echo: false #| eval: false +# Question 3 %timeit df_copy.drop('Commune', axis = 1).groupby('dep').sum() %timeit df_copy2.drop('Commune', axis = 1).groupby('dep').sum() # Le temps d'exécution est plus lent sur la base sans index par département. ``` - -{{% /box %}} - - - # Restructurer les données On présente généralement deux types de données : @@ -403,27 +462,45 @@ Le fait de passer d'un format *wide* au format *long* (ou vice-versa) peut être extrêmement pratique car certaines fonctions sont plus adéquates sur une forme de données ou sur l'autre. En règle générale, avec `Python` comme avec `R`, les formats *long* sont souvent préférables. -{{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} +::: {.cell .markdown} +```{=html} + +``` +::: ```{python} +#| echo: false +# Question 1 + df_wide = df.copy() df_wide[['Commune','dep', "Agriculture", "Tertiaire"]].head() ``` -* Restructurer les données au format *long* pour avoir des données d'émissions par secteur en gardant comme niveau d'analyse la commune (attention aux autres variables identifiantes). - ```{python} +#| echo: false +# Question 2 + df_wide.reset_index().melt(id_vars = ['INSEE commune','Commune','dep'], var_name = "secteur", value_name = "emissions") ``` -* Faire la somme par secteur et représenter graphiquement - ```{python} +#| echo: false +# Question 3 + (df_wide.reset_index() .melt(id_vars = ['INSEE commune','Commune','dep'], var_name = "secteur", value_name = "emissions") @@ -431,38 +508,50 @@ df_wide.reset_index().melt(id_vars = ['INSEE commune','Commune','dep'], ) ``` -* Garder, pour chaque département, le secteur le plus polluant - ```{python} +#| echo: false +# Question 4 + (df_wide.reset_index().melt(id_vars = ['INSEE commune','Commune','dep'], var_name = "secteur", value_name = "emissions") .groupby(['secteur','dep']).sum().reset_index().sort_values(['dep','emissions'], ascending = False).groupby('dep').head(1) ) ``` +::: {.cell .markdown} +```{=html} + +``` +::: ```{python} -#| echo: true +#| echo: false +# Question 1 df_wide = df.copy() ``` -* Reconstruire le DataFrame, au format long, des données d'émissions par secteur en gardant comme niveau d'analyse la commune puis faire la somme par département et secteur - ```{python} +#| echo: false +# Question 2 + df_long_agg = (df_wide.reset_index() .melt(id_vars = ['INSEE commune','Commune','dep'], var_name = "secteur", value_name = "emissions").groupby(["dep", "secteur"]).sum() @@ -471,34 +560,29 @@ df_long_agg = (df_wide.reset_index() df_long_agg.head() ``` -* Passer au format *wide* pour avoir une ligne par secteur et une colonne par département - ```{python} +#| echo: false +# Question 3 + df_wide_agg = df_long_agg.reset_index().pivot_table(values = "emissions", index = "secteur", columns = "dep") df_wide_agg.head() ``` - -* Calculer, pour chaque secteur, la place du département dans la hiérarchie des émissions nationales - - ```{python} +#| echo: false +# Question 4 + df_wide_agg.rank(axis = 1) ``` - -* A partir de là, en déduire le rang médian de chaque département dans la hiérarchie des émissions et regarder les 10 plus mauvais élèves, selon ce critère. - - ```{python} +#| echo: false +# Question 5 + df_wide_agg.rank(axis = 1).median().nlargest(10) ``` -{{% /box %}} - - - # Combiner les données Une information que l'on cherche à obtenir s'obtient de moins en moins à partir d'une unique base de données. Il devient commun de devoir combiner des données issues de sources différentes. Nous allons ici nous focaliser sur le cas le plus favorable qui est la situation où une information permet d'apparier de manière exacte deux bases de données (autrement nous serions dans une situation, beaucoup plus complexe, d'appariement flou). La situation typique est l'appariement entre deux sources de données selon un identifiant individuel ou un identifiant de code commune, ce qui est notre cas. @@ -510,19 +594,36 @@ On utilise de manière indifférente les termes *merge* ou *join*. Le deuxième ![](../02a_pandas_tutorial/pandas_join.png) -{{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} +::: {.cell .markdown} +```{=html} + +``` +::: ```{python} +#| echo: false +# Question 1 + df['emissions'] = df.sum(axis = 1) ``` -* Faire une jointure à gauche entre les données d'émissions et les données de cadrage. Comparer les émissions moyennes des villes sans *match* (celles dont des variables bien choisies de la table de droite sont NaN) avec celles où on a bien une valeur correspondante dans la base Insee - ```{python} +#| echo: false +# Question 2 + df_merged = df.merge(df_city, how = "left", left_index = True, right_index = True) print(df_merged[df_merged['LIBGEO'].isna()]['emissions'].mean()) @@ -530,10 +631,10 @@ print(df_merged[~df_merged['LIBGEO'].isna()]['emissions'].mean()) ``` - -* Faire un *inner join* puis calculer l'empreinte carbone (l'émission rapportée au nombre de ménages fiscaux) dans chaque commune. Sortir un histogramme en niveau puis en log et quelques statistiques descriptives sur le sujet. - ```{python} +#| echo: false +# Question 3 + df_merged = df.merge(df_city, left_index = True, right_index = True) df_merged['empreinte'] = df_merged['emissions']/df_merged['NBPERSMENFISC16'] @@ -542,18 +643,14 @@ np.log(df_merged['empreinte']).plot.hist() df_merged['empreinte'].describe() ``` - -* Regarder la corrélation entre les variables de cadrage et l'empreinte carbone. Certaines variables semblent-elles pouvoir potentiellement influer sur l'empreinte carbone ? - ```{python} +#| echo: false +# Question 4 + df_merged.corr()['empreinte'].nlargest(10) # Les variables en lien avec le transport. ``` -{{% /box %}} - - - # Exercices bonus Les plus rapides d'entre vous sont invités à aller un peu plus loin en s'entraînant avec des exercices bonus qui proviennent du [site de Xavier Dupré](http://www.xavierdupre.fr/app/ensae_teaching_cs/helpsphinx3). 3 notebooks en lien avec `numpy` et `pandas` vous y sont proposés : diff --git a/dev/build-correction-nb.sh b/dev/build-correction-nb.sh new file mode 100644 index 000000000..c8c81aed2 --- /dev/null +++ b/dev/build-correction-nb.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +git diff --name-only >> diff +python build/tweak_render.py +python build/pimp_notebook.py +python build/tweak_headers_quarto.py + +quarto render content --to ipynb --execute +mkdir -p corrections +python build/move_files.py corrections diff --git a/dev/render-changes.sh b/dev/render-changes.sh new file mode 100644 index 000000000..b4657cb15 --- /dev/null +++ b/dev/render-changes.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +git diff --name-only >> diff +python build/tweak_render.py +python build/pimp_notebook.py +if [ "$1" = "correction" ]; then + python build/tweak_headers_quarto.py +fi + +quarto render --to hugo +find . -name "index.md" | rename -f -d 's/^/_/' + +hugo server -p 5000 --bind 0.0.0.0 diff --git a/dev/render-chapter.sh b/dev/render-chapter.sh new file mode 100644 index 000000000..ae023f7ec --- /dev/null +++ b/dev/render-chapter.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +SECTION=$1 +CHAPTER=$2 + +SECTION_PATH="content/course/${SECTION}" +CHAPTER_PATH="${SECTION_PATH}/${CHAPTER}" + +quarto render ${SECTION_PATH}/index.qmd --to hugo +mv ${SECTION_PATH}/index.md ${SECTION_PATH}/_index.md +quarto render ${CHAPTER_PATH}/index.qmd --to hugo +mv ${CHAPTER_PATH}/index.md ${CHAPTER_PATH}/_index.md + +hugo server -p 5000 --bind 0.0.0.0 diff --git a/dev/set-up.sh b/dev/set-up.sh new file mode 100644 index 000000000..540d4cfe7 --- /dev/null +++ b/dev/set-up.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Install recent go version +GO_VERSION="1.19.2" +wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz -O go.tar.gz && \ + sudo rm -rf /usr/local/go && \ + sudo tar -C /usr/local -xzf go.tar.gz && \ + sudo rm go.tar.gz +echo "PATH=$PATH:/usr/local/go/bin" >> $HOME/.bashrc + +# Install hugo +HUGO_VERSION="0.97.3" +wget https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.deb -O hugo.deb && \ + sudo dpkg -i hugo.deb && \ + sudo rm hugo.deb + +# Install quarto +QUARTO_VERSION="1.1.251" +wget "https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb" -O quarto.deb && \ + sudo dpkg -i quarto.deb && \ + sudo rm quarto.deb + +# source $HOME/.profile diff --git a/view.sh b/view.sh deleted file mode 100644 index ecfad967b..000000000 --- a/view.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -# Install recent go version -GO_VERSION="1.18.4" -wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz && \ - sudo rm -rf /usr/local/go && \ - sudo tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz - -PATH="/usr/local/go/bin:${PATH}" - -# Install hugo -HUGO_VERSION="0.97.3" -wget https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.deb && \ - sudo apt install "./hugo_extended_${HUGO_VERSION}_Linux-64bit.deb" && \ - sudo rm -f hugo_extended_${HUGO_VERSION}_Linux-64bit.deb - -QUARTO_VERSION="1.0.37" -wget "https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb" && \ - sudo apt install "./quarto-${QUARTO_VERSION}-linux-amd64.deb" && \ - sudo rm -f "./quarto-${QUARTO_VERSION}-linux-amd64.deb" - - -hugo server -p 5000 --bind 0.0.0.0