Imagine you have just arrived in Melbourne to study. After the tuition fees, the biggest weekly cost for an international student is rent. Even if two students share a two-bedroom flat that comes out to be $400 a week making each pay $200 (an assumption used for illustration only). Ten years ago that was an ordinary budget. Today it is hard to find.
Where the numbers come from. A bond is lodged by tenants everytime when a new lease is started with the Residential Tenancies Bond Authority. That records are then used by Homes Victoria to showcase the median weekly rent for each suburb, with home type and quarter, from 2000 to December 2025. This story uses the two-bedroom flat, the usual choice for a student share house, and adds one-bedroom flats and three-bedroom houses for comparison.
The question. Where in Melbourne can you still find a cheap place to live, and how fast has that list shrunk?
How the story unfolds:
read_rent <- function(sheet) {
raw <- read_excel(f, sheet = sheet, col_names = FALSE, col_types = "text", .name_repair = "minimal")
periods <- as.character(unlist(raw[2, -(1:2)]))
periods <- fill(tibble(p = periods), p)$p # fill any blank header cells
kinds <- as.character(unlist(raw[3, -(1:2)]))
body <- raw[-(1:3), ]
names(body) <- c("region", "suburb", paste(periods, kinds, sep = "|"))
body |>
fill(region) |>
filter(!is.na(suburb)) |>
pivot_longer(-c(region, suburb), names_to = "key", values_to = "value") |>
separate(key, into = c("period", "kind"), sep = "\\|") |>
pivot_wider(names_from = kind, values_from = value) |>
mutate(count = suppressWarnings(as.numeric(Count)), # "-" (suppressed) becomes NA
median = suppressWarnings(as.numeric(Median)),
date = my(period),
sheet = sheet) |>
select(region, suburb, period, date, count, median, sheet)
}
all_types <- map_dfr(c("1 bedroom flat", "2 bedroom flat", "3 bedroom house"), read_rent)
flat2 <- all_types |> filter(sheet == "2 bedroom flat")
groups <- flat2 |> filter(suburb == "Group Total", !is.na(median))
suburbs <- flat2 |> filter(suburb != "Group Total")
Click a legend entry to hide a region, double-click to isolate one, and drag to zoom. The grey band marks 2020 to 2021.
plot_ly(groups, x = ~date, y = ~median, color = ~region, colors = viridis(13, end = 0.95),
type = "scatter", mode = "lines",
text = ~paste0(region, "<br>", period, ": $", median, " per week"), hoverinfo = "text") |>
layout(xaxis = list(title = ""),
yaxis = list(title = "Median weekly rent, 2-bedroom flat ($)"),
shapes = list(list(type = "rect", xref = "x", yref = "paper",
x0 = "2020-03-01", x1 = "2021-12-31", y0 = 0, y1 = 1,
fillcolor = "rgba(150,150,150,0.2)", line = list(width = 0))),
legend = list(orientation = "h", y = -0.25),
title = list(text = "Median rent for a 2-bedroom flat, by region of Victoria", x = 0))
Chart Description:
sel <- c(campus_sub, alt_sub)
yrs <- c(2019, 2021, 2025)
wide <- suburbs |>
filter(suburb %in% sel, period %in% paste("Dec", yrs)) |>
mutate(year = paste0("y", year(date))) |>
select(suburb, year, median) |>
pivot_wider(names_from = year, values_from = median) |>
mutate(suburb = fct_reorder(suburb, y2025),
type = ifelse(suburb %in% campus_sub, "Near an RMIT campus", "Cheaper alternative"))
plot_ly(wide) |>
add_segments(x = ~pmin(y2019, y2021, y2025), xend = ~pmax(y2019, y2021, y2025),
y = ~suburb, yend = ~suburb, color = I("grey75"), showlegend = FALSE, hoverinfo = "none") |>
add_markers(x = ~y2019, y = ~suburb, name = "Dec 2019", color = I("#7f8fa6"),
text = ~paste0(suburb, "<br>Dec 2019: $", y2019), hoverinfo = "text") |>
add_markers(x = ~y2021, y = ~suburb, name = "Dec 2021", color = I("#2b6cb0"),
text = ~paste0(suburb, "<br>Dec 2021: $", y2021), hoverinfo = "text") |>
add_markers(x = ~y2025, y = ~suburb, name = "Dec 2025", color = I("#c0392b"),
text = ~paste0(suburb, "<br>Dec 2025: $", y2025), hoverinfo = "text") |>
layout(xaxis = list(title = "Median weekly rent, 2-bedroom flat ($)"),
yaxis = list(title = "", tickfont = list(size = 11)),
margin = list(l = 220), legend = list(orientation = "h", y = -0.2),
title = list(text = "Rent in 2019, 2021 and 2025 near the three RMIT campuses and in cheaper suburbs", x = 0))
Campus-linked suburbs are my grouping: CBD and Carlton-Parkville and North Melbourne (City campus), Brunswick (Brunswick campus) and Bundoora (Bundoora campus).
Chart Description:
For each year, how many metropolitan suburbs still had a median two-bedroom flat at or under a weekly budget?
afford <- suburbs |>
filter(!region %in% regional, str_starts(period, "Dec"), count >= 30, !is.na(median), year(date) >= 2010) |>
group_by(year = year(date)) |>
summarise(total = n(),
`$350 or less` = sum(median <= 350),
`$400 or less` = sum(median <= 400),
`$450 or less` = sum(median <= 450), .groups = "drop") |>
pivot_longer(starts_with("$"), names_to = "budget", values_to = "n_suburbs")
plot_ly(afford, x = ~year, y = ~n_suburbs, color = ~budget, colors = viridis(3, end = 0.8),
type = "scatter", mode = "lines+markers",
text = ~paste0(year, ": ", n_suburbs, " of ", total, " metro suburbs<br>with a 2-bed flat at ", budget, " per week"),
hoverinfo = "text") |>
layout(xaxis = list(title = "December of each year", dtick = 1),
yaxis = list(title = "Number of metropolitan suburbs"),
legend = list(orientation = "h", y = -0.25),
title = list(text = "How many Melbourne suburbs still have a 2-bedroom flat within budget?", x = 0))
Only suburbs with at least 30 new leases in that quarter’s moving year are counted, so small suburbs with unreliable medians are left out.
Chart Description:
Each dot is a metropolitan suburb. The further left a dot sits, the cheaper it was in 2019. The higher it sits, the more rent has risen since.
a <- suburbs |> filter(period == "Dec 2019") |> select(region, suburb, r19 = median, n19 = count)
b <- suburbs |> filter(period == "Dec 2025") |> select(suburb, r25 = median, n25 = count)
chg <- inner_join(a, b, by = "suburb") |>
filter(!region %in% regional, n19 >= 30, n25 >= 30) |>
mutate(pct = (r25 / r19 - 1) * 100, campus = suburb %in% campus_sub)
plot_ly(chg, x = ~r19, y = ~pct, color = ~region, colors = viridis(9, end = 0.9),
type = "scatter", mode = "markers", size = ~n25, sizes = c(8, 35),
marker = list(opacity = 0.7, line = list(width = 0.5, color = "white")),
text = ~paste0(suburb, "<br>2019: $", r19, " 2025: $", r25, "<br>Change: +", round(pct), "%"),
hoverinfo = "text") |>
add_markers(data = filter(chg, campus), x = ~r19, y = ~pct, inherit = FALSE, name = "Near an RMIT campus",
marker = list(color = "rgba(0,0,0,0)", size = 22, line = list(color = "black", width = 2)),
text = ~suburb, hoverinfo = "text") |>
layout(xaxis = list(title = "Median weekly rent in Dec 2019 ($)"),
yaxis = list(title = "% rise to Dec 2025"),
legend = list(orientation = "h", y = -0.3),
title = list(text = "The cheaper the suburb in 2019, the faster its rent rose", x = 0))
Chart Description:
bars <- all_types |>
filter(period == "Dec 2025", suburb %in% sel) |>
mutate(suburb = factor(suburb, levels = sel),
sheet = factor(sheet, levels = c("1 bedroom flat", "2 bedroom flat", "3 bedroom house")))
plot_ly(bars, x = ~suburb, y = ~median, color = ~sheet, colors = viridis(3, end = 0.8), type = "bar",
text = ~paste0(suburb, "<br>", sheet, ": $", median, " per week<br>(", count, " new leases)"),
hoverinfo = "text") |>
layout(barmode = "group", xaxis = list(title = "", tickangle = -30),
yaxis = list(title = "Median weekly rent ($)"),
legend = list(orientation = "h", y = -0.4),
title = list(text = "Rent by home type: campus-linked suburbs (left) and cheaper alternatives (right)", x = 0))
Chart Description:
Melbourne’s budget flats have almost vanished. In December 2019, 52 of 108 metropolitan suburbs had a median two-bedroom flat at $400 a week or less, but by December 2025 only one did. Inner Melbourne’s median rent fell from $560 in 2019 to $420 in 2021, then climbed to $700 in 2025. That dip was in the time when the country’s borders were closed, but this data cannot indicate whether students were responsible. Cheaper suburbs have caught up fastest: rents in Clayton rose about 50% and in Dandenong about 45% since 2019, compared with roughly 21% to 23% in inner suburbs like Carlton-Parkville and the CBD. Rents are the median weekly rent of new leases that commenced in the year ending each quarter, based on Residential Tenancies Bond Authority records. They describe new lettings, not rents paid by existing tenants. Group Total rows give region medians (chart 1). Suburb-level rows are used elsewhere. Regional Victoria is excluded from charts 3 and 4.
The data were cleaned and plotted in R (R Core Team, 2026) using the tidyverse (Wickham et al., 2019), readxl (Wickham & Bryan, 2026), janitor (Firke, 2024), plotly (Sievert, 2020), crosstalk (Cheng & Sievert, 2025) and scales (Wickham et al., 2025), and the report was created with rmarkdown (Allaire et al., 2026). I used ChatGPT (OpenAI, 2026) to get an understanding of R syntax and Grammarly (Grammarly, 2026) to check grammar and sentence structure. Any analysis decisions and interpretations are my own. Helpful resources:
Wickham, Çetinkaya-Rundel and Grolemund’s R for Data Science (2nd ed.), for data wrangling with the tidyverse: https://r4ds.hadley.nz Xie, Allaire and Grolemund’s R Markdown: The Definitive Guide: https://bookdown.org/yihui/rmarkdown The plotly R documentation, for interactive charts: https://plotly.com/r
Homes Victoria. (2026). Rental report: Moving annual median rent by suburb and town, December quarter 2025[Data set]. State of Victoria. https://www.dffh.vic.gov.au/publications/rental-report
Sievert, C. (2020). Interactive web-based data visualization with R, plotly, and shiny.
Cheng, J., & Sievert, C. (2025). crosstalk: Inter-widget interactivity for HTML widgets (Version 1.2.2) [Computer software]. https://doi.org/10.32614/CRAN.package.crosstalk
Allaire, J., Xie, Y., Dervieux, C., McPherson, J., Luraschi, J., Ushey, K., Atkins, A., Wickham, H., Cheng, J., Chang, W., & Iannone, R. (2026). rmarkdown: Dynamic documents for R (Version 2.32) [Computer software]. https://github.com/rstudio/rmarkdown
Firke, S. (2024). janitor: Simple tools for examining and cleaning dirty data (Version 2.2.1) [Computer software]. https://doi.org/10.32614/CRAN.package.janitor
R Core Team. (2026). R: A language and environment for statistical computing (Version 4.5.3) [Computer software]. R Foundation for Statistical Computing. https://www.R-project.org/
Wickham, H., & Bryan, J. (2026). readxl: Read Excel files (Version 1.6.0) [Computer software]. https://doi.org/10.32614/CRAN.package.readxl
Wickham, H., Pedersen, T. L., & Seidel, D. (2025). scales: Scale functions for visualization (Version 1.4.0) [Computer software]. https://doi.org/10.32614/CRAN.package.scales
Grammarly. (2026). Grammarly [Computer software]. https://www.grammarly.com
OpenAI. (2026). ChatGPT [Large language model]. https://chatgpt.com