Épreuve pratique · 2026 · Sujet 17

Bac NSI pratique 2026 sujet 17 : Budget d’un club et mois oublié dans une boucle

Le club de handball souhaite identifier ses recettes et dépenses puis calculer son bilan annuel. Chaque mouvement contient type (recette ou dépense), catégorie, montant positif en euros et mois entier de 1 à 12. Les catégories possibles sont subventions, sponsoring, billetterie, marketing, salaires, déplacements, fonctionnement, cotisations et autres_charges. Le jeu complet contient 2026 mouvements. Le petit jeu rend les calculs vérifiables à la main :

Dans ce sujet

Un corrigé pédagogique pour comprendre et justifier vos réponses. Les conseils de rédaction ne constituent pas un barème officiel détaillé.

Application

Budget d’un club et mois oublié dans une boucle

Le club de handball souhaite identifier ses recettes et dépenses puis calculer son bilan annuel. Chaque mouvement contient type (recette ou dépense), catégorie, montant positif en euros et mois entier de 1 à 12. Les catégories possibles sont subventions, sponsoring, billetterie, marketing, salaires, déplacements, fonctionnement, cotisations et autres_charges. Le jeu complet contient 2026 mouvements. Le petit jeu rend les calculs vérifiables à la main :

TypeCatégorieMontant (€)Mois
recettecotisations12001
recettebilletterie3006
dépensefonctionnement4506
dépensedéplacements20012
dépensesalaires150012
recettesubventions80012

Le dossier comporte le sujet et les fichiers listés dans les téléchargements. Les codes de départ sont reproduits ci-dessous. Travaillez sur une copie. Les appels professeur signalés dans les questions servent à présenter votre démarche ; vous pouvez aussi demander de l’aide en cas de difficulté.

Code fourni : analyse_budget.py
import csv


def lire_mouvements_depuis_csv(nom_fichier_csv):
    """
    Lit les données d'un fichier CSV et les retourne en liste de dictionnaires.
    Les colonnes 'montant' et 'mois' sont converties respectivement en flottant et en entier.
    """
    mouvements_csv = []
    try:
        with open(nom_fichier_csv, mode='r', newline='', encoding='utf-8') as fichier_csv:
            lecteur_csv = csv.DictReader(fichier_csv)
            for ligne in lecteur_csv:
                ligne['montant'] = float(ligne['montant'])
                ligne['mois'] = int(ligne['mois'])
                mouvements_csv.append(ligne)
        return mouvements_csv
    except FileNotFoundError:
        print(f"Erreur : Le fichier '{nom_fichier_csv}' est introuvable.")
        return []


mouvements_test = [
    {'type': 'recette', 'catégorie': 'cotisations', 'montant': 1200.0, 'mois': 1},
    {'type': 'recette', 'catégorie': 'billetterie', 'montant': 300.0, 'mois': 6},
    {'type': 'dépense', 'catégorie': 'fonctionnement', 'montant': 450.0, 'mois': 6},
    {'type': 'dépense', 'catégorie': 'déplacements', 'montant': 200.0, 'mois': 12},
    {'type': 'dépense', 'catégorie': 'salaires', 'montant': 1500.0, 'mois': 12},
    {'type': 'recette', 'catégorie': 'subventions', 'montant': 800.0, 'mois': 12}
]

#############################################################################
# Écrire ci-dessous la fonction total_par_type et ses tests (Question 1)    #
#############################################################################


#############################################################################
# Fonctions de calcul du solde (Questions 2 et 3)                           #
#############################################################################

def solde_mensuel(mouvements, mois):
    """
    Calcule le solde pour un mois donné (recettes - dépenses).
    """
    total_recettes = 0
    total_depenses = 0

    for m in mouvements:
        if m['mois'] == mois:
            if m['type'] == 'recette':
                total_recettes += m['montant']
            else:
                total_depenses += m['montant']

    return total_recettes - total_depenses


def solde_annuel(mouvements):
    """
    Calcule le solde annuel en additionnant les soldes de chaque mois.
    """
    total = 0
    # Parcourt les mois de l'année pour cumuler le bilan
    for m in range(1, 12):
        total = total + solde_mensuel(mouvements, m)

    return total


#############################################################################
# Écrire ci-dessous la fonction test_solde_annuel (Question 2)              #
#############################################################################


#############################################################################
# Programme principal pour analyser le fichier complet                      #
#############################################################################

# mouvements_complets = lire_mouvements_depuis_csv("budget_complet.csv")
# print("Le solde annuel sur le fichier complet est de :", solde_annuel(mouvements_complets))

Question 1

#

Écrire total_par_type(mouvements,type_mouvement), qui somme les montants du type choisi, ou renvoie 0 si aucun mouvement ne correspond. Créer test_total avec au moins deux assertions sur mouvements_test ; les recettes attendues valent 2300.0.

Indice

Séparer le filtre du cumul.

Comprendre la correction
def total_par_type(mouvements, type_mouvement):
    total = 0
    for mouvement in mouvements:
        if mouvement['type'] == type_mouvement:
            total += mouvement['montant']
    return total
def test_total():
    assert total_par_type(mouvements_test,'recette') == 2300.0
    assert total_par_type(mouvements_test,'dépense') == 2150.0
    assert total_par_type([], 'recette') == 0

test_total()

Les montants sont positifs quelle que soit la nature du mouvement. Cette fonction calcule donc deux totaux positifs ; le signe intervient ensuite dans le solde. On filtre par type sans se limiter à une catégorie particulière. L’accumulateur initial 0 respecte naturellement le cas où aucun mouvement n’est retenu.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

Question 2

#

Calculer à la main le solde annuel de mouvements_test. Écrire test_solde_annuel avec une assertion du résultat théorique, puis l’exécuter.

Comprendre la correction

Les recettes totalisent 1200+300+800=2300 €. Les dépenses totalisent 450+200+1500=2150 €. Le solde exact vaut donc 150 €. Par mois, janvier vaut 1200 €, juin−150 €, décembre−900 € ; tous les autres mois valent 0.

def test_solde_annuel():
    assert solde_annuel(mouvements_test) == 150.0

test_solde_annuel()

La version fournie retourne 1050 € et échoue : le test a bien une utilité puisqu’il réfute une sortie plausible mais fausse. Ne pas changer l’attendu pour faire passer le programme. Le bilan manuel constitue ici une référence indépendante.

Diagnostiquer un total plausibleUn atelier pour expérimenter

Choisissez une réponse puis confrontez votre intuition à l’explication. Vous pouvez modifier vos choix.

Lire les réponses et leurs explications
  1. Le programme renvoie 1050€ au lieu de 150€. Quel mois explique 900€ d’écart ?

    Décembre.. Décembre vaut−900€ et a été omis.

  2. Que représente un total de dépenses 2150€ ?

    Une somme positive à soustraire des recettes.. Les montants sont positifs dans le fichier.

Appuyez votre explication sur les valeurs et les conditions exactes du programme.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

Question 3

#

Identifier pourquoi certains mouvements sont ignorés dans solde_annuel, corriger, puis appliquer la fonction à budget_complet.csv pour annoncer le véritable solde.

PythonRetrouver le mois disparu du bilanÉcrivez votre solution et mettez-la à l’épreuve

Corrigez solde_annuel(mouvements) en réutilisant solde_mensuel(mouvements, mois), fourni. Les montants sont positifs et le type vaut recette ou dépense. Le bilan doit cumuler les douze mois. Les données ci-dessous sont des cas complémentaires réduits et le petit jeu du sujet.

def solde_annuel(mouvements):
    # À vous de jouer
    pass

Les cas de test proposés :

  • Décembre seul : Le dernier mois doit être visité : range exclut sa borne finale.
  • Janvier seul : Le premier mois doit aussi être inclus, avec le bon signe.
  • Petit bilan officiel : 2300 de recettes moins 2150 de dépenses donnent 150.
  • Un montant différent chaque mois : 1 + 2 + ... + 12 vérifie le cumul complet sans mois oublié.
  • Aucun mouvement : Chaque mois contribue zéro.
Indice

Écrire la liste produite par range(1,12).

Comprendre la correction

range(1,12) produit les mois 1 à 11 : la borne finale est exclue. Tous les mouvements de décembre sont donc ignorés. La correction minimale remplace 12 par 13. Les trois lignes de décembre du petit jeu expliquent exactement la différence entre 1050 et 150.

def solde_annuel(mouvements):
    total = 0
    for mois in range(1,13):
        total += solde_mensuel(mouvements,mois)
    return total
test_solde_annuel()
mouvements_complets = lire_mouvements_depuis_csv('budget_complet.csv')
print(f'{solde_annuel(mouvements_complets):.2f}')

Le fichier réel donne 796580,58 € de recettes et 800003,75 € de dépenses, soit un déficit de 3423,17 € : le solde est−3423,17 €. Comme le lecteur utilise float, l’affichage brut peut présenter des décimales parasites ; arrondir l’affichage à deux décimales ne corrige pas un oubli de mois. Pour contrôler le résultat indépendamment, comparer le cumul mensuel au total des recettes moins le total des dépenses, avec une tolérance numérique.

from math import isclose
attendu = total_par_type(mouvements_complets,'recette') - total_par_type(mouvements_complets,'dépense')
assert isclose(solde_annuel(mouvements_complets),attendu,abs_tol=1e-7)
assert isclose(solde_annuel(mouvements_complets),-3423.17,abs_tol=1e-7)

Pour diagnostiquer une borne de boucle, prenez le plus petit jeu où seule la valeur frontière joue un rôle. Un mouvement unique de 100 euros au mois 12 doit donner un solde de 100, mais la version fournie retourne zéro. Le même mouvement au mois 11 est pris en compte. Ce couple de tests localise précisément le défaut à la borne finale, sans dépendre des 2026 lignes du fichier complet. Une fois ce mécanisme compris, le résultat annuel sur le grand jeu devient contrôlable par une autre méthode de totalisation.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)
Le mois manquant change le bilanUn atelier pour expérimenter

Choisissez la borne finale du range et modifiez les recettes de décembre. Le tableau montre les mois réellement parcourus et le cumul du petit jeu.

Lire le résultat de l’expérience initiale

Solde calculé : 150€.

Le douzième mois est inclus ; changer décembre modifie maintenant le bilan.

Mois inclusSolde mensuel (€)Cumul (€)
112001200
201200
301200
401200
501200
6-1501050
701050
801050
901050
1001050
1101050
12-900150

Une borne exclue est une règle de Python, pas un détail typographique.

Revoir les notions de cet exercice

Du sujet à la méthode

Votre prochaine séance de révision

  • Un calcul manuel simple doit servir de référence avant le fichier complet.
  • Une seconde méthode de totalisation permet de détecter les omissions de groupes.

Retrouver ces notions dans d’autres sujets

Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027

Énoncé : sujet 26_BCG_NSI_17 (PDF). Corrigé et explications pédagogiques proposés par Sofien.