diff --git a/documentation/01.5-pandas-dataframe.md b/documentation/01.5-pandas-dataframe.md index 8331c1c..547d3b6 100644 --- a/documentation/01.5-pandas-dataframe.md +++ b/documentation/01.5-pandas-dataframe.md @@ -438,10 +438,11 @@ print(selection1, selection2, sep="\n") ### Bon à savoir : vues et copies Lorsque vous extrayez une partie d'un `DataFrame`{.python}, vous obtenez généralement un objet de type `DataFrame`{.python}. -Cependant, il faut savoir que par défaut, le nouveau `DataFrame`{.python} est une **vue** sur le `DataFrame`{.python} initial. -Cela veut dire que si vous modifiez le contenu du `DataFrame`{.python} initial, le contenu du `DataFrame`{.python} extrait sera aussi modifié, et vice versa. +Cependant, il faut savoir que dans Pandas 2.0+, le nouveau `DataFrame`{.python} est une **copie** des données du `DataFrame`{.python} initial. +Cela veut dire que modifier le contenu d'un des deux `DataFrame`{.python} n'a pas d'effet sur l'autre. -Pour obtenir une copie des données du `DataFrame`{.python} extrait, il faut utiliser la méthode `.copy()`{.python}. +Pour obtenir une vue des données du `DataFrame`{.python} extrait et appliquer des modifications partagées, il faut +extraire en utilisant par exemple `.loc[(lignes, colonnes)]` (avec un `tuple`{.python}). ```python {.numberLines} import pandas as pd @@ -552,8 +553,13 @@ is_high = data > 3 # F, F, F, T, T, T high_data = data.loc[is_high & is_even] # 4, 6 ``` -On peut utiliser les opérateurs binaires `&`{.python} (`and`) et `|`{.python} (`or`) pour combiner les séries de booléens. -Le résultat est une série de booléens de la même longueur. +Pour des questions d'ordre d'évaluation des opérateurs, vous devrez quasi systématiquement utiliser des parenthèses pour discriminer vos conditions. +Les opérateurs à utiliser pour assembler vos propositions sont les opérateurs logiques binaires : + +- `&`{.python} : ET binaire +- `|`{.python} : OU binaire +- `~`{.python} : NON binaire +- `^`{.python} : OU exclusif binaire ---- @@ -712,6 +718,24 @@ L'attribut est documenté ici : [37]: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.day_name.html [38]: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.isocalendar.html +---- + +#### Exemple d'extraction de la semaine ISO + +Dans le standard ISO, le numéro de semaine est déterminé comme suit : + +La première semaine de l'année est celle dont la majorité de jours (au moins 4) est contenue +dans l'année en cours. + +```python {.numberLines} +import pandas as pd + +data = pd.DataFrame(data={ + "order": ["A15", "B84", "D25", "B43"], + "date": pd.to_datetime(["2024-06-10", "2021-02-14", "2023-06-25", "2024-11-30"]) +}) +data["week"] = data["date"].dt.isocalendar()["week"] +``` ---- @@ -870,6 +894,14 @@ Dans tous les cas sauf pour `df.at`{.python}, la clé peut être un des objets s - liste de clés de lignes/colonne ou numéro de ligne (`[a, b, c]`{.python}); - tuple à deux éléments (`(ligne(s), colonne(s))`{.python}). +**Attention** : Passer un tuple contenant un slice ne fonctionne pas syntaxiquement parlant +s'il est exprimé explicitement, ie. avec des parenthèses. + +```python {.numberLines} +df.loc[(0, 0:2)] # Ceci est une erreur de syntaxe +df.loc[0, 0:2] # Ceci est du Python valide +``` + ---- ## Valeurs vides @@ -1020,6 +1052,9 @@ df3 = df.fillna({"siret": "N/A", "siege": "Inconnu"}) df4 = df.ffill() # forward fill # Remplir les colonnes avec la prochaine valeur non vide trouvée df5 = df.bfill() # backward fill +# Remplir les valeurs NaN en interpolant +# https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html +df6 = df.interpolate(method="cubic") ``` ---- @@ -1157,6 +1192,8 @@ df = pd.DataFrame(data={ df.drop(labels=["prenom"], axis="columns", inplace=True) # Retirer les lignes aux index 0 et 2, mais retourner un nouveau DataFrame df2 = df.drop(labels=[0, 2], axis="index") +# Variante : Retirer les lignes aux index 0 et 2 +df3 = df.drop(index=[0, 2]) ``` ---- @@ -1200,7 +1237,7 @@ df = pd.DataFrame(data={ "numero": [8, 31, 23, 14] }) -# Appliquer une fonction sur chaque colonne +# Appliquer une fonction sur chaque ligne df2 = df.apply(lambda s: s * 2, axis="columns") ``` @@ -1434,7 +1471,7 @@ et organiser les éléments dans chaque groupe. ---- -## Astuces inoubliables +## Extra :En vrac À l'usage, de nombreux cas empiriques déborderont des bases vues dans ce chapitre. Par exemple, certains comportements sont difficiles à comprendre, ou certains usages nécessitent quelques ajustements pour la performance. @@ -1446,7 +1483,14 @@ comportements sont difficiles à comprendre, ou certains usages nécessitent que ### Warning : modifications sur une copie de slice -Lorsqu'un utilisateur utilise Pandas dans le cadre d'une recherche ou d'un traitement, il est fréquent de -vouloir filtrer les lignes d'un `DataFrame`, puis d'appliquer des modifications au contenu de ce dernier. +Lorsqu'un utilisateur utilise Pandas dans l'objectif de modifier un `DataFrame`{.python} +extrait d'un `DataFrame`{.python} plus large (après filtrage de lignes par exemple), il +sera généralement exposé à un message d'avertissement tel que le suivant : +```text {.numberLines} +SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. +Try using .loc[row_index,col_indexer] = value instead +``` +Le message peut être désactivé via les paramètres de Pandas, ou encore en récupérant explicitement +un `DataFrame`{.python} via la méthode `copy()`{.python}. diff --git a/documentation/02.2-eda-plotly.md b/documentation/02.2-eda-plotly.md index d8114aa..b2f55ec 100644 --- a/documentation/02.2-eda-plotly.md +++ b/documentation/02.2-eda-plotly.md @@ -18,7 +18,7 @@ propre à la publication. ## Comment fonctionne Plotly ? -Plotly est une bibliothèque Python compatible avec les `DataFrame`{.python} Pandas, qui permet de créer des graphiques +Plotly est une bibliothèque Python qui permet de créer des graphiques interactifs, et cela grâce aux technologies web (HTML, CSS et JS). Un navigateur suffit à afficher le contenu d'un graphique généré avec Plotly. @@ -55,10 +55,10 @@ Nous allons passer en revue quelques graphiques réalisables avec Express. ---- -#### `plotly.express.bar` +#### `plotly.express.bar` (`DataFrame`) -Générer un graphique en barres est assez simple avec Plotly, et fonctionne -de manière relativement satisfaisante par défaut (ex. texte en blanc sur fond sombre) +Générer un graphique en barres est assez simple avec Plotly Express, et fonctionne +de manière relativement satisfaisante par défaut (ex. gestion du contraste et angle des étiquettes) ```python {.numberLines} import pandas as pd @@ -799,6 +799,10 @@ figure.update_layout( figure.show(renderer="browser") ``` +La méthode `add_hrect()`{.python} dessine un rectangle prenant l'intégralité de la largeur du +graphique. Il ne reste qu'à définir les coordonnées `y0` et `y1` du rectangle, +coordonnées qui dépendent d'une référence (taille du graphique, pixels ou axe Y) + ---- ![Ajout d'un rectangle horizontal](assets/images/eda-plotly-trace-hrect.png) @@ -835,18 +839,29 @@ import numpy as np from plotly.graph_objs import Figure, Scatter from scipy.ndimage import gaussian_filter -# Créer une courbe aléatoire lissée -df = pd.DataFrame(data={"col1": gaussian_filter(np.random.random(size=200) * 79.0 + 1.0, sigma=1.5)}) +# Créer une courbe aléatoire lissée de valeurs entre 1 et 80 +SIZE: int = 500 +np.random.seed(4) # Pour reproduire les résultats +df = pd.DataFrame(data={"col1": gaussian_filter(np.random.random(size=SIZE) * 79.0 + 1.0, sigma=SIZE / 133)}) # Calculer les coefficients d'une régression de degré 2 pour col1 c2, c1, c0 = np.polyfit(df.index, df["col1"], 2) -df["reg1"] = df.index ** 2 * c2 + df.index * c1 + c0 - +df["reg1"] = c2 * df.index**2 + c1 * df.index + c0 # Créer une image avec deux lignes -figure = Figure([ - Scatter(name="values", x=df.index, y=df["col1"], line={"color": "black", "width": 2}, marker={"symbol": "circle"}, mode="lines+markers"), - Scatter(name="regression", x=df.index, y=df["reg1"], line={"color": "red", "width": 4, "dash": "dot"}, mode="lines") -]) -figure.layout.update({"template": "seaborn", "xaxis1": {"title": "time"}, "title": "Test de régression", "font": {"family": "Cabin", "size": 13}, "yaxis1": {"title": "value"}}) +figure = Figure( + data=[ + Scatter(name="values", x=df.index, y=df["col1"], line={"color": "gray", "width": 1}, mode="lines"), + Scatter( + name="regression", x=df.index, y=df["reg1"], line={"color": "red", "width": 4, "dash": "dot"}, mode="lines" + ), + ], + layout={ + "template": "seaborn", + "xaxis1": {"title": "time"}, + "title": "Test de régression", + "font": {"family": "Cabin", "size": 13}, + "yaxis1": {"title": "value"}, + }, +) figure.show(renderer="browser") ``` diff --git a/documentation/assets/images/eda-plotly-regression-curve.png b/documentation/assets/images/eda-plotly-regression-curve.png index 56c6459..7200899 100644 Binary files a/documentation/assets/images/eda-plotly-regression-curve.png and b/documentation/assets/images/eda-plotly-regression-curve.png differ diff --git a/documentation/assets/images/eda-plotly-subplot-base.png b/documentation/assets/images/eda-plotly-subplot-base.png index 52989eb..00ab218 100644 Binary files a/documentation/assets/images/eda-plotly-subplot-base.png and b/documentation/assets/images/eda-plotly-subplot-base.png differ diff --git a/pyproject.toml b/pyproject.toml index a1c2ce5..24ca609 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,11 @@ jupyter = "^1.0.0" xlsxwriter = "^3.1.1" plotly = "^5.15.0" +[tool.black] +line-length = 120 + +[tool.ruff] +line-length = 120 [build-system] requires = ["poetry-core"] diff --git a/source/plotting/charts/plotly_regression_curve.py b/source/plotting/charts/plotly_regression_curve.py new file mode 100644 index 0000000..f7bbb21 --- /dev/null +++ b/source/plotting/charts/plotly_regression_curve.py @@ -0,0 +1,30 @@ +import pandas as pd +import numpy as np +from plotly.graph_objs import Figure, Scatter +from scipy.ndimage import gaussian_filter + +# Créer une courbe aléatoire lissée de valeurs entre 1 et 80 +SIZE: int = 500 +np.random.seed(4) # Pour reproduire les résultats +df = pd.DataFrame(data={"col1": gaussian_filter(np.random.random(size=SIZE) * 79.0 + 1.0, sigma=SIZE / 133)}) +# Calculer les coefficients d'une régression de degré 2 pour col1 +c2, c1, c0 = np.polyfit(df.index, df["col1"], 2) +df["reg1"] = c2 * df.index**2 + c1 * df.index + c0 + +# Créer une image avec deux lignes +figure = Figure( + data=[ + Scatter(name="values", x=df.index, y=df["col1"], line={"color": "gray", "width": 1}, mode="lines"), + Scatter( + name="regression", x=df.index, y=df["reg1"], line={"color": "red", "width": 4, "dash": "dot"}, mode="lines" + ), + ], + layout={ + "template": "seaborn", + "xaxis1": {"title": "time"}, + "title": "Test de régression", + "font": {"family": "Cabin", "size": 13}, + "yaxis1": {"title": "value"}, + }, +) +figure.show(renderer="browser") diff --git a/source/plotting/charts/plotly_subplot_base.py b/source/plotting/charts/plotly_subplot_base.py index 1e93647..908dd1d 100644 --- a/source/plotting/charts/plotly_subplot_base.py +++ b/source/plotting/charts/plotly_subplot_base.py @@ -6,14 +6,12 @@ from plotly.graph_objs import Figure, Bar data = pd.DataFrame(data={ "product": ["tarte", "gâteau", "biscuit", "mille-feuille", "éclair", "brownie"], "price": [2.99, 3.49, 1.99, 4.99, 5.99, 6.99], - "weight": [250, 300, 200, 400, 500, 600] + "weight": [400, 500, 100, 250, 150, 350] }) -figure: Figure = make_subplots(rows=1, cols=3, subplot_titles=("Prix", "Poids unitaires")) -subplot = figure.get_subplot(row=1, col=2) -subplot.xaxis["domain"] = [0.3555555, 1.0] -print(subplot, dir(subplot)) +figure: Figure = make_subplots(rows=1, cols=2, subplot_titles=("Prix", "Poids unitaires")) +# subplot = figure.get_subplot(row=1, col=2) +# subplot.xaxis["domain"] = [0.3555555, 1.0] figure.add_trace(Bar(name="Prix", x=data["product"], y=data["price"]), row=1, col=1) figure.add_trace(Bar(name="Poids", x=data["product"], y=data["weight"]), row=1, col=2) figure.update_layout(template="seaborn", title="Prix et poids unitaires", font={"family": "Cabin", "size": 13}) -figure.update_traces(row=1, col=2, specs=2) figure.show(renderer="browser") diff --git a/source/plotting/charts/plotly_trace_rectangle.py b/source/plotting/charts/plotly_trace_rectangle.py index 70c6d4b..0865beb 100644 --- a/source/plotting/charts/plotly_trace_rectangle.py +++ b/source/plotting/charts/plotly_trace_rectangle.py @@ -1,5 +1,5 @@ import pandas as pd -from plotly.graph_objs import Figure, Bar, Scatter +from plotly.graph_objs import Figure, Bar data = pd.DataFrame( data={ @@ -8,13 +8,21 @@ data = pd.DataFrame( "weight": [250, 300, 200, 400, 500, 600], } ) -figure: Figure = Figure(data=[Bar(name="Prix", x=data["product"], y=data["price"])]) -figure.add_hrect(y0=2.75, y1=4.5, fillcolor="gray", opacity=0.25, layer="below") +figure: Figure = Figure(data=[]) +figure.add_hrect( + y0=2.75, + y1=4.5, + fillcolor="gray", + opacity=0.25, + layer="below", +) +figure.add_trace(Bar(name="Prix", x=data["product"], y=data["price"])) +figure.add_annotation(text="Zone de prix spéciale", xref="paper", yref="paper", x=0.5, y=0.5, xanchor="center", yanchor="middle", showarrow=False, font={"family": "Cabin", "size": 20}) figure.update_layout( template="seaborn", title="Prix et poids unitaires", font={"family": "Cabin", "size": 13}, xaxis={"title": "Produit", "showgrid": False}, - yaxis={"title": "Prix (€)", "showgrid": False} + yaxis={"title": "Prix (€)", "showgrid": False}, ) figure.show(renderer="browser")