Reviens à l’état fbf20e2 sans palettes par set
This commit is contained in:
parent
909a1eae71
commit
c0700a8829
14
README.md
14
README.md
@ -272,17 +272,3 @@ Le script lit `data/intermediate/minifigs_by_set.csv`, agrège le nombre de mini
|
|||||||
|
|
||||||
Le script lit `data/intermediate/minifig_counts_by_set.csv`, `data/intermediate/sets_enriched.csv`, `data/raw/sets.csv`, `data/raw/inventories.csv` et `data/raw/inventory_minifigs.csv`, produit `data/intermediate/minifig_parts_correlation.csv` (pièces vs minifigs pour le catalogue global et les thèmes filtrés), puis trace `figures/step26/minifig_parts_correlation.png` en superposant les nuages de points et leurs tendances linéaires.
|
Le script lit `data/intermediate/minifig_counts_by_set.csv`, `data/intermediate/sets_enriched.csv`, `data/raw/sets.csv`, `data/raw/inventories.csv` et `data/raw/inventory_minifigs.csv`, produit `data/intermediate/minifig_parts_correlation.csv` (pièces vs minifigs pour le catalogue global et les thèmes filtrés), puis trace `figures/step26/minifig_parts_correlation.png` en superposant les nuages de points et leurs tendances linéaires.
|
||||||
Un second export `data/intermediate/minifigs_per_set_timeline.csv` est généré pour l'évolution annuelle du nombre moyen de minifigs par set, visualisé dans `figures/step26/minifigs_per_set_timeline.png` (courbes catalogue vs thèmes filtrés).
|
Un second export `data/intermediate/minifigs_per_set_timeline.csv` est généré pour l'évolution annuelle du nombre moyen de minifigs par set, visualisé dans `figures/step26/minifigs_per_set_timeline.png` (courbes catalogue vs thèmes filtrés).
|
||||||
|
|
||||||
### Étape 27 : palettes dominantes par set (hors minifigs)
|
|
||||||
|
|
||||||
1. `source .venv/bin/activate`
|
|
||||||
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 (hors minifigs, pièces techniques exclues)
|
|
||||||
|
|
||||||
1. `source .venv/bin/activate`
|
|
||||||
2. `python -m scripts.plot_set_color_swatches_perceptual`
|
|
||||||
|
|
||||||
Le script lit `data/intermediate/colors_by_set.csv` (filtres appliqués : couleurs ignorées et pièces techniques/structurelles exclues), calcule pour chaque set les parts relatives de couleurs hors minifigs, sélectionne une palette diversifiée de 5 couleurs (priorité à la variété de teinte avant la luminosité), écrit `data/intermediate/set_color_swatches_perceptual.csv`, puis trace `figures/step28/set_color_swatches_perceptual.png` (pastilles dont la taille reflète la part relative).
|
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
"""Palette dominante par set (hors minifigs)."""
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Sequence
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
from lib.filesystem import ensure_parent_dir
|
|
||||||
from lib.rebrickable.stats import read_rows
|
|
||||||
|
|
||||||
|
|
||||||
PLACEHOLDER_COLOR = "#e0e0e0"
|
|
||||||
|
|
||||||
|
|
||||||
def load_swatches(path: Path) -> List[dict]:
|
|
||||||
"""Charge le CSV des couleurs dominantes par set."""
|
|
||||||
return read_rows(path)
|
|
||||||
|
|
||||||
|
|
||||||
def group_swatches(rows: Sequence[dict], top_n: int = 5) -> List[dict]:
|
|
||||||
"""Groupe les couleurs par set et complète avec des placeholders si besoin."""
|
|
||||||
grouped: Dict[str, List[dict]] = defaultdict(list)
|
|
||||||
meta: Dict[str, dict] = {}
|
|
||||||
for row in rows:
|
|
||||||
grouped[row["set_num"]].append(row)
|
|
||||||
meta[row["set_num"]] = {"name": row["name"], "year": int(row["year"])}
|
|
||||||
result: List[dict] = []
|
|
||||||
for set_num, colors in grouped.items():
|
|
||||||
sorted_colors = sorted(colors, key=lambda r: int(r["rank"]))
|
|
||||||
while len(sorted_colors) < top_n:
|
|
||||||
sorted_colors.append(
|
|
||||||
{
|
|
||||||
"set_num": set_num,
|
|
||||||
"name": meta[set_num]["name"],
|
|
||||||
"year": str(meta[set_num]["year"]),
|
|
||||||
"rank": str(len(sorted_colors) + 1),
|
|
||||||
"color_rgb": "",
|
|
||||||
"color_name": "N/A",
|
|
||||||
"quantity_non_minifig": "0",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result.append(
|
|
||||||
{
|
|
||||||
"set_num": set_num,
|
|
||||||
"name": meta[set_num]["name"],
|
|
||||||
"year": meta[set_num]["year"],
|
|
||||||
"colors": sorted_colors[:top_n],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result.sort(key=lambda r: (r["year"], r["name"], r["set_num"]))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def plot_set_color_swatches(swatches_path: Path, destination_path: Path) -> None:
|
|
||||||
"""Trace la palette de 5 couleurs dominantes par set (hors minifigs)."""
|
|
||||||
rows = load_swatches(swatches_path)
|
|
||||||
if not rows:
|
|
||||||
return
|
|
||||||
grouped = group_swatches(rows, top_n=5)
|
|
||||||
set_labels = [f"{item['year']} – {item['name']}" for item in grouped]
|
|
||||||
y_positions = list(range(len(grouped)))
|
|
||||||
height = max(4, len(grouped) * 0.4)
|
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(12, height))
|
|
||||||
for y, item in zip(y_positions, grouped):
|
|
||||||
for idx, color in enumerate(item["colors"]):
|
|
||||||
rgb = color["color_rgb"].strip()
|
|
||||||
face_color = f"#{rgb}" if rgb else PLACEHOLDER_COLOR
|
|
||||||
ax.scatter(
|
|
||||||
idx,
|
|
||||||
y,
|
|
||||||
s=500,
|
|
||||||
color=face_color,
|
|
||||||
edgecolor="#0d0d0d",
|
|
||||||
linewidth=0.6,
|
|
||||||
)
|
|
||||||
ax.set_yticks(y_positions)
|
|
||||||
ax.set_yticklabels(set_labels)
|
|
||||||
ax.set_xticks([])
|
|
||||||
ax.invert_yaxis()
|
|
||||||
ax.set_xlim(-0.6, 4.6)
|
|
||||||
ax.set_title("Top 5 couleurs principales par set (hors minifigs)")
|
|
||||||
ax.grid(False)
|
|
||||||
|
|
||||||
ensure_parent_dir(destination_path)
|
|
||||||
fig.tight_layout()
|
|
||||||
fig.savefig(destination_path, dpi=160)
|
|
||||||
plt.close(fig)
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
"""Visualisation des palettes perceptuelles (top 5) par set."""
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Sequence
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
from lib.filesystem import ensure_parent_dir
|
|
||||||
from lib.rebrickable.stats import read_rows
|
|
||||||
|
|
||||||
|
|
||||||
PLACEHOLDER_COLOR = "#e0e0e0"
|
|
||||||
|
|
||||||
|
|
||||||
def load_swatches(path: Path) -> List[dict]:
|
|
||||||
"""Charge le CSV des palettes perceptuelles."""
|
|
||||||
return read_rows(path)
|
|
||||||
|
|
||||||
|
|
||||||
def group_swatches(rows: Sequence[dict], top_n: int = 5) -> List[dict]:
|
|
||||||
"""Groupe les couleurs par set et complète avec placeholders si besoin."""
|
|
||||||
grouped: Dict[str, List[dict]] = defaultdict(list)
|
|
||||||
meta: Dict[str, dict] = {}
|
|
||||||
for row in rows:
|
|
||||||
grouped[row["set_num"]].append(row)
|
|
||||||
meta[row["set_num"]] = {"name": row["name"], "year": int(row["year"])}
|
|
||||||
result: List[dict] = []
|
|
||||||
for set_num, colors in grouped.items():
|
|
||||||
sorted_colors = sorted(colors, key=lambda r: int(r["rank"]))
|
|
||||||
while len(sorted_colors) < top_n:
|
|
||||||
sorted_colors.append(
|
|
||||||
{
|
|
||||||
"set_num": set_num,
|
|
||||||
"name": meta[set_num]["name"],
|
|
||||||
"year": str(meta[set_num]["year"]),
|
|
||||||
"rank": str(len(sorted_colors) + 1),
|
|
||||||
"color_rgb": "",
|
|
||||||
"color_name": "N/A",
|
|
||||||
"share_non_minifig": "0",
|
|
||||||
"quantity_non_minifig": "0",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result.append(
|
|
||||||
{
|
|
||||||
"set_num": set_num,
|
|
||||||
"name": meta[set_num]["name"],
|
|
||||||
"year": meta[set_num]["year"],
|
|
||||||
"colors": sorted_colors[:top_n],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result.sort(key=lambda r: (r["year"], r["set_num"], r["name"]))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def plot_set_color_swatches_perceptual(swatches_path: Path, destination_path: Path) -> None:
|
|
||||||
"""Trace les 5 couleurs perceptuelles par set avec taille proportionnelle à la part."""
|
|
||||||
rows = load_swatches(swatches_path)
|
|
||||||
if not rows:
|
|
||||||
return
|
|
||||||
grouped = group_swatches(rows, top_n=5)
|
|
||||||
set_labels = [f"{item['year']} – {item['name']}" for item in grouped]
|
|
||||||
y_positions = list(range(len(grouped)))
|
|
||||||
height = max(4, len(grouped) * 0.4)
|
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(12, height))
|
|
||||||
for y, item in zip(y_positions, grouped):
|
|
||||||
for idx, color in enumerate(item["colors"]):
|
|
||||||
rgb = color["color_rgb"].strip()
|
|
||||||
face_color = f"#{rgb}" if rgb else PLACEHOLDER_COLOR
|
|
||||||
share = float(color.get("share_non_minifig", "0"))
|
|
||||||
size = 450 + 900 * share
|
|
||||||
ax.scatter(
|
|
||||||
idx,
|
|
||||||
y,
|
|
||||||
s=size,
|
|
||||||
color=face_color,
|
|
||||||
edgecolor="#0d0d0d",
|
|
||||||
linewidth=0.6,
|
|
||||||
alpha=0.95,
|
|
||||||
)
|
|
||||||
ax.set_yticks(y_positions)
|
|
||||||
ax.set_yticklabels(set_labels)
|
|
||||||
ax.set_xticks([])
|
|
||||||
ax.invert_yaxis()
|
|
||||||
ax.set_xlim(-0.6, 4.6)
|
|
||||||
ax.set_title("Top 5 couleurs perceptuelles par set (hors minifigs, pièces techniques exclues)")
|
|
||||||
ax.grid(False)
|
|
||||||
|
|
||||||
ensure_parent_dir(destination_path)
|
|
||||||
fig.tight_layout()
|
|
||||||
fig.savefig(destination_path, dpi=160)
|
|
||||||
plt.close(fig)
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
"""Préparation des palettes dominantes par set (hors minifigs)."""
|
|
||||||
|
|
||||||
import csv
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Iterable, List, Sequence
|
|
||||||
|
|
||||||
import colorsys
|
|
||||||
|
|
||||||
from lib.filesystem import ensure_parent_dir
|
|
||||||
from lib.rebrickable.color_ignores import is_ignored_color_rgb
|
|
||||||
from lib.rebrickable.stats import read_rows
|
|
||||||
|
|
||||||
|
|
||||||
def load_colors_by_set(path: Path) -> List[dict]:
|
|
||||||
"""Charge colors_by_set.csv."""
|
|
||||||
return read_rows(path)
|
|
||||||
|
|
||||||
|
|
||||||
def load_sets_enriched(path: Path) -> Dict[str, dict]:
|
|
||||||
"""Indexe nom et année par set_num."""
|
|
||||||
lookup: Dict[str, dict] = {}
|
|
||||||
with path.open() as csv_file:
|
|
||||||
reader = csv.DictReader(csv_file)
|
|
||||||
for row in reader:
|
|
||||||
lookup[row["set_num"]] = {"name": row["name"], "year": int(row["year"]), "set_id": row["set_id"]}
|
|
||||||
return lookup
|
|
||||||
|
|
||||||
|
|
||||||
def parse_rgb_hex(value: str) -> tuple[float, float, float]:
|
|
||||||
"""Parse un code hexadécimal (RRGGBB) en composantes 0-1."""
|
|
||||||
normalized = value.strip()
|
|
||||||
if len(normalized) != 6:
|
|
||||||
return (0.0, 0.0, 0.0)
|
|
||||||
r = int(normalized[0:2], 16) / 255.0
|
|
||||||
g = int(normalized[2:4], 16) / 255.0
|
|
||||||
b = int(normalized[4:6], 16) / 255.0
|
|
||||||
return (r, g, b)
|
|
||||||
|
|
||||||
|
|
||||||
def hue_bucket(degrees: float) -> int:
|
|
||||||
"""Regroupe les teintes en grandes familles pour l'affichage."""
|
|
||||||
if degrees >= 330 or degrees < 30:
|
|
||||||
return 0 # rouge
|
|
||||||
if degrees < 90:
|
|
||||||
return 1 # jaune / orangé
|
|
||||||
if degrees < 150:
|
|
||||||
return 2 # vert
|
|
||||||
if degrees < 270:
|
|
||||||
return 3 # bleu
|
|
||||||
return 4 # violet
|
|
||||||
|
|
||||||
|
|
||||||
def color_display_key(row: dict) -> tuple[float, int, float, str]:
|
|
||||||
"""Clé de tri visuelle : teinte regroupée d'abord, puis luminosité."""
|
|
||||||
r, g, b = parse_rgb_hex(row["color_rgb"])
|
|
||||||
h, _s, v = colorsys.rgb_to_hsv(r, g, b)
|
|
||||||
degrees = h * 360.0
|
|
||||||
bucket = hue_bucket(degrees)
|
|
||||||
return (bucket, degrees, v, row["color_name"])
|
|
||||||
|
|
||||||
|
|
||||||
def build_top_colors_by_set(rows: Iterable[dict], sets_lookup: Dict[str, dict], top_n: int = 5) -> List[dict]:
|
|
||||||
"""Sélectionne les top couleurs hors minifigs pour chaque set."""
|
|
||||||
colors_by_set: Dict[str, List[dict]] = defaultdict(list)
|
|
||||||
for row in rows:
|
|
||||||
quantity = int(row["quantity_non_minifig"])
|
|
||||||
if quantity <= 0:
|
|
||||||
continue
|
|
||||||
if is_ignored_color_rgb(row["color_rgb"]):
|
|
||||||
continue
|
|
||||||
set_num = row["set_num"]
|
|
||||||
set_meta = sets_lookup.get(set_num)
|
|
||||||
if set_meta is None:
|
|
||||||
continue
|
|
||||||
colors_by_set[set_num].append(
|
|
||||||
{
|
|
||||||
"set_num": set_num,
|
|
||||||
"set_id": row["set_id"],
|
|
||||||
"year": set_meta["year"],
|
|
||||||
"name": set_meta["name"],
|
|
||||||
"color_rgb": row["color_rgb"],
|
|
||||||
"color_name": row["color_name"],
|
|
||||||
"quantity": quantity,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
results: List[dict] = []
|
|
||||||
for set_num, color_rows in colors_by_set.items():
|
|
||||||
sorted_rows = sorted(color_rows, key=lambda r: (-r["quantity"], r["color_name"]))
|
|
||||||
selected = sorted_rows[:top_n]
|
|
||||||
ordered = sorted(selected, key=color_display_key)
|
|
||||||
for rank, color_row in enumerate(ordered, start=1):
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"set_num": color_row["set_num"],
|
|
||||||
"set_id": color_row["set_id"],
|
|
||||||
"name": color_row["name"],
|
|
||||||
"year": str(color_row["year"]),
|
|
||||||
"rank": str(rank),
|
|
||||||
"color_rgb": color_row["color_rgb"],
|
|
||||||
"color_name": color_row["color_name"],
|
|
||||||
"quantity_non_minifig": str(color_row["quantity"]),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
results.sort(key=lambda r: (int(r["year"]), r["set_num"], r["name"], int(r["rank"])))
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def write_top_colors(path: Path, rows: Sequence[dict]) -> None:
|
|
||||||
"""Écrit le CSV des couleurs dominantes par set."""
|
|
||||||
ensure_parent_dir(path)
|
|
||||||
fieldnames = [
|
|
||||||
"set_num",
|
|
||||||
"set_id",
|
|
||||||
"name",
|
|
||||||
"year",
|
|
||||||
"rank",
|
|
||||||
"color_rgb",
|
|
||||||
"color_name",
|
|
||||||
"quantity_non_minifig",
|
|
||||||
]
|
|
||||||
with path.open("w", newline="") as csv_file:
|
|
||||||
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
|
||||||
writer.writeheader()
|
|
||||||
for row in rows:
|
|
||||||
writer.writerow(row)
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
"""Construction de palettes perceptuelles (top 5) par set hors minifigs."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Iterable, List, Sequence, Set
|
|
||||||
|
|
||||||
from lib.rebrickable.set_color_swatches import color_display_key, load_sets_enriched, parse_rgb_hex, hue_bucket
|
|
||||||
from lib.rebrickable.stats import read_rows
|
|
||||||
|
|
||||||
|
|
||||||
def load_colors_by_set(path: Path) -> List[dict]:
|
|
||||||
"""Charge colors_by_set.csv."""
|
|
||||||
return read_rows(path)
|
|
||||||
|
|
||||||
|
|
||||||
def compute_shares(rows: Iterable[dict]) -> Dict[str, List[dict]]:
|
|
||||||
"""Calcule les parts relatives de couleurs hors minifigs pour chaque set."""
|
|
||||||
by_set: Dict[str, List[dict]] = {}
|
|
||||||
totals: Dict[str, int] = {}
|
|
||||||
for row in rows:
|
|
||||||
quantity = int(row["quantity_non_minifig"])
|
|
||||||
if quantity <= 0:
|
|
||||||
continue
|
|
||||||
set_num = row["set_num"]
|
|
||||||
totals[set_num] = totals.get(set_num, 0) + quantity
|
|
||||||
current = by_set.get(set_num)
|
|
||||||
if current is None:
|
|
||||||
by_set[set_num] = [row]
|
|
||||||
else:
|
|
||||||
current.append(row)
|
|
||||||
shares: Dict[str, List[dict]] = {}
|
|
||||||
for set_num, color_rows in by_set.items():
|
|
||||||
total = totals.get(set_num, 0)
|
|
||||||
if total == 0:
|
|
||||||
continue
|
|
||||||
shares[set_num] = []
|
|
||||||
for row in color_rows:
|
|
||||||
share = int(row["quantity_non_minifig"]) / total
|
|
||||||
shares[set_num].append(
|
|
||||||
{
|
|
||||||
"set_num": row["set_num"],
|
|
||||||
"set_id": row["set_id"],
|
|
||||||
"name": row.get("name", ""),
|
|
||||||
"year": row["year"],
|
|
||||||
"color_rgb": row["color_rgb"],
|
|
||||||
"color_name": row["color_name"],
|
|
||||||
"quantity_non_minifig": row["quantity_non_minifig"],
|
|
||||||
"share_non_minifig": f"{share:.5f}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return shares
|
|
||||||
|
|
||||||
|
|
||||||
def select_diverse_palette(rows: List[dict], top_n: int) -> List[dict]:
|
|
||||||
"""Sélectionne une palette diversifiée : priorité à la part et à la variété de teinte."""
|
|
||||||
sorted_by_share = sorted(rows, key=lambda r: (-float(r["share_non_minifig"]), r["color_name"]))
|
|
||||||
selected: List[dict] = []
|
|
||||||
buckets_used: Set[int] = set()
|
|
||||||
for row in sorted_by_share:
|
|
||||||
r, g, b = parse_rgb_hex(row["color_rgb"])
|
|
||||||
h, _s, _v = __import__("colorsys").rgb_to_hsv(r, g, b)
|
|
||||||
bucket = hue_bucket(h * 360.0)
|
|
||||||
if bucket in buckets_used:
|
|
||||||
continue
|
|
||||||
selected.append(row)
|
|
||||||
buckets_used.add(bucket)
|
|
||||||
if len(selected) == top_n:
|
|
||||||
break
|
|
||||||
if len(selected) < top_n:
|
|
||||||
for row in sorted_by_share:
|
|
||||||
if row in selected:
|
|
||||||
continue
|
|
||||||
selected.append(row)
|
|
||||||
if len(selected) == top_n:
|
|
||||||
break
|
|
||||||
while len(selected) < top_n:
|
|
||||||
selected.append(
|
|
||||||
{
|
|
||||||
"set_num": rows[0]["set_num"] if rows else "",
|
|
||||||
"set_id": rows[0]["set_id"] if rows else "",
|
|
||||||
"name": rows[0]["name"] if rows else "",
|
|
||||||
"year": rows[0]["year"] if rows else "",
|
|
||||||
"color_rgb": "",
|
|
||||||
"color_name": "N/A",
|
|
||||||
"quantity_non_minifig": "0",
|
|
||||||
"share_non_minifig": "0",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ordered = sorted(selected, key=color_display_key)
|
|
||||||
for rank, row in enumerate(ordered, start=1):
|
|
||||||
row["rank"] = str(rank)
|
|
||||||
return ordered[:top_n]
|
|
||||||
|
|
||||||
|
|
||||||
def build_perceptual_swatches(rows: Iterable[dict], sets_lookup: Dict[str, dict], top_n: int = 5) -> List[dict]:
|
|
||||||
"""Construit les palettes perceptuelles (parts relatives + diversité de teinte)."""
|
|
||||||
shares = compute_shares(rows)
|
|
||||||
swatches: List[dict] = []
|
|
||||||
for set_num, color_rows in shares.items():
|
|
||||||
set_meta = sets_lookup.get(set_num)
|
|
||||||
if set_meta is None:
|
|
||||||
continue
|
|
||||||
selected = select_diverse_palette(color_rows, top_n)
|
|
||||||
for row in selected:
|
|
||||||
swatches.append(
|
|
||||||
{
|
|
||||||
"set_num": set_num,
|
|
||||||
"set_id": set_meta["set_id"],
|
|
||||||
"name": set_meta["name"],
|
|
||||||
"year": str(set_meta["year"]),
|
|
||||||
"rank": row["rank"],
|
|
||||||
"color_rgb": row["color_rgb"],
|
|
||||||
"color_name": row["color_name"],
|
|
||||||
"share_non_minifig": row["share_non_minifig"],
|
|
||||||
"quantity_non_minifig": row["quantity_non_minifig"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
swatches.sort(key=lambda r: (int(r["year"]), r["set_num"], int(r["rank"])))
|
|
||||||
return swatches
|
|
||||||
|
|
||||||
|
|
||||||
def write_perceptual_swatches(path: Path, rows: Sequence[dict]) -> None:
|
|
||||||
"""Écrit le CSV des palettes perceptuelles."""
|
|
||||||
from lib.filesystem import ensure_parent_dir
|
|
||||||
|
|
||||||
ensure_parent_dir(path)
|
|
||||||
fieldnames = [
|
|
||||||
"set_num",
|
|
||||||
"set_id",
|
|
||||||
"name",
|
|
||||||
"year",
|
|
||||||
"rank",
|
|
||||||
"color_rgb",
|
|
||||||
"color_name",
|
|
||||||
"share_non_minifig",
|
|
||||||
"quantity_non_minifig",
|
|
||||||
]
|
|
||||||
with path.open("w", newline="") as csv_file:
|
|
||||||
import csv
|
|
||||||
|
|
||||||
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
|
||||||
writer.writeheader()
|
|
||||||
for row in rows:
|
|
||||||
writer.writerow(row)
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
"""Trace la palette dominante de chaque set (hors minifigs)."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from lib.plots.set_color_swatches import plot_set_color_swatches
|
|
||||||
from lib.rebrickable.set_color_swatches import build_top_colors_by_set, load_colors_by_set, load_sets_enriched, write_top_colors
|
|
||||||
|
|
||||||
|
|
||||||
COLORS_BY_SET_PATH = Path("data/intermediate/colors_by_set.csv")
|
|
||||||
SETS_ENRICHED_PATH = Path("data/intermediate/sets_enriched.csv")
|
|
||||||
SWATCHES_PATH = Path("data/intermediate/set_color_swatches.csv")
|
|
||||||
DESTINATION_PATH = Path("figures/step27/set_color_swatches.png")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Construit le CSV de top couleurs par set et trace le nuancier."""
|
|
||||||
colors_rows = load_colors_by_set(COLORS_BY_SET_PATH)
|
|
||||||
sets_lookup = load_sets_enriched(SETS_ENRICHED_PATH)
|
|
||||||
swatches = build_top_colors_by_set(colors_rows, sets_lookup, top_n=5)
|
|
||||||
write_top_colors(SWATCHES_PATH, swatches)
|
|
||||||
plot_set_color_swatches(SWATCHES_PATH, DESTINATION_PATH)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
"""Trace les palettes perceptuelles (top 5) par set hors minifigs."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from lib.plots.set_color_swatches_perceptual import plot_set_color_swatches_perceptual
|
|
||||||
from lib.rebrickable.set_color_swatches_perceptual import (
|
|
||||||
build_perceptual_swatches,
|
|
||||||
load_colors_by_set,
|
|
||||||
load_sets_enriched,
|
|
||||||
write_perceptual_swatches,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
COLORS_BY_SET_PATH = Path("data/intermediate/colors_by_set.csv")
|
|
||||||
SETS_ENRICHED_PATH = Path("data/intermediate/sets_enriched.csv")
|
|
||||||
SWATCHES_PATH = Path("data/intermediate/set_color_swatches_perceptual.csv")
|
|
||||||
DESTINATION_PATH = Path("figures/step28/set_color_swatches_perceptual.png")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Construit et trace les palettes perceptuelles par set."""
|
|
||||||
colors_rows = load_colors_by_set(COLORS_BY_SET_PATH)
|
|
||||||
sets_lookup = load_sets_enriched(SETS_ENRICHED_PATH)
|
|
||||||
swatches = build_perceptual_swatches(colors_rows, sets_lookup, top_n=5)
|
|
||||||
write_perceptual_swatches(SWATCHES_PATH, swatches)
|
|
||||||
plot_set_color_swatches_perceptual(SWATCHES_PATH, DESTINATION_PATH)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
"""Tests de la préparation des palettes par set."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from lib.rebrickable.set_color_swatches import build_top_colors_by_set
|
|
||||||
|
|
||||||
|
|
||||||
def write_csv(path: Path, content: str) -> None:
|
|
||||||
"""Écrit un CSV brut."""
|
|
||||||
path.write_text(content)
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_top_colors_by_set_selects_top5_non_minifig(tmp_path: Path) -> None:
|
|
||||||
"""Sélectionne les 5 couleurs dominantes en excluant les minifigs."""
|
|
||||||
colors_path = tmp_path / "colors_by_set.csv"
|
|
||||||
write_csv(
|
|
||||||
colors_path,
|
|
||||||
"set_num,set_id,year,color_rgb,is_translucent,color_name,quantity_total,quantity_non_spare,quantity_minifig,quantity_non_minifig\n"
|
|
||||||
"123-1,123,2020,111111,false,Black,10,10,0,10\n"
|
|
||||||
"123-1,123,2020,222222,false,Red,5,5,0,5\n"
|
|
||||||
"123-1,123,2020,333333,false,Blue,3,3,0,3\n"
|
|
||||||
"123-1,123,2020,444444,false,Green,2,2,0,2\n"
|
|
||||||
"123-1,123,2020,555555,false,Yellow,1,1,0,1\n"
|
|
||||||
"123-1,123,2020,666666,false,Pink,1,1,0,1\n"
|
|
||||||
"123-1,123,2020,0033B2,false,Ignored Blue,50,50,0,50\n"
|
|
||||||
"124-1,124,2021,aaaaaa,false,Gray,4,4,4,0\n",
|
|
||||||
)
|
|
||||||
sets_path = tmp_path / "sets_enriched.csv"
|
|
||||||
write_csv(
|
|
||||||
sets_path,
|
|
||||||
"set_num,name,year,theme_id,num_parts,img_url,set_id,rebrickable_url,in_collection\n"
|
|
||||||
"123-1,Set A,2020,1,100,,123,,false\n"
|
|
||||||
"124-1,Set B,2021,1,50,,124,,false\n",
|
|
||||||
)
|
|
||||||
rows = build_top_colors_by_set(
|
|
||||||
[
|
|
||||||
row
|
|
||||||
for row in [
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "111111",
|
|
||||||
"color_name": "Black",
|
|
||||||
"quantity_non_minifig": "10",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "222222",
|
|
||||||
"color_name": "Red",
|
|
||||||
"quantity_non_minifig": "5",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "333333",
|
|
||||||
"color_name": "Blue",
|
|
||||||
"quantity_non_minifig": "3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "444444",
|
|
||||||
"color_name": "Green",
|
|
||||||
"quantity_non_minifig": "2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "555555",
|
|
||||||
"color_name": "Yellow",
|
|
||||||
"quantity_non_minifig": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "666666",
|
|
||||||
"color_name": "Pink",
|
|
||||||
"quantity_non_minifig": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "124-1",
|
|
||||||
"set_id": "124",
|
|
||||||
"year": "2021",
|
|
||||||
"color_rgb": "aaaaaa",
|
|
||||||
"color_name": "Gray",
|
|
||||||
"quantity_non_minifig": "0",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"123-1": {"name": "Set A", "year": 2020, "set_id": "123"},
|
|
||||||
"124-1": {"name": "Set B", "year": 2021, "set_id": "124"},
|
|
||||||
},
|
|
||||||
top_n=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert rows == [
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"name": "Set A",
|
|
||||||
"year": "2020",
|
|
||||||
"rank": "1",
|
|
||||||
"color_rgb": "111111",
|
|
||||||
"color_name": "Black",
|
|
||||||
"quantity_non_minifig": "10",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"name": "Set A",
|
|
||||||
"year": "2020",
|
|
||||||
"rank": "2",
|
|
||||||
"color_rgb": "222222",
|
|
||||||
"color_name": "Red",
|
|
||||||
"quantity_non_minifig": "5",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"name": "Set A",
|
|
||||||
"year": "2020",
|
|
||||||
"rank": "3",
|
|
||||||
"color_rgb": "333333",
|
|
||||||
"color_name": "Blue",
|
|
||||||
"quantity_non_minifig": "3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"name": "Set A",
|
|
||||||
"year": "2020",
|
|
||||||
"rank": "4",
|
|
||||||
"color_rgb": "444444",
|
|
||||||
"color_name": "Green",
|
|
||||||
"quantity_non_minifig": "2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"name": "Set A",
|
|
||||||
"year": "2020",
|
|
||||||
"rank": "5",
|
|
||||||
"color_rgb": "666666",
|
|
||||||
"color_name": "Pink",
|
|
||||||
"quantity_non_minifig": "1",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
"""Tests des palettes perceptuelles par set."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from lib.rebrickable.set_color_swatches_perceptual import build_perceptual_swatches
|
|
||||||
|
|
||||||
|
|
||||||
def write_csv(path: Path, content: str) -> None:
|
|
||||||
"""Écrit un CSV brut."""
|
|
||||||
path.write_text(content)
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_perceptual_swatches_diversifies_buckets(tmp_path: Path) -> None:
|
|
||||||
"""Sélectionne des couleurs variées par teinte en priorité."""
|
|
||||||
colors_path = tmp_path / "colors_by_set.csv"
|
|
||||||
write_csv(
|
|
||||||
colors_path,
|
|
||||||
"set_num,set_id,year,color_rgb,is_translucent,color_name,quantity_total,quantity_non_spare,quantity_minifig,quantity_non_minifig\n"
|
|
||||||
"123-1,123,2020,FF0000,false,Red,10,10,0,10\n"
|
|
||||||
"123-1,123,2020,00FF00,false,Green,8,8,0,8\n"
|
|
||||||
"123-1,123,2020,0000FF,false,Blue,6,6,0,6\n"
|
|
||||||
"123-1,123,2020,FFFF00,false,Yellow,5,5,0,5\n"
|
|
||||||
"123-1,123,2020,FF00FF,false,Magenta,4,4,0,4\n"
|
|
||||||
"123-1,123,2020,00FFFF,false,Cyan,3,3,0,3\n",
|
|
||||||
)
|
|
||||||
sets_lookup = {"123-1": {"name": "Set A", "year": 2020, "set_id": "123"}}
|
|
||||||
rows = build_perceptual_swatches(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "FF0000",
|
|
||||||
"color_name": "Red",
|
|
||||||
"quantity_non_minifig": "10",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "00FF00",
|
|
||||||
"color_name": "Green",
|
|
||||||
"quantity_non_minifig": "8",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "0000FF",
|
|
||||||
"color_name": "Blue",
|
|
||||||
"quantity_non_minifig": "6",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "FFFF00",
|
|
||||||
"color_name": "Yellow",
|
|
||||||
"quantity_non_minifig": "5",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "FF00FF",
|
|
||||||
"color_name": "Magenta",
|
|
||||||
"quantity_non_minifig": "4",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_num": "123-1",
|
|
||||||
"set_id": "123",
|
|
||||||
"year": "2020",
|
|
||||||
"color_rgb": "00FFFF",
|
|
||||||
"color_name": "Cyan",
|
|
||||||
"quantity_non_minifig": "3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sets_lookup,
|
|
||||||
top_n=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
ranks = [row["rank"] for row in rows if row["set_num"] == "123-1"]
|
|
||||||
assert ranks == ["1", "2", "3", "4", "5"]
|
|
||||||
assert len({row["color_name"] for row in rows}) == 5
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
"""Tests du graphique de palettes perceptuelles par set."""
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from lib.plots.set_color_swatches_perceptual import plot_set_color_swatches_perceptual
|
|
||||||
|
|
||||||
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
|
|
||||||
|
|
||||||
def test_plot_set_color_swatches_perceptual(tmp_path: Path) -> None:
|
|
||||||
"""Génère le graphique perceptuel."""
|
|
||||||
swatches_path = tmp_path / "set_color_swatches_perceptual.csv"
|
|
||||||
destination = tmp_path / "figures" / "step28" / "set_color_swatches_perceptual.png"
|
|
||||||
swatches_path.write_text(
|
|
||||||
"set_num,set_id,name,year,rank,color_rgb,color_name,share_non_minifig,quantity_non_minifig\n"
|
|
||||||
"123-1,123,Set A,2020,1,FF0000,Red,0.40000,10\n"
|
|
||||||
"123-1,123,Set A,2020,2,00FF00,Green,0.30000,8\n"
|
|
||||||
"123-1,123,Set A,2020,3,0000FF,Blue,0.20000,6\n"
|
|
||||||
"123-1,123,Set A,2020,4,FFFF00,Yellow,0.10000,5\n"
|
|
||||||
"123-1,123,Set A,2020,5,00FFFF,Cyan,0.05000,3\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
plot_set_color_swatches_perceptual(swatches_path, destination)
|
|
||||||
|
|
||||||
assert destination.exists()
|
|
||||||
assert destination.stat().st_size > 0
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
"""Tests du graphique de palettes dominantes par set."""
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from lib.plots.set_color_swatches import plot_set_color_swatches
|
|
||||||
|
|
||||||
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
|
|
||||||
|
|
||||||
def test_plot_set_color_swatches(tmp_path: Path) -> None:
|
|
||||||
"""Génère le nuancier top 5 par set."""
|
|
||||||
swatches_path = tmp_path / "set_color_swatches.csv"
|
|
||||||
destination = tmp_path / "figures" / "step27" / "set_color_swatches.png"
|
|
||||||
swatches_path.write_text(
|
|
||||||
"set_num,set_id,name,year,rank,color_rgb,color_name,quantity_non_minifig\n"
|
|
||||||
"123-1,123,Set A,2020,1,111111,Black,10\n"
|
|
||||||
"123-1,123,Set A,2020,2,222222,Red,5\n"
|
|
||||||
"123-1,123,Set A,2020,3,333333,Blue,3\n"
|
|
||||||
"123-1,123,Set A,2020,4,444444,Green,2\n"
|
|
||||||
"123-1,123,Set A,2020,5,555555,Yellow,1\n"
|
|
||||||
"124-1,124,Set B,2021,1,aaaaaa,Gray,4\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
plot_set_color_swatches(swatches_path, destination)
|
|
||||||
|
|
||||||
assert destination.exists()
|
|
||||||
assert destination.stat().st_size > 0
|
|
||||||
Loading…
x
Reference in New Issue
Block a user