Reproducible report
library(readxl)
df <- read_excel("german_credit_data_formatted-2.xlsx")
df <- as.data.frame(df)
The dim() command helps count the number of rows and columns in a dataset. The names() command returns the column names.
dim(df)
## [1] 1000 10
#summary(df)
str(df)
## 'data.frame': 1000 obs. of 10 variables:
## $ ...1 : num 0 1 2 3 4 5 6 7 8 9 ...
## $ Age : num 67 22 49 45 53 35 53 35 61 28 ...
## $ Sex : chr "male" "female" "male" "male" ...
## $ Job : num 2 2 1 2 2 1 2 3 1 3 ...
## $ Housing : chr "own" "own" "own" "free" ...
## $ Saving accounts : chr NA "little" "little" "little" ...
## $ Checking account: chr "little" "moderate" NA "little" ...
## $ Credit amount : num 1169 5951 2096 7882 4870 ...
## $ Duration : num 6 48 12 42 24 36 24 36 12 30 ...
## $ Purpose : chr "radio/TV" "radio/TV" "education" "furniture/equipment" ...
names(df)
## [1] "...1" "Age" "Sex" "Job"
## [5] "Housing" "Saving accounts" "Checking account" "Credit amount"
## [9] "Duration" "Purpose"
sapply(df,class)
## ...1 Age Sex Job
## "numeric" "numeric" "character" "numeric"
## Housing Saving accounts Checking account Credit amount
## "character" "character" "character" "numeric"
## Duration Purpose
## "numeric" "character"
df <- df[ , -1 ]
head(df)
## Age Sex Job Housing Saving accounts Checking account Credit amount
## 1 67 male 2 own <NA> little 1169
## 2 22 female 2 own little moderate 5951
## 3 49 male 1 own little <NA> 2096
## 4 45 male 2 free little little 7882
## 5 53 male 2 free little little 4870
## 6 35 male 1 free <NA> <NA> 9055
## Duration Purpose
## 1 6 radio/TV
## 2 48 radio/TV
## 3 12 education
## 4 42 furniture/equipment
## 5 24 car
## 6 36 education
Identify inconsistencies, duplicate records, and inaccurate data entries.
Provide a clear explanation of how these errors were detected and rectified.
Since this data does not have customer ID codes, we check for duplicated records by comparing observations with each other using the duplicated() command. The result shows that these 1000 records are not duplicated.
table(duplicated(df))
##
## FALSE
## 1000
To check for inaccurate data entries, we use the table() command for character columns and the range() command for numeric columns.
table(df$Age, useNA = "always")
##
## 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
## 2 14 14 27 48 44 41 50 51 43 37 40 38 34 33 32
## 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
## 40 39 29 24 21 25 17 22 17 17 15 18 17 12 14 12
## 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
## 8 9 7 10 8 3 9 5 3 6 7 2 8 5 5 5
## 67 68 70 74 75 <NA>
## 3 3 1 4 2 0
range(df$Age) # kết quả này phù hợp với nhóm khách hàng credit
## [1] 19 75
table(df$Sex, useNA = "always")
##
## female male <NA>
## 310 690 0
table(df$Job, useNA = "always")
##
## 0 1 2 3 <NA>
## 22 200 630 148 0
table(df$`Saving accounts`, useNA = "always")
##
## little moderate quite rich rich <NA>
## 603 103 63 48 183
# table(df$`Credit amount`)
summary(df$`Credit amount`)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 250 1366 2320 3271 3972 18424
summary(df$Duration)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 4.0 12.0 18.0 20.9 24.0 72.0
range(df$Duration)
## [1] 4 72
Identify missing data points and determine the best strategy to address them.
In the ‘Saving accounts’ column, there are 183 observations with no NA information.
table(is.na(df$`Saving accounts`))
##
## FALSE TRUE
## 817 183
sapply(X = df,
FUN = function(x){ length(which(is.na(x))) } )
## Age Sex Job Housing
## 0 0 0 0
## Saving accounts Checking account Credit amount Duration
## 183 394 0 0
## Purpose
## 0
Discuss the methods used (e.g., mean/median imputation, forward-fill, or removal of affected records).
In the ‘Saving accounts’ column, there are 183 NA values. To fill in the missing values, we use a probabilistic method based on the proportion of the remaining values.
saving_account <- na.omit(df$`Saving accounts`)
attributes(saving_account)$na.action <- NULL
sample(x = saving_account,
size = 183,
replace = TRUE) -> saving_account_sim
prop.table(table(df$`Saving accounts`))
##
## little moderate quite rich rich
## 0.73806610 0.12607099 0.07711138 0.05875153
prop.table(table(saving_account_sim))
## saving_account_sim
## little moderate quite rich rich
## 0.72677596 0.10928962 0.10928962 0.05464481
df$`Saving accounts`[which(is.na(df$`Saving accounts`))] <- saving_account_sim
Checking account
checking_account <- na.omit(df$`Checking account`)
attributes(checking_account)$na.action <- NULL
sample(x = checking_account,
size = 394,
replace = TRUE) -> checking_account_sim
prop.table(table(df$`Checking account`))
##
## little moderate rich
## 0.4521452 0.4438944 0.1039604
prop.table(table(checking_account_sim))
## checking_account_sim
## little moderate rich
## 0.4543147 0.4187817 0.1269036
df$`Checking account`[which(is.na(df$`Checking account`))] <- checking_account_sim
Justify why your chosen approach is appropriate for this dataset.
Identify potential outliers that may distort the analysis.
Describe the method used to detect outliers (e.g., Z-score, IQR, visualization techniques).
sapply(df, class)
## Age Sex Job Housing
## "numeric" "character" "numeric" "character"
## Saving accounts Checking account Credit amount Duration
## "character" "character" "numeric" "numeric"
## Purpose
## "character"
An outlier is an extreme value, meaning a value that is unusually high or low compared to the majority of the values in a variable. In the case of this dataset, although the ‘Age’ and ‘Duration’ columns are numeric, they only represent information about the credit user. Therefore, we only need to consider outliers for the `Credit column.
# library(car)
# car:::qqPlot(df$Age)
# car:::qqPlot(df$Duration)
# car:::qqPlot(df$`Credit amount`,
# ylab="Credit Amount",
# main = "QQ plot for credit amount")
credit_outlier <- boxplot(df$`Credit amount`)
sort(credit_outlier$out)
## [1] 7966 7980 8065 8072 8086 8133 8229 8318 8335 8358 8386 8471
## [13] 8487 8588 8613 8648 8858 8947 8978 9034 9055 9157 9271 9277
## [25] 9283 9398 9436 9566 9572 9629 9857 9960 10127 10144 10222 10297
## [37] 10366 10477 10623 10722 10875 10961 10974 11054 11328 11560 11590 11760
## [49] 11816 11938 11998 12169 12204 12389 12579 12612 12680 12749 12976 13756
## [61] 14027 14179 14318 14421 14555 14782 14896 15653 15672 15857 15945 18424
# df[c(96, 916), ]
# summary(df$`Credit amount`)
# quantile(df$`Credit amount`,
# probs = c(0.75, 0.8, 0.85, 0.9, 0.95, 1))
#
# quantile(df$`Credit amount`)
#
# IQR(df$`Credit amount`)
1365.50 - 1.5*IQR(df$`Credit amount`)
## [1] -2544.625
3972.25 + 1.5*IQR(df$`Credit amount`)
## [1] 7882.375
# hist(df$`Credit amount`,
# breaks = 30)
https://www.geo.fu-berlin.de/en/v/soga-r/Basics-of-statistics/Descriptive-Statistics/Measures-of-Position/Outliers-and-Boxplots/index.html
Decide whether to remove, transform, or retain the outliers and justify your choice.
We observe that, according to the IQR standard, there are 72 individuals with a credit amount considered as outliers. However, these values are still within a normal range (meaning that having a credit debt of up to 18,000 is not unusual). Therefore, I have decided to keep all of these values to include them in the subsequent analysis.
Bước 1: là đánh giá tổng thể dữ liệu sử dụng lệnh
table
lapply(df[ , -7], table)
## $Age
##
## 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
## 2 14 14 27 48 44 41 50 51 43 37 40 38 34 33 32 40 39 29 24 21 25 17 22 17 17
## 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 70 74
## 15 18 17 12 14 12 8 9 7 10 8 3 9 5 3 6 7 2 8 5 5 5 3 3 1 4
## 75
## 2
##
## $Sex
##
## female male
## 310 690
##
## $Job
##
## 0 1 2 3
## 22 200 630 148
##
## $Housing
##
## free own rent
## 108 713 179
##
## $`Saving accounts`
##
## little moderate quite rich rich
## 736 123 83 58
##
## $`Checking account`
##
## little moderate rich
## 453 434 113
##
## $Duration
##
## 4 5 6 7 8 9 10 11 12 13 14 15 16 18 20 21 22 24 26 27
## 6 1 75 5 7 49 28 9 179 4 4 64 2 113 8 30 2 184 1 13
## 28 30 33 36 39 40 42 45 47 48 54 60 72
## 3 40 3 83 5 1 11 5 1 48 2 13 1
##
## $Purpose
##
## business car domestic appliances education
## 97 337 12 59
## furniture/equipment radio/TV repairs vacation/others
## 181 280 22 12
summary(df$`Credit amount`)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 250 1366 2320 3271 3972 18424
Step 2: is to clean the data, mainly filling in the NA values in the
Saving account and Checking account columns by
creating a vector containing random values based on the frequency of the
values in the columns to ensure the data is close to the real
values.
vàChecking account`table(saving_account = df$`Saving accounts`,
checking_account = df$`Checking account`) -> saving_vs_checking
chisq.test(saving_vs_checking)
##
## Pearson's Chi-squared test
##
## data: saving_vs_checking
## X-squared = 23.233, df = 6, p-value = 0.000722
https://www.geo.fu-berlin.de/en/v/soga-r/Basics-of-statistics/Hypothesis-Tests/Chi-Square-Tests/Chi-Square-Independence-Test/index.html
Job và Credit amount# Job (numeric: 0 - unskilled and non-resident, 1 - unskilled and resident, 2 - skilled, 3 - highly skilled)
library(car)
df$Job_chr <- car:::recode(df$Job,
" '0' = 'unskilled and non-resident';
'1' = 'unskilled and resident';
'2' = 'skilled';
'3' = 'highly skilled';
else = NA ")
library(ggplot2)
library(extrafont)
ggplot(data = df,
mapping = aes(x = Job,
y = `Credit amount`,
fill = Job_chr)) +
geom_boxplot() +
scale_fill_manual(values = c("green",
"coral",
"#eec750",
"lightblue")) +
theme_bw() +
theme(axis.text.x = element_text(angle = 90,
hjust = 1)) +
theme(text = element_text(family = "Times New Roman"))
## Khảo sát
Credit amount, Job và
Purpose
addmargins(table(purpose = df$Purpose,
job = df$Job_chr))
## job
## purpose highly skilled skilled unskilled and non-resident
## business 15 60 2
## car 69 190 12
## domestic appliances 0 10 1
## education 8 35 1
## furniture/equipment 21 126 1
## radio/TV 26 195 2
## repairs 0 13 2
## vacation/others 9 1 1
## Sum 148 630 22
## job
## purpose unskilled and resident Sum
## business 20 97
## car 66 337
## domestic appliances 1 12
## education 15 59
## furniture/equipment 33 181
## radio/TV 57 280
## repairs 7 22
## vacation/others 1 12
## Sum 200 1000
Credit amount, Job và
Purposeboxplot
library(ggplot2)
ggplot(data = df,
mapping = aes(x = Purpose,
y = `Credit amount`,
fill = Purpose)) +
geom_boxplot() +
facet_wrap(~Job_chr) +
theme_bw() +
theme(axis.text.x = element_text(angle = 90,
hjust = 1))
mean col
library(ggplot2)
library(dplyr)
df |> dplyr:::group_by(Job_chr, Purpose) |>
dplyr:::summarise(mean_credit = mean(`Credit amount`),
person = n()) -> df_sum
df_sum <- as.data.frame(df_sum)
ggplot(data = df_sum,
mapping = aes(x = Purpose,
y = mean_credit,
fill = Purpose)) +
geom_col() +
facet_wrap(~Job_chr) +
theme_bw() +
theme(axis.text.x = element_text(angle = 90,
hjust = 1))
Age, ’Purpose’và Credit amountggplot(data=df,
mapping=aes(x=Age,
y=`Credit amount`,
color=Age)) +
geom_point() +
# geom_smooth(method = "lm") +
facet_wrap(~Purpose,
scales = "free")
cor.test(df$Age,
df$`Credit amount`)
##
## Pearson's product-moment correlation
##
## data: df$Age and df$`Credit amount`
## t = 1.0341, df = 998, p-value = 0.3013
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## -0.02933617 0.09451780
## sample estimates:
## cor
## 0.03271642
cor.test(df$`Credit amount`,
df$Job)
##
## Pearson's product-moment correlation
##
## data: df$`Credit amount` and df$Job
## t = 9.4069, df = 998, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.2274157 0.3413395
## sample estimates:
## cor
## 0.2853853
library(dplyr)
df |> dplyr:::group_by(`Saving accounts`) |>
dplyr:::summarise(mean_credit = mean(`Credit amount`),
person = n()) -> df_sum
library(ggplot2)
ggplot(data = df_sum,
mapping = aes(x = `Saving accounts`,
y = person,
fill = `Saving accounts`)) +
geom_col() +
scale_fill_manual(values = c("green",
"coral",
"#eec750",
"lightblue"))
df_cor <- df[ , c("Age", "Credit amount",
"Duration")]
cor_matrix <- cor(df_cor)
ggplot(data=df,
mapping=aes(x=Duration,
y=`Credit amount`)) +
geom_point() +
geom_smooth() +
theme_bw()
Credit ammount giữa male và femaledf_male <- df |> subset(Sex == "male")
mean(df_male$`Credit amount`)
## [1] 3448.041
df_female <- df |> subset(Sex == "female")
mean(df_female$`Credit amount`)
## [1] 2877.774
### check normality
library(car)
car:::qqPlot(df_male$`Credit amount`)
## [1] 69 558
car:::qqPlot(df_female$`Credit amount`)
## [1] 288 112
### check variance
var.test(x = df_male$`Credit amount`,
y = df_female$`Credit amount`)
##
## F test to compare two variances
##
## data: df_male$`Credit amount` and df_female$`Credit amount`
## F = 1.2415, num df = 689, denom df = 309, p-value = 0.02863
## alternative hypothesis: true ratio of variances is not equal to 1
## 95 percent confidence interval:
## 1.023039 1.496141
## sample estimates:
## ratio of variances
## 1.241496
# p-value = 0.0286 < 0.05 cho thấy phương sai của hai vector này không bằng nhau
t.test(x = df_male$`Credit amount`,
y = df_female$`Credit amount`)
##
## Welch Two Sample t-test
##
## data: df_male$`Credit amount` and df_female$`Credit amount`
## t = 3.0904, df = 658.03, p-value = 0.002084
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
## 207.9260 932.6068
## sample estimates:
## mean of x mean of y
## 3448.041 2877.774
wilcox.test(x = df_male$`Credit amount`,
y = df_female$`Credit amount`)
##
## Wilcoxon rank sum test with continuity correction
##
## data: df_male$`Credit amount` and df_female$`Credit amount`
## W = 122417, p-value = 0.0002507
## alternative hypothesis: true location shift is not equal to 0
p-value = 0.002 < 0.05 shows that the average credit ammount of male is different from female at the significance level of 0.05, or specifically, male has significantly higher credit ammount than female.
Because the credit ammount data of male and female are not normally distributed, in this case we test the difference between these two groups using wilcoxon test, we have p-value = 0.00025 < 0.05 for the same conclusion as t-test.
https://www.kaggle.com/datasets/uciml/german-credit
https://www.geo.fu-berlin.de/en/v/soga-r/Basics-of-statistics/Discrete-Random-Variables/The-Mean-and-Standard-Deviation/index.html
https://www.data-to-viz.com/#barplot