[Project] Prediction of Patients with Low Blood Pressure during Anesthesia

Doyun’s Journey
Doyun’s Lab
Published in
10 min readAug 29, 2020

Subject : Prediction of Patients with Low Blood Pressure during Anesthesia

Language : R

Data : ‘수술 중 마취한 환자의 혈압과 정보’ 데이터

Model : Random Forest, SVM, Boosting

1. Data parsing & preprocessing

# total 이라는 리스트에 모든 엑셀 파일을 불러와 저장setwd("C:\\r_temp\\homework")total <- lapply(Sys.glob("homework*.csv"), read.csv, stringsAsFactors = F)# total 리스트 요소들을 하나씩 뽑아 전처리 하는 과정for(i in 1:length(total)){total[[i]]$date1 <- strptime(total[[i]]$date1, "%Y-%m-%d %H:%M") # date를 POSIxt 형으로 변환# 집도의, 마취의, 신체적상태만 따로 추출하여 새로운 변수에 저장x <- subset(total[[i]], total[[i]]$item == "집도과1")y <- subset(total[[i]], total[[i]]$item == "마취의1")z <-subset(total[[i]], total[[i]]$item == "신체적상태")# 마취시작전 시간대 데이터만 추출하고 중복된 값 제거total[[i]] <- subset(total[[i]], total[[i]]$date1 <= total[[i]]$date1[14])total[[i]] <- total[[i]][-which(duplicated(total[[i]]$item)),]# 마취시작전 시간대 데이터에 집도의, 마취의, 신체적 상태 행으로 붙이기total[[i]] <- rbind(total[[i]], x, y, z)# class가 열 2개로 이루어져 있는 데이터 1개 열 제거total[[i]]$class.1 <- NULL# 필요없는 열 빼고 다시 저장total[[i]] <- total[[i]][, -c(1,7,8,9,10,11)]# value1 값이 공백인 것 빼고 불러오기total[[i]] <- total[[i]][!(total[[i]]$value1 == ""), ]# 마취일반정보와 V/S 정보만 빼기total[[i]] <- subset(total[[i]], total[[i]]$group == "V/S" | total[[i]]$group == "마취일반정보")}
  • 파일을 불러와 리스트에 저장 후, 전처리 진행

- (1) date1 변수를 POSIxt형으로 변환해 줍니다.

- (2) 집도과1, 마취의1, 신체적상태는 subset 하여 변수에 따로 저장해줍니다.

- (3) 마취시작 전 시간대 데이터만 추출한 후, 중복된 행을 제거합니다.

- (4) [단계 (3)] 데이터에 [단계 (2)]에서 추출한 변수들을 rbind 하여 행을 추가해줍니다.

- (5) class가 열이 2개로 이루어져 있는 열을 하나로 만들어 줍니다.

- (6) 분석에 필요하지 않은 열을 빼줍니다.

- (7) value1 변수에 공백으로 이루어져 있는 것을 제외하고 다시 저장해줍니다.

- (8) 마취일반정보와 V/S 정보만 빼서 저장을 마칩니다.

# 행과 열을 바꿔주는 작업for(i in 1:length(total)){# class 값을 따로 저장class1 <- total[[i]]$class[1]# item과 value1 값만 빼기total[[i]] <- total[[i]][,c(3, 4)]# 행과 열 바꿔주기total[[i]] <- t(total[[i]])# 데이터프레임으로 형 변환total[[i]] <- as.data.frame(total[[i]])# item 행을 열의 이름으로 바꿔주기colnames(total[[i]]) <- unlist(total[[i]][row.names(total[[i]]) == "item",])# item 값들 옆에 class 행 붙여주기total[[i]] <- cbind(total[[i]], class1)}
  • Before Modeling, 행과 열 바꾸기

- (1) 행과 열을 바꾸기 전에 class 값을 따로 저장해줍니다.

- (2) item, value1 값만 빼서 저장해줍니다.

- (3) 행과 열을 바꿔줍니다. [ t() 함수 사용 ]

- (4) [단계 (3)]을 거친 데이터를 데이터프레임 형으로 변환해줍니다.

- (5) item 행을 [단계 (4)]의 데이터프레임 열 이름으로 바꿔줍니다.

- (6) 모든 과정을 마친 후, class 값을 새로운 열으로 붙여줍니다.

# 312명의 사람 데이터 합쳐주기library(dplyr)result <- total[[6]]for(i in 1:length(total)) {result <- bind_rows(result, total[[i]][2,])}# 필요없는 행과 중복된 행 제거result <- result[-1,]result <- unique(result)# NA 값이 너무 많은 열 13 부터 열 22까지 지우기result <- result[,-c(13:22)]# 열 1 ~ 8 가 모두 NA 인 행 지우기result <- result[(rowSums(is.na(result)) < 8),]# 신체적 상태 숫자로 표현 ( 1 ~ 3)result$신체적상태 <- substr(result$신체적상태, 1, 1)
# 형 변환result[,1:8] <- as.numeric(unlist(result[,1:8]))result$집도과1 <- as.factor(result$집도과1)result$마취의1 <- as.factor(result$마취의1)result$신체적상태 <- as.factor(result$신체적상태)result$class1 <- as.factor(result$class1)
  • 리스트 요소를 모두 합쳐 데이터 프레임 생성 (bind_rows 함수 이용)
  • 만들어진 데이터 프레임을 다시 한 번 전처리

- (1) 필요없는 행과 중복되는 행을 제거해줍니다.

- (2) NA 값이 너무 많이 존재하는 열13 ~ 열22까지 지워줍니다

- (3) [단계 (2)]를 끝낸 데이터에서 NA 값이 8개 이상 존재하는 행을 지워줍니다.

- (4) “신체적상태”를 1 ~ 3으로 표현해줍니다

- (5) 모든 열들을 모델링하기 위해 형을 변환해줍니다.

2. Modeling

# train / test set 나누기set.seed(123)index <- sample.int(nrow(result), nrow(result)*0.7)re_train <- result[index,]re_test <- result[-index,]# NA 처리안한 rpart 모델library(rpart)fit <- rpart(formula = class1 ~., data = re_train)pred <- predict(fit, re_test, type = "class")tb <- table(re_test$class1, pred)sum(diag(tb))/sum(tb) # 정확도 = 58.6%
  • NA 값을 처리하지 않은 rpart 모델의 정확도는 58.6%
# librarylibrary(DMwR)library(caret)library(randomForest)library(e1071)library(C50)library(adabag)library(class)# knn Imputation 으로 NA 값 대체 해보기# 1 ~ 100 => 최적의 k = 50set.seed(123)a <- knnImputation(result, 50)names(a) <- c("nbps", "nbpd", "nbpm", "hr", "spo2", "bis", "tofratio", "tofcount", "집도과", "마취의", "신체적상태", "class")
  • k-nn imputation을 이용해 NA 값 처리

- (1) k-nn Imputation을 실시한 후, 열의 이름을 재지정해줍니다.

*k Nearest Neighbor Imputation = knn을 사용해 찾은 k개 주변 이웃의 값을 주변 이웃까지의 거리를 고려해 가중 평균한 값으로 대체하는 것.

- MODEL = 사용한 변수

-“NBP-S, NBP-D, NBP-M, HR, SPO2, BIS, TOF ratio, TOF count, 집도과, 마취의, 신체적상태”

  • Random Forest
# 랜덤 포레스트 모델a.fit <- randomForest(class ~ ., data = a_train,mtry = floor(sqrt(11)), ntree = 500, importance = T)a.pred <- predict(a.fit, a_test)tb.a <- table(a.pred, a_test$class)sum(diag(tb.a))/sum(tb.a) # 정확도 = 73.9%

- (1) 랜덤포레스트 모형 — mtry(분류 문제이기 때문에 sqrt(변수의 개수) 실시)는 각각의 tree마다 몇 개의 feature를 사용할 것인지 정하는 것이며, ntree는 tree의 총 개수를 의미합니다.

- (2) 정확도는 73.9% 정도로 나오는 것을 볼 수 있습니다.

  • SVM
# SVM 모델b.fit <- svm(class ~ ., data = a_train)b.pred <- predict(b.fit, a_test)tb.b <- table(b.pred, a_test$class)sum(diag(tb.b))/sum(tb.b) # 정확도 = 66.3%

- 정확도는 66.3% 정도로 나오는 것을 볼 수 있습니다.

  • Boosting
# 부스팅# trials => 50, 100  결과 비슷함c_bst <- C5.0(class ~ ., data = a_train, trials = 100)c_c50 <- C5.0(class ~ ., data = a_train)summary(c_c50) # Errors = 17.3% / 정확도 = 82.6%set.seed(300)m_ada <- boosting(class ~ ., data = a_train)p_ada <- predict(m_ada, a)p_ada$confusionsum(diag(p_ada$confusion))/sum(p_ada$confusion) # 89.2%

- (1) 과적합 문제로 채택하지 않았습니다.

  • 최종 Model로 Random Forest 채택
Random Forest Confusion Matrix

--

--