New Zealand Housing Demographics

Author: Martin Wong @ Autonomous Econ
This is a demo site built using Quarto and Python. Check out
the full Substack post here.

Average home ownership has been dropping in each successive generation
at every given age group, see Figure 1.

Code
import pandas as pd
import plotly.express as px

# Load the demographic share dataset
df_ownership = pd.read_csv("home_ownership_generation.csv", sep=",")


# Function to plot home ownership rates by age and generation using Plotly
def plot_ownership_by_age_and_generation_plotly(df):
    # Define the custom color palette
    colors = {
        "Gen X": "#636EFA",  # blue
        "Baby Boomer": "#B6E880",  # light green
        "Gen Z": "#FFA15A",  # orange
        "Millennial": "#EF553B",  # red
        "Silent": "#FF97FF",  # pink
    }

    # Create the plot
    fig = px.line(
        df,
        x="Age group",
        y="Home Ownership Rate",
        color="Generation",
        title="",
        labels={
            "Age group": "Age Group",
            "Home Ownership Rate": "Home Ownership Rate (%)",
        },
        line_shape="linear",
        color_discrete_map=colors,  # Apply the custom color palette
    )

    fig.update_layout(
        xaxis_title="Age Group",
        yaxis_title="",
        legend_title_text="",
        template="plotly_dark",
        xaxis=dict(tickangle=45),
        plot_bgcolor="#282a36",
        paper_bgcolor="#282a36",
        font=dict(size=14, family="Consolas"),  # Set the font to Consolas
        title_font=dict(size=18, family="Consolas"),
        width=500,  # Adjust based on your needs
        height=450,  # Adjust based on your needs
        margin=dict(l=20, r=20, t=40, b=20),
    )

    fig.update_yaxes(title_text="", range=[40, 90])

    fig.show()


# Plot the data using Plotly

df_ownership_grouped = (
    df_ownership[
        (df_ownership["Generation"] != "Other")
        & (df_ownership["Generation"] != "Total")
        & (~df_ownership["Age group"].isin(["0-4", "5-9", "10-14"]))
    ]
    .groupby(["Age group", "Generation"])["Home Ownership Rate"]
    .mean()
    .reset_index()
)

plot_ownership_by_age_and_generation_plotly(df_ownership_grouped)
Figure 1: Home Ownership Rate (mean) by Generation
Source: Statistics NZ

Average home ownership since 1986 has declined the most for those
aged 25-34, see Figure 2.

Code
import pandas as pd
import plotly.graph_objects as go

# Load the demographic share dataset
df_filtered = pd.read_csv("home_ownership_generation.csv", sep=",")

# Filter the data for the age ranges 25 to 74
age_ranges = [
    "25-29",
    "30-34",
    "35-39",
    "40-44",
    "45-49",
    "50-54",
    "55-59",
    "60-64",
    "65-69",
    "70-74",
]
df_filtered = df_filtered[df_filtered["Age group"].isin(age_ranges)]


# Define a function to map age ranges to the new intervals
def map_age(age):
    if age in ["25-29", "30-34"]:
        return "25-34"
    elif age in ["35-39", "40-44"]:
        return "35-44"
    elif age in ["45-49", "50-54"]:
        return "45-54"
    elif age in ["55-59", "60-64"]:
        return "55-64"
    elif age in ["65-69", "70-74"]:
        return "65-74"
    else:
        return None


# Apply the function to create a new age column
df_filtered.loc[:, "Age_Group"] = df_filtered["Age group"].apply(map_age)

# Group the data by the new age column and calculate the mean Home Ownership Rate for each group
df_grouped = (
    df_filtered.groupby(["Year", "Age_Group"])["Home Ownership Rate"]
    .mean()
    .reset_index()
)

# Create a line plot using Plotly
fig = go.Figure()

# Define colors for each age group
colors = {
    "25-34": "#636EFA",  #  blue
    "35-44": "#B6E880",  # light green
    "45-54": "#FFA15A",  # orange
    "55-64": "#EF553B",  # red
    "65-74": "#FF97FF",  # pink
}

# Add traces for each age group
for age_group in df_grouped["Age_Group"].unique():
    age_group_data = df_grouped[df_grouped["Age_Group"] == age_group]
    fig.add_trace(
        go.Scatter(
            x=age_group_data["Year"],
            y=age_group_data["Home Ownership Rate"],
            mode="lines+markers",
            name=age_group,
            line=dict(color=colors[age_group]),
            showlegend=False,
        )
    )
    # Add annotation for the last data point of each age group
    fig.add_annotation(
        x=age_group_data["Year"].values[-1] + 1,
        y=age_group_data["Home Ownership Rate"].values[-1],
        text=age_group,
        font=dict(color=colors[age_group], family="Consolas"),
        showarrow=False,
        xanchor="left",
        yanchor="middle",
    )

# Update layout for dark mode
fig.update_layout(
    title="Home Ownership Rate (mean) by Age Group",
    template="plotly_dark",
    plot_bgcolor="#282a36",
    paper_bgcolor="#282a36",
    title_font=dict(size=18, family="Consolas"),
    xaxis_title="Year",
    yaxis_title="Home Ownership Rate",
    font=dict(size=14, family="Consolas"),
    width=500,  # Adjust based on your needs
    height=450,  # Adjust based on your needs
    margin=dict(l=20, r=50, t=40, b=20),
)

fig.update_yaxes(title_text="", range=[40, 90])
fig.update_xaxes(title_text="", range=[1985, 2020])

fig.show()
Figure 2: Home Ownership Rate (mean) by age group
Source: Statistics NZ

Baby boomers have been the driving home ownership growth but
their influence will begin to wane in the coming decade, see Figure 3.

Code
import pandas as pd
from plotly.subplots import make_subplots
import plotly.graph_objects as go

# Load the dataset
df = pd.read_csv("pop_estimate_processed_nz.csv", sep=",")
df_2030 = pd.read_csv("pop_estimate_processed_2030.csv", sep=",")


# Concatenate df_2030 to df
df_combined = pd.concat([df, df_2030], ignore_index=True)

# Apply the relabeling function to the 'Age' column
df_combined["Age"] = df_combined["Age"].apply(
    lambda age: "90+" if age == "90 Years and over" else age
)

# Define the years of interest for plotting (excluding 2018)
years = [1996, 2006, 2023, 2030]

# Create a subplot figure with 2 rows and 2 columns
fig = make_subplots(
    rows=2, cols=2, shared_yaxes=True, subplot_titles=[f"{year}" for year in years]
)

row_col_pairs = [(1, 1), (1, 2), (2, 1), (2, 2)]

for (row, col), year in zip(row_col_pairs, years):
    # Filter data for the specific year
    year_data = df_combined[df_combined["Year"] == year]

    # Determine bar colors based on 'Generation'
    colors = [
        "orange" if gen == "Baby Boomer" else "skyblue"
        for gen in year_data["Generation"]
    ]

    # Create bar traces for each age group
    fig.add_trace(
        go.Bar(
            x=year_data["Age"],
            y=year_data["Population"],
            marker_color=colors,
            showlegend=False,
        ),
        row=row,
        col=col,
    )

# Add annotation pointing to the 'Baby Boomer' bar for 1996
fig.add_annotation(
    x="65-69 Years",
    y=270000,  # Adjust the y-coordinate to move the annotation down
    xref="x1",
    yref="y1",
    text="Baby Boomers",
    showarrow=False,
    font=dict(
        color="orange", size=16, family="Consolas"
    ),  # Increase the font size and set font family
)

# Update layout for dark mode
fig.update_layout(
    template="plotly_dark",
    width=700,  
    height=700,  
    margin=dict(l=20, r=20, t=40, b=20),
    title_text="",
    plot_bgcolor="#282a36",
    paper_bgcolor="#282a36",
    font=dict(size=14, family="Consolas"),  # Set the font family for the entire plot
)

# Update axis labels and rotate x-axis tick labels
fig.update_xaxes(title_text="", tickangle=45, tickfont=dict(family="Consolas"))
fig.update_yaxes(title_text="", showticklabels=True, tickfont=dict(family="Consolas"))
fig.update_yaxes(title_text="", range=[0, 400000])


fig.show()
Figure 3: Population Distribution by Age Group
Source: Statistics NZ