Logo CEDEPLAR

UNIVERSIDADE FEDERAL DE MINAS GERAIS

Centro de Economia Regional aplicada (CEDEPLAR)

PARA QUE E QUEM SERVE O METRO QUADRADO?

Como o jogo funciona em Belo Horizonte

Perito Responsável: Ramon Gregório Silva
Ocupação: Doutorando CEDEPLAR-UFMG
Ano de Referência das Transações: 2022

Timóteo - MG
Junho de 2026


R Markdown

This is an R Markdown document. Markdown is a simple formatting syntax for authoring HTML, PDF, and MS Word documents. For more details on using R Markdown see http://rmarkdown.rstudio.com.

When you click the Knit button a document will be generated that includes both content as well as the output of any embedded R code chunks within the document. You can embed an R code chunk like this:

# Garante que o pacote sidrar está instalado e carregado
if (!require("sidrar")) install.packages("sidrar")
library(sidrar)
library(tidyverse)
library(lubridate)

# 1. Puxa o IPCA (Série histórica) direto do IBGE a partir de 2008
ipca_ibge <- get_sidra(
  api = "/t/1737/n1/all/v/2266/p/all/d/v2266%2013"
)
## All others arguments are desconsidered when 'api' is informed
# 2. Trata os dados exatamente para o formato que você precisa
ipca_raw <- ipca_ibge %>%
  mutate(
    # Transforma o texto "janeiro 2008" em data real (2008-01-01)
    date = ym(paste0(str_sub(`Mês (Código)`, 1, 4), "-", str_sub(`Mês (Código)`, 5, 6))),
    value = Valor
  ) %>%
  filter(date >= as.Date("2008-01-01")) %>%
  select(date, value)

# 3. Executa a sua lógica exata de deflação
ipca_base <- ipca_raw %>%
  rename(data_mes = date, ipca_valor = value) %>%
  mutate(data_mes = floor_date(data_mes, "month")) %>%
  distinct(data_mes, .keep_all = TRUE) %>% 
  mutate(deflator = last(ipca_valor) / ipca_valor) %>%
  select(data_mes, deflator)


base_final <- base_completa %>%
  mutate(data_mes = floor_date(data_trans, "month"),
         ano = year(data_trans)) %>%
  left_join(ipca_base, by = "data_mes") %>%
  mutate(
    deflator = coalesce(deflator, 1),
    v_decl_real = v_decl * deflator,
    v_base_real = v_base * deflator,
    v_m2_real = v_decl_real / area
  )

library(dplyr)
library(stringr)

# Aplicando diretamente na sua base concluída
base_final <- base_final %>%
  mutate(
    rua_do_imovel = str_extract(endereco_oficial, "^.*?(?=\\s?\\d)") %>% str_trim()
  )

base_final <- base_final %>%
  mutate(
    rua_do_imovel = str_extract(endereco_oficial, "^.*?(?=\\s?\\d)") %>% str_trim()
  )

resultados_modelagem <- base_final %>%
  # Agrupamento refinado por rua e demais características
  group_by(ano, bairro, rua_do_imovel, padrao_acabamento, tipo_imovel) %>%
  
  # Cálculo do metro quadrado justo fixado no percentil 0.5
  mutate(v_m2_justo = quantile(v_m2_real, probs = 0.5, na.rm = TRUE)) %>%
  ungroup() %>%
  
  # Aplicação das métricas financeiras e fiscais de mercado
  mutate(
    valor_mercado_justo = v_m2_justo * area,
    itbi_devido_justo = valor_mercado_justo * 0.03,
    itbi_pago_efetivo = pmax(v_decl_real, v_base_real, na.rm = TRUE) * 0.03,
    prejuizo_cofres_publicos = pmax(0, itbi_devido_justo - itbi_pago_efetivo, na.rm = TRUE)
  )

resultados_modelagem <- resultados_modelagem %>%
  mutate(
    
    # 2. O seu coeficiente de mercado (CoefBase)
    # Aqui o cálculo é direto: quanto o mercado pagou vs quanto a PBH avaliou
    coefdecl =  v_m2_real/v_m2_justo
  )

df <-resultados_modelagem %>% filter(coefdecl<0.67)

Including Plots

library(ggplot2)
library(dplyr)

# 1. Preparação dos dados
dados_transacoes <- resultados_modelagem %>%
  group_by(ano) %>%
  summarise(total_transacoes = n())

# 2. Construção do gráfico
ggplot(dados_transacoes, aes(x = factor(ano), y = total_transacoes)) +
  geom_col(fill = "#2c3e50", color = "white") + # Colunas sólidas
  geom_text(aes(label = total_transacoes), vjust = -0.5, size = 3) + # Rótulos nos topos
  theme_minimal() +
  labs(
    title = "Volume de Transações Imobiliárias em BH por Ano",
    subtitle = "Análise da dinâmica de mercado (2008-2026)",
    x = "Ano",
    y = "Quantidade de Transações"
  ) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

library(plotly)
## Warning: pacote 'plotly' foi compilado no R versão 4.4.3
## 
## Anexando pacote: 'plotly'
## O seguinte objeto é mascarado por 'package:httr':
## 
##     config
## O seguinte objeto é mascarado por 'package:ggplot2':
## 
##     last_plot
## O seguinte objeto é mascarado por 'package:stats':
## 
##     filter
## O seguinte objeto é mascarado por 'package:graphics':
## 
##     layout
library(dplyr)

# 1. Preparação dos dados
dados_integrados <- resultados_modelagem %>% filter(tipo_imovel=='Apartamento') %>%
  group_by(ano) %>%
  summarise(
    media_m2_justo = mean(v_m2_justo, probs = 0.5, na.rm = TRUE),
    media_m2_real = mean(v_m2_real,probs = 0.5, na.rm = TRUE)
  )

# 2. Construção do gráfico interativo com Plotly
p <- plot_ly(dados_integrados, x = ~factor(ano)) %>%
  # Adicionando a linha do M2 Justo
  add_lines(y = ~media_m2_justo, name = "M2 Justo", 
            line = list(color = "#e67e22", width = 3)) %>%
  # Adicionando a linha do M2 Real
  add_lines(y = ~media_m2_real, name = "M2 Real", 
            line = list(color = "#27ae60", width = 3)) %>%
  # Layout e Design
  layout(
    title = "Evolução do Valor do m² em Belo Horizonte (2008-2026)",
    xaxis = list(title = "Ano", tickangle = 45),
    yaxis = list(title = "Valor do m² (R$)"),
    hovermode = "x unified", # Exibe os dois valores ao passar o mouse
    legend = list(orientation = "h", x = 0.5, xanchor = "center")
  )

# Exibir o gráfico
p
library(plotly)
library(dplyr)

# 1. Preparação dos dados: Cálculo dos quantis para v_m2_real
dados_quantis <- resultados_modelagem %>% 
  filter(tipo_imovel == 'Apartamento') %>%
  group_by(ano) %>%
  summarise(
    q25 = quantile(v_m2_real, probs = 0.25, na.rm = TRUE),
    q50 = quantile(v_m2_real, probs = 0.50, na.rm = TRUE),
    q75 = quantile(v_m2_real, probs = 0.75, na.rm = TRUE)
  )

# 2. Construção do gráfico interativo com Plotly
p <- plot_ly(dados_quantis, x = ~factor(ano)) %>%
  # Linha do Quantil 0.25 (Custo Médio Inferior)
  add_lines(y = ~q25, name = "Custo Médio (Q25)", 
            line = list(color = "#3498db", width = 2, dash = "dot")) %>%
  # Linha do Quantil 0.50 (Mediana - Custo Central)
  add_lines(y = ~q50, name = "Custo Médio (Q50)", 
            line = list(color = "#27ae60", width = 3)) %>%
  # Linha do Quantil 0.75 (Custo Médio Superior)
  add_lines(y = ~q75, name = "Custo Médio (Q75)", 
            line = list(color = "#c0392b", width = 2, dash = "dash")) %>%
  # Layout e Design
  layout(
    title = "Evolução dos Quantis do Valor do m² Real em BH (Apartamentos)",
    xaxis = list(title = "Ano", tickangle = 45),
    yaxis = list(title = "Valor do m² Real (R$)"),
    hovermode = "x unified",
    legend = list(orientation = "h", x = 0.5, xanchor = "center")
  )

# Exibir o gráfico
p
library(dplyr)
library(lubridate)

# Processamento da série mensal
dados_mensais <- resultados_modelagem %>% 
  filter(tipo_imovel == 'Apartamento') %>%
  mutate(mes = floor_date(as.Date(data_trans), "month")) %>%
  group_by(mes) %>%
  summarise(
    q25 = quantile(v_m2_real, probs = 0.25, na.rm = TRUE),
    q50 = quantile(v_m2_real, probs = 0.50, na.rm = TRUE),
    q75 = quantile(v_m2_real, probs = 0.75, na.rm = TRUE)
  ) %>%
  arrange(mes)
library(prophet)
## Warning: pacote 'prophet' foi compilado no R versão 4.4.3
## Carregando pacotes exigidos: Rcpp
## Carregando pacotes exigidos: rlang
## Warning: pacote 'rlang' foi compilado no R versão 4.4.3
## 
## Anexando pacote: 'rlang'
## Os seguintes objetos são mascarados por 'package:purrr':
## 
##     flatten, flatten_chr, flatten_dbl, flatten_int, flatten_lgl,
##     flatten_raw, invoke, splice
# O Prophet exige colunas com nomes específicos: 'ds' (data) e 'y' (valor)
df_prophet <- dados_mensais %>% 
  rename(ds = mes, y = q50)

# Cria e treina o modelo
m <- prophet(df_prophet)
## Disabling weekly seasonality. Run prophet with weekly.seasonality=TRUE to override this.
## Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
# Faz a previsão para os próximos 12 meses (após abril/2026)
futuro <- make_future_dataframe(m, periods = 12, freq = 'month')
previsao <- predict(m, futuro)

# Visualiza a mágica
plot(m, previsao)

prophet_plot_components(m, previsao)
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## ℹ The deprecated feature was likely used in the prophet package.
##   Please report the issue at <https://github.com/facebook/prophet/issues>.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

# 1. Função ajustada para treinar e prever cada quantil
prever_quantil <- function(dados, quantil_col) {
  df <- dados %>% select(mes, !!sym(quantil_col)) %>% rename(ds = mes, y = !!sym(quantil_col))
  
  # Corrigido: definindo weekly.seasonality = FALSE e removendo 'quiet'
  m <- prophet(df, 
               yearly.seasonality = TRUE, 
               weekly.seasonality = FALSE, 
               daily.seasonality = FALSE)
  
  futuro <- make_future_dataframe(m, periods = 6, freq = 'month') # Previsão até final de 2026
  previsao <- predict(m, futuro)
  return(previsao %>% mutate(quantil = quantil_col))
}

# 2. Executar a previsão para os três (Q25, Q50, Q75)
todos_quantis <- bind_rows(
  prever_quantil(dados_mensais, "q25"),
  prever_quantil(dados_mensais, "q50"),
  prever_quantil(dados_mensais, "q75")
)

# 3. Gráfico Interativo
p <- plot_ly(todos_quantis, x = ~ds) %>%
  add_lines(y = ~yhat, color = ~quantil, 
            colors = c("#3498db", "#27ae60", "#c0392b"),
            line = list(width = 3),
            name = ~quantil) %>%
  add_ribbons(ymin = ~yhat_lower, ymax = ~yhat_upper, 
              color = ~quantil, 
              colors = c("#3498db", "#27ae60", "#c0392b"),
              opacity = 0.2, 
              showlegend = FALSE) %>%
  layout(
    title = "Previsão Consolidada: Envelope de Mercado BH (2026)",
    xaxis = list(title = "Data"),
    yaxis = list(title = "Valor do m² (R$)"),
    hovermode = "x unified"
  )

p
# 1. Filtrar para o intervalo de Abr/2026 até Abr/2027
previsao_12meses <- todos_quantis %>%
  filter(ds >= as.Date("2026-04-01") & ds <= as.Date("2027-04-01"))
## Warning: There were 2 warnings in `filter()`.
## The first warning was:
## ℹ In argument: `ds >= as.Date("2026-04-01") & ds <= as.Date("2027-04-01")`.
## Caused by warning in `check_tzones()`:
## ! atributos 'tzone' inconsistentes
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
# 2. Gráfico interativo focado no próximo ano
p_12meses <- plot_ly(previsao_12meses, x = ~ds) %>%
  add_lines(y = ~yhat, color = ~quantil, 
            colors = c("#3498db", "#27ae60", "#c0392b"),
            line = list(width = 3), name = ~quantil) %>%
  add_ribbons(ymin = ~yhat_lower, ymax = ~yhat_upper, 
              color = ~quantil, 
              opacity = 0.2, showlegend = FALSE) %>%
  layout(
    title = "Previsão de Mercado: Abr/2026 a Abr/2027",
    xaxis = list(title = "Data"),
    yaxis = list(title = "Valor do m² (R$)"),
    hovermode = "x unified"
  )

p_12meses
library(plotly)
library(dplyr)
library(prophet)

# 1. Preparação da série mensal de ITBI (Soma)
dados_itbi <- resultados_modelagem %>% 
  filter(tipo_imovel == 'Apartamento') %>%
  mutate(mes = floor_date(as.Date(data_trans), "month")) %>%
  group_by(mes) %>%
  summarise(y = sum(itbi_pago_efetivo, na.rm = TRUE)) %>%
  rename(ds = mes)

# 2. Treino do modelo Prophet
m_itbi <- prophet(dados_itbi, 
                  yearly.seasonality = TRUE, 
                  weekly.seasonality = FALSE, 
                  daily.seasonality = FALSE)

# 3. Previsão para os próximos 12 meses
futuro_itbi <- make_future_dataframe(m_itbi, periods = 12, freq = 'month')
previsao_itbi <- predict(m_itbi, futuro_itbi)

# 4. Visualização Interativa (Previsão + Dispersão dos dados históricos)
p_itbi <- plot_ly(previsao_itbi, x = ~ds) %>%
  # Linha de tendência
  add_lines(y = ~yhat, name = "Tendência ITBI", line = list(color = "#e67e22", width = 3)) %>%
  # Intervalo de incerteza
  add_ribbons(ymin = ~yhat_lower, ymax = ~yhat_upper, 
              name = "Incerteza (Confiança)", line = list(color = 'transparent'),
              fillcolor = 'rgba(230, 126, 34, 0.2)') %>%
  # Pontos históricos reais (Soma mensal)
  add_markers(data = dados_itbi, x = ~ds, y = ~y, 
              name = "Real Pago", marker = list(color = "#2c3e50", size = 6)) %>%
  layout(
    title = "Previsão de Arrecadação: ITBI Pago Efetivo (BH)",
    xaxis = list(title = "Data"),
    yaxis = list(title = "Valor Total ITBI (R$)"),
    hovermode = "x unified"
  )

p_itbi
library(plotly)
library(dplyr)
library(prophet)

# 1. Preparação dos dados e treino (mantendo a série completa para o modelo aprender)
dados_itbi <- resultados_modelagem %>% 
  filter(tipo_imovel == 'Apartamento') %>%
  mutate(mes = floor_date(as.Date(data_trans), "month")) %>%
  group_by(mes) %>%
  summarise(y = sum(itbi_pago_efetivo, na.rm = TRUE)) %>%
  rename(ds = mes)

m_itbi <- prophet(dados_itbi, yearly.seasonality = TRUE, weekly.seasonality = FALSE)
## Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
futuro <- make_future_dataframe(m_itbi, periods = 12, freq = 'month')
previsao <- predict(m_itbi, futuro)

# 2. Filtrar exatamente o período desejado (Abr/26 a Dez/26)
previsao_restrita <- previsao %>% 
  filter(ds >= as.Date("2026-04-01") & ds <= as.Date("2026-12-01"))
## Warning: There were 2 warnings in `filter()`.
## The first warning was:
## ℹ In argument: `ds >= as.Date("2026-04-01") & ds <= as.Date("2026-12-01")`.
## Caused by warning in `check_tzones()`:
## ! atributos 'tzone' inconsistentes
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
# 3. Calcular o acumulado apenas para este intervalo de 9 meses
valor_acumulado_2026 <- previsao_restrita %>% 
  summarise(total = sum(yhat)) %>%
  pull(total)

texto_quadro <- paste0("Total Acumulado (Abr-Dez/26)<br>R$ ", format(round(valor_acumulado_2026, 2), big.mark = "."))
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' e 'decimal.mark' são ambos '.', o que pode ser confuso
# 4. Gráfico Interativo com o quadro ajustado
p <- plot_ly(previsao_restrita, x = ~ds) %>%
  add_lines(y = ~yhat, name = "Previsão Mensal", line = list(color = "#e67e22", width = 3)) %>%
  add_ribbons(ymin = ~yhat_lower, ymax = ~yhat_upper, 
              name = "Incerteza", fillcolor = 'rgba(230, 126, 34, 0.1)', line = list(color = 'transparent')) %>%
  layout(
    title = "Projeção de Arrecadação: ITBI BH (Abr/26 - Dez/26)",
    xaxis = list(title = "Data"),
    yaxis = list(title = "Valor Mensal (R$)"),
    hovermode = "x unified",
    annotations = list(
      x = 0.05, y = 0.95, xref = "paper", yref = "paper",
      text = texto_quadro,
      showarrow = FALSE,
      font = list(color = "white", size = 14),
      bgcolor = "#2c3e50",
      bordercolor = "#e67e22",
      borderwidth = 2,
      padding = 10
    )
  )

p
library(dplyr)
library(prophet)
library(lubridate)

# 1. Preparação dos dados e treino
dados_itbi <- resultados_modelagem %>% 
  filter(tipo_imovel == 'Apartamento') %>%
  mutate(mes = floor_date(as.Date(data_trans), "month")) %>%
  group_by(mes) %>%
  summarise(y = sum(itbi_pago_efetivo, na.rm = TRUE)) %>%
  rename(ds = mes)

m_itbi <- prophet(dados_itbi, yearly.seasonality = TRUE, weekly.seasonality = FALSE)
## Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
futuro <- make_future_dataframe(m_itbi, periods = 8, freq = 'month')
previsao <- predict(m_itbi, futuro)

# 2. Separar Realizado e Previsto
# Realizado: Jan a Abr 2026
realizado <- dados_itbi %>% 
  filter(ds >= as.Date("2026-01-01") & ds <= as.Date("2026-04-01")) %>%
  summarise(total = sum(y)) %>% pull(total)

# Previsto: Mai a Dez 2026
previsto <- previsao %>% 
  filter(ds >= as.Date("2026-05-01") & ds <= as.Date("2026-12-01")) %>%
  summarise(
    media = sum(yhat),
    inferior = sum(yhat_lower),
    superior = sum(yhat_upper)
  )
## Warning: There were 2 warnings in `filter()`.
## The first warning was:
## ℹ In argument: `ds >= as.Date("2026-05-01") & ds <= as.Date("2026-12-01")`.
## Caused by warning in `check_tzones()`:
## ! atributos 'tzone' inconsistentes
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
# 3. Consolidar a tabela de Estimativa 2026
estimativa_2026 <- data.frame(
  Cenario = c("Média", "Cenário Inferior (80% CI)", "Cenário Superior (80% CI)"),
  Estimativa_Total = c(
    realizado + previsto$media,
    realizado + previsto$inferior,
    realizado + previsto$superior
  )
)

# Exibir tabela formatada
library(knitr)
kable(estimativa_2026, format = "markdown", digits = 2, 
      col.names = c("Cenário 2026", "Total ITBI Acumulado (R$)"))
Cenário 2026 Total ITBI Acumulado (R$)
Média 387889385
Cenário Inferior (80% CI) 267160798
Cenário Superior (80% CI) 511442110
# 1. Obter o valor realizado até Junho/2026 (considerando que temos dados até 25/06)
realizado_acumulado_junho <- dados_itbi %>% 
  filter(ds <= as.Date("2026-06-01")) %>% 
  summarise(total = sum(y)) %>% pull(total)

# 2. Obter o valor previsto pelo Prophet para o mesmo período
previsto_acumulado_junho <- previsao %>% 
  filter(ds <= as.Date("2026-06-01")) %>% 
  summarise(total = sum(yhat)) %>% pull(total)
## Warning: There was 1 warning in `filter()`.
## ℹ In argument: `ds <= as.Date("2026-06-01")`.
## Caused by warning in `check_tzones()`:
## ! atributos 'tzone' inconsistentes
# 3. Cálculo do Desvio (Performance)
desvio_perc <- ((realizado_acumulado_junho / previsto_acumulado_junho) - 1) * 100

# 4. Exibição do "Status do Mercado"
cat(sprintf("--- STATUS DO MERCADO IMOBILIÁRIO (JUN/2026) ---\n"))
## --- STATUS DO MERCADO IMOBILIÁRIO (JUN/2026) ---
cat(sprintf("Realizado Acumulado: R$ %.2f\n", realizado_acumulado_junho))
## Realizado Acumulado: R$ 6246290534.15
cat(sprintf("Previsto Acumulado: R$ %.2f\n", previsto_acumulado_junho))
## Previsto Acumulado: R$ 6346496977.10
cat(sprintf("Desvio da Meta: %.2f%%\n", desvio_perc))
## Desvio da Meta: -1.58%
library(tidyr)

# 1. Preparar série de Preço (Q50) e ITBI (Soma)
serie_preco <- dados_mensais %>% 
  select(mes, q50) %>% rename(ds = mes, preco = q50)

serie_itbi <- resultados_modelagem %>% 
  mutate(mes = floor_date(as.Date(data_trans), "month")) %>%
  group_by(mes) %>%
  summarise(itbi = sum(itbi_pago_efetivo, na.rm = TRUE)) %>%
  rename(ds = mes)

# 2. Unir séries e calcular variações mensais (retorno)
df_analise <- serie_preco %>%
  inner_join(serie_itbi, by = "ds") %>%
  mutate(
    var_preco = (preco / lag(preco) - 1),
    var_itbi = (itbi / lag(itbi) - 1)
  ) %>%
  filter(!is.na(var_preco), !is.na(var_itbi))

# 3. Calcular Correlação
correlacao <- cor(df_analise$var_preco, df_analise$var_itbi, use = "complete.obs")

# 4. Exibir resultado
cat(sprintf("Correlação entre Variação do Preço e Variação do ITBI: %.2f\n", correlacao))
## Correlação entre Variação do Preço e Variação do ITBI: 0.27
# Gráfico para visualizar essa relação
plot(df_analise$var_preco, df_analise$var_itbi, 
     main = "Relação: Variação de Preço vs. Variação de Volume (ITBI)",
     xlab = "Variação do Preço (m²)", ylab = "Variação do Volume (ITBI)",
     pch = 19, col = "steelblue")
abline(lm(var_itbi ~ var_preco, data = df_analise), col = "red", lwd = 2)

## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors
## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors

## [1] "bairro"     "custo"      "ano"        "mediana_m2"
##  [1] 2015 2018 2019 2020 2021 2022 2023 2024 2025 2026 2008 2009 2011 2012 2013
## [16] 2010 2016 2017 2014

Note that the echo = FALSE parameter was added to the code chunk to prevent printing of the R code that generated the plot.

library(dplyr)
library(tidyr)
library(DT)

# 1. Base consolidada (a mesma de antes, mantendo a estrutura limpa)
df_base <- df_agrupado %>%
  group_by(bairro, custo, ano) %>%
  summarise(media_m2 = mean(mediana_m2, na.rm = TRUE), .groups = "drop") %>%
  crossing(area = c(20, 100, 200)) %>%
  mutate(preco_ano = media_m2 * area)

# 2. Função dinâmica focada no Ano Final
# O front-end envia apenas o ano_final desejado
calcular_tabela_por_ano_final <- function(df, ano_alvo, taxa_inflacao = 0.06) {
  
  # Pegamos os dados do ano alvo
  dados_alvo <- df %>% filter(ano == ano_alvo) %>% 
    select(bairro, custo, area, preco_final = preco_ano)
  
  # Cruzamos com todos os anos anteriores para calcular a variação
  df %>%
    filter(ano < ano_alvo) %>%
    left_join(dados_alvo, by = c("bairro", "custo", "area")) %>%
    mutate(
      tempo_posse = ano_alvo - ano,
      # Ajuste pela inflação do custo inicial até o ano final
      ganho_bruto = preco_final - preco_ano,
      
      # Redução fiscal (10% a cada 4 anos)
      fator_reducao = pmax(0.20, 1 - (floor(tempo_posse / 4) * 0.10)),
      base_trib = pmax(0, ganho_bruto * fator_reducao),
      
      # Impostos
      ir_pago = base_trib * 0.15,
      itbi = preco_final * 0.03,
      
      # Resultado Líquido
      ganho_liq = ganho_bruto - ir_pago - itbi,
      ic_inf = ganho_liq * 0.95,
      ic_sup = ganho_liq * 1.05
    )
}

# --- Exemplo de uso no seu Front-end ---
# Ao selecionar 2026, você terá a tabela com todos os anos anteriores comparados
tabela_dinamica <- calcular_tabela_por_ano_final(df_base, ano_alvo = 2026)

datatable(
  tabela_dinamica,
  filter = 'top',
  options = list(pageLength = 20, scrollX = TRUE)
)
## Warning in instance$preRenderHook(instance): It seems your data is too big for
## client-side DataTables. You may consider server-side processing:
## https://rstudio.github.io/DT/server.html

Argumentos centrais

O Oligopólio como Gestor de Ciclos No Santo Antônio, o terreno é um ativo finito. As poucas construtoras com capacidade financeira e know-how para aprovar projetos complexos em áreas de zoneamento restrito detêm, na prática, o controle da oferta. Elas não competem pelo menor preço, mas pelo controle da taxa de absorção.

Quando essas empresas decidem lançar unidades, elas precificam o metro quadrado não com base apenas no CUB (Custo Unitário Básico da Construção), mas na expectativa de inflação de ativos. Em 2014, o pico de R$ 7.231/m² no Santo Antônio não foi um acidente de mercado; foi um patamar fixado estrategicamente pelo oligopólio, aproveitando o otimismo desenfreado do período. Para o consumidor, a ilusão era de um ativo em constante valorização; para o oligopólio, era o ponto de saída ótimo para capitalizar sobre a euforia.

O “Efeito Tesoura”: Vendedores Estratégicos vs. Compradores de Ciclo O movimento de 2014 em BH criou um abismo financeiro que ainda hoje é visível nos balanços patrimoniais dos investidores:

A Saída do Vendedor (O Smart Money): Os proprietários de longa data (que compraram em 2008 ou antes) e os investidores institucionais que detinham estoques de terrenos realizaram seus lucros no topo. Eles “passaram a conta” da estagnação imobiliária que viria na década seguinte para o comprador.

A Armadilha do Comprador (O Retail Investor): O comprador de 2014, seduzido pelo “boom” dos preços, entrou no mercado pagando o custo do sucesso dos outros. O retorno líquido de míseros R$ 16 mil em 12 anos revela uma realidade dura: o comprador não estava adquirindo um ativo de valorização real, mas sim financiando a saída do capital de quem operou o oligopólio.

A Dinâmica do Santo Antônio: Um Caso de Estudo O bairro Santo Antônio é a vitrine dessa distorção. Por ser um bairro de “Custo Alto”, ele atrai o investidor conservador, que busca segurança, mas que acaba refém da baixa liquidez e do preço “ancorado”.

A “Ancoragem” de Preço: O oligopólio, ao manter poucos lançamentos e preços unitários elevados, cria uma referência de mercado artificial. Enquanto isso, imóveis usados no bairro ficam “presos” a essa referência, mesmo quando não há liquidez real para sustentá-la.

O Ganho Tributável e a Ilusão: A análise que fizemos do ganho de R$ 835 mil para o vendedor de 2008/2026 versus os R$ 16 mil do comprador de 2014/2026 é o resumo do mercado de BH: o lucro é, invariavelmente, uma transferência de riqueza de quem entrou no final do ciclo para quem saiu no topo.

Conclusão: O Mercado como um Jogo de Informação O que vemos em Belo Horizonte não é uma “crise de moradia”, mas uma crise de timing. O poder de fogo das construtoras e dos grandes detentores de terra (o oligopólio) permite que eles “sequem” o mercado nos momentos de stress, retirando a oferta para forçar a alta do m², ou despejando produtos no mercado quando o gráfico aponta o auge.

O investidor individual que ignora esses movimentos e compra “tijolo” em épocas de stress, acreditando na lenda urbana da “valorização eterna do imóvel”, é, invariavelmente, a peça que permite ao sistema se autorregular às custas do seu próprio capital. Em Belo Horizonte, o sucesso imobiliário sempre foi menos sobre a localização do imóvel e mais sobre quem possui a informação privilegiada do ciclo.