← Back to projects
CASE STUDY

Rent Affordability Radar

Monthly pulse to track rental stress across NZ territories. Built with Postgres + Power BI; designed for councils and housing NGOs to brief leadership in 10 seconds.

Executive summary

Interactive report

Try selecting beds/area on the left. Cards show the latest month; the bar ranks areas by “pressure score”.

Introduction

What problem are we solving?

Local councils and housing NGOs need a simple, credible view of rental stress to inform public updates and resource allocation. Raw bond data and ad-hoc spreadsheets make month-on-month changes hard to trust and even harder to communicate.

Problems

Key pain points

Success looked like: one page, latest month KPIs, obvious top/bottom areas, and a link that anyone can open.

Solutions

Chosen approach

  • Data model: monthly grain in Postgres (month, area_code, beds, rent_median, pressure_score).
  • Standardised measures (DAX): “Median Rent (latest)” and “YoY % (latest)” so every visual agrees.
  • Design: two slicers (beds, area), monthly line for trend, Top pressure score bar chart (slicer-immune), KPI cards for the latest month.
  • Distribution: Power BI “Publish to web” for frictionless viewing; the same iFrame is used on the portfolio site.

Why this works

  • Speed: monthly update becomes a dataset refresh, not a slide rebuild.
  • Clarity: executives get a 10-second answer: “What changed, and where should we look?”

Alternatives considered

  • Static PDF dashboards — simpler to share but no filters or drill; discarded.
  • Tableau Public — similar reach, but the team already operates Power BI; kept BI stack consistent.
  • Private embed — more control, but adds login friction for public comms; “Publish to web” fit the brief.

Key measures (DAX)

// Latest month label
Latest month label :=
FORMAT(MAX('Rent_Affordability'[month]), "yyyy MMM")

// Median Rent (latest)
Median Rent (latest) :=
VAR _latest = CALCULATE(MAX('Rent_Affordability'[month]), ALL('Rent_Affordability'))
RETURN CALCULATE(AVERAGE('Rent_Affordability'[rent_median]),
                 'Rent_Affordability'[month] = _latest)

// YoY % (latest)
YoY % (latest) :=
VAR _latest = CALCULATE(MAX('Rent_Affordability'[month]), ALL('Rent_Affordability'))
VAR _cur = CALCULATE(AVERAGE('Rent_Affordability'[rent_median]), 'Rent_Affordability'[month] = _latest)
VAR _prev = CALCULATE(AVERAGE('Rent_Affordability'[rent_median]),
  FILTER(ALL('Rent_Affordability'),
    YEAR('Rent_Affordability'[month])=YEAR(_latest)-1 &&
    MONTH('Rent_Affordability'[month])=MONTH(_latest)))
RETURN DIVIDE(_cur - _prev, _prev)
Conclusion

What we learned

Example (snapshot when published): latest median rent $475, YoY 1.06%, pressure highest in Wellington among compared areas.

Next steps

Recommendations