Intention
Suivre et quantifier un remboursement de dette, un Ă©pargne avec intĂ©rĂȘts, pour un hypothĂȘque ou paiment de voiture. Le script doit pouvoir ĂȘtre configurable, C'est-Ă -dire de pouvoir ajouter et supprimer des paiments et ajuster les soldes en temps rĂ©el.
Conception
Ce projet est mis en place avec Streamlit sur Pycharm. Ă partir d'une base de donnĂ©e simple SQLite, un calcul d'intĂ©rĂȘt mensuel selon le remboursement est modifier Ă chaque moi. Un graphique interactif permet de visualiser le remboursement et une comparaison est fait entre un remboursement anticipĂ© et le remboursmeent rĂ©el qui dĂ©pend des donnĂ©es entrĂ©es.
Application
Voici le code de l'application. Cette application ne fonctionne pas sans la base de données et le script est lancer sur un container via docker. De cette maniÚre, l'application est accessible sur mon réseau personnel.
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
import sqlite3
from datetime import datetime
from dateutil.relativedelta import relativedelta
# --- CONFIGURATION ---
st.set_page_config(page_title="Debt Tracker Pro", layout="wide", page_icon="đ")
DETTE_INITIALE = 9500.0
TAUX_ANNUEL = 0.047
TAUX_MENSUEL = TAUX_ANNUEL / 12
NOM_DB = 'finances.db'
Initialisation de la base de donnée.
# --- FONCTIONS DB ---
def recuperer_donnees(table):
try:
conn = sqlite3.connect(NOM_DB)
df = pd.read_sql_query(f"SELECT * FROM {table}", conn)
conn.close()
if not df.empty and 'date' in df.columns:
df['date'] = pd.to_datetime(df['date'])
return df
except:
return pd.DataFrame()
La presente section permet d'ajouter ou enlever des paiments. C'est aussi dans cette section que l'on peut recommencer Ă zĂ©ro. Le calendrier permet de changer la date si paiment antĂ©rieur Ă Ă©tĂ© fait et doit ĂȘtre mis sur le graphique.
# --- SIDEBAR ---
with st.sidebar:
st.header("đ° Gestion des fonds")
mtn = st.number_input("Montant du versement ($)", min_value=0.0, step=100.0)
dt = st.date_input("Date du versement", datetime.now())
if st.button("Enregistrer le paiement"):
conn = sqlite3.connect(NOM_DB)
c = conn.cursor()
c.execute("INSERT INTO paiements (date, montant) VALUES (?, ?)", (dt.strftime('%Y-%m-%d'), mtn))
conn.commit()
conn.close()
st.rerun()
st.divider()
st.subheader("đ Historique")
df_reel = recuperer_donnees('paiements')
if not df_reel.empty:
df_reel = df_reel.sort_values(by='date', ascending=True)
df_reel['total_cumule'] = df_reel['montant'].cumsum()
df_reel = df_reel.sort_values(by='date', ascending=False)
st.dataframe(df_reel[['date', 'montant', 'total_cumule']], hide_index=True)
st.divider()
st.subheader("đ Ajustement manuel du solde rĂ©el")
df_adj = recuperer_donnees("ajustements")
dernier_ajustement = df_adj['solde'].iloc[-1] if not df_adj.empty else DETTE_INITIALE
solde_reel_manuel = st.number_input(
"Corriger le solde réel ($)",
min_value=0.0,
value=float(dernier_ajustement),
step=100.0
)
if st.button("Enregistrer l'ajustement"):
conn = sqlite3.connect(NOM_DB)
c = conn.cursor()
c.execute(
"INSERT INTO ajustements (date, solde) VALUES (?, ?)",
(datetime.now().strftime('%Y-%m-%d'), solde_reel_manuel)
)
conn.commit()
conn.close()
st.success("Ajustement enregistré !")
st.rerun()
st.divider()
st.subheader("đ ïž Zone de test")
if st.button("Backup ONE"):
conn = sqlite3.connect(NOM_DB)
c = conn.cursor()
c.execute("DELETE FROM paiements WHERE id = (SELECT MAX(id) FROM paiements)")
conn.commit()
conn.close()
st.rerun()
if st.button("Erase all"):
conn = sqlite3.connect(NOM_DB)
c = conn.cursor()
c.execute("DELETE FROM paiements")
conn.commit()
conn.close()
st.rerun()
La section suivante est l'endroit oĂč tout les calculs sont faits. C'est ici qu'il faut indiquer les intĂ©rĂȘts cummulĂ©s de base. C'est aussi l'endroit qui permet de mettre automatique le montant de 0 si aucun paiment n'a Ă©tĂ© inscrit le mois prĂ©sent pour afficher dans le graphique.
# --- CALCULS ---
df_theo = recuperer_donnees('echeancier')
dates_reelles = []
soldes_reels = []
total_interets_cumules = 0.0
if not df_theo.empty:
date_depart = df_theo['date'].min()
date_sim = date_depart
df_adj = recuperer_donnees("ajustements")
solde_en_cours = df_adj['solde'].iloc[-1] if not df_adj.empty else DETTE_INITIALE
aujourdhui = datetime.now()
# Point de départ (Mois 0)
dates_reelles.append(date_depart - relativedelta(months=1))
soldes_reels.append(solde_en_cours)
# Simulation
while date_sim <= aujourdhui + relativedelta(months=1):
interets_mois = solde_en_cours * TAUX_MENSUEL
total_interets_cumules += interets_mois
if not df_reel.empty:
debut_m = date_sim.replace(day=1)
fin_m = date_sim + relativedelta(day=31)
mask = (df_reel['date'] >= debut_m) & (df_reel['date'] <= fin_m)
paiements_mois = df_reel[mask]['montant'].sum()
else:
paiements_mois = 0
solde_en_cours = max(0, solde_en_cours + interets_mois - paiements_mois)
dates_reelles.append(date_sim)
soldes_reels.append(solde_en_cours)
date_sim += relativedelta(months=1, day=31)
Dans l'affichage, il est possible de changer le titre et d'ajouter des commentaires ou bien des sous-section. Ces dĂ©tails peuvent ĂȘtre un taux d'intĂ©rĂȘts, une date projetĂ© de fin de paiment ou bien une date butoire pour accumuler un montant. Pour ce projet, c'est l'endroit oĂč je vĂ©rifie le solde actuel, le montant thĂ©orique selon ma pĂ©vision, et les intĂ©rĂȘt cummulĂ©s comme Ă©chelle de motivation. C'est aussi Ă cet endroit que j'affiche le montant restant a payer sur le solde du mois prĂ©sent.
# --- AFFICHAGE ---
st.title("Analyse de ma dette étudiante")
solde_theo_actuel = 0.0
aujourdhui = datetime.now()
if not df_theo.empty:
mask = (df_theo['date'].dt.month == aujourdhui.month) & (df_theo['date'].dt.year == aujourdhui.year)
res = df_theo[mask]
if not res.empty:
solde_theo_actuel = res['solde_theorique'].iloc[0]
ecart = soldes_reels[-1] - solde_theo_actuel if soldes_reels else 0
c1, c2, c3 = st.columns(3)
c1.metric("Solde Réel", f"{soldes_reels[-1]:,.2f} $" if soldes_reels else "N/A")
c2.metric(f"Objectif {aujourdhui.strftime('%b %Y')}", f"{solde_theo_actuel:,.2f} $",
delta=f"{ecart:,.2f} $ vs Plan", delta_color="inverse")
c3.metric("IntĂ©rĂȘts CumulĂ©s", f"{total_interets_cumules:,.2f} $")
Le graphique est intéractif et permet le suivi mensuel des paiments. Pour donner une référence, une ligne abstraite en gris est affichée sur le graphique. Cette ligne permet de visualiser les écarts selon le solde réel et le solde théorique calculé avec des étiquettes à chaque mois.
# --- GRAPHIQUE ---
if not df_theo.empty:
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_theo['date'], y=df_theo['solde_theorique'],
name='Plan', line=dict(color='rgba(150,150,150,0.5)', dash='dash')
))
fig.add_trace(go.Scatter(
x=dates_reelles, y=soldes_reels,
name='Ma Réalité', line=dict(color='#00CC96', width=4)
))
fig.update_layout(
xaxis=dict(title="Date", tickformat="%b %Y", dtick="M1", tickangle=-90,
showgrid=True, gridcolor='rgba(200,200,200,0.2)'),
yaxis=dict(title="Dette ($)", showgrid=True, gridcolor='rgba(200,200,200,0.2)'),
hovermode="x unified",
plot_bgcolor='rgba(0,0,0,0)',
height=500
)
st.plotly_chart(fig, use_container_width=True)
else:
st.error("â ïž La table 'echeancier' est vide. Lancez d'abord le script de base de donnĂ©es.")
Voici un visuel de l'application avec les fonctions de paiments à la gauche, le graphique à la droite ainsi que les détails au dessus du graphique.
Base de donnee
La base de donnĂ©es sert Ă enregistrer les montants ajoutĂ©s au fil du temps. C'est aussi l'endroit d'oĂč provient la ligne de suivie thĂ©orique. C'est une base donnĂ©e SQLite qui calcul le taux d'intĂ©rĂȘt et le montant actuel du solde pour ajouter les intĂ©rĂȘts mensul Ă payer. De cette maniĂšre, le solde est reprĂ©setatif au maximum. Elle doit ĂȘtre instancier un ligne de commande, si non, un message d'erreur apparait Ă la place du graphique.
Amélioration à venir
Des fonctions de retour en arriĂšre peuvent ĂȘtre ajoutĂ© pour facilitĂ© la gestion des paiments. Un outil d'esportation CSV serait intĂ©ressant pour utiliser la base de donnĂ©e autrement.