Chargement des bases
(RDS préparés en amont)
# WD = dossier du Rmd (si exécution sous RStudio)
if (requireNamespace("rstudioapi", quietly = TRUE) &&
rstudioapi::isAvailable() &&
nzchar(rstudioapi::getActiveDocumentContext()$path)) {
setwd(dirname(rstudioapi::getActiveDocumentContext()$path))
}
out_dir <- "output"
df_villeurbanne <- readRDS(file.path(out_dir, "df_villeurbanne.rds"))
df_villeurbanne_conso <- readRDS(file.path(out_dir, "df_villeurbanne_conso.rds"))
glimpse(df_villeurbanne_conso)
## Rows: 392
## Columns: 28
## $ Exercice <int> 2017, 2017, 2020, 2020, 2020, …
## $ `Tranche revenu par habitant` <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ `Code Insee 2024 Commune` <fct> 69266, 69266, 69266, 69266, 69…
## $ `Nom 2024 Commune` <fct> Villeurbanne, Villeurbanne, Vi…
## $ Catégorie <fct> Commune, Commune, Commune, Com…
## $ `Code Siren Collectivité` <fct> 216902668, 216902668, 21690266…
## $ `Code Insee Collectivité` <fct> 69266, 69266, 69266, 69266, 69…
## $ `Libellé Budget` <fct> VILLEURBANNE, VILLEURBANNE, VI…
## $ Agrégat <fct> Achats et charges externes, Au…
## $ `Montant BP` <dbl> 21624748.37, 581503.52, 240467…
## $ `Montant BA` <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ `Montant flux BP-BA` <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ Montant <dbl> 21624748.37, 581503.52, 240467…
## $ `Montant en millions` <dbl> 21.62474837, 0.58150352, 24.04…
## $ `Population totale` <dbl> 150075, 150075, 148754, 148754…
## $ `Montant en € par habitant` <dbl> 144.0929427, 3.8747528, 161.65…
## $ ordre_analyse1_section1 <lgl> NA, NA, NA, NA, NA, NA, NA, NA…
## $ ordre_analyse1_section2 <chr> NA, NA, NA, "4", NA, NA, "5", …
## $ ordre_analyse1_section3 <chr> NA, NA, NA, NA, NA, NA, NA, NA…
## $ ordre_analyse2_section1 <chr> NA, NA, "3", NA, NA, NA, NA, N…
## $ ordre_analyse2_section2 <chr> "3", NA, NA, "1", NA, "4", NA,…
## $ ordre_analyse2_section3 <chr> NA, NA, NA, "2", NA, NA, NA, N…
## $ ordre_analyse3_section1 <lgl> NA, NA, NA, NA, NA, NA, NA, NA…
## $ ordre_analyse3_section2 <chr> NA, NA, NA, NA, "2", NA, "1", …
## $ ordre_analyse3_section3 <chr> NA, NA, NA, NA, NA, NA, "1", N…
## $ ordre_analyse4_section1 <chr> NA, NA, NA, NA, NA, NA, NA, "2…
## $ annee_join <int> 2017, 2017, 2020, 2020, 2020, …
## $ `Population totale du dernier exercice` <dbl> 157953, 157953, 157953, 157953…
Préparation commune
aux 3 périmètres (BP / BA / CONSO)
# Fabrique une "vue" homogène avec une seule colonne montant, selon périmètre
make_view <- function(df_conso, per = c("BP","BA","CONSO")) {
per <- match.arg(per)
df_conso %>%
mutate(
Exercice = as.integer(Exercice),
montant = dplyr::case_when(
per == "BP" ~ as.numeric(`Montant BP`),
per == "BA" ~ as.numeric(`Montant BA`),
per == "CONSO" ~ as.numeric(Montant),
TRUE ~ NA_real_
)
) %>%
select(Exercice, Agrégat, montant)
}
# Séries annuelles
series <- function(df, ag) {
df %>%
filter(Agrégat == ag) %>%
group_by(Exercice) %>%
summarise(val = sum(montant, na.rm = TRUE), .groups="drop")
}
series_multi <- function(df, ag_vec) {
df %>%
filter(Agrégat %in% ag_vec) %>%
group_by(Exercice, Agrégat) %>%
summarise(val = sum(montant, na.rm = TRUE), .groups="drop")
}
# Table "total + détail" avec contrôle de somme
table_total_detail <- function(df, total_ag, detail_ag, unit = 1e6) {
tot <- series(df, total_ag) %>% rename(total = val)
det <- series_multi(df, detail_ag) %>%
tidyr::pivot_wider(names_from = Agrégat, values_from = val, values_fill = 0)
tab <- tot %>%
left_join(det, by="Exercice")
detail_cols <- setdiff(names(tab), c("Exercice","total"))
tab <- tab %>%
rowwise() %>%
mutate(
detail_sum = sum(c_across(all_of(detail_cols)), na.rm = TRUE),
reste = total - detail_sum,
ecart = total - (detail_sum + reste)
) %>%
ungroup()
long <- tab %>%
select(Exercice, total, all_of(detail_cols), reste) %>%
pivot_longer(cols = -Exercice, names_to = "poste", values_to = "val") %>%
mutate(val = val/unit)
wide <- long %>%
pivot_wider(names_from = Exercice, values_from = val) %>%
mutate(poste = factor(poste, levels = c("total", detail_cols, "reste"))) %>%
arrange(poste) %>%
mutate(poste = as.character(poste)) %>%
mutate(poste = recode(poste, total = paste0("TOTAL — ", total_ag), reste = "Reste / non ventilé"))
list(
table = wide,
check = tab %>%
select(Exercice, total, detail_sum, reste, ecart) %>%
mutate(across(c(total, detail_sum, reste), ~./unit))
)
}
# Graphes "qualité de gestion" (pack minimal, sans facettes)
plot_indicateurs_gestion <- function(df, titre_prefix = "") {
# CAF / Epargne
epargne_ag <- c("Epargne de gestion", "Epargne brute", "Epargne nette")
epargne <- series_multi(df, epargne_ag) %>% mutate(m = val/1e6)
p1 <- ggplot(epargne, aes(x=Exercice, y=m, color=Agrégat)) +
geom_hline(yintercept=0, linetype=2) +
geom_line(linewidth=1) + geom_point() +
labs(title=paste0(titre_prefix, "CAF / Épargne (M€)"), x="Exercice", y="M€") +
theme_minimal()
# Taux d’épargne brute = EB / RF
rf <- series(df, "Recettes de fonctionnement") %>% rename(rf = val)
eb <- series(df, "Epargne brute") %>% rename(eb = val)
taux <- rf %>% left_join(eb, by="Exercice") %>%
mutate(taux_eb = ifelse(rf==0, NA_real_, eb/rf))
p2 <- ggplot(taux, aes(x=Exercice, y=taux_eb)) +
geom_hline(yintercept=0, linetype=2) +
geom_line(linewidth=1) + geom_point() +
scale_y_continuous(labels=percent) +
labs(title=paste0(titre_prefix, "Taux d’épargne brute (EB/RF)"), x="Exercice", y="%") +
theme_minimal()
# Rigidité = personnel / dépenses de fonctionnement
dfct <- series(df, "Dépenses de fonctionnement") %>% rename(dfct = val)
pers <- series(df, "Frais de personnel") %>% rename(pers = val)
rigid <- dfct %>% left_join(pers, by="Exercice") %>%
mutate(part = ifelse(dfct==0, NA_real_, pers/dfct))
p3 <- ggplot(rigid, aes(x=Exercice, y=part)) +
geom_line(linewidth=1) + geom_point() +
scale_y_continuous(labels=percent) +
labs(title=paste0(titre_prefix, "Rigidité (Personnel/DF)"), x="Exercice", y="%") +
theme_minimal()
# Investissement : niveau
inv <- series(df, "Dépenses d'investissement") %>% mutate(m = val/1e6)
p4 <- ggplot(inv, aes(x=Exercice, y=m)) +
geom_line(linewidth=1) + geom_point() +
labs(title=paste0(titre_prefix, "Dépenses d’investissement (M€)"), x="Exercice", y="M€") +
theme_minimal()
# Dette & désendettement
dette <- series(df, "Encours de dette") %>% rename(dette = val)
eb2 <- series(df, "Epargne brute") %>% rename(eb = val)
des <- dette %>% left_join(eb2, by="Exercice") %>%
mutate(dette_m = dette/1e6,
capacite = ifelse(eb<=0, NA_real_, dette/eb))
p5 <- ggplot(des, aes(x=Exercice, y=dette_m)) +
geom_line(linewidth=1) + geom_point() +
labs(title=paste0(titre_prefix, "Encours de dette (M€)"), x="Exercice", y="M€") +
theme_minimal()
p6 <- ggplot(des, aes(x=Exercice, y=capacite)) +
geom_hline(yintercept=0, linetype=2) +
geom_line(linewidth=1) + geom_point() +
labs(title=paste0(titre_prefix, "Capacité de désendettement (dette/EB)"), x="Exercice", y="années") +
theme_minimal()
# Solde global
solde <- series(df, "Capacité ou besoin de financement") %>% mutate(m = val/1e6)
p7 <- ggplot(solde, aes(x=Exercice, y=m)) +
geom_hline(yintercept=0, linetype=2) +
geom_line(linewidth=1) + geom_point() +
labs(title=paste0(titre_prefix, "Capacité / besoin de financement (M€)"), x="Exercice", y="M€") +
theme_minimal()
list(p1=p1, p2=p2, p3=p3, p4=p4, p5=p5, p6=p6, p7=p7)
}
# ---- Décomposition OFGL (une seule fois) ----
# Fonctionnement
fct_dep_total <- "Dépenses de fonctionnement"
fct_dep_detail <- c(
"Frais de personnel",
"Achats et charges externes",
"Dépenses d'intervention",
"Charges financières",
"Autres dépenses de fonctionnement"
# "Autres dotations de fonctionnement" : volontairement hors DF (SIG)
)
fct_rec_total <- "Recettes de fonctionnement"
fct_rec_detail <- c(
"Concours de l'Etat",
"Impôts et taxes",
"Subventions reçues et participations",
"Ventes de biens et services",
"Autres recettes de fonctionnement"
)
# Investissement
inv_dep_total <- "Dépenses d'investissement"
inv_dep_detail <- c(
"Dépenses d'investissement hors remb",
"Remboursements d'emprunts hors GAD"
)
inv_rec_total <- "Recettes d'investissement"
inv_rec_detail <- c(
"Recettes d'investissement hors emprunts",
"Emprunts hors GAD"
)