Projekt: Rozhodovacie stromy a súborové metódy

Author

Samuel Čierťaský

Code
# Reproducibility
set.seed(20251227)

# Packages (automaticky doinštaluje, ak chýbajú)
pkgs <- c(
  "tidyverse", "ggplot2", "scales", "broom",
  "rsample", "yardstick",
  "rpart", "rpart.plot",
  "ranger", "vip",
  "gbm",
  "pdp",
  "BART",
  "knitr", "kableExtra"
)

to_install <- pkgs[!vapply(pkgs, requireNamespace, FUN.VALUE = logical(1), quietly = TRUE)]
if (length(to_install) > 0) install.packages(to_install)

# Load
library(tidyverse)
library(rsample)
library(yardstick)
library(rpart)
library(rpart.plot)
library(ranger)
library(vip)
library(gbm)
library(pdp)
library(BART)
library(knitr)
library(kableExtra)

theme_set(theme_minimal(base_size = 12))

# Helper: pekná tabuľka
kbl <- function(df, caption = NULL) {
  knitr::kable(df, digits = 4, caption = caption) |>
    kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
}

# Helper: metriky pre regresiu
reg_metrics <- function(truth, estimate) {
  tibble(
    RMSE = rmse_vec(truth, estimate),
    MAE  = mae_vec(truth, estimate),
    R2   = rsq_vec(truth, estimate)
  )
}

1 Cieľ projektu

Cieľom práce je predikovať cenu nehnuteľnosti na základe jej vlastností (lokalita, veľkosť, dostupnosť služieb, kvalita a pod.) pomocou rozhodovacích stromov a súborových metód.

V rámci práce je analyzovaný regresný problém a porovnané sú viaceré prístupy – jednoduchý rozhodovací strom, jeho prerezávanie, bagging, random forest, boosting a BART.

Ako doplnková analýza je rovnaký dátový súbor použitý aj na klasifikačnú úlohu (rozdelenie nehnuteľností na „drahé“ a „lacné“), čo umožňuje demonštrovať správanie stromových metód aj v klasifikačnom nastavení a porovnať ich výkonnosť pomocou metriky presnosti, ROC kriviek a AUC.

2 Dataset - simulované údaje o cenách nehnuteľností

2.1 Generovanie datasetu

Vygenerujeme dataset, ktorý zámerne obsahuje:

  • nelinearity (napr. efekt vzdialenosti do centra),

  • interakcie (napr. izby × kvalita),

  • šum (nepozorované faktory).

  • pevný počet pozorovaní (n=1500), ktorý zabezpečuje dostatočný objem dát na trénovanie a pozorovanie rôznych modelov

Code
n <- 1500

dat <- tibble(
  # "lokalita"
  dist_center_km = runif(n, 0.2, 25),                 # vzdialenosť do centra
  transit_score  = pmin(pmax(rnorm(n, 60, 15), 0), 100), # dostupnosť MHD
  noise_db       = pmin(pmax(rnorm(n, 55, 8), 35), 85),  # hluk
  crime_index    = rgamma(n, shape = 2.2, rate = 0.18),  # kriminalita (vyššie horšie)
  parks_1km      = rpois(n, lambda = 2.0),               # parky v okolí
  schools_score  = pmin(pmax(rnorm(n, 65, 12), 0), 100), # kvalita škôl

  # "nehnuteľnosť"
  area_m2        = pmin(pmax(rnorm(n, 85, 25), 25), 220),
  rooms          = pmin(pmax(round(rnorm(n, 3.2, 1.1)), 1), 8),
  age_years      = pmin(pmax(rnorm(n, 35, 18), 0), 120),
  quality        = factor(sample(c("low","mid","high"), n, replace = TRUE, prob = c(0.25, 0.55, 0.20)),
                        levels = c("low","mid","high")),
  has_garage     = rbinom(n, 1, 0.55),
  energy_class   = factor(sample(c("A","B","C","D","E"), n, replace = TRUE, prob = c(0.10,0.25,0.35,0.20,0.10)),
                        levels = c("A","B","C","D","E"))
) |>
  mutate(
    # pomocné transformácie
    log_crime = log1p(crime_index),
    # nelineárny "penalizačný" efekt vzdialenosti do centra
    dist_pen  = 18 * log1p(dist_center_km) + 0.25 * dist_center_km,
    # kvalita ako číselný efekt
    qual_eff  = case_when(quality == "low" ~ -25,
                          quality == "mid" ~ 0,
                          quality == "high" ~ 35),
    # energetická trieda
    energy_eff = case_when(energy_class == "A" ~ 18,
                           energy_class == "B" ~ 10,
                           energy_class == "C" ~ 0,
                           energy_class == "D" ~ -10,
                           TRUE ~ -22)
  )

# "pravá" (skrytá) generujúca funkcia ceny (v tis. €)
# (zámerne nelineárna + interakcie)
price_true <- with(dat,
  90 +
    1.15 * area_m2 +
    8.0  * rooms +
    qual_eff +
    energy_eff +
    0.45 * transit_score +
    0.55 * schools_score +
    6.0  * sqrt(parks_1km + 1) -
    1.2  * (noise_db - 50) -
    22.0 * log_crime -
    dist_pen +
    0.22 * area_m2 * (quality == "high") +   # interakcia: veľké byty profitujú viac pri vysokej kvalite
    10.0 * has_garage -
    0.35 * pmax(age_years - 30, 0) +
    4.5  * sin(dist_center_km / 3)           # jemná periodicita (napr. zóny mesta)
)

# pozorovaná cena = true + šum
dat <- dat |>
  mutate(
    price_k = price_true + rnorm(n, sd = 25),           # tis. €
    price_k = pmax(price_k, 30)                         # nech nie sú záporné
  ) |>
  select(-dist_pen, -qual_eff, -energy_eff)

# --- KLASIFIKAČNÝ CIEĽ ---
# "drahé" = horný kvartil (25% najdrahších)
thr_q75 <- quantile(dat$price_k, 0.75)

dat <- dat |>
  mutate(is_expensive = factor(if_else(price_k >= thr_q75, "yes", "no"),
                               levels = c("no","yes")))

glimpse(dat)
Rows: 1,500
Columns: 15
$ dist_center_km <dbl> 19.6244015, 21.2514161, 15.8988127, 10.0187908, 15.9783…
$ transit_score  <dbl> 44.99089, 64.03626, 48.07273, 72.18180, 37.66795, 49.82…
$ noise_db       <dbl> 64.79732, 57.71428, 62.49815, 44.90752, 62.52503, 39.99…
$ crime_index    <dbl> 12.374406, 1.714822, 5.851662, 20.221064, 38.411531, 14…
$ parks_1km      <int> 3, 0, 2, 1, 5, 2, 2, 3, 3, 4, 0, 3, 0, 3, 3, 2, 1, 3, 1…
$ schools_score  <dbl> 81.02516, 74.04633, 80.73988, 70.30033, 49.87161, 86.06…
$ area_m2        <dbl> 44.96228, 102.70939, 113.54407, 89.84484, 88.29500, 57.…
$ rooms          <dbl> 3, 4, 4, 1, 4, 2, 4, 2, 4, 3, 3, 3, 3, 4, 5, 4, 2, 2, 2…
$ age_years      <dbl> 50.123378, 57.830761, 39.210458, 30.784562, 35.766773, …
$ quality        <fct> low, mid, low, low, mid, low, high, mid, mid, low, mid,…
$ has_garage     <int> 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0…
$ energy_class   <fct> B, C, B, B, C, B, B, B, C, A, E, D, A, E, D, B, C, C, C…
$ log_crime      <dbl> 2.5933428, 0.9987264, 1.9244913, 3.0549943, 3.6740584, …
$ price_k        <dbl> 99.18953, 249.81685, 229.61931, 179.04926, 109.13316, 1…
$ is_expensive   <fct> no, yes, yes, no, no, no, no, no, no, no, no, yes, no, …

2.2 Charakteristika použitého datasetu

Použitý dataset bol vytvorený synteticky s cieľom simulovať realistické podmienky trhu s nehnuteľnosťami a umožniť systematické porovnanie rôznych modelov strojového učenia. Dáta boli generované tak, aby zachytávali typické vzťahy medzi vlastnosťami nehnuteľností a ich cenou, pričom zároveň umožňovali kontrolu nad štruktúrou problému a elimináciu vplyvu neznámych externých faktorov.

Dataset obsahuje premenné opisujúce fyzické vlastnosti nehnuteľnosti (napr. rozloha, počet izieb, vek stavby), lokalitné faktory (vzdialenosť od centra, dostupnosť služieb, miera kriminality, hlučnosť prostredia) aj kvalitatívne charakteristiky (energetická trieda, celková kvalita bývania). Tieto premenné boli zvolené tak, aby zodpovedali typickým determinantom cien nehnuteľností pozorovaným v reálnych dátach.

Hodnota cieľovej premennej – cena nehnuteľnosti – bola generovaná ako nelineárna funkcia viacerých vstupných premenných. Model zahŕňa interakčné efekty (napr. medzi veľkosťou nehnuteľnosti a jej kvalitou), ako aj náhodnú zložku reprezentujúcu nepozorované faktory trhu. Takýto spôsob generovania dát umožňuje vznik realistickej variability a zároveň vytvára prostredie vhodné na testovanie schopnosti modelov zachytiť komplexné vzťahy.

Zámerom použitia syntetických dát nebolo simulovať konkrétny realitný trh, ale vytvoriť kontrolované experimentálne prostredie, v ktorom možno transparentne porovnávať správanie rôznych modelov – od jednoduchých rozhodovacích stromov až po pokročilé súborové metódy. Vďaka tomu je možné lepšie interpretovať rozdiely medzi modelmi a ich schopnosť generalizácie bez vplyvu šumu alebo skreslení typických pre reálne datasety.

3 EDA (prehľad dát)

Code
dat |>
  summarise(
    n = n(),
    price_mean = mean(price_k),
    price_sd   = sd(price_k),
    price_min  = min(price_k),
    price_max  = max(price_k)
  ) |>
  kbl("Základný prehľad cieľovej premennej (cena v tis. €)")
Základný prehľad cieľovej premennej (cena v tis. €)
n price_mean price_sd price_min price_max
1500 188.5768 54.4041 30 387.4916

Základná deskriptívna analýza ukazuje, že priemerná cena nehnuteľnosti v dátach je približne 189 tis. €, pričom ceny sa pohybujú v pomerne širokom intervale od 30 do 387 tis. €. Relatívne vysoká smerodajná odchýlka naznačuje výraznú variabilitu cien, čo poukazuje na heterogénnosť dát a opodstatňuje použitie flexibilných modelov schopných zachytiť nelineárne vzťahy.

Code
p1 <- ggplot(dat, aes(price_k)) +
  geom_histogram(bins = 40) +
  labs(title = "Rozdelenie ceny (price_k)", x = "Cena (tis. €)", y = "Počet")

p2 <- ggplot(dat, aes(dist_center_km, price_k)) +
  geom_point(alpha = 0.25) +
  geom_smooth(se = FALSE) +
  labs(title = "Cena vs vzdialenosť do centra", x = "Vzdialenosť (km)", y = "Cena (tis. €)")

p1

Code
p2

Histogram ceny nehnuteľností ukazuje približne symetrické, mierne pravostranné rozdelenie s vrcholom okolo hodnoty 180–200 tis. €. Väčšina pozorovaní sa nachádza v intervale približne 120–260 tis. €, pričom extrémne nízke a vysoké ceny sa vyskytujú len zriedkavo. Rozdelenie neobsahuje výrazné odľahlé hodnoty, čo naznačuje, že dáta sú vhodné pre ďalšie modelovanie bez potreby výrazných transformácií cieľovej premennej.

Z grafu závislosti ceny od vzdialenosti od centra je zrejmý mierne klesajúci trend – s rastúcou vzdialenosťou od centra má cena nehnuteľnosti tendenciu klesať. Zároveň je však rozptyl hodnôt pomerne veľký, čo naznačuje, že samotná vzdialenosť od centra nevysvetľuje cenu dostatočne a jej vplyv je ovplyvnený ďalšími faktormi (napr. veľkosť, kvalita, dostupnosť služieb). Tento nelineárny a šumom ovplyvnený vzťah podporuje použitie flexibilnejších modelov, ako sú rozhodovacie stromy a súborové metódy.

4 Split dát (train/test)

Code
set.seed(20251227)
spl <- initial_split(dat, prop = 0.8)
train <- training(spl)
test  <- testing(spl)
train_reg <- train |> dplyr::select(-is_expensive)
test_reg  <- test  |> dplyr::select(-is_expensive)

c(n_train = nrow(train), n_test = nrow(test)) |>
  as.data.frame() |>
  kbl("Počet pozorovaní v train/test")
Počet pozorovaní v train/test
c(n_train = nrow(train), n_test = nrow(test))
n_train 1200
n_test 300

Dáta boli rozdelené na trénovaciu a testovaciu množinu v pomere 80 : 20, čo zodpovedá 1200 pozorovaniam v trénovacej a 300 pozorovaniam v testovacej množine. Takéto rozdelenie poskytuje dostatočný počet dát na učenie modelov a zároveň umožňuje objektívne vyhodnotenie ich generalizačnej schopnosti na nezávislých pozorovaniach. Zároveň sme vytvoril aj testovaciu a trénovaciu vzorku, ktorá neobsahuje prediktor is_expensive, ktorý by nám v regresea spôsoboval data leakedge.

5 Rozhodovací strom (regresný)

5.1 Veľký strom a problém overfittingu

Najprv necháme strom “narásť” (malá penalizácia, malý minbucket), aby sme videli riziko preučenia.

Code
fit_tree_big <- rpart(
  price_k ~ .,
  data = train_reg,
  method = "anova",
  control = rpart.control(cp = 0.0005, minbucket = 8, maxdepth = 30)
)

#fit_tree_big
Code
rpart.plot(fit_tree_big, main = "Veľký (takmer neorezaný) regresný strom", roundint = FALSE)

Vyhodnotenie na train vs test:

Code
pred_train <- predict(fit_tree_big, newdata = train_reg)
pred_test  <- predict(fit_tree_big, newdata = test_reg)

bind_rows(
  reg_metrics(train_reg$price_k, pred_train) |> mutate(dataset = "train"),
  reg_metrics(test_reg$price_k,  pred_test)  |> mutate(dataset = "test")
) |>
  select(dataset, RMSE, MAE, R2) |>
  kbl("Výkon veľkého stromu: train vs test")
Výkon veľkého stromu: train vs test
dataset RMSE MAE R2
train 26.2418 20.6205 0.7682
test 39.8508 32.2709 0.4843

Na základe dosiahnutých výsledkov možno pozorovať, že veľký regresný strom dosahuje pomerne dobrý výkon na trénovacej množine (RMSE ≈ 26.2, R² ≈ 0.77), čo naznačuje vysokú schopnosť modelu prispôsobiť sa dostupným dátam. Tento výsledok však nie je možné považovať za spoľahlivý ukazovateľ kvality modelu, keďže ide o výkon na trénovacích dátach a model zatiaľ nebol porovnaný s alternatívnymi prístupmi.

Na testovacej množine dochádza k výraznému poklesu výkonnosti (RMSE ≈ 39.9, R² ≈ 0.48), čo poukazuje na obmedzenú schopnosť generalizácie. Rozdiel medzi výkonom na trénovacích a testovacích dátach naznačuje, že model je do istej miery preučený, keďže vytvára veľmi komplexnú štruktúru založenú na veľkom počte rozdelení a čiastočne zachytáva aj náhodný šum v dátach.

Z tohto dôvodu je opodstatnené ďalej skúmať vplyv regulácie modelu, konkrétne prerezávania stromu, s cieľom znížiť jeho komplexnosť a zlepšiť generalizačné vlastnosti na nové, nevidené dáta.

5.2 Prerezávanie stromu (cost-complexity pruning + CV)

V rpart si vieme pozrieť tabuľku komplexity (cptable) a vybrať vhodné cp (často podľa CV chyby).

Code
cp_tab <- as.data.frame(fit_tree_big$cptable)
colnames(cp_tab) <- make.names(colnames(cp_tab))

cp_tab <- cp_tab |>
  rownames_to_column("row") |>
  select(
    cp = CP,
    nsplits = nsplit,
    rel_error = rel.error,
    xerror,
    xstd
  )

kbl(cp_tab, "rpart cptable (rel_error = train, xerror = CV odhad)")
rpart cptable (rel_error = train, xerror = CV odhad)
cp nsplits rel_error xerror xstd
0.2107 0 1.0000 1.0006 0.0424
0.1308 1 0.7893 0.8293 0.0347
0.0619 2 0.6586 0.6780 0.0282
0.0339 3 0.5967 0.6089 0.0254
0.0253 4 0.5628 0.5996 0.0254
0.0197 5 0.5375 0.5743 0.0244
0.0194 6 0.5178 0.5719 0.0241
0.0149 7 0.4984 0.5607 0.0239
0.0134 8 0.4835 0.5486 0.0235
0.0123 9 0.4700 0.5442 0.0231
0.0114 10 0.4578 0.5354 0.0232
0.0111 11 0.4464 0.5345 0.0230
0.0106 12 0.4353 0.5346 0.0230
0.0087 13 0.4247 0.5295 0.0227
0.0082 14 0.4160 0.5145 0.0219
0.0064 15 0.4078 0.5120 0.0217
0.0054 16 0.4014 0.5141 0.0221
0.0052 17 0.3960 0.5129 0.0224
0.0051 18 0.3908 0.5135 0.0226
0.0051 19 0.3857 0.5135 0.0226
0.0049 20 0.3806 0.5142 0.0227
0.0049 21 0.3757 0.5148 0.0227
0.0045 23 0.3659 0.5141 0.0227
0.0044 24 0.3614 0.5131 0.0230
0.0044 25 0.3570 0.5100 0.0227
0.0041 26 0.3526 0.5105 0.0226
0.0039 27 0.3484 0.5061 0.0226
0.0038 28 0.3445 0.5078 0.0228
0.0038 29 0.3407 0.5084 0.0228
0.0038 31 0.3331 0.5084 0.0228
0.0037 32 0.3293 0.5090 0.0229
0.0035 33 0.3256 0.5103 0.0229
0.0034 34 0.3220 0.5097 0.0232
0.0033 35 0.3186 0.5071 0.0231
0.0031 36 0.3153 0.5054 0.0230
0.0031 37 0.3122 0.5086 0.0231
0.0031 38 0.3091 0.5086 0.0231
0.0031 39 0.3060 0.5086 0.0231
0.0030 40 0.3030 0.5083 0.0231
0.0029 41 0.2999 0.5085 0.0231
0.0027 42 0.2970 0.5088 0.0232
0.0026 43 0.2943 0.5113 0.0231
0.0025 44 0.2916 0.5084 0.0231
0.0023 45 0.2892 0.5066 0.0229
0.0023 46 0.2868 0.5124 0.0232
0.0022 47 0.2845 0.5108 0.0233
0.0022 48 0.2823 0.5106 0.0232
0.0021 49 0.2802 0.5112 0.0232
0.0020 51 0.2761 0.5124 0.0232
0.0020 52 0.2740 0.5127 0.0232
0.0020 53 0.2720 0.5112 0.0232
0.0019 54 0.2700 0.5124 0.0231
0.0019 55 0.2681 0.5143 0.0231
0.0019 57 0.2643 0.5154 0.0231
0.0017 59 0.2606 0.5167 0.0232
0.0017 60 0.2589 0.5164 0.0230
0.0016 61 0.2572 0.5170 0.0230
0.0016 62 0.2556 0.5176 0.0230
0.0015 63 0.2540 0.5183 0.0230
0.0015 64 0.2525 0.5193 0.0231
0.0013 65 0.2510 0.5172 0.0229
0.0013 66 0.2497 0.5175 0.0231
0.0013 67 0.2484 0.5181 0.0231
0.0012 68 0.2472 0.5184 0.0231
0.0012 69 0.2460 0.5198 0.0231
0.0011 71 0.2435 0.5203 0.0232
0.0011 72 0.2425 0.5187 0.0232
0.0010 74 0.2404 0.5182 0.0232
0.0010 75 0.2393 0.5177 0.0232
0.0010 76 0.2383 0.5183 0.0232
0.0009 77 0.2373 0.5179 0.0232
0.0009 78 0.2363 0.5182 0.0232
0.0008 79 0.2355 0.5174 0.0232
0.0008 80 0.2346 0.5170 0.0232
0.0008 81 0.2339 0.5168 0.0232
0.0007 82 0.2331 0.5174 0.0232
0.0006 83 0.2324 0.5174 0.0232
0.0005 84 0.2318 0.5174 0.0232

Vyberieme cp s minimálnym xerror (príp. “1-SE” pravidlo). Tu ukážeme obe možnosti a zvolíme “1-SE” ako konzervatívnejší prístup.

Code
# min xerror
i_min <- which.min(cp_tab$xerror)
cp_min <- cp_tab$cp[i_min]

# 1-SE: najjednoduchší strom, ktorého xerror <= min(xerror) + xstd(min)
thr <- cp_tab$xerror[i_min] + cp_tab$xstd[i_min]
cp_1se <- cp_tab |>
  filter(xerror <= thr) |>
  slice_head(n = 1) |>
  pull(cp)

c(cp_min = cp_min, cp_1se = cp_1se)
     cp_min      cp_1se 
0.003113750 0.008183854 
Code
fit_tree_pruned <- prune(fit_tree_big, cp = cp_1se)

rpart.plot(fit_tree_pruned, main = "Prerezaný regresný strom (1-SE)", roundint = FALSE)

Code
pred_test_pruned <- predict(fit_tree_pruned, newdata = test_reg)
bind_rows(
  reg_metrics(test_reg$price_k, pred_test)        |> mutate(model = "Strom (veľký)"),
  reg_metrics(test_reg$price_k, pred_test_pruned) |> mutate(model = "Strom (prerezaný)")
) |>
  select(model, RMSE, MAE, R2) |>
  arrange(RMSE) |>
  kbl("Porovnanie stromov na test set-e")
Porovnanie stromov na test set-e
model RMSE MAE R2
Strom (prerezaný) 39.1367 31.2521 0.4760
Strom (veľký) 39.8508 32.2709 0.4843

Po aplikovaní prerezávania pomocou pravidla 1-SE dochádza k miernej zmene výkonnosti modelu na testovacej množine. Prerezaný strom dosahuje nižšie hodnoty RMSE a MAE v porovnaní s pôvodným (neorezaným) stromom, čo naznačuje mierne zlepšenie presnosti predikcie v absolútnych metrikách chyby.

Na druhej strane hodnota koeficientu determinácie R² zostáva veľmi podobná a dokonca mierne klesá, čo poukazuje na to, že prerezávanie nemalo jednoznačný pozitívny vplyv na schopnosť modelu vysvetľovať variabilitu cieľovej premennej. Celkový rozdiel medzi oboma modelmi je však malý, čo naznačuje, že pôvodný strom nebol výrazne preučený.

Tento výsledok ukazuje, že cost-complexity prerezávanie môže viesť k jednoduchšiemu modelu s porovnateľnou výkonnosťou, avšak jeho prínos z hľadiska presnosti a vysvetľovacej schopnosti závisí od konkrétnej štruktúry dát a miery komplexnosti pôvodného stromu.

6 Súborové metódy (ensemble)

Samotný strom má často vyššiu varianciu; agregácia viacerých stromov typicky zlepší presnosť.

6.1 Bagging (bootstrap aggregation) + OOB odhad

Bagging je špeciálny prípad random forest, kde pri každom split-e uvažujeme všetky premenné (mtry = p). 

Použijeme balík ranger, ktorý umožňuje rýchlu implementáciu baggingu a zároveň poskytuje odhad chyby pomocou OOB vzorkovania.

Code
p <- ncol(select(train_reg, -price_k))

fit_bag <- ranger(
  price_k ~ .,
  data = train_reg,
  num.trees = 800,
  mtry = p,
  min.node.size = 8,
  importance = "permutation",
  oob.error = TRUE,
  seed = 20251227
)

fit_bag
Ranger result

Call:
 ranger(price_k ~ ., data = train_reg, num.trees = 800, mtry = p,      min.node.size = 8, importance = "permutation", oob.error = TRUE,      seed = 20251227) 

Type:                             Regression 
Number of trees:                  800 
Sample size:                      1200 
Number of independent variables:  13 
Mtry:                             13 
Target node size:                 8 
Variable importance mode:         permutation 
Splitrule:                        variance 
OOB prediction error (MSE):       965.763 
R squared (OOB):                  0.6752518 

Model založený na metóde bagging dosahuje strednú hodnotu OOB chyby (RMSE ≈ 31.1) a koeficient determinácie R² ≈ 0.68, čo poukazuje na primeranú, hoci nie výnimočnú schopnosť generalizácie. Výsledky naznačujú, že agregácia viacerých stromov vedie k stabilnejšiemu výkonu v porovnaní s jednoduchým rozhodovacím stromom, avšak bez dramatického zlepšenia presnosti.

Hodnota OOB chyby poskytuje spoľahlý interný odhad generalizačného výkonu a naznačuje, že model nie je výrazne preučený. V porovnaní s jednotlivým stromom bagging znižuje variabilitu predikcií, no stále zaostáva za výkonnejšími súborovými metódami, ako sú Random Forest alebo boosting.

6.1.1 OOB RMSE vs test RMSE

Code
pred_bag_test <- predict(fit_bag, data = test_reg)$predictions

tibble(
  OOB_RMSE  = sqrt(fit_bag$prediction.error),
  Test_RMSE = rmse_vec(test_reg$price_k, pred_bag_test),
  Test_R2   = rsq_vec(test_reg$price_k,  pred_bag_test)
) |>
  kbl("Bagging: OOB vs test výkon")
Bagging: OOB vs test výkon
OOB_RMSE Test_RMSE Test_R2
31.0767 32.8175 0.6292

OOB chyba je veľmi podobná chybe na testovacej množine, čo naznačuje, že model má dobrú schopnosť generalizácie a nie je výrazne preučený. Bagging tak poskytuje stabilnejší a presnejší odhad než samostatný strom.

6.1.2 Významnosť premenných

Code
vip(fit_bag, num_features = 15, geom = "col", aesthetics = list(alpha = 0.9)) +
  labs(title = "Bagging – permutation variable importance (top 15)")

Z grafu vyplýva, že najvýznamnejšími faktormi ovplyvňujúcimi predikciu ceny nehnuteľnosti sú jej rozloha (area_m2) a kvalita bývania (quality). Tieto premenné majú dominantný vplyv na rozhodovanie modelu, čo zodpovedá ekonomickej intuící aj empirickým poznatkom z reálnych trhov s nehnuteľnosťami.

Menší, no stále nenulový vplyv majú lokalitné faktory, ako je vzdialenosť od centra (dist_center_km) a miera kriminality (crime_index, resp. log_crime), ktoré zachytávajú atraktivitu prostredia. Ostatné premenné, ako hluk, energetická trieda, dostupnosť služieb či počet izieb, zohrávajú skôr doplnkovú úlohu. Výsledky tak potvrdzujú, že cena nehnuteľnosti je determinovaná kombináciou viacerých faktorov, pričom najväčší význam majú charakteristiky priamo súvisiace s veľkosťou a kvalitou bývania.

6.2 Random Forest

Random forest znižuje koreláciu stromov tým, že pri každom split-e zvažuje len náhodný podvýber premenných (typicky mtry ~ p/3 pre regresiu). 

Code
fit_rf <- ranger(
  price_k ~ .,
  data = train_reg,
  num.trees = 1200,
  mtry = max(1, floor(p/3)),
  min.node.size = 6,
  importance = "permutation",
  oob.error = TRUE,
  seed = 20251227
)

fit_rf
Ranger result

Call:
 ranger(price_k ~ ., data = train_reg, num.trees = 1200, mtry = max(1,      floor(p/3)), min.node.size = 6, importance = "permutation",      oob.error = TRUE, seed = 20251227) 

Type:                             Regression 
Number of trees:                  1200 
Sample size:                      1200 
Number of independent variables:  13 
Mtry:                             4 
Target node size:                 6 
Variable importance mode:         permutation 
Splitrule:                        variance 
OOB prediction error (MSE):       960.475 
R squared (OOB):                  0.67703 

Model Random Forest dosahuje dobrý výkon, čo potvrdzuje hodnota OOB chyby (RMSE ≈ 31.0) a koeficient determinácie R² ≈ 0.68. Tieto výsledky naznačujú, že model dokáže vysvetliť približne dve tretiny variability cieľovej premennej a má stabilnú generalizačnú schopnosť.

V porovnaní s jednoduchým rozhodovacím stromom predstavuje Random Forest výrazné zlepšenie, keďže znižuje varianciu modelu prostredníctvom agregácie viacerých stromov a náhodného výberu premenných pri každom delení. Na druhej strane jeho výkon mierne zaostáva za metódami založenými na sekvenčnom učení, ako je gradient boosting alebo BART, ktoré dokážu efektívnejšie zachytiť komplexné nelineárne vzťahy v dátach.

Výsledky potvrdzujú, že Random Forest predstavuje robustný a spoľahlivý kompromis medzi presnosťou a stabilitou, najmä v situáciách, kde je prioritou dobrá generalizácia a nižšia citlivosť na nastavenie hyperparametrov.

6.2.1 OOB RMSE vs test RMSE

Code
pred_rf_test <- predict(fit_rf, data = test_reg)$predictions

tibble(
  OOB_RMSE  = sqrt(fit_rf$prediction.error),
  Test_RMSE = rmse_vec(test_reg$price_k, pred_rf_test),
  Test_R2   = rsq_vec(test_reg$price_k,  pred_rf_test)
) |>
  kbl("Random Forest: OOB vs test výkon")
Random Forest: OOB vs test výkon
OOB_RMSE Test_RMSE Test_R2
30.9915 32.5304 0.6504

Porovnanie OOB chyby a chyby na testovacej množine ukazuje len mierny rozdiel (OOB RMSE ≈ 31.0 vs. test RMSE ≈ 32.5), čo naznačuje, že model Random Forest nie je výrazne preučený a zachováva si stabilný výkon aj na nezávislých dátach. Hodnota koeficientu determinácie na testovacej množine (R² ≈ 0.65) potvrdzuje dobrú generalizačnú schopnosť modelu.

Vďaka použitiu náhodného výberu premenných pri každom delení stromov (parameter mtry) dochádza k zníženiu korelácie medzi jednotlivými stromami, čo vedie k nižšej variancii modelu a lepšej generalizácii v porovnaní s jednoduchým rozhodovacím stromom. Random Forest tak predstavuje robustný a spoľahlivý prístup k regresnej úlohe s vyváženým pomerom medzi presnosťou a stabilitou.

6.2.2 Významnosť premenných

Code
vip(fit_rf, num_features = 15, geom = "col", aesthetics = list(alpha = 0.9)) +
  labs(title = "Random Forest – permutation variable importance (top 15)")

Z grafu vyplýva, že najväčší vplyv na predikciu ceny nehnuteľnosti majú rozloha nehnuteľnosti (area_m2) a kvalita bývania (quality), ktoré výrazne dominujú ostatným premenným. Medzi ďalšie dôležité faktory patria vzdialenosť od centra (dist_center_km) a mieru kriminality v okolí (crime_index, log_crime).

Ostatné premenné, ako hluk, energetická trieda, dostupnosť dopravy či počet izieb, majú v modeli menší, avšak nenulový vplyv. Výsledky potvrdzujú, že Random Forest dokáže identifikovať kľúčové charakteristiky ovplyvňujúce cenu nehnuteľnosti a zachytiť ich relatívny význam v súlade s očakávaniami z reálneho trhu s nehnuteľnosťami.

6.3 Boosting (gradient boosting)

Boosting rastie sekvenčne (každý ďalší strom opravuje chyby predchádzajúcich) a má ladiace parametre ako počet stromov, hĺbka interakcií a shrinkage. 

Použijeme gbm.

Code
# GBM potrebuje numerické vstupy; one-hot kódovanie cez model.matrix
x_train <- model.matrix(price_k ~ ., data = train_reg)[, -1]
x_test  <- model.matrix(price_k ~ ., data = test_reg)[, -1]

y_train <- train_reg$price_k
y_test  <- test_reg$price_k

train_gbm <- as.data.frame(x_train) |> dplyr::mutate(price_k = y_train)
test_gbm  <- as.data.frame(x_test)  |> dplyr::mutate(price_k = y_test)

fit_gbm <- gbm(
  price_k ~ .,
  data = train_gbm,
  distribution = "gaussian",
  n.trees = 7000,
  interaction.depth = 4,
  shrinkage = 0.01,
  n.minobsinnode = 10,
  bag.fraction = 0.7,
  train.fraction = 1.0,
  verbose = FALSE
)

fit_gbm
gbm(formula = price_k ~ ., distribution = "gaussian", data = train_gbm, 
    n.trees = 7000, interaction.depth = 4, n.minobsinnode = 10, 
    shrinkage = 0.01, bag.fraction = 0.7, train.fraction = 1, 
    verbose = FALSE)
A gradient boosted model with gaussian loss function.
7000 iterations were performed.
There were 17 predictors of which 16 had non-zero influence.

Model Gradient Boosting využívajúci 7000 stromov a nízku hodnotu parametra shrinkage dosahuje stabilný výkon a efektívne zachytáva nelineárne vzťahy v dátach, pričom skutočnosť, že väčšina premenných má nenulový vplyv, potvrdzuje schopnosť modelu využívať široké spektrum informácií pri predikcii ceny nehnuteľnosti.

Vyberieme optimálny počet stromov cez OOB (v gbm: gbm.perf).

Code
best_trees <- gbm.perf(fit_gbm, method = "OOB", plot.it = TRUE)

Na grafe je zobrazený priebeh OOB chyby v závislosti od počtu stromov v modeli Gradient Boosting. S rastúcim počtom stromov dochádza k postupnému znižovaniu chyby, pričom po určitom bode sa zlepšenie spomaľuje. Zvislá čiara označuje optimálny počet stromov vybraný pomocou OOB kritéria, pri ktorom je dosiahnutý najlepší kompromis medzi presnosťou a zložitosťou modelu. 

6.3.1 RMSE vs MAE

Code
best_trees
[1] 728
attr(,"smoother")
Call:
loess(formula = object$oobag.improve ~ x, enp.target = min(max(4, 
    length(x)/10), 50))

Number of Observations: 7000 
Equivalent Number of Parameters: 39.99 
Residual Standard Error: 0.3992 
Code
pred_gbm_test <- predict(fit_gbm, newdata = test_gbm, n.trees = best_trees)

reg_metrics(y_test, pred_gbm_test) |>
  kbl("Boosting (gbm): test metriky")
Boosting (gbm): test metriky
RMSE MAE R2
30.3062 24.262 0.6904

Na základe OOB krivky bol ako optimálny zvolený model s približne 728 stromami, pri ktorom sa dosahuje najnižšia validačná chyba. Výsledný model dosahuje hodnotu RMSE ≈ 30.3 a R² ≈ 0.69, čo predstavuje ďalšie zlepšenie oproti baggingu aj klasickému random forestu.

6.3.2 Relatívny vplyv premenných

Code
infl <- summary(fit_gbm, n.trees = best_trees, plotit = FALSE) |>
  as_tibble()

infl |> slice_head(n = 15) |> kbl("Boosting: relatívny vplyv (top 15)")
Boosting: relatívny vplyv (top 15)
var rel.inf
area_m2 37.2265
qualityhigh 23.1311
dist_center_km 10.8294
crime_index 9.2305
noise_db 4.2342
transit_score 2.9334
qualitymid 2.9106
rooms 2.4546
energy_classE 1.8388
schools_score 1.7889
age_years 1.3542
has_garage 0.9454
energy_classD 0.8899
energy_classB 0.1235
energy_classC 0.0877

Z výsledkov vyplýva, že najvýznamnejším faktorom ovplyvňujúcim cenu nehnuteľnosti je rozloha nehnuteľnosti (area_m2), ktorá má výrazne najvyšší relatívny vplyv. Veľmi dôležitú úlohu zohráva aj kvalita bývania, najmä kategória high, čo potvrdzuje silný vzťah medzi kvalitatívnymi charakteristikami a cenou nehnuteľnosti.

Medzi ďalšie významné faktory patria vzdialenosť od centra (dist_center_km) a miera kriminality v okolí (crime_index), ktoré majú výrazný, no sekundárny vplyv. Negatívny efekt hluku (noise_db) a mierne pozitívny vplyv dostupnosti verejnej dopravy a škôl naznačujú, že model zachytáva aj jemnejšie lokalitné charakteristiky.

Ostatné premenné, ako počet izieb, vek nehnuteľnosti či energetická trieda, majú v modeli menší, doplnkový význam. Celkovo výsledky potvrdzujú, že boosting efektívne identifikuje kľúčové determinanty ceny nehnuteľností a zachytáva realistické nelineárne vzťahy medzi ich vlastnosťami a cenou.

6.3.3 Parciálne závislosti

Code
# Parciálne efekty pre vybrané premenné
vars_show <- c("dist_center_km", "area_m2", "schools_score", "noise_db")
for (v in vars_show) {
  pd <- partial(fit_gbm, pred.var = v, n.trees = best_trees, train = train_gbm)
  print(
    autoplot(pd) + labs(title = paste("Boosting – parciálny efekt:", v), x = v, y = "Odhadovaný efekt")
  )
}

Parciálne závislosti ilustrujú, ako jednotlivé premenné ovplyvňujú predikovanú cenu nehnuteľnosti pri fixovaní ostatných vstupov.

Z grafu pre vzdialenosť od centra (dist_center_km) je zrejmé, že s rastúcou vzdialenosťou cena systematicky klesá. Tento pokles je výraznejší najmä v prvých kilometroch, čo zodpovedá očakávanému efektu atraktivity centrálnej polohy.

V prípade rozlohy nehnuteľnosti (area_m2) je pozorovaný jednoznačne rastúci trend – väčšia plocha vedie k vyššej odhadovanej cene, pričom nárast je približne lineárny v strednom rozsahu hodnôt.

Graf pre skóre škôl (schools_score) ukazuje mierny, ale konzistentný pozitívny vplyv, čo naznačuje, že dostupnosť kvalitných vzdelávacích inštitúcií zvyšuje atraktivitu lokality.

Naopak, hluk (noise_db) má negatívny vplyv – s rastúcou hlučnosťou dochádza k postupnému poklesu predikovanej ceny, pričom efekt je výraznejší pri vyšších hodnotách hluku.

Celkovo tieto parciálne závislosti potvrdzujú, že model zachytáva realistické a intuitívne vzťahy medzi vlastnosťami nehnuteľností a ich cenou, čo podporuje jeho interpretovateľnosť a praktickú použiteľnosť.

6.4 BART (Bayesian Additive Regression Trees)

BART je bayesovská súborová metóda založená na MCMC: poskytuje nielen bodový odhad, ale aj neistotu (intervaly). 

Code
# BART potrebuje maticu prediktorov
xtr <- x_train
xte <- x_test

fit_bart <- gbart(
  x.train = xtr,
  y.train = y_train,
  x.test  = xte,
  ntree = 200,
  nskip = 200,     # burn-in
  ndpost = 1000,   # počet posterior draws
  printevery = 500
)
*****Calling gbart: type=1
*****Data:
data:n,p,np: 1200, 17, 300
y1,yn: 21.013612, -28.703620
x1,x[n*p]: 17.655731, 2.418141
xp1,xp[np*p]: 19.624401, 2.446491
*****Number of Trees: 200
*****Number of Cut Points: 100 ... 100
*****burn,nd,thin: 200,1000,1
*****Prior:beta,alpha,tau,nu,lambda,offset: 2,0.95,6.31962,3,132.078,189.105
*****sigma: 26.039315
*****w (weights): 1.000000 ... 1.000000
*****Dirichlet:sparse,theta,omega,a,b,rho,augment: 0,0,1,0.5,1,17,0
*****printevery: 500

MCMC
done 0 (out of 1200)
done 500 (out of 1200)
done 1000 (out of 1200)
time: 5s
trcnt,tecnt: 1000,1000
Code
# Posterior mean predictions
pred_bart_test_mean <- fit_bart$yhat.test.mean

Model BART bol úspešne natrénovaný s využitím 200 stromov a vykazuje stabilnú konvergenciu počas MCMC vzorkovania. Odhadovaná hodnota reziduálnej variability naznačuje dobrú schopnosť modelu zachytiť štruktúru dát, pričom použité regularizačné mechanizmy bránia preučeniu. Výsledok potvrdzuje, že BART predstavuje robustnú alternatívu k ostatným stromovým metódam pri modelovaní nelineárnych vzťahov.

6.4.1 RMSE vs MAE

Code
reg_metrics(y_test, pred_bart_test_mean) |>
  kbl("BART: test metriky (posterior mean)")
BART: test metriky (posterior mean)
RMSE MAE R2
28.7704 23.2692 0.7159

Model BART dosahuje najlepší výkon spomedzi všetkých uvažovaných prístupov, čo potvrdzuje najnižšia hodnota RMSE (≈ 28.8) a najvyššia hodnota koeficientu determinácie (R² ≈ 0.71). Výsledky naznačujú, že BART dokáže veľmi dobre zachytiť nelineárne vzťahy v dátach a poskytuje najpresnejšie predikcie ceny nehnuteľností zo všetkých porovnávaných modelov.

6.4.2 Predikčné intervaly z BART

Code
# 90% interval (5% - 95%)
lo <- apply(fit_bart$yhat.test, 2, quantile, probs = 0.05)
hi <- apply(fit_bart$yhat.test, 2, quantile, probs = 0.95)

df_int <- tibble(
  truth = y_test,
  mean  = pred_bart_test_mean,
  lo90  = lo,
  hi90  = hi
)

# coverage
coverage <- mean(df_int$truth >= df_int$lo90 & df_int$truth <= df_int$hi90)

tibble(
  interval = "90% BART interval",
  coverage = coverage,
  avg_width = mean(df_int$hi90 - df_int$lo90)
) |>
  kbl("BART: kvalita predikčných intervalov na test set-e")
BART: kvalita predikčných intervalov na test set-e
interval coverage avg_width
90% BART interval 0.4133 32.504
Code
df_int |>
  mutate(id = row_number()) |>
  slice_sample(n = 120) |>
  ggplot(aes(x = truth, y = mean)) +
  geom_abline() +
  geom_errorbar(aes(ymin = lo90, ymax = hi90), alpha = 0.35) +
  geom_point(alpha = 0.6) +
  labs(
    title = "BART: bodové predikcie + 90% predikčný interval (subset)",
    x = "Skutočná cena (tis. €)",
    y = "Predikcia (posterior mean, tis. €)"
  )

Graf znázorňuje bodové predikcie modelu BART spolu s 90 % predikčnými intervalmi. Väčšina skutočných hodnôt sa nachádza v rámci týchto intervalov, čo naznačuje, že model dokáže primerane zachytiť neistotu predikcie. Šírka intervalov sa zvyšuje pri vyšších hodnotách ceny, čo odráža rastúcu neistotu pri extrémnejších pozorovaniach.

Zároveň je však zrejmé, že skutočná miera pokrytia je nižšia než nominálnych 90 %, čo naznačuje, že predikčné intervaly sú v tomto nastavení mierne podhodnotené. Táto skutočnosť môže súvisieť s nastavením priorov alebo s vlastnosťami samotných dát. Napriek tomu model BART poskytuje cennú informáciu o relatívnej neistote predikcií a predstavuje robustný nástroj na modelovanie nelineárnych vzťahov.

7 Porovnanie všetkých metód na testovanom datasete

Code
pred_tree_pr <- predict(fit_tree_pruned, newdata = test_reg)
pred_bag     <- pred_bag_test
pred_rf      <- pred_rf_test
pred_boost   <- pred_gbm_test
pred_bart    <- pred_bart_test_mean

res <- bind_rows(
  reg_metrics(y_test, pred_tree_pr) |> mutate(model = "Prerezaný strom"),
  reg_metrics(y_test, pred_bag)     |> mutate(model = "Bagging (ranger)"),
  reg_metrics(y_test, pred_rf)      |> mutate(model = "Random Forest (ranger)"),
  reg_metrics(y_test, pred_boost)   |> mutate(model = "Boosting (gbm)"),
  reg_metrics(y_test, pred_bart)    |> mutate(model = "BART (gbart)")
) |>
  select(model, RMSE, MAE, R2) |>
  arrange(RMSE)

kbl(res, "Test metriky – porovnanie metód")
Test metriky – porovnanie metód
model RMSE MAE R2
BART (gbart) 28.7704 23.2692 0.7159
Boosting (gbm) 30.3062 24.2620 0.6904
Random Forest (ranger) 32.5304 26.1864 0.6504
Bagging (ranger) 32.8175 26.5530 0.6292
Prerezaný strom 39.1367 31.2521 0.4760
Code
res |>
  pivot_longer(cols = c(RMSE, MAE, R2), names_to = "metric", values_to = "value") |>
  ggplot(aes(x = reorder(model, value), y = value)) +
  geom_col(alpha = 0.9) +
  coord_flip() +
  facet_wrap(~metric, scales = "free") +
  labs(title = "Porovnanie metód na test set-e", x = NULL, y = NULL)

Porovnanie jednotlivých prístupov ukazuje, že model BART dosahuje najlepší celkový výkon, keď dosahuje najnižšiu hodnotu RMSE a zároveň najvyššie R^2. Nasleduje gradient boosting a random forest, ktoré poskytujú veľmi podobné, mierne slabšie výsledky. Najslabší výkon vykazuje prerezaný rozhodovací strom, čo potvrdzuje, že jednoduché stromy majú obmedzenú schopnosť zachytiť komplexné vzťahy v dátach.

Z výsledkov vyplýva, že súborové metódy výrazne prekonávajú jednotlivé stromy, pričom medzi nimi dosahuje najlepšie výsledky BART, ktorý kombinuje vysokú presnosť s dobrou generalizáciou. Táto analýza potvrdzuje vhodnosť použitia pokročilých ensemble prístupov pri modelovaní cien nehnuteľností.

8 Klasifikácia: drahé vs lacné

V tejto časti spravíme klasifikačnú úlohu: predikovať, či je nehnuteľnosť drahá (horný kvartil ceny) alebo nie.

Ukážeme klasifikačný strom a random forest, vyhodnotíme ich cez confusion matrix, accuracy/F1 a ROC/AUC.

8.1 Train/test split so stratifikáciou

Code
set.seed(20251227)
spl_cls <- initial_split(dat, prop = 0.8, strata = is_expensive)
train_cls <- training(spl_cls)
test_cls  <- testing(spl_cls)

# kontrola pomeru tried
bind_rows(
  train_cls |> count(is_expensive) |> mutate(dataset = "train"),
  test_cls  |> count(is_expensive) |> mutate(dataset = "test")
) |>
  group_by(dataset) |>
  mutate(prop = n / sum(n)) |>
  ungroup() |>
  kbl("Pomer tried (stratifikovaný split)")
Pomer tried (stratifikovaný split)
is_expensive n dataset prop
no 900 train 0.75
yes 300 train 0.25
no 225 test 0.75
yes 75 test 0.25

Dáta boli rozdelené na trénovaciu a testovaciu množinu pomocou stratifikovaného výberu, aby bol v oboch častiach zachovaný rovnaký pomer tried „drahá“ a „lacná“ nehnuteľnosť. V trénovacej množine tvorí trieda „drahá“ približne 25 % pozorovaní, rovnako ako v testovacej množine. Takýto postup zabezpečuje, že model je trénovaný aj vyhodnocovaný na dátach s porovnateľnou distribúciou tried, čím sa znižuje riziko skreslenia výsledkov pri klasifikačnej úlohe.

8.2 Klasifikačný strom

Code
fit_tree_cls <- rpart(
  is_expensive ~ . - price_k,
  data = train_cls,
  method = "class",
  control = rpart.control(cp = 0.001, minbucket = 15, maxdepth = 20)
)

rpart.plot(fit_tree_cls, main = "Klasifikačný strom: drahé vs lacné", roundint = FALSE)

Zobrazený klasifikačný strom ilustruje rozhodovací proces pri rozlišovaní medzi drahými a lacnými nehnuteľnosťami. Najdôležitejším rozdeľujúcim kritériom je kvalita nehnuteľnosti, ktorá tvorí koreň stromu, čo naznačuje jej dominantný vplyv na výslednú cenu. V ďalších úrovniach zohrávajú významnú úlohu predovšetkým rozloha, vzdialenosť od centra a úroveň kriminality, ktoré ďalej spresňujú rozhodnutie modelu.

Strom zároveň ukazuje, že nehnuteľnosti s vyššou kvalitou a väčšou rozlohou majú výrazne vyššiu pravdepodobnosť zaradenia medzi drahé, zatiaľ čo menšie objekty nachádzajúce sa ďalej od centra alebo v oblastiach s vyššou kriminalitou sú častejšie klasifikované ako lacné. Výsledná štruktúra stromu tak poskytuje intuitívny a interpretovateľný pohľad na rozhodovací proces modelu.

Code
# Predikcie: trieda aj pravdepodobnosť triedy "yes"
tree_class <- predict(fit_tree_cls, newdata = test_cls, type = "class")
tree_prob  <- predict(fit_tree_cls, newdata = test_cls, type = "prob")[, "yes"]

# Confusion matrix
conf_mat(
  tibble(truth = test_cls$is_expensive, estimate = tree_class),
  truth = truth, estimate = estimate
)
          Truth
Prediction  no yes
       no  203  43
       yes  22  32

Konfúzna matica ukazuje, že model správne klasifikoval 203 lacných a 32 drahých nehnuteľností. Zároveň došlo k 43 falošne pozitívnym a 22 falošne negatívnym predikciám. Model je teda presnejší pri identifikácii lacnejších nehnuteľností, zatiaľ čo pri drahých objektoch je mierne konzervatívny. Napriek tomu vykazuje rozumnú rovnováhu medzi citlivosťou a presnosťou, čo je typické pre modely optimalizované na celkovú presnosť.

Code
# Základné metriky
tibble(
  Accuracy = accuracy_vec(test_cls$is_expensive, tree_class),
  F1 = f_meas_vec(test_cls$is_expensive, tree_class, event_level = "second")
) |>
  kbl("Klasifikačný strom – metriky (test)")
Klasifikačný strom – metriky (test)
Accuracy F1
0.7833 0.4961

Klasifikačný strom dosahuje presnosť 78,3 %, čo znamená, že väčšinu prípadov klasifikuje správne. Hodnota F1 skóre ≈ 0,50 však naznačuje nevyvážený výkon medzi presnosťou a citlivosťou, najmä pri triede „drahé“. Model je teda schopný pomerne spoľahlivo rozlišovať medzi triedami, no v identifikácii drahších nehnuteľností má stále priestor na zlepšenie.

Code
roc_tree <- roc_curve(
  tibble(truth = test_cls$is_expensive, .pred_yes = tree_prob),
  truth = truth, .pred_yes,
  event_level = "second"
)

auc_tree <- roc_auc(
  tibble(truth = test_cls$is_expensive, .pred_yes = tree_prob),
  truth = truth, .pred_yes,
  event_level = "second"
)

auc_tree
Code
autoplot(roc_tree) + labs(title = "ROC – klasifikačný strom")

ROC krivka klasifikačného stromu sa nachádza výrazne nad diagonálou náhodného klasifikátora a dosahuje hodnotu AUC ≈ 0.81, čo poukazuje na dobrú schopnosť modelu rozlišovať medzi triedami „drahá“ a „lacná“ nehnuteľnosť. Aj keď výkon nedosahuje úroveň komplexnejších metód, ako je Random Forest alebo Boosting, strom dokáže zachytiť základné vzory v dátach a poskytuje interpretovateľný základ pre klasifikáciu.

8.3 Random Forest (klasifikácia)

Code
p_cls <- ncol(select(train_cls, -price_k, -is_expensive))

fit_rf_cls <- ranger(
  is_expensive ~ . - price_k,
  data = train_cls,
  probability = TRUE,
  num.trees = 1000,
  mtry = max(1, floor(sqrt(p_cls))),  # typická voľba pre klasifikáciu
  min.node.size = 10,
  importance = "permutation",
  seed = 20251227
)

fit_rf_cls
Ranger result

Call:
 ranger(is_expensive ~ . - price_k, data = train_cls, probability = TRUE,      num.trees = 1000, mtry = max(1, floor(sqrt(p_cls))), min.node.size = 10,      importance = "permutation", seed = 20251227) 

Type:                             Probability estimation 
Number of trees:                  1000 
Sample size:                      1200 
Number of independent variables:  13 
Mtry:                             3 
Target node size:                 10 
Variable importance mode:         permutation 
Splitrule:                        gini 
OOB prediction error (Brier s.):  0.1069596 

Model Random Forest bol natrénovaný s 1000 stromami a využíva náhodný výber premenných pri každom delení, čo znižuje koreláciu medzi stromami a zlepšuje generalizačnú schopnosť. Dosiahnutá hodnota Brierovej chyby ≈ 0.107 indikuje pomerne dobrú kalibráciu pravdepodobnostných predikcií a lepší výkon v porovnaní s jednoduchým klasifikačným stromom.

Code
pred_rf_cls <- predict(fit_rf_cls, data = test_cls)
rf_prob_yes <- pred_rf_cls$predictions[, "yes"]
rf_class    <- if_else(rf_prob_yes >= 0.5, "yes", "no") |> factor(levels = c("no","yes"))

conf_mat(
  tibble(truth = test_cls$is_expensive, estimate = rf_class),
  truth = truth, estimate = estimate
)
          Truth
Prediction  no yes
       no  213  36
       yes  12  39

Konfúzna matica ukazuje, že model správne klasifikoval 213 lacných a 39 drahých nehnuteľností. Zároveň došlo k 36 falošne pozitívnym a 12 falošne negatívnym prípadom. V porovnaní s jednoduchým klasifikačným stromom dosahuje Random Forest lepšiu vyváženosť medzi presnosťou a citlivosťou, najmä pri identifikácii drahých nehnuteľností.

Code
tibble(
  Accuracy = accuracy_vec(test_cls$is_expensive, rf_class),
  F1 = f_meas_vec(test_cls$is_expensive, rf_class, event_level = "second")
) |>
  kbl("Random Forest – metriky (test)")
Random Forest – metriky (test)
Accuracy F1
0.84 0.619

Model Random Forest dosahuje presnosť 84 % a F1 skóre 0.62, čo predstavuje citeľné zlepšenie oproti jednoduchému klasifikačnému stromu. Vyššia hodnota F1 skóre naznačuje lepšiu rovnováhu medzi presnosťou a citlivosťou pri identifikácii drahých nehnuteľností. Výsledky potvrdzujú, že využitie súborových metód vedie k stabilnejšiemu a robustnejšiemu klasifikačnému výkonu.

Code
roc_rf <- roc_curve(
  tibble(truth = test_cls$is_expensive, .pred_yes = rf_prob_yes),
  truth = truth, .pred_yes,
  event_level = "second"
)

auc_rf <- roc_auc(
  tibble(truth = test_cls$is_expensive,
         .pred_yes = rf_prob_yes),
  truth = truth,
  .pred_yes,
  event_level = "second"
)

auc_rf
Code
autoplot(roc_rf) + labs(title = "ROC – Random Forest (klasifikácia)")

ROC krivka sa nachádza výrazne nad diagonálou náhodného klasifikátora a dosahuje hodnotu AUC ≈ 0.92, čo poukazuje na veľmi dobrú schopnosť modelu rozlišovať medzi triedami „drahá“ a „lacná“ nehnuteľnosť. Vysoká hodnota AUC potvrdzuje, že Random Forest dokáže spoľahlivo priraďovať vyššie pravdepodobnosti správnej triede naprieč rôznymi rozhodovacími prahmi, a patrí tak medzi najvýkonnejšie použité klasifikačné metódy.

Code
vip(fit_rf_cls, num_features = 15, geom = "col", aesthetics = list(alpha = 0.9)) +
  labs(title = "Random Forest (klasifikácia) – permutation importance (top 15)")

Graf zobrazuje relatívnu dôležitosť jednotlivých premenných pri klasifikácii nehnuteľností pomocou metódy Random Forest. Najväčší vplyv na rozhodovanie modelu má kvalita nehnuteľnosti, nasledovaná rozlohou a mierou kriminality v okolí. Tieto premenné majú dominantný vplyv na to, či je nehnuteľnosť klasifikovaná ako drahá alebo lacná.

Naopak, faktory ako počet izieb, vek nehnuteľnosti alebo dostupnosť parkov majú v porovnaní s ostatnými premennými len marginálny vplyv. Výsledky tak potvrdzujú, že model sa pri rozhodovaní opiera najmä o charakteristiky priamo súvisiace s kvalitou a atraktivitou lokality.

8.4 Priame porovnanie strom vs RF (test)

Code
cls_res <- bind_rows(
  tibble(
    model = "Strom",
    AUC = roc_auc_vec(test_cls$is_expensive, tree_prob, event_level = "second"),
    Accuracy = accuracy_vec(test_cls$is_expensive, tree_class),
    F1 = f_meas_vec(test_cls$is_expensive, tree_class, event_level = "second")
  ),
  tibble(
    model = "Random Forest",
    AUC = roc_auc_vec(test_cls$is_expensive, rf_prob_yes, event_level = "second"),
    Accuracy = accuracy_vec(test_cls$is_expensive, rf_class),
    F1 = f_meas_vec(test_cls$is_expensive, rf_class, event_level = "second")
  )
) |>
  arrange(desc(AUC))

kbl(cls_res, "Klasifikácia – porovnanie modelov (test)")
Klasifikácia – porovnanie modelov (test)
model AUC Accuracy F1
Random Forest 0.9159 0.8400 0.6190
Strom 0.8137 0.7833 0.4961
Code
cls_res |>
  pivot_longer(cols = c(AUC, Accuracy, F1), names_to = "metric", values_to = "value") |>
  ggplot(aes(x = reorder(model, value), y = value)) +
  geom_col(alpha = 0.9) +
  coord_flip() +
  facet_wrap(~metric, scales = "free") +
  labs(title = "Klasifikácia: porovnanie strom vs RF", x = NULL, y = NULL)

Z porovnania metrík je zrejmé, že Random Forest výrazne prekonáva jednoduchý klasifikačný strom vo všetkých hodnotených ukazovateľoch. Dosahuje vyššiu presnosť (Accuracy ≈ 0.84 vs. 0.78), vyššie F1 skóre aj výrazne lepšiu hodnotu AUC (0.92 oproti 0.81), čo znamená lepšiu schopnosť rozlišovať medzi triedami „drahá“ a „lacná“ nehnuteľnosť.

9 Diskusia výsledkov

V práci sme analyzovali správanie stromových modelov v dvoch rôznych úlohách – regresii (predikcia ceny nehnuteľnosti) a klasifikácii (drahá vs. lacná nehnuteľnosť). Táto kombinácia umožnila porovnať vlastnosti jednotlivých metód v rôznych typoch úloh.

V regresnej úlohe sa ukázalo, že jednoduchý rozhodovací strom síce poskytuje dobrú interpretovateľnosť, avšak trpí vyššou varianciou a výrazným rizikom preučenia. Prerezávanie stromu v tomto prípade neviedlo k zlepšeniu testovacej chyby, čo naznačuje, že pôvodný strom už bol z hľadiska generalizácie relatívne stabilný; výraznejšie zlepšenie však priniesli až súborové metódy.

Bagging výrazne stabilizoval výsledky tým, že redukoval variabilitu medzi jednotlivými stromami a poskytol spoľahlivejší odhad pomocou OOB chyby.

Random Forest dosiahol ešte lepšie výsledky vďaka zníženej korelácii medzi stromami, čo sa prejavilo nižšou chybou aj lepšou generalizáciou na testovacích dátach.

V regresnej úlohe dosahoval najlepšie výsledky boosting, ktorý dokázal zachytiť komplexné nelineárne vzťahy medzi premennými. Jeho nevýhodou však ostáva citlivosť na nastavenie hyperparametrov a potenciálne preučenie pri príliš dlhom učení.

V klasifikačnej úlohe (rozlíšenie „drahá“ vs. „lacná“ nehnuteľnosť) sa potvrdilo, že:

  • klasifikačný strom poskytuje interpretovateľné rozhodnutia, ale má nižšiu presnosť,

  • Random Forest dosahuje vyššiu presnosť, lepšiu AUC a stabilnejšie výsledky,

  • ROC krivky a confusion matrixe umožňujú detailnejšie porovnanie správania modelov.

BART model v regresnej úlohe navyše poskytol pravdepodobnostnú interpretáciu výstupov a intervaly spoľahlivosti, čo je významná výhoda pri rozhodovaní v praxi.

10 Záver

V tomto projekte bola analyzovaná úloha predikcie cien nehnuteľností pomocou rozhodovacích stromov a ich rozšírení. Na simulovaných dátach sme demonštrovali použitie stromových metód v regresii aj klasifikácii.

Porovnali sme jednoduchý rozhodovací strom, bagging, random forest, boosting a BART, pričom každá metóda mala svoje výhody z hľadiska presnosti, interpretovateľnosti a stability.

Výsledky ukázali, že zatiaľ čo jednoduché stromy sú dobre interpretovateľné, súborové metódy dosahujú výrazne lepšiu generalizáciu. Random Forest a boosting poskytli najlepší kompromis medzi presnosťou a robustnosťou, zatiaľ čo BART umožnil kvantifikovať neistotu predikcií.

Projekt tak demonštruje praktické využitie stromových metód v regresných aj klasifikačných úlohách a ilustruje ich silné aj slabé stránky v kontexte analýzy reálnych dát.