Nezaradené

Machine learning v jazyku R – Odhad cien bytov 150 150 cleandata

Machine learning v jazyku R – Odhad cien bytov

Machine Learning (Strojové učenie) je podmnožina umelej inteligencie, ktorá sa zameriava na vývoj algoritmov schopných učiť sa a robiť predikcie na základe dát. V jazyku R existuje množstvo nástrojov a frameworkov, ktoré uľahčujú vytváranie a tréning ML modelov. Medzi najpopulárnejšie patrí tidymodels, ktorý poskytujú robustné a efektívne prostriedky na tvorbu rôznych modelov a predikciu. V tomto článku sa zameriam na vytvorenie modelu na predikovanie cien bytov, pričom priblížim aj tému explainable ML na lepšie porozumenie výsledkom modelu.

Vytvorenie ML Modelu v jazyku R

Úvod

V tomto blogovom príspevku prejdem procesom prípravy a tvorby ML modelu, ktorý bude na základe vložených parametrov predikovať cenu bytu. Dáta boli vopred nachystané, ich čistenie a manipuláciu si môžete pozrieť v tomto článku.

Explainable ML

V rámci strojového učenia predstavuje koncept “explainable/interpretable ML” (vysvetliteľné strojové učenie) kľúčový posun smerom k transparentnosti a interpretovateľnosti modelov. Napriek tomu, že modely ako XGBoost ponúkajú výnimočnú prediktívnu silu, ich interné rozhodovacie procesy môžu byť zložité a nejasné, a teda môžu byť tiež vnímané ako “black box”, čo znamená, že nie je ľahké pochopiť, ako sa k predikciám dospelo. Toto vnímanie komplikuje dôveru a akceptáciu modelov v kritických aplikáciách, kde je potrebné pochopenie dôvodov za predikciami.

Vysvetliteľné strojové učenie sa snaží preklenúť túto priepasť poskytujúc nástroje a metódy na objasnenie, ako modely dospeli k svojim rozhodnutiam. Knižnice ako “vip” v jazyku R a metódy ako SHAP (Shapley Additive exPlanations) hodnoty umožňujú analytikom a vývojárom lepšie pochopiť príspevky jednotlivých funkcií k výslednému predikovanému výstupu. Táto schopnosť detailne rozložiť predikčný proces umožňuje nielen hlbšiu analýzu a optimalizáciu modelov, ale tiež zvyšuje transparentnosť a dôveru zo strany koncových užívateľov.

Vysvetliteľné strojové učenie. Zdroj: Lundberg, Scott & Erion, Gabriel & Chen, Hugh & DeGrave, Alex & Prutkin, Jordan & Nair, Bala & Katz, Ronit & Himmelfarb, Jonathan & Bansal, Nisha & Lee, Su-In. (2019). Explainable AI for Trees: From Local Explanations to Global Understanding.

Dôležitosť explainable/interpretable ML naberá na váhe najmä v sektoroch, kde sú dôsledky rozhodnutí založených na predikciách modelu vysoké, ako sú zdravotníctvo, financie alebo právo. V týchto oblastiach je kľúčové, aby boli modely nielen presné, ale aj ich rozhodnutia pochopiteľné a spravodlivé. Vysvetliteľné strojové učenie tak stojí v centre úsilia o vytvorenie technológií, ktoré sú nielen inteligentné, ale aj zrozumiteľné a etické.

Výborná kniha na túto tému je napríklad Interpretable Machine Learning: A Guide For Making Black Box Models Explainable, ktorá je veľmi zrorumiteľne napísaná a obsahuje aj odkazy na knižnice v R aj Pythone. Navyše je dostupná aj online. Náročnejšia publikácia je potom Explainable AI for Trees: From Local Explanations to Global Understanding, ktorá ide viac do detailu a “matematiky” na pozadí.

Načítanie knižníc a dát

Klasicky začíname s načítaním knižníc. Aj tento krát použijeme tidyverse – súbor knižníc navrhnutých na prácu s dátami v R, ktorý zahŕňa napr. ggplot2 pre vizualizáciu, dplyr pre manipuláciu s dátami, tidyr pre úpravu tvaru dát, a iné. Opäť sa objavuje knižnica sf, keďže dáta obsahujú priestorové objekty. Novikou v tomto blogu je tidymodels. Je to framework pre modelovanie a strojové učenie, ktorý poskytuje koherentnú súpravu knižníc na predspracovanie dát, rozdelenie dát, cross-validáciu, výber modelu atď. Je navrhnutý tak, aby bol v súlade s princípmi tidyverse a umožňoval ľahkú integráciu s inými nástrojmi z tohto ekosystému. Dve nasledujúce knižnice pomáhajú s konceptom “explainable/interpretable ML”. Knižnica vip je určená na vizualizáciu dôležitosti premenných v rôznych modeloch strojového učenia. Umožňuje ľahko identifikovať, ktoré premenné majú najväčší vplyv na predikcie modelu, čo je kľúčové pre interpretáciu modelu a pochopenie dát. Knižnica shapviz je špecificky navrhnutá na výpočet a vizualizáciu SHAP hodnôt pre modely vytvorené pomocou XGBoost v R. SHAP hodnoty poskytujú podrobné vysvetlenie predikcií modelu na úrovni jednotlivých pozorovaní, čo pomáha v interpretácii “black box” modelov. Umožnujú pochopiť, ako dôležité sú premenné v rámci modelu. doParallel slúži na paralelné výpočty, a teda skrátenie času trénovania modelu. Posledná knižnica extrafont slúži na načítanie fontov inštalovaných na Windows-e.

Knižnice
# import libraries
if (!require("pacman")) {
  install.packages("pacman")
}

pacman::p_load(
  tidyverse,
  sf, # data contain geometry
  tidymodels,
  vip,
  shapviz,
  doParallel,
  extrafont,
  vetiver,
  xgboost
)

loadfonts(device = "win")
options(scipen = 999)

unregister_dopar <- function() {
  env <- foreach:::.foreachGlobals
  rm(list = ls(name = env), pos = env)
}
Dáta
# import data
original_conditions <- c(
  "Pôvodný stav", "Čiastočná rekonštrukcia", "Kompletná rekonštrukcia",
  "Novostavba", "Vo výstavbe", "Developerský projekt"
)
english_conditions <- c(
  "Original condition", "Partial reconstruction", "Complete reconstruction",
  "New building", "Under construction", "Development project"
)

original_type <- c("3 izbový byt", "1 izbový byt", "2 izbový byt", "4 izbový byt", "Garsónka", "5 a viac izbový byt", "Dvojgarsónka")
english_type <- c("3-room apartment", "1-room apartment", "2-room apartment", "4-room apartment", "Studio", "5 or more room apartment", "Two-room apartment")

apartments_analysis_data <- readRDS("data/apartments_final_data.rds") |>
  filter(!is.na(price)) |>
  mutate(
    coord = st_coordinates(st_centroid(geometry)),
    lon = coord[, 1],
    lat = coord[, 2],
    type = str_replace_na(recode(type, !!!setNames(original_type, english_type)),"Neznáme"),
    condition = str_replace_na(recode(condition, !!!setNames(original_conditions, english_conditions)),"Neznáme"),
    certificate = str_replace_na(str_replace(certificate, "none", "Nemá"), "Neznáme")
  ) |>
  select(-coord) # Optionally remove the original coordinates column

# remove geometry since we have coordinates now
apartments_analysis_data$geometry <- NULL

saveRDS(apartments_analysis_data, "data/apartments_data_App.RDS")

Tidymodels framework


Tidymodels je súbor knižníc v R, ktorý poskytuje jednotné a flexibilné rozhranie pre celý proces strojového učenia, od predspracovania dát cez ich analýzu až po modelovanie. Vytvorenie modelu v rámci frameworku tidymodels pozostáva z niekoľkých základných krokov:

  • Príprava a rozdelenie dát: Prvý krok je rozdelenie nášho “dátového budgetu”. Typicky sa dáta delia na trénovaciu (slúži na odhad parametrov modelu) a testovaciu (slúži na nezávislé zhodnotenie modelu) sadu pomocou funkcie initial_split(). Tento krok je základom pre overovanie modelu a zabránenie overfittingu. S využitím vfold_cv() alebo podobných funkcií vytvoríme schému krížovej validácie (rôzne verzie tréningových dát – tzv. “folds”), ktorá sa použije na evaluáciu modelu.
    Ako rozdelenie na tréningovú a testovaciu sadu, tak aj vytvorenie validačných schém umožnuje specifikovať premennú, ktorej rozdelenie ostane (približne) zachované (strata =).

Rozdelenie dátového budgetu. Zdroj: https://www.tidymodels.org/start/resampling/#data-split
  • Vytvorenie receptu (recipe): recipe() definuje predspracovanie dát, vrátane výberu premenných, transformácií, normalizácie, kódovania kategorických premenných a riešenia chýbajúcich hodnôt. Jednotlivé kroky sa pridávajú pomocou step_*() funkcií. Recipies zabezpečujú, že predspracovanie je konzistentné a reprodukovateľné.

  • Špecifikácia modelu: Model sa špecifikuje nezávisle od dát. Pomocou funkcií ako linear_reg(), rand_forest(), boost_tree() a iných definujeme typ modelu, mód (regresia, klasifikácia), engine(xgboost, lightgbm…) a jeho hlavné parametre bez toho, aby sme ich ihneď fitovali na dáta. Tento krok umožňuje flexibilitu v experimentovaní s rôznymi modelmi.

  • Nastavenie workflow: workflow() integruje recept a model do jednotného objektu. Workflow umožňuje efektívnejšie spracovanie, keďže spojíme predspracovanie dát a modelovanie do jednej operácie, čo zjednodušuje evaluáciu a porovnávanie modelov.

  • Výber a nastavenie hyperparametrov: Pomocou parameters() môžeme definovať a prispôsobiť rozsahy hyperparametrov pre tuning modelu. Tidymodels ponúka rôzne metódy pre vyhľadávanie optimálnych hodnôt, napr. tune_grid(), tune_bayes(), tune_race() a iné.

  • Cross-validácia a tuning modelu: Tuning hyperparametrov prebieha na trénovacej sade (resp. na jednotlivých “fold-och”) s cieľom nájsť najlepšiu kombináciu hyperparametrov. Najlepší model vyberieme pomocou funkcie select_best(), pričom špecifikujeme metriku, podľa ktorej model vyberáme.

  • Finalizácia a fitovanie modelu: Po vybraní najlepších hyperparametrov finalizujeme model pomocou finalize_model() a potom ho fitujete na trénovacie dáta s fit(). Tento krok produkuje finálny model pripravený na evaluáciu a predikcie.

  • Evaluácia modelu: Pomocou testovacej sady dát overíme výkonnosť modelu. Metriky ako RMSE, presnosť (accuracy), AUC a mnoho iných (výber by mal zodpovedať nášmu cieľu, čo platí najmä pri klasifikácii) poskytujú hodnotenie, ako dobre model predpovedá nevidené dáta.

Tieto kroky poskytujú ucelený prístup k vytváraniu, optimalizácii a evaluácii prediktívnych modelov. Tidymodels zabezpečuje konzistenciu a reprodukovateľnosť po celom procese.

Toto je opis základného procesu tvorby ML modelu pomocou tidymodels frameworku. Je možné samozrejme vytvoriť aj omnoho komplikovanejší proces s postupným tuningom hyperparametrov, tvorbou stacked modelu (meta-learner modelu) atď.

Tvorba modelu

Rozdelenie dát
# split dataframes to train(80)/test(20)
set.seed(123)
apartments_train_split <- initial_split(apartments_analysis_data, prop = 0.7, strata = price)

apartments_train <- training(apartments_train_split)
apartments_test <- testing(apartments_train_split)

folds <- vfold_cv(apartments_train, v = 5, strata = price)
saveRDS(apartments_test, "data/test_set.RDS")
Spracovanie dát
apartments_xgboost_recipe <- recipe(apartments_train, price ~ .) |>
  step_rm(name_nsi) |>
  step_string2factor(all_nominal_predictors()) |>
  step_impute_knn(all_nominal_predictors()) |>
  step_unknown(all_nominal_predictors()) |>
  step_dummy(all_nominal_predictors())
Špeficikácia modelu
xgb_model <-
  boost_tree(
    trees = tune(), loss_reduction = tune(),
    tree_depth = tune(), min_n = tune(),
    mtry = tune(), sample_size = tune(),
    learn_rate = tune()
  ) |>
  set_mode("regression") |>
  set_engine("xgboost")
Workflow
apartments_workflow <- workflow() |>
  add_recipe(apartments_xgboost_recipe) |>
  add_model(xgb_model)
Výber parametrov
apartments_xgboost_params <- parameters(
  trees(), learn_rate(), loss_reduction(),
  tree_depth(), min_n(),
  sample_size = sample_prop(),
  finalize(mtry(), apartments_train)
)

apartments_xgboost_params <- apartments_xgboost_params |> update(trees = trees(c(300, 600)))


Pri tuningu použijeme paralelné spracovanie na x – 2 jadrách. Niektoré algoritmy umožnujú aj spracovanie pomocou GPU ak je dostupná kompatibilná grafická karta (bavíme sa o tvorbe modelu na lokálnom zariadení, samozrejme v produkcii je vhodnejšie využiť služby ako GCP, AWS a iné)

Tuning parametrov
registerDoParallel(cores = detectCores() - 1)

xgboost_tune <-
  apartments_workflow |>
  tune_bayes(
    resamples = folds,
    param_info = apartments_xgboost_params,
    iter = 100,
    metrics = metric_set(rmse, mape),
    control = control_bayes(
      no_improve = 20,
      save_pred = T, verbose = F
    )
  )

unregister_dopar()
Finalizácia modelu
apartments_best_model <- select_best(xgboost_tune, metric = "rmse")
apartments_final_model <- finalize_model(xgb_model, apartments_best_model)
apartments_workflow <- apartments_workflow |> update_model(apartments_final_model)
apartments_xgb_fit <- fit(apartments_workflow, data = apartments_train)

saveRDS(apartments_xgb_fit, "data/xgb_fit.RDS")
Výsledné metriky a testovacia predikcia
apartments_final_res <- last_fit(apartments_workflow, split = apartments_train_split)
apartments_pred <-
  predict(apartments_xgb_fit, apartments_test) |>
  bind_cols(apartments_test)

Vyhodnotenie modelu

Z grafu je zrejmé, že výsledný model vcelku dobre predikuje ceny bytov s hodnotou do približne 300 tisíc EUR. Pri drahších bytoch je variabilita odchýlok od reálnych cien vyššia.

Porovnanie predikcie s reálnou cenou 1
plot1 <-
  apartments_pred |>
  ggplot(aes(x = .pred, y = price)) +
  geom_point() +
  geom_smooth(method = "loess", color = "red") +
  scale_y_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE), breaks = c(200000, 400000, 600000)) +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE), breaks = c(200000, 400000, 600000)) +
  labs(
    title = NULL,
    x = "Predikovaná cena",
    y = "Reálna Cena"
  ) +
  theme_minimal() +
  theme(
    text = element_text(family = "Courier New", size = 12)
  ) +
  coord_fixed()

plot1

Korelácia reálnych a predikovaných cien

Pri pohľade na rozloženie reálnych a predikovaných cien môžeme vidieť kde presne dochádza k najväčším odchýlkam. Model predikoval viac bytov v cene približne 100 až 170 tisíc EUR a približne 200 až 225 tisíc EUR. Naopak menej zastúpené sú najmä byty s cenou približne 250 až 300 tisíc EUR. Teraz je potrebné rozhodnutie, či sa treba vrátiť k procesu feature engineering, čiže manipulácii s premennými alebo pokračovať s model v stave v akom je.

Porovnanie predikcie s reálnou cenou 2
plot2 <-
  apartments_pred |>
  select(predikcia = .pred, realita = price) |>
  gather(key, value) |>
  ggplot(aes(x = value, color = key)) +
  geom_density(alpha = .5) +
  labs(
    title = NULL,
    x = "Cena",
    y = "Hustota",
    color = ""
  ) +
  scale_y_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  theme_minimal() +
  theme(
    text = element_text(family = "Courier New", size = 12),
    legend.position = "bottom"
  )

plot2

Rozloženie reálnych vs predikovaných cien

Model v priemere mierne nadhodnocuje (do 5 percent) byty s reálnou cenou medzi 100 až 170 tisíc EUR. Najvyššie nadhodnotenie (viac ako 5 percent) je pri cenách do 100 tisíc EUR. Naopak byty s cenou od 180 tisíc EUR do 260 tisíc eur mierne podhodnocuje. V prípade bytov od 260 tisíc EUR (približne 7 percent z inzerátov) sú už odchýlky nestabilné, trend je však smerom k výraznejšiemu podhodnocovaniu.

Odchýlky
# Calculate the difference between prediction and actual value
plot3_data <- apartments_pred |>
  select(.pred, price) |> 
  mutate(diff = .pred - price,
         bin = floor(price/10000)*10000) |>
  group_by(bin) |>
  summarize(mean_diff = mean(diff)) |> 
  mutate(rel_diff = case_when(
    mean_diff/bin >= 0.05 ~ 0.05,
    mean_diff/bin >= 0 ~ 0,
    mean_diff/bin <= -0.05 ~ -0.05,
    mean_diff/bin < 0 ~ -0.02
    ))


# Plot with coloring based on mean difference
plot3 <- ggplot(plot3_data, aes(x = bin, y = mean_diff, color = factor(rel_diff))) +
  geom_point() +
  labs(
    title = NULL,
    x = "Cena",
    y = "Priemerná chyba"
  ) +
  scale_color_manual(values = c("#506BA0", "#55B9F5", "#90ee90","#037f51" )) +
  scale_y_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  theme_minimal() +
  theme(
    text = element_text(family = "Courier New", size = 12),
    legend.position = "none")

plot3

Odchýlky predikcie od reality

Pri XGBoost je dôležitosť premenných založená na Gain, tento termín odkazuje na príspevok každej premennej k celkovému zlepšeniu modelu, ktoré je dosiahnuté vďaka rozdeleniam (splits) na konkrétnej premennej. Keď XGBoost vytvára stromy, každé rozdelenie v strome sa vyberá tak, aby maximalizovalo “zisk”, čo znamená, že sa snaží o čo najväčšie zlepšenie prediktívnej presnosti. Gain v tomto kontexte meria, o koľko sa zlepšuje predikcia, keď sa použije rozdelenie založené na danej premennej. Tento zisk je často vážený a sumarizovaný cez všetky stromy v modeli. Premenné, ktoré najviac prispievajú k zlepšeniu sú tie, ktoré model najviac využíva na dosiahnutie presnejších predikcií, a preto sú považované za dôležitejšie. Viac informácií môžete nájsť napríklad v tomto článku na medium.com.

Najdôležitejšou premennou je podľa modelu geografická dĺžka. Nasledujú rozloha bytu a lokalita Bratislava I. Až 4 z top 10 premenných súvisia s indexom bývania.

vip – Variable Importance Plots
apartments_xgb_fit |>
  fit(data = apartments_train) |>
  pull_workflow_fit() |>
  vip(geom = "point", include_type = TRUE) + 
  theme_minimal() +
  theme(
    panel.grid.minor = element_blank(),
    text = element_text(family = "Courier New", size = 12)
  ) + 
  labs(
    y = "Dôležitosť",
    x = "Premenná"
  ) +
  theme(
    panel.grid.minor = element_blank(),
    text = element_text(family = "Courier New", size = 12)
  )

Dôležitosť premenných v modeli


SHAP (Shapley Additive exPlanations) hodnoty sú metóda používaná na vysvetlenie príspevku jednotlivých premenných k predikcii konkrétneho modelu strojového učenia. SHAP hodnoty poskytujú detailné vysvetlenie predikcie pre každý záznam (riadok dát) tým, že ukážu, ako každá premenná prispieva k výslednej predikcii, či už zvyšovaním alebo znížením predikovanej hodnoty. Tiež zaručujú konzistentnosť, čo znamená, že ak máme dve premenné a jedna konzistentne prispieva k predikcii viac než druhá, bude mať aj vyššiu SHAP hodnotu. Okrem poskytovania vysvetlení na úrovni jednotlivých predikcií (lokálne vysvetlenie) môžu byť SHAP hodnoty agregované na poskytnutie prehľadu o dôležitosti premenných v celom modeli (globálne vysvetlenie).

SHAP hodnoty nám poskytujú ďaľšie informácie k dôležitosti premenných. Napr. sa potvrdzuje západo-východný gradient, ktorý sme spomínali v závere tretej časti tejto série. Ak sa byt nachádza v okrese Bratislava I, môže mu to pridať na hodnote 45 až 95 tisíc EUR. Ak sa jedná o novostavbu cena môže stúpnuť o 15 až 50 tisíc EUR, kompletná rekonštrukcia pridáva na hodnote do 25 tisíc EUR, naopak pôvodný stav má negatívny efekt do približne -20 tisíc EUR.

SHAP
fitted_data <- apartments_xgboost_recipe |>
  prep() |>
  bake(new_data = apartments_analysis_data) |>
  select(-price)

set.seed(123)
shp <- shapviz(extract_fit_engine(apartments_xgb_fit), X_pred = fitted_data |> as.matrix())

shap_plot <- sv_importance(shp, kind = "beeswarm")  +
  scale_x_continuous(labels = function(x) format(x, big.mark = " ", scientific = FALSE)) +
  scale_color_gradient(low = "blue", high = "red",
                      breaks = c(0,1),
                      labels = c("nízka", "vysoká"),
                      guide = guide_colorbar(barwidth = 12, barheight = 0.3)) + # Customize based on your exact needs
  labs(x = "SHAP hodnota (vplyv na predikovanú cenu)",
       y = "Premenná",
       color = "Hodnota premennej "
       ) +
  theme_minimal() +
  theme(
    panel.grid.minor = element_blank(),
    text = element_text(family = "Courier New", size = 10),
    legend.position = "bottom"
  )

Top 15 premenných podľa SHAP hodnôt

Okrem celkového príspevku jednotlivých premenných v rámci modelu ako celku (globálne vysvetlenie) sa vieme pozrieť aj na jednotlivé predikcie. Napr. keď sa pozrieme na inzerát na riadku 1804, môžeme vidieť, že lokalita v okrese Bratislava I mu pridala na hodnote 63 tisíc EUR, geografická dĺžka ďalších 33 tisíc EUR a rozloha 84 m2 20 tisíc EUR. Naopak to, že sa nejedná o novostavbu znižuje cenu o 12 tisíc EUR a pôvodný stav uberá ďalších 9 tisíc EUR.

Príklad SHAP hodnôt jednej predikcie

Uloženie modelu a dát pre aplikáciu

Aby sa model dal použiť v Shiny aplikácii, je nutné ho uložiť vo vhodnom formáte. Natrénovaný model transformujeme na vetiver model. Tento objekt obsahuje všetko, čo treba, aby sa dal použiť v novom prostredí. Ďalej ukladáme engine pomocou knižnice xgboost. Budeme ho potrebovať pri SHAP grafe.

Exploratory data analysis (EDA) v jazyku R 150 150 cleandata

Exploratory data analysis (EDA) v jazyku R

Exploratory Data Analysis (EDA) je proces analýzy dát s cieľom porozumieť ich základným charakteristikám, často pomocou základných štatistických a vizuálnych metód. V jazyku R sa EDA realizuje rôznymi nástrojmi a technikami, ktoré umožňujú odhaľovať vzory, anomálie a vzťahy medzi premennými, čo je kľúčové pre efektívnu prípravu dát na ďalšie analýzy a modelovanie.

consolidate_sk

Úvod

V tomto blogu sa budem venovať Exploratory Data Analysis (EDA), čiže úvodnej analýze údajov, ktorej cieľom je zistiť aká je kvalita, obsah a štruktúra údajov. V tomto prípade ide o dáta z inzercií nehnuteľností. Dáta sú scrape-nuté z webu Nehnutelnosti, procesmi webscraping-u a geokódovania som prešiel v predchádzajúcich blokoch “Web scrapingpomocou jazyka R” a “Geocoding pomocou jazyka R”.

Čo je EDA


Exploratory Data Analysis je neoddeliteľnou súčasťou dátovej analytiky/dátovej vedy (Data science).

EDA v data science projekte. Zdroj: https://commons.wikimedia.org/wiki/File:Data_visualization_process_v1.png


Účelom EDA je zhrnúť hlavné charakteristiky súboru údajov (ako kvalita, obsah a štruktúra), objaviť vzorce a vzťahy medzi premennými a identifikovať trendy. Malo by nás viesť k pochopeniu údajov a identifikácii kritických premenných vzhľadom na naše ciele. Ako je znázornené na obrázku, ide o iteratívny proces. Na základe vašich zistení môžete buď pokračovať v modelovaní/testovaní hypotéz a reportovaní, alebo sa vrátiť k čisteniu/spracovaniu údajov.
EDA zvyčajne začína načítaním údajov a kontrolou niekoľkých riadkov, aby ste získali prvotný “pocit” z údajov spolu s kontrolou štruktúry údajov, veľkosti vzorky, typov údajov, chýbajúcich hodnôt atď. Potom pokračuje podrobnejšou analýzou, ktorá nám pomáha pochopiť vzťahy a identifikovať odľahlé hodnoty a dôležité premenné. V EDA používame rôzne techniky a nástroje. Vo všeobecnosti ich možno rozdeliť do niekoľkých skupín:

  • Súhrnné (jednopremenné) štatistiky – min, max, priemer, medián, kvartily, IQR, štandardná odchýlka, počty, frekvencia atď.
  • Vizualizácia dát – histogram, boxplot, Paretov graf, bodové grafy, korelačná matica, čiarové grafy (pre časové rady), heatmapy atď.
  • Bi-/viacpremenné štatistiky – korelácia, t-test, chí-kvadrát test, ANOVA, Kruskal-Wallisov test atď.

Na základe zistení vytvoríme záver a buď pokračujeme v projekte, alebo sa vrátime k dodatočnému upratovaniu dát. Je to teda iteratívny proces.
Aj keď radšej robím EDA manuálne, existuje niekoľko R knižníc pre automatizované EDA. Sú užitočné pri prvotnom skúmaní údajov a identifikácii napr. dátových typov, premenných s veľkou časťou chýbajúcich hodnôt a iných “high-level” charakteristík. Sú to napríklad:

  • DataExplorer
  • ExPanDaR
  • dataMaid
  • dlookr

Úvodné čistenie dát


Začínam klasicky, načítaním knižníc pomocou funkcie p_load z knižnice pacman.

Knižnice
# import libraries
if (!require("pacman")) {
  install.packages("pacman")
}

pacman::p_load(
  janitor, # clean_names()
  skimr, # skim()
  sf, # geospatial data
  ggpubr,
  ggQC, # pareto chart
  scales, # scales
  GGally, # eval_data_col
  knitr,
  modelsummary, # datasummary_correlation()
  gtsummary, # tables
  ggstatsplot, # ggwithinstats()
  effectsize, # interpret_kendalls_w()
  tidyverse, # data wrangling
  kableExtra, # tables
  extrafont # fonts
)

loadfonts(device = "win")


Nasleduje prvotné čistenie dát. V nasledujúcom kóde spájam 3 rôzne súbory. Keďže sú z rôznych zdrojov, je potrebné niektoré hodnoty upraviť do rovnakého tvaru (prípad názvov obcí).
Následne upravujem premenné do správnych typov, odfiltrujem preč záznamy, ktorých hodnoty sú odľahlé alebo úplne chýbajú a nemá zmysel ich imputovať.
Krok preloženia slovenských výrazov do angličtiny nie je nevzhnutný. Robím ho jednak z dôvodu, že som zvyknutý pracovať s anglickými výrazmi pri kódovaní a chcem aby aj dataset bol v tomto ohľade konzistentný. Druhým dôvodom je, že budem dataset nahrávať na Kaggle.
V poslednom kroku robím dve verzie datasetu. Jedna obsahuje premennú ‘geometry’ typu sfc_MULTIPOLYGON, ktorá robí problém alebo extrémne spomaluje výpočty niektorých sumačných funkciách, ak sú aplikované na celý dataset. Preto na všetku EDA budem používať verziu bez nej.

Feature engineering
# Load advertisements data from RDS file
advertisements <- readRDS("data/advertisements.RDS")

# Clean and restructure advertisements data
advertisements <- advertisements %>%
  separate(type_of_real_estate, c("type", "area"), sep = " • ", remove = TRUE) %>%
  select(link, type)

# Load and process districts mapping data from Excel file
districts_mapping <- openxlsx::read.xlsx("data/obce_okresy.xlsx") %>%
  mutate(
    municipality = str_replace(municipality, "Košice - ", "Košice - mestská časť "),
    municipality = str_replace(municipality, "Bratislava - ", "Bratislava - mestská časť ")
  )

# Load and process scraped data with geocoding
scraped_data <- readRDS("data/advertisements_complete_geocoded.RDS") %>%
  filter(!is.na(link)) %>%
  select(-c(address1, address2, info_text, district, municipality, address)) %>%
  rename(quality_of_living = quanlity_of_living) %>%
  mutate(
    NAME_NSI = str_replace(NAME_NSI, "Hodruša-Hámre", "Hodruša - Hámre"),
    NAME_NSI = str_replace(NAME_NSI, "Perín-Chym", "Perín - Chym"),
    NAME_NSI = str_replace(NAME_NSI, "Šaštín-Stráže", "Šaštín - Stráže"),
    NAME_NSI = str_replace(NAME_NSI, "Kostolná-Záriečie", "Kostolná - Záriečie")
  )

# Join advertisements and scraped data
joined_data <- scraped_data %>%
  left_join(advertisements, by = "link", multiple = "first", keep = FALSE) %>%
  clean_names() %>%
  filter(!is.na(price)) %>%
  mutate(
    # Convert relevant columns to numeric format
    pocet_izieb_miestnosti = as.numeric(pocet_izieb_miestnosti),
    uzit_plocha = str_replace(str_replace(uzit_plocha, ",", "."), " m2", ""),
    energie = str_replace(str_replace(energie, ",", "."), " €/mesiac", ""),
    provizia_zahrnuta_v_cene = str_replace_na(provizia_zahrnuta_v_cene, "Nie"),
    # Create a 'rooms' column based on 'type' and handle missing values
    rooms = case_when(type == "1 izbový byt" ~ 1,
      type == "2 izbový byt" ~ 2,
      type == "3 izbový byt" ~ 3,
      type == "4 izbový byt" ~ 4,
      type == "5 a viac izbový byt" ~ 5,
      type == "Garsónka" ~ 1,
      type == "Dvojgarsónka" ~ 2,
      .default = NA
    ),
    pocet_izieb_miestnosti = coalesce(pocet_izieb_miestnosti, rooms, pocet_izieb_miestnosti)
  ) %>%
  mutate_at(c(
    "index_of_living",
    "uzit_plocha",
    "energie",
    "pocet_nadzemnych_podlazi",
    "podlazie",
    "pocet_izieb_miestnosti",
    "rok_vystavby",
    "rok_poslednej_rekonstrukcie",
    "pocet_balkonov",
    "pocet_lodzii"
  ), as.numeric) %>%
  select(-link) %>%
  filter(pocet_izieb_miestnosti < 10 & !is.na(pocet_izieb_miestnosti)) %>%
  mutate(
    type = coalesce(type, case_when(
      pocet_izieb_miestnosti == 1 ~ "1 izbový byt",
      pocet_izieb_miestnosti == 2 ~ "2 izbový byt",
      pocet_izieb_miestnosti == 3 ~ "3 izbový byt",
      pocet_izieb_miestnosti == 4 ~ "4 izbový byt",
      pocet_izieb_miestnosti >= 5 ~ "5 a viac izbový byt"
    ))
  ) %>%
  select(-rooms) %>%
  filter(!(type %in% c("Apartmán", "Mezonet", "Iný byt", "Loft"))) %>%
  rename(
    index = index_of_living,
    condition = stav,
    area = uzit_plocha,
    provision = provizia_zahrnuta_v_cene,
    certificate = energeticky_certifikat,
    energy_costs = energie,
    construction_type = typ_konstrukcie,
    year_built = rok_vystavby,
    last_reconstruction = rok_poslednej_rekonstrukcie,
    total_floors = pocet_nadzemnych_podlazi,
    floor = podlazie,
    lift = vytah,
    balkonies = pocet_balkonov,
    loggia = pocet_lodzii,
    cellar = pivnica,
    orientation = orientacia
  ) %>%
  mutate(
    # Recreate 'rooms' column after filtering and handle missing values
    rooms = as.numeric(case_when(
      type == "1 izbový byt" ~ 1,
      type == "2 izbový byt" ~ 2,
      type == "3 izbový byt" ~ 3,
      type == "4 izbový byt" ~ 4,
      type == "5 a viac izbový byt" ~ 5,
      type == "Garsónka" ~ 1,
      type == "Dvojgarsónka" ~ 2,
      .default = NA
    )),
    # Transform 'provision' to binary
    provision = as.numeric(case_when(
      provision == "Áno" ~ 1,
      provision == "Nie" ~ 0,
      .default = NA
    )),
    # Transform 'lift' to binary
    lift = as.numeric(case_when(
      lift == "Áno" ~ 1,
      .default = 0
    )),
    # Transform 'cellar' to binary
    cellar = as.numeric(case_when(
      cellar == "Áno" ~ 1,
      .default = 0
    )),
    certificate = if_else(certificate == "nemá", "none", certificate)
  ) %>%
  select(-pocet_izieb_miestnosti) %>%
  mutate(
    # Convert relevant columns to numeric format
    across(c(
      "environment", "safety", "transport", "relax", "quality_of_living", "services"
    ), na_if, "0"),
    across(c(
      "environment", "safety", "transport", "relax", "quality_of_living", "services"
    ), as.numeric)
  )

# Translating Slovak terms into English
# Define mapping vectors
original_conditions <- c(
  "Pôvodný stav", "Čiastočná rekonštrukcia", "Kompletná rekonštrukcia",
  "Novostavba", "Vo výstavbe", "Developerský projekt"
)
english_conditions <- c(
  "Original condition", "Partial reconstruction", "Complete reconstruction",
  "New building", "Under construction", "Development project"
)
original_construction_type <- c("Tehlová", "Panelová", "Iná", "Kvádrová", "Zmiešaná", "Panelová, Tehlová", "Skeletová", "Tehlová, Železobetónová", "Kamenná", "Montovaná", "Drevená")
english_construction_type <- c("Brick", "Panel", "Other", "Cube", "Mixed", "Panel, Brick", "Skeletal", "Brick, Reinforced concrete", "Stone", "Mounted", "Wooden")
original_orientation <- c("Juhozápadná", "Južná", "Západná", "Východná", "Juhovýchodná", "Severovýchodná", "Severozápadná", "Severná")
english_orientation <- c("Southwest", "South", "West", "East", "Southeast", "Northeast", "Northwest", "North")
original_type <- c("3 izbový byt", "1 izbový byt", "2 izbový byt", "4 izbový byt", "Garsónka", "5 a viac izbový byt", "Dvojgarsónka")
english_type <- c("3-room apartment", "1-room apartment", "2-room apartment", "4-room apartment", "Studio", "5 or more room apartment", "Double studio")

# Translate values
joined_data <- joined_data %>%
  mutate(
    condition = recode(condition, !!!setNames(english_conditions, original_conditions)),
    construction_type = recode(construction_type, !!!setNames(english_construction_type, original_construction_type)),
    orientation = recode(orientation, !!!setNames(english_orientation, original_orientation)),
    type = recode(type, !!!setNames(english_type, original_type))
  )

# Join with districts mapping data
joined_data <- joined_data %>%
  left_join(districts_mapping, join_by(name_nsi == municipality), keep = FALSE, multiple = "first")

# Create a copy of joined data without geometry information
joined_data_wo_geom <- joined_data
joined_data_wo_geom$geometry <- NULL

write.csv2(joined_data_wo_geom, "data/apartments_appraisal.csv", row.names = F)

EDA

Záver a nasledujúce kroky


EDA poskytla cenné poznatky, ktoré budú zohľadnené v predikčnom modeli:

  • Rozloženie cien je vychýlené doprava – ponuky drahých bytov sú obmedzené
  • Geopriestorové rozloženie má západ-východný gradient – nižšie ceny sú na východe a juhu, s výnimkou niekoľkých regionálnych centier.
  • Väčšina miest v datasete má pomerne vysokú úroveň indexu bývania. Vo všeobecnosti existuje pozitívny vzťah medzi jeho hodnotou a cenou.
  • Existujú preukázané rozdiely medzi cenami bytov s rôznymi stavmi. Nie je prekvapujúce, že nové byty majú najvyššie ceny.
  • Podobný efekt je pri energetickom certifikáte. Počet chýbajúcich údajov je v tomto prípade vysoký a budem ho riešiť imputáciou.
  • Veľká väčšina bytov v súbore má 2 a 3 izby. Cena rastie s rastúcou veľkostnou triedou. Zvýšenie ceny z 2 izbovej na 3 izbovú skupinu je v však priemere dosť nízke. Dva možné dôvody sú – dopyt po 2 izbových bytoch (keďže sú stále lacnejšie ako 3 izbové) a lokalita. Ak by sa väčšina 2-izbových bytov nachádzala na západe, ich cena by bola v priemere za celú krajinu vyššia v porovnaní s rovnomerným priestorovým rozložením.
Geocoding pomocou jazyka R 150 150 cleandata

Geocoding pomocou jazyka R

Geocoding je proces prevádzania adries na zemepisné súradnice, které sa môžu použiť pre umiestnenie týchto adries na mapu. Využitie takto obohatených dát je široké. Od analytiky po praktické použitie v oblastiach ako logistika, marketing, verejná správa, zdravotníctvo a mnoho iných.

geocode_sk

Knižnice pre geocoding

V dnešnom príspevku sa budem venovať procesu geokódovania inzerátov získaných webscrapingom (procesu som sa venoval v tomto článku) a následnému spojeniu týchto geokódovaných údajov s údajmi o obciach v SR. Tento proces otvára dvere k hlbšiemu porozumeniu vašich dát a umožňuje pokročilé geopriestorové analýzy.
Tentokrát si vystačíme s tromi knižnicami. S tidyverse na manipuláciu s dátami, tidygeocoder na samotný geocoding adries a sf pre prácu s priestorovými dátami (v našom prípade bodmi a polygónmi) sme pripravení načítať a spracovať naše dáta.

Knižnice
# import libraries

pacman::p_load(
  tidyverse, 
  tidygeocoder, 
  sf
)

Načítanie dát a geocoding

Načítame geopriestorové údaje o obciach a údaje o inzerátoch. Údaje o obciach sa dajú jednoducho získať pomocou knižnice giscoR a funkcie gisco_get_communes(). Ja som mal tieto dáta už pripravené vo formáte RDS.

Načítanie dát
# Read geospatial data for communes
communes <- readRDS("data/geospatial_data/communes.RDS") |> 
  select(NAME_NSI)

# Read advertisements data
advertisements_complete <- readRDS("data/advertisements_complete.RDS") 


Každý z inzerátov v datasete obsahuje adresu. Cieľom je extrahovať unikátne adresy z týchto reklám a premeniť ich na geopriestorové údaje. Geokódovanie vykonávam pomocou funkcie geocode() metódou OpenStreetMap. Tento proces môže trvať dlho, keďže každú sekundu sa odošle jedna požiadavka.

Geokódovanie adries
# Extract unique addresses from advertisements data
advertisements <- advertisements_complete |> 
  select(address) |> 
  rename(address1 = address) |> 
  as_tibble() |> 
  mutate(address1 = str_extract(address1, 
                                "^[^,]+,[^,]+")) |> 
  unique()

# Geocode addresses using OpenStreetMap method
geocoded_advertisements <- geocode(advertisements, 
                            address = address1, 
                            method = 'osm', 
                            lat = latitude , 
                            long = longitude)


Po geokódovaní adries ich spojím späť s údajmi z inzerátov. To umožní priradiť každému inzerátu geografickú polohu. V tejto fáze tiež aplikujem filter, aby som zabezpečil, že v analýze zohľadňujem iba reklamy v špecifickom geografickom rozsahu, ktorý odpovedá oblasti Slovenska. Občas sa totiž stalo, že adresa bola lokalizovaná v zahraničí.

Prepojenie inzerátov a geografických dát
# Join geocoded addresses with advertisements data
advertisements_complete_geocoded <- advertisements_complete |> 
  left_join(geocoded_advertisements, by = c("address" = "address1"), keep = FALSE, multiple = "first") |> 
  filter(latitude <= 49.613611 & latitude >= 47.75740 & longitude <= 22.565833 & longitude >= 16.833333) |>  
  st_as_sf(coords = c("longitude", "latitude"), crs = 4326)


Posledným krokom je priestorové spojenie geokódovaných reklám s dátami o obciach. Tento proces mi umožní zistiť, v ktorých obciach sa nachádzajú inzerované byty. Využívam k tomu knižnicu sf, ktorá je štandardom pre prácu s priestorovými dátami v R. Keďže už pracujem s priestorovými údajmi, musím použit špecifický typ spojenia st_join metódou st_contains, ktorý spája dve priestorové jednotky (body, polygóny) podľa toho, či jedna obsahuje druhú. Výsledný dataset integruje originálne informácie o inzerciách s ich geopriestorovými vlastnosťami a príslušnosťou k obciam.

Uloženie finálneho súboru
# Spatial join with communes data
advertisements_complete_geocoded <- communes %>% 
  st_join(advertisements_complete_geocoded, join = st_contains)

# Save the final geocoded advertisements data
saveRDS(advertisements_complete_geocoded, "data/advertisements_complete_geocoded.RDS")

Nasledujúce kroky


Geokódovanie a práca s geopriestorovými dátami otvárajú nové možnosti pre analýzu a vizualizáciu informácií. V tomto prípade som prešiel od jednoduchých adresných údajov k ich geografickej reprezentácii a integrácii s údajmi o obciach. Tento prístup umožňuje lepšie porozumieť distribúcii inzerovaných bytov na území Slovenska, čo je jednak dôležité pre nacenenie nehnuteľnosti a v praxi často kľúčové pre efektívne plánovanie marketingových a obchodných stratégií. Výhodou, pri tvorbe modelu strojového učenia (napr. algoritmy ako random forest, xgboost a iné) zas je, že súradnice môžeme rotovať a tým optimalizovať ako budú výsledné rozhodovacie stromy vyzerať.

  • 1
  • 2