mydata <- read.table("./Salary.csv",
header = TRUE,
sep = ",",
dec = ".")
head(mydata, 10)
## Age Gender Education.Level Job.Title Years.of.Experience Salary Country Race Senior
## 1 32 Male 1 Software Engineer 5 90000 UK White 0
## 2 28 Female 2 Data Analyst 3 65000 USA Hispanic 0
## 3 45 Male 3 Manager 15 150000 Canada White 1
## 4 36 Female 1 Sales Associate 7 60000 USA Hispanic 0
## 5 52 Male 2 Director 20 200000 USA Asian 0
## 6 29 Male 1 Marketing Analyst 2 55000 USA Hispanic 0
## 7 42 Female 2 Product Manager 12 120000 USA Asian 0
## 8 31 Male 1 Sales Manager 4 80000 China Korean 0
## 9 26 Female 1 Marketing Coordinator 1 45000 China Chinese 0
## 10 38 Male 3 Scientist 10 110000 Australia Australian 1
This dataset provides a collection of annual salary information from various industries and regions across the globe and includes details on job titles, education level, experience, geographic locations, race and level of positions of such annual salaries. The data set contains 6684 units of observations, and 9 variables.
Description of variables:
mydata$Education.LevelFactor <- factor(mydata$Education.Level,
levels = c(0, 1, 2, 3),
labels = c("High School", "Bachelor Degree", "Master Degree", "Phd"))
mydata$SeniorFactor <- factor(mydata$Senior,
levels = c(0, 1),
labels = c("Not senior", "Senior"))
After analyzing the data, I noticed some of the potential data entry errors, for example some annual salaries were as low as 350$. The annual salary of $350 is not realistic and thus suggests a data entry error. To ensure accurate analysis, I will create a cleaned data set (mydata2), excluding annual salaries below $5000.
mydata2 <- subset(mydata, Salary >= 5000) # Created a new dataset with condition: only annual salaries above 5,000
mydata2$SalaryK <- mydata2$Salary / 1000. # Created a new variable: Salary in thousands
colnames(mydata2)[6] <- "AnnualSalary" # Renamed a variable
head(mydata2[order(mydata2$AnnualSalary), c(-7, -8, -10, -11, -12) ], 6)
## Age Gender Education.Level Job.Title Years.of.Experience AnnualSalary Senior
## 3876 22 Male 0 Sales Associate 1 25000 0
## 3889 29 Female 0 Sales Associate 1 25000 0
## 3904 29 Female 0 Sales Associate 1 25000 0
## 3919 29 Female 0 Sales Associate 1 25000 0
## 3934 29 Female 0 Sales Associate 1 25000 0
## 3949 29 Female 0 Sales Associate 1 25000 0
mean(mydata2$AnnualSalary)
## [1] 115375.9
median(mydata2$AnnualSalary)
## [1] 115000
sd(mydata2$AnnualSalary)
## [1] 52747.8
Explanation of Sample Statistics:
summary(mydata2[c(-2, -3, -4, -7, -8, -9, -12)])
## Age Years.of.Experience AnnualSalary Education.LevelFactor SeniorFactor
## Min. :21.00 Min. : 0.000 Min. : 25000 High School : 436 Not senior:5721
## 1st Qu.:28.00 1st Qu.: 3.000 1st Qu.: 70000 Bachelor Degree:3018 Senior : 959
## Median :32.00 Median : 7.000 Median :115000 Master Degree :1858
## Mean :33.61 Mean : 8.081 Mean :115376 Phd :1368
## 3rd Qu.:38.00 3rd Qu.:12.000 3rd Qu.:160000
## Max. :62.00 Max. :34.000 Max. :250000
Explanation of Additional Sample Statistics:
Additionally, after summarizing the data I noticed some of the respondents had 0 years of experience (less than 0.5 years), we can assume they just started their job.
library(ggplot2)
ggplot(mydata2, aes(x = SalaryK)) +
geom_histogram(fill = "darkseagreen1",
color = "darkseagreen4",
bins = 10) +
labs(title = "Salary Distribution",
x = "Annual Salary (in thousands)",
y = "Frequency") +
theme_minimal(base_size = 14)
Results of the histogram:
boxplot(AnnualSalary ~ Gender,
data = mydata2,
main = "Salary by Gender",
ylab = "Annual Salary",
col = c("lightpink", "darkslategray3"),
border = c("lightpink3", "darkslategray4"))
Results of the boxplot:
ggplot(mydata2, aes(x=Years.of.Experience, y=SalaryK)) +
geom_point(color = "plum2") +
geom_smooth(method= "lm", se= FALSE, color="plum4") +
labs(title = "Experience vs Salary",
x="Years of Experience", y="Annual Salary (in thousands)") +
theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'
Results of the scatterplot:
library(readxl)
mydata3 <- read_xlsx("./Task 2/Business School.xlsx")
mydata3 <- as.data.frame(mydata3)
head(mydata3)
## Student ID Undergrad Degree Undergrad Grade MBA Grade Work Experience Employability (Before) Employability (After)
## 1 1 Business 68.4 90.2 No 252 276
## 2 2 Computer Science 70.2 68.7 Yes 101 119
## 3 3 Finance 76.4 83.3 No 401 462
## 4 4 Business 82.6 88.7 No 287 342
## 5 5 Finance 76.9 75.4 No 275 347
## 6 6 Computer Science 83.3 82.1 No 254 313
## Status Annual Salary
## 1 Placed 111000
## 2 Placed 107000
## 3 Placed 109000
## 4 Placed 148000
## 5 Placed 255500
## 6 Placed 103500
ggplot(mydata3, aes(x = `Undergrad Degree`)) +
geom_bar(color= "paleturquoise4", fill= "paleturquoise2") +
labs(title = "Distribution of Undergrad Degrees",
x = "Undergrad Degree",
y = "Frequency") +
theme_minimal() +
geom_text(stat = "count",
aes(label = ..count..),
vjust = -0.3)
The most common undergraduate degree is Business, with 35 out of 100 students in the data set.
This means more than one-third of the MBA students come from a Business background, while Art and Engineering are the least represented.
library(psych)
##
## Attaching package: 'psych'
## The following objects are masked from 'package:ggplot2':
##
## %+%, alpha
describe(mydata3$"Annual Salary")
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 100 109058 41501.49 103500 104600.2 25945.5 20000 340000 320000 2.22 9.41 4150.15
ggplot(mydata3, aes(x = `Annual Salary`)) +
geom_histogram(bins = 10, fill = "thistle2", color = "thistle4") +
scale_x_continuous(labels = scales::comma) +
labs(title = "Annual Salary Distribution",
x = "Annual Salary (USD per year)",
y = "Frequency") +
theme_minimal(base_size = 13)
The distribution of annual salaries is skewed to the right, with salaries concentrated around the median and a few high outliers up to 340,000. The average annual salary is about 109,058 USD, with most students earning between 80,000 and 130,000 USD.
t.test(mydata3$`MBA Grade`,
mu = 74,
alternative = "two.sided")
##
## One Sample t-test
##
## data: mydata3$`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
library(effectsize)
##
## Attaching package: 'effectsize'
## The following object is masked from 'package:psych':
##
## phi
effectsize::cohens_d(mydata3$`MBA Grade`, mu= 74)
## Cohen's d | 95% CI
## ------------------------
## 0.27 | [0.07, 0.46]
##
## - Deviation from a difference of 74.
Since p<0.05, we reject H0. The current MBA student’s average grade is significantly higher than 74. The effect size is small (d=0.27), meaning the difference is statistically significant (with n=100 students, the test has enough power to detect even small deviations).
effectsize::interpret_cohens_d(0.27, rules="sawilowsky2009")
## [1] "small"
## (Rules: sawilowsky2009)
As previously stated the effect size (Cohen’s d = 0.27) is small, meaning the grade difference is statistically significant but of limited practical importance.
library(readxl)
mydata4 <- read_xlsx("./Task 3/Apartments.xlsx")
mydata4 <- as.data.frame(mydata4)
head(mydata4)
## 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:
mydata4$ParkingFactor <- factor(mydata4$Parking,
levels = c(0, 1),
labels = c("No", "Yes"))
mydata4$BalconyFactor <- factor(mydata4$Balcony,
levels = c(0, 1),
labels = c("No", "Yes"))
head(mydata4)
## Age Distance Price Parking Balcony ParkingFactor BalconyFactor
## 1 7 28 1640 0 1 No Yes
## 2 18 1 2800 1 0 Yes No
## 3 7 28 1660 0 0 No No
## 4 28 29 1850 0 1 No Yes
## 5 18 18 1640 1 1 Yes Yes
## 6 28 12 1770 0 1 No Yes
t.test(mydata4$Price,
mu = 1900,
alternative = "two.sided")
##
## One Sample t-test
##
## data: mydata4$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
We can reject H0 at 5% significance level (since p < 0.05). The true average price is significantly different from 1900 EUR (it is grater than 1900, the mean Price is 2018.94 EUR). We can say that with 95% certainty the true average price of the apartments lies between 1937.44 and 2100.44 EUR.
fit1 <- lm(Price ~ Age,
data = mydata4)
summary(fit1)
##
## Call:
## lm(formula = Price ~ Age, data = mydata4)
##
## 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(mydata4$Price, mydata4$Age)
## [1] -0.230255
The regression coefficient for Age is -8.975. This means that, if the apartment gets 1 year older, its average price decreases by about 8.975 EUR per m², assuming all other variables remain the same. Since p = 0.034 (< 0.05), this negative effect is statistically significant.
Coefficient of correlation between Price and Age is -0.23. This indicates a weak negative relationship: as apartments get older, price tends to decline slightly.
Coefficient of determination (R²) equals to 0.053. This means that only about 5.3% of the variation in Price can be explained by the linear effect of Age. The rest 94.7% of the variation in Price can be explained by other factors not included in this regression.
library(car)
## Loading required package: carData
##
## Attaching package: 'car'
## The following object is masked from 'package:psych':
##
## logit
scatterplotMatrix(mydata4[ , c(-4, -5, -6, -7)],
smooth = FALSE)
The scatterplot matrix shows negative relationships between Price and both Age and Distance, while Age and Distance are not strongly related. Therefore, there is no potential problem with multicollinearity in this data set.
fit2 <- lm(Price ~ Age + Distance,
data = mydata4)
summary(fit2)
##
## Call:
## lm(formula = Price ~ Age + Distance, data = mydata4)
##
## 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
The regression shows that both Age and Distance significantly reduce Price.
The model explains 44% of Price variation (R²=0.44) and is statistically significant overall (p<0.001).
library(car)
vif(fit2)
## Age Distance
## 1.001845 1.001845
The VIF values for Age and Distance are about 1, well below 5, meaning there is no multicollinearity issue in the model.
mydata4$StdResid <- round(rstandard(fit2), 3)
head(mydata4[order(mydata4$StdResid),], 5)
## Age Distance Price Parking Balcony ParkingFactor BalconyFactor StdResid
## 53 7 2 1760 0 1 No Yes -2.152
## 13 12 14 1650 0 1 No Yes -1.499
## 72 12 14 1650 0 0 No No -1.499
## 20 13 8 1800 0 0 No No -1.381
## 35 14 16 1660 0 1 No Yes -1.261
hist(mydata4$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals",
col = "rosybrown1",
border = "brown4")
The histogram of standardized residuals is roughly centered around zero, with most values between -2 and 2. This suggests no extreme outliers or major deviations.
mydata4$CooksD <- round(cooks.distance(fit2), 3)
head(mydata4[order(-mydata4$CooksD),], 5)
## 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
hist(mydata4$CooksD,
xlab = "Cooks distance",
ylab = "Frequency",
main = "Histogram of Cooks distances",
col = "thistle1",
border = "orchid4")
After observing both, the histogram of standardized residuals and the histogram of Cooks distances, we can observe a unit with a notecably larger Cooks distance than other units. This unit has thus has a stronger influence on the regression model. It is the 38th unit in the data set, its standardized residual is 2.577 (close to 3), which suggests we should remove it.
mydata4$StdFitted <- scale(fit2$fitted.values)
library(car)
scatterplot(x = mydata4$StdFitted, y = mydata4$StdRes,
xlab = "Standardized fitted values",
ylab = "Standardized Residuals",
boxplots = FALSE,
regLine = FALSE,
smooth = FALSE)
The scatterplot shows that the points are spread randomly without a clear shape or pattern. This suggests no heteroskedasticity problem, as the variance of residuals looks roughly constant.
library(olsrr)
##
## Attaching package: 'olsrr'
## The following object is masked from 'package:datasets':
##
## rivers
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 = 0.968106
## Prob > Chi2 = 0.325153
To formally test for Heteroskedasticity, I used the Breusch Pagan Test:
As the p-value equals to 0.087 (p>0.05) we fail to reject H0. This means that there is no significant evidence of heteroskedasticity.
Both the scatterplot and statistical test indicate that the homoscedasticity assumption holds.
hist(mydata4$StdResid,
xlab = "Standardized residuals",
ylab = "Frequency",
main = "Histogram of standardized residuals",
col = "rosybrown1",
border = "brown4")
shapiro.test(mydata4$StdResid)
##
## Shapiro-Wilk normality test
##
## data: mydata4$StdResid
## W = 0.95303, p-value = 0.003645
The histogram shows that residuals are not normally distributed. The Shapiro-Wilk test confirms this: at the 5% level, we reject H0 (p< 0.05), so the distribution is not normal.
mydata4 <- mydata4 [!(mydata4$CooksD == 0.320), ]
fit2 <- lm(Price ~ Age + Distance,
data = mydata4)
summary(fit2)
##
## Call:
## lm(formula = Price ~ Age + Distance, data = mydata4)
##
## Residuals:
## Min 1Q Median 3Q Max
## -604.92 -229.63 -56.49 192.97 599.35
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2456.076 73.931 33.221 < 2e-16 ***
## Age -6.464 3.159 -2.046 0.044 *
## Distance -22.955 2.786 -8.240 2.52e-12 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 276.1 on 81 degrees of freedom
## Multiple R-squared: 0.4838, Adjusted R-squared: 0.4711
## F-statistic: 37.96 on 2 and 81 DF, p-value: 2.339e-12
sqrt(summary(fit2)$r.squared)
## [1] 0.6955609
Both Age and Distance negatively affect price. Distance has the stronger effect, and the model explains nearly half of the price variation.
fit3 <- lm(Price ~ Age + Distance + ParkingFactor + BalconyFactor,
data = mydata4)
summary(fit3)
##
## Call:
## lm(formula = Price ~ Age + Distance + ParkingFactor + BalconyFactor,
## data = mydata4)
##
## Residuals:
## Min 1Q Median 3Q Max
## -473.21 -192.37 -28.89 204.17 558.77
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2329.724 93.066 25.033 < 2e-16 ***
## Age -5.821 3.074 -1.894 0.06190 .
## Distance -20.279 2.886 -7.026 6.66e-10 ***
## ParkingFactorYes 167.531 62.864 2.665 0.00933 **
## BalconyFactorYes -15.207 59.201 -0.257 0.79795
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 267.5 on 79 degrees of freedom
## Multiple R-squared: 0.5275, Adjusted R-squared: 0.5035
## F-statistic: 22.04 on 4 and 79 DF, p-value: 3.018e-12
To shortly summarize, Distance lowers price, Parking raises it, Age has a weak effect, and having Balconies does not really have a significant effect on Price.
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 81 6176767
## 2 79 5654480 2 522287 3.6485 0.03051 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
ANOVA tests whether model improvements are significant.
Since p = 0.0305 (p < 0.05), we reject H0 and conclude that fit3, with more variables, provides a better fit.
summary(fit3)
##
## Call:
## lm(formula = Price ~ Age + Distance + ParkingFactor + BalconyFactor,
## data = mydata4)
##
## Residuals:
## Min 1Q Median 3Q Max
## -473.21 -192.37 -28.89 204.17 558.77
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2329.724 93.066 25.033 < 2e-16 ***
## Age -5.821 3.074 -1.894 0.06190 .
## Distance -20.279 2.886 -7.026 6.66e-10 ***
## ParkingFactorYes 167.531 62.864 2.665 0.00933 **
## BalconyFactorYes -15.207 59.201 -0.257 0.79795
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 267.5 on 79 degrees of freedom
## Multiple R-squared: 0.5275, Adjusted R-squared: 0.5035
## F-statistic: 22.04 on 4 and 79 DF, p-value: 3.018e-12
Age (-5.821, p=0.0619). This means that older properties tend to be cheaper, but effect is only marginally significant.
Distance (-20.279, p<0.001). Each additional unit farther reduces price by about 20, which is strongly significant.
ParkingFactorYes (167.531, p<0.01). This suggests that when the apartment has a Parking, this increases price by 167.531 EUR; statistically significant.
BalconyFactorYes (-15.207, p=0.79795). This suggests that apartment having Balcony has almost no effect; not significant.
Model fit:
To summarize, Distance and Parking strongly affect price, Age has a weak effect, and Balcony is not significant. The overall model is a good fit.
mydata4$FittedValues <- fitted.values(fit3)
mydata4$Residuals <- residuals(fit3)
head(mydata4[ , colnames(mydata4) %in% c("Price", "Balcony", "FittedValues", "Residuals")])
## Price Balcony FittedValues Residuals
## 1 1640 1 1705.952 -65.95206
## 2 2800 0 2372.197 427.80292
## 3 1660 0 1721.159 -61.15894
## 4 1850 1 1563.431 286.56890
## 5 1640 1 2012.244 -372.24396
## 6 1770 1 1908.177 -138.17733
For apartment ID2:
The actual price of the apartment is greater by 427.803 EUR than the predicted (fitted) price from the regression. This means that the model underestimated the price of apartment ID2 by 427.803 EUR.