1

Premiers éléments de l'étude

This commit is contained in:
2025-12-01 21:57:05 +01:00
commit 22b4dae0ba
46 changed files with 2595 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
"""Calculs de statistiques simples sur les pièces filtrées."""
import csv
from collections import defaultdict
from pathlib import Path
from typing import Dict, Iterable, List, Sequence, Tuple
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.inventory_reconciliation import compute_inventory_gaps
from lib.rebrickable.stats import read_rows as read_stats_rows
def read_rows(path: Path) -> List[dict]:
"""Charge un fichier CSV en mémoire sous forme de dictionnaires."""
with path.open() as csv_file:
reader = csv.DictReader(csv_file)
return list(reader)
def select_non_spare_parts(rows: Iterable[dict]) -> List[dict]:
"""Filtre les pièces en excluant les rechanges."""
return [row for row in rows if row["is_spare"] == "false"]
def variation_key(row: dict) -> Tuple[str, str, str]:
"""Clé d'unicité pour une variation de pièce (référence + couleur)."""
return (row["part_num"], row["color_rgb"], row["is_translucent"])
def color_key(row: dict) -> Tuple[str, str]:
"""Clé d'unicité pour une couleur."""
return (row["color_rgb"], row["is_translucent"])
def aggregate_quantities_by_variation(rows: Iterable[dict]) -> Dict[Tuple[str, str, str], int]:
"""Calcule la quantité totale par variation de pièce (hors rechanges)."""
quantities: Dict[Tuple[str, str, str], int] = defaultdict(int)
for row in rows:
quantities[variation_key(row)] += int(row["quantity_in_set"])
return quantities
def read_total_filtered_parts(stats_path: Path) -> int:
"""Lit le total de pièces attendu pour les thèmes filtrés depuis stats.csv."""
rows = read_stats_rows(stats_path)
return int(
next(row["valeur"] for row in rows if row["libelle"] == "Total de pièces pour les thèmes filtrés")
)
def build_stats(
rows: Iterable[dict],
sets_path: Path,
parts_path: Path,
stats_path: Path,
) -> List[Tuple[str, str]]:
"""Construit les statistiques principales sur les pièces filtrées et les écarts d'inventaire."""
non_spares = select_non_spare_parts(rows)
quantities = aggregate_quantities_by_variation(non_spares)
total_variations = len(quantities)
color_set = {color_key(row) for row in non_spares}
least_used_key = min(quantities, key=quantities.get)
most_used_key = max(quantities, key=quantities.get)
least_used = quantities[least_used_key]
most_used = quantities[most_used_key]
total_non_spare = sum(quantities.values())
gaps = compute_inventory_gaps(sets_path, parts_path)
gap_count = len(gaps)
worst_gap = max(gaps, key=lambda gap: gap["delta"]) if gap_count > 0 else {"set_id": "none", "delta": 0}
catalog_total_parts = read_total_filtered_parts(stats_path)
catalog_inventory_delta = catalog_total_parts - total_non_spare
return [
("Total de variations de pièces (hors rechanges)", str(total_variations)),
(
"Pièce la moins utilisée (référence + couleur)",
f"{least_used_key[0]} / {least_used_key[1]} / {least_used_key[2]} ({least_used})",
),
(
"Pièce la plus commune (référence + couleur)",
f"{most_used_key[0]} / {most_used_key[1]} / {most_used_key[2]} ({most_used})",
),
("Total de couleurs utilisées (hors rechanges)", str(len(color_set))),
("Total de pièces hors rechanges", str(total_non_spare)),
(
"Ecart total catalogue (stats) - inventaire (hors rechanges)",
str(catalog_inventory_delta),
),
("Nombre de sets en écart inventaire/catalogue", str(gap_count)),
("Ecart maximal inventaire/catalogue", f"{worst_gap['set_id']} ({worst_gap['delta']})"),
]
def write_parts_stats(destination_path: Path, stats: Sequence[Tuple[str, str]]) -> None:
"""Écrit les statistiques dans un CSV à deux colonnes."""
ensure_parent_dir(destination_path)
with destination_path.open("w", newline="") as csv_file:
writer = csv.writer(csv_file)
writer.writerow(["libelle", "valeur"])
for label, value in stats:
writer.writerow([label, value])