library(readxl)
## Warning: package 'readxl' was built under R version 4.3.3
data <- read_xlsx('./Apartments.xlsx')
head(data)
## # A tibble: 6 × 5
## Age Distance Price Parking Balcony
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 7 28 1640 0 1
## 2 18 1 2800 1 0
## 3 7 28 1660 0 0
## 4 28 29 1850 0 1
## 5 18 18 1640 1 1
## 6 28 12 1770 0 1
Description:
data$ID <- seq(1, nrow(data))
data$ParkingF <- factor(data$Parking,
levels = c("0", "1"),
labels = c("NO", "YES"))
data$BalconyF <- factor(data$Balcony,
levels = c("0", "1"),
labels = c("NO", "YES"))
head(data)
## # A tibble: 6 × 8
## Age Distance Price Parking Balcony ID ParkingF BalconyF
## <dbl> <dbl> <dbl> <dbl> <dbl> <int> <fct> <fct>
## 1 7 28 1640 0 1 1 NO YES
## 2 18 1 2800 1 0 2 YES NO
## 3 7 28 1660 0 0 3 NO NO
## 4 28 29 1850 0 1 4 NO YES
## 5 18 18 1640 1 1 5 YES YES
## 6 28 12 1770 0 1 6 NO YES
Assumptions:
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.3.3
ggplot(data, aes(x = Price)) +
geom_histogram(binwidth = 500, colour = "black") +
ylab("Frequency") +
xlab("Price")
As the histogram doesn’t show evidently normal distribution of the
variable, I run a Shapiro-Wilk test
shapiro.test(data$Price)
##
## Shapiro-Wilk normality test
##
## data: data$Price
## W = 0.94017, p-value = 0.0006513
We reject H0 at p = 0.0006513, so the variable is not normally distributed and I need to run a non-parametric test (Wilcoxon Signed Rank Test):
median(data$Price)
## [1] 1950
wilcox.test(data$Price,
mu = 1950,
correct = FALSE)
##
## Wilcoxon signed rank test
##
## data: data$Price
## V = 2048, p-value = 0.2408
## alternative hypothesis: true location is not equal to 1950
library(effectsize)
## Warning: package 'effectsize' was built under R version 4.3.3
effectsize(wilcox.test(data$Price,
mu = 1950,
correct = FALSE))
## r (rank biserial) | 95% CI
## ---------------------------------
## 0.15 | [-0.10, 0.38]
##
## - Deviation from a difference of 1950.
Conclusion: we can’t reject H0 at p = 0.2408, so we conclude that the median price of the apartments is 1950
fit1 <- lm(Price ~ Age,
data = data)
summary(fit1)
##
## Call:
## lm(formula = Price ~ Age, data = data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -623.9 -278.0 -69.8 243.5 776.1
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2185.455 87.043 25.108 <2e-16 ***
## Age -8.975 4.164 -2.156 0.034 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 369.9 on 83 degrees of freedom
## Multiple R-squared: 0.05302, Adjusted R-squared: 0.04161
## F-statistic: 4.647 on 1 and 83 DF, p-value: 0.03401
cor_coef <- sqrt(summary(fit1)$r.squared)
cor_coef
## [1] 0.230255
There’s a weak correlation between Age and Price. This correlation is negative, which means that when the age increases, the price of the apartment decreases
We can say that 5.3% of the variability of Price is explained by the linear effect of age
library(car)
## Warning: package 'car' was built under R version 4.3.3
## Loading required package: carData
## Warning: package 'carData' was built under R version 4.3.3
scatterplotMatrix(data[ , c(-4, -5, -6, -7)],
smooth = FALSE)
It seems that there are not multicollinearity problems because it looks like there is no correlation between the variables Distance and Age based on the scatterplot matrix
fit2 <- lm(Price ~ Age + Distance,
data = data)
summary(fit2)
##
## Call:
## lm(formula = Price ~ Age + Distance, data = data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -603.23 -219.94 -85.68 211.31 689.58
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2460.101 76.632 32.10 < 2e-16 ***
## Age -7.934 3.225 -2.46 0.016 *
## Distance -20.667 2.748 -7.52 6.18e-11 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 286.3 on 82 degrees of freedom
## Multiple R-squared: 0.4396, Adjusted R-squared: 0.4259
## F-statistic: 32.16 on 2 and 82 DF, p-value: 4.896e-11
vif(fit2)
## Age Distance
## 1.001845 1.001845
mean(vif(fit2))
## [1] 1.001845
As the VIF of the variables is less than 1 and the mean VIF is close to 1, none of the variables should be removed, since the is no multicollinearity
data$StdResid <- round(rstandard(fit2), 3) #Standardized residuals
data$CooksD <- round(cooks.distance(fit2), 3) #Cooks distances
hist(data$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals")
shapiro.test(data$StdResid)
##
## Shapiro-Wilk normality test
##
## data: data$StdResid
## W = 0.95303, p-value = 0.003645
hist(data$CooksD,
xlab = "Cooks distance",
ylab = "Frequency",
main = "Histogram of Cooks distances")
By looking at the histogram of the standardized residuals we can
conclude that there are no outliers, because there are no values outside
the range (-3, +3)
As regards the histogram of Cooks distances, that gives information about the variables with high influence, we can see that there’s a gap and we need to remove the units that follow this gap
We find the units that make the histogram non-continuous:
head(data[order(-data$CooksD),], 5)
## # A tibble: 5 × 10
## Age Distance Price Parking Balcony ID ParkingF BalconyF StdResid CooksD
## <dbl> <dbl> <dbl> <dbl> <dbl> <int> <fct> <fct> <dbl> <dbl>
## 1 5 45 2180 1 1 38 YES YES 2.58 0.32
## 2 43 37 1740 0 0 55 NO NO 1.44 0.104
## 3 2 11 2790 1 0 33 YES NO 2.05 0.069
## 4 7 2 1760 0 1 53 NO YES -2.15 0.066
## 5 37 3 2540 1 1 22 YES YES 1.58 0.061
We can see that the first unit is the one that has to be removed
library(dplyr)
##
## Attaching package: 'dplyr'
## The following object is masked from 'package:car':
##
## recode
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
data <- data %>%
filter(!ID == 38)
We fit the model again:
fit2 <- lm(Price ~ Age + Distance,
data = data)
data$StdResid <- round(rstandard(fit2), 3) #Standardized residuals
data$CooksD <- round(cooks.distance(fit2), 3) #Cooks distances
hist(data$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals")
hist(data$CooksD,
xlab = "Cooks distance",
ylab = "Frequency",
main = "Histogram of Cooks distances")
We can still see a gap in the Cooks distances histogram so we need to
remove the observation that follows that gap
head(data[order(-data$CooksD),], 3)
## # A tibble: 3 × 10
## Age Distance Price Parking Balcony ID ParkingF BalconyF StdResid CooksD
## <dbl> <dbl> <dbl> <dbl> <dbl> <int> <fct> <fct> <dbl> <dbl>
## 1 43 37 1740 0 0 55 NO NO 1.60 0.129
## 2 2 11 2790 1 0 33 YES NO 2.22 0.084
## 3 7 2 1760 0 1 53 NO YES -2.24 0.072
We remove the first observation
library(dplyr)
data <- data %>%
filter(!ID == 55)
And, now, we fit the model again and look if there are still gaps
fit2 <- lm(Price ~ Age + Distance,
data = data)
data$StdResid <- round(rstandard(fit2), 3) #Standardized residuals
data$CooksD <- round(cooks.distance(fit2), 3) #Cooks distances
hist(data$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals")
hist(data$CooksD,
xlab = "Cooks distance",
ylab = "Frequency",
main = "Histogram of Cooks distances")
We can still see gaps in the histograms, so we must remove the units
that follow the gaps
head(data[order(-data$CooksD),], 5)
## # A tibble: 5 × 10
## Age Distance Price Parking Balcony ID ParkingF BalconyF StdResid CooksD
## <dbl> <dbl> <dbl> <dbl> <dbl> <int> <fct> <fct> <dbl> <dbl>
## 1 2 11 2790 1 0 33 YES NO 2.17 0.084
## 2 7 2 1760 0 1 53 NO YES -2.35 0.084
## 3 37 3 2540 1 1 22 YES YES 1.57 0.065
## 4 8 26 2300 1 1 25 YES YES 1.85 0.054
## 5 45 21 1910 0 1 31 NO YES 1.07 0.05
The first 2 observations are the ones that we must remove so that we can have a continuous histogram
library(dplyr)
data <- data %>%
filter(!ID == 33)
data <- data %>%
filter(!ID == 53)
Now, I fit the model again to check if there are no more gaps
fit2 <- lm(Price ~ Age + Distance,
data = data)
data$StdResid <- round(rstandard(fit2), 3) #Standardized residuals
data$CooksD <- round(cooks.distance(fit2), 3) #Cooks distances
hist(data$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals")
hist(data$CooksD,
xlab = "Cooks distance",
ylab = "Frequency",
main = "Histogram of Cooks distances")
Finally, the resulting histogram is continuous, which means that there are no units of high impact. The dataset now contains 81 observations
data$StdFitted <- scale(fit2$fitted.values)
library(car)
scatterplot(y = data$StdResid, x = data$StdFitted,
ylab = "Standardized residuals",
xlab = "Standardized fitted values",
boxplots = FALSE,
regLine = FALSE,
smooth = FALSE)
By looking at the scatterplot, I can conclude that there’s
heteroskedasticity because the variances of the errors are not constant.
For smaller values of x the variance is lower than for bigger values of
x
hist(data$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals")
shapiro.test(data$StdResid)
##
## Shapiro-Wilk normality test
##
## data: data$StdResid
## W = 0.94297, p-value = 0.001286
By looking and the histogram and also by looking at the results of the Shapiro-Wilk test we can conclude that the standardized residuals are not normally distributed (we reject H0 at p = 0.001)
This violates one of the assumptions of the classical linear regression model, but it is not problematic because the sample size is large enough
library(readxl)
data <- read_xlsx('./Apartments.xlsx')
data$ParkingF <- factor(data$Parking,
levels = c("0", "1"),
labels = c("NO", "YES"))
data$BalconyF <- factor(data$Balcony,
levels = c("0", "1"),
labels = c("NO", "YES"))
data$ID <- seq(1, nrow(data))
fit2 <- lm(Price ~ Age + Distance,
data = data)
summary(fit2)
##
## Call:
## lm(formula = Price ~ Age + Distance, data = data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -603.23 -219.94 -85.68 211.31 689.58
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2460.101 76.632 32.10 < 2e-16 ***
## Age -7.934 3.225 -2.46 0.016 *
## Distance -20.667 2.748 -7.52 6.18e-11 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 286.3 on 82 degrees of freedom
## Multiple R-squared: 0.4396, Adjusted R-squared: 0.4259
## F-statistic: 32.16 on 2 and 82 DF, p-value: 4.896e-11
B0: the price of a new apartment (age 0) and at 0 km from the city center is, on average, USD 2460.101 millions (p < 0.001)
B1: for each additional year that the apartment has, the average price decreases by USD 7.934 millions (assuming that the distance remains constant) (p = 0.016)
B2: for each additional km further away from the city center, the average price of the apartments decreases by USD 20.667 millions (assuming that the age of the apartment remains constant) (p < 0.001)
fit3 <- lm(Price ~ Age + Distance + ParkingF + BalconyF,
data = data)
anova(fit2, fit3)
## Analysis of Variance Table
##
## Model 1: Price ~ Age + Distance
## Model 2: Price ~ Age + Distance + ParkingF + BalconyF
## Res.Df RSS Df Sum of Sq F Pr(>F)
## 1 82 6720983
## 2 80 5991088 2 729894 4.8732 0.01007 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
H0: the more complex model doesn’t provide a significantly better fit to the data than the simpler model (no significant difference between fit2 and fit3)
H1: the more complex model provides a significantly better fit to the data than the simpler model (significant difference between fit2 and fit3)
So, looking at the anova test, we reject H0 at p = 0.01, which means that the more complex model (fit 3), fits the data significantly better than the simpler model (fit2)
summary(fit3)
##
## Call:
## lm(formula = Price ~ Age + Distance + ParkingF + BalconyF, data = data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -459.92 -200.66 -57.48 260.08 594.37
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2301.667 94.271 24.415 < 2e-16 ***
## Age -6.799 3.110 -2.186 0.03172 *
## Distance -18.045 2.758 -6.543 5.28e-09 ***
## ParkingFYES 196.168 62.868 3.120 0.00251 **
## BalconyFYES 1.935 60.014 0.032 0.97436
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 273.7 on 80 degrees of freedom
## Multiple R-squared: 0.5004, Adjusted R-squared: 0.4754
## F-statistic: 20.03 on 4 and 80 DF, p-value: 1.849e-11
B3: given the age, distance and the fact that the apartment has or not a balcony, if it has parking the apartment is, on average, USD 196.168 millions more expensive compared to the apartments that don’t have parking (p = 0.003)
B4: given the age, distance and whether the apartment has or not parking, if it has a balcony, the price of the apartment will be, on average, USD 1.935 millions more expensive than the apartments with no balcony (Note: there is no evidence that this coefficient is significant, so it wouldn’t be necessary to explain it)
F-statistic hypotheses:
In this case, as p < 0.001, we reject H0, which means that the price can be explained with a linear relationship between the variables Age, Distance, BalconyF and ParkingF
data$Fitted <- fitted.values(fit3)
residualID2 <- data[data$ID == 2, 'Price'] - data[data$ID == 2, 'Fitted']
residualID2
## Price
## 1 442.5889