Logement — encadrement des loyers à Paris

Données - Ville de Paris

Code
source(here::here("_rinit.R"))
invisible(Sys.setlocale("LC_TIME", "fr_FR.UTF-8"))
invisible(Sys.setenv(TZ = "Europe/Paris"))
# cd ~/iCloud/website/data/paris; /usr/local/bin/R -e 'quarto::quarto_render("logement-encadrement-des-loyers.qmd")'
# sh ~/iCloud/website/website_short.sh; open https://fgeerolf.com/data/paris/logement-encadrement-des-loyers.html

source(here::here("data", "paris", "_paris.R"))   # wkb2sfc(), paris_basemap()

knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE,
                      fig.width = 8, fig.height = 4.6, dpi = 150)

# --- Données : Parquet versionné dans le dépôt (opendata.paris.fr) -----------
loyers_path <- here::here("data", "paris", "logement-encadrement-des-loyers.parquet")
loyers <- arrow::read_parquet(loyers_path)

# Ordre chronologique des tranches d'époque (+ accent absent de la source)
loyers <- loyers |>
  dplyr::mutate(
    annee  = as.integer(annee),
    epoque = factor(dplyr::recode(epoque, "Apres 1990" = "Après 1990"),
                    levels = c("Avant 1946", "1946-1970", "1971-1990", "Après 1990")),
    meuble = factor(meuble_txt, levels = c("non meublé", "meublé"))
  )

annee_max <- max(loyers$annee)

# Écart meublé / non meublé (à quartier × pièces × époque × année donnés)
prime_meuble <- loyers |>
  dplyr::select(annee, id_quartier, piece, epoque, meuble_txt, ref) |>
  tidyr::pivot_wider(names_from = meuble_txt, values_from = ref) |>
  dplyr::mutate(prime = `meublé` / `non meublé` - 1)

# Un polygone par quartier administratif (80 quartiers, 14 secteurs)
quartiers_sf <- loyers |>
  dplyr::distinct(id_quartier, nom_quartier, id_zone, geo_shape) |>
  dplyr::mutate(geometry = wkb2sfc(geo_shape)) |>
  dplyr::select(-geo_shape) |>
  sf::st_as_sf(crs = 4326)

# --- Habillage commun des cartes / graphiques -------------------------------
fill_loyers <- function(name = "€/m²", ...) {
  scale_fill_viridis_c(option = "rocket", direction = -1, name = name,
                       labels = scales::label_number(suffix = " €"), ...)
}
theme_carte <- theme_void(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        plot.subtitle = element_text(colour = "grey35"),
        legend.position = "right")
theme_barres <- theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        plot.subtitle = element_text(colour = "grey35"),
        panel.grid.major.y = element_blank())

L’encadrement des loyers est en vigueur à Paris depuis le 1er juillet 2019. Pour chaque logement mis en location, le bail ne peut pas dépasser un loyer de référence majoré (loyer de référence + 20 %) ; un loyer de référence minoré (loyer de référence − 30 %) sert de plancher pour le complément de loyer. Ces montants, exprimés en € par m² de surface habitable, sont fixés chaque année par arrêté préfectoral.

Le jeu de données « Logement encadrement des loyers » d’opendata.paris.fr donne ces trois montants pour chaque croisement de :

Soit 17920 lignes (80 × 4 × 4 × 2 × 7 — un panneau complet, sans trou).

Code
loyers |>
  dplyr::filter(annee == annee_max) |>
  dplyr::arrange(id_quartier, piece, epoque, meuble) |>
  dplyr::transmute(
    Secteur = id_zone, Quartier = nom_quartier, `Pièces` = piece,
    `Époque` = as.character(epoque), Location = meuble_txt,
    `Réf. minoré` = min, `Référence` = ref, `Réf. majoré` = max
  ) |>
  head(16) |>
  gt::gt() |>
  gt::tab_header(title = gt::md(paste0(
    "Aperçu — les 32 profils du 1<sup>er</sup> quartier, ", annee_max))) |>
  gt::tab_options(table.width = "100%")
Aperçu — les 32 profils du 1er quartier, 2025
Secteur Quartier Pièces Époque Location Réf. minoré Référence Réf. majoré
2 St-Germain-l'Auxerrois 1 Avant 1946 non meublé 24.6 35.1 42.1
2 St-Germain-l'Auxerrois 1 Avant 1946 meublé 28.0 40.0 48.0
2 St-Germain-l'Auxerrois 1 1946-1970 non meublé 23.7 33.9 40.7
2 St-Germain-l'Auxerrois 1 1946-1970 meublé 27.0 38.6 46.3
2 St-Germain-l'Auxerrois 1 1971-1990 non meublé 22.2 31.7 38.0
2 St-Germain-l'Auxerrois 1 1971-1990 meublé 25.3 36.1 43.3
2 St-Germain-l'Auxerrois 1 Après 1990 non meublé 22.1 31.6 37.9
2 St-Germain-l'Auxerrois 1 Après 1990 meublé 25.2 36.0 43.2
2 St-Germain-l'Auxerrois 2 Avant 1946 non meublé 21.5 30.7 36.8
2 St-Germain-l'Auxerrois 2 Avant 1946 meublé 24.5 35.0 42.0
2 St-Germain-l'Auxerrois 2 1946-1970 non meublé 20.0 28.5 34.2
2 St-Germain-l'Auxerrois 2 1946-1970 meublé 22.8 32.5 39.0
2 St-Germain-l'Auxerrois 2 1971-1990 non meublé 20.5 29.3 35.2
2 St-Germain-l'Auxerrois 2 1971-1990 meublé 23.4 33.4 40.1
2 St-Germain-l'Auxerrois 2 Après 1990 non meublé 19.7 28.1 33.7
2 St-Germain-l'Auxerrois 2 Après 1990 meublé 22.4 32.0 38.4

Carte — loyer de référence majoré 2025

Pour chaque quartier, la médiane du loyer de référence majoré sur les 32 profils de logement (pièces × époque × meublé). Le plafond décroît nettement du centre-ouest (7e, 6e, 8e) vers la périphérie nord-est (19e, 20e).

Code
carte_maj <- loyers |>
  dplyr::filter(annee == annee_max) |>
  dplyr::group_by(id_quartier) |>
  dplyr::summarise(maj = median(max), .groups = "drop")

quartiers_sf |>
  dplyr::left_join(carte_maj, by = "id_quartier") |>
  ggplot() +
  geom_sf(aes(fill = maj), colour = "white", linewidth = 0.15) +
  fill_loyers("Loyer de\nréférence\nmajoré") +
  labs(title = paste0("Loyer de référence majoré — Paris, ", annee_max),
       subtitle = "Médiane sur les 32 profils de logement, par quartier administratif") +
  theme_carte

Carte interactive

Survol pour le détail par quartier (secteur, loyers minoré / de référence / majoré médians).

Code
lf <- quartiers_sf |>
  dplyr::left_join(
    loyers |>
      dplyr::filter(annee == annee_max) |>
      dplyr::group_by(id_quartier) |>
      dplyr::summarise(min = median(min), ref = median(ref), maj = median(max),
                       .groups = "drop"),
    by = "id_quartier"
  )

pal <- colorNumeric("magma", domain = lf$maj, reverse = TRUE)

leaflet(lf, options = leafletOptions(minZoom = 11)) |>
  paris_basemap() |>
  addPolygons(
    fillColor = ~pal(maj), fillOpacity = 0.75,
    color = "white", weight = 1,
    highlightOptions = highlightOptions(weight = 2, color = "#333", bringToFront = TRUE),
    label = ~sprintf("%s (secteur %d) — %.1f €/m²", nom_quartier, id_zone, maj),
    popup = ~sprintf(
      "<b>%s</b><br>Secteur %d<br>Loyer de référence : <b>%.1f €/m²</b><br>Minoré : %.1f &nbsp;&middot;&nbsp; Majoré : %.1f",
      nom_quartier, id_zone, ref, min, maj)
  ) |>
  addLegend("bottomright", pal = pal, values = ~maj,
            title = paste0("Réf. majoré<br>€/m² — ", annee_max), opacity = 0.9)

La carte dépend du profil de logement

Le plafond au m² est d’autant plus élevé que le logement est petit et ancien. À secteur donné, un studio d’avant 1946 se loue légalement bien plus cher au m² qu’un 4-pièces récent.

Code
carte_piece <- loyers |>
  dplyr::filter(annee == annee_max, meuble == "non meublé") |>
  dplyr::group_by(id_quartier, piece) |>
  dplyr::summarise(ref = median(ref), .groups = "drop")

quartiers_sf |>
  dplyr::left_join(carte_piece, by = "id_quartier") |>
  dplyr::mutate(piece = factor(piece, labels = paste0(1:4, c(" pièce", " pièces", " pièces", " pièces ou +")))) |>
  ggplot() +
  geom_sf(aes(fill = ref), colour = "white", linewidth = 0.1) +
  fill_loyers("Loyer de\nréférence") +
  facet_wrap(~piece) +
  labs(title = paste0("Loyer de référence selon le nombre de pièces — ", annee_max),
       subtitle = "Logements non meublés, médiane sur les époques de construction") +
  theme_carte

Évolution 2019–2025

Le loyer de référence moyen (tous quartiers, tous profils) progresse d’environ 10.1% sur la période — une hausse bornée d’une année sur l’autre par l’évolution de l’IRL (indice de référence des loyers).

Code
loyers |>
  dplyr::group_by(annee, epoque) |>
  dplyr::summarise(ref = mean(ref), .groups = "drop") |>
  ggplot(aes(annee, ref, colour = epoque)) +
  geom_line(linewidth = 1) +
  geom_point(size = 2) +
  scale_colour_viridis_d(option = "mako", end = 0.85, name = "Époque de\nconstruction") +
  scale_x_continuous(breaks = scales::breaks_width(1)) +
  scale_y_continuous(labels = scales::label_number(suffix = " €")) +
  labs(title = "Loyer de référence moyen par époque de construction",
       subtitle = "Paris, moyenne sur les 80 quartiers et tous les profils",
       x = NULL, y = "€/m²") +
  theme_barres

Code
croissance <- loyers |>
  dplyr::filter(annee %in% range(annee)) |>
  dplyr::group_by(id_quartier, annee) |>
  dplyr::summarise(ref = mean(ref), .groups = "drop") |>
  tidyr::pivot_wider(names_from = annee, values_from = ref, names_prefix = "a") |>
  dplyr::mutate(g = .data[[paste0("a", max(loyers$annee))]] /
                    .data[[paste0("a", min(loyers$annee))]] - 1)

quartiers_sf |>
  dplyr::left_join(croissance, by = "id_quartier") |>
  ggplot() +
  geom_sf(aes(fill = g), colour = "white", linewidth = 0.15) +
  scale_fill_viridis_c(option = "viridis", labels = scales::percent,
                       name = "Hausse\ncumulée") +
  labs(title = paste0("Hausse du loyer de référence ", min(loyers$annee), "–", max(loyers$annee)),
       subtitle = "Variation cumulée du loyer de référence moyen, par quartier") +
  theme_carte

Ce que révèlent les données

Effet du nombre de pièces

Code
base_4p <- loyers |>
  dplyr::filter(annee == annee_max) |>
  dplyr::group_by(piece) |>
  dplyr::summarise(ref = mean(ref), .groups = "drop")

base_4p |>
  ggplot(aes(factor(piece), ref)) +
  geom_col(fill = "#7a1f3d", width = 0.65) +
  geom_text(aes(label = scales::number(ref, 0.1, suffix = " €")), vjust = -0.4, size = 3.5) +
  scale_x_discrete(labels = c("1 pièce", "2 pièces", "3 pièces", "4 pièces ou +")) +
  labs(title = "Un studio se loue plus cher au m² qu'un grand appartement",
       subtitle = paste0("Loyer de référence moyen selon le nombre de pièces — Paris, ", annee_max),
       x = NULL, y = "€/m²") +
  theme_barres +
  coord_cartesian(clip = "off")

Un studio (32.2 €/m²) est environ 27% plus cher au m² qu’un 4-pièces (25.4 €/m²).

Effet de l’époque de construction

Code
loyers |>
  dplyr::filter(annee == annee_max) |>
  dplyr::group_by(epoque) |>
  dplyr::summarise(ref = mean(ref), .groups = "drop") |>
  ggplot(aes(epoque, ref)) +
  geom_col(fill = "#7a1f3d", width = 0.65) +
  geom_text(aes(label = scales::number(ref, 0.1, suffix = " €")), vjust = -0.4, size = 3.5) +
  labs(title = "L'ancien haussmannien est le plus cher au m²",
       subtitle = paste0("Loyer de référence moyen selon l'époque de construction — Paris, ", annee_max),
       x = NULL, y = "€/m²") +
  theme_barres +
  coord_cartesian(clip = "off")

Le bâti d’avant 1946 (haussmannien, faubourien) affiche le loyer de référence le plus élevé ; les constructions de 1946 à 1990 sont les moins chères, celles d’après 1990 se situant entre les deux.

Prime « meublé »

Le loyer de référence d’un logement meublé s’obtient en appliquant un coefficient quasi uniforme au loyer non meublé (écart-type de 0.11 point de pourcentage entre profils en 2025). Ce coefficient est lui-même un paramètre de l’arrêté : il est passé d’environ +13 % en 2019 à +14 % en 2025.

Code
prime_meuble |>
  dplyr::group_by(annee) |>
  dplyr::summarise(prime = mean(prime), .groups = "drop") |>
  ggplot(aes(annee, prime)) +
  geom_line(linewidth = 1, colour = "#7a1f3d") +
  geom_point(size = 2.5, colour = "#7a1f3d") +
  geom_text(aes(label = scales::percent(prime, 0.1)), vjust = -1, size = 3.3) +
  scale_x_continuous(breaks = scales::breaks_width(1)) +
  scale_y_continuous(labels = scales::percent, limits = c(0.10, 0.16)) +
  labs(title = "La prime « meublé » du loyer de référence",
       subtitle = "Écart moyen meublé / non meublé, Paris",
       x = NULL, y = NULL) +
  theme_barres

La fourchette légale est mécanique

Le loyer de référence majoré vaut référence + 20 %, le minoré référence − 30 % (aux arrondis au décime près). La « largeur » de l’encadrement ne porte donc aucune information propre.

Code
loyers |>
  dplyr::mutate(`majoré / référence` = max / ref,
                `minoré / référence` = min / ref) |>
  dplyr::summarise(
    `majoré / référence` = sprintf("%.3f  (min %.3f – max %.3f)",
      mean(`majoré / référence`), min(`majoré / référence`), max(`majoré / référence`)),
    `minoré / référence` = sprintf("%.3f  (min %.3f – max %.3f)",
      mean(`minoré / référence`), min(`minoré / référence`), max(`minoré / référence`))
  ) |>
  tidyr::pivot_longer(dplyr::everything(), names_to = "Ratio", values_to = "Valeur (moyenne et étendue)") |>
  gt::gt() |>
  gt::tab_options(table.width = "100%")
Ratio Valeur (moyenne et étendue)
majoré / référence 1.200 (min 1.198 – max 1.203)
minoré / référence 0.700 (min 0.697 – max 0.703)

Classement des quartiers 2025

Code
loyers |>
  dplyr::filter(annee == annee_max) |>
  dplyr::group_by(Secteur = id_zone, Quartier = nom_quartier) |>
  dplyr::summarise(
    `Réf. minoré` = median(min), `Référence` = median(ref), `Réf. majoré` = median(max),
    .groups = "drop"
  ) |>
  dplyr::arrange(dplyr::desc(`Réf. majoré`)) |>
  dplyr::mutate(Rang = dplyr::row_number(), .before = 1) |>
  gt::gt() |>
  gt::fmt_number(columns = c(`Réf. minoré`, `Référence`, `Réf. majoré`), decimals = 1) |>
  gt::data_color(columns = `Réf. majoré`, palette = "Reds") |>
  gt::opt_interactive(use_search = TRUE, use_compact_mode = TRUE,
                      page_size_default = 15, page_size_values = c(10, 15, 25, 80)) |>
  gt::tab_footnote(gt::md(paste0(
    "Médiane sur les 32 profils de logement. Source : ",
    "[opendata.paris.fr](https://opendata.paris.fr/explore/dataset/logement-encadrement-des-loyers/)."))) |>
  gt::tab_options(table.width = "100%")
Médiane sur les 32 profils de logement. Source : opendata.paris.fr.
Code
q <- loyers |>
  dplyr::filter(annee == annee_max) |>
  dplyr::group_by(nom_quartier) |>
  dplyr::summarise(maj = median(max), .groups = "drop")
cat(sprintf(
  "\nLe quartier le plus cher (**%s**, %.1f €/m²) dépasse le moins cher (**%s**, %.1f €/m²) de **%s**.\n",
  q$nom_quartier[which.max(q$maj)], max(q$maj),
  q$nom_quartier[which.min(q$maj)], min(q$maj),
  scales::percent(max(q$maj) / min(q$maj) - 1, 1)))

Le quartier le plus cher (Ecole-Militaire, 39.3 €/m²) dépasse le moins cher (Amérique, 26.7 €/m²) de 47%.

Source & données

Code
tibble::tibble(
  ` ` = c("Jeu de données", "Producteur", "Licence", "Fichier", "Mise à jour", "Observations"),
  `  ` = c(
    "[Logement encadrement des loyers](https://opendata.paris.fr/explore/dataset/logement-encadrement-des-loyers/)",
    "Loyers fixés par arrêté du préfet de la région d'Île-de-France ; diffusion : Ville de Paris (opendata.paris.fr)",
    "Open Database License (ODbL)",
    sprintf("`data/paris/logement-encadrement-des-loyers.parquet` (%s ko)",
            round(file.size(loyers_path) / 1024)),
    format(as.Date(file.mtime(loyers_path)), "%d %B %Y"),
    sprintf("%s lignes, %d à %d", nrow(loyers), min(loyers$annee), max(loyers$annee))
  )
) |>
  gt::gt() |>
  gt::fmt_markdown(columns = `  `) |>
  gt::tab_options(table.width = "100%", column_labels.hidden = TRUE)
Jeu de données Logement encadrement des loyers
Producteur Loyers fixés par arrêté du préfet de la région d’Île-de-France ; diffusion : Ville de Paris (opendata.paris.fr)
Licence Open Database License (ODbL)
Fichier data/paris/logement-encadrement-des-loyers.parquet (173 ko)
Mise à jour 04 septembre 2026
Observations 17920 lignes, 2019 à 2025