1

111 lines
4.0 KiB
Python

"""Graphiques sur la taille moyenne des sets (pièces par set)."""
from pathlib import Path
from typing import Dict, Iterable, List, Tuple
import matplotlib.pyplot as plt
from lib.filesystem import ensure_parent_dir
from lib.milestones import load_milestones
from lib.rebrickable.stats import read_rows
def compute_average_parts_per_set(rows: Iterable[dict]) -> List[Tuple[int, float]]:
"""Calcule la moyenne annuelle de pièces par set."""
per_year: Dict[int, Dict[str, int]] = {}
for row in rows:
year = int(row["year"])
per_year[year] = per_year.get(year, {"parts": 0, "sets": 0})
per_year[year]["parts"] += int(row["num_parts"])
per_year[year]["sets"] += 1
results: List[Tuple[int, float]] = []
for year in sorted(per_year):
totals = per_year[year]
results.append((year, totals["parts"] / totals["sets"]))
return results
def compute_rolling_mean(series: List[Tuple[int, float]], window: int) -> List[Tuple[int, float]]:
"""Calcule la moyenne glissante sur une fenêtre donnée."""
values = [value for _, value in series]
years = [year for year, _ in series]
rolling: List[Tuple[int, float]] = []
for index in range(len(values)):
if index + 1 < window:
rolling.append((years[index], 0.0))
else:
window_values = values[index - window + 1 : index + 1]
rolling.append((years[index], sum(window_values) / window))
return rolling
def plot_parts_per_set(
enriched_sets_path: Path,
milestones_path: Path,
destination_path: Path,
rolling_window: int = 3,
) -> None:
"""Génère un graphique de la moyenne annuelle et glissante des pièces par set."""
sets_rows = read_rows(enriched_sets_path)
milestones = load_milestones(milestones_path)
annual_series = compute_average_parts_per_set(sets_rows)
rolling_series = compute_rolling_mean(annual_series, rolling_window)
years = [year for year, _ in annual_series]
annual_values = [value for _, value in annual_series]
rolling_values = [value for _, value in rolling_series]
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(years, annual_values, marker="o", color="#2ca02c", label="Moyenne annuelle (pièces/set)")
ax.plot(
years,
rolling_values,
marker="^",
color="#9467bd",
label=f"Moyenne glissante {rolling_window} ans (pièces/set)",
)
ax.set_xlabel("Année")
ax.set_ylabel("Pièces par set")
ax.set_title("Évolution de la taille moyenne des sets (thèmes filtrés)")
ax.grid(True, linestyle="--", alpha=0.3)
ax.set_xlim(min(years) - 0.4, max(years) + 0.4)
ax.set_xticks(list(range(min(years), max(years) + 1)))
ax.tick_params(axis="x", labelrotation=45)
peak = max(max(annual_values), max(rolling_values))
top_limit = peak * 2
milestones_in_range = sorted(
[m for m in milestones if min(years) <= m["year"] <= max(years)],
key=lambda m: (m["year"], m["description"]),
)
milestone_offsets: Dict[int, int] = {}
offset_step = 0.4
max_offset = 0
for milestone in milestones_in_range:
year = milestone["year"]
count_for_year = milestone_offsets.get(year, 0)
milestone_offsets[year] = count_for_year + 1
horizontal_offset = offset_step * (count_for_year // 2 + 1)
max_offset = max(max_offset, count_for_year)
if count_for_year % 2 == 1:
horizontal_offset *= -1
text_x = year + horizontal_offset
ax.axvline(year, color="#d62728", linestyle="--", linewidth=1, alpha=0.65)
ax.text(
text_x,
top_limit,
milestone["description"],
rotation=90,
verticalalignment="top",
horizontalalignment="center",
fontsize=8,
color="#d62728",
)
ax.set_ylim(0, top_limit * (1 + max_offset * 0.02))
ax.legend(loc="upper left", bbox_to_anchor=(1.12, 1))
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=150)
plt.close(fig)