Occupation des créneaux sportifs municipaux

Données - Ville de Paris

Code
source(here::here("_rinit.R"))
source(here::here("data", "paris", "_paris.R"))   # wkb2sfc(), paris_basemap(), arrondissements_sf()
invisible(Sys.setlocale("LC_TIME", "fr_FR.UTF-8"))
# cd ~/iCloud/website/data/paris; /usr/local/bin/R -e 'quarto::quarto_render("occupation-des-creneaux-sportifs.qmd")'

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

cren_path <- here::here("data", "paris", "occupation-des-creneaux-sportifs.parquet")
jours_fr <- c("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche")

cren <- arrow::read_parquet(cren_path) |>
  dplyr::mutate(
    jour  = factor(jour_de_la_semaine, levels = jours_fr,
                   labels = stringr::str_to_title(jours_fr)),
    heure = as.integer(substr(heure_de_debut_du_creneau, 1, 2)),
    ar    = suppressWarnings(as.integer(substr(code_postal_de_l_equipement, 4, 5)))
  )

n_tot      <- nrow(cren)
n_equip    <- dplyr::n_distinct(cren$nom_de_l_equipement)
n_clubs    <- dplyr::n_distinct(cren$nom_de_l_association_club)
arr_sf     <- arrondissements_sf()

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())
theme_carte <- theme_void(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        plot.subtitle = element_text(colour = "grey35"),
        plot.margin = margin(5, 12, 5, 5))

L’emploi du temps hebdomadaire des équipements sportifs municipaux mis à disposition des clubs et associations : 18 768 créneaux récurrents, répartis sur 372 équipements (gymnases, piscines, courts de tennis…) et 1 750 clubs/associations.

Code
cren |>
  dplyr::filter(!is.na(nom_de_l_association_club)) |>
  dplyr::transmute(
    Club = nom_de_l_association_club, Discipline = discipline, Jour = jour,
    Horaire = paste0(heure_de_debut_du_creneau, "–", heure_de_fin_du_creneau),
    Équipement = nom_de_l_equipement, Payant = payant
  ) |>
  dplyr::slice_sample(n = 8) |>
  gt::gt() |>
  gt::tab_header(title = "8 créneaux tirés au hasard") |>
  gt::tab_options(table.width = "100%")
8 créneaux tirés au hasard
Club Discipline Jour Horaire Équipement Payant
ART ET CULTURE 15 TENNIS Dimanche 10:00:00–11:00:00 TEP René et André Mourlon oui
ASSOCIATION SPORTIVE MAINE MONTPARNASSE -(ASSM) TENNIS Lundi 18:00:00–19:00:00 Tennis du Commandant Mouchotte non
SPORTS LOISIRS EDUCATIFS CULTURELS 16EME TENNIS Mardi 20:00:00–21:00:00 TEP Suchet - TEP Couvert oui
AGAM ASSOCIATION POUR LA GYMNASTIQUE ET LES ARTS MARTIAUX TAEKWONDO Jeudi 20:30:00–22:30:00 Salles de sport Marguerite Broquedis (ex Reims) oui
PARIS AMICALE CAMOU VOLLEY BALL Mercredi 16:30:00–18:30:00 Gymnase Dupleix oui
CLUB D'ESCRIME DE LA TOUR D'AUVERGNE TA ESCRIME ARTISTIQUE Vendredi 12:00:00–14:00:00 Salles de sport Buffault non
WADO PARIS MONTROUGE KARATE JUTSU ARTS MARTIAUX Samedi 10:00:00–12:00:00 Gymnase des Grands Moulins oui
TENNIS CLUB DU XIII TC XIII TENNIS Mardi 20:00:00–21:00:00 Tennis Charles Moureu non

Quand et quoi

Code
cren |>
  dplyr::count(discipline, sort = TRUE) |>
  head(15) |>
  dplyr::mutate(discipline = forcats::fct_reorder(stringr::str_to_title(discipline), n)) |>
  ggplot(aes(n, discipline)) +
  geom_col(fill = "#2f6f4f", width = 0.7) +
  geom_text(aes(label = n), hjust = -0.2, size = 3) +
  scale_x_continuous(expand = expansion(mult = c(0, 0.1))) +
  labs(title = "15 disciplines les plus programmées",
       subtitle = "Le tennis représente à lui seul plus d'un quart des créneaux",
       x = NULL, y = NULL) +
  theme_barres

Code
cren |>
  dplyr::filter(!is.na(heure)) |>
  dplyr::count(jour, heure) |>
  ggplot(aes(heure, jour, fill = n)) +
  geom_tile(colour = "white", linewidth = 0.4) +
  scale_fill_viridis_c(option = "mako", direction = -1, name = "Créneaux") +
  scale_x_continuous(breaks = seq(6, 23, 2)) +
  scale_y_discrete(limits = rev(levels(cren$jour))) +
  labs(title = "Quand ont lieu les créneaux",
       subtitle = "Les soirées de semaine (17h-20h) concentrent l'essentiel ; le mercredi élargit aussi sur l'après-midi",
       x = "Heure de début", y = NULL) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"), plot.subtitle = element_text(colour = "grey35"),
        panel.grid = element_blank())

Accès et tarification

Code
cren |>
  dplyr::filter(!is.na(payant)) |>
  dplyr::count(payant) |>
  dplyr::mutate(payant = ifelse(payant == "oui", "Payant", "Gratuit")) |>
  ggplot(aes(n, payant)) +
  geom_col(fill = "#3b6f8a", width = 0.55) +
  geom_text(aes(label = scales::percent(n / sum(n), 1)), hjust = -0.2, size = 3.5) +
  scale_x_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(title = "Créneaux payants vs gratuits", x = NULL, y = NULL) +
  theme_barres

Code
cren |>
  dplyr::filter(!is.na(prix_moyen_annuel_en_eur), prix_moyen_annuel_en_eur > 0,
                prix_moyen_annuel_en_eur < 1000) |>
  ggplot(aes(prix_moyen_annuel_en_eur)) +
  geom_histogram(bins = 40, fill = "#3b6f8a") +
  scale_x_continuous(labels = scales::label_number(suffix = " €")) +
  labs(title = "Prix moyen annuel, créneaux payants",
       subtitle = "Cotisation annuelle indicative renseignée par le club, hors valeurs extrêmes",
       x = NULL, y = "Créneaux") +
  theme_barres

Code
cren |>
  dplyr::filter(!is.na(genre)) |>
  dplyr::count(genre, sort = TRUE) |>
  dplyr::mutate(genre = forcats::fct_reorder(genre, n)) |>
  ggplot(aes(n, genre)) +
  geom_col(fill = "#8a5a2b", width = 0.55) +
  geom_text(aes(label = scales::percent(n / sum(n), 1)), hjust = -0.2, size = 3.5) +
  scale_x_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(title = "Créneaux mixtes, non-mixtes hommes/femmes",
       x = NULL, y = NULL) +
  theme_barres

Code
cnt <- cren |> dplyr::filter(!is.na(ar)) |> dplyr::count(ar, name = "n")

arr_sf |>
  dplyr::left_join(cnt, by = c("c_ar" = "ar")) |>
  ggplot() +
  geom_sf(aes(fill = n), colour = "white", linewidth = 0.2) +
  scale_fill_viridis_c(option = "rocket", direction = -1, na.value = "grey92", name = "Créneaux") +
  labs(title = "Créneaux sportifs par arrondissement (adresse de l'équipement)",
       subtitle = "15ᵉ, 12ᵉ et 14ᵉ en tête — arrondissements les plus dotés en équipements") +
  theme_carte

Clubs et équipements les plus actifs

Code
cren |>
  dplyr::filter(!is.na(nom_de_l_association_club)) |>
  dplyr::count(nom_de_l_association_club, sort = TRUE) |>
  head(8) |>
  dplyr::mutate(nom_de_l_association_club = forcats::fct_reorder(
    stringr::str_trunc(nom_de_l_association_club, 40), n)) |>
  ggplot(aes(n, nom_de_l_association_club)) +
  geom_col(fill = "#7a1f3d", width = 0.65) +
  geom_text(aes(label = n), hjust = -0.2, size = 3) +
  scale_x_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(title = "Clubs avec le plus de créneaux", x = NULL, y = NULL) +
  theme_barres

Rechercher un créneau

Table cherchable : recherche plein texte, filtres par colonne sous chaque en-tête, tri en cliquant sur l’en-tête.

Code
cren |>
  dplyr::transmute(
    Club = nom_de_l_association_club, Discipline = stringr::str_to_title(discipline),
    Jour = as.character(jour),
    Horaire = paste0(heure_de_debut_du_creneau, "–", heure_de_fin_du_creneau),
    Équipement = nom_de_l_equipement, Payant = payant, Genre = genre
  ) |>
  reactable::reactable(
    searchable = TRUE,
    filterable = TRUE,
    pagination = TRUE,
    defaultPageSize = 25,
    showPageSizeOptions = TRUE,
    pageSizeOptions = c(10, 25, 50, 100),
    compact = TRUE,
    resizable = TRUE,
    highlight = TRUE,
    wrap = TRUE,
    defaultColDef = reactable::colDef(
      align = "left",
      headerStyle = list(fontWeight = "bold"),
      style = list(whiteSpace = "normal", wordBreak = "break-word")
    ),
    columns = list(
      Club = reactable::colDef(minWidth = 180),
      Discipline = reactable::colDef(minWidth = 110, maxWidth = 160),
      Jour = reactable::colDef(minWidth = 80, maxWidth = 110),
      Horaire = reactable::colDef(minWidth = 90, maxWidth = 120),
      `Équipement` = reactable::colDef(minWidth = 160),
      Payant = reactable::colDef(minWidth = 60, maxWidth = 90),
      Genre = reactable::colDef(minWidth = 70, maxWidth = 100)
    )
  )

Source & données

Code
tibble::tibble(
  ` ` = c("Jeu de données", "Producteur", "Licence", "Fichiers", "Mise à jour", "Observations"),
  `  ` = c(
    "[Occupation des créneaux sportifs](https://opendata.paris.fr/explore/dataset/occupation-des-creneaux-sportifs/information/)",
    "Direction de la Jeunesse et des Sports — Ville de Paris",
    "Open Database License (ODbL)",
    sprintf("`data/paris/occupation-des-creneaux-sportifs.parquet` (%s ko) + `arrondissements.parquet`",
            round(file.size(cren_path) / 1024)),
    format(as.Date(file.mtime(cren_path)), "%d %B %Y"),
    sprintf("%s créneaux, %d équipements, %s clubs", format(n_tot, big.mark = " "), n_equip,
            format(n_clubs, big.mark = " "))
  )
) |>
  gt::gt() |> gt::fmt_markdown(columns = `  `) |>
  gt::tab_options(table.width = "100%", column_labels.hidden = TRUE)
Jeu de données Occupation des créneaux sportifs
Producteur Direction de la Jeunesse et des Sports — Ville de Paris
Licence Open Database License (ODbL)
Fichiers data/paris/occupation-des-creneaux-sportifs.parquet (344 ko) + arrondissements.parquet
Mise à jour 04 septembre 2026
Observations 18 768 créneaux, 372 équipements, 1 750 clubs