BonQuery
  • Home
  • Dashboards
  • Left in the Cold
  • Data & Methods
    • About the Data
    • Daily Data Sources
    • Central Intake Validation
  • About
  • FR

Monthly Snapshot

What Toronto’s shelter numbers show this month

Choose a month and see how many people used Toronto’s shelter system, entered or left the count, or moved into housing.
Author

Miriam Marling

data = FileAttachment("../data/shelter_flow.json").json()
MON = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
fmtN = d => d.toLocaleString()

// Build month dropdown in descending order (most recent first).
// Display: "March 2026"  Return value: "2026-03-01" (the YYYY-MM-DD from DB)
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("default", {month: "long", year: "numeric"}),
      value: d.flow_date
    }
  })

Toronto publishes new shelter numbers each month. Choose a month to see how many people were counted, how many entered or left the count, and how many moved into housing.

The page covers January 2018 to the newest month in the City’s public data. It updates after the City posts new numbers, usually around the 15th.

viewof reportingDate = Inputs.select(reportingMonthOptions, {
  label: "Reporting Month",
  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")

// KPI values
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

// For YTD charts
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">People counted as homeless in Toronto's shelter system</div>
    <div class="kpi-value">${activelyHomeless.toLocaleString()}</div>
  </div>
</div>`
html`<div class="kpi-row">
  <div class="kpi-card" style="background:var(--bq-kpi-inflow-bg);">
    <div class="kpi-label">People added to the count this month</div>
    <div class="kpi-value" style="color:var(--bq-kpi-inflow);">+${inflowTotal.toLocaleString()}</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">Added minus removed</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()}
    </div>
  </div>
  <div class="kpi-card" style="background:var(--bq-kpi-outflow-bg);">
    <div class="kpi-label">People removed from the count this month</div>
    <div class="kpi-value" style="color:var(--bq-kpi-outflow);">−${outflowTotal.toLocaleString()}</div>
  </div>
</div>`

How many have been homeless for a long time?

The City calls someone “chronically homeless” after at least 180 nights in a shelter in one year, or 546 nights over three years.

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: "Chronic",     pct: chronicPct,       fill: "#5856D6"},
  {label: "Not Chronic", 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: ["Chronic","Not Chronic"], 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 === "Chronic" ? "white" : _chartFg,
      textAnchor: "middle",
      lineWidth: 8,
      fontSize: 13,
      fontWeight: "600"
    }))
  ]
})

Newly counted as homeless: total so far this year

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]
    }
  })
}

// Sort: by month, then prior year first (so bars appear: prior ↑ current ↓ within each month)
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: "Total so far this year", 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(),
      textAnchor: "start", dx: 5, fontSize: 11
    })
  ]
})

Moved into housing: total so far this year

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: "Total so far this year", 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(),
      textAnchor: "start", dx: 5, fontSize: 11
    })
  ]
})

Who was added to the count this month?

inflowBreakdown = snapshot ? [
  {label: "Newly counted as homeless",     value: snapshot.newly_identified,      fill: "#E63946"},
  {label: "Returned after permanent housing", value: snapshot.returned_from_housing, fill: "#FFD600"},
  {label: "Returned after 3 months away",  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: "People", 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(),
        textAnchor: "start", dx: 6, fontSize: 12
      })
    ]
  })
}

Who was removed from the count this month?

outflowBreakdown = snapshot ? [
  {label: "Moved into permanent housing", value: snapshot.moved_to_housing, fill: "#66CC66"},
  {label: "Not seen in shelter for 3 months", 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: "People", 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(),
        textAnchor: "start", dx: 6, fontSize: 12
      })
    ]
  })
}

Age of people counted as homeless in the last 3 months

ageBandDefs2 = [
  {key: "age_under_16", label: "Under 16"},
  {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])
  ]
})

Gender of people counted as homeless in the last 3 months

genderDefs2 = [
  {key: "gender_male",                label: "Men"},
  {key: "gender_female",              label: "Women"},
  {key: "gender_trans_nb_two_spirit", label: "Transgender, Non-Binary, or Two-Spirit"}
]

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: "Share of gender total", 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
      })
    ]
  })
}

Explore other dashboards:

  • Historical Trends
  • YTD Comparison

You can also open this dashboard in Oracle APEX. It uses the same data.

Who is not counted?

These numbers do not count everyone without housing. They count people who used a City-funded overnight service at least once in the past three months. They miss people sleeping outside, using a shelter the City does not fund, or staying with friends or family. The City estimates that about 18% of people experiencing homelessness are missing from this count.

What the City’s labels mean

We use simpler labels in the charts. These are the City’s official terms.

Actively homeless: counted as homeless in the shelter system. This means the person used a City-funded overnight service in the past three months and was not recorded as moving to permanent housing. The person may not be in a shelter on the day shown.

Chronic homelessness: homeless for a long time. This means at least 180 nights in shelter during one year, or 546 nights during three years.

Added to the count this month:

  • Newly counted as homeless: first entered the shelter count.
  • Returned after permanent housing: came back after moving into housing.
  • Returned after three months away: used shelter again after not using it for at least three months.

Removed from the count this month:

  • Moved into permanent housing: recorded as leaving for housing.
  • Not seen in shelter for three months: removed because the person did not use a City-funded shelter during that time. This does not mean the person found housing.

Definitions are based on the City of Toronto’s Shelter System Flow Data page.

See where the numbers come from →

Data source

All numbers come from the City of Toronto’s Shelter System Flow public data. The City updates it monthly.

Contains information licensed under the Open Government Licence - Toronto.


bayesian_gender = FileAttachment("../data/bayesian_gender.json").json()
shelter_flow    = data
WHAT THE DATA SHOWS

Men are the largest group in Toronto’s shelter count

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

// Most recent month — for the hero ratio and sub-line
_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("default", {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()

_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">more <strong>men</strong> than women were counted as homeless in Toronto's shelter system</div>
    <div class="hero-sub"><strong>${_menCount.toLocaleString()} men</strong> · ${_womenCount.toLocaleString()} women · ${_transCount.toLocaleString()} transgender, non-binary, or two-spirit · ${_flowMonth}</div>
  </div>
</div>`
_genderStats = [
  {label: "Men",          mean: _menMean,   ci_lo: _menMean   - 1.96 * _menSE,   ci_hi: _menMean   + 1.96 * _menSE,   isFocus: true},
  {label: "Women",        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: `Average monthly shelter count by gender, ${_yearRange}`,
  caption: "The thin lines show the likely range around each average. Source: City of Toronto Shelter System Flow data.",
  x: {label: null, domain: _genderStats.map(d => d.label)},
  y: {label: "Average people counted each month", grid: true,
      tickFormat: d => d.toLocaleString(),
      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(),
      dy: -10, textAnchor: "middle", fontSize: 13,
      fill: d => d.isFocus ? "#cc1f15" : _chartFg,
      fontWeight: "600"
    }),
    Plot.ruleY([0])
  ]
})
NoteShow the statistical details
{
  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()

  const bayesPairs = [
    {comp: "Men vs Women",         rr: rr.men_vs_women,       diff: pd.men_vs_women,       note: false},
    {comp: "Men vs Trans/NB/2S",   rr: rr.men_vs_transnb2s,   diff: pd.men_vs_transnb2s,   note: true},
    {comp: "Women vs 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>Bayesian analysis (negative-binomial regression, fit with
      <code>brms</code> ${bg.brms_version} on ${bg.n_months} months of data):</strong>
      Weakly informative priors were used throughout.
      Sampling diagnostics: R&#x0302;&nbsp;${diag.max_rhat},
      ESS&nbsp;${diag.min_ess_bulk.toLocaleString()},
      ${diag.n_divergent} divergent transitions.
    </p>
    <table>
      <thead><tr>
        <th style="text-align:left;padding:3px 10px 3px 0;font-weight:600">Comparison</th>
        <th style="text-align:right;padding:3px 10px;font-weight:600">Rate ratio (95% HDI)</th>
        <th style="text-align:right;padding:3px 0;font-weight:600">Difference in count (95% HDI)</th>
      </tr></thead>
      <tbody>${bayesRows}</tbody>
    </table>
    <p class="bq-note" style="margin:0 0 0.5rem">
      † The Trans/NB/2S group averages ~${Math.round(_transMean)} individuals per month,
      a small count that is also likely under-reported in administrative shelter records.
      Tests involving this group should be interpreted with caution.
    </p>
    <p style="margin:0 0 0.3rem">
      <strong>Men vs Women:</strong>
      Men's expected monthly count is roughly
      <strong>${fmt1(rr.men_vs_women.median)}&times; that of women</strong>
      (95% HDI ${fmt1(rr.men_vs_women.hdi[0])}&ndash;${fmt1(rr.men_vs_women.hdi[1])}&times;),
      an average gap of about
      <strong>${fmtN(pd.men_vs_women.median)} people per month</strong>
      (95% HDI ${fmtN(pd.men_vs_women.hdi[0])}&ndash;${fmtN(pd.men_vs_women.hdi[1])}).
      Across a wide range of reasonable priors, the posterior probability that the true mean
      monthly count of men exceeds women is indistinguishable from 1.
    </p>
    <div class="plain-eng">
      <div class="pe-label">What it means</div>
      The model estimates a nearly 100% chance that the monthly count of men is
      higher than the count of women. The estimated count of men is about
      ${fmt1(rr.men_vs_women.median)}&times; the count of women, or about
      ${fmtN(pd.men_vs_women.median)} more people each month.
      <strong>Men are the largest group in the City's shelter count.</strong>
    </div>
  </div>`
}
md`Men were at least **${_menShareMinPct}%** of the people counted as homeless in Toronto's shelter system in every month from ${_recordStartYear} to ${_recordEndYear}. In most months, their share was higher. On average, the count of men was about **${_gapAvgStr} people higher** than all other gender groups combined. That gap changed little over time.`
Back to top
Need a safe place or support now? These contacts serve Toronto unless another area is named. BonQuery is an independent data project, not a service provider.
Assaulted Women's HelplineFree and confidential, anywhere in Ontario, 24/7 1-866-863-0511 Young people: Kids Help PhoneAges 5 to 29, confidential, 24/7. Call, or text CONNECT to 686868. 1-800-668-6868 Thinking about suicide, or worried about someone?Call or text 988 anywhere in Canada, 24/7. 988 Toronto emergency shelter line416-338-4766 Toronto shelter line, toll-free1-877-338-3398 Toronto mental-health crisis support211 Toronto street outreach311 Toronto 311 from outside city limits416-392-2489 City services TTY relay711 Immediate danger in Canada911 Toronto warming-centre status
Besoin d'un endroit sûr ou de soutien maintenant? Ces services desservent Toronto, sauf si une autre région est indiquée. BonQuery est un projet de données indépendant, pas un fournisseur de services.
Ligne d'aide aux femmes victimes de violenceGratuite et confidentielle, partout en Ontario, 24 h/24 et 7 j/7 1-866-863-0511 Jeunes : Jeunesse, J'écouteDe 5 à 29 ans, service confidentiel 24 h/24 et 7 j/7. Appelez ou textez le 686868. 1-800-668-6868 Vous pensez au suicide ou vous vous inquiétez pour quelqu'un?Appelez ou textez le 988 partout au Canada, 24 h/24 et 7 j/7. 988 Ligne des refuges d'urgence de Toronto416-338-4766 Ligne des refuges de Toronto, sans frais1-877-338-3398 Soutien en cas de crise de santé mentale à Toronto211 Intervention de rue à Toronto311 Toronto 311 depuis l'extérieur de la ville416-392-2489 Service de relais ATS de la Ville711 Danger immédiat au Canada911 État des centres de réchauffement de Toronto
 

Independent public-interest analysis of Toronto’s shelter system.
© 2026 Miriam Marling · BonQuery