Between 2016 and 2017, Melbourne’s housing market experienced a period of rapid growth, due to strong population growth, low interest rates and high demand. Residential housing price rose by 13.8% from June 2016 to June 2017. (ABS, 2017)
However, by late 2017 and into 2018, the market began to cool down. This was partly due to regulatory interventions, where the Australian Prudential Regulation Authority (APRA) tightened lending standards and introduced limits on investor borrowing. (APRA, 2017)
This dataset, which includes property sales data from 2016 to 2018, captures this transitional period in Melbourne’s real estate market. It offers the opportunity to analyze how different property features, such as the number of rooms, land size, and proximity to the central business district (CBD) — influenced sale prices and property classifications during a time of significant economic and regulatory change.
This project aims to explore and analyze the Melbourne Housing Market dataset (2016–2018) to gain insights and build predictive models. The two main objectives are:
1. Regression Task
To identify the best model in predicting house prices according to features such as number of rooms, land size, building area and distance from CBD. By using the best model, buyers and sellers can estimate fair market value, and real estate agents can improve their pricing strategies.
2. Classification Task
To identify the best model in classifying properties into their property types, such as house, unit or townhouse. If implemented, this can help in better filtering and recommendations on property platforms, and improve accuracy in automated listings and valuation tools.
This project uses the publicly available Melbourne Housing Market dataset, sourced from Kaggle.(https://www.kaggle.com/datasets/anthonypino/melbourne-housing-market/data) The dataset contains 34857 property records with 21 features. The target variables would be “Price” (for regression) and “Type” (for classification)
# loading packages
library(dplyr)
library(ggplot2)
library(tidyr)
library(corrplot)
library(caret)
df <- read.csv("Melbourne_housing_FULL.csv", header = T)
dim(df)
## [1] 34857 21
head(df,5)
## Suburb Address Rooms Type Price Method SellerG Date
## 1 Abbotsford 68 Studley St 2 h NA SS Jellis 3/09/2016
## 2 Abbotsford 85 Turner St 2 h 1480000 S Biggin 3/12/2016
## 3 Abbotsford 25 Bloomburg St 2 h 1035000 S Biggin 4/02/2016
## 4 Abbotsford 18/659 Victoria St 3 u NA VB Rounds 4/02/2016
## 5 Abbotsford 5 Charles St 3 h 1465000 SP Biggin 4/03/2017
## Distance Postcode Bedroom2 Bathroom Car Landsize BuildingArea YearBuilt
## 1 2.5 3067 2 1 1 126 NA NA
## 2 2.5 3067 2 1 1 202 NA NA
## 3 2.5 3067 2 1 0 156 79 1900
## 4 2.5 3067 3 2 1 0 NA NA
## 5 2.5 3067 3 2 0 134 150 1900
## CouncilArea Lattitude Longtitude Regionname Propertycount
## 1 Yarra City Council -37.8014 144.9958 Northern Metropolitan 4019
## 2 Yarra City Council -37.7996 144.9984 Northern Metropolitan 4019
## 3 Yarra City Council -37.8079 144.9934 Northern Metropolitan 4019
## 4 Yarra City Council -37.8114 145.0116 Northern Metropolitan 4019
## 5 Yarra City Council -37.8093 144.9944 Northern Metropolitan 4019
An initial check on the data quality is conducted. Some insights from initial checking:
BuildingArea, Landsize, YearBuilt and other variables has significant amount of data missing
There is 1 duplicated row
3 YearBuilt values are unrealistic
# Summary table for variables
summarize_df <- function(df) {
summary_table <- tibble(
ColumnName = names(df),
TotalObservations = nrow(df),
DataType = sapply(df, function(x) class(x)[1]),
MissingValues = sapply(df, function(x) sum(is.na(x))),
PercentMissing = sapply(df, function(x) sum(is.na(x)) / length(x) * 100)
)
return(summary_table)
}
print(summarize_df(df), n= Inf)
## # A tibble: 21 × 5
## ColumnName TotalObservations DataType MissingValues PercentMissing
## <chr> <int> <chr> <int> <dbl>
## 1 Suburb 34857 character 0 0
## 2 Address 34857 character 0 0
## 3 Rooms 34857 integer 0 0
## 4 Type 34857 character 0 0
## 5 Price 34857 integer 7610 21.8
## 6 Method 34857 character 0 0
## 7 SellerG 34857 character 0 0
## 8 Date 34857 character 0 0
## 9 Distance 34857 character 0 0
## 10 Postcode 34857 character 0 0
## 11 Bedroom2 34857 integer 8217 23.6
## 12 Bathroom 34857 integer 8226 23.6
## 13 Car 34857 integer 8728 25.0
## 14 Landsize 34857 integer 11810 33.9
## 15 BuildingArea 34857 numeric 21115 60.6
## 16 YearBuilt 34857 integer 19306 55.4
## 17 CouncilArea 34857 character 0 0
## 18 Lattitude 34857 numeric 7976 22.9
## 19 Longtitude 34857 numeric 7976 22.9
## 20 Regionname 34857 character 0 0
## 21 Propertycount 34857 character 0 0
# checking for duplicate row
sum(duplicated(df)) #1 duplicated row
## [1] 1
# Finding duplicated row to verify
df[duplicated(df)| duplicated(df, fromLast = T), ]
## Suburb Address Rooms Type Price Method SellerG Date
## 15858 Nunawading 1/7 Lilian St 3 t NA SP Jellis 17/06/2017
## 15859 Nunawading 1/7 Lilian St 3 t NA SP Jellis 17/06/2017
## Distance Postcode Bedroom2 Bathroom Car Landsize BuildingArea YearBuilt
## 15858 15.4 3131 3 3 2 405 226 2000
## 15859 15.4 3131 3 3 2 405 226 2000
## CouncilArea Lattitude Longtitude Regionname
## 15858 Manningham City Council -37.82678 145.1678 Eastern Metropolitan
## 15859 Manningham City Council -37.82678 145.1678 Eastern Metropolitan
## Propertycount
## 15858 4973
## 15859 4973
# checking for unrealistic values (negatives or zeroes)
df |> filter(Landsize < 0 | Rooms <=0 | Price <= 0)
## [1] Suburb Address Rooms Type Price
## [6] Method SellerG Date Distance Postcode
## [11] Bedroom2 Bathroom Car Landsize BuildingArea
## [16] YearBuilt CouncilArea Lattitude Longtitude Regionname
## [21] Propertycount
## <0 rows> (or 0-length row.names)
# Checking for unrealistic year built
df |> filter(YearBuilt<1800 | YearBuilt > 2018)
## Suburb Address Rooms Type Price Method SellerG Date
## 1 Bulleen 3 Maringa St 4 h NA SP Ray 7/11/2016
## 2 Mount Waverley 5 Armstrong St 3 h 1200000 VB McGrath 24/06/2017
## 3 Bentleigh 1 Wyuna Ct 3 h 1100000 VB Woodards 17/03/2018
## Distance Postcode Bedroom2 Bathroom Car Landsize BuildingArea YearBuilt
## 1 11.8 3105 4 2 2 729 255 2106
## 2 14.2 3149 3 1 4 807 117 1196
## 3 11.4 3204 3 1 4 635 242 2019
## CouncilArea Lattitude Longtitude Regionname
## 1 Manningham City Council -37.76370 145.0881 Eastern Metropolitan
## 2 Monash City Council -37.86788 145.1212 Eastern Metropolitan
## 3 Glen Eira City Council -37.92963 145.0367 Southern Metropolitan
## Propertycount
## 1 4480
## 2 13366
## 3 6795
df$Suburb <- as.factor(df$Suburb) #changing from character to factor
df$Type <- as.factor(df$Type) #changing from character to factor
df$Method <- as.factor(df$Method) #changing from character to factor
df$Date <- as.Date(df$Date) #changing from character to date
df$Distance <- as.numeric(df$Distance) #changing from character to numeric
## Warning: NAs introduced by coercion
df$Postcode <- as.factor(df$Postcode) #changing from character to factor
df$Landsize <- as.numeric(df$Landsize) #changing from integer to numeric
df$CouncilArea <- as.factor(df$CouncilArea) #changing from character to factor
df$Regionname <- as.factor(df$Regionname) #changing from character to factor
sapply(df,class)
## Suburb Address Rooms Type Price
## "factor" "character" "integer" "factor" "integer"
## Method SellerG Date Distance Postcode
## "factor" "character" "Date" "numeric" "factor"
## Bedroom2 Bathroom Car Landsize BuildingArea
## "integer" "integer" "integer" "numeric" "numeric"
## YearBuilt CouncilArea Lattitude Longtitude Regionname
## "integer" "factor" "numeric" "numeric" "factor"
## Propertycount
## "character"
df1 <- subset(df,(!is.na(df[,5])))
colSums(is.na(df1))
## Suburb Address Rooms Type Price
## 0 0 0 0 0
## Method SellerG Date Distance Postcode
## 0 0 0 1 0
## Bedroom2 Bathroom Car Landsize BuildingArea
## 6441 6447 6824 9265 16591
## YearBuilt CouncilArea Lattitude Longtitude Regionname
## 15163 0 6254 6254 0
## Propertycount
## 0
dim(df1)
## [1] 27247 21
#Showing the outliers for Building Area
boxplot(df1$BuildingArea,
main = "Building Area Boxplot",
ylab = "Building Area (square meters)",
col = "skyblue",
border = "darkblue",
notch = TRUE,
outline = TRUE
)
#We can see that there huge outliers. Hence why we choose to impute the missing values with median instead of mean
#Calculating median
median_buildingarea <-median(df$BuildingArea, na.rm = TRUE)
#Impute missing values with median
df1$BuildingArea[is.na(df1$BuildingArea)] <- median_buildingarea
#Verify that no more missing values for Price remain
sum(is.na(df1$BuildingArea))
## [1] 0
#Bedroom2 and Rooms seem to share similar values.
#A quick check via a simple scatterplot shows an almost perfectly linear relationship
df_temp1 <- df1[!is.na(df1$Bedroom2),]
ggplot(data = df_temp1, aes(x = Bedroom2, y = Rooms)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "lm", se = TRUE, color = "lightgreen", fill = "red") +
labs(title = "Bedroom2 & Rooms Relationship",
x = "Number of Bedrooms (Bedroom2)",
y = "Number of Rooms") +
theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'
#Hence, we will impute the values of Rooms into the missing values of Bedrooms2
my.na <- is.na(df1$Bedroom2)
df1$Bedroom2[my.na] <- df1$Rooms[my.na]
colSums(is.na(df1))
## Suburb Address Rooms Type Price
## 0 0 0 0 0
## Method SellerG Date Distance Postcode
## 0 0 0 1 0
## Bedroom2 Bathroom Car Landsize BuildingArea
## 0 6447 6824 9265 0
## YearBuilt CouncilArea Lattitude Longtitude Regionname
## 15163 0 6254 6254 0
## Propertycount
## 0
#Create subset of data without missing bathroom values
bathroom_noNA <- subset(df1,
!is.na(Rooms) &
!is.na(Bathroom))
#Calculating the average ratio
bathroom_room_ratio <- bathroom_noNA$Bathroom / bathroom_noNA$Rooms
average_bathroom_room_ratio <- mean(bathroom_room_ratio, na.rm = TRUE)
print(paste("Average Bathroom to Room Ratio: ", round(average_bathroom_room_ratio, 3)))
## [1] "Average Bathroom to Room Ratio: 0.536"
#Identifying indices of rows with missing values
bathroom_NA <- which(is.na(df1$Bathroom))
#Imputing the missing values for Bathroom using the value of Rooms multiplied by the average ratio then rounded to the nearest whole number
df1$Bathroom[bathroom_NA] <- round(df1$Rooms[bathroom_NA]*average_bathroom_room_ratio)
#Checking change in missing values
colSums(is.na(df1))
## Suburb Address Rooms Type Price
## 0 0 0 0 0
## Method SellerG Date Distance Postcode
## 0 0 0 1 0
## Bedroom2 Bathroom Car Landsize BuildingArea
## 0 0 6824 9265 0
## YearBuilt CouncilArea Lattitude Longtitude Regionname
## 15163 0 6254 6254 0
## Propertycount
## 0
#Imputing for Car
df1 <- df1 %>%
group_by(Suburb) %>%
mutate(Car = ifelse(is.na(Car),
median(Car, na.rm = TRUE), # Calculate median for the current Suburb group
Car)) %>%
ungroup()
#Imputing for Landsize
df1 <- df1 %>%
group_by(Suburb) %>%
mutate(Landsize = ifelse(is.na(Landsize),
median(Landsize, na.rm = TRUE), # Calculate median for the current Suburb group
Landsize)) %>%
ungroup()
#Imputing for YearBuilt
df1 <- df1 %>%
group_by(Suburb) %>%
mutate(YearBuilt = ifelse(is.na(YearBuilt),
median(YearBuilt, na.rm = TRUE), # Calculate median for the current Suburb group
YearBuilt)) %>%
ungroup()
#Imputing for Longtitude
df1 <- df1 %>%
group_by(Suburb) %>%
mutate(Longtitude = ifelse(is.na(Longtitude),
median(Longtitude, na.rm = TRUE), # Calculate median for the current Suburb group
Longtitude)) %>%
ungroup()
#Imputing for Lattitude
df1 <- df1 %>%
group_by(Suburb) %>%
mutate(Lattitude = ifelse(is.na(Lattitude),
median(Lattitude , na.rm = TRUE), # Calculate median for the current Suburb group
Lattitude)) %>%
ungroup()
#Checking change in missing values
colSums(is.na(df1))
## Suburb Address Rooms Type Price
## 0 0 0 0 0
## Method SellerG Date Distance Postcode
## 0 0 0 1 0
## Bedroom2 Bathroom Car Landsize BuildingArea
## 0 0 70 78 0
## YearBuilt CouncilArea Lattitude Longtitude Regionname
## 103 0 70 70 0
## Propertycount
## 0
df2 <- df1 %>%
drop_na()
#Checking change in missing values
colSums(is.na(df2))
## Suburb Address Rooms Type Price
## 0 0 0 0 0
## Method SellerG Date Distance Postcode
## 0 0 0 0 0
## Bedroom2 Bathroom Car Landsize BuildingArea
## 0 0 0 0 0
## YearBuilt CouncilArea Lattitude Longtitude Regionname
## 0 0 0 0 0
## Propertycount
## 0
#Checking if the duplicated row still exists
sum(duplicated(df2))
## [1] 0
#Since no more duplicated row, then only the rows with unrealistic YearBuilt will be removed
df3 <- df2 %>%
filter(YearBuilt >= 1800 & YearBuilt <= 2018)
dim(df2)
## [1] 27140 21
dim(df3)
## [1] 27138 21
The Exploratory Data Analysis (EDA) phase is crucial for understanding the underlying patterns, relationships, and anomalies within the dataset. This section aims to visualize distributions, identify correlations, and uncover insights that will guide subsequent model building.
An initial examination of the Price variable’s distribution is performed to understand its typical range and characteristics.
# Load necessary libraries for data manipulation and visualization
library(dplyr)
library(ggplot2)
library(tidyr)
library(corrplot)
# Load the cleaned dataset
df <- df3
# Plot the distribution of Property Prices
eda_p_price_dist <- ggplot(df, aes(x = Price)) +
geom_histogram(binwidth = 50000, fill = "#1F77B4", color = "white", alpha = 0.8) +
labs(title = "Distribution of Property Prices",
x = "Price (AUD)",
y = "Frequency") +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1),
panel.grid.major = element_line(color = "grey", linetype = "dotted"),
panel.grid.minor = element_line(color = "lightgrey", linetype = "dotted"))
eda_p_price_dist
Analysis: The histogram shows a right-skewed distribution for property prices, with most properties in lower to mid-price ranges. This implies a significant concentration of affordable housing, while a small segment of high-value properties forms a longer tail. This skewness suggests that data transformation (e.g., log transformation) may be beneficial for improving linear model performance.
Question: Which factors significantly influence property prices in the Melbourne housing market?
# Group data by the number of rooms and calculate the median price for each group
rooms_price <- df %>%
group_by(Rooms) %>%
summarise(MedianPrice = median(Price, na.rm = TRUE),
Count = n()) %>%
filter(Count > 10) # Filtering out categories with insufficient observations
# Generate a bar plot illustrating the median property price by the number of rooms
eda_p_rooms_price <- ggplot(rooms_price, aes(x = as.factor(Rooms), y = MedianPrice)) +
geom_bar(stat = "identity", fill = "#2CA02C", alpha = 0.8) +
labs(title = "Median Property Price by Number of Rooms",
x = "Number of Rooms",
y = "Median Price (AUD)") +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1))
eda_p_rooms_price
Analysis: A consistent positive correlation between the number of rooms and median property price is observed, indicating that increased functional space drives higher value. The prevalence of 3-4 room properties and their rising median prices suggest strong market demand for family-sized dwellings, where additional living areas significantly contribute to property value.
A correlation matrix is utilized to provide a comprehensive understanding of the linear relationships among all numerical features and the target variable, Price.
# Select numerical columns for correlation analysis and remove rows with NA values
numerical_cols <- df %>%
select(Price, Rooms, Bedroom2, Bathroom, Car, Landsize, BuildingArea, YearBuilt, Distance) %>%
drop_na()
# Compute the Pearson correlation matrix for the selected numerical variables
cor_matrix <- cor(numerical_cols)
# Visualize the correlation matrix using a circle method
corrplot(cor_matrix, method = "circle", type = "upper",
tl.col = "black", tl.srt = 45,
addCoef.col = "black", number.cex = 0.7,
main = "Correlation Matrix of Numerical Variables")
Analysis: The correlation matrix quantifies linear relationships. Strong positive correlations exist between Price and attributes like Rooms, Bedroom2, Bathroom, BuildingArea, and Landsize, affirming that physical dimensions and amenities are primary value drivers. Conversely, a negative correlation with Distance to CBD highlights the premium on prime urban locations. Year Built shows a weaker, indirect correlation, indicating its complex interplay with other property characteristics and market trends.
eda_p_price_vs_distance <- ggplot(df, aes(x = Distance, y = Price)) +
geom_point(alpha = 0.2, color = "#2CA02C") + # Use alpha for transparency to see data density
geom_smooth(method = "lm", color = "red", se = FALSE) + # Add a linear model trend line
scale_y_continuous(labels = scales::comma) + # Format y-axis labels to be more readable
labs(title = "Property Price vs. Distance from CBD",
x = "Distance from CBD (km)",
y = "Price (in AUD)") +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1))
eda_p_price_vs_distance
Analysis:The graph reveals a clear financial premium for properties close to the CBD, highlighting the trade-off buyers make between price and convenience. This confirms Distance will be a powerful predictor of property values in our regression model.
Question: What factors can help predict the Type of a property (House, Townhouse, or Unit)?
Analyzing the geographical distribution of different property types offers valuable insights for classification purposes.
# Identify the top 10 suburbs based on property listing count
top_suburbs <- df %>%
count(Suburb, sort = TRUE) %>%
head(10) %>%
pull(Suburb)
# Filter the dataset to include only the top suburbs and count property types within each
suburb_type_counts <- df %>%
filter(Suburb %in% top_suburbs) %>%
group_by(Suburb, Type) %>%
summarise(Count = n(), .groups = 'drop') %>%
mutate(Percentage = Count / sum(Count) * 100)
# Generate a stacked bar chart depicting the distribution of property types across the top 10 suburbs
eda_p_suburb_type <- ggplot(suburb_type_counts, aes(x = Suburb, y = Percentage, fill = Type)) +
geom_bar(stat = "identity", position = "fill", color = "white") +
labs(title = "Distribution of Property Types in Top 10 Suburbs",
x = "Suburb",
y = "Percentage",
fill = "Property Type") +
scale_fill_manual(values = c("h" = "#1F77B4", "t" = "#FF7F0E", "u" = "#2CA02C")) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_text(angle = 45, hjust = 1),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1))
eda_p_suburb_type
Analysis: This chart illustrates significant variability in property type distribution across active Melbourne suburbs. Distinct patterns, from house-dominated to mixed or unit-centric areas, reflect the profound influence of local urban planning, historical development, and demographic housing demands. This geographical segmentation strongly indicates that Suburb is a critical contextual feature for property type classification, reflecting fundamental differences in land use and housing supply. #### 3.2 Price Distribution by Property Type
# Create a boxplot to visualize the distribution of property prices across different property types
eda_p_price_by_type <- ggplot(df, aes(x = Type, y = Price, fill = Type)) +
geom_boxplot(outlier.shape = NA, alpha = 0.8) + # Outliers are excluded for clearer visualization
coord_cartesian(ylim = quantile(df$Price, c(0.05, 0.95), na.rm = TRUE)) + # Y-axis limits are set to focus on the central data range
labs(title = "Price Distribution by Property Type",
x = "Property Type",
y = "Price (AUD)") +
scale_fill_manual(values = c("h" = "#FF7F0E", "t" = "#D62728", "u" = "#9467BD")) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1),
legend.position = "none")
eda_p_price_by_type
Analysis: The boxplot clearly shows that detached houses (“h”) consistently command significantly higher median prices and broader price ranges than townhouses (“t”) and units (“u”). This pronounced price separation results from factors like land value, desirability, and inherent differences in living space and privacy. This strong discriminative power of price distribution is a robust indicator for property type classification, reflecting fundamental market valuations.
# Generate a boxplot to illustrate the distribution of the number of rooms across property types
eda_p_rooms_by_type <- ggplot(df, aes(x = Type, y = Rooms, fill = Type)) +
geom_boxplot(outlier.shape = NA, alpha = 0.8) +
labs(title = "Number of Rooms Distribution by Property Type",
x = "Property Type",
y = "Number of Rooms") +
scale_fill_manual(values = c("h" = "#1F77B4", "t" = "#FF7F0E", "u" = "#2CA02C")) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1),
legend.position = "none")
eda_p_rooms_by_type
Analysis: Room count clearly differentiates property types. Houses (“h”) generally have more rooms, reflecting their larger footprint and family-oriented design. Townhouses (“t”) fall in an intermediate range, offering more space than units. Units (“u”), designed for compact urban living, consistently have the fewest rooms. This consistent pattern provides a strong structural indicator for distinguishing property types.
# Create a boxplot depicting the distribution of land size across property types
# Extreme outliers are filtered for improved visualization clarity
eda_p_landsize_by_type <- ggplot(df, aes(x = Type, y = Landsize, fill = Type)) +
geom_boxplot(outlier.shape = NA, alpha = 0.8) +
coord_cartesian(ylim = c(0, quantile(df$Landsize, 0.99, na.rm = TRUE))) + # Y-axis is limited to better view the data
labs(title = "Landsize Distribution by Property Type",
x = "Property Type",
y = "Landsize (sqm)") +
scale_fill_manual(values = c("h" = "#D62728", "t" = "#9467BD", "u" = "#8C564B")) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1),
legend.position = "none")
eda_p_landsize_by_type
Analysis: Land size differences are highly significant for classification. Houses (“h”) unequivocally occupy substantially larger parcels, reflecting the preference for private land ownership. Townhouses (“t”) reside on smaller plots, balancing space with density. Units (“u”) are associated with minimal or no individual land. This stark contrast directly reflects fundamental structural and ownership characteristics, making Landsize a highly distinguishing feature.
set.seed(123)
library(caret)
library(lattice)
library(ranger)
# Load the dataset
df <- read.csv("cleaned_Melbourne_housing_FULL.csv")
# Delete unnecessary columns (such as Address, etc.)
df_model <- df[, c("Rooms", "Bathroom", "Car", "Landsize", "BuildingArea", "YearBuilt", "Price")]
# Remove missing values
df_model <- na.omit(df_model)
# Split training set and testing set
train_index <- createDataPartition(df_model$Price, p = 0.7, list = FALSE)
train_data <- df_model[train_index, ]
test_data <- df_model[-train_index, ]
# Training Control Settings
ctrl <- trainControl(method = "cv", number = 3)
# Train a linear regression model
model_lm <- train(Price ~ ., data = train_data, method = "lm", trControl = ctrl)
# Train a Random Forest model using ranger
model_rf <- train(Price ~ ., data = train_data, method = "ranger", trControl = ctrl, importance = 'impurity')
# Predict
pred_lm <- predict(model_lm, newdata = test_data)
pred_rf <- predict(model_rf, newdata = test_data)
# Load Evaluation Metrics
library(Metrics)
# Linear Regression Performance
lm_rmse <- rmse(test_data$Price, pred_lm)
lm_r2 <- R2(pred_lm, test_data$Price)
# Random Forest Performance
rf_rmse <- rmse(test_data$Price, pred_rf)
rf_r2 <- R2(pred_rf, test_data$Price)
# Output comparison table
data.frame(
Model = c("Linear Regression", "Random Forest"),
RMSE = c(lm_rmse, rf_rmse),
R2 = c(lm_r2, rf_r2)
)
## Model RMSE R2
## 1 Linear Regression 506921.5 0.3668723
## 2 Random Forest 425378.8 0.5547148
library(ggplot2)
ggplot(data = NULL, aes(x = test_data$Price, y = pred_lm)) +
geom_point(alpha = 0.4) +
geom_abline(color = "red") +
labs(title = "Linear Regression Predicted vs Actual Prices",
x = "Actual Price", y = "Predicted Price") +
theme_minimal()
ggplot(data = test_data, aes(x = Price, y = pred_rf)) +
geom_point(alpha = 0.4) +
geom_abline(color = "red") +
labs(title = "Random Forest Predicted vs Actual Prices",
x = "Actual Price", y = "Predicted Price") +
theme_minimal()
Conclusion:This project establishes a regression model to predict housing price data in college student residential areas. By comparing the evaluation results of linear regression and random forest models, it is found that the random forest model outperforms linear regression in two main indicators - Root Mean Square Error (RMSE) and coefficient of determination (R²). The RMSE of the random forest model is 424,312, lower than the 506,922 of linear regression, indicating smaller prediction errors. Meanwhile, the R² value reaches 0.557, showing that it can better explain the underlying mechanisms of housing price changes. Therefore, the random forest model is more suitable as the core prediction model for this project.
# Load libraries
set.seed(123)
library(caret)
library(randomForest)
library(gbm)
library(Metrics)
library(ggplot2)
# Load the dataset
df <- read.csv("cleaned_Melbourne_housing_FULL.csv")
# Select relevant features for regression
df_model <- df[, c("Rooms", "Bathroom", "Car", "Landsize", "BuildingArea", "YearBuilt", "Price")]
df_model <- na.omit(df_model)
# Split into training and testing sets (70/30 split)
train_index <- createDataPartition(df_model$Price, p = 0.7, list = FALSE)
train_data <- df_model[train_index, ]
test_data <- df_model[-train_index, ]
# Training control settings
ctrl <- trainControl(method = "cv", number = 3)
# Train Random Forest model
model_rf <- train(Price ~ ., data = train_data, method = "ranger", trControl = ctrl)
# Train Gradient Boosting model
model_gbm <- train(Price ~ ., data = train_data, method = "gbm", verbose = FALSE, trControl = ctrl)
# Predictions
pred_rf <- predict(model_rf, newdata = test_data)
pred_gbm <- predict(model_gbm, newdata = test_data)
# Evaluation: RMSE and R-squared
rf_rmse <- rmse(test_data$Price, pred_rf)
rf_r2 <- R2(pred_rf, test_data$Price)
gbm_rmse <- rmse(test_data$Price, pred_gbm)
gbm_r2 <- R2(pred_gbm, test_data$Price)
# Comparison table
results <- data.frame(
Model = c("Random Forest", "Gradient Boosting"),
RMSE = c(rf_rmse, gbm_rmse),
R2 = c(rf_r2, gbm_r2)
)
print(results)
## Model RMSE R2
## 1 Random Forest 424474.1 0.5569621
## 2 Gradient Boosting 446912.3 0.5098354
# Graphical representation
# Random Forest
ggplot(data = NULL, aes(x = test_data$Price, y = pred_rf)) +
geom_point(alpha = 0.4) +
geom_abline(color = "red") +
labs(title = "Random Forest: Predicted vs Actual Prices",
x = "Actual Price", y = "Predicted Price") +
theme_minimal()
# Gradient Boosting
ggplot(data = NULL, aes(x = test_data$Price, y = pred_gbm)) +
geom_point(alpha = 0.4) +
geom_abline(color = "red") +
labs(title = "Gradient Boosting: Predicted vs Actual Prices",
x = "Actual Price", y = "Predicted Price") +
theme_minimal()
Conclusion for model 2: By comparing both results of Random Forest and Gradient Boosting (GBM), it shows that both models are suitable in predicting housing price. However, the result shows that Random Forest have slightly better performance on this data set with a lower RMSE(424517.6) and Higher R²(0.5567944).Therefore, Random Forest is more preferred model in house price prediction.
# Load libraries
set.seed(123)
library(caret)
library(randomForest)
library(gbm)
library(Metrics)
library(ggplot2)
# Select relevant features for regression
df_model <- df[, c("Type","Rooms", "Bathroom", "Car", "Landsize", "BuildingArea", "YearBuilt")]
df_model <- na.omit(df_model)
# Specifying the target variable
df_model$Type <- as.factor(df_model$Type)
# Split into training and testing sets (70/30 split)
train_index <- createDataPartition(df_model$Type, p = 0.7, list = FALSE)
train_data <- df_model[train_index, ]
test_data <- df_model[-train_index, ]
# Training control settings
ctrl <- trainControl(method = "cv", number = 3)
# Train Random Forest model
model_rf <- train(Type ~ ., data = train_data, method = "ranger", trControl = ctrl, importance = 'impurity')
# Train Gradient Boosting model
model_gbm <- train(Type ~ ., data = train_data, method = "gbm", verbose = FALSE, trControl = ctrl)
# Predictions
pred_rf <- predict(model_rf, newdata = test_data)
pred_gbm <- predict(model_gbm, newdata = test_data)
# Evaluation: RMSE and R-squared
cm_rf <- confusionMatrix(pred_rf, test_data$Type)
cm_gbm <- confusionMatrix(pred_gbm, test_data$Type)
# Comparison table
results <- data.frame(
Model = c("Random Forest", "Gradient Boosting"),
Accuracy = c(cm_rf$overall["Accuracy"], cm_gbm$overall["Accuracy"]),
Kappa = c(cm_rf$overall["Kappa"], cm_gbm$overall["Kappa"])
)
print(results)
## Model Accuracy Kappa
## 1 Random Forest 0.8109337 0.5812594
## 2 Gradient Boosting 0.8044226 0.5582174
Conclusion for model comparison 1: By comparing both results of Random Forest and Gradient Boosting (GBM), it shows that both Random Forest is more suitable in the classification of property types. The result shows that Random Forest have slightly better performance on this data set with a higher Accuracy: 0.8109337 and Kappa: 0.5812594.Therefore, Random Forest is more preferred model in property type classification.
# Compute variable importance
importance_rf <- varImp(model_rf)
# Print top features
print(importance_rf)
## ranger variable importance
##
## Overall
## Rooms 100.000
## Landsize 84.354
## YearBuilt 33.471
## BuildingArea 17.502
## Car 5.055
## Bathroom 0.000
# Plot variable importance
plot(importance_rf, main = "Variable Importance - Random Forest")
The most important features in property type prediction are ‘Rooms’ and ‘Landsize’.
# Load libraries
set.seed(123)
library(caret)
library(randomForest)
library(gbm)
library(ggplot2)
# Select features
df_model <- df[, c("Type", "Rooms", "Bathroom", "Car", "Landsize", "BuildingArea", "YearBuilt")]
df_model <- na.omit(df_model)
# Set target as factor
df_model$Type <- as.factor(df_model$Type)
# Train/test split
train_index <- createDataPartition(df_model$Type, p = 0.7, list = FALSE)
train_data <- df_model[train_index, ]
test_data <- df_model[-train_index, ]
# Training control
ctrl <- trainControl(method = "cv", number = 3)
# Train models
model_rf <- train(Type ~ ., data = train_data, method = "ranger", trControl = ctrl)
model_dt <- train(Type ~ ., data = train_data, method = "rpart", trControl = ctrl)
# Predictions
pred_rf <- predict(model_rf, newdata = test_data)
pred_dt <- predict(model_dt, newdata = test_data)
# Evaluation using Confusion Matrix
cm_rf <- confusionMatrix(pred_rf, test_data$Type)
cm_dt <- confusionMatrix(pred_dt, test_data$Type)
# Print overall accuracy
results <- data.frame(
Model = c("Random Forest", "Decision Tree"),
Accuracy = c(cm_rf$overall["Accuracy"], cm_dt$overall["Accuracy"]),
Kappa = c(cm_rf$overall["Kappa"], cm_dt$overall["Kappa"])
)
print(results)
## Model Accuracy Kappa
## 1 Random Forest 0.8109337 0.5812594
## 2 Decision Tree 0.7831695 0.4837227
Conclusion for model comparison 2: By comparing both results of Random Forest and Decision Tree, it shows that both Random Forest is more suitable in the classification of property types. The result shows that Random Forest have slightly better performance on this data set with a higher Accuracy: 0.8109337 and Kappa: 0.5812594.Therefore, Random Forest is more preferred model in property type classification.
# Print importance for the decision tree
importance_dt <- varImp(model_dt)
print(importance_dt)
## rpart variable importance
##
## Overall
## Rooms 100.000
## Landsize 53.967
## Car 19.594
## BuildingArea 7.738
## Bathroom 6.371
## YearBuilt 0.000
The most important features in property type prediction are ‘Rooms’ and ‘Landsize’.
# Load libraries
library(tidymodels)
library(xgboost)
# Ensure target is a factor
train_data$Type <- as.factor(train_data$Type)
test_data$Type <- as.factor(test_data$Type)
# Preprocessing recipe
xgb_recipe <- recipe(Type ~ ., data = train_data) %>%
step_zv(all_predictors()) %>%
step_normalize(all_numeric_predictors())
# XGBoost model specification with tuning parameters
xgb_spec <- boost_tree(
mode = "classification",
trees = 200,
tree_depth = tune(),
learn_rate = tune(),
loss_reduction = tune(),
mtry = tune(),
sample_size = tune()
) %>%
set_engine("xgboost")
# Combine recipe and model into a workflow
xgb_workflow <- workflow() %>%
add_recipe(xgb_recipe) %>%
add_model(xgb_spec)
# 3-fold cross-validation
set.seed(123)
folds <- vfold_cv(train_data, v = 3)
# Tuning grid using Latin Hypercube Sampling
xgb_grid <- grid_latin_hypercube(
tree_depth(range = c(2, 10)),
learn_rate(range = c(0.01, 0.3)),
loss_reduction(range = c(0, 5)),
mtry = finalize(mtry(), train_data),
sample_size = sample_prop(range = c(0.5, 1)),
size = 20
)
# Tune model with CV
set.seed(123)
xgb_tuned <- tune_grid(
xgb_workflow,
resamples = folds,
grid = xgb_grid,
control = control_grid(save_pred = TRUE)
)
# Select best performing model based on accuracy
best_params <- select_best(xgb_tuned, metric = "accuracy")
# Finalize model with best parameters
final_xgb <- finalize_workflow(xgb_workflow, best_params)
# Fit model on full training data
final_model <- fit(final_xgb, data = train_data)
# Predict on test data
xgb_preds <- predict(final_model, new_data = test_data) %>%
bind_cols(test_data)
# Confusion matrix
conf_matrix <- conf_mat(xgb_preds, truth = Type, estimate = .pred_class)
print(conf_matrix)
## Truth
## Prediction h t u
## h 5030 425 347
## t 199 264 85
## u 289 168 1333
# Evaluation metrics: Accuracy and Kappa
xgb_results <- metrics(xgb_preds, truth = Type, estimate = .pred_class)
accuracy_val <- xgb_results %>% filter(.metric == "accuracy") %>% pull(.estimate)
kappa_val <- xgb_results %>% filter(.metric == "kap") %>% pull(.estimate)
# Create result frame
xgb_output <- data.frame(
Model = "XGBoost",
Accuracy = round(accuracy_val, 7),
Kappa = round(kappa_val, 7)
)
print(xgb_output)
## Model Accuracy Kappa
## 1 XGBoost 0.8141278 0.5977218
Overall Findings
Property prices in Melbourne are skewed toward the lower-to-mid range, with high-value properties concentrated among the affluent segment.
A positive correlation exists between the number of rooms and property prices, reflecting the needs of larger families requiring more personal space.
A negative correlation between property prices and distance from the CBD confirms that urban properties are more expensive due to better accessibility and amenities.
Suburban areas exhibit variability in property types, with some areas dominated by houses and others by units.
Property characteristics such as the number of rooms and land size follow this pattern across types:Units < Townhouses < Houses
The Random Forest model performed best in predicting property prices, by achieving RMSE of 425378.8 and R²: 0.5547148
Model Comparison Table:
| Model | RMSE | R² |
|---|---|---|
| Random Forest | 425378.8 | 0.5547148 |
| Linear Regression | 506921.5 | 0.3668723 |
| Gradient Boosting | 446912.3 | 0.5098354 |
Random Forest also outperformed Gradient Boosting and Decision Tree in the classification of property types, by achieving accuracy of 0.8109337 and Kappa of 0.5812594.
Model Comparison Table:
| Model | Accuracy | Kappa |
|---|---|---|
| Random Forest | 0.8109337 | 0.5812594 |
| Gradient Boosting | 0.8044226 | 0.5582174 |
| Decision Tree | 0.7831695 | 0.4837227 |
| Gradient Boosting (Tuned) | 0.8141278 | 0.5977218 |
Hyperparameter Tuning of the XGBoost model shows slight improvement in the Accuracy-Score and Kappa-Score (0.8141278, 0.5977218), surpassing Random Forest.
Melbourne offers a wide range of affordable housing options in the lower-to-mid price segment. Property prices tend to increase with the number of rooms and land area, but decrease as the distance from the CBD increases. Among the evaluated models, Random Forest is the most effective for both predicting property prices based on house features and the classification of property types based on house features. However, hyperparamter tuning improved the Accuracy-Score and Kappa-Score of XGBoost, surpassing Random Forest.