Chart 1 — Rental stress for lowest 40% income households

This chart shows how rental stress has increased over time for the lowest-income Australians.

table12 <- read_excel(file, sheet = "Table 1.2", skip = 4) %>%
  filter(!is.na(`1994–95`)) %>%
  rename(category = 1)
## New names:
## • `` -> `...1`
## • `` -> `...2`
rent_costs_q1 <- table12 %>% filter(category == "Lowest quintile")
rent_costs_q2 <- table12 %>% filter(category == "Second quintile")

rent_costs_q1 <- rent_costs_q1 %>% mutate(across(-category, ~as.numeric(.)))
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `across(-category, ~as.numeric(.))`.
## Caused by warning:
## ! NAs introduced by coercion
rent_costs_q2 <- rent_costs_q2 %>% mutate(across(-category, ~as.numeric(.)))
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `across(-category, ~as.numeric(.))`.
## Caused by warning:
## ! NAs introduced by coercion
table13 <- read_excel(file, sheet = "Table 1.3", skip = 4) %>%
  filter(!is.na(`1994–95`)) %>%
  rename(category = 1)
## New names:
## • `` -> `...1`
## • `` -> `...2`
income_q1 <- table13 %>% filter(category == "Lowest quintile")
income_q2 <- table13 %>% filter(category == "Second quintile")

income_q1 <- income_q1 %>% mutate(across(-category, ~as.numeric(.)))
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `across(-category, ~as.numeric(.))`.
## Caused by warning:
## ! NAs introduced by coercion
income_q2 <- income_q2 %>% mutate(across(-category, ~as.numeric(.)))
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `across(-category, ~as.numeric(.))`.
## Caused by warning:
## ! NAs introduced by coercion
year_cols <- intersect(names(rent_costs_q1), names(income_q1))

stress_q1 <- (as.numeric(rent_costs_q1[1, year_cols]) /
                as.numeric(income_q1[1, year_cols])) * 100
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
stress_q2 <- (as.numeric(rent_costs_q2[1, year_cols]) /
                as.numeric(income_q2[1, year_cols])) * 100
## Warning: NAs introduced by coercion
## Warning: NAs introduced by coercion
stress_avg <- (stress_q1 + stress_q2) / 2

rent_stress <- tibble(
  year = year_cols,
  stress = stress_avg
)

fig1 <- plot_ly(
  data = rent_stress,
  x = ~year,
  y = ~stress,
  type = 'scatter',
  mode = 'lines+markers',
  line = list(color = '#D8352C', width = 3),
  marker = list(color = '#D8352C', size = 6)
) %>%
  layout(
    title = "Approximate Official Rental Stress (Lowest 40% Income Households)",
    xaxis = list(title = "Year"),
    yaxis = list(title = "% of income spent on housing"),
    width = 600
  )
## Warning: Specifying width/height in layout() is now deprecated.
## Please specify in ggplotly() or plot_ly()
fig1

Chart 2 — Rental stress by income quintile

This multivariate chart compares rental stress across all income groups.

# ---- load table 1.2 (costs) ----
table12 <- read_excel(file, sheet = "Table 1.2", skip = 4)
## New names:
## • `` -> `...1`
## • `` -> `...2`
names(table12) <- names(table12) %>%
  str_replace_all("\\(.*?\\)", "") %>%
  str_replace_all("–", "-") %>%
  str_trim()

table12 <- table12 %>%
  filter(!is.na(`1994-95`)) %>%
  rename(category = 1)

quintiles <- c(
  "Lowest quintile",
  "Second quintile",
  "Third quintile",
  "Fourth quintile",
  "Highest quintile"
)

rent_costs <- table12 %>%
  filter(category %in% quintiles)

# ---- load table 1.3 (income) ----
table13 <- read_excel(file, sheet = "Table 1.3", skip = 4)
## New names:
## • `` -> `...1`
## • `` -> `...2`
names(table13) <- names(table13) %>%
  str_replace_all("\\(.*?\\)", "") %>%
  str_replace_all("–", "-") %>%
  str_trim()

table13 <- table13 %>%
  filter(!is.na(`1994-95`)) %>%
  rename(category = 1)

income <- table13 %>%
  filter(category %in% quintiles)

# ---- strict year columns ----
years_rent   <- names(rent_costs)[grepl("^[0-9]{4}-[0-9]{2}$", names(rent_costs))]
years_income <- names(income)[grepl("^[0-9]{4}-[0-9]{2}$", names(income))]
year_cols    <- intersect(years_rent, years_income)

rent_costs[year_cols] <- lapply(rent_costs[year_cols], as.numeric)
income[year_cols]     <- lapply(income[year_cols], as.numeric)

# ---- build stress_df (no purrr) ----
stress_df <- data.frame()

for (q in quintiles) {
  
  rent_vals   <- unlist(rent_costs[rent_costs$category == q, year_cols])
  income_vals <- unlist(income[income$category == q, year_cols])
  
  # skip if missing
  if (length(rent_vals) == 0 || length(income_vals) == 0) next
  
  # ensure equal length
  n <- min(length(rent_vals), length(income_vals))
  
  temp <- data.frame(
    category = rep(q, n),
    year     = year_cols[1:n],
    stress   = (rent_vals[1:n] / income_vals[1:n]) * 100
  )
  
  stress_df <- rbind(stress_df, temp)
}


# ---- plot ----
fig2 <- plot_ly()

for (cat in unique(stress_df$category)) {
  
  temp <- stress_df %>% filter(category == cat)
  
  # convert year to numeric index
  temp$year_num <- seq_along(temp$year)
  
  # safe LOESS fit
  smoothed <- tryCatch(
    {
      predict(loess(stress ~ year_num, data = temp))
    },
    error = function(e) {
      # fallback: use raw values if smoothing fails
      temp$stress
    }
  )
  
  fig2 <- fig2 %>%
    add_trace(
      x = temp$year,
      y = smoothed,
      type = "scatter",
      mode = "lines",
      name = cat
    )
}

fig2 <- fig2 %>%
  layout(
    title = "Rental Stress by Income Quintile (Smoothed)",
    xaxis = list(title = "Year"),
    yaxis = list(title = "% of income spent on housing"),
    width = 600
  )
## Warning: Specifying width/height in layout() is now deprecated.
## Please specify in ggplotly() or plot_ly()
fig2

Chart 3 — Housing costs by household type

This chart shows how different household types experience different housing cost pressures.

# -----------------------------
# 1. Define household categories
# -----------------------------
households <- c(
  "Couple family with dependent children",
  "One parent family with dependent children",
  "Couple only",
  "Other one family households",
  "Multiple family households",
  "Lone person households",
  "Group households"
)

# -----------------------------
# 2. Filter rows
# -----------------------------
house_costs <- table12 %>%
  filter(category %in% households)

# -----------------------------
# 3. Strict year detection
# -----------------------------
years_house <- names(house_costs)[grepl("^[0-9]{4}-[0-9]{2}$", names(house_costs))]

# -----------------------------
# 4. Convert to numeric safely
# -----------------------------
house_costs <- house_costs %>%
  mutate(across(all_of(years_house), ~ as.numeric(as.character(.))))

# -----------------------------
# 5. Build long dataframe
# -----------------------------
house_df <- data.frame()

for (h in households) {
  vals <- unlist(house_costs[house_costs$category == h, years_house])
  
  temp <- data.frame(
    household = h,
    year      = years_house,
    cost      = vals
  )
  
  house_df <- rbind(house_df, temp)
}

# -----------------------------
# 6. Plot Graph 3
# -----------------------------
fig3 <- plot_ly()

for (cat in unique(house_df$household)) {
  
  temp <- house_df %>% filter(household == cat)
  
  temp$year_num <- seq_along(temp$year)
  
  smoothed <- tryCatch(
    {
      predict(loess(cost ~ year_num, data = temp))
    },
    error = function(e) {
      temp$cost
    }
  )
  
  fig3 <- fig3 %>%
    add_trace(
      x = temp$year,
      y = smoothed,
      type = "scatter",
      mode = "lines",
      name = cat
    )
}

fig3 <- fig3 %>%
  layout(
    title = "Housing Costs by Household Type (Smoothed)",
    xaxis = list(title = "Year"),
    yaxis = list(title = "Weekly Housing Costs ($)"),
    width = 600
  )
## Warning: Specifying width/height in layout() is now deprecated.
## Please specify in ggplotly() or plot_ly()
fig3

Chart 4 — Distribution of households by rental stress category

This stacked area chart shows how the proportion of households in each stress category has changed.

# -----------------------------
# 1. Define rental stress categories
# -----------------------------
stress_cats <- c(
  "25% or less",
  "More than 25% to 30%",
  "More than 30% to 50%",
  "More than 50%"
)

# -----------------------------
# 2. Filter rows from Table 1.3
# -----------------------------
stress_data <- table13 %>%
  filter(category %in% stress_cats)

# -----------------------------
# 3. Strict year detection
# -----------------------------
years_stress <- names(stress_data)[grepl("^[0-9]{4}-[0-9]{2}$", names(stress_data))]

# -----------------------------
# 4. Convert to numeric safely
# -----------------------------
stress_data <- stress_data %>%
  mutate(across(all_of(years_stress), ~ as.numeric(as.character(.))))

# -----------------------------
# 5. Build long dataframe
# -----------------------------
stress_df <- data.frame()

for (s in stress_cats) {
  vals <- unlist(stress_data[stress_data$category == s, years_stress])
  
  temp <- data.frame(
    stress_category = s,
    year            = years_stress,
    percent         = vals
  )
  
  stress_df <- rbind(stress_df, temp)
}

# -----------------------------
# 6. Plot Graph 4
# -----------------------------
# --- Auto-detect the category column (character/factor) ---
cat_col <- names(stress_df)[sapply(stress_df, is.character) | sapply(stress_df, is.factor)]
cat_col <- cat_col[cat_col != "year"][1]   # exclude year

# --- Auto-detect the numeric column (percentage values) ---
num_col <- names(stress_df)[sapply(stress_df, is.numeric) & names(stress_df) != "year"][1]

# --- Build stacked area chart safely ---
fig4 <- plot_ly(
  data = stress_df,
  x = ~year,
  y = as.formula(paste0("~`", num_col, "`")),
  color = as.formula(paste0("~`", cat_col, "`")),
  type = "scatter",
  mode = "none",
  stackgroup = "one"
) %>%
  layout(
    title = "Distribution of Households by Rental Stress Category (Stacked Area)",
    xaxis = list(title = "Year"),
    yaxis = list(title = "Percentage of Households (%)"),
    width = 600
  )
## Warning: Specifying width/height in layout() is now deprecated.
## Please specify in ggplotly() or plot_ly()
fig4

Chart 5 — Housing costs by dwelling type

This chart compares long-term cost trends across dwelling types.

# -----------------------------
# 1. Dwelling categories that exist
# -----------------------------
dwellings <- c(
  "Separate house",
  "Semi-detached, row or terrace house, townhouse",
  "Flat or apartment"
)

# -----------------------------
# 2. Filter rows
# -----------------------------
dwelling_costs <- table12 %>%
  filter(category %in% dwellings)

# -----------------------------
# 3. Detect year columns
# -----------------------------
years_dwelling <- names(dwelling_costs)[grepl("^[0-9]{4}-[0-9]{2}$", names(dwelling_costs))]

# -----------------------------
# 4. Convert to numeric
# -----------------------------
dwelling_costs <- dwelling_costs %>%
  mutate(across(all_of(years_dwelling), ~ as.numeric(as.character(.))))

# -----------------------------
# 5. Build long dataframe
# -----------------------------
dwelling_df <- data.frame()

for (d in dwellings) {
  vals <- unlist(dwelling_costs[dwelling_costs$category == d, years_dwelling])
  if (length(vals) == 0) next
  
  base <- vals[1]  # first year value
  
  pct_change <- ((vals - base) / base) * 100
  
  temp <- data.frame(
    dwelling = rep(d, length(vals)),
    year     = years_dwelling,
    pct_change = pct_change
  )
  
  dwelling_df <- rbind(dwelling_df, temp)
}

##
dwellings <- c(
  "Separate house",
  "Semi-detached, row or terrace house, townhouse",
  "Flat or apartment"
)

dwelling_costs <- table12 %>%
  filter(category %in% dwellings)

years_dwelling <- names(dwelling_costs)[grepl("^[0-9]{4}-[0-9]{2}$", names(dwelling_costs))]

dwelling_costs <- dwelling_costs %>%
  mutate(across(all_of(years_dwelling), ~ as.numeric(as.character(.))))

dwelling_df <- data.frame()

for (d in dwellings) {
  vals <- unlist(dwelling_costs[dwelling_costs$category == d, years_dwelling])
  if (length(vals) == 0) next
  
  temp <- data.frame(
    dwelling = rep(d, length(vals)),
    year     = years_dwelling,
    cost     = vals
  )
  
  dwelling_df <- rbind(dwelling_df, temp)
}

# -----------------------------
# 6. Plot percentage-change graph
# -----------------------------
fig5 <- plot_ly()

for (cat in unique(dwelling_df$dwelling)) {
  
  temp <- dwelling_df %>% filter(dwelling == cat)
  
  temp$year_num <- seq_along(temp$year)
  
  smoothed <- tryCatch(
    {
      predict(loess(cost ~ year_num, data = temp))
    },
    error = function(e) {
      temp$cost
    }
  )
  
  fig5 <- fig5 %>%
    add_trace(
      x = temp$year,
      y = smoothed,
      type = "scatter",
      mode = "lines",
      name = cat
    )
}

fig5 <- fig5 %>%
  layout(
    title = "Housing Costs by Dwelling Type (Smoothed)",
    xaxis = list(title = "Year"),
    yaxis = list(title = "Weekly Housing Costs ($)"),
    width = 600
  )
## Warning: Specifying width/height in layout() is now deprecated.
## Please specify in ggplotly() or plot_ly()
fig5

References

ABS. (2023). Housing occupancy and costs, Australia.