spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Figure ch22 — métré de contrôle du sujet du cas 1 : écart relatif entre la quantité recalculée depuis le plan coté3et la quantité du devis, par poste mesuré ou dénombré, avec les bandes de tolérance (±5 % conforme, ±10 % à préciser).4DONNÉES SYNTHÉTIQUES, graine 20261031. Sortie : assets/figures/ch22/ch22-fig-ecarts-metre.pdf"""5import sys, pathlib6ROOT = pathlib.Path(__file__).resolve().parents[3]7sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))8import matplotlib.pyplot as plt9from matplotlib.lines import Line2D10from traite.style import setup, palette, savefig, LARGEUR11import ch21_cout_neuf as c2112import ch22_metre as cas1314COURT = {"Excavation et remblai": "Excavation et remblai (m³)", "Fondations béton (semelles, murs)": "Fondations béton (m³)",15 "Dalle de sous-sol": "Dalle de sous-sol (m²)", "Drain français et imperméabilisation": "Drain français (m lin.)",16 "Charpente murs et planchers": "Charpente murs et planchers (m²)", "Fermes de toit": "Fermes de toit (m²)",17 "Couverture bardeaux d'asphalte": "Couverture (m²)", "Revêtement extérieur (brique 40 % / fibrociment 60 %)": "Revêtement extérieur (m²)",18 "Isolation et pare-vapeur": "Isolation et pare-vapeur (m²)", "Fenêtres (PVC double vitrage)": "Fenêtres (unités)",19 "Portes extérieures": "Portes extérieures (unités)", "Porte de garage": "Porte de garage (unité)",20 "Gypse et tirage de joints": "Gypse (m²)", "Peinture intérieure": "Peinture (m²)", "Planchers bois franc": "Planchers bois franc (m²)",21 "Planchers céramique": "Planchers céramique (m²)", "Planchers flottants (sous-sol)": "Planchers flottants (m²)",22 "Escaliers": "Escaliers (unités)", "Portes intérieures et moulures": "Portes intérieures (unités)",23 "Plomberie brute et finition": "Plomberie (appareils)", "Gouttières et solins": "Gouttières et solins (m lin.)",24 "Balcon et perron": "Balcon et perron (m²)", "Entrée asphaltée": "Entrée asphaltée (m²)", "Supervision de chantier (salaires)": "Supervision (semaines)"}25COUL = {"conforme": palette.cat[0], "à préciser": palette.status["warning"], "à justifier": palette.status["critical"]}262728def main():29 setup()30 devis, _ = c21.charger()31 V = cas.verification(devis)32 V = V[V["classe"] != "forfait"].copy()33 V["ordre"] = V["corps"].map({c: i for i, c in enumerate(c21.ORDRE_CORPS)})34 V = V.sort_values(["ordre"], kind="stable").reset_index(drop=True)35 V = V.iloc[::-1].reset_index(drop=True) # premier poste en haut36 y = list(range(len(V)))37 fig, ax = plt.subplots(figsize=(LARGEUR, 4.6))38 ax.axvspan(-10, -5, color=palette.status["warning"], alpha=0.10, lw=0); ax.axvspan(5, 10, color=palette.status["warning"], alpha=0.10, lw=0)39 ax.axvspan(-5, 5, color=palette.cat[0], alpha=0.07, lw=0)40 ax.axvline(0, color=palette.ink2, lw=0.8)41 for k, r in V.iterrows():42 ax.plot([0, r["ecart"] * 100], [k, k], color=palette.grid, lw=1.2, zorder=1)43 ax.plot(r["ecart"] * 100, k, "o", color=COUL[r["verdict"]], ms=5.5, zorder=3)44 if abs(r["ecart"]) > 0.0005:45 ax.text(r["ecart"] * 100 + (0.6 if r["ecart"] >= 0 else -0.6), k, f"{r['ecart']*100:+.1f} %", va="center",46 ha="left" if r["ecart"] >= 0 else "right", fontsize=7.2, color=palette.ink)47 ax.set_yticks(y); ax.set_yticklabels([COURT.get(p, p) for p in V["poste"]], fontsize=7.6)48 ax.set_xlim(-22, 26); ax.set_xlabel("Écart du métré de contrôle par rapport au devis (%) : (quantité recalculée − quantité du devis) / quantité du devis")49 ax.grid(axis="y", visible=False)50 # séparateurs de corps51 prev = None52 for k, c in enumerate(V["corps"]):53 if prev is not None and c != prev:54 ax.axhline(k - 0.5, color=palette.grid, lw=0.6)55 prev = c56 nb = V["verdict"].value_counts()57 handles = [Line2D([], [], marker="o", color=COUL[v], ls="", ms=5.5, label=f"{v} ({int(nb.get(v, 0))})") for v in ("conforme", "à préciser", "à justifier")]58 ax.legend(handles=handles, loc="lower right", fontsize=7.5, title="Verdict (tolérances ±5 % et ±10 %)", title_fontsize=7.5)59 out = savefig(fig, "ch22/ch22-fig-ecarts-metre.pdf"); print("OK", out)606162if __name__ == "__main__":63 main()64