Ajouter la variante de têtes imprimées et simplifier les graphs globaux
This commit is contained in:
parent
ff2fa1819a
commit
9ef0d6cb9f
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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__":
|
||||
|
||||
@ -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__":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user