Ajoute le graphique du nombre de minifigs par set
This commit is contained in:
parent
5b1a94023b
commit
03d69ff6c8
10
README.md
10
README.md
@ -223,3 +223,13 @@ Cette étape se lance après le téléchargement des données d'inventaire (éta
|
|||||||
2. `python -m scripts.compute_minifigs_by_set`
|
2. `python -m scripts.compute_minifigs_by_set`
|
||||||
|
|
||||||
Le script lit l'inventaire agrégé `data/intermediate/parts_filtered.csv` ainsi que le catalogue des pièces (`data/raw/parts.csv`). Il sélectionne les têtes de minifigs (catégorie 59), ignore les rechanges et dédoublonne par set et référence. Le CSV `data/intermediate/minifigs_by_set.csv` contient une ligne par set et par référence de tête : `set_num`, `part_num`, `part_name`.
|
Le script lit l'inventaire agrégé `data/intermediate/parts_filtered.csv` ainsi que le catalogue des pièces (`data/raw/parts.csv`). Il sélectionne les têtes de minifigs (catégorie 59), ignore les rechanges et dédoublonne par set et référence. Le CSV `data/intermediate/minifigs_by_set.csv` contient une ligne par set et par référence de tête : `set_num`, `part_num`, `part_name`.
|
||||||
|
|
||||||
|
### Étape 21 : visualiser le nombre de minifigs par set
|
||||||
|
|
||||||
|
1. `source .venv/bin/activate`
|
||||||
|
2. `python -m scripts.plot_minifigs_per_set`
|
||||||
|
|
||||||
|
Le script relit `data/intermediate/sets_enriched.csv`, `data/intermediate/parts_filtered.csv` et `data/raw/parts.csv`, compte les têtes de minifigs hors rechanges et produit deux sorties :
|
||||||
|
|
||||||
|
- `data/intermediate/minifig_counts_by_set.csv` : `set_num`, `set_id`, `name`, `year`, `minifig_count`
|
||||||
|
- `figures/step20/minifigs_per_set.png` : diagramme en barres horizontales (ordre décroissant) du nombre de minifigs par set filtré
|
||||||
|
|||||||
42
lib/plots/minifig_counts.py
Normal file
42
lib/plots/minifig_counts.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Graphique du nombre de minifigs par set."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from lib.filesystem import ensure_parent_dir
|
||||||
|
from lib.rebrickable.stats import read_rows
|
||||||
|
|
||||||
|
|
||||||
|
def load_counts(path: Path) -> List[dict]:
|
||||||
|
"""Charge le CSV des comptes de minifigs par set."""
|
||||||
|
return read_rows(path)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_minifigs_per_set(counts_path: Path, destination_path: Path) -> None:
|
||||||
|
"""Trace un diagramme en barres du nombre de minifigs par set (thèmes filtrés)."""
|
||||||
|
rows = load_counts(counts_path)
|
||||||
|
labels = [f"{row['set_num']} - {row['name']}" for row in rows]
|
||||||
|
values = [int(row["minifig_count"]) for row in rows]
|
||||||
|
positions = list(range(len(rows)))
|
||||||
|
max_value = max(values)
|
||||||
|
|
||||||
|
height = max(6, len(rows) * 0.18)
|
||||||
|
fig, ax = plt.subplots(figsize=(14, height))
|
||||||
|
bars = ax.barh(positions, values, color="#1f77b4", edgecolor="#0d0d0d", linewidth=0.6)
|
||||||
|
ax.set_yticks(positions)
|
||||||
|
ax.set_yticklabels(labels)
|
||||||
|
ax.invert_yaxis()
|
||||||
|
ax.set_xlabel("Nombre de minifigs")
|
||||||
|
ax.set_title("Minifigs par set (thèmes filtrés)")
|
||||||
|
ax.set_xlim(0, max_value + 0.8)
|
||||||
|
ax.grid(True, axis="x", linestyle="--", alpha=0.25)
|
||||||
|
for index, bar in enumerate(bars):
|
||||||
|
value = values[index]
|
||||||
|
ax.text(value + 0.2, bar.get_y() + bar.get_height() / 2, str(value), va="center", fontsize=8)
|
||||||
|
|
||||||
|
ensure_parent_dir(destination_path)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(destination_path, dpi=160)
|
||||||
|
plt.close(fig)
|
||||||
71
lib/rebrickable/minifig_counts.py
Normal file
71
lib/rebrickable/minifig_counts.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""Comptage des minifigs par set filtré."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Iterable, List, Sequence, Set
|
||||||
|
|
||||||
|
from lib.filesystem import ensure_parent_dir
|
||||||
|
from lib.rebrickable.minifigs_by_set import load_parts_catalog, select_head_parts
|
||||||
|
from lib.rebrickable.stats import read_rows
|
||||||
|
|
||||||
|
|
||||||
|
def load_sets(path: Path) -> List[dict]:
|
||||||
|
"""Charge les sets enrichis depuis un CSV."""
|
||||||
|
return read_rows(path)
|
||||||
|
|
||||||
|
|
||||||
|
def load_parts_filtered(path: Path) -> List[dict]:
|
||||||
|
"""Charge parts_filtered.csv en mémoire."""
|
||||||
|
return read_rows(path)
|
||||||
|
|
||||||
|
|
||||||
|
def count_heads_by_set(
|
||||||
|
sets_rows: Iterable[dict],
|
||||||
|
parts_rows: Iterable[dict],
|
||||||
|
head_parts: Set[str],
|
||||||
|
) -> List[dict]:
|
||||||
|
"""Compte les têtes de minifigs présentes dans chaque set (hors rechanges)."""
|
||||||
|
counts: Dict[str, int] = {row["set_num"]: 0 for row in sets_rows}
|
||||||
|
for row in parts_rows:
|
||||||
|
if row["part_num"] not in head_parts:
|
||||||
|
continue
|
||||||
|
if row["is_spare"] == "true":
|
||||||
|
continue
|
||||||
|
counts[row["set_num"]] += int(row["quantity_in_set"])
|
||||||
|
results: List[dict] = []
|
||||||
|
for row in sets_rows:
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"set_num": row["set_num"],
|
||||||
|
"set_id": row["set_id"],
|
||||||
|
"name": row["name"],
|
||||||
|
"year": row["year"],
|
||||||
|
"minifig_count": counts[row["set_num"]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
results.sort(key=lambda r: (-r["minifig_count"], r["set_num"]))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def build_minifig_counts_by_set(
|
||||||
|
sets_path: Path,
|
||||||
|
parts_filtered_path: Path,
|
||||||
|
parts_catalog_path: Path,
|
||||||
|
) -> List[dict]:
|
||||||
|
"""Construit la liste des sets avec leur nombre de minifigs."""
|
||||||
|
sets_rows = load_sets(sets_path)
|
||||||
|
parts_rows = load_parts_filtered(parts_filtered_path)
|
||||||
|
catalog = load_parts_catalog(parts_catalog_path)
|
||||||
|
head_parts = select_head_parts(catalog)
|
||||||
|
return count_heads_by_set(sets_rows, parts_rows, head_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def write_minifig_counts(destination_path: Path, rows: Sequence[dict]) -> None:
|
||||||
|
"""Écrit le CSV listant le nombre de minifigs par set."""
|
||||||
|
ensure_parent_dir(destination_path)
|
||||||
|
fieldnames = ["set_num", "set_id", "name", "year", "minifig_count"]
|
||||||
|
with destination_path.open("w", newline="") as csv_file:
|
||||||
|
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow(row)
|
||||||
24
scripts/plot_minifigs_per_set.py
Normal file
24
scripts/plot_minifigs_per_set.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""Trace le nombre de minifigs par set filtré."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from lib.plots.minifig_counts import plot_minifigs_per_set
|
||||||
|
from lib.rebrickable.minifig_counts import build_minifig_counts_by_set, write_minifig_counts
|
||||||
|
|
||||||
|
|
||||||
|
SETS_PATH = Path("data/intermediate/sets_enriched.csv")
|
||||||
|
PARTS_FILTERED_PATH = Path("data/intermediate/parts_filtered.csv")
|
||||||
|
PARTS_CATALOG_PATH = Path("data/raw/parts.csv")
|
||||||
|
COUNTS_PATH = Path("data/intermediate/minifig_counts_by_set.csv")
|
||||||
|
DESTINATION_PATH = Path("figures/step20/minifigs_per_set.png")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Construit le CSV de comptage des minifigs et trace le graphique associé."""
|
||||||
|
counts = build_minifig_counts_by_set(SETS_PATH, PARTS_FILTERED_PATH, PARTS_CATALOG_PATH)
|
||||||
|
write_minifig_counts(COUNTS_PATH, counts)
|
||||||
|
plot_minifigs_per_set(COUNTS_PATH, DESTINATION_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
47
tests/test_minifig_counts.py
Normal file
47
tests/test_minifig_counts.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Tests du comptage de minifigs par set."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from lib.rebrickable.minifig_counts import build_minifig_counts_by_set
|
||||||
|
|
||||||
|
|
||||||
|
def write_csv(path: Path, content: str) -> None:
|
||||||
|
"""Écrit un CSV brut."""
|
||||||
|
path.write_text(content)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_minifig_counts_by_set_counts_heads_without_spares(tmp_path: Path) -> None:
|
||||||
|
"""Compte les têtes hors rechanges pour chaque set et conserve les sets à 0."""
|
||||||
|
sets_path = tmp_path / "sets_enriched.csv"
|
||||||
|
write_csv(
|
||||||
|
sets_path,
|
||||||
|
"set_num,name,year,set_id\n"
|
||||||
|
"123-1,Set A,2020,123\n"
|
||||||
|
"124-1,Set B,2021,124\n"
|
||||||
|
"125-1,Set C,2022,125\n",
|
||||||
|
)
|
||||||
|
parts_filtered_path = tmp_path / "parts_filtered.csv"
|
||||||
|
write_csv(
|
||||||
|
parts_filtered_path,
|
||||||
|
"part_num,color_rgb,is_translucent,set_num,set_id,year,quantity_in_set,is_spare,is_minifig_part\n"
|
||||||
|
"head-a,ffffff,false,123-1,123,2020,1,false,true\n"
|
||||||
|
"head-a,ffffff,false,123-1,123,2020,2,true,true\n"
|
||||||
|
"head-b,ffffff,false,124-1,124,2021,3,false,true\n"
|
||||||
|
"other,000000,false,124-1,124,2021,1,false,false\n",
|
||||||
|
)
|
||||||
|
parts_catalog_path = tmp_path / "parts.csv"
|
||||||
|
write_csv(
|
||||||
|
parts_catalog_path,
|
||||||
|
"part_num,name,part_cat_id\n"
|
||||||
|
"head-a,Head A,59\n"
|
||||||
|
"head-b,Head B,59\n"
|
||||||
|
"other,Other,1\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
counts = build_minifig_counts_by_set(sets_path, parts_filtered_path, parts_catalog_path)
|
||||||
|
|
||||||
|
assert counts == [
|
||||||
|
{"set_num": "124-1", "set_id": "124", "name": "Set B", "year": "2021", "minifig_count": 3},
|
||||||
|
{"set_num": "123-1", "set_id": "123", "name": "Set A", "year": "2020", "minifig_count": 1},
|
||||||
|
{"set_num": "125-1", "set_id": "125", "name": "Set C", "year": "2022", "minifig_count": 0},
|
||||||
|
]
|
||||||
25
tests/test_minifig_counts_plot.py
Normal file
25
tests/test_minifig_counts_plot.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""Tests du graphique des minifigs par set."""
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from lib.plots.minifig_counts import plot_minifigs_per_set
|
||||||
|
|
||||||
|
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
|
||||||
|
|
||||||
|
def test_plot_minifigs_per_set_outputs_image(tmp_path: Path) -> None:
|
||||||
|
"""Génère l'image du nombre de minifigs par set."""
|
||||||
|
counts_path = tmp_path / "minifig_counts_by_set.csv"
|
||||||
|
destination_path = tmp_path / "figures" / "step20" / "minifigs_per_set.png"
|
||||||
|
counts_path.write_text(
|
||||||
|
"set_num,set_id,name,year,minifig_count\n"
|
||||||
|
"123-1,123,Set A,2020,2\n"
|
||||||
|
"124-1,124,Set B,2021,1\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
plot_minifigs_per_set(counts_path, destination_path)
|
||||||
|
|
||||||
|
assert destination_path.exists()
|
||||||
|
assert destination_path.stat().st_size > 0
|
||||||
Loading…
x
Reference in New Issue
Block a user