Skip to contents

Brazilian housing finance is largely organized around the SFH (Sistema Financeiro da Habitação), in which mortgages are funded by savings deposits (SBPE) and by the FGTS workers’ fund. Three datasets cover this market. abecip, bcb_series, and bcb_realestate appear below in increasing order of detail.

Table name Source Description
abecip Abecip Savings flows and financed units by the SBPE system.
bcb_series Brazilian Central Bank (SGS) Economic time series related to credit and housing credit (e.g. lending volumes, rates, delinquency, etc.).
bcb_realestate Brazilian Central Bank (Estatísticas do Mercado Imobiliário) A more detailed dataset containing economic time series related to housing credit.

realestatebr follows its sources closely, so monetary values come in nominal Brazilian reais with no inflation adjustment. This holds for all three datasets used here.

The code below defines a common theme for the plots in this article and can be omitted.

library(ekioplot)
color_palette <- ekioplot::ekio_pal()
theme_series <- function() {
  ekioplot::theme_ekio(base_size = 10) +
    theme(
      palette.color.discrete = color_palette
    )
}

Funding

SBPE savings accounts are an important funding source for mortgages, so sustained withdrawals eventually constrain new lending. The sbpe table from abecip tracks monthly deposits, withdrawals, and the resulting stock.

sbpe <- get_dataset("abecip", table = "sbpe")

glimpse(sbpe)
#> Rows: 540
#> Columns: 15
#> $ date              <date> 1982-01-01, 1982-02-01, 1982-03-01, 1982-04-01, 198…
#> $ sbpe_inflow       <dbl> 238234.1, 224080.0, 247218.8, 264925.0, 227636.3, 31…
#> $ sbpe_outflow      <dbl> 261523.1, 161176.0, 118662.8, 378395.0, 137201.3, 15…
#> $ sbpe_netflow      <dbl> -23289, 62904, 128556, -113470, 90435, 164739, -9934…
#> $ sbpe_netflow_pct  <dbl> -0.009387130, 0.021881448, 0.043761242, -0.037006429…
#> $ sbpe_yield        <dbl> 417103, 0, 0, 485995, 0, 0, 642432, 0, 0, 957944, 0,
#> $ sbpe_stock        <dbl> 2874764, 2937668, 3066224, 3438749, 3529184, 3693923…
#> $ rural_inflow      <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 
#> $ rural_outflow     <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 
#> $ rural_netflow     <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 
#> $ rural_netflow_pct <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 
#> $ rural_yield       <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 
#> $ rural_stock       <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 
#> $ total_stock       <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 
#> $ total_netflow     <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 

The plot shows the accumulated net flow over twelve months. Periods of persistent outflows, such as the years after 2021, signal tighter funding ahead.

sbpe_flows <- sbpe |>
  filter(date >= as.Date("2012-01-01")) |>
  mutate(
    netflow_12m = zoo::rollsumr(sbpe_netflow, k = 12, fill = NA) / 1e3
  )

ggplot(sbpe_flows, aes(date, netflow_12m)) +
  geom_area(fill = color_palette[1], alpha = 0.9) +
  geom_hline(yintercept = 0) +
  scale_x_date(
    date_breaks = "2 year",
    date_labels = "%Y",
    expand = expansion(mult = c(0))
  ) +
  labs(
    title = "SBPE Net Savings Flow",
    subtitle = "Accumulated net flow in 12 months",
    y = "R$ (billion)"
  ) +
  theme_series()

The rest of the funding market shows up in bcb_realestate. That dataset takes some time to learn, since a chain of v1 to v5 components identifies each series, but the query below stays short. It returns total credit funding from private sources for real estate loans in Brazil.

bcb_re <- get_dataset("bcb_realestate")

bcb_funding <- bcb_re |>
  filter(category == "fontes", v1 == "br") |>
  mutate(funding_source = factor(type), value_bln = value / 1e9)
ggplot(bcb_funding, aes(date, value_bln)) +
  geom_area(aes(fill = funding_source)) +
  scale_fill_manual(values = color_palette, labels = \(x) toupper(x)) +
  scale_x_date(
    date_breaks = "1 year",
    date_labels = "%Y",
    expand = expansion(mult = c(0))
  ) +
  scale_y_continuous(expand = expansion(c(0, 0.05))) +
  labs(
    title = "Market Funding Sources",
    subtitle = "Private source credit funding for real estate loans",
    y = "R$ (billion)",
    fill = NULL
  ) +
  theme_series()

Lending

The Central Bank reports the flow of new real estate loans to households. That series sits in the core table of bcb_series, which gathers the most relevant real estate credit series. We filter on name_simplified here, but the dataset also carries code_bcb, which matches the original SGS codes.

bcb <- get_dataset("bcb_series", table = "core")

fimob <- bcb |>
  filter(name_simplified == "fimob_pf_total") |>
  mutate(
    lending_12m = zoo::rollsumr(value, k = 12, fill = NA) / 1e3
  )

As mentioned above, these values are nominal (i.e. not adjusted for inflation).

ggplot(filter(fimob, !is.na(lending_12m)), aes(date, lending_12m)) +
  geom_line(color = color_palette[1], lwd = 0.8) +
  scale_x_date(date_breaks = "2 years", date_labels = "%Y") +
  labs(
    title = "New Mortgage Lending to Households",
    subtitle = "Earmarked real estate operations, accumulated in 12 months",
    y = "R$ (billion)"
  ) +
  theme_series()

In physical terms, the units table from abecip counts how many homes were financed with SBPE funds, split between construction and acquisition loans.

units <- get_dataset("abecip", table = "units")

units_12m <- units |>
  select(date, units_construction, units_acquisition) |>
  tidyr::pivot_longer(-date, names_to = "type", values_to = "units") |>
  mutate(
    units_12m = zoo::rollsumr(units, k = 12, fill = NA) / 1e3,
    type_label = if_else(
      type == "units_construction",
      "Construction",
      "Acquisition"
    ),
    .by = type
  )
ggplot(filter(units_12m, !is.na(units_12m)), aes(date, units_12m)) +
  geom_line(aes(color = type_label), lwd = 0.8) +
  scale_color_manual(values = color_palette[c(1, 2)]) +
  scale_x_date(date_breaks = "2 years", date_labels = "%Y") +
  labs(
    title = "Units Financed with SBPE Funds",
    subtitle = "Accumulated in 12 months",
    y = "Units (thousand)",
    color = NULL
  ) +
  theme_series()

The price of credit

Mortgage rates move with monetary policy but are partly insulated by regulated pricing within the SFH. The core table includes both the average rate charged on new operations and the average cost of the outstanding stock, which adjusts much more slowly.

rates <- bcb |>
  filter(name_simplified %in% c("taxa_fimob_pf_total", "icc_fimob")) |>
  mutate(
    rate_label = if_else(
      name_simplified == "icc_fimob",
      "Outstanding stock (average cost)",
      "New operations (average rate)"
    )
  )

rates_recent <- filter(rates, date >= as.Date("2012-01-01"))
ggplot(rates_recent, aes(date, value)) +
  geom_line(aes(color = rate_label), lwd = 0.8) +
  scale_color_manual(values = color_palette[c(1, 2)]) +
  scale_x_date(date_breaks = "2 years", date_labels = "%Y") +
  labs(
    title = "Household Mortgage Rates",
    subtitle = "Earmarked real estate credit operations (% per year)",
    y = "%",
    color = NULL
  ) +
  theme_series()

For a more detailed view of credit rates, we turn to bcb_realestate. The code below finds the rates for all credit lines at the national (BR) level. In these series pf stands for pessoa física (private individual) and pj for pessoa jurídica (company).

credit_rates <- bcb_re |>
  filter(
    category == "credito",
    v1 == "taxa",
    # only for families/individuals (i.e. pessoa física)
    v2 == "pf",
    # weighted average for Brazil
    abbrev_state == "BR"
  ) |>
  mutate(
    credit_label = dplyr::recode(
      v3,
      comercial = "Commercial",
      fgts = "FGTS",
      `home-equity` = "Home Equity",
      livre = "Market",
      sfh = "SFH"
    )
  )

The plot shows the average rate applied across each credit line, making the size of the spread clear.

ggplot(credit_rates, aes(date, value)) +
  geom_line(aes(color = credit_label), lwd = 0.8) +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  scale_y_continuous(limits = c(0, NA)) +
  labs(
    title = "Household Mortgage Rates",
    subtitle = "Detailed view of credit rates (% per year)",
    y = "%",
    color = NULL
  ) +
  theme_series()

Credit risk

Delinquency in housing credit is typically low because loans are collateralized, although borrowers can still fall behind when economic conditions deteriorate.

risk <- bcb |>
  filter(
    name_simplified %in%
      c(
        "inad_credito_direcionado_pf_fimob",
        "atraso_fimob_pf_total"
      )
  ) |>
  mutate(
    risk_label = if_else(
      name_simplified == "atraso_fimob_pf_total",
      "Arrears (15-90 days)",
      "Past due (90+ days)"
    )
  )
ggplot(risk, aes(date, value)) +
  geom_line(aes(color = risk_label), lwd = 0.8) +
  scale_color_manual(values = color_palette[c(2, 1)]) +
  scale_x_date(date_breaks = "2 years", date_labels = "%Y") +
  labs(
    title = "Household Mortgage Delinquency",
    subtitle = "Share of earmarked real estate credit operations",
    y = "%",
    color = NULL
  ) +
  theme_series()

Once again, for a more detailed view we use bcb_realestate. The code below finds the risk classification of the stock of SFH loans.

sfh_risk_stock <- bcb_re |>
  filter(
    category == "credito",
    type == "estoque",
    v1 == "risco-operacao",
    # only for families/individuals (i.e. pessoa física)
    v2 == "pf",
    v3 == "sfh",
    v5 == "br"
  ) |>
  mutate(
    credit_risk = factor(
      v4,
      levels = c("aa", "a", "b", "c", "d-mais"),
      labels = c("AA", "A", "B", "C", "D+")
    )
  )
ggplot(sfh_risk_stock, aes(date, value)) +
  geom_area(aes(fill = credit_risk)) +
  scale_fill_manual(values = color_palette) +
  scale_x_date(
    date_breaks = "1 year",
    date_labels = "%Y",
    expand = expansion(mult = c(0))
  ) +
  scale_y_continuous(expand = expansion(c(0, 0.05))) +
  labs(
    title = "Risk Classification of SFH Loans",
    subtitle = "Share of total SFH credit loans classified by risk credit (following resolution 2.682/1999)",
    y = "% (share of stock)",
    fill = NULL
  ) +
  theme_series()

Where the credit is

The bcb_realestate dataset has regional data by state. The code below sums the household credit portfolio across credit lines for each state’s latest available month.

stock_states <- bcb_re |>
  filter(
    category == "credito",
    type == "estoque",
    v1 == "carteira",
    v2 == "credito",
    v3 == "pf",
    abbrev_state != "BR"
  ) |>
  filter(date == max(date), .by = abbrev_state) |>
  summarise(stock = sum(value) / 1e9, .by = abbrev_state) |>
  slice_max(stock, n = 10)
ggplot(stock_states, aes(stock, reorder(abbrev_state, stock))) +
  geom_col(fill = color_palette[1], width = 0.8) +
  scale_x_continuous(expand = expansion(c(0, 0.05))) +
  labs(
    title = "Household Credit Stock by State",
    subtitle = "Ten largest states, latest available observation",
    y = NULL,
    x = "R$ (billion)"
  ) +
  theme_series() +
  theme(
    panel.grid.major.x = element_line(),
    panel.grid.major.y = element_blank(),
    axis.line.x = element_blank(),
    axis.ticks.x = element_blank(),
    axis.title.x = element_text(),
    axis.line.y = element_line(color = "gray10", linewidth = 0.5),
    axis.ticks.y = element_line(color = "gray10", linewidth = 0.5)
  )

Home equity lending

Home equity loans (CGI, Crédito com Garantia de Imóvel) remain a small but growing market. The cgi table from abecip summarizes contracts, balances, and default rates.

cgi <- get_dataset("abecip", table = "cgi")

glimpse(cgi)
#> Rows: 84
#> Columns: 8
#> $ year                <dbl> 2017, 2017, 2017, 2017, 2017, 2017, 2017, 2017, 20…
#> $ date                <date> 2017-01-01, 2017-02-01, 2017-03-01, 2017-04-01, 2…
#> $ new_contracts       <dbl> 1323, 1197, 1324, 809, 926, 982, 928, 905, 781, 80…
#> $ stock_contracts     <dbl> 108661, 101825, 99712, 99340, 102869, 98730, 98359…
#> $ loan                <dbl> 181295778, 157080758, 195053012, 132217320, 174307…
#> $ outstanding_balance <dbl> 12725110368, 11120992240, 10598834209, 10514921327…
#> $ average_term        <dbl> 144.8, 145.3, 140.8, 130.2, 134.3, 128.7, 137.4, 1…
#> $ default_rate        <dbl> 4.8, 5.0, 4.9, 5.2, 4.7, 4.4, 5.0, 4.7, 4.6, 4.7, 
ggplot(cgi, aes(date, stock_contracts / 1e3)) +
  geom_line(color = color_palette[1], lwd = 0.8) +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  labs(
    title = "Outstanding Home Equity Contracts",
    y = "Contracts (thousand)"
  ) +
  theme_series()

Learn more

The help topics ?abecip, ?bcb_series, and ?bcb_realestate document the columns of each dataset used here. For the primary market, see The Primary Market and the Construction Cycle; for price indices, see Working with Property Price Indices.