The story

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:

  1. The long view. Rents in every part of Victoria roughly tripled between 2000 and 2025. The inner city is the most expensive throughout.
  2. The dip and the rebound. Around 2020 and 2021, while international borders were closed, rents in the inner city fell sharply, then jumped back higher than before. The dip was largest near the City and Brunswick campuses, while cheaper suburbs hardly noticed it.
  3. The disappearing budget flat. In 2019 about half of Melbourne’s suburbs (52 of 108) still had a two-bedroom flat at $400 a week or less. By 2025 only one did.
  4. The catch-up. The cheaper suburbs did not stay cheap. Areas such as Clayton and Dandenong saw some of the fastest rises, so the “move further out” trick now saves much less.
  5. What it means near campus. A final comparison shows what one-bedroom flats, two-bedroom flats and houses cost near the City, Brunswick and Bundoora campuses and in cheaper alternatives such as Footscray, Reservoir and Sunshine.
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")

1. Twenty-five years of rent, by region

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:

2. The dip and the rebound near the RMIT campuses

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:

3. The disappearing budget flat

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:

4. The catch-up: cheaper suburbs rose the most

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:

5. What a home costs near campus (December 2025)

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:

Conclusion:

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.

Acknowledgements

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

References (APA 7, check the details)

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