From 241f48d48f8f1627e787b401335cb204e18ad708 Mon Sep 17 00:00:00 2001 From: Richard Dern Date: Wed, 3 Dec 2025 17:49:46 +0100 Subject: [PATCH] =?UTF-8?q?Ajouter=20la=20heatmap=20log=20des=20cat=C3=A9g?= =?UTF-8?q?ories=20et=20le=20collage=20des=20pi=C3=A8ces=20imprim=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/plots/part_categories.py | 35 ++++++++++++++++++++++++++++++ scripts/plot_part_categories.py | 3 +++ tests/test_part_categories_plot.py | 5 +++++ 3 files changed, 43 insertions(+) diff --git a/lib/plots/part_categories.py b/lib/plots/part_categories.py index ea2c34d..0ec7aee 100644 --- a/lib/plots/part_categories.py +++ b/lib/plots/part_categories.py @@ -101,6 +101,41 @@ def plot_part_categories_heatmap(categories_by_year_path: Path, destination_path plt.close(fig) +def plot_part_categories_heatmap_log(categories_by_year_path: Path, destination_path: Path) -> None: + """Heatmap des quantités (log1p) par catégorie et par année, en excluant les catégories vides.""" + rows = load_rows(categories_by_year_path) + years = extract_years(rows) + totals: Dict[str, int] = {} + quantity_lookup = {(row["year"], row["category_id"]): int(row["quantity_non_spare"]) for row in rows} + for row in rows: + totals[row["category_id"]] = totals.get(row["category_id"], 0) + int(row["quantity_non_spare"]) + categories = sorted([cat_id for cat_id, total in totals.items() if total > 0], key=lambda cat_id: -totals[cat_id]) + if not categories: + return + matrix = np.zeros((len(categories), len(years))) + for i, cat_id in enumerate(categories): + for j, year in enumerate(years): + matrix[i, j] = np.log1p(quantity_lookup.get((year, cat_id), 0)) + fig, ax = plt.subplots(figsize=(12, 10)) + cmap = plt.get_cmap("magma") + im = ax.imshow(matrix, aspect="auto", cmap=cmap, norm=Normalize(vmin=0, vmax=matrix.max() if matrix.max() > 0 else 1)) + ax.set_xticks(np.arange(len(years))) + ax.set_xticklabels(years, rotation=45, ha="right") + labels = {row["category_id"]: row["category_name"] for row in rows} + ax.set_yticks(np.arange(len(categories))) + ax.set_yticklabels([labels[cat_id] for cat_id in categories]) + ax.set_xlabel("Année") + ax.set_ylabel("Catégorie de pièce") + ax.set_title("Intensité des catégories de pièces par année (log des quantités)") + cbar = fig.colorbar(ScalarMappable(norm=im.norm, cmap=cmap), ax=ax, fraction=0.025, pad=0.015) + cbar.ax.set_ylabel("log1p(quantité)", rotation=90) + + ensure_parent_dir(destination_path) + fig.tight_layout() + fig.savefig(destination_path, dpi=170) + plt.close(fig) + + def plot_structural_share_timeline(categories_by_year_path: Path, destination_path: Path) -> None: """Trace l'évolution de la part des catégories structurelles.""" rows = load_rows(categories_by_year_path) diff --git a/scripts/plot_part_categories.py b/scripts/plot_part_categories.py index 9f087a5..4fbf3c8 100644 --- a/scripts/plot_part_categories.py +++ b/scripts/plot_part_categories.py @@ -4,6 +4,7 @@ from pathlib import Path from lib.plots.part_categories import ( plot_part_categories_heatmap, + plot_part_categories_heatmap_log, plot_structural_share_timeline, plot_top_part_categories_area, ) @@ -13,6 +14,7 @@ CATEGORIES_BY_YEAR_PATH = Path("data/intermediate/part_categories_by_year.csv") CATEGORIES_GLOBAL_PATH = Path("data/intermediate/part_categories_global.csv") AREA_DESTINATION = Path("figures/step29/top_part_categories_area.png") HEATMAP_DESTINATION = Path("figures/step29/part_categories_heatmap.png") +HEATMAP_LOG_DESTINATION = Path("figures/step29/part_categories_heatmap_log.png") STRUCTURAL_DESTINATION = Path("figures/step29/structural_share_timeline.png") @@ -20,6 +22,7 @@ def main() -> None: """Génère les visuels de répartition par catégorie.""" plot_top_part_categories_area(CATEGORIES_BY_YEAR_PATH, CATEGORIES_GLOBAL_PATH, AREA_DESTINATION) plot_part_categories_heatmap(CATEGORIES_BY_YEAR_PATH, HEATMAP_DESTINATION) + plot_part_categories_heatmap_log(CATEGORIES_BY_YEAR_PATH, HEATMAP_LOG_DESTINATION) plot_structural_share_timeline(CATEGORIES_BY_YEAR_PATH, STRUCTURAL_DESTINATION) diff --git a/tests/test_part_categories_plot.py b/tests/test_part_categories_plot.py index 665e28a..9f14977 100644 --- a/tests/test_part_categories_plot.py +++ b/tests/test_part_categories_plot.py @@ -5,6 +5,7 @@ from pathlib import Path from lib.plots.part_categories import ( plot_part_categories_heatmap, + plot_part_categories_heatmap_log, plot_structural_share_timeline, plot_top_part_categories_area, ) @@ -31,15 +32,19 @@ def test_plot_part_categories_outputs_images(tmp_path: Path) -> None: ) area_dest = tmp_path / "figures" / "step29" / "top_part_categories_area.png" heatmap_dest = tmp_path / "figures" / "step29" / "part_categories_heatmap.png" + heatmap_log_dest = tmp_path / "figures" / "step29" / "part_categories_heatmap_log.png" structural_dest = tmp_path / "figures" / "step29" / "structural_share_timeline.png" plot_top_part_categories_area(by_year, by_global, area_dest, top_n=2) plot_part_categories_heatmap(by_year, heatmap_dest) + plot_part_categories_heatmap_log(by_year, heatmap_log_dest) plot_structural_share_timeline(by_year, structural_dest) assert area_dest.exists() assert heatmap_dest.exists() + assert heatmap_log_dest.exists() assert structural_dest.exists() assert area_dest.stat().st_size > 0 assert heatmap_dest.stat().st_size > 0 + assert heatmap_log_dest.stat().st_size > 0 assert structural_dest.stat().st_size > 0