sparemetoder / app.py
EivindRebnord's picture
Create app.py
b380aad verified
Raw
History Blame Contribute Delete
13.5 kB
import math
from dataclasses import dataclass
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import gradio as gr
# ------------------------------------------------------------
# Modellparametre og beregninger
# ------------------------------------------------------------
@dataclass
class Params:
P: float # Beløp: engangsinnskudd ELLER månedsbeløp (avhenger av monthly)
inflation: float # årlig inflasjon (desimal)
N: int # horisont N (år) for tabeller og grafer
monthly: bool # True = månedlig; False = engangsbeløp
buf_rate: float # bufferspar nominell rente p.a.
tax_capital: float # skatt på bankrente
loan_rate_eff: float # effektiv lånerente p.a.
tax_interest_ded: float # rentefradrag (andel)
ask_return: float # ASK nominell avkastning p.a.
shield_rate: float # skjermingsrente p.a.
tax_stock_eff: float # effektiv aksjeskatt på gevinst ved uttak
wealth_tax_rate: float # formuesskatt p.a.
disc_stock: float # verdsettingsrabatt aksjer
disc_bank: float # verdsettingsrabatt bank
def fv_lump(principal: float, r: float, t_years: float) -> float:
return principal * ((1 + r) ** t_years)
def fv_annuity(pmt_month: float, r_year: float, t_years: float) -> float:
"""FV av månedlig sparing ved månedsslutt."""
n = int(round(12 * t_years))
if n <= 0:
return 0.0
i = r_year / 12.0
if abs(i) < 1e-12:
return pmt_month * n
return pmt_month * (((1 + i) ** n - 1) / i)
def shielding_lump(principal: float, s: float, t_years: float) -> float:
"""Akkumulert skjerming for engangsinnskudd i t år."""
return principal * ((1 + s) ** t_years - 1.0)
def shielding_monthly(pmt_month: float, s: float, t_years: float) -> float:
"""Presis skjerming per månedsinnskudd — hver betaling skjermes etter faktisk holdetid."""
n = int(round(12 * t_years))
if n <= 0:
return 0.0
acc = 0.0
for m in range(1, n + 1):
months_left = n - m + 1
years_left = months_left / 12.0
acc += pmt_month * ((1 + s) ** years_left - 1.0)
return acc
def real_value(nominal: float, inflation: float, t_years: float) -> float:
return nominal / ((1 + inflation) ** t_years)
def ann_real_rate_lump(real_end: float, P: float, t_years: int) -> float:
if t_years <= 0 or P <= 0:
return 0.0
return (real_end / P) ** (1.0 / t_years) - 1.0
def ann_real_rate_monthly(real_end: float, PMT: float, t_years: int) -> float:
"""
Løs årlig realrente r fra real_end = PMT * [((1+r/12)^(12t) - 1) / (r/12)].
Newton-Raphson med sikring.
"""
if t_years <= 0 or PMT <= 0:
return 0.0
n = 12 * t_years
# Startgjetning: enkel ratio
guess = 0.03
r = guess
for _ in range(100):
i = r / 12.0
if abs(i) < 1e-12:
i = 1e-12
f = PMT * (((1 + i) ** n - 1) / i) - real_end
# derivert: d/d r av ovenstående
# d/d i først:
A = (1 + i) ** n
df_di = PMT * (( (n * (1 + i) ** (n - 1)) * i - (A - 1) ) / (i ** 2))
df_dr = df_di / 12.0
if abs(df_dr) < 1e-16:
break
step = f / df_dr
r_new = r - step
# dempning for stabilitet
if abs(r_new) > 1.0:
r_new = np.sign(r_new) * 1.0
if abs(r_new - r) < 1e-10:
r = r_new
break
r = r_new
return float(r)
def compute_series(par: Params, max_years: int = 50) -> pd.DataFrame:
rows = []
r_buf_after_tax = par.buf_rate * (1 - par.tax_capital)
r_loan_after_tax = par.loan_rate_eff * (1 - par.tax_interest_ded)
for t in range(0, max_years + 1):
# Bufferspar
nom_buf = fv_annuity(par.P, r_buf_after_tax, t) if par.monthly else fv_lump(par.P, r_buf_after_tax, t)
# Nedbetaling (sikker avkastning lik lånerente etter skatt)
nom_loan = fv_annuity(par.P, r_loan_after_tax, t) if par.monthly else fv_lump(par.P, r_loan_after_tax, t)
# ASK
if par.monthly:
fv = fv_annuity(par.P, par.ask_return, t)
contrib = par.P * (12 * t)
shield = shielding_monthly(par.P, par.shield_rate, t)
taxbase = max(0.0, fv - contrib - shield)
tax = par.tax_stock_eff * taxbase
nom_ask = fv - tax
else:
fv = fv_lump(par.P, par.ask_return, t)
shield = shielding_lump(par.P, par.shield_rate, t)
taxbase = max(0.0, fv - par.P - shield)
tax = par.tax_stock_eff * taxbase
nom_ask = fv - tax
# Realverdier
real_buf = real_value(nom_buf, par.inflation, t)
real_loan = real_value(nom_loan, par.inflation, t)
real_ask = real_value(nom_ask, par.inflation, t)
# Formuesskatt (enkel pr-år beregning)
wealth_buf = nom_buf * (1 - par.disc_bank) * par.wealth_tax_rate
wealth_ask = nom_ask * (1 - par.disc_stock) * par.wealth_tax_rate
wealth_loan = nom_loan * par.wealth_tax_rate
# Likviditet
liq_buf = nom_buf
liq_loan = 0.0
liq_ask = nom_ask
rows.append(dict(
År=t,
Nom_Bufferspar=nom_buf,
Nom_Nedbetaling=nom_loan,
Nom_ASK=nom_ask,
Real_Bufferspar=real_buf,
Real_Nedbetaling=real_loan,
Real_ASK=real_ask,
WealthTax_Buf=wealth_buf,
WealthTax_Loan=wealth_loan,
WealthTax_ASK=wealth_ask,
Likviditet_Buf=liq_buf,
Likviditet_Nedbetaling=liq_loan,
Likviditet_ASK=liq_ask
))
return pd.DataFrame(rows)
def build_small_table(df: pd.DataFrame, par: Params, t: int) -> pd.DataFrame:
"""Liten tabell for én horisont t med korrekt annuitetsr for månedlig."""
row = df.loc[df["År"] == t].iloc[0]
out_rows = []
# Bufferspar
if par.monthly:
r_ann = ann_real_rate_monthly(row["Real_Bufferspar"], par.P, t)
else:
r_ann = ann_real_rate_lump(row["Real_Bufferspar"], par.P, t)
out_rows.append({
"Metode": "Bufferspar (etter skatt)",
"Sluttsum nominelt": round(row["Nom_Bufferspar"], 2),
"Sluttsum real": round(row["Real_Bufferspar"], 2),
"Årlig realavkastning etter skatt": round(r_ann, 4)
})
# Nedbetaling
if par.monthly:
r_ann = ann_real_rate_monthly(row["Real_Nedbetaling"], par.P, t)
else:
r_ann = ann_real_rate_lump(row["Real_Nedbetaling"], par.P, t)
out_rows.append({
"Metode": "Ekstra nedbetaling (etter skatt)",
"Sluttsum nominelt": round(row["Nom_Nedbetaling"], 2),
"Sluttsum real": round(row["Real_Nedbetaling"], 2),
"Årlig realavkastning etter skatt": round(r_ann, 4)
})
# ASK
if par.monthly:
r_ann = ann_real_rate_monthly(row["Real_ASK"], par.P, t)
else:
r_ann = ann_real_rate_lump(row["Real_ASK"], par.P, t)
out_rows.append({
"Metode": "ASK (m/skjermingsfradrag)",
"Sluttsum nominelt": round(row["Nom_ASK"], 2),
"Sluttsum real": round(row["Real_ASK"], 2),
"Årlig realavkastning etter skatt": round(r_ann, 4)
})
return pd.DataFrame(out_rows)
def make_line_plot(df: pd.DataFrame, cols: list, title: str, ylabel: str):
fig, ax = plt.subplots(figsize=(8, 4))
for c in cols:
ax.plot(df["År"], df[c], label=c.replace("_", " "))
ax.set_title(title)
ax.set_xlabel("År")
ax.set_ylabel(ylabel)
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
return fig
INTRO_MD = """
# Sammenligning av sparemetoder (interaktiv)
- **Bufferspar** (bankkonto) — nominell rente og 22 % skatt → etter-skatt rente brukes direkte.
- **Ekstra nedbetaling** — “sikker avkastning” lik lånerente etter fradrag.
- **ASK** — skatt ved uttak på gevinst utover **akkumulert skjerming**. Månedsparing beregner **presis skjerming per innskudd**.
- **Realverdi** = deflatert med inflasjon. Vi viser også **formuesskatt** (estimat) og **likviditet** over tid.
I resultatpanelet ser du små tabeller for **3, 5, 10 og N år** (hver i egen tab) med *sluttsum nominelt*, *sluttsum real* og **årlig realavkastning etter skatt**. For **månedlig sparing** løses årlig realrente korrekt fra annuitetsformelen.
"""
def run_calc(P, inflation, N, monthly, buf_rate, tax_capital, loan_rate_eff, tax_interest_ded,
ask_return, shield_rate, tax_stock_eff, wealth_tax_rate, disc_stock, disc_bank):
par = Params(
P=float(P),
inflation=float(inflation),
N=int(N),
monthly=bool(monthly),
buf_rate=float(buf_rate),
tax_capital=float(tax_capital),
loan_rate_eff=float(loan_rate_eff),
tax_interest_ded=float(tax_interest_ded),
ask_return=float(ask_return),
shield_rate=float(shield_rate),
tax_stock_eff=float(tax_stock_eff),
wealth_tax_rate=float(wealth_tax_rate),
disc_stock=float(disc_stock),
disc_bank=float(disc_bank),
)
df = compute_series(par, max_years=max(10, par.N))
# Små tabeller per horisont
t3 = build_small_table(df, par, 3 if par.N != 3 else 3)
t5 = build_small_table(df, par, 5 if par.N != 5 else 5)
t10 = build_small_table(df, par, 10 if par.N != 10 else 10)
tN = build_small_table(df, par, par.N)
# Grafer
fig_nom = make_line_plot(
df, ["Nom_Bufferspar", "Nom_Nedbetaling", "Nom_ASK"], "Nominell verdi", "kr"
)
fig_real = make_line_plot(
df, ["Real_Bufferspar", "Real_Nedbetaling", "Real_ASK"], "Realverdi (inflasjonsjustert)", "kr (2025-kroner)"
)
fig_tax = make_line_plot(
df, ["WealthTax_Buf", "WealthTax_Loan", "WealthTax_ASK"], "Årlig formuesskatt (estimat)", "kr/år"
)
fig_liq = make_line_plot(
df, ["Likviditet_Buf", "Likviditet_Nedbetaling", "Likviditet_ASK"], "Likviditet (tilgjengelige midler)", "kr"
)
return t3, t5, t10, tN, fig_nom, fig_real, fig_tax, fig_liq
# ------------------------------------------------------------
# Gradio UI
# ------------------------------------------------------------
with gr.Blocks(title="Sparemetoder – interaktiv") as demo:
gr.Markdown(INTRO_MD)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("## Parametere")
P = gr.Number(value=100_000, label="Beløp (engangsbeløp ELLER månedsbeløp)")
monthly = gr.Checkbox(value=True, label="Månedlig sparing?")
N = gr.Slider(3, 40, value=15, step=1, label="Horisont N (år)")
inflation = gr.Slider(0.0, 0.06, value=0.025, step=0.001, label="Inflasjon p.a.")
gr.Markdown("### Bank / lån")
buf_rate = gr.Slider(0.00, 0.10, value=0.035, step=0.0005, label="Bufferspar nominell rente p.a.")
tax_capital = gr.Slider(0.0, 0.5, value=0.22, step=0.01, label="Skatt på kapitalinntekt (bank)")
loan_rate_eff = gr.Slider(0.0, 0.10, value=0.0487, step=0.0005, label="Lånerente effektiv p.a.")
tax_interest_ded = gr.Slider(0.0, 0.5, value=0.22, step=0.01, label="Rentefradrag (andel)")
gr.Markdown("### ASK")
ask_return = gr.Slider(0.00, 0.30, value=0.08, step=0.001, label="ASK — forventet avkastning p.a.")
shield_rate = gr.Slider(0.00, 0.10, value=0.03, step=0.001, label="Skjermingsrente p.a.")
tax_stock_eff = gr.Slider(0.0, 0.6, value=0.3784, step=0.0001, label="Effektiv skatt aksjegevinst (ASK)")
gr.Markdown("### Formuesskatt / verdsettingsrabatter")
wealth_tax_rate = gr.Slider(0.00, 0.02, value=0.0085, step=0.0001, label="Formuesskatt p.a.")
disc_stock = gr.Slider(0.0, 0.5, value=0.20, step=0.01, label="Verdsettingsrabatt aksjer")
disc_bank = gr.Slider(0.0, 0.5, value=0.00, step=0.01, label="Verdsettingsrabatt bank")
with gr.Column(scale=2):
gr.Markdown("## Resultater")
with gr.Tabs():
with gr.Tab("3 år"):
t3 = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"])
with gr.Tab("5 år"):
t5 = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"])
with gr.Tab("10 år"):
t10 = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"])
with gr.Tab("N år"):
tN = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"])
with gr.Tabs():
with gr.Tab("Nominell verdi"):
img_nom = gr.Plot()
with gr.Tab("Realverdi"):
img_real = gr.Plot()
with gr.Tab("Formuesskatt"):
img_tax = gr.Plot()
with gr.Tab("Likviditet"):
img_liq = gr.Plot()
inputs = [P, inflation, N, monthly, buf_rate, tax_capital, loan_rate_eff, tax_interest_ded,
ask_return, shield_rate, tax_stock_eff, wealth_tax_rate, disc_stock, disc_bank]
outputs = [t3, t5, t10, tN, img_nom, img_real, img_tax, img_liq]
for comp in inputs:
comp.change(fn=run_calc, inputs=inputs, outputs=outputs)
gr.Button("Oppdater nå").click(fn=run_calc, inputs=inputs, outputs=outputs)
if __name__ == "__main__":
demo.launch()