From 9ef0d6cb9f7f0236e265797c36c04393e2def454 Mon Sep 17 00:00:00 2001 From: Richard Dern Date: Wed, 3 Dec 2025 16:21:22 +0100 Subject: [PATCH] =?UTF-8?q?Ajouter=20la=20variante=20de=20t=C3=AAtes=20imp?= =?UTF-8?q?rim=C3=A9es=20et=20simplifier=20les=20graphs=20globaux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + lib/plots/global_minifig_heads.py | 61 +++++++++++++++---------- lib/rebrickable/global_minifig_heads.py | 14 ++++-- scripts/compute_global_minifig_heads.py | 10 ++++ scripts/plot_global_minifig_heads.py | 3 ++ 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0548da4..76aa8e3 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,7 @@ Le script lit `data/intermediate/minifig_heads_by_year.csv` et produit `figures/ 3. `python -m scripts.plot_global_minifig_skin_tones` Ces scripts lisent les CSV bruts du catalogue complet (`data/raw/inventories.csv`, `inventory_parts.csv`, `parts.csv`, `colors.csv`, `sets.csv`), extraient les têtes de minifigs via `part_cat_id=59`, agrègent les couleurs par année dans `data/intermediate/global_minifig_heads_by_year.csv`, puis tracent `figures/step17/global_minifig_heads_yellow_share.png` montrant la part annuelle de la couleur Yellow comparée au reste, jalons inclus. +Une variante `data/intermediate/global_minifig_heads_printed_by_year.csv` et `figures/step17/global_minifig_heads_printed_shares.png` se limite aux têtes dont le nom contient « print » pour observer la diversification des têtes imprimées. ### Étape 19 : total de minifigs des sets filtrés diff --git a/lib/plots/global_minifig_heads.py b/lib/plots/global_minifig_heads.py index 2ba4b7b..6c34c2e 100644 --- a/lib/plots/global_minifig_heads.py +++ b/lib/plots/global_minifig_heads.py @@ -5,6 +5,7 @@ from typing import Dict, Iterable, List, Tuple import matplotlib.pyplot as plt +from lib.color_sort import sort_hex_colors_lab from lib.filesystem import ensure_parent_dir from lib.rebrickable.stats import read_rows @@ -14,35 +15,48 @@ def load_global_heads(heads_path: Path) -> List[dict]: return read_rows(heads_path) -def select_top_colors(rows: Iterable[dict], limit: int = 12) -> List[Tuple[str, str, str]]: - """Retourne les couleurs les plus fréquentes globalement (nom, rgb, is_translucent).""" - totals: Dict[Tuple[str, str, str], int] = {} - for row in rows: - key = (row["color_name"], row["color_rgb"], row["is_translucent"]) - totals[key] = totals.get(key, 0) + int(row["quantity"]) - sorted_colors = sorted(totals.items(), key=lambda item: (-item[1], item[0][0], item[0][1])) - return [color for color, _ in sorted_colors[:limit]] +def order_colors(rows: Iterable[dict]) -> List[Tuple[str, str, str]]: + """Retourne toutes les couleurs triées par teinte (Yellow en premier).""" + colors_set = {(row["color_name"], row["color_rgb"], row["is_translucent"]) for row in rows} + yellow_colors = [color for color in colors_set if color[0] == "Yellow"] + remaining = [color for color in colors_set if color[0] != "Yellow"] + ordered_hex = sort_hex_colors_lab([color[1] for color in remaining]) + hex_rank = {hex_value: index for index, hex_value in enumerate(ordered_hex)} + sorted_remaining = sorted(remaining, key=lambda c: (hex_rank[c[1]], c[2], c[0])) + return yellow_colors + sorted_remaining + + +def build_color_labels(colors: Iterable[Tuple[str, str, str]]) -> Dict[Tuple[str, str, str], str]: + """Construit des libellés uniques (suffixe opaque/translucide si besoin).""" + name_counts: Dict[str, int] = {} + for name, _, _ in colors: + name_counts[name] = name_counts.get(name, 0) + 1 + labels: Dict[Tuple[str, str, str], str] = {} + for name, color_rgb, is_trans in colors: + if name_counts[name] > 1: + suffix = " translucide" if is_trans == "true" else " opaque" + labels[(name, color_rgb, is_trans)] = f"{name}{suffix}" + else: + labels[(name, color_rgb, is_trans)] = name + return labels def build_share_matrix( - rows: Iterable[dict], top_colors: List[Tuple[str, str, str]] -) -> Tuple[List[int], List[Tuple[str, str, str]], List[Dict[str, float]]]: - """Construit les parts par année en regroupant les couleurs hors top dans 'Autres'.""" + rows: Iterable[dict], colors: List[Tuple[str, str, str]] +) -> Tuple[List[int], List[Tuple[str, str, str]], List[Dict[Tuple[str, str, str], float]]]: + """Construit les parts par année pour toutes les couleurs triées.""" years = sorted({int(row["year"]) for row in rows}) - colors = top_colors + [("Autres", "444444", "false")] - shares_by_year: List[Dict[str, float]] = [] + shares_by_year: List[Dict[Tuple[str, str, str], float]] = [] rows_by_year: Dict[int, List[dict]] = {year: [] for year in years} for row in rows: rows_by_year[int(row["year"])].append(row) for year in years: year_rows = rows_by_year[year] total = sum(int(r["quantity"]) for r in year_rows) - shares: Dict[str, float] = {color[0]: 0.0 for color in colors} + shares: Dict[Tuple[str, str, str], float] = {color: 0.0 for color in colors} for r in year_rows: key = (r["color_name"], r["color_rgb"], r["is_translucent"]) - quantity = int(r["quantity"]) - target = "Autres" if key not in top_colors else r["color_name"] - shares[target] = shares.get(target, 0.0) + quantity / total if total > 0 else 0.0 + shares[key] = shares.get(key, 0.0) + (int(r["quantity"]) / total if total > 0 else 0.0) shares_by_year.append(shares) return years, colors, shares_by_year @@ -54,14 +68,16 @@ def plot_global_head_shares( ) -> None: """Trace les parts des couleurs de têtes de minifigs par année (catalogue complet).""" rows = load_global_heads(heads_path) - top_colors = select_top_colors(rows, limit=top_limit) - years, colors, shares_by_year = build_share_matrix(rows, top_colors) + colors = order_colors(rows) + labels_map = build_color_labels(colors) + years, colors, shares_by_year = build_share_matrix(rows, colors) fig, ax = plt.subplots(figsize=(14, 6)) bottoms = [0.0] * len(years) y_positions = list(range(len(years))) - for name, color_rgb, is_trans in colors: - values = [shares[name] for shares in shares_by_year] + for color in colors: + name, color_rgb, is_trans = color + values = [shares[color] for shares in shares_by_year] edge = "#f2f2f2" if is_trans == "true" else "#0d0d0d" ax.bar( years, @@ -69,7 +85,7 @@ def plot_global_head_shares( bottom=bottoms, color=f"#{color_rgb}", edgecolor=edge, - label=name, + label=labels_map[color], linewidth=0.7, ) bottoms = [b + v for b, v in zip(bottoms, values)] @@ -82,7 +98,6 @@ def plot_global_head_shares( else: ax.set_xticks(years) ax.set_title("Répartition des couleurs de têtes de minifigs par année (catalogue complet)") - ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1), frameon=False) ax.grid(True, axis="y", linestyle="--", alpha=0.25) ensure_parent_dir(destination_path) fig.tight_layout() diff --git a/lib/rebrickable/global_minifig_heads.py b/lib/rebrickable/global_minifig_heads.py index 2dc1cdb..5b6d6ca 100644 --- a/lib/rebrickable/global_minifig_heads.py +++ b/lib/rebrickable/global_minifig_heads.py @@ -10,15 +10,18 @@ from lib.rebrickable.parts_inventory import normalize_boolean, select_latest_inv HEAD_CATEGORIES = {"59"} -def load_head_parts(parts_path: Path, head_categories: Set[str] | None = None) -> Set[str]: +def load_head_parts(parts_path: Path, head_categories: Set[str] | None = None, require_printed: bool = False) -> Dict[str, str]: """Construit l'ensemble des références de têtes via leur catégorie.""" categories = head_categories or HEAD_CATEGORIES - head_parts: Set[str] = set() + head_parts: Dict[str, str] = {} with parts_path.open() as parts_file: reader = csv.DictReader(parts_file) for row in reader: - if row["part_cat_id"] in categories: - head_parts.add(row["part_num"]) + if row["part_cat_id"] not in categories: + continue + if require_printed and "print" not in row["name"].lower(): + continue + head_parts[row["part_num"]] = row["name"] return head_parts @@ -53,9 +56,10 @@ def aggregate_global_heads_by_year( colors_path: Path, sets_path: Path, head_categories: Set[str] | None = None, + require_printed: bool = False, ) -> List[dict]: """Agrège les couleurs de têtes par année sur le catalogue complet.""" - head_parts = load_head_parts(parts_path, head_categories) + head_parts = load_head_parts(parts_path, head_categories, require_printed=require_printed) latest_inventories = select_latest_inventories(inventories_path) latest_inventory_ids = {data["id"]: set_num for set_num, data in latest_inventories.items()} colors_lookup = build_color_lookup(colors_path) diff --git a/scripts/compute_global_minifig_heads.py b/scripts/compute_global_minifig_heads.py index bf58122..21eedf5 100644 --- a/scripts/compute_global_minifig_heads.py +++ b/scripts/compute_global_minifig_heads.py @@ -11,6 +11,7 @@ PARTS_PATH = Path("data/raw/parts.csv") COLORS_PATH = Path("data/raw/colors.csv") SETS_PATH = Path("data/raw/sets.csv") DESTINATION_PATH = Path("data/intermediate/global_minifig_heads_by_year.csv") +DESTINATION_PRINTED_PATH = Path("data/intermediate/global_minifig_heads_printed_by_year.csv") def main() -> None: @@ -23,6 +24,15 @@ def main() -> None: SETS_PATH, ) write_global_heads_by_year(DESTINATION_PATH, heads_by_year) + heads_printed_by_year = aggregate_global_heads_by_year( + INVENTORIES_PATH, + INVENTORY_PARTS_PATH, + PARTS_PATH, + COLORS_PATH, + SETS_PATH, + require_printed=True, + ) + write_global_heads_by_year(DESTINATION_PRINTED_PATH, heads_printed_by_year) if __name__ == "__main__": diff --git a/scripts/plot_global_minifig_heads.py b/scripts/plot_global_minifig_heads.py index 49b49cc..269f561 100644 --- a/scripts/plot_global_minifig_heads.py +++ b/scripts/plot_global_minifig_heads.py @@ -6,12 +6,15 @@ from lib.plots.global_minifig_heads import plot_global_head_shares HEADS_PATH = Path("data/intermediate/global_minifig_heads_by_year.csv") +HEADS_PRINTED_PATH = Path("data/intermediate/global_minifig_heads_printed_by_year.csv") DESTINATION_PATH = Path("figures/step17/global_minifig_heads_shares.png") +DESTINATION_PRINTED_PATH = Path("figures/step17/global_minifig_heads_printed_shares.png") def main() -> None: """Construit la heatmap stackée des parts de couleurs de têtes.""" plot_global_head_shares(HEADS_PATH, DESTINATION_PATH) + plot_global_head_shares(HEADS_PRINTED_PATH, DESTINATION_PRINTED_PATH) if __name__ == "__main__":