1

Filtre les pièces techniques et documente l’étape 28

This commit is contained in:
Richard Dern 2025-12-02 15:28:22 +01:00
parent 812fd4a862
commit 74f8fa57e1
7 changed files with 109 additions and 13 deletions

View File

@ -164,14 +164,14 @@ Le script lit `data/intermediate/parts_filtered.csv` et `data/final/stats.csv` (
2. `python -m scripts.plot_colors_grid`
Le script lit `data/intermediate/parts_filtered.csv` et `data/raw/colors.csv`, puis génère deux visuels : `figures/step12/colors_grid.png` pour l'ensemble des pièces (rechanges incluses) et `figures/step12/colors_grid_minifigs.png` pour la seule palette des minifigs. Les couleurs sont triées perceptuellement et mises en scène sur une grille hexagonale.
Les codes couleurs 0033B2 et 05131D sont ignorés pour ne pas polluer les palettes.
Les codes couleurs 0033B2 et 05131D sont ignorés, et les pièces considérées comme techniques/structurelles (catégories Technic, roues, tubes, axes, etc.) sont filtrées afin de ne garder que les éléments « visibles » pour lesthétique.
### Étape 13 : palette de couleurs par set
1. `source .venv/bin/activate`
2. `python -m scripts.build_colors_by_set`
Le script agrège `data/intermediate/parts_filtered.csv` avec les libellés de couleurs `data/raw/colors.csv` et produit `data/intermediate/colors_by_set.csv` contenant, pour chaque set et chaque couleur, les quantités totales, hors rechanges, issues des minifigs et hors minifigs. Ce fichier sert de base aux visualisations et matrices de palette.
Le script agrège `data/intermediate/parts_filtered.csv` avec les libellés de couleurs `data/raw/colors.csv` et produit `data/intermediate/colors_by_set.csv` contenant, pour chaque set et chaque couleur, les quantités totales, hors rechanges, issues des minifigs et hors minifigs. Les couleurs ignorées (0033B2, 05131D) et les pièces techniques/structurelles sont exclues. Ce fichier sert de base aux visualisations et matrices de palette.
### Étape 14 : évolution annuelle des palettes
@ -187,6 +187,7 @@ Le script lit `data/intermediate/colors_by_set.csv` et produit deux agrégats :
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 (01) 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).
Toutes les vues héritent du filtrage des couleurs ignorées et des pièces techniques/structurelles appliqué en amont.
### Étape 16 : couleurs de peau des minifigs
@ -194,6 +195,7 @@ Une troisième variante normalise les quantités par année : `figures/step15/co
2. `python -m scripts.compute_minifig_heads`
Le script identifie les têtes de minifigs via la catégorie Rebrickable dédiée (part_cat_id 59 dans `data/raw/parts.csv`), filtre les pièces de rechange, puis agrège leurs couleurs depuis `data/intermediate/parts_filtered.csv`. Les sorties sont `data/intermediate/minifig_heads_by_set.csv` (quantités de têtes par set, couleur et année) et `data/intermediate/minifig_heads_by_year.csv` (agrégées par année). Ces fichiers serviront de base pour analyser l'évolution des teintes de peau (ou assimilées) des minifigs.
Les couleurs ignorées (0033B2, 05131D) sont écartées lors de lagrégation.
### Étape 17 : visualiser les couleurs de peau des minifigs
@ -277,3 +279,7 @@ Un second export `data/intermediate/minifigs_per_set_timeline.csv` est généré
2. `python -m scripts.plot_set_color_swatches`
Le script lit `data/intermediate/colors_by_set.csv` (hors rechanges) et `data/intermediate/sets_enriched.csv`, sélectionne pour chaque set les 5 couleurs les plus présentes en excluant les pièces de minifigs (`quantity_non_minifig`), écrit `data/intermediate/set_color_swatches.csv`, puis trace `figures/step27/set_color_swatches.png` affichant chaque set avec ses 5 pastilles de couleurs dominantes.
### Étape 28 : palettes perceptuelles par set (en préparation)
Objectif : produire une palette de 5 couleurs « perceptuelles » par set, moins biaisée par le volume de pièces. Létape sappuiera sur les mêmes filtres (couleurs 0033B2/05131D exclues, pièces techniques/structurelles ignorées), pondérera les couleurs par parts relatives hors minifigs, appliquera un tri perceptuel et une sélection diversifiée pour refléter lesthétique plutôt que le poids en pièces. La version volumique (`figures/step27/set_color_swatches.png`) reste disponible en attendant la finalisation de cette étape.

View File

@ -9,7 +9,7 @@ from matplotlib.lines import Line2D
from lib.filesystem import ensure_parent_dir
from lib.color_sort import lab_sort_key, sort_hex_colors_lab
from lib.rebrickable.color_ignores import is_ignored_color_rgb
from lib.rebrickable.color_ignores import is_ignored_color_rgb, is_ignored_part_category
from lib.rebrickable.parts_inventory import normalize_boolean
from lib.rebrickable.stats import read_rows
@ -21,18 +21,38 @@ def sort_colors_perceptually(colors: Iterable[dict]) -> List[dict]:
return sorted(colors, key=lambda color: index_map[color["color_rgb"]])
def load_used_colors(parts_path: Path, colors_path: Path, minifig_only: bool = False) -> List[dict]:
def load_part_categories(parts_catalog_path: Path) -> Dict[str, str]:
"""Indexe les catégories par part_num."""
categories: Dict[str, str] = {}
with parts_catalog_path.open() as catalog_file:
import csv
reader = csv.DictReader(catalog_file)
for row in reader:
categories[row["part_num"]] = row["part_cat_id"]
return categories
def load_used_colors(
parts_path: Path,
colors_path: Path,
parts_catalog_path: Path,
minifig_only: bool = False,
) -> List[dict]:
"""Charge les couleurs utilisées (hors rechanges) et leurs quantités totales.
Si minifig_only est vrai, ne conserve que les pièces marquées is_minifig_part=true.
Sinon, exclut les pièces de minifig.
"""
rows = read_rows(parts_path)
categories = load_part_categories(parts_catalog_path)
colors_lookup = {(row["rgb"], normalize_boolean(row["is_trans"])): row["name"] for row in read_rows(colors_path)}
totals: Dict[Tuple[str, str], int] = {}
for row in rows:
if is_ignored_color_rgb(row["color_rgb"]):
continue
if is_ignored_part_category(categories[row["part_num"]]):
continue
if minifig_only and row.get("is_minifig_part") != "true":
continue
if not minifig_only and row.get("is_minifig_part") == "true":
@ -85,11 +105,12 @@ def build_background(width: float, height: float, resolution: int = 600) -> np.n
def plot_colors_grid(
parts_path: Path,
colors_path: Path,
parts_catalog_path: Path,
destination_path: Path,
minifig_only: bool = False,
) -> None:
"""Dessine une grille artistique des couleurs utilisées."""
colors = load_used_colors(parts_path, colors_path, minifig_only=minifig_only)
colors = load_used_colors(parts_path, colors_path, parts_catalog_path, minifig_only=minifig_only)
positions = build_hex_positions(len(colors))
x_values = [x for x, _ in positions]
y_values = [y for _, y in positions]

View File

@ -1,8 +1,54 @@
"""Couleurs à exclure des palettes."""
"""Couleurs et catégories de pièces à exclure des palettes."""
import csv
from pathlib import Path
from typing import Set
IGNORED_COLOR_RGB = {"0033b2", "05131d"}
IGNORED_PART_CATEGORY_IDS = {
"1", # Baseplates
"8", # Technic Bricks
"12", # Technic Connectors
"17", # Gear Parts
"18", # Hinges, Arms and Turntables
"22", # Pneumatics
"23", # Panels
"25", # Technic Steering, Suspension and Engine
"26", # Technic Special
"29", # Wheels and Tyres
"30", # Tubes and Hoses
"31", # String, Bands and Reels
"34", # Supports, Girders and Cranes
"40", # Technic Panels
"43", # Znap
"44", # Mechanical
"45", # Electronics
"46", # Technic Axles
"51", # Technic Beams
"52", # Technic Gears
"53", # Technic Pins
"54", # Technic Bushes
"55", # Technic Beams Special
}
def is_ignored_color_rgb(rgb: str) -> bool:
"""Retourne vrai si le code couleur doit être ignoré."""
return rgb.strip().lower() in IGNORED_COLOR_RGB
def is_ignored_part_category(part_cat_id: str) -> bool:
"""Retourne vrai si la catégorie de pièce est exclue."""
return part_cat_id.strip() in IGNORED_PART_CATEGORY_IDS
def load_ignored_part_numbers(parts_catalog_path: Path) -> Set[str]:
"""Charge les références de pièces dont la catégorie est exclue."""
ignored: Set[str] = set()
with parts_catalog_path.open() as parts_file:
reader = csv.DictReader(parts_file)
for row in reader:
if is_ignored_part_category(row["part_cat_id"]):
ignored.add(row["part_num"])
return ignored

View File

@ -25,10 +25,17 @@ def build_colors_lookup(colors_path: Path) -> Dict[Tuple[str, str], str]:
return colors
def aggregate_colors_by_set(parts: Iterable[dict], colors_lookup: Dict[Tuple[str, str], str]) -> List[dict]:
def aggregate_colors_by_set(
parts: Iterable[dict],
colors_lookup: Dict[Tuple[str, str], str],
ignored_parts: set[str] | None = None,
) -> List[dict]:
"""Agrège les quantités par set et par couleur."""
ignored = ignored_parts or set()
totals: Dict[Tuple[str, str, str, str, str], dict] = {}
for row in parts:
if row["part_num"] in ignored:
continue
if is_ignored_color_rgb(row["color_rgb"]):
continue
key = (row["set_num"], row["set_id"], row["year"], row["color_rgb"], row["is_translucent"])

View File

@ -8,10 +8,12 @@ from lib.rebrickable.colors_by_set import (
load_parts,
write_colors_by_set,
)
from lib.rebrickable.color_ignores import load_ignored_part_numbers
PARTS_PATH = Path("data/intermediate/parts_filtered.csv")
COLORS_PATH = Path("data/raw/colors.csv")
PARTS_CATALOG_PATH = Path("data/raw/parts.csv")
DESTINATION_PATH = Path("data/intermediate/colors_by_set.csv")
@ -19,7 +21,8 @@ def main() -> None:
"""Génère colors_by_set.csv depuis parts_filtered.csv."""
parts = load_parts(PARTS_PATH)
colors_lookup = build_colors_lookup(COLORS_PATH)
aggregated = aggregate_colors_by_set(parts, colors_lookup)
ignored_parts = load_ignored_part_numbers(PARTS_CATALOG_PATH)
aggregated = aggregate_colors_by_set(parts, colors_lookup, ignored_parts=ignored_parts)
write_colors_by_set(DESTINATION_PATH, aggregated)

View File

@ -7,14 +7,15 @@ from lib.plots.colors_grid import plot_colors_grid
PARTS_PATH = Path("data/intermediate/parts_filtered.csv")
COLORS_PATH = Path("data/raw/colors.csv")
PARTS_CATALOG_PATH = Path("data/raw/parts.csv")
DESTINATION_PATH = Path("figures/step12/colors_grid.png")
MINIFIG_DESTINATION_PATH = Path("figures/step12/colors_grid_minifigs.png")
def main() -> None:
"""Construit les visuels des palettes de couleurs utilisées."""
plot_colors_grid(PARTS_PATH, COLORS_PATH, DESTINATION_PATH, minifig_only=False)
plot_colors_grid(PARTS_PATH, COLORS_PATH, MINIFIG_DESTINATION_PATH, minifig_only=True)
plot_colors_grid(PARTS_PATH, COLORS_PATH, PARTS_CATALOG_PATH, DESTINATION_PATH, minifig_only=False)
plot_colors_grid(PARTS_PATH, COLORS_PATH, PARTS_CATALOG_PATH, MINIFIG_DESTINATION_PATH, minifig_only=True)
if __name__ == "__main__":

View File

@ -33,6 +33,7 @@ def test_plot_colors_grid(tmp_path: Path) -> None:
"""Produit un fichier image avec les couleurs utilisées."""
parts_path = tmp_path / "parts_filtered.csv"
colors_path = tmp_path / "colors.csv"
parts_catalog_path = tmp_path / "parts.csv"
destination_path = tmp_path / "colors_grid.png"
write_csv(
@ -45,6 +46,16 @@ def test_plot_colors_grid(tmp_path: Path) -> None:
["3004", "0033B2", "false", "1000", "10", "false"],
],
)
write_csv(
parts_catalog_path,
["part_num", "name", "part_cat_id"],
[
["3001", "Brick", "11"],
["3002", "Pane", "23"],
["3003", "Slope", "3"],
["3004", "Technic", "8"],
],
)
write_csv(
colors_path,
["id", "name", "rgb", "is_trans", "num_parts", "num_sets", "y1", "y2"],
@ -52,13 +63,14 @@ def test_plot_colors_grid(tmp_path: Path) -> None:
["1", "White", "FFFFFF", "False", "0", "0", "0", "0"],
["2", "Black", "000000", "True", "0", "0", "0", "0"],
["3", "Red", "FF0000", "False", "0", "0", "0", "0"],
["4", "Ignored Blue", "0033B2", "False", "0", "0", "0", "0"],
],
)
colors = load_used_colors(parts_path, colors_path)
assert len(colors) == 3
colors = load_used_colors(parts_path, colors_path, parts_catalog_path)
assert len(colors) == 2
plot_colors_grid(parts_path, colors_path, destination_path)
plot_colors_grid(parts_path, colors_path, parts_catalog_path, destination_path)
assert destination_path.exists()
assert destination_path.stat().st_size > 0