MATH1324 Assignment 2: Statistical data analysis project

Analysis of Melbourne Housing Price

Brandon Heng S3987154

Last updated: 25 May, 2024

Introduction

Problem Statement

Data

Column descriptions

In the dataset, there are 21 columns. However, for the analysis, only four columns are needed:

mel_house_prices <- read_csv("Melbourne_housing_FULL.csv")
colnames(mel_house_prices)
##  [1] "Suburb"        "Address"       "Rooms"         "Type"         
##  [5] "Price"         "Method"        "SellerG"       "Date"         
##  [9] "Distance"      "Postcode"      "Bedroom2"      "Bathroom"     
## [13] "Car"           "Landsize"      "BuildingArea"  "YearBuilt"    
## [17] "CouncilArea"   "Lattitude"     "Longtitude"    "Regionname"   
## [21] "Propertycount"
knitr::kable(head(mel_house_prices, 10))
Suburb Address Rooms Type Price Method SellerG Date Distance Postcode Bedroom2 Bathroom Car Landsize BuildingArea YearBuilt CouncilArea Lattitude Longtitude Regionname Propertycount
Abbotsford 68 Studley St 2 h NA SS Jellis 3/09/2016 2.5 3067 2 1 1 126 NA NA Yarra City Council -37.8014 144.9958 Northern Metropolitan 4019
Abbotsford 85 Turner St 2 h 1480000 S Biggin 3/12/2016 2.5 3067 2 1 1 202 NA NA Yarra City Council -37.7996 144.9984 Northern Metropolitan 4019
Abbotsford 25 Bloomburg St 2 h 1035000 S Biggin 4/02/2016 2.5 3067 2 1 0 156 79 1900 Yarra City Council -37.8079 144.9934 Northern Metropolitan 4019
Abbotsford 18/659 Victoria St 3 u NA VB Rounds 4/02/2016 2.5 3067 3 2 1 0 NA NA Yarra City Council -37.8114 145.0116 Northern Metropolitan 4019
Abbotsford 5 Charles St 3 h 1465000 SP Biggin 4/03/2017 2.5 3067 3 2 0 134 150 1900 Yarra City Council -37.8093 144.9944 Northern Metropolitan 4019
Abbotsford 40 Federation La 3 h 850000 PI Biggin 4/03/2017 2.5 3067 3 2 1 94 NA NA Yarra City Council -37.7969 144.9969 Northern Metropolitan 4019
Abbotsford 55a Park St 4 h 1600000 VB Nelson 4/06/2016 2.5 3067 3 1 2 120 142 2014 Yarra City Council -37.8072 144.9941 Northern Metropolitan 4019
Abbotsford 16 Maugie St 4 h NA SN Nelson 6/08/2016 2.5 3067 3 2 2 400 220 2006 Yarra City Council -37.7965 144.9965 Northern Metropolitan 4019
Abbotsford 53 Turner St 2 h NA S Biggin 6/08/2016 2.5 3067 4 1 2 201 NA 1900 Yarra City Council -37.7995 144.9974 Northern Metropolitan 4019
Abbotsford 99 Turner St 2 h NA S Collins 6/08/2016 2.5 3067 3 2 1 202 NA 1900 Yarra City Council -37.7996 144.9989 Northern Metropolitan 4019

Preprocessing

mel_house_prices <- mel_house_prices %>% select(Address, Regionname, Distance, Price)
head(mel_house_prices)
cleaned_mel_house_prices <- mel_house_prices
cleaned_mel_house_prices$Regionname <- mel_house_prices$Regionname %>% factor(
                       levels = c("Northern Metropolitan", "Western Metropolitan", "Southern Metropolitan", "Eastern Metropolitan", "South-Eastern Metropolitan", "Eastern Victoria", "Northern Victoria", "Western Victoria"))
str(cleaned_mel_house_prices)
## tibble [34,857 × 4] (S3: tbl_df/tbl/data.frame)
##  $ Address   : chr [1:34857] "68 Studley St" "85 Turner St" "25 Bloomburg St" "18/659 Victoria St" ...
##  $ Regionname: Factor w/ 8 levels "Northern Metropolitan",..: 1 1 1 1 1 1 1 1 1 1 ...
##  $ Distance  : num [1:34857] 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 ...
##  $ Price     : num [1:34857] NA 1480000 1035000 NA 1465000 ...
summary(mel_house_prices)
##    Address           Regionname           Distance         Price         
##  Length:34857       Length:34857       Min.   : 0.00   Min.   :   85000  
##  Class :character   Class :character   1st Qu.: 6.40   1st Qu.:  635000  
##  Mode  :character   Mode  :character   Median :10.30   Median :  870000  
##                                        Mean   :11.18   Mean   : 1050173  
##                                        3rd Qu.:14.00   3rd Qu.: 1295000  
##                                        Max.   :48.10   Max.   :11200000  
##                                        NA's   :1       NA's   :7610

Descriptive Statistics and Visualisation.

The price column

price_sta <- cleaned_mel_house_prices %>% summarise(Min = min(Price,na.rm = TRUE),
                                  Q1 = quantile(Price,probs = .25,na.rm = TRUE),
                                  Median = median(Price, na.rm = TRUE),
                                  Q3 = round(quantile(Price,probs = .75,na.rm = TRUE),2),
                                  IQR = IQR(Price, na.rm = TRUE),
                                  Max = max(Price,na.rm = TRUE),
                                  Mean = round(mean(Price, na.rm = TRUE),2),
                                  SD = round(sd(Price, na.rm = TRUE),2),
                                  n = n(),
                                  Missing = sum(is.na(Price)))
knitr::kable(price_sta)
Min Q1 Median Q3 IQR Max Mean SD n Missing
85000 635000 870000 1295000 660000 11200000 1050173 641467.1 34857 7610
price_mean <- mean(cleaned_mel_house_prices$Price, na.rm = TRUE)
price_his_plot <- ggplot(cleaned_mel_house_prices, aes(x = Price)) +
  geom_histogram(fill = "blue", color = "white", bins = 30) +
  geom_vline(aes(xintercept = price_mean), color = "red", linetype = "dashed", size = 1) +
  annotate("text", x = price_mean, y = Inf, label = paste("Mean:", round(price_mean, 2)), vjust = 1.5, hjust = -0.1, color = "red") + 
  labs(x = "Price", y = "Frequency") +
  scale_x_continuous(labels = label_comma())
annotate_figure(price_his_plot, bottom=text_grob("Figure 1. Histogram of prices", face = "bold", size = 14))

The distance column

distance_sta <- cleaned_mel_house_prices %>% summarise(Min = min(Distance,na.rm = TRUE),
                                  Q1 = quantile(Distance,probs = .25,na.rm = TRUE),
                                  Median = median(Distance, na.rm = TRUE),
                                  Q3 = round(quantile(Distance,probs = .75,na.rm = TRUE),2),
                                  IQR = IQR(Distance, na.rm = TRUE),
                                  Max = max(Distance,na.rm = TRUE),
                                  Mean = round(mean(Distance, na.rm = TRUE),2),
                                  SD = round(sd(Distance, na.rm = TRUE),2),
                                  n = n(),
                                  Missing = sum(is.na(Distance)))
knitr::kable(distance_sta)
Min Q1 Median Q3 IQR Max Mean SD n Missing
0 6.4 10.3 14 7.6 48.1 11.18 6.79 34857 1
distance_mean <- mean(cleaned_mel_house_prices$Distance, na.rm = TRUE)
distance_his_plot <- ggplot(cleaned_mel_house_prices, aes(x = Distance)) +
  geom_histogram(fill = "red", color = "white", bins = 30) +
  geom_vline(aes(xintercept = distance_mean), color = "blue", linetype = "dashed", size = 1) +
  annotate("text", x = distance_mean, y = Inf, label = paste("Mean:", round(distance_mean, 2)), vjust = 1.5, hjust = -0.1, color = "red") + 
  labs(x = "Distance", y = "Frequency") +
  scale_x_continuous(labels = label_comma())

annotate_figure(distance_his_plot, bottom=text_grob("Figure 2. Histogram of distance", face = "bold", size = 14))

Descriptive Statistics and Visualisation Cont.

Missing values

The price column

cleaned_mel_house_prices <- mel_house_prices[!is.na(mel_house_prices$Price), ]
sum(is.na(cleaned_mel_house_prices$Price))
## [1] 0

The distance column

cleaned_mel_house_prices <- cleaned_mel_house_prices[!is.na(cleaned_mel_house_prices$Distance), ]
sum(is.na(cleaned_mel_house_prices$Distance))
## [1] 0

Outliers

b4_price_box_plot <- ggplot(cleaned_mel_house_prices, aes(y = Price)) +
  geom_boxplot(fill = "dodgerblue3", color = "black", alpha = 0.7) +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Price", y = "House Price")

b4_distance_box_plot <- ggplot(cleaned_mel_house_prices, aes(y = Distance)) +
  geom_boxplot(fill = "red", color = "black", alpha = 0.7) +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Distance", y = "Distance from CBD")

# Arrange the histograms side by side
annotate_figure(grid.arrange(b4_price_box_plot, b4_distance_box_plot, nrow = 1), bottom=text_grob("Figure 3. Box plot of price and distance before removing outliers", face = "bold", size = 14))

cal_lower_upper_bound = function(x){
  # Calculate Q1, Q3, and IQR
  Q1 <- quantile(x, 0.25)
  Q3 <- quantile(x, 0.75)
  IQR <- Q3 - Q1
  
  # Define lower and upper bounds
  lower_bound <- Q1 - 1.5 * IQR
  upper_bound <- Q3 + 1.5 * IQR
  
  return(list(lower_bound = lower_bound, upper_bound = upper_bound))
}
is.outlier = function(x, lower_bound, upper_bound){(x < lower_bound)|(x > upper_bound)}
price_boundary <- cal_lower_upper_bound(cleaned_mel_house_prices$Price)
print(sum(is.outlier(cleaned_mel_house_prices$Price, price_boundary$lower_bound, price_boundary$upper_bound)))
## [1] 1278
distance_boundary <- cal_lower_upper_bound(cleaned_mel_house_prices$Distance)
print(sum(is.outlier(cleaned_mel_house_prices$Distance, distance_boundary$lower_bound, distance_boundary$upper_bound)))
## [1] 1188
cleaned_mel_house_prices <- cleaned_mel_house_prices %>% filter(
  Price >= price_boundary$lower_bound,
  Price <= price_boundary$upper_bound,
  Distance >= distance_boundary$lower_bound,
  Distance <= distance_boundary$upper_bound
)
af_price_box_plot <- ggplot(cleaned_mel_house_prices, aes(y = Price)) +
  geom_boxplot(fill = "dodgerblue3", color = "black", alpha = 0.7) +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Price", y = "House Price")

af_distance_box_plot <- ggplot(cleaned_mel_house_prices, aes(y = Distance)) +
  geom_boxplot(fill = "red", color = "black", alpha = 0.7) +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Distance", y = "Distance from CBD")

# Arrange the histograms side by side
annotate_figure(grid.arrange(af_price_box_plot, af_distance_box_plot, nrow = 1), bottom=text_grob("Figure 4. Box plot of price and distance after removing outliers", face = "bold", size = 14))

Scatter Plot of Distance and Price

# scatter plot
sctter_plot <- ggplot(cleaned_mel_house_prices, aes(x = Distance, y = Price)) +
  geom_point(color = "blue", size = 0.1) +
  geom_smooth(method = "lm", se = TRUE, color = "red") +
  labs(title = "Scatter Plot of Distance and Price", x = "Distance", y = "Price") +
  theme_minimal()

annotate_figure(grid.arrange(sctter_plot, nrow = 1), bottom=text_grob("Figure 5. Scatter Plot of Distance and Price", face = "bold", size = 14))

Descriptive Statistics and Visualisation Cont.

west_east_mel_house_price <- cleaned_mel_house_prices[cleaned_mel_house_prices$Regionname %in% c("Western Metropolitan", "Eastern Metropolitan", "Eastern Victoria", "Western Victoria"), ]

west_east_mel_house_price <- west_east_mel_house_price %>%
  mutate(Regionname = as.character(Regionname)) %>%
  mutate(Regionname = if_else(Regionname == "Western Metropolitan" | Regionname == "Western Victoria", "west", "east"))

west_east_mel_house_price$Regionname <- west_east_mel_house_price$Regionname %>% factor(
                       levels = c("west", "east"))

str(west_east_mel_house_price)
## tibble [8,732 × 4] (S3: tbl_df/tbl/data.frame)
##  $ Address   : chr [1:8732] "154 Halsey Rd" "50 Bedford St" "50 Bedford St" "23 Hart St" ...
##  $ Regionname: Factor w/ 2 levels "west","east": 1 1 1 1 1 1 1 1 1 1 ...
##  $ Distance  : num [1:8732] 13.5 13.5 13.5 13.5 13.5 13.5 13.5 13.5 13.5 13.5 ...
##  $ Price     : num [1:8732] 840000 730000 770000 603000 700000 ...
price_by_regions <- west_east_mel_house_price %>% group_by(Regionname) %>% summarise(Min = min(Price,na.rm = TRUE),
                                                                Q1 = quantile(Price,probs = .25,na.rm = TRUE),
                                                                Median = median(Price, na.rm = TRUE),
                                                                Q3 = round(quantile(Price,probs = .75,na.rm = TRUE),2),
                                                                IQR = IQR(Price, na.rm = TRUE), 
                                                                Max = max(Price,na.rm = TRUE),
                                                                Mean = round(mean(Price, na.rm = TRUE),2),
                                                                SD = round(sd(Price, na.rm = TRUE),2),
                                                                n = n(),
                                                                Missing = sum(is.na(Price)))
knitr::kable(price_by_regions)
Regionname Min Q1 Median Q3 IQR Max Mean SD n Missing
west 85000 597125 760000 986375 389250 2255000 833300.5 346228.4 5566 0
east 288000 800000 1005000 1300000 500000 2285000 1068211.1 361267.1 3166 0

Histogram

west_mean <-  mean(west_east_mel_house_price[west_east_mel_house_price$Regionname == "west",]$Price, na.rm = TRUE)
west_his_plot <- ggplot(west_east_mel_house_price[west_east_mel_house_price$Regionname == "west",], aes(x = Price)) +
  geom_histogram(fill = "blue", color = "white", bins = 30) +
  geom_vline(aes(xintercept = west_mean), color = "red", linetype = "dashed", size = 1) +
  annotate("text", x = west_mean, y = Inf, label = paste("Mean:", round(west_mean, 2)), vjust = 1.5, hjust = -0.1, color = "red") + 
  labs(x = "Price", y = "Frequency") +
  ggtitle("Western region")

east_mean <-  mean(west_east_mel_house_price[west_east_mel_house_price$Regionname == "east",]$Price, na.rm = TRUE)
east_his_plot <- ggplot(west_east_mel_house_price[west_east_mel_house_price$Regionname == "east", ], aes(x = Price)) +
  geom_histogram(fill = "red", color = "white", bins = 30) +
  geom_vline(aes(xintercept = east_mean), color = "blue", linetype = "dashed", size = 1) +
  annotate("text", x = east_mean, y = Inf, label = paste("Mean:", round(east_mean, 2)), vjust = 1.5, hjust = -0.1, color = "red") + 
  labs(x = "Price", y = "Frequency") +
  ggtitle("Eastern region")

annotate_figure(grid.arrange(west_his_plot, east_his_plot, nrow = 1), bottom=text_grob("Figure 6. Histogram of prices for western and eastern region", face = "bold", size = 14))

Boxplots

west_median <- median(west_east_mel_house_price[west_east_mel_house_price$Regionname == "west",]$Price)
west_box_plot <- ggplot(west_east_mel_house_price[west_east_mel_house_price$Regionname == "west",], aes(y = Price)) +
  geom_boxplot(fill = "dodgerblue3", color = "black", alpha = 0.7) +
  annotate("text", x = Inf, y = west_median, label = paste("Median:", round(median(west_median), 2)), color = "black", hjust = 1.7, vjust = -1) +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Western region", y = "Price", x = "")

east_median <- median(west_east_mel_house_price[west_east_mel_house_price$Regionname == "east",]$Price)
east_box_plot <- ggplot(west_east_mel_house_price[west_east_mel_house_price$Regionname == "east",], aes(y = Price)) +
  geom_boxplot(fill = "red", color = "black", alpha = 0.7) +
  annotate("text", x = Inf, y = east_median, label = paste("Median:", round(median(east_median), 2)), color = "black", hjust = 1.7, vjust = -1) +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Eastern region", y = "Price", x = "")

annotate_figure(grid.arrange(west_box_plot, east_box_plot, nrow = 1), bottom=text_grob("Figure 7. Box plots of prices for western and eastern region", face = "bold", size = 14))

Hypothesis Testing I

\[H_0: \mu_1 = \mu_2 \]

\[H_A: \mu_1 \ne \mu_2\]

Hypthesis Testing I Cont.

The normality assumption

qq <- ggqqplot(west_east_mel_house_price, x="Price", facet.by = "Regionname")
annotate_figure(grid.arrange(qq, nrow = 1), bottom=text_grob("Figure 8. Normal Q-Q plot of west and east region properties's prices", face = "bold", size = 14))

Homogeneity of Variance - Levene’s Test

leveneTest(Price ~ Regionname, data = west_east_mel_house_price) %>% as.data.frame()

Hypthesis Testing I Cont.

t.test(
  Price ~ Regionname,
  data = west_east_mel_house_price,
  var.equal = FALSE,
  alternative = "two.sided"
)
## 
##  Welch Two Sample t-test
## 
## data:  Price by Regionname
## t = -29.652, df = 6350.2, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group west and group east is not equal to 0
## 95 percent confidence interval:
##  -250440.7 -219380.5
## sample estimates:
## mean in group west mean in group east 
##           833300.5          1068211.1

The p-value method rejects \(H_0\) because \(p = 2.2 \cdot 10^{-16} < \alpha = 0.05\). There was a statistically significant difference between the means.

Different between two means

833300.5 - 1068211.1 
## [1] -234910.6

Critical value

qt(p = 0.025, df = 6350.2)
## [1] -1.960338

Overall conclusion

Hypothesis Testing II

cor(cleaned_mel_house_prices$Distance, cleaned_mel_house_prices$Price)
## [1] -0.1364971
model <- cleaned_mel_house_prices %>%  lm(Price ~ Distance, data = .)
model %>% summary()
## 
## Call:
## lm(formula = Price ~ Distance, data = .)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -950798 -314536  -94357  268677 1371442 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  1086686       6169  176.15   <2e-16 ***
## Distance      -11452        528  -21.69   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 431500 on 24779 degrees of freedom
## Multiple R-squared:  0.01863,    Adjusted R-squared:  0.01859 
## F-statistic: 470.4 on 1 and 24779 DF,  p-value: < 2.2e-16
model %>% confint()
##                  2.5 %     97.5 %
## (Intercept) 1074594.66 1098778.23
## Distance     -12487.48  -10417.57
par(mfrow = c(1,1))
plot(model)

Hypothesis Testing II Cont.

Assumptions

Before reporting the regression model, there are five assumptions for linear regression that have to be validated.

  1. Independence: Independence was assumed because the distance and price data came from different houses sold.

  2. Linearity: The residuals vs fitted graph shows a flat relationship between fitted value and residuals (look at the red line), suggesting a linear model for distance and price. Also, the scatter plot in Figure 5 and the observed negative correlation of about -0.13 show a weak linear relationship between the variables, i.e., distance increases but price slightly decreases.

  3. Normality of residuals: Normal Q-Q plot show the deviations from normality. It is not safe to assume that the residuals are approximately normally distributed.

  4. Influential cases: The residual vs leverage graph contains no points that are above Cook’s distance. Outliers do not have a significant impact.

  5. Homoscedasticity: The residual vs. fitted graph shows homoscedasticity, or consistent variance between the data. In addition, a straight line crosses across the middle, indicating that it fits the data.

Reporting the regression model

Overall conclusion

Discussion I

Discussion II

References

Frost J (2024) Guidelines for Removing and Handling Outliers in Data, Statistics By Jim website, accessed 25 May 2024. https://statisticsbyjim.com/basics/remove-outliers/

Kinsella E (2022) Moving west, ABC website, accessed 25 May 2024. https://www.abc.net.au/news/2022-03-20/melbourne-west-altona-housing-property-prices-development/100828670

Pino T (2016) Melbourne Housing Market [data set], Kaggle website, accessed 25 May 2024. https://www.kaggle.com/datasets/anthonypino/melbourne-housing-market