Celem niniejszego projektu jest budowa zaawansowanych modeli uczenia nadzorowanego dedykowanych problemowi regresji, jakim jest wycena wartości nieruchomości. W ramach analizy zweryfikowana zostanie skuteczność klasycznych podejść regresyjnych w zderzeniu z modelami nieliniowymi i zespołowymi. Końcowym efektem prac jest wybór optymalnego modelu, który charakteryzuje się najwyższą zdolnością generalizacji i najniższym poziomem błędu predykcji na danych testowych.
W pierwszym kroku ładowane są niezbędne pakiety narzędziowe odpowiedzialne za przetwarzanie danych, wizualizację, modelowanie oraz ekstrakcję metryk jakościowych.
library(tidyverse)
library(caret)
library(corrplot)
library(skimr)
library(DataExplorer)
library(rpart)
library(rpart.plot)
library(partykit)
library(randomForest)
library(ranger)
library(neuralnet)
library(NeuralNetTools)
# Wylaczenie notacji naukowej i ustawienie lokalizacji komunikatow
options(scipen = 999)
Sys.setenv(LANG = "pl")
# Wczytanie danych surowych
df_houses <- read.csv("r5_houses.csv", stringsAsFactors = FALSE)
Przeprowadzenie wstępnego wglądu w strukturę ramki danych pozwala zidentyfikować typy zmiennych (numeryczne ciągłe, dyskretne oraz potencjalne zmienne kategoryczne zapisane jako liczby) oraz poznać podstawowe charakterystyki rozkładów.
cat("--- Struktura zbioru danych ---\n")
## --- Struktura zbioru danych ---
glimpse(df_houses)
## Rows: 21,408
## Columns: 22
## $ price <dbl> 221900, 538000, 180000, 604000, 510000, 1225000, 257500,…
## $ bedrooms <int> 3, 3, 2, 4, 3, 4, 3, 3, 3, 3, 3, 2, 3, 3, 5, 4, 3, 4, 2,…
## $ bathrooms <dbl> 1.00, 2.25, 1.00, 3.00, 2.00, 4.50, 2.25, 1.50, 1.00, 2.…
## $ sqft_living <int> 1180, 2570, 770, 1960, 1680, 5420, 1715, 1060, 1780, 189…
## $ sqft_lot <int> 5650, 7242, 10000, 5000, 8080, 101930, 6819, 9711, 7470,…
## $ floors <dbl> 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 1.0, 1…
## $ waterfront <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ view <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0,…
## $ condition <int> 3, 3, 3, 5, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 3, 3, 3, 4, 4,…
## $ grade <int> 7, 7, 6, 7, 8, 11, 7, 7, 7, 7, 8, 7, 7, 7, 7, 9, 7, 7, 7…
## $ sqft_above <int> 1180, 2170, 770, 1050, 1680, 3890, 1715, 1060, 1050, 189…
## $ sqft_basement <int> 0, 400, 0, 910, 0, 1530, 0, 0, 730, 0, 1700, 300, 0, 0, …
## $ yr_built <int> 1955, 1951, 1933, 1965, 1987, 2001, 1995, 1963, 1960, 20…
## $ yr_renovated <int> 0, 1991, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ zipcode <int> 98178, 98125, 98028, 98136, 98074, 98053, 98003, 98198, …
## $ lat <dbl> 47.5112, 47.7210, 47.7379, 47.5208, 47.6168, 47.6561, 47…
## $ long <dbl> -122.257, -122.319, -122.233, -122.393, -122.045, -122.0…
## $ house_age <int> 60, 64, 82, 50, 28, 14, 20, 52, 55, 12, 50, 73, 88, 38, …
## $ was_renovated <int> 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ total_sqft <int> 1180, 2970, 770, 2870, 1680, 6950, 1715, 1060, 2510, 189…
## $ living_ratio <dbl> 0.20881260, 0.35482535, 0.07699230, 0.39192162, 0.207895…
## $ is_luxury <int> 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
cat("\n--- Podstawowe statystyki opisowe ---\n")
##
## --- Podstawowe statystyki opisowe ---
summary(df_houses)
## price bedrooms bathrooms sqft_living
## Min. : 75000 Min. : 0.000 Min. :0.000 Min. : 290
## 1st Qu.: 320000 1st Qu.: 3.000 1st Qu.:1.500 1st Qu.:1420
## Median : 450000 Median : 3.000 Median :2.250 Median :1900
## Mean : 518937 Mean : 3.362 Mean :2.098 Mean :2052
## 3rd Qu.: 635000 3rd Qu.: 4.000 3rd Qu.:2.500 3rd Qu.:2520
## Max. :1999950 Max. :33.000 Max. :7.500 Max. :7730
## sqft_lot floors waterfront view
## Min. : 520 Min. :1.000 Min. :0.000000 Min. :0.0000
## 1st Qu.: 5027 1st Qu.:1.000 1st Qu.:0.000000 1st Qu.:0.0000
## Median : 7578 Median :1.500 Median :0.000000 Median :0.0000
## Mean : 15036 Mean :1.491 Mean :0.005325 Mean :0.2168
## 3rd Qu.: 10556 3rd Qu.:2.000 3rd Qu.:0.000000 3rd Qu.:0.0000
## Max. :1651359 Max. :3.500 Max. :1.000000 Max. :4.0000
## condition grade sqft_above sqft_basement
## Min. :1.000 Min. : 1.000 Min. : 290 Min. : 0.0
## 1st Qu.:3.000 1st Qu.: 7.000 1st Qu.:1190 1st Qu.: 0.0
## Median :3.000 Median : 7.000 Median :1550 Median : 0.0
## Mean :3.408 Mean : 7.628 Mean :1767 Mean : 284.6
## 3rd Qu.:4.000 3rd Qu.: 8.000 3rd Qu.:2190 3rd Qu.: 550.0
## Max. :5.000 Max. :13.000 Max. :7420 Max. :3260.0
## yr_built yr_renovated zipcode lat
## Min. :1900 Min. : 0.00 Min. :98001 Min. :47.16
## 1st Qu.:1951 1st Qu.: 0.00 1st Qu.:98033 1st Qu.:47.47
## Median :1975 Median : 0.00 Median :98065 Median :47.57
## Mean :1971 Mean : 82.23 Mean :98078 Mean :47.56
## 3rd Qu.:1997 3rd Qu.: 0.00 3rd Qu.:98118 3rd Qu.:47.68
## Max. :2015 Max. :2015.00 Max. :98199 Max. :47.78
## long house_age was_renovated total_sqft
## Min. :-122.5 Min. : 0.00 Min. :0.0000 Min. : 290
## 1st Qu.:-122.3 1st Qu.: 18.00 1st Qu.:0.0000 1st Qu.:1490
## Median :-122.2 Median : 40.00 Median :0.0000 Median :2160
## Mean :-122.2 Mean : 44.01 Mean :0.0412 Mean :2336
## 3rd Qu.:-122.1 3rd Qu.: 64.00 3rd Qu.:0.0000 3rd Qu.:2960
## Max. :-121.3 Max. :115.00 Max. :1.0000 Max. :9950
## living_ratio is_luxury
## Min. :0.00061 Min. :0.00000
## 1st Qu.:0.15622 1st Qu.:0.00000
## Median :0.24701 Median :0.00000
## Mean :0.32329 Mean :0.06806
## 3rd Qu.:0.40658 3rd Qu.:0.00000
## Max. :4.64491 Max. :1.00000
# Sprawdzenie ostatecznych wymiarow zbioru danych
cat("\nWymiary zbioru (wiersze, kolumny):\n")
##
## Wymiary zbioru (wiersze, kolumny):
dim(df_houses)
## [1] 21408 22
Przed przystąpieniem do modelowania konieczne jest zweryfikowanie kompletności bazy danych pod kątem występowania brakujących wartości oraz eliminacja zmiennych bezwartościowych, czyli takich, których wariancja jest bliska zeru.
cat("--- Liczba braków danych w kolumnach ---\n")
## --- Liczba braków danych w kolumnach ---
missing_values <- colSums(is.na(df_houses))
print(missing_values)
## price bedrooms bathrooms sqft_living sqft_lot
## 0 0 0 0 0
## floors waterfront view condition grade
## 0 0 0 0 0
## sqft_above sqft_basement yr_built yr_renovated zipcode
## 0 0 0 0 0
## lat long house_age was_renovated total_sqft
## 0 0 0 0 0
## living_ratio is_luxury
## 0 0
cat("\n--- Analiza zmiennych near-zero variance ---\n")
##
## --- Analiza zmiennych near-zero variance ---
nzv_analysis <- nearZeroVar(df_houses, saveMetrics = TRUE)
print(nzv_analysis)
## freqRatio percentUnique zeroVar nzv
## price 1.000000 18.226831091 FALSE FALSE
## bedrooms 1.441826 0.060724963 FALSE FALSE
## bathrooms 1.392264 0.126121076 FALSE FALSE
## sqft_living 1.022222 4.591741405 FALSE FALSE
## sqft_lot 1.238754 45.184043348 FALSE FALSE
## floors 1.312778 0.028026906 FALSE FALSE
## waterfront 186.789474 0.009342302 FALSE TRUE
## view 20.770053 0.023355755 FALSE TRUE
## condition 2.468750 0.023355755 FALSE FALSE
## grade 1.481115 0.056053812 FALSE FALSE
## sqft_above 1.009524 4.264760837 FALSE FALSE
## sqft_basement 59.684932 1.363976084 FALSE TRUE
## yr_built 1.239910 0.541853513 FALSE FALSE
## yr_renovated 225.560440 0.326980568 FALSE TRUE
## zipcode 1.020339 0.326980568 FALSE FALSE
## lat 1.000000 23.514573991 FALSE FALSE
## long 1.027027 3.512705531 FALSE FALSE
## house_age 1.239910 0.541853513 FALSE FALSE
## was_renovated 23.272109 0.009342302 FALSE TRUE
## total_sqft 1.008065 5.222346786 FALSE FALSE
## living_ratio 1.200000 94.282511211 FALSE FALSE
## is_luxury 13.693205 0.009342302 FALSE FALSE
W celu zbadania współliniowości cech ciągłych wyznaczono macierz korelacji liniowej Pearsona. Identyfikacja silnie skorelowanych predyktorów pozwala na uniknięcie problemów numerycznych i błędów interpretacyjnych w modelach strukturalnych.
# Do korelacji wybieramy tylko zmienne numeryczne
numeric_cols <- df_houses %>%
select(where(is.numeric))
# Obliczenie macierzy korelacji
data_corr <- cor(numeric_cols, use = "complete.obs")
# Wyświetlenie macierzy korelacji
corrplot(data_corr,
method = "color",
type = "lower",
order = "hclust",
tl.col = "black",
tl.srt = 45,
addCoef.col = "black",
number.cex = 0.6,
diag = TRUE,
title = "Korelacje zmiennych numerycznych",
mar = c(0,0,1,0))
Zmienne jakościowe i dychotomiczne zostały przekształcone na typ faktorowy. Ze względu na fakt, że najwyższa klasa jakości wykonania budynku (grade = 13) reprezentuje skrajnie małą liczbę obserwacji i mogłaby nie wylosować się do zbioru treningowego (generując błędy braku dopasowania poziomów w modelach regresji liniowej oraz sieciach neuronowych - co było problemem w przy pierwszej próbie), została ona na etapie czyszczenia połączona z klasą 12. Następnie zweryfikowano rozkłady cech za pomocą histogramów.
df_cleaned <- df_houses %>%
mutate(
# Polaczenie rzadkiej klasy grade 12 i 13 w celu stabilizacji estymacji
grade = ifelse(grade == 13, 12, grade),
# Konwersja zmiennych kategorycznych na czynniki
waterfront = as.factor(waterfront),
view = as.factor(view),
condition = as.factor(condition),
grade = as.factor(grade),
was_renovated = as.factor(was_renovated),
is_luxury = as.factor(is_luxury),
zipcode = as.factor(zipcode)
)
cat("--- Końcowa struktura danych po modyfikacji typów ---\n")
## --- Końcowa struktura danych po modyfikacji typów ---
str(df_cleaned)
## 'data.frame': 21408 obs. of 22 variables:
## $ price : num 221900 538000 180000 604000 510000 ...
## $ bedrooms : int 3 3 2 4 3 4 3 3 3 3 ...
## $ bathrooms : num 1 2.25 1 3 2 4.5 2.25 1.5 1 2.5 ...
## $ sqft_living : int 1180 2570 770 1960 1680 5420 1715 1060 1780 1890 ...
## $ sqft_lot : int 5650 7242 10000 5000 8080 101930 6819 9711 7470 6560 ...
## $ floors : num 1 2 1 1 1 1 2 1 1 2 ...
## $ waterfront : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
## $ view : Factor w/ 5 levels "0","1","2","3",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ condition : Factor w/ 5 levels "1","2","3","4",..: 3 3 3 5 3 3 3 3 3 3 ...
## $ grade : Factor w/ 11 levels "1","3","4","5",..: 6 6 5 6 7 10 6 6 6 6 ...
## $ sqft_above : int 1180 2170 770 1050 1680 3890 1715 1060 1050 1890 ...
## $ sqft_basement: int 0 400 0 910 0 1530 0 0 730 0 ...
## $ yr_built : int 1955 1951 1933 1965 1987 2001 1995 1963 1960 2003 ...
## $ yr_renovated : int 0 1991 0 0 0 0 0 0 0 0 ...
## $ zipcode : Factor w/ 70 levels "98001","98002",..: 67 56 17 59 38 30 3 69 61 24 ...
## $ lat : num 47.5 47.7 47.7 47.5 47.6 ...
## $ long : num -122 -122 -122 -122 -122 ...
## $ house_age : int 60 64 82 50 28 14 20 52 55 12 ...
## $ was_renovated: Factor w/ 2 levels "0","1": 1 2 1 1 1 1 1 1 1 1 ...
## $ total_sqft : int 1180 2970 770 2870 1680 6950 1715 1060 2510 1890 ...
## $ living_ratio : num 0.209 0.355 0.077 0.392 0.208 ...
## $ is_luxury : Factor w/ 2 levels "0","1": 1 1 1 1 1 2 1 1 1 1 ...
# Generowanie histogramów dla zbioru surowego
plot_histogram(
data = df_houses %>% select(where(is.numeric)),
title = "Histogramy zmiennych numerycznych (Zbiór surowy)",
ggtheme = theme_minimal(),
theme_config = list(
plot.title = element_text(hjust = 0.5, face = "bold", size = 14)
)
)
Zbiór danych został podzielony na część treningową (70%) i testową (30%) z zachowaniem stratyfikacji względem zmiennej celu (price). Identyczność rozkładów w obu podzbiorach zweryfikowano za pomocą statystyk opisowych oraz wykresu gęstości.
set.seed(123)
train_indices <- createDataPartition(df_cleaned$price, p = 0.7, list = FALSE)
housing.train <- df_cleaned[train_indices, ]
housing.test <- df_cleaned[-train_indices, ]
# Wyciągnięcie wektorów cen w celu kontroli rozkładów
real.train <- housing.train$price
real.test <- housing.test$price
# Definicja funkcji do wyciągania statystyk porownawczych
pobierz_statystyki <- function(wektor_cen, nazwa_zbioru) {
data.frame(
Zbior = nazwa_zbioru,
Min = min(wektor_cen),
Q1 = quantile(wektor_cen, 0.25),
Mediana = median(wektor_cen),
Srednia = mean(wektor_cen),
Q3 = quantile(wektor_cen, 0.75),
Max = max(wektor_cen),
Odchylenie_Std = sd(wektor_cen)
)
}
statystyki_podzialu <- rbind(
pobierz_statystyki(real.train, "Treningowy (70%)"),
pobierz_statystyki(real.test, "Testowy (30%)")
)
cat("--- Porównanie statystyk rozkładu ceny ---\n")
## --- Porównanie statystyk rozkładu ceny ---
print(statystyki_podzialu)
## Zbior Min Q1 Mediana Srednia Q3 Max
## 25% Treningowy (70%) 75000 320000 450000 518500.0 635000 1998000
## 25%1 Testowy (30%) 78000 320000 450000 519955.3 635000 1999950
## Odchylenie_Std
## 25% 285259.2
## 25%1 289769.1
Wizualna kontrola gęstości rozkładu cen potwierdza tożsamość obu wydzielonych podzbiorów.
df_wykres_train <- data.frame(Cena = real.train, Zbior = "Treningowy (70%)")
df_wykres_test <- data.frame(Cena = real.test, Zbior = "Testowy (30%)")
df_porownanie <- rbind(df_wykres_train, df_wykres_test)
ggplot(df_porownanie, aes(x = Cena, fill = Zbior)) +
geom_density(alpha = 0.4) +
scale_x_continuous(labels = scales::label_dollar(prefix = "", suffix = " USD"),
limits = c(0, 1500000)) +
scale_fill_manual(values = c("Treningowy (70%)" = "#26A69A", "Testowy (30%)" = "#9C27B0")) +
labs(
title = "Porównanie rozkładu ceny w zbiorze treningowym i testowym",
subtitle = "Weryfikacja zachowania stratyfikacji danych",
x = "Cena nieruchomości",
y = "Gęstość rozkładu",
fill = "Zbiór danych"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
plot.subtitle = element_text(size = 11, hjust = 0.5, color = "gray30"),
legend.position = "bottom"
)
W celu eliminacji silnej asymetrii prawostronnej cech metrażowych i cenowych zastosowano transformację logarytmiczną log1p. Dodatkowo, na potrzeby stabilizacji procesu uczenia sieci neuronowych, predyktory numeryczne poddano standaryzacji (Z-score). W celu uniknięcia błędu wycieku danych (data leakage), parametry pozycjonujące i skalujące (średnia i odchylenie standardowe) zostały wyznaczone wyłącznie na próbie treningowej, a następnie zaaplikowane do zbioru testowego.
vars_to_log <- c("price", "sqft_living", "sqft_lot", "sqft_above",
"sqft_basement", "total_sqft", "yr_renovated")
# Zastosowanie transformacji logarytmicznej
housing.train <- housing.train %>% mutate(across(all_of(vars_to_log), log1p))
housing.test <- housing.test %>% mutate(across(all_of(vars_to_log), log1p))
# Aktualizacja wektorów cen o wartości w skali log
real.train <- housing.train$price
real.test <- housing.test$price
# Definicja predyktorów numerycznych do standaryzacji
numeric_predictors <- c("bedrooms", "bathrooms", "sqft_living", "sqft_lot",
"floors", "sqft_above", "sqft_basement", "yr_built",
"yr_renovated", "lat", "long", "house_age",
"total_sqft", "living_ratio")
# Obliczenie profili skalowania wyłącznie na podstawie zbioru treningowego
scaler_profile <- preProcess(housing.train[, numeric_predictors],
method = c("center", "scale"))
# Utworzenie oddzielnych obiektów dedykowanych sieciom neuronowym
housing.train_scaled <- housing.train
housing.test_scaled <- housing.test
# Aplikacja profilu skalującego do zbioru treningowego i testowego
housing.train_scaled[, numeric_predictors] <- predict(scaler_profile, housing.train[, numeric_predictors])
housing.test_scaled[, numeric_predictors] <- predict(scaler_profile, housing.test[, numeric_predictors])
housing.vars.all <- setdiff(names(housing.train), "price")
Użycie struktury kart pozwala na czytelne, niezależne zweryfikowanie każdego z zaimplementowanych algorytmów uczenia nadzorowanego.
# Definicja funkcji metryk przed pierwszym uzyciem w sekcjach modelowania
regresja.metryki <- function(real_log, predicted_log) {
real <- expm1(real_log)
predicted <- pmax(expm1(predicted_log), 0)
text_mse <- mean((real - predicted)^2)
RMSE <- sqrt(text_mse)
MAE <- mean(abs(real - predicted))
MedAE <- median(abs(real - predicted))
MAPE <- mean(abs(real - predicted) / real)
R2 <- cor(predicted, real)^2
wynik <- data.frame(MSE = text_mse, RMSE, MAE, MedAE, MAPE, R2)
return(wynik)
}
Klasyczna regresja liniowa estymowana metodą najmniejszych kwadratów stanowi punkt odniesienia dla pozostałych modeli. Model usunął dwie kolumny (house_age oraz is_luxury1) z powodu idealnej współliniowości, co wynika z faktu, że zmienne te zostały sztucznie odtworzone z innych predyktorów (np. roku budowy). Model OLS charakteryzuje się wysoką stabilnością. Współczynnik determinacji wynosi ok. 72% na zbiorze treningowym i 71.5% na zbiorze testowym. Średni błąd procentowy (MAPE) ustabilizował się na poziomie 19.6%. Model liniowy wykazuje jednak ograniczenia strukturalne i nie potrafi wychwycić głębszych interakcji nieliniowych zachodzących na rynku.
# Budujemy klasyczny model regresji liniowej
model.ols <- lm(price ~ . - zipcode, data = housing.train)
# Generowanie predykcji
pred.ols.train <- predict(model.ols, newdata = housing.train)
pred.ols.test <- predict(model.ols, newdata = housing.test)
# Obliczenie metryk
metryki.ols.train <- regresja.metryki(real_log = real.train, predicted_log = pred.ols.train)
metryki.ols.test <- regresja.metryki(real_log = real.test, predicted_log = pred.ols.test)
cat("--- Wyniki regresji OLS na zbiorze testowym ---\n")
## --- Wyniki regresji OLS na zbiorze testowym ---
print(metryki.ols.test)
## MSE RMSE MAE MedAE MAPE R2
## 1 24231482046 155664.6 101959.2 68398.52 0.1964613 0.7156871
W tej sekcji porównano domyślne drzewo regresyjne z wariantem strojonym metodą walidacji krzyżowej (5-fold CV) pod kątem parametru złożoności. Domyślne drzewo okazało się najsłabszym modelem w zestawieniu (R2=0.628, MAPE = 22.2%), co wynika ze zbyt restrykcyjnych kryteriów stopu algorytmu rpart. Strojenie hiperparametru doprowadziło do wyboru optymalnego CP=0.001. Pozwoliło to na głębszy podział przestrzeni cech i poskutkowało drastyczną poprawą jakości, strojone drzewo osiągnęło R2=0.744 na teście, pokonując tym samym benchmark OLS.
# Drzewo domyślne
tree.default <- rpart(price ~ . - zipcode, data = housing.train, method = "anova")
pred.default.train <- predict(tree.default, newdata = housing.train)
pred.default.test <- predict(tree.default, newdata = housing.test)
metryki.default.train <- regresja.metryki(real_log = real.train, predicted_log = pred.default.train)
metryki.default.test <- regresja.metryki(real_log = real.test, predicted_log = pred.default.test)
# Wizualizacja rpart.plot
layout(matrix(1:1, ncol = 1))
rpart.plot(tree.default, type = 2, extra = 0, fallen.leaves = TRUE, nn = TRUE,
box.palette = "Blues", shadow.col = 0, cex = 0.75,
main = "Domyślne drzewo regresyjne")
# Wizualizacja za pomoca partykit
tree.party <- as.party(tree.default)
plot(tree.party, main = "Drzewo regresyjne z rozkładami cen w liściach",
gp = gpar(fontsize = 7), ip_panel = id_node_inner,
tp_panel = node_boxplot(tree.party, col = "#26A69A", fill = "#26A69A", alpha = 0.5))
# Strojenie drzewa
set.seed(123)
ctrl.tree <- trainControl(method = "cv", number = 5)
grid.tree <- expand.grid(cp = seq(from = 0.001, to = 0.05, by = 0.002))
tree.tuned <- train(price ~ . - zipcode, data = housing.train, method = "rpart",
trControl = ctrl.tree, tuneGrid = grid.tree, metric = "RMSE")
pred.tree.tuned.train <- predict(tree.tuned, newdata = housing.train)
pred.tree.tuned.test <- predict(tree.tuned, newdata = housing.test)
metryki.tree.tuned.train <- regresja.metryki(real_log = real.train, predicted_log = pred.tree.tuned.train)
metryki.tree.tuned.test <- regresja.metryki(real_log = real.test, predicted_log = pred.tree.tuned.test)
cat("--- Wyniki strojonego drzewa na zbiorze testowym ---\n")
## --- Wyniki strojonego drzewa na zbiorze testowym ---
print(metryki.tree.tuned.test)
## MSE RMSE MAE MedAE MAPE R2
## 1 21853827364 147830.4 92444.39 56535.6 0.177291 0.744226
Metody zespołowe typu Bagging i Random Forest redukują wariancję pojedynczych drzew poprzez uśrednianie predykcji. Domyślny las losowy (100 drzew) osiągnął bardzo wysokie dopasowanie do danych testowych (R2=0.856, MAPE = 13.18%). Model strojony za pomocą szybkiego silnika ranger wskazał jako optymalne parametry: mtry = 9 oraz min.node.size = 5. Zgodnie z oczekiwaniami teoretycznymi, algorytm silnie dopasowuje się do próby treningowej (R2=0.969), wykazując cechy przeuczenia. Mimo to, jego zdolność generalizacji na zbiorze testowym pozostaje istotna (R2=0.852). Wykres ważności cech wskazuje, że kluczowy wpływ na cenę mają: wielkość powierzchni mieszkalnej (sqft_living), standard wykończenia (grade) oraz lokalizacja geograficzna (lat).
# Las domyślny
set.seed(123)
rf.default <- randomForest(price ~ . - zipcode, data = housing.train, ntree = 100, importance = TRUE)
pred.rf.default.train <- predict(rf.default, newdata = housing.train)
pred.rf.default.test <- predict(rf.default, newdata = housing.test)
metryki.rf.default.train <- regresja.metryki(real_log = real.train, predicted_log = pred.rf.default.train)
metryki.rf.default.test <- regresja.metryki(real_log = real.test, predicted_log = pred.rf.default.test)
# Las strojony
set.seed(123)
ctrl.rf <- trainControl(method = "cv", number = 5)
grid.rf <- expand.grid(mtry = c(3, 6, 9), min.node.size = c(5, 15), splitrule = "variance")
rf.tuned <- train(price ~ . - zipcode, data = housing.train, method = "ranger",
trControl = ctrl.rf, tuneGrid = grid.rf, num.trees = 100, importance = "permutation")
pred.rf.tuned.train <- predict(rf.tuned, newdata = housing.train)
pred.rf.tuned.test <- predict(rf.tuned, newdata = housing.test)
metryki.rf.tuned.train <- regresja.metryki(real_log = real.train, predicted_log = pred.rf.tuned.train)
metryki.rf.tuned.test <- regresja.metryki(real_log = real.test, predicted_log = pred.rf.tuned.test)
# Wykres ważności zmiennych
importance.rf <- varImp(rf.tuned)$importance
importance.df <- data.frame(Zmienna = rownames(importance.rf), Waznosc = importance.rf$Overall) %>% arrange(desc(Waznosc))
ggplot(importance.df, aes(x = reorder(Zmienna, Waznosc), y = Waznosc)) +
geom_bar(stat = "identity", fill = "#26A69A", alpha = 0.8) +
coord_flip() +
labs(title = "Ważność zmiennych w optymalnym modelu Random Forest", x = "Zmienne predykcyjne", y = "Wartość ważności") +
theme_minimal()
# Wykres diagnostyczny Real vs Predicted
df_pred_vs_real <- data.frame(Rzeczywiste = expm1(real.test), Przewidywane = expm1(pred.rf.tuned.test))
ggplot(df_pred_vs_real, aes(x = Rzeczywiste, y = Przewidywane)) +
geom_point(alpha = 0.4, color = "#26A69A") +
geom_abline(intercept = 0, slope = 1, color = "#FF7043", linetype = "dashed", linewidth = 1) +
scale_x_continuous(labels = scales::label_dollar(prefix = "", suffix = " USD"), limits = c(0, 1500000)) +
scale_y_continuous(labels = scales::label_dollar(prefix = "", suffix = " USD"), limits = c(0, 1500000)) +
labs(title = "Wartości rzeczywiste vs przewidywane (Random Forest)", x = "Realna cena", y = "Cena przewidziana") +
theme_minimal()
Sztuczne sieci neuronowe zaimplementowane za pomocą pakietu neuralnet wymagały pełnego przekształcenia wejściowych faktorów na postać zero-jedynkową (One-Hot Encoding). Przetestowano dwa warianty architektury: sieć jednowarstwową z 6 neuronami ukrytymi c(6) oraz sieć głębszą, dwuwarstwową c(4, 2). Wariant jednowarstwowy NN_c6 okazał się najlepszym modelem pod względem błędu globalnego. Potrzebował około 30 minut na zbieżność algorytmu na pełnym zbiorze, ale wygenerował najniższy błąd średniokwadratowy na próbie ukrytej (RMSE = 112 577 USD). Co niezwykle istotne z punktu widzenia metodologicznego, model ten wykazuje minimalny, niemal zerowy rozjazd między metrykami treningowymi (R2=0.865) a testowymi (R 2=0.850). Świadczy to o bardzo dobrej generalizacji wiedzy i optymalnym doborze struktur sieci, bez zjawiska szkodliwego przeuczenia. Wariant głębszy NN_c4_2 okazał się zbyt wąski w kolejnych warstwach i odnotował słabsze wyniki (R 2=0.833).
# Przygotowanie danych (One-Hot Encoding)
X_train_matrix <- model.matrix(price ~ . - zipcode, data = housing.train_scaled)
X_test_matrix <- model.matrix(price ~ . - zipcode, data = housing.test_scaled)
X_train_nn <- as.data.frame(X_train_matrix)
X_test_nn <- as.data.frame(X_test_matrix)
names(X_train_nn) <- make.names(names(X_train_nn))
names(X_test_nn) <- make.names(names(X_test_nn))
X_train_nn$price <- housing.train_scaled$price
X_test_nn$price <- housing.test_scaled$price
nn_predictors <- setdiff(names(X_train_nn), c("X.Intercept.", "price"))
# Ładowanie modeli z sesji w celu szybkiego renderowania raportu
nn_housing <- readRDS("wytrenowane_modele/housing.nn.c6.rds")
nn_housing_2 <- readRDS("wytrenowane_modele/housing.nn.c4.2.rds")
# Wyciągnięcie wyników na zbiorze testowym
pred_nn_test_log <- compute(nn_housing, X_test_nn[, nn_predictors])$net.result
metryki.nn.test <- regresja.metryki(real_log = real.test, predicted_log = as.numeric(pred_nn_test_log))
pred_nn2_test_log <- compute(nn_housing_2, X_test_nn[, nn_predictors])$net.result
metryki.nn2.test <- regresja.metryki(real_log = real.test, predicted_log = as.numeric(pred_nn2_test_log))
# Wizualizacja sieci c(6)
plotnet(nn_housing, circle_col = list("lightyellow", "lightblue"), bord_col = "grey40",
pos_col = "darkgreen", neg_col = "red3", circle_cex = 2, cex_val = 0.5, max_sp = TRUE,
main = "Topologia sieci neuronowej c(6)")
Poniższe tabele prezentują kompletne zestawienie wszystkich zbudowanych modeli, odtworzone bezpośrednio z uzyskanych wyników empirycznych.
# Reczne odtworzenie tabeli zbiorczej
tabela_train <- data.frame(
Model = c("OLS", "Tree_Default", "Tree_Tuned", "RF_Default", "RF_Tuned", "NN_c6", "NN_c4_2"),
MSE = c(23017421073, 29644177967, 18984669054, 2752151890, 3008391760, 11102941971, 13226030478),
RMSE = c(151714.93, 172174.85, 137784.87, 52460.96, 54848.81, 105370.50, 115004.48),
MAE = c(100299.77, 111602.74, 87894.60, 30094.76, 31453.75, 67891.60, 72612.29),
MedAE = c(67363.04, 71790.04, 55641.31, 17221.77, 17829.84, 42864.26, 45199.86),
MAPE = c(0.1957, 0.2161, 0.1707, 0.0573, 0.0597, 0.1335, 0.1410),
R2 = c(0.7200, 0.6420, 0.7695, 0.9717, 0.9690, 0.8647, 0.8387)
)
knitr::kable(tabela_train, digits = 4, format = "markdown")
| Model | MSE | RMSE | MAE | MedAE | MAPE | R2 |
|---|---|---|---|---|---|---|
| OLS | 23017421073 | 151714.93 | 100299.77 | 67363.04 | 0.1957 | 0.7200 |
| Tree_Default | 29644177967 | 172174.85 | 111602.74 | 71790.04 | 0.2161 | 0.6420 |
| Tree_Tuned | 18984669054 | 137784.87 | 87894.60 | 55641.31 | 0.1707 | 0.7695 |
| RF_Default | 2752151890 | 52460.96 | 30094.76 | 17221.77 | 0.0573 | 0.9717 |
| RF_Tuned | 3008391760 | 54848.81 | 31453.75 | 17829.84 | 0.0597 | 0.9690 |
| NN_c6 | 11102941971 | 105370.50 | 67891.60 | 42864.26 | 0.1335 | 0.8647 |
| NN_c4_2 | 13226030478 | 115004.48 | 72612.29 | 45199.86 | 0.1410 | 0.8387 |
tabela_test <- data.frame(
Model = c("NN_c6", "RF_Default", "RF_Tuned", "NN_c4_2", "Tree_Tuned", "OLS", "Tree_Default"),
MSE = c(12673581852, 12905411967, 13307955582, 14110750344, 21853827364, 24231482046, 31920335784),
RMSE = c(112577.0, 113602.0, 115360.1, 118788.7, 147830.4, 155664.6, 178662.6),
MAE = c(70609.89, 68385.35, 69424.56, 74371.99, 92444.39, 101959.17, 115402.87),
MedAE = c(44658.62, 40261.91, 41046.83, 46019.75, 56535.60, 68398.52, 73211.88),
MAPE = c(0.1370, 0.1318, 0.1336, 0.1434, 0.1773, 0.1965, 0.2219),
R2 = c(0.8503, 0.8561, 0.8519, 0.8336, 0.7442, 0.7157, 0.6280)
)
knitr::kable(tabela_test, digits = 4, format = "markdown")
| Model | MSE | RMSE | MAE | MedAE | MAPE | R2 |
|---|---|---|---|---|---|---|
| NN_c6 | 12673581852 | 112577.0 | 70609.89 | 44658.62 | 0.1370 | 0.8503 |
| RF_Default | 12905411967 | 113602.0 | 68385.35 | 40261.91 | 0.1318 | 0.8561 |
| RF_Tuned | 13307955582 | 115360.1 | 69424.56 | 41046.83 | 0.1336 | 0.8519 |
| NN_c4_2 | 14110750344 | 118788.7 | 74371.99 | 46019.75 | 0.1434 | 0.8336 |
| Tree_Tuned | 21853827364 | 147830.4 | 92444.39 | 56535.60 | 0.1773 | 0.7442 |
| OLS | 24231482046 | 155664.6 | 101959.17 | 68398.52 | 0.1965 | 0.7157 |
| Tree_Default | 31920335784 | 178662.6 | 115402.87 | 73211.88 | 0.2219 | 0.6280 |
Model sieci neuronowej o architekturze NN_c6 okazał się najlepszym narzędziem estymacyjnym na zbiorze ukrytym, osiągając najniższy błąd globalny (RMSE = 112 577 USD) oraz wysoki poziom wyjaśnionej wariancji (R2=85.03%). Drugie miejsce zajął domyślny las losowy (RF_Default) z minimalnie lepszym współczynnikiem determinacji (R2=85.61%).
Wszystkie modele zaawansowane (z wyjątkiem sztywnego, domyślnego drzewa rpart) bezproblemowo pobiły klasyczny benchmark ekonometryczny OLS (R2=0.7157). Pokazuje to, że struktura zależności cenowych na rynku nieruchomości opiera się na silnych, nieliniowych interakcjach cech przestrzennych i standardu budynków.
Modele lasów losowych osiągnęły wysokie dopasowanie na zbiorze treningowym (R2 powyżej 96.8%), jednak na zbiorze testowym zanotowały nieznaczny spadek dopasowania, ustępując miejsca stabilnej i zrównoważonej sieci neuronowej, która wykazuje minimalne zróżnicowanie między treningiem a testem.
Osiągnięcie średniego procentowego błędu prognozy na poziomie 13.1% - 13.7% przez modele zespołowe i sieciowe jest wynikiem w pełni satysfakcjonującym w standardach rynkowych. Opracowany system może z powodzeniem wspierać procesy masowej, automatycznej wyceny nieruchomości (AVM) dla bankowości oraz agencji pośrednictwa.
# Koncowe zabezpieczenie danych na dysku
save(housing.train, housing.test, housing.train_scaled, housing.test_scaled,
housing.vars.all, real.train, real.test, file = "housing_prepared.RData")