diff --git a/README.md b/README.md index 32c5c4d..a6b7ea5 100644 --- a/README.md +++ b/README.md @@ -183,4 +183,5 @@ Le script lit `data/intermediate/colors_by_set.csv` et produit deux agrégats : 1. `source .venv/bin/activate` 2. `python -m scripts.plot_colors_timeline` -Le script lit les deux agrégats de l'étape précédente et produit `figures/step14/colors_translucent_share.png` (part des pièces translucides par année et nombre de couleurs distinctes), `figures/step14/colors_heatmap_linear.png` (heatmap année × couleur en quantités brutes) et `figures/step14/colors_heatmap_log.png` (heatmap avec échelle log1p). +Le script lit les agrégats de l'étape 14 et produit `figures/step15/colors_translucent_share.png` (part des pièces translucides par année et nombre de couleurs distinctes), `figures/step15/colors_heatmap_linear.png` (heatmap année × couleur en quantités brutes) et `figures/step15/colors_heatmap_log.png` (heatmap avec échelle log1p). +Une troisième variante normalise les quantités par année : `figures/step15/colors_heatmap_share.png`. Dans cette vue, chaque colonne (année) est ramenée à une part relative (0–1) du total de pièces de l'année. Cela met en évidence la structure de palette indépendamment du volume : deux années restent comparables même si leur nombre total de pièces diffère fortement, mais l'information de volume absolu n'apparaît plus (à privilégier pour les comparaisons de proportions, pas pour mesurer la rareté volumique). diff --git a/lib/plots/colors_timeline.py b/lib/plots/colors_timeline.py index f0fa238..67a4ea9 100644 --- a/lib/plots/colors_timeline.py +++ b/lib/plots/colors_timeline.py @@ -79,11 +79,21 @@ def build_heatmap_data(rows: Iterable[dict]) -> Tuple[List[int], List[str], np.n return years, labels, matrix, swatches -def plot_colors_heatmap(matrix_path: Path, destination_path: Path, use_log_scale: bool = False) -> None: - """Génère une heatmap année × couleur basée sur les quantités totales avec pastilles.""" +def plot_colors_heatmap( + matrix_path: Path, + destination_path: Path, + use_log_scale: bool = False, + normalize_by_year: bool = False, +) -> None: + """Génère une heatmap année × couleur avec pastilles (linéaire, log1p ou normalisée).""" rows = load_rows(matrix_path) years, labels, matrix, swatches = build_heatmap_data(rows) - values = np.log1p(matrix) if use_log_scale else matrix + values = matrix + if normalize_by_year: + column_totals = matrix.sum(axis=0, keepdims=True) + values = matrix / column_totals + if use_log_scale: + values = np.log1p(values) fig, ax = plt.subplots(figsize=(14, max(6, len(labels) * 0.26))) y_positions = np.arange(len(labels)) @@ -108,10 +118,18 @@ def plot_colors_heatmap(matrix_path: Path, destination_path: Path, use_log_scale ) ax.set_xlim(-1.1, len(years) - 0.5) ax.set_xlabel("Année") - title_scale = "log1p des quantités" if use_log_scale else "quantités totales" + if normalize_by_year: + title_scale = "parts de couleur (par année)" + else: + title_scale = "log1p des quantités" if use_log_scale else "quantités totales" ax.set_title(f"Intensité des couleurs par année ({title_scale})") cbar = fig.colorbar(heatmap, ax=ax, shrink=0.82, pad=0.018) - cbar_label = "log1p(quantité totale)" if use_log_scale else "quantité totale" + if normalize_by_year: + cbar_label = "Part de la couleur (0-1)" + elif use_log_scale: + cbar_label = "log1p(quantité totale)" + else: + cbar_label = "quantité totale" cbar.set_label(cbar_label) ensure_parent_dir(destination_path) fig.subplots_adjust(left=0.26, right=0.97, bottom=0.08, top=0.94) diff --git a/scripts/plot_colors_timeline.py b/scripts/plot_colors_timeline.py index 718d25f..640c0e2 100644 --- a/scripts/plot_colors_timeline.py +++ b/scripts/plot_colors_timeline.py @@ -7,9 +7,10 @@ from lib.plots.colors_timeline import plot_colors_heatmap, plot_translucent_shar TIMELINE_PATH = Path("data/intermediate/colors_timeline.csv") MATRIX_PATH = Path("data/intermediate/colors_year_color_matrix.csv") -TRANSLUCENT_DESTINATION = Path("figures/step14/colors_translucent_share.png") -HEATMAP_LINEAR_DESTINATION = Path("figures/step14/colors_heatmap_linear.png") -HEATMAP_LOG_DESTINATION = Path("figures/step14/colors_heatmap_log.png") +TRANSLUCENT_DESTINATION = Path("figures/step15/colors_translucent_share.png") +HEATMAP_LINEAR_DESTINATION = Path("figures/step15/colors_heatmap_linear.png") +HEATMAP_LOG_DESTINATION = Path("figures/step15/colors_heatmap_log.png") +HEATMAP_SHARE_DESTINATION = Path("figures/step15/colors_heatmap_share.png") def main() -> None: @@ -17,6 +18,7 @@ def main() -> None: plot_translucent_share(TIMELINE_PATH, TRANSLUCENT_DESTINATION) plot_colors_heatmap(MATRIX_PATH, HEATMAP_LINEAR_DESTINATION, use_log_scale=False) plot_colors_heatmap(MATRIX_PATH, HEATMAP_LOG_DESTINATION, use_log_scale=True) + plot_colors_heatmap(MATRIX_PATH, HEATMAP_SHARE_DESTINATION, normalize_by_year=True) if __name__ == "__main__": diff --git a/tests/test_colors_timeline_plot.py b/tests/test_colors_timeline_plot.py index 7f3cced..18b4325 100644 --- a/tests/test_colors_timeline_plot.py +++ b/tests/test_colors_timeline_plot.py @@ -28,8 +28,9 @@ def test_plot_translucent_share(tmp_path: Path) -> None: def test_plot_colors_heatmap(tmp_path: Path) -> None: """Génère une heatmap année × couleur.""" matrix_path = tmp_path / "colors_year_color_matrix.csv" - destination_linear = tmp_path / "figures" / "step14" / "colors_heatmap_linear.png" - destination_log = tmp_path / "figures" / "step14" / "colors_heatmap_log.png" + destination_linear = tmp_path / "figures" / "step15" / "colors_heatmap_linear.png" + destination_log = tmp_path / "figures" / "step15" / "colors_heatmap_log.png" + destination_share = tmp_path / "figures" / "step15" / "colors_heatmap_share.png" matrix_path.write_text( "year,color_rgb,is_translucent,color_name,quantity_total\n" "2020,AAAAAA,false,Gray,5\n" @@ -40,8 +41,11 @@ def test_plot_colors_heatmap(tmp_path: Path) -> None: plot_colors_heatmap(matrix_path, destination_linear, use_log_scale=False) plot_colors_heatmap(matrix_path, destination_log, use_log_scale=True) + plot_colors_heatmap(matrix_path, destination_share, normalize_by_year=True) assert destination_linear.exists() assert destination_linear.stat().st_size > 0 assert destination_log.exists() assert destination_log.stat().st_size > 0 + assert destination_share.exists() + assert destination_share.stat().st_size > 0