12  기술 통계로 데이터 탐색하기(Base R 버전)

기술 통계(Descriptive Statistics)는 그야말로 데이터셋을 기술(describe)하여 데이터를 이해하는 과정을 말한다. 이 장에서는 이런 기술 통계 방법을 통해서 데이터를 탐색하는 방법에 대해서 설명한다.

이런 일을 하기 위한 fancy한 R 패키지들도 많지만, 여기서는 주로 Base R을 사용하여 기술 통계를 수행하는 방법을 설명하려고 한다(이 내용들은 나중에).

기술 통계 방법은 변수의 scales of measurement에 따라서 내용을 머리 속에 정리하는 것이 좋다.

주로 사용할 R 함수들은 다음과 같다.

분할표(contingency table)

범주형 데이터의 요약은 분할표가 기본이다. (R 팩터(factor)와 levels를 같이 알아야 한다.) 이것은 모든 levels에 대하여 개수를 카운트하여 정리한 표이다.

다음 사이트에 내용이 잘 정리되어 있다.

12.1 데이터셋 소개

우리가 사용할 데이터셋은 캐글(kaggle)에 올라와 있는 Stroke Prediction Dataset이다(캐글에 회원 등록을 하면 누구나 이 데이터셋을 다운로드할 수 있다).

인터넷에는 이 데이터셋을 사용한 통계 분석 및 머신러닝 관련된 자료들도 많이 찾을 수 있다.

이 데이터셋은 워킹 디렉터리의 data 서브 디렉터리에 healthcare-dataset-stroke-data.csv라는 이름으로 저장되어 있다고 생각해 보자. 이 파일의 확장자는 .csv로 “Comma-Separated Values file”이다. 플레인 텍스트로 되어 있고, 코마로 값들을 구분한다. CSV 파일은 엑셀로도 쉽게 읽을 수 있고, 엑셀에서 쉽게 CSV로 저장할 수도 있으며, 플레인 텍스트이기 때문에 아무 텍스트 편집기로도 읽을 수 있어서 데이터 과학에서 자주 사용된다.

readr이라는 패키지의 read_csv() 함수를 사용하여 파일을 읽는다. 이 함수는 데이터를 tibble(R 데이터프레임을 보강한 것)로 가지고 온다. (아직 우리가 tibble을 배우지 않아서) 원래의 R 데이터프레임으로 바뀌기 위해서 as.data.frame()를 사용했다.

library(readr)
stroke_df <- as.data.frame(read_csv("data/healthcare-dataset-stroke-data.csv"))
head(stroke_df) # 앞 6개의 행
     id gender age hypertension heart_disease ever_married     work_type
1  9046   Male  67            0             1          Yes       Private
2 51676 Female  61            0             0          Yes Self-employed
3 31112   Male  80            0             1          Yes       Private
4 60182 Female  49            0             0          Yes       Private
5  1665 Female  79            1             0          Yes Self-employed
6 56669   Male  81            0             0          Yes       Private
  Residence_type avg_glucose_level  bmi  smoking_status stroke
1          Urban            228.69 36.6 formerly smoked      1
2          Rural            202.21  N/A    never smoked      1
3          Rural            105.92 32.5    never smoked      1
4          Urban            171.23 34.4          smokes      1
5          Rural            174.12   24    never smoked      1
6          Urban            186.21   29 formerly smoked      1

str() 함수는 데이터의 구조를 파악하는데 좋다. 이 데이터셋은 5,110개의 행과 12개의 열로 구성되어 있다.

str(stroke_df)
'data.frame':   5110 obs. of  12 variables:
 $ id               : num  9046 51676 31112 60182 1665 ...
 $ gender           : chr  "Male" "Female" "Male" "Female" ...
 $ age              : num  67 61 80 49 79 81 74 69 59 78 ...
 $ hypertension     : num  0 0 0 0 1 0 1 0 0 0 ...
 $ heart_disease    : num  1 0 1 0 0 0 1 0 0 0 ...
 $ ever_married     : chr  "Yes" "Yes" "Yes" "Yes" ...
 $ work_type        : chr  "Private" "Self-employed" "Private" "Private" ...
 $ Residence_type   : chr  "Urban" "Rural" "Rural" "Urban" ...
 $ avg_glucose_level: num  229 202 106 171 174 ...
 $ bmi              : chr  "36.6" "N/A" "32.5" "34.4" ...
 $ smoking_status   : chr  "formerly smoked" "never smoked" "never smoked" "smokes" ...
 $ stroke           : num  1 1 1 1 1 1 1 1 1 1 ...

캐글 사이트를 보면 각 변수에 대한 설명이 있다.

  1. id: unique identifier
  2. gender: “Male”, “Female” or “Other”
  3. age: age of the patient
  4. hypertension: 0 if the patient doesn’t have hypertension, 1 if the patient has hypertension
  5. heart_disease: 0 if the patient doesn’t have any heart diseases, 1 if the patient has a heart disease
  6. ever_married: “No” or “Yes”
  7. work_type: “children”, “Govt_jov”, “Never_worked”, “Private” or “Self-employed”
  8. Residence_type: “Rural” or “Urban”
  9. avg_glucose_level: average glucose level in blood
  10. bmi: body mass index
  11. smoking_status: “formerly smoked”, “never smoked”, “smokes” or “Unknown”*
  12. stroke: 1 if the patient had a stroke or 0 if not
  • Note: “Unknown” in smoking_status means that the information is unavailable for this patient

마지막 열 stroke은 뇌졸중의 유무를 0, 1로 정리했고, 주요 뇌졸중 위험인자들이 정리되어 있다. 결측값은 "N/A" 또는 흡연(smoking_status)인 겅우에는 "Unknown"으로 코딩되어 있는 것을 볼 수 있다. 그리고 문자열로 코딩된 것들은 대부분 R에서는 카테코리형 데이터를 표현하는 팩터(factor)로 되어 있어야 하는데 아직은 원래 값 그대로의 상태로 있다. 우선 이런 것들을 정리해 보자.

12.2 데이터 클리닝: 의도와 목적에 맞게 데이터 정리하기

이제 데이터셋을 정리해 보자.

먼저 id는 환자의 아이디인데, 숫자로 되어 있어서 이것을 문자로 바꾼다.

stroke_df$id <- as.character(stroke_df$id)

gender는 남자, 여자, 기타로 되어 있는데 팩터로 바꿔준다.

stroke_df$gender <- factor(stroke_df$gender, levels = c("Male", "Female", "Other"))

heart_diseasehypertension은 0과 1로 되어 있는데 역시 팩터로 바꾼다.

stroke_df$heart_disease <- factor(stroke_df$heart_disease, levels = c(0, 1), labels = c("No", "Yes"))
stroke_df$hypertension <- factor(stroke_df$hypertension, levels = c(0, 1), labels = c("No", "Yes"))

ever_married는 결혼 여부로 No, Yes로 되어 있는데 팩터로 바꾼다. work_type는 직업으로 children, Govt_job, Never_worked, Private, Self-employed로 되어 있다. 이것도 팩터로 바꾼다. Residence_type는 거주지로 Rural, Urban으로 되어 있는데, 이것도 팩터로 바꾼다.

stroke_df$ever_married <- factor(stroke_df$ever_married, levels = c("No", "Yes"))
stroke_df$work_type <- factor(stroke_df$work_type, levels = c("children", "Govt_job", "Never_worked", "Private", "Self-employed"))
stroke_df$Residence_type <- factor(stroke_df$Residence_type, levels = c("Rural", "Urban"))

다른 변수들을 모두 소문자로 되어 있는데 Residence_type만 대문자로 되어 있다. 이름을 소문자로 다시 정해준다.

names(stroke_df)[8] <- "residence_type"

bmi는 문자열로 되어 있는데 이것은 숫자로 바꾼다. 그런데 결측값이 N/A로 코딩되어 있어서, 먼저 이 값을 NA 바꾸고 나서 진행한다.

stroke_df$bmi[stroke_df$bmi == "N/A"] <- NA
stroke_df$bmi <- as.numeric(stroke_df$bmi)

smoking_status는 흡연 여부로 formerly smoked, never smoked, smokes, Unknown으로 되어 있어 팩터로 바꾼다. stroke는 뇌졸중 여부로 0, 1로 되어 있다. 팩터로 바꾼다.

stroke_df$smoking_status <- factor(stroke_df$smoking_status, levels = c("formerly smoked", "never smoked", "smokes", "Unknown"))
stroke_df$stroke <- factor(stroke_df$stroke, levels = c(0, 1), labels = c("No", "Yes"))

이제 데이터셋을 정리했다. 앞의 몇 개의 샘플을 보고, str() 함수를 사용하여 그 구조를 확인한다.

head(stroke_df, 10)
      id gender age hypertension heart_disease ever_married     work_type
1   9046   Male  67           No           Yes          Yes       Private
2  51676 Female  61           No            No          Yes Self-employed
3  31112   Male  80           No           Yes          Yes       Private
4  60182 Female  49           No            No          Yes       Private
5   1665 Female  79          Yes            No          Yes Self-employed
6  56669   Male  81           No            No          Yes       Private
7  53882   Male  74          Yes           Yes          Yes       Private
8  10434 Female  69           No            No           No       Private
9  27419 Female  59           No            No          Yes       Private
10 60491 Female  78           No            No          Yes       Private
   residence_type avg_glucose_level  bmi  smoking_status stroke
1           Urban            228.69 36.6 formerly smoked    Yes
2           Rural            202.21   NA    never smoked    Yes
3           Rural            105.92 32.5    never smoked    Yes
4           Urban            171.23 34.4          smokes    Yes
5           Rural            174.12 24.0    never smoked    Yes
6           Urban            186.21 29.0 formerly smoked    Yes
7           Rural             70.09 27.4    never smoked    Yes
8           Urban             94.39 22.8    never smoked    Yes
9           Rural             76.15   NA         Unknown    Yes
10          Urban             58.57 24.2         Unknown    Yes
str(stroke_df)
'data.frame':   5110 obs. of  12 variables:
 $ id               : chr  "9046" "51676" "31112" "60182" ...
 $ gender           : Factor w/ 3 levels "Male","Female",..: 1 2 1 2 2 1 1 2 2 2 ...
 $ age              : num  67 61 80 49 79 81 74 69 59 78 ...
 $ hypertension     : Factor w/ 2 levels "No","Yes": 1 1 1 1 2 1 2 1 1 1 ...
 $ heart_disease    : Factor w/ 2 levels "No","Yes": 2 1 2 1 1 1 2 1 1 1 ...
 $ ever_married     : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 2 2 1 2 2 ...
 $ work_type        : Factor w/ 5 levels "children","Govt_job",..: 4 5 4 4 5 4 4 4 4 4 ...
 $ residence_type   : Factor w/ 2 levels "Rural","Urban": 2 1 1 2 1 2 1 2 1 2 ...
 $ avg_glucose_level: num  229 202 106 171 174 ...
 $ bmi              : num  36.6 NA 32.5 34.4 24 29 27.4 22.8 NA 24.2 ...
 $ smoking_status   : Factor w/ 4 levels "formerly smoked",..: 1 2 2 3 2 1 2 2 4 4 ...
 $ stroke           : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 2 2 2 2 2 ...

이 데이터셋을 .rds 폼으로 저장한다.

saveRDS(stroke_df, "data/stroke_df.rds")

데이터셋을 다운로드할 수 있다. RDS 파일을 읽을 때는 다음과 같이 readRDS() 함수를 사용하고, 이 데이터프레임의 이름을 지정해 주면 된다.

stroke_df <- readRDS("data/stroke_df.rds")

12.3 기술 통계로 데이터 탐색

이제 정리된 데이터셋을 가지고 기술 통계를 사용하여 데이터를 탐색해 보자.

12.3.1 summary()로 전체적으로 파악하기

개별 변수들을 보기 전에 summary() 함수를 데이터프레임에 적용해 보자. 이 함수는 각 변수의 요약 통계량을 한꺼번에 볼 수 있어서 편리하다. 결과를 관찰해 보면 변수가 숫자형 변수인 경우에는 Minimum, 1st Qu., Median, Mean, 3rd Qu., Maximum이 출력되고, 팩터형 변수인 경우에는 각 레벨의 빈도수가 출력됨을 볼 수 있다.

summary(stroke_df)
      id               gender          age        hypertension heart_disease
 Length:5110        Male  :2115   Min.   : 0.08   No :4612     No :4834     
 Class :character   Female:2994   1st Qu.:25.00   Yes: 498     Yes: 276     
 Mode  :character   Other :   1   Median :45.00                             
                                  Mean   :43.23                             
                                  3rd Qu.:61.00                             
                                  Max.   :82.00                             
                                                                            
 ever_married         work_type    residence_type avg_glucose_level
 No :1757     children     : 687   Rural:2514     Min.   : 55.12   
 Yes:3353     Govt_job     : 657   Urban:2596     1st Qu.: 77.25   
              Never_worked :  22                  Median : 91.89   
              Private      :2925                  Mean   :106.15   
              Self-employed: 819                  3rd Qu.:114.09   
                                                  Max.   :271.74   
                                                                   
      bmi                smoking_status stroke    
 Min.   :10.30   formerly smoked: 885   No :4861  
 1st Qu.:23.50   never smoked   :1892   Yes: 249  
 Median :28.10   smokes         : 789             
 Mean   :28.89   Unknown        :1544             
 3rd Qu.:33.10                                    
 Max.   :97.60                                    
 NA's   :201                                      

이 데이터셋에서 변수는 age, avg_glucose_level, bmi는 숫자형 변수이고, 나머지 변수들은 모두 팩터형 변수이다.

데이터셋의 목적이 뇌졸중의 위험인자를 탐색하는 것일 가능성이 높다. 뇌졸중 여부인 stroke 변수를 기준으로 나머지 변수들을 탐색해 보자

우선 우리의 관심사가 뇌졸중의 유무로 먼저 이 변수를 염두에 두고 시작하는 것이 좋겠다. 전체 5110명 가운데 뇌졸중이 없는 경우가 4861명이고 뇌졸중이 있는 경우가 249명이다.

barplot(table(stroke_df$stroke),
    main = "Stroke Distribution",
    xlab = "Stroke",
    ylab = "Frequency",
    col = c("lightblue", "lightgreen"),
    border = "black"
)

12.3.2 숫자형 변수 탐색하기(Numerical Variables)

숫자형 변수는 여러 가지 통계량으로 요약할 수 있다. 이 데이터셋에서 숫자형 변수는 age, avg_glucose_level, bmi이다. 이 변수들은 연속형 변수이기 때문에 평균(mean), 분산(var), 표준편차(sd) 등으로 요약할 수 있다. 또 숫자형 변수는 분포가 중요하기 때문에 히스토그램, 박스 플롯, 스캐터 플롯 등으로 정리할 수 있겠다.

먼저 뇌졸중 여부에 상관없이 전체적으로 보고, 그 다음 뇌졸중의 유무에 따라 어떻게 달라지는지 살펴보자.

이제 age를 보자.

전체의 age의 평균, 분산, 표준편차는 다음과 같다.

mean(stroke_df$age, na.rm = TRUE) # 평균
## [1] 43.22661
var(stroke_df$age, na.rm = TRUE) # 분산
## [1] 511.3318
sd(stroke_df$age, na.rm = TRUE) # 표준편차
## [1] 22.61265

분포를 보자.

hist(stroke_df$age,
    main = "Age Distribution",
    xlab = "Age",
    ylab = "Frequency",
    col = "lightblue",
    border = "black"
)

박스 플롯을 보면 다음과 같다. 히스토그램과 비교할 수 있게 수평으로 그려 보았다.

boxplot(stroke_df$age,
    main = "Age Boxplot",
    xlab = "Age",
    horizontal = TRUE,
    col = "lightblue",
    border = "black"
)

뇌줄중 유무에 따른 차이를 보자.

hist(stroke_df$age[stroke_df$stroke == "No"],
    main = "Age Distribution by Stroke (without Stroke)",
    xlab = "Age",
    ylab = "Frequency",
    col = "lightblue",
    border = "black"
)

hist(stroke_df$age[stroke_df$stroke == "Yes"],
    main = "Age Distribution by Stroke (with Stroke)",
    xlab = "Age",
    ylab = "Frequency",
    col = "lightgreen",
    border = "black"
)

박스 플롯을 보자. R 포뮬라를 사용하여 stroke에 따라서 age를 구분하도록 지시하였다.

boxplot(stroke_df$age ~ stroke_df$stroke,
    main = "Age Boxplot by Stroke",
    xlab = "Stroke",
    ylab = "Age",
    col = c("lightblue", "lightgreen"),
    border = "black"
)

뇌졸중이 있는 그룹과 없는 구룹의 평균 차이가 있는지 t.test()를 사용해 검정해 보았다.

result <- t.test(stroke_df$age[stroke_df$stroke == "No"],
    stroke_df$age[stroke_df$stroke == "Yes"],
    var.equal = TRUE
)
result

    Two Sample t-test

data:  stroke_df$age[stroke_df$stroke == "No"] and stroke_df$age[stroke_df$stroke == "Yes"]
t = -18.081, df = 5108, p-value < 2.2e-16
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -28.54933 -22.96396
sample estimates:
mean of x mean of y 
 41.97154  67.72819 

broom 패키지는 결과물을 정리하여 보고, 관련된 값을 추출할 때 편리하다.

library(broom)
tidy(result)
# A tibble: 1 × 10
  estimate estimate1 estimate2 statistic  p.value parameter conf.low conf.high
     <dbl>     <dbl>     <dbl>     <dbl>    <dbl>     <dbl>    <dbl>     <dbl>
1    -25.8      42.0      67.7     -18.1 7.03e-71      5108    -28.5     -23.0
# ℹ 2 more variables: method <chr>, alternative <chr>

다음은 avg_glucose_level이다.

hist(stroke_df$avg_glucose_level,
    main = "Average Glucose Level Distribution",
    xlab = "Average Glucose Level",
    ylab = "Frequency",
    col = "lightblue",
    border = "black"
)

마찬가지로 뇌줄중의 유무에 따른 차이를 박스 플롯으로 보자.

boxplot(stroke_df$avg_glucose_level ~ stroke_df$stroke,
    main = "Average Glucose Level Boxplot by Stroke",
    xlab = "Stroke",
    ylab = "Average Glucose Level",
    col = c("lightblue", "lightgreen"),
    border = "black"
)

t.test를 사용하여 뇌졸중 유무에 따른 평균 차이를 검정해 보자.

result <- t.test(stroke_df$avg_glucose_level[stroke_df$stroke == "No"],
    stroke_df$avg_glucose_level[stroke_df$stroke == "Yes"],
    var.equal = TRUE
)
result

    Two Sample t-test

data:  stroke_df$avg_glucose_level[stroke_df$stroke == "No"] and stroke_df$avg_glucose_level[stroke_df$stroke == "Yes"]
t = -9.5134, df = 5108, p-value < 2.2e-16
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -33.46754 -22.03091
sample estimates:
mean of x mean of y 
 104.7955  132.5447 

마지막으로 bmi를 보자.

결측값의 개수를 확인하자.

sum(is.na(stroke_df$bmi))
[1] 201

결측값을 제외하고 평균, 분산, 표준편차를 구하자.

mean(stroke_df$bmi, na.rm = TRUE) # 평균
[1] 28.89324
var(stroke_df$bmi, na.rm = TRUE) # 분산
[1] 61.68636
sd(stroke_df$bmi, na.rm = TRUE) # 표준편차
[1] 7.854067

다음은 히스토그램이다.

hist(stroke_df$bmi,
    main = "BMI Distribution",
    xlab = "BMI",
    ylab = "Frequency",
    col = "lightblue",
    border = "black"
)

뇌졸중 유무에 따른 차이를 박스 플롯으로 보자.

boxplot(stroke_df$bmi ~ stroke_df$stroke,
    main = "BMI Boxplot by Stroke",
    xlab = "Stroke",
    ylab = "BMI",
    col = c("lightblue", "lightgreen"),
    border = "black"
)

t.test를 사용하여 뇌졸중 유무에 따른 평균 차이를 검정해 보자.

result <- t.test(stroke_df$bmi[stroke_df$stroke == "No"],
    stroke_df$bmi[stroke_df$stroke == "Yes"],
    var.equal = TRUE
)
result

    Two Sample t-test

data:  stroke_df$bmi[stroke_df$stroke == "No"] and stroke_df$bmi[stroke_df$stroke == "Yes"]
t = -2.9709, df = 4907, p-value = 0.002983
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -2.7358507 -0.5606053
sample estimates:
mean of x mean of y 
 28.82306  30.47129 

ageavg_glucose_level의 관계를 스캐터 플롯으로 그려 보자.

plot(stroke_df$age, stroke_df$avg_glucose_level,
    main = "Age vs. Average Glucose Level",
    xlab = "Age",
    ylab = "Average Glucose Level",
    col = ifelse(stroke_df$stroke == "Yes", "lightgreen", "lightblue"),
    pch = 19
)

상관계수를 구해 보자.

cor(stroke_df$age, stroke_df$avg_glucose_level, use = "pairwise.complete.obs")
[1] 0.2381711

agebmi의 관계는 어떨까?

plot(stroke_df$age, stroke_df$bmi,
    main = "Age vs. BMI",
    xlab = "Age",
    ylab = "BMI",
    col = ifelse(stroke_df$stroke == "Yes", "lightgreen", "lightblue"),
    pch = 19
)

상관계수를 구해 보자.

cor(stroke_df$age, stroke_df$bmi, use = "pairwise.complete.obs")
[1] 0.333398

avg_glucose_levelbmi의 관계는 어떨까?

plot(stroke_df$avg_glucose_level, stroke_df$bmi,
    main = "Average Glucose Level vs. BMI",
    xlab = "Average Glucose Level",
    ylab = "BMI",
    col = ifelse(stroke_df$stroke == "Yes", "lightgreen", "lightblue"),
    pch = 19
)

상관계수를 구해보자.

cor(stroke_df$avg_glucose_level, stroke_df$bmi, use = "pairwise.complete.obs")
[1] 0.1755022

12.3.3 범주형 변수 탐색 (Categorical Variables)

이 데이터셋에서 범주형 변수는 gender, hypertension, heart_disease, ever_married, work_type, residence_type, smoking_status이다. 이 변수들은 모두 팩터형 변수이기 때문에 빈도수를 구할 수 있다.

빈도수는 contingency table로 정리할 수 있는데, table() 함수를 사용하여 만든다.

table(stroke_df$gender)

  Male Female  Other 
  2115   2994      1 
table(stroke_df$hypertension)

  No  Yes 
4612  498 
table(stroke_df$heart_disease)

  No  Yes 
4834  276 
table(stroke_df$ever_married)

  No  Yes 
1757 3353 
table(stroke_df$work_type)

     children      Govt_job  Never_worked       Private Self-employed 
          687           657            22          2925           819 
table(stroke_df$residence_type)

Rural Urban 
 2514  2596 
table(stroke_df$smoking_status)

formerly smoked    never smoked          smokes         Unknown 
            885            1892             789            1544 

gender의 뇌졸중 유무에 따른 빈도수를 구해보자.

table(stroke_df$gender, stroke_df$stroke)
        
           No  Yes
  Male   2007  108
  Female 2853  141
  Other     1    0
prop.table(table(stroke_df$gender, stroke_df$stroke), margin = 1)
        
                 No        Yes
  Male   0.94893617 0.05106383
  Female 0.95290581 0.04709419
  Other  1.00000000 0.00000000

hypertension의 뇌졸중 유무에 따른 빈도수를 구해보자.

table(stroke_df$hypertension, stroke_df$stroke)
     
        No  Yes
  No  4429  183
  Yes  432   66
prop.table(table(stroke_df$hypertension, stroke_df$stroke), margin = 1)
     
             No       Yes
  No  0.9603209 0.0396791
  Yes 0.8674699 0.1325301

이것을 막대 그래프(bar plot)으로 그려 보자.

barplot(table(stroke_df$hypertension, stroke_df$stroke),
    main = "Hypertension by Stroke",
    xlab = "Hypertension",
    ylab = "Frequency",
    col = c("lightblue", "lightgreen"),
    border = "black",
    beside = TRUE
)

chisq.test()를 사용하여 뇌졸중 유무에 따른 비율 차이를 검정해 보자.

result <- chisq.test(table(stroke_df$hypertension, stroke_df$stroke))
result

    Pearson's Chi-squared test with Yates' continuity correction

data:  table(stroke_df$hypertension, stroke_df$stroke)
X-squared = 81.605, df = 1, p-value < 2.2e-16

hypertension의 뇌졸중에 대한 relative risk를 계산해 보자.

table(stroke_df$hypertension, stroke_df$stroke)
     
        No  Yes
  No  4429  183
  Yes  432   66

Relative Risk는 보통 전향적 연구에서 사용하고, 위험 인자가 없는 집단에서의 risk와 위험 인자가 있는 집단에서의 risk(확률)을 비교한다.

66/(432+66) / (183/(4429+183))
[1] 3.340049

이것이 후향적 연구였다면 odds ratio를 계산할 것이다.

(66/432) / (183/4429) 
[1] 3.697556
그림 12.1: Relative Risk와 Odd’s ratio

epitools라는 패키지를 사용하면 상대위험도(relative risk)와 오즈비(odds ratio)를 쉽게 계산할 수 있고, 신뢰구간도 계산된다.

tbl <- table(stroke_df$hypertension, stroke_df$stroke)
library(epitools)
riskratio(tbl)
$data
       
          No Yes Total
  No    4429 183  4612
  Yes    432  66   498
  Total 4861 249  5110

$measure
     risk ratio with 95% C.I.
      estimate   lower upper
  No  1.000000      NA    NA
  Yes 3.340049 2.56046 4.357

$p.value
     two-sided
       midp.exact fisher.exact   chi.square
  No           NA           NA           NA
  Yes 5.77316e-15 4.549182e-15 6.068123e-20

$correction
[1] FALSE

attr(,"method")
[1] "Unconditional MLE & normal approximation (Wald) CI"
oddsratio(tbl)
$data
       
          No Yes Total
  No    4429 183  4612
  Yes    432  66   498
  Total 4861 249  5110

$measure
     odds ratio with 95% C.I.
      estimate    lower    upper
  No  1.000000       NA       NA
  Yes 3.701246 2.729655 4.964923

$p.value
     two-sided
       midp.exact fisher.exact   chi.square
  No           NA           NA           NA
  Yes 5.77316e-15 4.549182e-15 6.068123e-20

$correction
[1] FALSE

attr(,"method")
[1] "median-unbiased estimate & mid-p exact CI"

heart_disease의 뇌졸중 유무에 따른 빈도수를 구해보자.

table(stroke_df$heart_disease, stroke_df$stroke)
     
        No  Yes
  No  4632  202
  Yes  229   47
prop.table(table(stroke_df$heart_disease, stroke_df$stroke), margin = 1)
     
              No        Yes
  No  0.95821266 0.04178734
  Yes 0.82971014 0.17028986

이것을 막대 그래프(bar plot)으로 그려 보자.

barplot(table(stroke_df$heart_disease, stroke_df$stroke),
    main = "Heart Disease by Stroke",
    xlab = "Heart Disease",
    ylab = "Frequency",
    col = c("lightblue", "lightgreen"),
    border = "black",
    beside = TRUE
)

chisq.test()를 사용하여 뇌졸중 유무에 따른 비율 차이를 검정해 보자.

result <- chisq.test(table(stroke_df$heart_disease, stroke_df$stroke))
result

    Pearson's Chi-squared test with Yates' continuity correction

data:  table(stroke_df$heart_disease, stroke_df$stroke)
X-squared = 90.26, df = 1, p-value < 2.2e-16

ever_married의 뇌졸중 유무에 따른 빈도수를 구해보자.

table(stroke_df$ever_married, stroke_df$stroke)
     
        No  Yes
  No  1728   29
  Yes 3133  220
prop.table(table(stroke_df$ever_married, stroke_df$stroke), margin = 1)
     
              No        Yes
  No  0.98349459 0.01650541
  Yes 0.93438712 0.06561288

chisq.test()를 사용하여 뇌졸중 유무에 따른 비율 차이를 검정해 보자.

result <- chisq.test(table(stroke_df$ever_married, stroke_df$stroke))
result

    Pearson's Chi-squared test with Yates' continuity correction

data:  table(stroke_df$ever_married, stroke_df$stroke)
X-squared = 58.924, df = 1, p-value = 1.639e-14

work_type의 뇌졸중 유무에 따른 빈도수를 구해보자.

table(stroke_df$work_type, stroke_df$stroke)
               
                  No  Yes
  children       685    2
  Govt_job       624   33
  Never_worked    22    0
  Private       2776  149
  Self-employed  754   65
prop.table(table(stroke_df$work_type, stroke_df$stroke), margin = 1)
               
                         No         Yes
  children      0.997088792 0.002911208
  Govt_job      0.949771689 0.050228311
  Never_worked  1.000000000 0.000000000
  Private       0.949059829 0.050940171
  Self-employed 0.920634921 0.079365079

residence_type의 뇌졸중 유무에 따른 빈도수를 구해보자.

table(stroke_df$residence_type, stroke_df$stroke)
       
          No  Yes
  Rural 2400  114
  Urban 2461  135
prop.table(table(stroke_df$residence_type, stroke_df$stroke), margin = 1)
       
                No        Yes
  Rural 0.95465394 0.04534606
  Urban 0.94799692 0.05200308

smoking_status의 뇌졸중 유무에 따른 빈도수를 구해보자.

table(stroke_df$smoking_status, stroke_df$stroke)
                 
                    No  Yes
  formerly smoked  815   70
  never smoked    1802   90
  smokes           747   42
  Unknown         1497   47
prop.table(table(stroke_df$smoking_status, stroke_df$stroke), margin = 1)
                 
                          No        Yes
  formerly smoked 0.92090395 0.07909605
  never smoked    0.95243129 0.04756871
  smokes          0.94676806 0.05323194
  Unknown         0.96955959 0.03044041

이것을 막대 그래프(bar plot)으로 그려 보자.

barplot(table(stroke_df$smoking_status, stroke_df$stroke),
    main = "Smoking Status by Stroke",
    xlab = "Smoking Status",
    ylab = "Frequency",
    col = c("lightblue", "lightgreen"),
    border = "black",
    beside = TRUE
)

12.4 tableone 패키지

의학 논문의 첫 번째 표는 대부분 기술 통계를 정리하여 제시한다. R에는 tableone이라는 패키지가 있는데, 이것을 사용하면 쉽게 table 1을 만들 수 있다.

# 분석에 필요없는 id 열을 제거한 데이터프레임
df <- stroke_df[, -1]
library(tableone)
t1 <- CreateTableOne(data = df, str = "stroke")
t1
                               Stratified by stroke
                                No             Yes             p      test
  n                               4861            249                     
  gender (%)                                                    0.790     
     Male                         2007 (41.3)     108 ( 43.4)             
     Female                       2853 (58.7)     141 ( 56.6)             
     Other                           1 ( 0.0)       0 (  0.0)             
  age (mean (SD))                41.97 (22.29)  67.73 (12.73)  <0.001     
  hypertension = Yes (%)           432 ( 8.9)      66 ( 26.5)  <0.001     
  heart_disease = Yes (%)          229 ( 4.7)      47 ( 18.9)  <0.001     
  ever_married = Yes (%)          3133 (64.5)     220 ( 88.4)  <0.001     
  work_type (%)                                                <0.001     
     children                      685 (14.1)       2 (  0.8)             
     Govt_job                      624 (12.8)      33 ( 13.3)             
     Never_worked                   22 ( 0.5)       0 (  0.0)             
     Private                      2776 (57.1)     149 ( 59.8)             
     Self-employed                 754 (15.5)      65 ( 26.1)             
  residence_type = Urban (%)      2461 (50.6)     135 ( 54.2)   0.298     
  avg_glucose_level (mean (SD)) 104.80 (43.85) 132.54 (61.92)  <0.001     
  bmi (mean (SD))                28.82 (7.91)   30.47 (6.33)    0.003     
  smoking_status (%)                                           <0.001     
     formerly smoked               815 (16.8)      70 ( 28.1)             
     never smoked                 1802 (37.1)      90 ( 36.1)             
     smokes                        747 (15.4)      42 ( 16.9)             
     Unknown                      1497 (30.8)      47 ( 18.9)             
  stroke = Yes (%)                   0 ( 0.0)     249 (100.0)  <0.001