This dashboard titled “Mental Health in Australia: Trends 2015–2023” presents an interactive exploration of community mental health service activity across Australia. Using publicly available data from the Australian Institute of Health and Welfare (AIHW), it visualises national trends and demographic patterns in service access and usage. The dashboard draws primarily from the Community Mental Health Care – Demographic Focus (CSV 2022–23) dataset, offering insights into how service engagement has changed over time by age, gender and principal diagnosis.
The purpose of this project is to transform complex tabular data into a clear, engaging visual story that highlights service utilisation patterns and demographic disparities, supporting a broader understanding of mental health service accessibility and demand across Australia.
Data sources
Australian Institute of Health and Welfare. (2025). Data tables – Community mental health care services (CSV 2022–23). Australian Government. https://www.aihw.gov.au/mental-health/resources/data-tables?&page=2
Australian Institute of Health and Welfare. (n.d.). Mental health. https://www.aihw.gov.au/mental-health
1. National Trend (Line Chart):-This visualisation displays the overall national rate of community mental health services per 1,000 population across the ten-year period. It helps users observe how service activity has changed over time.
2. Rate by Gender (Multi-Line Chart):-The gender-based line chart compares mental health service utilisation by gender, including male, female and unknown categories. It allows users to identify patterns.
3. Age Profile – Latest Year (Horizontal Bar Chart):-This bar chart presents service rates by age group for the most recent reporting year (2023). It provides a snapshot view of which age groups have the highest or lowest levels of engagement with community mental health services.
4. Heat Map – Age vs Year:-The heat map shows the distribution of service rates across both time and age. It allows users to explore how service activity has varied for different age groups over the ten-year period.
5. Service Rate by Principal Diagnosis (Scatter Plot):-This scatter plot visualises mental health service rates by diagnosis type and year. Each colour represents a different diagnostic category, such as mood disorders, stress-related disorders etc.
From 2015 to 2023, Australia’s community mental health services exhibit gradual expansion with slight fluctuations reflecting system capacity and demand. The overall service rate peaks around 2020 before tapering slightly, potentially reflecting the impact of pandemic disruptions and later recovery.
Gender Trends: Female and male service rates follow similar trajectories, indicating broad gender equity in access. The “unknown” category declines sharply, likely due to improved data capture in later reporting years.
Age Trends: The 35–54 age group consistently demonstrates the highest engagement with community mental health services, suggesting that middle adulthood remains the core demographic for mental health interventions.
Diagnosis Patterns: Affective and stress-related disorders account for a significant proportion of service contacts, reinforcing their prevalence in Australia’s mental health landscape.
Temporal Insights: The heat map visualisation reveals consistent mid-age intensity and minimal volatility in overall rates, implying stable long-term service delivery despite demographic shifts.
---
title: "Mental Health in Australia: Trends 2015–2023"
subtitle: "AIHW Community Mental Health Care — Demographic Focus CSV (2022–23)"
author: "Venissa Dsouza "
output:
flexdashboard::flex_dashboard:
orientation: rows
vertical_layout: fill
self_contained: true
social: menu
source_code: embed
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(message = FALSE, warning = FALSE)
library(tidyverse)
library(readr)
library(janitor)
library(stringr)
library(plotly)
library(htmltools)
theme_set(theme_minimal(base_size = 13))
```
```{r}
#AIHW CMHC Demographic Focus Loader
cmhc_file <- "data/CMHC_DemogFocus_2223.csv"
stopifnot(file.exists(cmhc_file))
raw <- read_csv(cmhc_file, show_col_types = FALSE,
locale = readr::locale(encoding = "UTF-8")) %>%
clean_names() %>%
mutate(across(where(is.character), ~ iconv(.x, from = "", to = "UTF-8", sub = "")))
cmhc <- raw %>%
select(financial_year, sex, age_group, principal_diagnosis,
indiginous_status, measure_name, measure_value) %>%
mutate(
measure_value = suppressWarnings(as.numeric(measure_value)),
# --- AGE LABEL CLEANING ---
age_group = as.character(age_group),
age_group = stringr::str_trim(age_group),
age_group = stringr::str_replace_all(age_group, "\\s+", ""),
age_group = stringr::str_replace_all(age_group, "–|—", "-"), # normalize dashes
# "5560" -> "55-60"
age_group = dplyr::if_else(
stringr::str_detect(age_group, "^\\d{4}$"),
stringr::str_replace(age_group, "^(\\d{2})(\\d{2})$", "\\1-\\2"),
age_group
),
# "55to60" / "55_60" / "55-60" -> "55-60"
age_group = stringr::str_replace(age_group, "^(\\d{1,2})[^0-9]+(\\d{1,2})$", "\\1-\\2"),
# "85plus" / "85+" -> "85+"
age_group = stringr::str_replace(age_group, "^(85\\+|85plus|85andover|85\\s*\\+)$", "85+")
)
measures <- unique(cmhc$measure_name)
rate_meas <- measures[grepl("rate", measures, ignore.case = TRUE)][1]
count_meas <- measures[grepl("number|count|contact|patient", measures, ignore.case = TRUE)][1]
if(!is.na(rate_meas)){
d <- cmhc %>% filter(measure_name == rate_meas)
med <- median(d$measure_value, na.rm = TRUE)
d <- d %>%
mutate(rate_per_1000 = ifelse(is.finite(med) && med > 200, measure_value/10, measure_value),
y_label = "Rate per 1,000 population")
} else if(!is.na(count_meas)){
d <- cmhc %>% filter(measure_name == count_meas) %>%
mutate(rate_per_1000 = measure_value, y_label = "Count (proxy)")
} else {
stop("No usable measure found. Available measure_name values: ", paste(measures, collapse=", "))
}
extract_end_year <- function(x){
x <- as.character(x)
m <- stringr::str_match(x, "^(\\d{4}).*?(\\d{2,4})$")
if (!is.na(m[1,1])) {
start <- suppressWarnings(as.integer(m[1,2]))
end_part <- suppressWarnings(as.integer(m[1,3]))
if (!is.na(start) && !is.na(end_part)) {
end <- ifelse(end_part < 100, 2000 + end_part, end_part)
if (end < start) end <- end + 10L
return(as.integer(end))
}
}
y <- suppressWarnings(as.integer(stringr::str_extract(x, "\\b\\d{4}\\b")))
return(as.integer(y))
}
ts <- d %>%
mutate(
year = as.integer(sapply(financial_year, extract_end_year)),
sex = ifelse(is.na(sex) | sex == "", "All", sex),
age_group = ifelse(is.na(age_group) | age_group == "", "All ages", age_group)
) %>%
filter(!is.na(year), !is.na(rate_per_1000))
ts <- ts %>%
mutate(
age_group = case_when(
age_group %in% c("011years", "0-11", "011") ~ "0–11 years",
age_group %in% c("1217years", "12-17", "1217") ~ "12–17 years",
age_group %in% c("1824years", "18-24", "1824") ~ "18–24 years",
age_group %in% c("2534years", "25-34", "2534") ~ "25–34 years",
age_group %in% c("3544years", "35-44", "3544") ~ "35–44 years",
age_group %in% c("4554years", "45-54", "4554") ~ "45–54 years",
age_group %in% c("5564years", "55-64", "5564") ~ "55–64 years",
age_group %in% c("65yearsandover", "65+", "65andabove") ~ "65 years and over",
TRUE ~ age_group
)
)
# If a 2015–2024 window exists, apply it; otherwise keep full range
if(any(ts$year >= 2015 & ts$year <= 2024)) {
ts <- ts %>% filter(year >= 2015, year <= 2024)
}
# AIHW CMHC Session Focus Loader
sess_file <- "data/CMHC_SessionFocus_2223.csv"
stopifnot(file.exists(sess_file))
sess_raw <- readr::read_csv(
sess_file, show_col_types = FALSE,
locale = readr::locale(encoding = "UTF-8")
) |>
janitor::clean_names() |>
dplyr::mutate(across(where(is.character), ~ iconv(.x, "", "UTF-8", sub = "")))
sess <- sess_raw |>
dplyr::select(any_of(c(
"financial_year","state","phn","sa4","service_provider_type",
"session_type","measure_name","measure_value"
))) |>
dplyr::mutate(measure_value = suppressWarnings(as.numeric(measure_value)))
sess_measures <- sort(unique(sess$measure_name))
sess_count <- sess_measures[grepl("number|count|contact|session", sess_measures, ignore.case = TRUE)][1]
sess_d <- sess |>
dplyr::filter(measure_name == sess_count) |>
dplyr::mutate(count = measure_value) |>
dplyr::filter(is.finite(count))
# AIHW CMHC Geospatial Focus Loader
geo_file <- "data/CMHC_GeospatialFocus_2223.csv"
stopifnot(file.exists(geo_file))
geo_raw <- readr::read_csv(
geo_file, show_col_types = FALSE,
locale = readr::locale(encoding = "UTF-8")
) |>
janitor::clean_names() |>
dplyr::mutate(across(where(is.character), ~ iconv(.x, "", "UTF-8", sub = "")))
geo <- geo_raw |>
dplyr::select(any_of(c(
"financial_year","state","phn","sa4","measure_name","measure_value"
))) |>
dplyr::mutate(measure_value = suppressWarnings(as.numeric(measure_value)))
geo_measures <- sort(unique(geo$measure_name))
geo_rate <- geo_measures[grepl("rate", geo_measures, ignore.case = TRUE)][1]
geo_count <- geo_measures[grepl("number|count|contact|patient|session", geo_measures, ignore.case = TRUE)][1]
if (!is.na(geo_rate)) {
geo_d <- geo |> dplyr::filter(measure_name == geo_rate) |>
dplyr::mutate(val = measure_value)
geo_ylabel <- "Rate per 1,000 (if per 10,000, code converts separately)"
} else if (!is.na(geo_count)) {
geo_d <- geo |> dplyr::filter(measure_name == geo_count) |>
dplyr::mutate(val = measure_value)
geo_ylabel <- "Count"
} else {
geo_d <- dplyr::tibble(state = character(), val = numeric())
geo_ylabel <- "Value"
}
# Harmonise year
geo_d <- geo_d |>
dplyr::mutate(year = suppressWarnings(as.integer(vapply(financial_year, extract_end_year, 0L)))) |>
dplyr::filter(!is.na(year), is.finite(val))
```
About My Dashboard
================================================================
Row
---------------------------------------------------------------
### **Introduction**
This dashboard titled “Mental Health in Australia: Trends 2015–2023” presents an interactive exploration of community mental health service activity across Australia. Using publicly available data from the Australian Institute of Health and Welfare (AIHW), it visualises national trends and demographic patterns in service access and usage. The dashboard draws primarily from the Community Mental Health Care – Demographic Focus (CSV 2022–23) dataset, offering insights into how service engagement has changed over time by age, gender and principal diagnosis.
The purpose of this project is to transform complex tabular data into a clear, engaging visual story that highlights service utilisation patterns and demographic disparities, supporting a broader understanding of mental health service accessibility and demand across Australia.
**Data sources**
- Australian Institute of Health and Welfare. (2025). *Data tables – Community mental health care services (CSV 2022–23).* Australian Government.
https://www.aihw.gov.au/mental-health/resources/data-tables?&page=2
- Australian Institute of Health and Welfare. (n.d.). *Mental health.* https://www.aihw.gov.au/mental-health
### **What are the Different Visualizations About ?**
**1. National Trend (Line Chart):-**This visualisation displays the overall national rate of community mental health services per 1,000 population across the ten-year period. It helps users observe how service activity has changed over time.
**2. Rate by Gender (Multi-Line Chart):-**The gender-based line chart compares mental health service utilisation by gender, including male, female and unknown categories. It allows users to identify patterns.
**3. Age Profile – Latest Year (Horizontal Bar Chart):-**This bar chart presents service rates by age group for the most recent reporting year (2023). It provides a snapshot view of which age groups have the highest or lowest levels of engagement with community mental health services.
**4. Heat Map – Age vs Year:-**The heat map shows the distribution of service rates across both time and age. It allows users to explore how service activity has varied for different age groups over the ten-year period.
**5. Service Rate by Principal Diagnosis (Scatter Plot):-**This scatter plot visualises mental health service rates by diagnosis type and year. Each colour represents a different diagnostic category, such as mood disorders, stress-related disorders etc.
Visualizations
================================================================
Row
-------------------------------------
### **National trend** {data-height=460}
```{r, echo=FALSE}
nat <- ts |>
dplyr::group_by(year) |>
dplyr::summarise(rate_per_1000 = mean(rate_per_1000, na.rm = TRUE), .groups="drop") |>
dplyr::arrange(year)
plotly::plot_ly(
nat, x=~year, y=~rate_per_1000, type="scatter", mode="lines+markers"
) |>
plotly::layout(
title = "Community mental health service rate (per 1,000)",
xaxis = list(title="", dtick=1), yaxis=list(title="Rate per 1,000"),
margin = list(l=20, r=20, t=30, b=5), hovermode="x unified"
) |>
plotly::config(responsive=TRUE, displaylogo=FALSE)
```
### **By Gender** {data-height=460}
```{r}
by_sex <- ts |>
filter(!is.na(sex)) |>
group_by(year, sex) |>
summarise(rate_per_1000 = mean(rate_per_1000, na.rm = TRUE), .groups = "drop") |>
arrange(year, sex) |>
mutate(sex = enc2utf8(as.character(sex)))
stopifnot(nrow(by_sex) > 0)
plotly::plot_ly(
by_sex,
x = ~as.integer(year),
y = ~rate_per_1000,
split = ~sex,
type = "scatter",
mode = "lines+markers"
) |>
plotly::layout(
title = "Rate per 1,000 by Gender",
xaxis = list(title = "", dtick = 1),
yaxis = list(title = "Rate per 1,000"),
legend = list(orientation = "h", x = 0, y = -0.15),
margin = list(l = 20, r = 20, t = 30, b = 5),
hovermode = "x unified"
) |>
plotly::config(responsive = TRUE, displaylogo = FALSE)
```
Row
---------------------
### **Latest year by age group** {data-height=540}
```{r}
latest_year <- max(ts$year, na.rm = TRUE)
age_latest <- ts |>
filter(year == latest_year, !is.na(age_group), !is.na(rate_per_1000)) |>
group_by(age_group) |>
summarise(rate_per_1000 = mean(rate_per_1000, na.rm = TRUE), .groups = "drop") |>
arrange(desc(rate_per_1000)) |>
mutate(age_group = enc2utf8(as.character(age_group)))
stopifnot(nrow(age_latest) > 0)
plotly::plot_ly(
age_latest,
x = ~rate_per_1000,
y = ~reorder(age_group, rate_per_1000),
type = "bar",
orientation = "h"
) |>
plotly::layout(
title = paste0("Age profile - ", latest_year),
xaxis = list(title = "Rate per 1,000 population"),
yaxis = list(title = ""),
margin = list(l = 20, r = 20, t = 30, b = 60)
) |>
plotly::config(responsive = TRUE, displaylogo = FALSE)
```
### **Heat map — age vs year** {data-height=460}
```{r}
heat_age <- ts %>% filter(!is.na(age_group)) %>% group_by(year, age_group) %>% summarise(rate=mean(rate_per_1000,na.rm=TRUE), .groups='drop')
plot_ly(heat_age, x=~year, y=~age_group, z=~rate, type='heatmap', colorscale='Viridis',colorbar = list(
title = list(text = "Rate per 1,000", side = "right"),
len = 1, # full height of the plot
thickness = 20, # width of the color scale bar
tickfont = list(size = 11),
titlefont = list(size = 12),
x = 1.08 # moves it slightly away from the heatmap
)) %>% layout(title='Heat map: age vs year', xaxis=list(title='Year'), yaxis=list(title='Age group'))
```
Scatter Plot & Inference
=====================================================================================
### **Scatter — rate vs year by principal diagnosis** {data-height=460}
```{r}
scatter_diag <- ts %>%
filter(!is.na(principal_diagnosis)) %>%
group_by(year, principal_diagnosis) %>%
summarise(rate_per_1000 = mean(rate_per_1000, na.rm = TRUE), .groups="drop")
plot_ly(
scatter_diag,
x = ~year, y = ~rate_per_1000,
color = ~principal_diagnosis,
type = "scatter", mode = "markers",
marker = list(size = 10, opacity = 0.7)
) %>%
layout(
title = "Service rate by diagnosis type (scatter)",
xaxis = list(title = "Year", dtick = 1),
yaxis = list(title = "Rate per 1,000"),
legend = list(orientation = "h", x = 0, y = -0.2),
margin = list(l = 60, r = 20, t = 60, b = 60)
) %>%
config(responsive = TRUE, displaylogo = FALSE)
```
### **Inference and Output Summary**
From 2015 to 2023, Australia’s community mental health services exhibit gradual expansion with slight fluctuations reflecting system capacity and demand. The overall service rate peaks around 2020 before tapering slightly, potentially reflecting the impact of pandemic disruptions and later recovery.
**Gender Trends:** Female and male service rates follow similar trajectories, indicating broad gender equity in access. The “unknown” category declines sharply, likely due to improved data capture in later reporting years.
**Age Trends:** The 35–54 age group consistently demonstrates the highest engagement with community mental health services, suggesting that middle adulthood remains the core demographic for mental health interventions.
**Diagnosis Patterns:** Affective and stress-related disorders account for a significant proportion of service contacts, reinforcing their prevalence in Australia’s mental health landscape.
**Temporal Insights:** The heat map visualisation reveals consistent mid-age intensity and minimal volatility in overall rates, implying stable long-term service delivery despite demographic shifts.