{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Présence scolaire et programme de cantine\n",
    "\n",
    "*Présence scolaire, 2024 · R*\n",
    "\n",
    "Cassion · data-analysis.cassion.dev\n",
    "\n",
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/thecassion/cassion-learning-platform/blob/main/apps/data-analysis/public/datasets/examples/school-attendance-2024/feeding-attendance.r.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La question, et la réponse trop facile\n",
    "\n",
    "Les écoles dotées d'un programme de cantine ont-elles une meilleure présence ?\n",
    "Les moyennes brutes répondent oui, d'environ cinq points. L'intérêt est de savoir\n",
    "combien de ces cinq points survivent à une question correctement posée.\n",
    "\n",
    "Tous les jeux de données de cette plateforme sont synthétiques. Aucun élève réel\n",
    "n'est représenté.\n",
    "\n",
    "## Mise en place"
   ],
   "id": "cell-001"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| message: false\n",
    "library(readr)\n",
    "library(dplyr)\n",
    "library(tidyr)\n",
    "library(ggplot2)\n",
    "\n",
    "BASE <- \"https://data-analysis.cassion.dev/datasets/files/\"\n",
    "\n",
    "attendance <- read_csv(paste0(BASE, \"school-attendance-2024.v1.csv\"),\n",
    "  col_types = cols(student_id = col_character(), present = col_character(),\n",
    "                   attendance_date = col_date()))\n",
    "\n",
    "roster <- read_csv(paste0(BASE, \"school-roster-2024.v1.csv\"),\n",
    "  col_types = cols(student_id = col_character(), school_id = col_character(),\n",
    "                   .default = col_guess()))\n",
    "\n",
    "dim(attendance); dim(roster)"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Nettoyer d'abord le booléen et les doubles inscriptions\n",
    "\n",
    "Une école a employé `Y` et `N` ; deux élèves figurent deux fois sur la liste\n",
    "après un transfert jamais désinscrit."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count(attendance, present)"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "attendance <- attendance |>\n",
    "  mutate(present_clean = case_when(\n",
    "    tolower(trimws(present)) %in% c(\"true\", \"y\", \"yes\")  ~ TRUE,\n",
    "    tolower(trimws(present)) %in% c(\"false\", \"n\", \"no\")  ~ FALSE,\n",
    "    TRUE ~ NA\n",
    "  ))\n",
    "\n",
    "roster_resolved <- roster |>\n",
    "  arrange(is.na(grade)) |>          # garder l'inscription qui porte un niveau\n",
    "  distinct(student_id, .keep_all = TRUE)\n",
    "\n",
    "cat(\"lignes de liste :\", nrow(roster), \" eleves uniques :\", nrow(roster_resolved), \"\\n\")\n",
    "\n",
    "daily <- attendance |>\n",
    "  inner_join(roster_resolved, by = \"student_id\") |>\n",
    "  filter(!is.na(present_clean))\n",
    "\n",
    "cat(\"jours-eleves analyses :\", nrow(daily), \"\\n\")"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exclure la grève pour qu'une fermeture ne se lise pas comme une absence\n",
    "\n",
    "Deux écoles n'ont aucune ligne de présence pour quinze jours de classe en mars.\n",
    "Les lignes étant absentes plutôt que fausses, elles ne tirent pas la moyenne vers\n",
    "le bas — mais toute analyse qui réindexerait sur un calendrier complet les\n",
    "transformerait en absences, et l'une des deux écoles dispose d'une cantine."
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "school_days <- daily |>\n",
    "  group_by(school_id) |>\n",
    "  summarise(days = n_distinct(attendance_date), .groups = \"drop\") |>\n",
    "  arrange(days)\n",
    "\n",
    "head(school_days, 4)"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "closed <- school_days |> filter(days < max(days)) |> pull(school_id)\n",
    "\n",
    "roster_resolved |>\n",
    "  filter(school_id %in% closed) |>\n",
    "  distinct(school_id, feeding_programme)"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "strike_window <- as.Date(c(\"2024-03-11\", \"2024-03-29\"))\n",
    "\n",
    "no_strike <- daily |>\n",
    "  filter(!(attendance_date >= strike_window[1] & attendance_date <= strike_window[2]))\n",
    "\n",
    "bind_rows(\n",
    "  daily     |> group_by(feeding_programme) |> summarise(scope = \"all days\",           rate = mean(present_clean), .groups = \"drop\"),\n",
    "  no_strike |> group_by(feeding_programme) |> summarise(scope = \"strike window dropped\", rate = mean(present_clean), .groups = \"drop\")\n",
    ") |>\n",
    "  mutate(rate = round(rate, 4)) |>\n",
    "  pivot_wider(names_from = feeding_programme, values_from = rate)"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L'écart bouge à peine, ce qui est le résultat rassurant : la fermeture est\n",
    "invisible parce que les lignes n'ont jamais été écrites. Faites tout de même la\n",
    "vérification — c'est ainsi que l'on constate que la fermeture a été correctement\n",
    "traitée, au lieu de le supposer.\n",
    "\n",
    "## La comparaison qui se surestime"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "daily |>\n",
    "  group_by(feeding_programme) |>\n",
    "  summarise(\n",
    "    student_days = n(),\n",
    "    attendance   = round(mean(present_clean), 4),\n",
    "    .groups = \"drop\"\n",
    "  )"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cinq points, sur soixante-dix mille observations. Il est tentant de tester cela\n",
    "directement, et la valeur p serait spectaculaire — et dépourvue de sens.\n",
    "\n",
    "**Le programme de cantine est attribué aux écoles, non aux élèves.** Soixante-dix\n",
    "mille jours-élèves ne constituent pas soixante-dix mille observations\n",
    "indépendantes du programme : il y en a vingt-quatre. Tester au niveau du\n",
    "jour-élève traite chaque enfant d'une école comme une preuve indépendante\n",
    "concernant le programme de cette école, et c'est ainsi qu'un petit effet acquiert\n",
    "une valeur p invraisemblable."
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "school_means <- daily |>\n",
    "  group_by(school_id, feeding_programme) |>\n",
    "  summarise(students = n_distinct(student_id), attendance = mean(present_clean), .groups = \"drop\")\n",
    "\n",
    "school_means |>\n",
    "  group_by(feeding_programme) |>\n",
    "  summarise(\n",
    "    schools   = n(),\n",
    "    mean_rate = round(mean(attendance), 4),\n",
    "    sd        = round(sd(attendance), 4),\n",
    "    .groups = \"drop\"\n",
    "  )"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tester à l'unité à laquelle le programme a été attribué"
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t.test(attendance ~ feeding_programme, data = school_means)"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L'écart survit — environ cinq points — mais l'intervalle de confiance s'étend\n",
    "d'environ 1,6 à 8,9 points. C'est la précision honnête d'une comparaison entre\n",
    "neuf écoles et quinze, et c'est une affirmation très différente de « la cantine\n",
    "augmente la présence de 5,0 points »."
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| fig-width: 7\n",
    "#| fig-height: 4.5\n",
    "ggplot(school_means, aes(x = feeding_programme, y = 100 * attendance)) +\n",
    "  geom_boxplot(width = 0.45, outlier.shape = NA, colour = \"#5A6B66\") +\n",
    "  geom_jitter(width = 0.09, size = 2.2, colour = \"#2F5D50\", alpha = 0.8) +\n",
    "  labs(\n",
    "    x = \"Programme de cantine\", y = \"Presence (%)\",\n",
    "    title = \"Chaque point est une ecole, non un eleve\",\n",
    "    subtitle = \"24 ecoles : voila la taille d'echantillon de cette question\"\n",
    "  ) +\n",
    "  theme_minimal(base_size = 11) +\n",
    "  theme(panel.grid.minor = element_blank())"
   ],
   "id": "cell-017"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le recouvrement entre les deux groupes constitue le constat. Plusieurs écoles\n",
    "sans cantine affichent une meilleure présence que plusieurs écoles qui en ont\n",
    "une : le programme n'est donc pas le seul facteur de présence — et une décision\n",
    "d'intervention au niveau des écoles doit le savoir.\n",
    "\n",
    "## Ce que cela ne peut pas établir\n",
    "\n",
    "Les écoles n'ont pas été attribuées au programme de façon aléatoire. Si la\n",
    "cantine est allée vers des écoles déjà mieux gérées, mieux desservies ou aux\n",
    "parents plus impliqués, cette comparaison mesure aussi ces facteurs."
   ],
   "id": "cell-018"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "school_means |>\n",
    "  arrange(desc(attendance)) |>\n",
    "  mutate(rank = row_number()) |>\n",
    "  select(rank, school_id, feeding_programme, students, attendance) |>\n",
    "  mutate(attendance = round(attendance, 3)) |>\n",
    "  head(10)"
   ],
   "id": "cell-019"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Rien dans ce jeu de données ne permet de séparer le programme de ce qui a\n",
    "sélectionné les écoles pour en bénéficier. La phrase défendable est « les écoles\n",
    "dotées d'une cantine présentent une assiduité supérieure d'environ cinq points,\n",
    "IC 95 % de 1,6 à 8,9, dans une comparaison non appariée de 24 écoles » — et non\n",
    "« la cantine augmente la présence de cinq points ».\n",
    "\n",
    "## Ce qu'il faut rapporter\n",
    "\n",
    "L'effet avec l'intervalle issu du test au niveau école, le nombre d'écoles de\n",
    "chaque côté, leur recouvrement, et la phrase indiquant que l'attribution n'était\n",
    "pas aléatoire. Une évaluation de programme qui rapporte une valeur p au niveau du\n",
    "jour-élève a répondu à une question que personne n'a posée."
   ],
   "id": "cell-020"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "R",
   "language": "R",
   "name": "ir"
  },
  "language_info": {
   "name": "R",
   "file_extension": ".r"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
