size_levels <- c("Small business", "Medium business", "Large business")
size_labels <- c("Small (under 20 jobs)", "Medium (20-199 jobs)", "Large (200+ jobs)")


size_colours <- setNames(c("#009E73", "#E69F00", "#0072B2"), size_labels)


tidy_plotly <- function(p, title) {
  p |>
    layout(title = list(text = paste0("<b>", title, "</b>"),
                        x = 0, xref = "paper", xanchor = "left",
                        y = 0.97, yref = "container", yanchor = "top",
                        font = list(size = 16)),
           legend = list(orientation = "h", x = 0, xanchor = "left",
                         y = 1.02, yanchor = "bottom", title = list(text = "")),
           margin = list(t = 95),
           hoverlabel = list(font = list(size = 13)),
           font = list(family = "Arial", size = 13)) |>
    config(displaylogo = FALSE,
           modeBarButtonsToRemove = c("lasso2d", "select2d", "autoScale2d"))
}
# Source 1: CLUE, whole municipality (2002-2024)
clue_muni <- read_csv("clue_business_data.csv")

# Source 2: CLUE, by small area (2002-2022).
# Drop the summary rows so nothing is counted twice.
clue_area <- read_csv("clue_small_area.csv") |>
  filter(clue_industry != "All CLUE Industry",
         clue_small_area != "City of Melbourne (total)")

# Source 3: ABS Australian Industry 2024-25.

read_abs_table <- function(sheet, measure) {
  read_excel("81550DO007_202425.xlsx", sheet = sheet, range = "A9:F43",
             col_names = c("label", "size_0_4", "size_5_19",
                           "size_20_199", "size_200_plus", "total")) |>
    mutate(region = if_else(is.na(size_0_4), label, NA_character_)) |>
    fill(region) |>
    filter(!is.na(size_0_4)) |>
    rename(financial_year = label) |>
    mutate(`Small business`  = size_0_4 + size_5_19,
           `Medium business` = size_20_199,
           `Large business`  = size_200_plus,
           region = str_to_title(region)) |>
    select(region, financial_year, all_of(size_levels), total) |>
    pivot_longer(all_of(size_levels), names_to = "business_size", values_to = "value") |>
    mutate(share = value / total * 100, measure = measure)
}

abs_all <- bind_rows(
  read_abs_table("Table_1", "Jobs"),
  read_abs_table("Table_2", "Wages and salaries"),
  read_abs_table("Table_3", "Sales and service income")
)

Introduction

Small businesses are often called the backbone of the Australian economy, and in Melbourne’s city centre they are everywhere: cafes, shops and small offices make up around 71% of all businesses. But counting businesses is not the same as counting jobs. In 2024, large businesses with 200 or more employees made up only about 3% of businesses in the City of Melbourne, yet they provided 58% of its jobs (City of Melbourne, 2025).

This story asks whether larger businesses really do account for more jobs than smaller ones, and whether that has changed over time. It uses the City of Melbourne’s Census of Land Use and Employment (CLUE) from 2002 to 2024 to look at the city as a whole, its industries and its neighbourhoods (City of Melbourne, 2023, 2025). It then compares the city with Victoria and Australia using ABS data (Australian Bureau of Statistics, 2026). The charts move from the overall picture to change over time, then to industries, local areas and finally the wider state and national context

A note on the data: the City of Melbourne suppresses any figure that relates to two or fewer businesses, to protect commercial confidentiality. Those jobs appear as blanks and are left out of the totals below. Most suppressed cells belong to large businesses, so the large-business figures in this report are, if anything, slight underestimates.

1. A few businesses, most of the jobs

size_2024 <- clue_muni |>
  filter(census_year == 2024) |>
  group_by(business_size) |>
  summarise(establishments = sum(total_establishments),
            jobs = sum(total_jobs, na.rm = TRUE), .groups = "drop") |>
  mutate(`Share of businesses` = establishments / sum(establishments) * 100,
         `Share of jobs`       = jobs / sum(jobs) * 100)

chart1_data <- size_2024 |>
  pivot_longer(c(`Share of businesses`, `Share of jobs`),
               names_to = "measure", values_to = "share") |>
  mutate(size_label = factor(business_size,
           levels = c("Non employing", size_levels),
           labels = c("Non-employing\n(0 jobs)", "Small\n(1-19 jobs)",
                      "Medium\n(20-199 jobs)", "Large\n(200+ jobs)")),
         count = if_else(measure == "Share of jobs", jobs, establishments),
         tooltip = paste0("<b>", str_replace(size_label, "\n", " "), "</b><br>",
                          measure, ": ", round(share, 1), "%<br>",
                          if_else(measure == "Share of jobs", "Jobs: ", "Businesses: "),
                          comma(count)))

p1 <- ggplot(chart1_data, aes(size_label, share, fill = measure, text = tooltip)) +
  geom_col(position = position_dodge(width = 0.8), width = 0.75) +
  scale_fill_manual(values = c("Share of businesses" = "#B8B8B8",
                               "Share of jobs" = "#2F3E4E")) +
  scale_y_continuous(labels = label_percent(scale = 1), limits = c(0, 80),
                     expand = expansion(mult = c(0, 0.02))) +
  labs(x = NULL, y = "Share of total", fill = NULL) +
  theme_minimal(base_size = 13) +
  theme(panel.grid.major.x = element_blank())

ggplotly(p1, tooltip = "text") |>
  tidy_plotly("Large businesses are 3% of businesses but 58% of jobs (City of Melbourne, 2024)")

Source: City of Melbourne (2025), Business establishments and jobs data by business size and CLUE industry in municipality.

  • Large businesses represent around 3% of businesses but provide 58% of jobs.
  • This shows that a small proportion of businesses contributes a large share of employment.
  • Large businesses play an important role in Melbourne’s job market.

2. The gap has widened since 2017

trend_base <- clue_muni |> filter(business_size %in% size_levels)

trend <- bind_rows(mutate(trend_base, clue_industry = "All industries"), trend_base) |>
  group_by(clue_industry, census_year, business_size) |>
  summarise(jobs = sum(total_jobs, na.rm = TRUE), .groups = "drop") |>
  complete(clue_industry, census_year, business_size = size_levels,
           fill = list(jobs = 0)) |>
  group_by(clue_industry, census_year) |>
  mutate(share = if (sum(jobs) > 0) jobs / sum(jobs) * 100 else NA_real_) |>
  ungroup() |>
  mutate(size_label = factor(business_size, levels = size_levels, labels = size_labels))

industries <- c("All industries",
                sort(setdiff(unique(trend$clue_industry), "All industries")))


p2 <- plot_ly()
for (ind in industries) {
  for (s in size_labels) {
    d <- filter(trend, clue_industry == ind, size_label == s)
    p2 <- p2 |>
      add_lines(data = d, x = ~census_year, y = ~jobs, name = s,
                line = list(color = size_colours[[s]], width = 3),
                visible = (ind == "All industries"),
                hovertemplate = paste0(s, ": %{y:,.0f} jobs<extra></extra>")) |>
      add_lines(data = d, x = ~census_year, y = ~share, name = s, yaxis = "y2",
                line = list(color = size_colours[[s]], width = 3),
                showlegend = FALSE, visible = (ind == "All industries"),
                hovertemplate = paste0(s, ": %{y:.1f}% of jobs<extra></extra>"))
  }
}

# Dropdown: show only the 6 lines that belong to the chosen industry
industry_menu <- lapply(industries, function(i)
  list(label = i, method = "restyle",
       args = list("visible", rep(industries == i, each = 6))))

p2 |>
  tidy_plotly("Jobs by business size, City of Melbourne, 2002-2024") |>
  layout(
    xaxis  = list(title = "Census year", anchor = "y2"),
    yaxis  = list(title = "Number of jobs", tickformat = ",",
                  rangemode = "tozero", domain = c(0.55, 1)),
    yaxis2 = list(title = "Share of jobs", ticksuffix = "%",
                  range = c(0, 100), domain = c(0, 0.45)),
    hovermode = "x unified",
    legend = list(x = 1, xanchor = "right"),
    margin = list(t = 110),
    updatemenus = list(list(type = "dropdown", x = 0, xanchor = "left",
                            y = 1.02, yanchor = "bottom", buttons = industry_menu))
  )

Source: City of Melbourne (2025), Business establishments and jobs data by business size and CLUE industry in municipality.

  • Compares job numbers and job shares across business sizes from 2002 to 2024.
  • Shows how employment patterns change over time and across industries.
  • Helps identify whether large businesses are becoming more important for employment.

3. Which industries depend on large businesses?

heat_x <- c("Small (under 20)", "Medium (20-199)", "Large (200+)")

heat <- clue_muni |>
  filter(business_size %in% size_levels) |>
  group_by(census_year, clue_industry, business_size) |>
  summarise(jobs = sum(total_jobs, na.rm = TRUE),
            suppressed = any(is.na(total_jobs)), .groups = "drop") |>
  complete(census_year, clue_industry, business_size = size_levels,
           fill = list(jobs = 0, suppressed = FALSE)) |>
    group_by(census_year, clue_industry) |>
  mutate(share = if (sum(jobs) > 0) jobs / sum(jobs) * 100 else NA_real_) |>
  ungroup() |>
  mutate(share = if_else(suppressed, NA_real_, share),
         size_label = as.character(factor(business_size, levels = size_levels,
                                          labels = heat_x)),
         na_label = if_else(suppressed, "n/a", ""),
         tooltip = paste0("<b>", clue_industry, " (", census_year, ")</b><br>",
                          size_label, ": ",
                          if_else(suppressed, "suppressed (2 or fewer businesses)",
                                  paste0(round(share, 1), "% of the industry's jobs<br>",
                                         comma(jobs), " jobs"))))

# Sort industries by how much they relied on large businesses in 2024
industry_order <- heat |>
  filter(census_year == 2024, business_size == "Large business") |>
  arrange(coalesce(share, 0)) |>
  pull(clue_industry)

heat_cols <- viridisLite::mako(100, direction = -1)
heat_scale <- lapply(seq_along(heat_cols),
                     function(i) list((i - 1) / (length(heat_cols) - 1), heat_cols[i]))

years3 <- sort(unique(heat$census_year))

# Two traces per year: the heatmap, and an "n/a" label layer for suppressed cells
p3 <- plot_ly()
for (yr in years3) {
  d <- filter(heat, census_year == yr)
  p3 <- p3 |>
    add_trace(data = d, type = "heatmap", x = ~size_label, y = ~clue_industry,
              z = ~share, text = ~tooltip, hoverinfo = "text",
              texttemplate = "%{z:.0f}%", textfont = list(size = 12),
              colorscale = heat_scale, zmin = 0, zmax = 100, xgap = 2, ygap = 2,
              colorbar = list(title = list(text = "Share of<br>industry<br>jobs"),
                              ticksuffix = "%", len = 0.6),
              visible = (yr == max(years3))) |>
    add_trace(data = d, type = "scatter", mode = "text",
              x = ~size_label, y = ~clue_industry, text = ~na_label,
              hovertext = ~tooltip, hoverinfo = "text",
              textfont = list(color = "#555555", size = 12),
              showlegend = FALSE, visible = (yr == max(years3)))
}

p3 |>
  tidy_plotly("Share of each industry's jobs by business size, City of Melbourne") |>
  layout(
    xaxis = list(title = "", side = "top", categoryorder = "array",
                 categoryarray = heat_x, showgrid = FALSE, zeroline = FALSE),
    yaxis = list(title = "", categoryorder = "array", categoryarray = industry_order,
                 showgrid = FALSE, zeroline = FALSE),
    margin = list(t = 110),
    sliders = list(list(
      active = length(years3) - 1,
      currentvalue = list(prefix = "Census year: ", font = list(size = 14)),
      pad = list(t = 20),
      steps = lapply(years3, function(yr)
        list(label = as.character(yr), method = "update",
             args = list(list(visible = rep(years3 == yr, each = 2)))))
    ))
  )

Source: City of Melbourne (2025), Business establishments and jobs data by business size and CLUE industry in municipality.

  • Shows the percentage of industry jobs provided by small, medium and large businesses.
  • Highlights differences in employment structure between industries.
  • The year slider helps identify changes in these patterns over time.
  • Suppressed values are shown as n/a and should not be treated as zero.

4. Where in the city do large businesses dominate?

area <- clue_area |>
  filter(business_size %in% size_levels) |>
  group_by(census_year, clue_small_area, business_size) |>
  summarise(jobs = sum(total_jobs, na.rm = TRUE),
            suppressed_est = sum(total_establishments[is.na(total_jobs)]),
            .groups = "drop") |>
  complete(census_year, clue_small_area, business_size = size_levels,
           fill = list(jobs = 0, suppressed_est = 0)) |>
  group_by(census_year, clue_small_area) |>
  mutate(area_jobs = sum(jobs), share = jobs / area_jobs * 100) |>
  ungroup() |>
  filter(area_jobs > 0) |>
  mutate(size_label = factor(business_size, levels = size_levels, labels = size_labels),
         tooltip = paste0("<b>", clue_small_area, " (", census_year, ")</b><br>",
                          size_label, ": ", round(share, 1), "% of jobs (",
                          comma(jobs), ")",
                          if_else(suppressed_est > 0,
                                  paste0("<br>Jobs at ", suppressed_est,
                                         " business(es) suppressed, not included"),
                                  ""),
                          "<br>Area total counted: ", comma(area_jobs), " jobs"))

# Sort areas by their large-business share in 2022
area_order <- area |>
  filter(census_year == 2022, business_size == "Large business") |>
  arrange(share) |>
  pull(clue_small_area)

years4 <- sort(unique(area$census_year))

# Three stacked bar traces (small, medium, large) per year
p4 <- plot_ly()
for (yr in years4) {
  for (s in size_labels) {
    d <- filter(area, census_year == yr, size_label == s)
    p4 <- p4 |>
      add_trace(data = d, type = "bar", orientation = "h",
                x = ~share, y = ~clue_small_area, name = s,
                marker = list(color = size_colours[[s]]),
                text = ~tooltip, hoverinfo = "text", textposition = "none",
                visible = (yr == max(years4)))
  }
}

p4 |>
  tidy_plotly("Share of jobs by business size in each small area") |>
  layout(
    barmode = "stack",
    xaxis = list(title = "Share of jobs in the area", ticksuffix = "%", range = c(0, 100)),
    yaxis = list(title = "", categoryorder = "array", categoryarray = area_order),
    sliders = list(list(
      active = length(years4) - 1,
      currentvalue = list(prefix = "Census year: ", font = list(size = 14)),
      pad = list(t = 50),
      steps = lapply(years4, function(yr)
        list(label = as.character(yr), method = "update",
             args = list(list(visible = rep(years4 == yr, each = 3)))))
    ))
  )

Source: City of Melbourne (2023), Business establishments and jobs data by business size and industry.

  • Compares the share of jobs by business size across Melbourne’s small areas.
  • Identifies areas where employment is more concentrated in large businesses.
  • The year slider shows how local employment patterns change from 2002 to 2022.
  • Suppressed jobs may make the large-business share appear lower than it actually is.]*

5. Is Melbourne’s city centre typical?

measures <- c("Jobs", "Wages and salaries", "Sales and service income")
places <- c("Australia (ABS 2024-25)", "Victoria (ABS 2024-25)",
            "City of Melbourne (CLUE 2024)")

benchmark <- bind_rows(
  size_2024 |>
    filter(business_size %in% size_levels) |>
    transmute(place = places[3], business_size, measure = "Jobs",
              share = jobs / sum(jobs) * 100,
              detail = paste0(comma(jobs), " jobs")),
  abs_all |>
    filter(financial_year == "2024-25", region %in% c("Victoria", "Australia")) |>
    transmute(place = paste0(region, " (ABS 2024-25)"), business_size, measure, share,
              detail = if_else(measure == "Jobs",
                               paste0(comma(value * 1000), " jobs"),
                               paste0("$", comma(value), " million")))
) |>
  mutate(size_label = factor(business_size, levels = size_levels, labels = size_labels),
         tooltip = paste0("<b>", place, "</b><br>", size_label, ": ",
                          round(share, 1), "% of ", str_to_lower(measure),
                          "<br>", detail))

# For each measure: a grey connector per place, then one dot trace per business size.
# Wages and sales also get a note, because CLUE does not record them.
p5 <- plot_ly()
trace_measure <- c()
for (m in measures) {
  d <- filter(benchmark, measure == m)
  seg <- d |> group_by(place) |> summarise(lo = min(share), hi = max(share))
  p5 <- p5 |>
    add_segments(data = seg, x = ~lo, xend = ~hi, y = ~place, yend = ~place,
                 line = list(color = "#CCCCCC", width = 6),
                 hoverinfo = "none", showlegend = FALSE, visible = (m == "Jobs"))
  trace_measure <- c(trace_measure, m)
  for (s in size_labels) {
    p5 <- p5 |>
      add_markers(data = filter(d, size_label == s), x = ~share, y = ~place,
                  name = s, marker = list(color = size_colours[[s]], size = 16,
                                          line = list(color = "white", width = 1)),
                  text = ~tooltip, hoverinfo = "text", visible = (m == "Jobs"))
    trace_measure <- c(trace_measure, m)
  }
  if (m != "Jobs") {
    p5 <- p5 |>
      add_text(x = 30, y = places[3],
               text = "No wages or sales data in CLUE for the City of Melbourne",
               textfont = list(color = "#777777", size = 13),
               hoverinfo = "none", showlegend = FALSE, visible = FALSE)
    trace_measure <- c(trace_measure, m)
  }
}

p5 |>
  tidy_plotly("City centre vs Victoria and Australia: who provides the jobs, wages and sales?") |>
  layout(
    xaxis = list(title = "Share of jobs", ticksuffix = "%", range = c(0, 65)),
    yaxis = list(title = "", categoryorder = "array", categoryarray = places),
    margin = list(t = 140),
    updatemenus = list(list(
      type = "buttons", direction = "right",
      x = 1, xanchor = "right", y = 1.17, yanchor = "bottom",
      buttons = lapply(measures, function(m)
        list(label = m, method = "update",
             args = list(list(visible = trace_measure == m),
                         list(xaxis = list(title = paste("Share of", str_to_lower(m)),
                                           ticksuffix = "%", range = c(0, 65))))))
    ))
  )

Sources: City of Melbourne (2025), Business establishments and jobs data by business size and CLUE industry in municipality; Australian Bureau of Statistics (2026), Australian industry, 2024–25 financial year, Tables 1–3.

  • Compares business-size contributions to jobs across Melbourne, Victoria and Australia.
  • Shows differences between the city’s employment structure and broader regional patterns.
  • The wages and sales views provide comparisons for Victoria and Australia.
  • City-level wages and sales data are unavailable, so these measures cannot be compared directly with Melbourne.

Conclusion

[Write your conclusion here: answer the question, using evidence from the five charts.]

References

Australian Bureau of Statistics. (2026). Australian industry, 2024–25 financial year [Data set]. ABS. https://www.abs.gov.au/statistics/industry/industry-overview/australian-industry/latest-release

City of Melbourne. (2025). Business establishments and jobs data by business size and CLUE industry in municipality [Data set]. City of Melbourne Open Data. https://data.melbourne.vic.gov.au/explore/dataset/business-establishments-and-jobs-data-by-business-size-and-clue-industry-in-municipality/

City of Melbourne. (2023). Business establishments and jobs data by business size and industry [Data set]. Data Vic. https://discover.data.vic.gov.au/dataset/55806b3a-9f14-4047-b640-b689626cb4ea

Generative AI acknowledgement

Acknowledgement of Generative AI Use

I used ChatGPT (OpenAI, 2026) to help debug my R code, identify and fix coding errors, and improve the grammar, spelling, and clarity of my writing. I reviewed the suggestions and checked the final code and written content myself.