BonQuery
  • Home
  • Left in the Cold
  • Audits
    • INDEPENDENT OVERSIGHT
    • Discrepancy Tracker
    • Emergency Occupancy Audit
    • Central Intake Audit
    • About the Data
  • Shelter Dashboards
    • Inside Toronto’s Shelter System
    • Monthly System Flow Replication
    • Year-to-Date Capacity Match
    • Historical Trend Lines
    • Referral Requests Analysis
    • Daily Occupancy & Capacity
  • About
  • Accueil
  • Laissés dans le froid
  • Audits
    • SURVEILLANCE INDÉPENDANTE
    • Suivi des divergences
    • Audit de l’occupation d’urgence
    • Audit de l’admission centrale
    • À propos des données
  • Tableaux de bord
    • Le réseau de refuges de Toronto
    • Réplication du flux mensuel
    • Concordance année en cours
    • Tendances historiques
    • Analyse des demandes d’orientation
    • Occupation et capacité quotidiennes
  • À propos
  • FR
  • EN

Portrait mensuel

Réseau de refuges de Toronto — tableau de bord du mois en cours

Portrait mensuel interactif du réseau de refuges de Toronto — dénombrements de l’itinérance active, indicateurs de flux et répartitions par groupe de population pour tout mois depuis janvier 2018.

Auteur(-trice)

Miriam Marling

data = FileAttachment("../data/shelter_flow.json").json()
MON = ["janv.","févr.","mars","avr.","mai","juin",
       "juill.","août","sept.","oct.","nov.","déc."]
fmtN = d => d.toLocaleString("fr-CA")

reportingMonthOptions = data
  .filter(d => d.population_group === "All Population")
  .sort((a, b) => b.flow_date.localeCompare(a.flow_date))
  .map(d => {
    const [y, m] = d.flow_date.split("-").map(Number)
    return {
      label: new Date(y, m - 1, 15).toLocaleString("fr-CA", {month: "long", year: "numeric"}),
      value: d.flow_date
    }
  })

La Ville de Toronto publie mensuellement des données sur son réseau de refuges, et ses propres tableaux de bord présentent bien les chiffres principaux. Cette section va plus loin : filtres interactifs sur chaque graphique, une note explicite sur ce que les données couvrent et ce qu’elles excluent, et des conclusions qui restent habituellement enfouies dans la documentation technique.

Les données couvrent chaque mois de janvier 2018 au mois le plus récemment publié, directement issues du jeu de données Flux du réseau de refuges de la Ville de Toronto. Les nouvelles données se chargent automatiquement chaque mois, généralement vers le 15, après que la Ville publie les chiffres du mois précédent.

viewof reportingDate = Inputs.select(reportingMonthOptions, {
  label: "Mois de référence",
  format: d => d.label,
  value: reportingMonthOptions[0]
})
snapshot   = data.find(d => d.flow_date === reportingDate.value && d.population_group === "All Population")
chronicRow = data.find(d => d.flow_date === reportingDate.value && d.population_group === "Chronic")

activelyHomeless = snapshot ? snapshot.actively_homeless : 0
inflowTotal  = snapshot ? snapshot.newly_identified + snapshot.returned_from_housing + snapshot.returned_to_shelter : 0
outflowTotal = snapshot ? snapshot.moved_to_housing + snapshot.became_inactive : 0
netChange    = inflowTotal - outflowTotal

reportingYear  = +reportingDate.value.slice(0, 4)
reportingMonth = +reportingDate.value.slice(5, 7)

html`<div class="kpi-hero">
  <div class="kpi-box">
    <div class="kpi-label">Personnes en situation d'itinérance active au cours des 3 derniers mois</div>
    <div class="kpi-value">${activelyHomeless.toLocaleString("fr-CA")}</div>
  </div>
</div>`
html`<div class="kpi-row">
  <div class="kpi-card" style="background:var(--bq-kpi-inflow-bg);">
    <div class="kpi-label">Entrées dans le réseau de refuges</div>
    <div class="kpi-value" style="color:var(--bq-kpi-inflow);">+${inflowTotal.toLocaleString("fr-CA")}</div>
  </div>
  <div class="kpi-card" style="background:${netChange > 0 ? 'var(--bq-kpi-net-pos-bg)' : netChange < 0 ? 'var(--bq-kpi-net-neg-bg)' : 'var(--bq-kpi-net-zero-bg)'};">
    <div class="kpi-label">Variation (Entrées − Sorties)</div>
    <div class="kpi-value" style="color:${netChange > 0 ? 'var(--bq-kpi-net-pos)' : netChange < 0 ? 'var(--bq-kpi-net-neg)' : 'var(--bq-kpi-net-zero)'};">
      ${netChange >= 0 ? '+' : ''}${netChange.toLocaleString("fr-CA")}
    </div>
  </div>
  <div class="kpi-card" style="background:var(--bq-kpi-outflow-bg);">
    <div class="kpi-label">Sorties du réseau de refuges</div>
    <div class="kpi-value" style="color:var(--bq-kpi-outflow);">−${outflowTotal.toLocaleString("fr-CA")}</div>
  </div>
</div>`

Itinérance chronique

Note : Oracle APEX affiche ce graphique sous forme de donut. Observable Plot ne prend pas en charge les graphiques circulaires nativement — présenté ici sous forme de barre de pourcentage avec les mêmes données et couleurs.

chronicPct    = chronicRow ? chronicRow.population_group_pct / 100 : 0
_cs = getComputedStyle(document.body)
_nonChronicFill = _cs.getPropertyValue("--bq-bg-alt").trim() || "#CCCCCC"
_chartFg = _cs.getPropertyValue("--bq-fg").trim() || "#333"
_chartStroke = _cs.getPropertyValue("--bq-chart-stroke").trim() || "#333"

chronicData   = [
  {label: "Chronique",     pct: chronicPct,       fill: "#5856D6"},
  {label: "Non chronique", pct: 1 - chronicPct,   fill: _nonChronicFill}
]

Plot.plot({
  width,
  height: 90,
  marginLeft: 10,
  marginRight: 10,
  x: {domain: [0, 1], axis: null},
  y: {axis: null},
  color: {domain: ["Chronique","Non chronique"], range: ["#5856D6", _nonChronicFill], legend: true},
  marks: [
    Plot.barX(chronicData, Plot.stackX({x: "pct", y: () => "", fill: "label", inset: 0})),
    Plot.text(chronicData, Plot.stackX({
      x: "pct",
      y: () => "",
      text: d => d.pct >= 0.04 ? `${d.label}\n${(d.pct * 100).toFixed(1)} %` : "",
      fill: d => d.label === "Chronique" ? "white" : _chartFg,
      textAnchor: "middle",
      lineWidth: 8,
      fontSize: 13,
      fontWeight: "600"
    }))
  ]
})

Comparaison cumulative annuelle : Nouvellement repérés

ytdComputed = {
  const rows = data
    .filter(d => d.population_group === "All Population")
    .filter(d => {
      const y = +d.flow_date.slice(0, 4)
      const m = +d.flow_date.slice(5, 7)
      return (y === reportingYear || y === reportingYear - 1) && m <= reportingMonth
    })
    .sort((a, b) => a.flow_date.localeCompare(b.flow_date))

  const accNI = {}, accMH = {}
  return rows.map(d => {
    const y = +d.flow_date.slice(0, 4)
    const m = +d.flow_date.slice(5, 7)
    accNI[y] = (accNI[y] || 0) + d.newly_identified
    accMH[y] = (accMH[y] || 0) + d.moved_to_housing
    return {
      year: y, month: m,
      monthLabel: MON[m - 1],
      yearLabel: String(y),
      isCurrentYear: y === reportingYear,
      ytd_ni: accNI[y],
      ytd_mh: accMH[y]
    }
  })
}

ytdNI = ytdComputed
  .sort((a, b) => a.month - b.month || a.year - b.year)
  .map(d => ({...d, barLabel: `${d.monthLabel} ${d.yearLabel}`}))

ytdNIDomain = ytdNI.map(d => d.barLabel)

Plot.plot({
  width,
  height: 60 + ytdNI.length * 22,
  marginLeft: 100,
  marginRight: 80,
  x: {label: "Nouvellement repérés cumulatifs (ann. cours)", grid: true, tickFormat: fmtN},
  y: {label: null, domain: ytdNIDomain},
  color: {
    domain: [String(reportingYear - 1), String(reportingYear)],
    range: ["#FFB3C1", "#FF2D55"],
    legend: true
  },
  marks: [
    Plot.ruleX([0]),
    Plot.barX(ytdNI, {x: "ytd_ni", y: "barLabel", fill: "yearLabel", tip: true,
                      insetTop: 1, insetBottom: 1}),
    Plot.text(ytdNI, {
      x: "ytd_ni", y: "barLabel",
      text: d => d.ytd_ni.toLocaleString("fr-CA"),
      textAnchor: "start", dx: 5, fontSize: 11
    })
  ]
})

Comparaison cumulative annuelle : Accès au logement permanent

ytdMH = ytdComputed
  .sort((a, b) => a.month - b.month || a.year - b.year)
  .map(d => ({...d, barLabel: `${d.monthLabel} ${d.yearLabel}`}))

Plot.plot({
  width,
  height: 60 + ytdMH.length * 22,
  marginLeft: 100,
  marginRight: 80,
  x: {label: "Accès au logement permanent cumulatif (ann. cours)", grid: true, tickFormat: fmtN},
  y: {label: null, domain: ytdMH.map(d => d.barLabel)},
  color: {
    domain: [String(reportingYear - 1), String(reportingYear)],
    range: ["#AED5AE", "#5BA75B"],
    legend: true
  },
  marks: [
    Plot.ruleX([0]),
    Plot.barX(ytdMH, {x: "ytd_mh", y: "barLabel", fill: "yearLabel", tip: true,
                      insetTop: 1, insetBottom: 1}),
    Plot.text(ytdMH, {
      x: "ytd_mh", y: "barLabel",
      text: d => d.ytd_mh.toLocaleString("fr-CA"),
      textAnchor: "start", dx: 5, fontSize: 11
    })
  ]
})

Répartition des entrées

inflowBreakdown = snapshot ? [
  {label: "Nouvellement repérés",           value: snapshot.newly_identified,      fill: "#E63946"},
  {label: "Retour du logement permanent",   value: snapshot.returned_from_housing, fill: "#FFD600"},
  {label: "Retour au refuge",               value: snapshot.returned_to_shelter,   fill: "#FB8C00"}
].sort((a, b) => b.value - a.value) : []

inflowMax = d3.max(inflowBreakdown, d => d.value) || 1

{
  const isMobile = width < 650
  const colorOpts = isMobile ? {
    color: {
      domain: inflowBreakdown.map(d => d.label),
      range:  inflowBreakdown.map(d => d.fill),
      legend: true
    }
  } : {}
  return Plot.plot({
    width,
    height: isMobile ? 190 : 130,
    marginLeft: isMobile ? 10 : 240,
    marginRight: 80,
    x: {label: "Personnes", grid: true, tickFormat: fmtN, domain: [0, inflowMax * 1.15]},
    y: {label: null, domain: inflowBreakdown.map(d => d.label), axis: isMobile ? null : "left"},
    ...colorOpts,
    marks: [
      Plot.ruleX([0]),
      Plot.barX(inflowBreakdown, {
        x: "value", y: "label",
        fill: isMobile ? "label" : "fill",
        insetTop: 3, insetBottom: 3
      }),
      Plot.text(inflowBreakdown, {
        x: "value", y: "label",
        text: d => d.value.toLocaleString("fr-CA"),
        textAnchor: "start", dx: 6, fontSize: 12
      })
    ]
  })
}

Répartition des sorties

outflowBreakdown = snapshot ? [
  {label: "Accès au logement permanent", value: snapshot.moved_to_housing, fill: "#66CC66"},
  {label: "Devenus inactifs",            value: snapshot.became_inactive,  fill: "#512DA8"}
].sort((a, b) => b.value - a.value) : []

outflowMax = d3.max(outflowBreakdown, d => d.value) || 1

{
  const isMobile = width < 650
  const colorOpts = isMobile ? {
    color: {
      domain: outflowBreakdown.map(d => d.label),
      range:  outflowBreakdown.map(d => d.fill),
      legend: true
    }
  } : {}
  return Plot.plot({
    width,
    height: isMobile ? 160 : 100,
    marginLeft: isMobile ? 10 : 220,
    marginRight: 80,
    x: {label: "Personnes", grid: true, tickFormat: fmtN, domain: [0, outflowMax * 1.15]},
    y: {label: null, domain: outflowBreakdown.map(d => d.label), axis: isMobile ? null : "left"},
    ...colorOpts,
    marks: [
      Plot.ruleX([0]),
      Plot.barX(outflowBreakdown, {
        x: "value", y: "label",
        fill: isMobile ? "label" : "fill",
        insetTop: 3, insetBottom: 3
      }),
      Plot.text(outflowBreakdown, {
        x: "value", y: "label",
        text: d => d.value.toLocaleString("fr-CA"),
        textAnchor: "start", dx: 6, fontSize: 12
      })
    ]
  })
}

Âge des personnes en situation d’itinérance active au cours des 3 derniers mois

ageBandDefs2 = [
  {key: "age_under_16", label: "Moins de 16 ans"},
  {key: "age_16_24",    label: "16–24"},
  {key: "age_25_34",    label: "25–34"},
  {key: "age_35_44",    label: "35–44"},
  {key: "age_45_54",    label: "45–54"},
  {key: "age_55_64",    label: "55–64"},
  {key: "age_65_over",  label: "65+"}
]

ageSnapshotData = (() => {
  if (!snapshot) return []
  const total = ageBandDefs2.reduce((s, b) => s + snapshot[b.key], 0)
  return ageBandDefs2.map(b => ({
    band: b.label,
    pct: total > 0 ? snapshot[b.key] / total : 0
  }))
})()

ageSnapMax = d3.max(ageSnapshotData, d => d.pct) || 0.4

Plot.plot({
  width,
  height: 340,
  marginLeft: 60,
  marginTop: 30,
  x: {label: null, domain: ageBandDefs2.map(b => b.label)},
  y: {label: null, axis: null, domain: [0, ageSnapMax * 1.4]},
  color: {domain: ageBandDefs2.map(b => b.label), legend: true},
  marks: [
    Plot.barY(ageSnapshotData, {x: "band", y: "pct", fill: "band"}),
    Plot.text(ageSnapshotData, {
      x: "band", y: "pct",
      text: d => (d.pct * 100).toFixed(1) + " %",
      dy: -10, fontSize: 13, fontWeight: "600", textAnchor: "middle"
    }),
    Plot.ruleY([0])
  ]
})

Genre des personnes en situation d’itinérance active au cours des 3 derniers mois

genderDefs2 = [
  {key: "gender_male",                label: "Hommes"},
  {key: "gender_female",              label: "Femmes"},
  {key: "gender_trans_nb_two_spirit", label: "Trans, non binaires ou bispirituels"}
]

genderSnapshotData = (() => {
  if (!snapshot) return []
  const total = genderDefs2.reduce((s, g) => s + snapshot[g.key], 0)
  return genderDefs2
    .map(g => ({gender: g.label, pct: total > 0 ? snapshot[g.key] / total : 0}))
    .sort((a, b) => b.pct - a.pct)
})()

genderSnapMax = d3.max(genderSnapshotData, d => d.pct) || 1

{
  const isMobile = width < 650
  return Plot.plot({
    width,
    height: isMobile ? 160 : 130,
    marginLeft: isMobile ? 10 : 260,
    marginRight: 80,
    x: {label: "Part du total par genre", grid: true,
        tickFormat: d => (d * 100).toFixed(0) + " %",
        domain: [0, genderSnapMax * 1.15]},
    y: {label: null, domain: genderSnapshotData.map(d => d.gender), axis: isMobile ? null : "left"},
    color: {legend: true},
    marks: [
      Plot.ruleX([0]),
      Plot.barX(genderSnapshotData, {x: "pct", y: "gender", fill: "gender",
                                     insetTop: 3, insetBottom: 3}),
      Plot.text(genderSnapshotData, {
        x: "pct", y: "gender",
        text: d => (d.pct * 100).toFixed(1) + " %",
        textAnchor: "start", dx: 6, fontSize: 12
      })
    ]
  })
}

Consulter les autres tableaux de bord :

  • Tendances historiques
  • Comparaison annuelle cumulative

Ce tableau de bord est également disponible en tant que tableau de bord Oracle APEX reposant sur la même base de données — utile pour voir comment les mêmes données s’affichent dans la plateforme native à faible code d’Oracle, ou pour explorer le SQL et la configuration APEX sous-jacents.

Comment lire ces données

Ces chiffres reflètent le réseau de refuges, et non l’ensemble du phénomène de l’itinérance. Les données recensent les personnes ayant utilisé un service d’hébergement de nuit financé par la Ville au moins une fois au cours des trois derniers mois. Elles n’incluent pas les personnes qui dorment à l’extérieur, dans des refuges non financés par la Ville, ou qui séjournent temporairement chez des amis ou de la famille. La Ville estime qu’environ 18 % des personnes en situation d’itinérance absolue à Toronto ne sont pas reflétées dans ces chiffres. Les tendances présentées ici reflètent les changements dans le réseau de refuges spécifiquement — et non dans l’ensemble de l’itinérance dans la ville.

Définitions clés

Les tableaux de bord utilisent quelques termes ayant des significations précises qu’il vaut la peine de connaître avant de lire les graphiques.

Itinérance active. Une personne ayant utilisé les services d’hébergement financés par la Ville au moins une fois au cours des trois derniers mois et dont le déménagement vers un logement permanent n’a pas été enregistré. C’est le chiffre principal de chaque tableau de bord. Comme il remonte à trois mois en arrière, ce dénombrement inclut des personnes qui peuvent ne pas être en refuge un soir donné.

Itinérance chronique. La définition fédérale : une personne ayant passé au moins 180 nuits en refuge au cours de l’année écoulée, ou au moins 546 nuits au cours des trois dernières années. Une personne peut satisfaire à cette définition tout en étant encore dans le réseau de refuges — elle n’a pas besoin d’être partie et revenue.

Catégories d’entrées (personnes intégrant le réseau de refuges ce mois-ci) :

  • Nouvellement repérés. Personnes entrant dans le réseau de refuges pour la première fois. Exception pour le groupe « Chronique » : dans ce cas, cette colonne comptabilise les personnes devenues en situation d’itinérance chronique au cours du mois de référence, quelle que soit la durée de leur utilisation antérieure du réseau.
  • Retour du logement permanent. Personnes ayant précédemment accédé à un logement permanent et revenues dans le réseau de refuges.
  • Retour au refuge. Personnes qui étaient dans le réseau, ne l’ont pas utilisé pendant au moins trois mois, et y sont revenues.

Catégories de sorties (personnes quittant le réseau de refuges ce mois-ci) :

  • Accès au logement permanent. Personnes ayant quitté le réseau de refuges pour un logement permanent.
  • Devenus inactifs. Personnes n’ayant pas utilisé les services d’hébergement au cours des trois derniers mois, y compris le mois de référence.

Définitions basées sur la page Données sur les flux du réseau de refuges de la Ville de Toronto.

Lire toutes les conclusions →

Source des données

Toutes les données proviennent du jeu de données Flux du réseau de refuges de Toronto de la Ville de Toronto, publié mensuellement sur le portail de données ouvertes de la Ville.

Contient des informations sous licence Licence du gouvernement ouvert – Toronto.


bayesian_gender = FileAttachment("../data/bayesian_gender.json").json()
shelter_flow    = data
CONSTAT CLÉ

Le groupe le plus touché — et le moins entendu

_allFlow = shelter_flow
  .filter(d => d.population_group === "All Population"
            && d.gender_male != null && d.gender_female != null)

_latestFlow = _allFlow.sort((a, b) => b.flow_date.localeCompare(a.flow_date))[0]

_menCount   = _latestFlow ? _latestFlow.gender_male : 0
_womenCount = _latestFlow ? _latestFlow.gender_female : 0
_transCount = _latestFlow ? _latestFlow.gender_trans_nb_two_spirit : 0
_ratio      = _womenCount > 0 ? (_menCount / _womenCount).toFixed(1) : "—"
_flowMonth  = _latestFlow
  ? new Date(_latestFlow.flow_date + "T00:00:00")
      .toLocaleString("fr-CA", {month: "long", year: "numeric"})
  : ""

function _mean(arr) { return arr.reduce((a, b) => a + b, 0) / arr.length }
function _sd(arr) {
  const m = _mean(arr)
  return Math.sqrt(arr.reduce((s, x) => s + (x - m) ** 2, 0) / (arr.length - 1))
}
function _se(arr) { return _sd(arr) / Math.sqrt(arr.length) }

function _Phi(z) {
  const c = [0.319381530, -0.356563782, 1.781477937, -1.821255978, 1.330274429]
  const t = 1 / (1 + 0.2316419 * Math.abs(z))
  const tail = t*(c[0]+t*(c[1]+t*(c[2]+t*(c[3]+t*c[4])))) * Math.exp(-z*z/2) / Math.sqrt(2*Math.PI)
  return z >= 0 ? 1 - tail : tail
}

_menSeries   = _allFlow.map(d => d.gender_male)
_womenSeries = _allFlow.map(d => d.gender_female)
_transSeries = _allFlow.map(d => d.gender_trans_nb_two_spirit).filter(v => v != null)

_menMean   = _mean(_menSeries);   _menSE   = _se(_menSeries)
_womenMean = _mean(_womenSeries); _womenSE = _se(_womenSeries)
_transMean = _mean(_transSeries); _transSE = _se(_transSeries)

_n1 = _menSeries.length;   _s1 = _sd(_menSeries)
_n2 = _womenSeries.length; _s2 = _sd(_womenSeries)
_n3 = _transSeries.length; _s3 = _sd(_transSeries)

function _wt(m1, s1, n1, m2, s2, n2) {
  const v1 = s1**2/n1, v2 = s2**2/n2
  const t  = (m1 - m2) / Math.sqrt(v1 + v2)
  const df = (v1 + v2)**2 / (v1**2/(n1-1) + v2**2/(n2-1))
  return {t, df}
}

_pw_mw = _wt(_menMean,   _s1, _n1, _womenMean, _s2, _n2)
_pw_mt = _wt(_menMean,   _s1, _n1, _transMean, _s3, _n3)
_pw_wt = _wt(_womenMean, _s2, _n2, _transMean, _s3, _n3)

function _pAdj(t) { return Math.min(1, 3 * 2 * _Phi(-Math.abs(t))) }
function _pLabel(t) {
  const p = _pAdj(t)
  return p < 0.0001 ? "< 0.0001 ***" : p < 0.001 ? "< 0.001 **" : p < 0.05 ? p.toFixed(3) + " *" : p.toFixed(3)
}

_yearRange = _allFlow.length > 0
  ? _allFlow[_allFlow.length - 1].flow_date.slice(0, 4) + "–" + _latestFlow.flow_date.slice(0, 4)
  : ""

_menShareSeries = _allFlow.map(d => d.gender_male / d.actively_homeless)
_menShareMinPct = Math.floor(Math.min(..._menShareSeries) * 100)

_gapAvg    = Math.round(_menMean - (_womenMean + _transMean))
_gapAvgStr = _gapAvg.toLocaleString("fr-CA")

_recordStartYear = _allFlow.map(d => d.flow_date.slice(0, 4)).sort()[0]
_recordEndYear   = _latestFlow.flow_date.slice(0, 4)
html`<div class="crisis-hero">
  <div class="hero-number">${_ratio}×</div>
  <div class="hero-text">
    <div class="hero-label">plus d'<strong>hommes</strong> que de femmes en situation d'itinérance active dans les refuges de Toronto</div>
    <div class="hero-sub"><strong>${_menCount.toLocaleString("fr-CA")} hommes</strong> · ${_womenCount.toLocaleString("fr-CA")} femmes · ${_transCount.toLocaleString("fr-CA")} personnes trans, non binaires ou bispirituelles · ${_flowMonth}</div>
  </div>
</div>`
_genderStats = [
  {label: "Hommes",      mean: _menMean,   ci_lo: _menMean   - 1.96 * _menSE,   ci_hi: _menMean   + 1.96 * _menSE,   isFocus: true},
  {label: "Femmes",      mean: _womenMean, ci_lo: _womenMean - 1.96 * _womenSE, ci_hi: _womenMean + 1.96 * _womenSE, isFocus: false},
  {label: "Trans/NB/2S", mean: _transMean, ci_lo: _transMean - 1.96 * _transSE, ci_hi: _transMean + 1.96 * _transSE, isFocus: false}
].sort((a, b) => b.mean - a.mean)

Plot.plot({
  width, height: 300, marginBottom: 36, marginLeft: 70, marginRight: 20,
  title: `Nombre mensuel moyen de personnes en itinérance active, par genre — ${_yearRange} (${_n1} mois)`,
  caption: "Les barres d'erreur indiquent les intervalles de confiance à 95 % de la moyenne mensuelle. Source : Données sur les flux du réseau de refuges de Toronto (open.toronto.ca).",
  x: {label: null, domain: _genderStats.map(d => d.label)},
  y: {label: "Moyenne d'individus en itinérance active / mois", grid: true,
      tickFormat: d => d.toLocaleString("fr-CA"),
      domain: [0, Math.max(..._genderStats.map(d => d.ci_hi)) * 1.12]},
  marks: [
    Plot.barY(_genderStats, {
      x: "label", y: "mean",
      fill: d => d.isFocus ? "#ff3b30" : "#4a6fa5",
      fillOpacity: d => d.isFocus ? 1 : 0.55
    }),
    Plot.ruleY(_genderStats, {
      x: "label", y1: "ci_lo", y2: "ci_hi",
      stroke: _chartStroke, strokeWidth: 1.8
    }),
    Plot.tickY(_genderStats, {
      x: "label", y: "ci_hi",
      stroke: _chartStroke, strokeWidth: 1.8,
      insetLeft: 18, insetRight: 18
    }),
    Plot.tickY(_genderStats, {
      x: "label", y: "ci_lo",
      stroke: _chartStroke, strokeWidth: 1.8,
      insetLeft: 18, insetRight: 18
    }),
    Plot.text(_genderStats, {
      x: "label", y: "ci_hi",
      text: d => Math.round(d.mean).toLocaleString("fr-CA"),
      dy: -10, textAnchor: "middle", fontSize: 13,
      fill: d => d.isFocus ? "#cc1f15" : _chartFg,
      fontWeight: "600"
    }),
    Plot.ruleY([0])
  ]
})
{
  const bg   = bayesian_gender
  const rr   = bg.rate_ratios
  const pd   = bg.pairwise_diffs
  const diag = bg.diagnostics
  const fmt1 = x => Number(x).toFixed(1)
  const fmtN = x => Math.round(x).toLocaleString("fr-CA")

  const bayesPairs = [
    {comp: "Hommes c. femmes",          rr: rr.men_vs_women,       diff: pd.men_vs_women,       note: false},
    {comp: "Hommes c. Trans/NB/2S",     rr: rr.men_vs_transnb2s,   diff: pd.men_vs_transnb2s,   note: true},
    {comp: "Femmes c. Trans/NB/2S",     rr: rr.women_vs_transnb2s, diff: pd.women_vs_transnb2s, note: true}
  ]
  const bayesRows = bayesPairs.map(p => html`<tr>
    <td style="padding:3px 10px 3px 0">${p.comp}${p.note ? html`<sup> †</sup>` : ""}</td>
    <td style="padding:3px 10px;text-align:right;font-variant-numeric:tabular-nums">
      ${fmt1(p.rr.median)}&times; (${fmt1(p.rr.hdi[0])}&ndash;${fmt1(p.rr.hdi[1])})
    </td>
    <td style="padding:3px 0;text-align:right;font-variant-numeric:tabular-nums;font-weight:${p.note?"400":"700"}">
      ${fmtN(p.diff.median)} (${fmtN(p.diff.hdi[0])}&ndash;${fmtN(p.diff.hdi[1])})
    </td>
  </tr>`)

  return html`<div class="bq-bayes">
    <p style="margin:0 0 0.3rem">
      <strong>Analyse bayésienne (régression binomiale négative, ajustée avec
      <code>brms</code> ${bg.brms_version} sur ${bg.n_months} mois de données) :</strong>
      Des a priori faiblement informatifs ont été utilisés.
      Diagnostics d'échantillonnage : R&#x0302;&nbsp;${diag.max_rhat},
      ESS&nbsp;${diag.min_ess_bulk.toLocaleString("fr-CA")},
      ${diag.n_divergent} transition(s) divergente(s).
    </p>
    <table>
      <thead><tr>
        <th style="text-align:left;padding:3px 10px 3px 0;font-weight:600">Comparaison</th>
        <th style="text-align:right;padding:3px 10px;font-weight:600">Rapport de taux (IDC 95 %)</th>
        <th style="text-align:right;padding:3px 0;font-weight:600">Différence de décompte (IDC 95 %)</th>
      </tr></thead>
      <tbody>${bayesRows}</tbody>
    </table>
    <p class="bq-note" style="margin:0 0 0.5rem">
      † Le groupe Trans/NB/2S représente en moyenne ~${Math.round(_transMean)} personnes par mois,
      un effectif réduit qui est également susceptible d'être sous-déclaré dans les
      dossiers administratifs des refuges. Les tests impliquant ce groupe doivent être
      interprétés avec prudence.
    </p>
    <p style="margin:0 0 0.3rem">
      <strong>Hommes c. femmes :</strong>
      Le décompte mensuel attendu des hommes est d'environ
      <strong>${fmt1(rr.men_vs_women.median)}&times; celui des femmes</strong>
      (IDC 95 % ${fmt1(rr.men_vs_women.hdi[0])}&ndash;${fmt1(rr.men_vs_women.hdi[1])}&times;),
      soit un écart moyen d'environ
      <strong>${fmtN(pd.men_vs_women.median)} personnes par mois</strong>
      (IDC 95 % ${fmtN(pd.men_vs_women.hdi[0])}&ndash;${fmtN(pd.men_vs_women.hdi[1])}).
      Pour une large gamme d'a priori raisonnables, la probabilité a posteriori que le
      vrai décompte mensuel moyen des hommes dépasse celui des femmes est
      indiscernable de 1.
    </p>
    <div class="plain-eng">
      <div class="pe-label">En clair</div>
      Une probabilité a posteriori indiscernable de 1 signifie que la probabilité estimée
      est proche de 100 % que le vrai décompte mensuel des hommes dépasse celui des femmes.
      L'IDC à 95 % indique également que l'écart est constamment élevé — le décompte des
      hommes représente environ ${fmt1(rr.men_vs_women.median)}&times; celui des femmes,
      soit environ ${fmtN(pd.men_vs_women.median)} personnes de plus par mois.
      <strong>Les hommes constituent le groupe le plus nombreux et le plus systématiquement
      sur-représenté dans le réseau de refuges de Toronto.</strong>
    </div>
  </div>`
}
md`Les hommes ont représenté au moins **${_menShareMinPct} %** de la population en itinérance active de Toronto chaque mois depuis le début de l'enregistrement des données (${_recordStartYear} à ${_recordEndYear}). Ce chiffre correspond à la **part mensuelle la plus faible jamais observée pour les hommes sur l'ensemble de la période** ; dans la plupart des mois, cette part est plus élevée. L'écart mensuel moyen entre les hommes et tous les autres (femmes, personnes trans, non binaires et bispirituelles confondues) est d'environ **${_gapAvgStr} personnes**, et il a à peine bougé à mesure que le total augmentait et diminuait. Ce schéma n'est ni nouveau, ni subtil, ni caché dans les données. Pourtant, il fait rarement la une des journaux, et peu de conversations publiques sur l'itinérance dans la ville s'ouvrent sur ce constat.`
 

Un audit de données indépendant pour les résidents de Toronto. / An independent data audit for Toronto residents.
© 2026 Miriam Marling · BonQuery