data(mtcars) # load the dataset
mydata <- mtcars # copy it into your variable
head(mydata) # first 6 rows
## mpg cyl disp hp drat wt qsec vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
Data set has 32 observations and 11 variables. Data set was acquired from R’s own “mtcars” data set.
Population: All automobiles that were available on the US market in 1973–74 (or more broadly, all cars that could have been studied from that era).
Unit: An individual automobile model (e.g., Mazda RX4, Ford Pantera L, etc.).
Sample: The 32 specific automobile models included in the dataset. This is a non-random sample, chosen by the magazine for testing and it is not necessarily representative of the entire population of cars.
Explanation of the variables:
# Create table
mtcars_info <- data.frame(
Variable = c("mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb"),
Unit = c("Miles per US gallon","Number of cylinders","Displacement (cu.in.)",
"Gross horsepower","Rear axle ratio","Weight (1000 lbs)","1/4 mile time",
"Engine type","Transmission","Number of forward gears","Number of carburetors"),
Numeric_Categorical = c("Ratio","Ordinal/Discrete","Ratio","Ratio","Ratio","Ratio","Ratio",
"Binary","Binary","Ordinal","Ordinal"))
# View table
mtcars_info
## Variable Unit Numeric_Categorical
## 1 mpg Miles per US gallon Ratio
## 2 cyl Number of cylinders Ordinal/Discrete
## 3 disp Displacement (cu.in.) Ratio
## 4 hp Gross horsepower Ratio
## 5 drat Rear axle ratio Ratio
## 6 wt Weight (1000 lbs) Ratio
## 7 qsec 1/4 mile time Ratio
## 8 vs Engine type Binary
## 9 am Transmission Binary
## 10 gear Number of forward gears Ordinal
## 11 carb Number of carburetors Ordinal
The variables are explained in the table format above for better readability (I used function data.frame to create a table,I had to enter each value of the table seperatly, making sure that the order of the data is right)
mydata$TransmissionFactor <- factor(mydata$am,
levels = c(0, 1),
labels = c("Automatic", "Manual"))
head(mydata, 3)
## mpg cyl disp hp drat wt qsec vs am gear carb TransmissionFactor
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4 Manual
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4 Manual
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1 Manual
Here I created a new factor variable, so that it is easier to tell, what transmission each car has (automatic or manual). I converted numeric into a categorical variable (factor)
mydata$Efficiency <- mydata$mpg / mydata$hp
mydata$Car <- rownames(mtcars)
mydata[1:3, c("Car", "Efficiency")]
## Car Efficiency
## Mazda RX4 Mazda RX4 0.1909091
## Mazda RX4 Wag Mazda RX4 Wag 0.1909091
## Datsun 710 Datsun 710 0.2451613
Here I created a completely new variable (ratio) called “Efficiency ratio”.
It shows us miles per gallon per unit of horsepower. The idea is to see, how fuel-efficient the car is relative to engine power.
I also created a new variable “Car”, so I could display just car name with the newly created Efficiency ration in the table
# Map old variable names to readable names
readable_names <- c("Miles per Gallon", "Horsepower", "Weight (1000 lbs)")
original_vars <- c("mpg", "hp", "wt")
# Create descriptive statistics table
descriptive_stats <- data.frame(
Variable = readable_names,
Mean = round(sapply(mydata[, original_vars], mean), 3),
Median = round(sapply(mydata[, original_vars], median), 3),
SD = round(sapply(mydata[, original_vars], sd), 3),
Q1 = round(sapply(mydata[, original_vars], function(x) quantile(x, 0.25)), 3),
Q3 = round(sapply(mydata[, original_vars], function(x) quantile(x, 0.75)), 3),
Min = round(sapply(mydata[, original_vars], min), 3),
Max = round(sapply(mydata[, original_vars], max), 3))
descriptive_stats
## Variable Mean Median SD Q1 Q3 Min Max
## mpg Miles per Gallon 20.091 19.200 6.027 15.425 22.80 10.400 33.900
## hp Horsepower 146.688 123.000 68.563 96.500 180.00 52.000 335.000
## wt Weight (1000 lbs) 3.217 3.325 0.978 2.581 3.61 1.513 5.424
First I created “original_vars” and “readable_names” to make the table more readable.
This code creates a descriptive statistics table for selected variables in mydata. It uses sapply() to apply functions like mean(), median(), sd(), min(), max(), and quantile() to each variable, and round(…, 3) to round results to three decimals. Finally, data.frame() organizes all the statistics into a neat table with readable variable names.
I choose to analyse three different variables. Here are the explanations for selected values:
Mean (20.091 for Miles per Gallon) – The average fuel efficiency across all cars in the sample is 20.091 mpg
Median (123.000 for Horsepower) – The median horsepower is 123. This means that half of the cars have engine power below (or equal) to123 hp and half have above 123 hp.
Standard Deviation (65.563 for Horsepower) – The standard deviation of 65.563 shows that horsepower values are quite spread out around the mean. A higher SD indicates greater variability in engine power across the sample of cars.
Q1 (2.581) and Q3 (3.610 for Weight) – The 1st quartile (2.581) means that 25% of cars weigh less (or equal) than 2.581 (1000 lbs), and the 3rd quartile (3.610) means that 75% of cars weigh less than 3.610 (1000 lbs).
Minimum (10.400) and Maximum (33.900 for Miles per Gallon) – The lightest and heaviest fuel efficiency values in the sample are 10.4 and 33.9 mpg
# Histogram for Miles per Gallon
hist(mydata$mpg,
main = "Distribution of Miles per Gallon",
xlab = "Miles per Gallon",
col = "skyblue",
breaks = 10)
# Histogram for Horsepower
hist(mydata$hp,
main = "Distribution of Horsepower",
xlab = "Horsepower",
col = "lightgreen",
breaks = 10)
# Histogram for Weight
hist(mydata$wt,
main = "Distribution of Car Weight",
xlab = "Weight (1000 lbs)",
col = "lightcoral",
breaks = 10)
Miles per Gallon (mpg): The distribution of mpg is right-skewed, with most cars achieving between 15 and 22 mpg.
Horsepower (hp): Horsepower is left-skewed, with the majority of cars concentrated between 75 and 175 hp.
Weight (wt): Car weight is unimodal and slightly right-skewed, with most vehicles weighing between 2.5 and 3.5 thousand pounds.
# Scatterplot: Horsepower vs. Weight
plot(mydata$wt, mydata$hp,
main = "Horsepower vs. Weight",
xlab = "Weight (1000 lbs)",
ylab = "Horsepower",
pch = 19, col = "darkblue")
# Fit linear regression
model_hp <- lm(hp ~ wt, data = mydata)
# Add regression line
abline(model_hp, col = "red", lwd = 2)
# Scatterplot: Miles per Gallon vs. Weight with regression line
plot(mydata$wt, mydata$mpg,
main = "MPG vs. Weight",
xlab = "Weight (1000 lbs)",
ylab = "Miles per Gallon",
pch = 19, col = "darkred")
# Fit linear regression
model <- lm(mpg ~ wt, data = mydata)
# Add regression line
abline(model, col = "blue", lwd = 2)
Horsepower vs. Weight: Heavier cars show a moderate positive correlation with horsepower, since larger engines are needed to power heavier vehicles.
MPG vs. Weight: There is a strong negative correlation, as heavier cars consume more fuel and thus achieve lower miles per gallon.
library(readxl)
mydata2 <- read_xlsx("./Business School.xlsx")
mydata2 <- as.data.frame(mydata2)
head(mydata2)
## Student ID Undergrad Degree Undergrad Grade MBA Grade Work Experience Employability (Before)
## 1 1 Business 68.4 90.2 No 252
## 2 2 Computer Science 70.2 68.7 Yes 101
## 3 3 Finance 76.4 83.3 No 401
## 4 4 Business 82.6 88.7 No 287
## 5 5 Finance 76.9 75.4 No 275
## 6 6 Computer Science 83.3 82.1 No 254
## Employability (After) Status Annual Salary
## 1 276 Placed 111000
## 2 119 Placed 107000
## 3 462 Placed 109000
## 4 342 Placed 148000
## 5 347 Placed 255500
## 6 313 Placed 103500
Population: The population of interest is all MBA students at this business school
Unit: The unit of observation is an individual student.
Variables:
variables <- data.frame(
Variable = c("Student ID", "Undergrad Degree", "Undergrad Grade", "MBA Grade",
"Work Experience", "Employability (Before)", "Employability (After)",
"Status", "Annual Salary"),
Description = c("Identifier of each student",
"Undergraduate field of study (Business, Finance, etc.)",
"Undergraduate grade (%)",
"MBA grade (%)",
"Whether the student had prior work experience (Yes/No)",
"Employability score before MBA",
"Employability score after MBA",
"Placement outcome (Placed / Not placed)",
"Salary after MBA (currency units)"),
Scale = c("Nominal",
"Nominal",
"Ratio (continuous)",
"Ratio (continuous)",
"Nominal (binary)",
"Ratio (continuous)",
"Ratio (continuous)",
"Nominal (binary)",
"Ratio (continuous)")
)
variables
## Variable Description Scale
## 1 Student ID Identifier of each student Nominal
## 2 Undergrad Degree Undergraduate field of study (Business, Finance, etc.) Nominal
## 3 Undergrad Grade Undergraduate grade (%) Ratio (continuous)
## 4 MBA Grade MBA grade (%) Ratio (continuous)
## 5 Work Experience Whether the student had prior work experience (Yes/No) Nominal (binary)
## 6 Employability (Before) Employability score before MBA Ratio (continuous)
## 7 Employability (After) Employability score after MBA Ratio (continuous)
## 8 Status Placement outcome (Placed / Not placed) Nominal (binary)
## 9 Annual Salary Salary after MBA (currency units) Ratio (continuous)
As in the Task 1, I presented the variables in a table format, so it is more easily readable.
library(ggplot2)
##
## Attaching package: 'ggplot2'
## The following objects are masked from 'package:psych':
##
## %+%, alpha
ggplot(mydata2, aes(x = `Undergrad Degree`)) +
geom_bar(fill = "steelblue") +
labs(title = "Distribution of Undergraduate Degrees",
x = "Undergraduate Degree",
y = "Count")
Business (n=35 students) is the most common undergraduate degree.
# Descriptive statistics
summary(mydata2$`Annual Salary`)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 20000 87125 103500 109058 124000 340000
# Histogram of Annual Salary
library(ggplot2)
ggplot(mydata2, aes(x = `Annual Salary`)) +
geom_histogram(binwidth = 20000, fill = "steelblue", color = "white", alpha = 0.8) +
labs(title = "Distribution of Annual Salary",
x = "Annual Salary",
y = "Count") +
theme_minimal(base_size = 13)
The average annual salary of the students is about 109,000.
The median annual salary is slightly lower, at 103,500, indicating a right-skewed distribution.
Most students earn between 87,000 (Q1) and 124,000 (Q3).
The lowest reported salary is 20,000, while the highest reaches 340,000.
The large difference between the mean and maximum suggests the presence of a few outliers with very high salaries.
t.test(mydata2$`MBA Grade`, mu = 74)
##
## One Sample t-test
##
## data: mydata2$`MBA Grade`
## t = 2.6587, df = 99, p-value = 0.00915
## alternative hypothesis: true mean is not equal to 74
## 95 percent confidence interval:
## 74.51764 77.56346
## sample estimates:
## mean of x
## 76.04055
H₀: μ(MBA Grade) = 74
H₁: μ(MBA Grade) ≠ 74
We can reject the H0 at p = 0.009, concluding that the mean MBA grade (M = 76.04) is significantly higher than 74. The effect size (Cohen’s d ≈ 0.27) is small, indicating the difference is statistically significant but practically modest.
library(readxl)
mydata3 <- read_xlsx("./ApartmentsV2.xlsx")
mydata3 <- as.data.frame(mydata3)
head(mydata3)
## Age Distance Price Parking Balcony
## 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:
# Convert categorical variables into factors
mydata3$ParkingFactor <- factor(mydata3$Parking,
levels = c(0, 1),
labels = c("No", "Yes"))
mydata3$BalconyFactor <- factor(mydata3$Balcony,
levels = c(0, 1),
labels = c("No", "Yes"))
# Check structure
str(mydata3)
## 'data.frame': 85 obs. of 7 variables:
## $ Age : num 7 18 7 28 18 28 14 18 22 25 ...
## $ Distance : num 28 1 28 29 18 12 20 6 7 2 ...
## $ Price : num 1640 2800 1660 1850 1640 1770 1850 1970 2270 2570 ...
## $ Parking : num 0 1 0 0 1 0 0 1 1 1 ...
## $ Balcony : num 1 0 0 1 1 1 1 1 0 0 ...
## $ ParkingFactor: Factor w/ 2 levels "No","Yes": 1 2 1 1 2 1 1 2 2 2 ...
## $ BalconyFactor: Factor w/ 2 levels "No","Yes": 2 1 1 2 2 2 2 2 1 1 ...
Here I changed the categorical variables into numerical, so we can use them in the regression model.
t.test(mydata3$Price, mu = 1900)
##
## One Sample t-test
##
## data: mydata3$Price
## t = 2.9022, df = 84, p-value = 0.004731
## alternative hypothesis: true mean is not equal to 1900
## 95 percent confidence interval:
## 1937.443 2100.440
## sample estimates:
## mean of x
## 2018.941
h0: μ(Price) = 1900 € h1: μ(Price) ≠ 1900 €
Since p < 0.05, we reject H₀ at p=0,00431. The average apartment price is significantly different from 1900 EUR. The 95% confidence interval suggests that the true mean price is between about 1937 and 2100 EUR, therefore higher than 1900 EUR.
# Fit simple linear regression model: Price ~ Age
fit1 <- lm(Price ~ Age, data = mydata3)
# View regression summary
summary(fit1)
##
## Call:
## lm(formula = Price ~ Age, data = mydata3)
##
## 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
# Compute correlation between Price and Age
cor(mydata3$Price, mydata3$Age)
## [1] -0.230255
Regression coefficient: The slope is –8.98, which means that if age increases by 1 year, the apartment price decreases on average by about 8,975 EUR. The coefficient is statistically significant at p = 0,034.
Coefficient of correlation (r = –0.23): There is a weak negative linear relationship between age and price.
Coefficient of determination (R² = 0.053): Only about 5.3% of the variation in price is explained by the age of the apartment. The explanatory power of the model is low.
library(car)
## Loading required package: carData
##
## Attaching package: 'car'
## The following object is masked from 'package:psych':
##
## logit
# Clean up column names (removes spaces if any)
names(mydata3) <- trimws(names(mydata3))
# Now plot scatterplot matrix
scatterplotMatrix(
mydata3[, c("Price", "Age", "Distance")],
main = "Scatterplot Matrix: Price, Age and Distance",
smooth = FALSE)
The scatteprlot matrix shows us a (strong) linear negative relationship between Price and Age. The relationship between Price and Age is less linear and not as strong, but it is still negative,
There is also no clear relation between Age and Distance. Thus, there is no potential multicollinearity problem. To further support this statement, I will run another analysis!.
# Load the required package
library(Hmisc)
##
## Attaching package: 'Hmisc'
## The following object is masked from 'package:psych':
##
## describe
## The following objects are masked from 'package:base':
##
## format.pval, units
# Run rcorr on numeric matrix
rcorr(as.matrix(mydata3[ , c(-4,-5,-6,-7)]))
## Age Distance Price
## Age 1.00 0.04 -0.23
## Distance 0.04 1.00 -0.63
## Price -0.23 -0.63 1.00
##
## n= 85
##
##
## P
## Age Distance Price
## Age 0.6966 0.0340
## Distance 0.6966 0.0000
## Price 0.0340 0.0000
The correlation analysis shows no multicollinearity among predictors, as all correlations are well below |0.9|. Distance has a semi-strong negative correlation with Price (r = -0.63). Age shows a weak negative correlation (r = -0.23). Overall, Distance is the most important predictors, and multicollinearity is not a concern.
# Multiple regression: Price ~ Age + Distance
fit2 <- lm(Price ~ Age + Distance, data = mydata3)
# Show summary
summary(fit2)
##
## Call:
## lm(formula = Price ~ Age + Distance, data = mydata3)
##
## 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
Explanation of the output:
The intercept (2460 EUR) is the expected price when age and distance are both zero.
If age increases by 1 year, the price decreases on average by 7,9 EUR, holding distance constant (ceteris paribus).
If distance increases by 1 km, the price decreases on average by 20,7 EUR, holding age constant (ceteris paribus)
Both age (p = 0,016) and distance (p < 0,001) are statistically significant predictors.
The model explains about 44% of the variation in prices (R² = 0,44).
library(car)
# Variance Inflation Factors for fit2
vif(fit2)
## Age Distance
## 1.001845 1.001845
# Calculating the average VIF
mean(vif(fit2))
## [1] 1.001845
The VIF values for both Age and Distance are about 1,00, which is the minimum possible value. This means that the explanatory variables are completely independent of each other, and there is no multicollinearity problem in the regression model. Therefore also average VIF is close to one, which is a desirable outcome.
mydata3$StdResid <- round(rstandard(fit2), 3) #Standardized residuals
mydata3$CooksD <- round(cooks.distance(fit2), 3) #Cooks distances
hist(mydata3$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals")
shapiro.test(mydata3$StdResid)
##
## Shapiro-Wilk normality test
##
## data: mydata3$StdResid
## W = 0.95303, p-value = 0.003645
Upon visual inspection, we can see that residuals are slightly asymmetrically distributed to the right. Since all the values are between -3 and +3, we have no problem with outliers. This would be the problem for datasets with less then 30 units.
To also formally test the distribution, we need to use the Shapiro-Wilks normality test. The p-value is less then 0,05, therefore we can reject the null hypothesis that the errors are normally distributed. Therefore, it means that the errors in the population are most likely not normally distributed, as is required by the assumption.
hist(mydata3$CooksD <- round(cooks.distance(fit2), 3),
xlab = "Cooks distance",
ylab = "Frequency",
main = "Histogram of Cooks distances")
To study Cook’s distance, we first display the histogram of Cook’s distances. We observe that there are some units with the large gap inbetween them. They are most probably the units with high impact, so we will remove them.
head(mydata3[order(-mydata3$CooksD),], 6) #Six units with highest value of Cooks distance
## Age Distance Price Parking Balcony ParkingFactor BalconyFactor StdResid CooksD
## 38 5 45 2180 1 1 Yes Yes 2.577 0.320
## 55 43 37 1740 0 0 No No 1.445 0.104
## 33 2 11 2790 1 0 Yes No 2.051 0.069
## 53 7 2 1760 0 1 No Yes -2.152 0.066
## 22 37 3 2540 1 1 Yes Yes 1.576 0.061
## 39 40 2 2400 0 1 No Yes 1.091 0.038
To identify this units, I showed the top six units with the highest Cooks distance. The unit with the highest Cook’s distance is the unit 38 (the only one with price 2180!). In addition to that, unit 55 (the only one with unique price of 1740), also has a slighlty higer value, so I decided to remove this one as well.
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:Hmisc':
##
## src, summarize
## The following object is masked from 'package:car':
##
## recode
## The following objects are masked from 'package:pastecs':
##
## first, last
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
mydata3 <- mydata3 %>%
filter(!Price %in% c(2180, 1740))
After identifying the units, I removed them using the inverted filter function (!). Again, because this is the only unit with price=2180, I could filter by that variable. I did the same for unit with price 1740. Again, I could only do this, because I checked that these were the only units with that price, so I used the price as a unique identifying value.
Now I need to recheck Cook’s distance an reevaluate the regression model.
# Reevaluatin the regression model
fit2 <- lm(Price ~ Age + Distance, data = mydata3)
# Rechecking residuals and Cook's distance
mydata3$StdResid <- round(rstandard(fit2), 3) #Standardized residuals
mydata3$CooksD <- round(cooks.distance(fit2), 3) #Cooks distances
head(mydata3[order(mydata3$StdResid),], 3) #Three units with lowest value of stand. residuals
## Age Distance Price Parking Balcony ParkingFactor BalconyFactor StdResid CooksD
## 52 7 2 1760 0 1 No Yes -2.346 0.084
## 13 12 14 1650 0 1 No Yes -1.515 0.014
## 70 12 14 1650 0 0 No No -1.515 0.014
head(mydata3[order(-mydata3$CooksD),], 6) #Six units with highest value of Cooks distance
## Age Distance Price Parking Balcony ParkingFactor BalconyFactor StdResid CooksD
## 33 2 11 2790 1 0 Yes No 2.173 0.084
## 52 7 2 1760 0 1 No Yes -2.346 0.084
## 22 37 3 2540 1 1 Yes Yes 1.566 0.065
## 25 8 26 2300 1 1 Yes Yes 1.853 0.054
## 31 45 21 1910 0 1 No Yes 1.073 0.050
## 38 40 2 2400 0 1 No Yes 1.044 0.038
hist(mydata3$CooksD <- round(cooks.distance(fit2), 3),
xlab = "Cooks distance",
ylab = "Frequency",
main = "Histogram of Cooks distances")
After running the Cook’s distance histogram again, there were now more units coming out as units with large impact. I decided to remove two more units (units with Cook’s Distance of 0,084 - after second calcualtion)
library(dplyr)
mydata3 <- mydata3 %>%
filter(!Price %in% c(2790, 1760))
After removing the two more units, I will check Cook’s distances again.
fit2 <- lm(Price ~ Age + Distance, data = mydata3) # refit after any filtering
cd <- round(cooks.distance(fit2), 3)
mydata3$CooksD <- cd # lengths now match
hist(mydata3$CooksD, xlab="Cook's distance", ylab="Frequency",
main="Histogram of Cook's distances")
Now there is finally no more breaks in the histogram, so I can proceede with the exercise.
# Reevaluatin the regression model
fit2 <- lm(Price ~ Age + Distance, data = mydata3)
mydata3$StdFitted <- scale(fit2$fitted.values)
library(car)
scatterplot(y = mydata3$StdResid, x = mydata3$StdFitted,
ylab = "Standardized residuals",
xlab = "Standardized fitted values",
boxplots = FALSE,
regLine = FALSE,
smooth = FALSE)
# Load the olsrr package
library(olsrr)
##
## Attaching package: 'olsrr'
## The following object is masked from 'package:datasets':
##
## rivers
# Perform Breusch-Pagan test for heteroskedasticity
ols_test_breusch_pagan(fit2)
##
## Breusch Pagan Test for Heteroskedasticity
## -----------------------------------------
## Ho: the variance is constant
## Ha: the variance is not constant
##
## Data
## ---------------------------------
## Response : Price
## Variables: fitted values of Price
##
## Test Summary
## ----------------------------
## DF = 1
## Chi2 = 1.64762
## Prob > Chi2 = 0.1992832
Because I removed another unit with strong impact, I first refitted the model.
H0: The error variance is constant (homoskedasticity). H1: The error variance is not constant (heteroskedasticity).
Result: The Breusch–Pagan test yields χ² = 1.65, p = 0.199 > 0.05, so we cannot to reject H₀ and conclude there is no evidence of heteroskedasticity. I will thus assume homoskedasticity.
Graphical check: The residual–fitted plot shows random scatter without a clear funnel shape, supporting the conclusion that variance appears roughly constant.
mydata3$StdResid <- round(rstandard(fit2), 3)
mydata3$CooksD <- round(cooks.distance(fit2), 3)
hist(mydata3$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals")
shapiro.test(mydata3$StdResid)
##
## Shapiro-Wilk normality test
##
## data: mydata3$StdResid
## W = 0.94297, p-value = 0.001286
H0: The standardized residuals are normally distributed. H1: The standardized residuals are not normally distributed.
The histogram of standardized residuals looks roughly symmetric but not perfectly bell-shaped. The formal Shapiro–Wilk test confirms this: W=0.943, p=0.0013. Since the p-value is below 0.0, we reject the null hypothesis of normality. This means the standardized residuals are not normally distributed**, even though the histogram might visually suggest approximate symmetry.
fit2 <- lm(Price ~ Age + Distance, data = mydata3)
summary(fit2)
##
## Call:
## lm(formula = Price ~ Age + Distance, data = mydata3)
##
## Residuals:
## Min 1Q Median 3Q Max
## -408.72 -217.93 -38.86 220.60 506.18
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2492.715 75.544 32.997 < 2e-16 ***
## Age -7.496 3.169 -2.365 0.0205 *
## Distance -24.574 2.700 -9.100 6.9e-14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 259.4 on 78 degrees of freedom
## Multiple R-squared: 0.5324, Adjusted R-squared: 0.5204
## F-statistic: 44.4 on 2 and 78 DF, p-value: 1.335e-13
Explanation of the coeficients:
For every additional year of Age, the apartment price decreases on average by 7.5 EUR, holding Distance constant (ceteris paribus).
For every additional unit of Distance, the apartment price decreases on average by 24.6 EUR, holding Age constant (ceteris paribus).
The intercept (2492.7 EUR) is the expected price when Age and Distance are both zero.
Both predictors are statistically significant (p<0.05), meaning Age and Distance have a real effect on Price.
The model explains about 53% of the variation in prices (R2=0.53), which is a moderate fit.
The residual standard error (259.4) indicates the typical deviation of observed prices from the predicted values.
# Fit multiple linear regression model with factors
fit3 <- lm(Price ~ Age + Distance + ParkingFactor + BalconyFactor, data = mydata3)
# Show model summary
summary(fit3)
##
## Call:
## lm(formula = Price ~ Age + Distance + ParkingFactor + BalconyFactor,
## data = mydata3)
##
## Residuals:
## Min 1Q Median 3Q Max
## -403.16 -204.08 -41.24 239.52 528.62
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2372.693 93.344 25.419 < 2e-16 ***
## Age -6.906 3.118 -2.215 0.0298 *
## Distance -22.254 2.839 -7.837 2.25e-11 ***
## ParkingFactorYes 137.479 60.854 2.259 0.0267 *
## BalconyFactorYes 17.234 57.099 0.302 0.7636
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 254.3 on 76 degrees of freedom
## Multiple R-squared: 0.5621, Adjusted R-squared: 0.539
## F-statistic: 24.38 on 4 and 76 DF, p-value: 5.29e-13
I was careful to include the Factor variables and not the original categorical variabvles.
# Compare fit2 and fit3 using ANOVA
anova(fit2, fit3)
## Analysis of Variance Table
##
## Model 1: Price ~ Age + Distance
## Model 2: Price ~ Age + Distance + ParkingFactor + BalconyFactor
## Res.Df RSS Df Sum of Sq F Pr(>F)
## 1 78 5248925
## 2 76 4915944 2 332981 2.5739 0.08287 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
H0: Model fit3 does not fit the data better than fit2.
H1: Model fit3 fits the data better than fit2.
The null hypothesis states that adding Parking and Balcony does not improve the model fit compared to using only Age and Distance. The alternative hypothesis states that the extended model (fit3) explains more variation than the simpler model (fit2). The ANOVA result shows F=2.57,p=0.083. Since the p-value is above 0.05, we do not reject the null hypothesis, meaning fit3 does not significantly improve the fit over fit2, although there is weak evidence at the 10% level.
summary(fit3)
##
## Call:
## lm(formula = Price ~ Age + Distance + ParkingFactor + BalconyFactor,
## data = mydata3)
##
## Residuals:
## Min 1Q Median 3Q Max
## -403.16 -204.08 -41.24 239.52 528.62
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2372.693 93.344 25.419 < 2e-16 ***
## Age -6.906 3.118 -2.215 0.0298 *
## Distance -22.254 2.839 -7.837 2.25e-11 ***
## ParkingFactorYes 137.479 60.854 2.259 0.0267 *
## BalconyFactorYes 17.234 57.099 0.302 0.7636
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 254.3 on 76 degrees of freedom
## Multiple R-squared: 0.5621, Adjusted R-squared: 0.539
## F-statistic: 24.38 on 4 and 76 DF, p-value: 5.29e-13
Explanation of the output:
Apartments with Parking are priced about 137 EUR higher on average than those without, significant at the 5% level (p = 0.027), ceteris paribus (other variables unchanged)
Having a Balcony adds about 17 EUR on average, but this effect is not statistically significant (p = 0.764), ceteris paribus (other variables unchanged)
The model explains about 56% of the variation in apartment prices (Adjusted R² = 0.54).
F-test hypotheses:
H0: All regression coefficients (except the intercept) are equal to 0.
H1: At least one predictor has a non-zero effect on Price.
The highly significant F-statistic (F(4,76) = 24.38, p < 0.001) leads us to reject H₀, confirming that the model explains a substantial portion of the variation in Price.
# Save fitted values and residuals in the dataset
mydata3$fitted_fit3 <- fitted(fit3)
mydata3$residual_fit3 <- residuals(fit3)
# Show fitted value and residual for Apartment ID 2
mydata3[2, c("Price", "fitted_fit3", "residual_fit3")]
## Price fitted_fit3 residual_fit3
## 2 2800 2363.611 436.3894
The observed price of apartment ID 2 is 2800 EUR.
The fitted value from model fit3 is 2363.6 EUR.
The residual, calculated as observed minus fitted, is 436.4 EUR.
This means the model underestimates the price of apartment 2 by about 436 EUR.