이번 연도에 AI 융합학과로 전과하면서 눈코뜰 새 없이 바쁘게 지내고 있다.
마음 맞는 동기들과 팀을 꾸려서 이런저런 AI 챌린지들도 계속 나가고 있고, 중간고사 기간 직전에는 원주로 대회를 다녀왔다!
그래서 중간고사가 끝난 지금에서야 대회 후기를 업로드할 수 있게 되었다.
🔍 AI 기반 의료 데이터 분석 경진대회?
AI 기반 의료 데이터 분석 경진대회는 4월 11일에서 12일 양일간 진행된 대회로, 원주 오크밸리에서 열렸다.
1차로 지원서를 작성했고 본선에 나가는 데에만 경쟁률이 2:1이었다. 다행히 1차를 통과할 수 있었다.
본선에는 총 3가지의 주제가 있었다. 3가지 주제 중 하나를 고르는 것이 아니라 모두 진행해야 했다. 1번과 2번 주제가 3:7의 비율로 팀전 순위가 나뉘었고 3번은 개인전으로 진행됐다.
1. 건강보험공단 의료 데이터 분석
2. 폐렴 이미지 모델 생성과 F1 score 개선
3. Stable Diffusion을 이용한 이미지 생성
한 눈에 봐도 알 수 있겠지만 3개의 주제를 하룻밤만에 진행하려면 밤샘이 필요했다.
대회 측에서는 5명의 팀원을 권장했지만 우리 팀은 3명으로 출전했다. 아주. 슬픈.. 행동이었다.
당연히 3번 개인전 나가는 건 꿈도 못 꿨다. 우스갯소리로 3번 나가면 배신이라고 팀원들과 얘기를 나눈 기억이 있다.
🔍 출발 ! ! !
교통편은 지원되지 않아 팀원들과 시외버스를 타고 원주로 향했다. 시외버스 터미널에서 오크밸리까지는 셔틀버스가 운영되어 편하게 갈 수 있었다. 대회가 진행된 오크밸리 그랜드볼룸은 엄청 웅장했다. 내가 본 대회장 중 가장 웅장했다...
🔍 대회 시작!
일단 우리 조는 3명이었기 때문에 각 주제를 맡아 진행했다. 내가 마침 하루 전에 올라왔던 의료 데이터에 대한 피처 엔지니어링을 진행해 놓았던 상태라 1번 주제를 맡아 준비하게 되었다. 2번째 주제의 배점이 크니 2번 주제는 다른 조원 두 명이 담당했다. 그래서 2번 주제에 대해서는 아는 것이 많이 없다. 하하!
일단 분석할 데이터는 대회 하루 전날(4/10)에 업로드되었다. 의료 데이터를 활용해 본 경험이 아예 없었기 때문에 최대한 빠르게, 데이터가 올라오자마자 확인해 보았다. 큰 틀로 설명하자면 해당 데이터는 2011년부터 2017년도의 10만 명의 건강 데이터였다.
일단 처음에 대회 측에서 업로드해 준 데이터에 대한 설명이 전혀 없었다.
때문에 이게 무슨 데이터인지, 어떤 피처들이 있는지, 각 피처에는 어떤 level이 있는지, 이해가 필요했다. 예를 들어 해당 데이터 파일 내에서 요단백이 0~5 사이의 숫자로 표현되었는데 해당 레벨이 어떤 의미를 가지고 있는지, 어디부터 주의를 요하는 level인 건지, 시력과 청력 또한 0과 1로 이루어진 이진 데이터인데 어떤 것이 정상을 의미하는지, 이상을 의미하는지 등을 말이다.
이런 부분에서 의료 데이터와 관련한 도메인 지식이 부족함을 느꼈고 이에 대해 더 자세하고 원칙적으로 분석해야겠다는 생각이 들었다. 그래서 피처 엔지니어링을 하는 데에 있어서 신뢰할만한 근거와 척도를 찾으려 노력했다.
피처들이 모두 '총 콜레스테롤, HDL 콜레스테롤' 등의 성분으로 이루어져 있었고, 이 데이터에서 어떠한 인사이트를 이끌어내기 위해서는 피처 엔지니어링이 꼭 필요하다고 생각했다. 이 정도로 초안 구성을 끝내고 데이터 전처리에 들어갔다. 일단 각 년도마다 데이터에 대한 칼럼수가 달랐고, 칼럼 이름이 달라 통일해 주었다. 또한 허리둘레가 1000인 등의 이상치도 존재해서 각 칼럼마다 이상치 확인 후 제거해 주었다.
🔍 피처 엔지니어링
제작한 피처 | 사용된 피처 |
비만도 평가 | 신장과 체중 데이터를 활용해 BMI 계산 |
고혈압 위험도 | 수축기 혈압, 이완기혈압 |
당뇨병 위험도 | 식전혈당 데이터 |
심혈관 질환 위험 평가 | 총콜레스테롤, 트리글리세라이드, HDL 콜레스테롤, LDL 콜레스테롤 등의 혈청 데이터 |
간 건강 평가 | (혈청지오티)AST, (혈청지오티)ALT, 감마지피티 |
신장 기능 | 요단백(level 3부터 주의), 혈청크레아티닌(정상 범위 0.50~1.4 mg/dL) |
위의 표와 같이 피처 엔지니어링을 진행했다. 데이터의 각 칼럼에 대해 찾아보니 모두 성인병과 관련한 성분들임을 확인할 수 있었다.
그리고 정말 의외로.. 애를 먹었던 피처는 '시도코드'였다. 각 시와 도에 매핑되어 있는 코드였는데 어떤 기준을 따르는지 알기 어려웠다. 가장 근접한 코드는 행정기관코드였는데 세종특별자치도, 강원특별자치도 등으로 도 명이 변경되는 바람에 연도 또한 확인이 필요했다. 해당 데이터가 11년부터 17년 사이의 데이터였기 때문에 2008년의 행정기관코드까지 확인해 봤는데 몇 개의 코드가 일치하지 않았고.. 이를 찾는 데에만 시간을 좀 소비했다. 결국 찾지 못했고 다음날 대회에 가서 문의한 후에야 알 수 있었다는.. 답은 표본코호트 매뉴얼 파일이었다.
사실 피처 엔지니어링을 진행한 코드는 너무 간단해서 공개할까 말까 고민했는데 첨부한다.
✏️ 1. 비만도 평가
사실 허리둘레도 포함시켰으면 좋았겠다는 생각이 들지만 당시 발표 자료를 준비하느라 구현하지 못했다.
대한비만학회의 자료를 참고해 제작했다. (출처)
def calculate_bmi(weight, height_cm):
if pd.isnull(weight) or pd.isnull(height_cm):
return np.nan
height_m = height_cm / 100.0
bmi = weight / (height_m ** 2)
return int(bmi)
def bmi_category(bmi):
if bmi < 18.5:
return '저체중'
elif 18.5 <= bmi <= 24.9:
return '정상'
elif 25 <= bmi <= 29.9:
return '과체중'
else:
return '비만'
df_all['BMI'] = df_all.apply(lambda row: calculate_bmi(row['체중(5Kg단위)'], row['신장(5Cm단위)']), axis=1)
df_all['비만도'] = df_all['BMI'].apply(bmi_category)
✏️ 2. 고혈압 위험도 평가
고혈압 위험도 평가는 국가건강정보포털의 2018 고혈압 진료지침에 따른 혈압의 분류 표를 참고했다. (출처)
def hypertension_risk(systolic_bp, diastolic_bp):
if systolic_bp < 120 and diastolic_bp < 80:
return '정상'
elif 120 <= systolic_bp < 140 or 80 <= diastolic_bp < 90:
return '전고혈압'
elif 140 <= systolic_bp or 90 <= diastolic_bp:
return '고혈압'
df_all['고혈압위험도'] = df_all.apply(lambda row: hypertension_risk(row['수축기혈압'], row['이완기혈압']), axis=1)
✏️ 3. 당뇨병 위험도 평가
대한진단검사의학회의 당뇨병 진단 기준을 참고했다. (출처)
def diabetes_risk(fasting_glucose):
if fasting_glucose < 100:
return '정상'
elif 100 <= fasting_glucose < 126:
return '당뇨병전단계(공복혈당장애)'
else:
return '당뇨병'
df_all['당뇨병위험도'] = df_all['식전혈당(공복혈당)'].apply(diabetes_risk)
✏️ 4. 고지혈증 위험도 평가
전남대병원 홈페이지의 고지혈증 진단 기준을 참고했다. (출처)
def evaluate_hyperlipidemia(total_cholesterol, ldl_cholesterol, hdl_cholesterol, triglycerides):
if total_cholesterol >= 230:
total_cholesterol_risk = '총콜레스테롤 높음'
elif 200 <= total_cholesterol < 230:
total_cholesterol_risk = '총콜레스테롤 경계'
else:
total_cholesterol_risk = '총콜레스테롤 정상'
if ldl_cholesterol >= 150:
ldl_cholesterol_risk = 'LDL콜레스테롤 높음'
elif 130 <= ldl_cholesterol < 150:
ldl_cholesterol_risk = 'LDL콜레스테롤 경계'
elif 100 <= ldl_cholesterol < 130:
ldl_cholesterol_risk = 'LDL콜레스테롤 정상'
else:
ldl_cholesterol_risk = 'LDL콜레스테롤 적정'
if hdl_cholesterol >= 60:
hdl_cholesterol_risk = 'HDL콜레스테롤 정상'
elif 40 < hdl_cholesterol < 60:
hdl_cholesterol_risk = 'HDL콜레스테롤 적정'
else:
hdl_cholesterol_risk = 'HDL콜레스테롤 낮음'
if triglycerides >= 200:
triglycerides_risk = '트리글리세라이드 높음'
elif 150 <= triglycerides < 200:
triglycerides_risk = '트리글리세라이드 경계'
else:
triglycerides_risk = '트리글리세라이드 정상'
risks = [total_cholesterol_risk, ldl_cholesterol_risk, hdl_cholesterol_risk, triglycerides_risk]
high_risks = [risk for risk in risks if '높음' in risk or '낮음' in risk]
if high_risks:
return '고지혈증 위험'
else:
return '고지혈증 위험 낮음'
df_all['고지혈증위험도'] = df_all.apply(lambda row: evaluate_hyperlipidemia(row['총콜레스테롤'],
row['LDL콜레스테롤'],
row['HDL콜레스테롤'],
row['트리글리세라이드']), axis=1)
print(df_all[['총콜레스테롤', 'LDL콜레스테롤', 'HDL콜레스테롤', '트리글리세라이드', '고지혈증위험도']].head())
df_all.drop(['총콜레스테롤', 'LDL콜레스테롤', 'HDL콜레스테롤', '트리글리세라이드'], axis=1, inplace=True)
✏️ 5. 간 건강 위험도 평가
간수치의 경우에는 남성과 여성의 진단 기준이 다른 점을 고려했다. 그러나 자료들마다 정상 및 위험 기준이 조금씩 달랐다. 경계선이 낮은 GC녹십자의료재단의 자료를 사용해 구현했다. (출처)
def liver_health_risk_gender(AST, ALT, GGT, gender_code):
# 남성의 경우
if gender_code == 1:
if AST <= 40 and ALT <= 41 and GGT <= 63:
return '정상'
else:
return '주의 및 위험'
# 여성의 경우
else:
if AST <= 32 and ALT <= 33 and GGT <= 35:
return '정상'
else:
return '주의 및 위험'
df_all['간건강위험도'] = df_all.apply(lambda row: liver_health_risk_gender(row['(혈청지오티)AST'], row['(혈청지오티)ALT'],
row['감마지티피'], row['성별코드']), axis=1)
print(df_all[['(혈청지오티)AST', '(혈청지오티)ALT', '감마지티피', '간건강위험도']].head())
df_all.drop(['(혈청지오티)AST', '(혈청지오티)ALT', '감마지티피'], axis=1, inplace=True)
✏️ 6. 신장 위험도
신장 위험도는 대회 측에서 제공했던 데이터의 표준 지침인 '표본코호트DB_사용자매뉴얼' 파일을 참고해 제작했다.
def renal_disorder_risk(proteinuria, creatinine):
if proteinuria < 3 and 0.5 <= creatinine < 1.4:
return '정상'
else:
return '주의 및 위험'
df_all['신장장애위험도'] = df_all.apply(lambda row: renal_disorder_risk(row['요단백'], row['혈청크레아티닌']), axis=1)
🔍 발표
위 자료들을 시도 코드를 기준으로 하여 발표 자료를 만들었고, 발표를 진행했다.
🔍 결과
수상은 하지 못하였고, 수료증을 받게 되었다. 사실 상보다 값진 배움을 얻었고, 2024 상반기 포인트가 되었다!
🔍 아쉬웠던 점 / 배운 것
수상을 하지 못했음에도 후기를 올리는 이유가 있다...! 배운 것이 많았다.
얼마나 배운 것이 많았던지 대회가 끝난 당일날 집에 돌아와 간단하게 작성해 놓았던 글이 있었다.
1. 적은 인원수
이 대회는 한 팀에 5명을 권장했다. 이유가 있었다. 해야 할 것들이 많았다.
주제가 총 3개고, 마지막 주제인 개인전을 아예 제외하더라도 3명이 진행하기에는 할 게 너무 많았다. 주제 2는 아예 당일 공개였고, 주제 1은 전날 데이터 공개였다.
내가 맡았던 주제 1에 대해서만 얘기를 해보자면, 분석 -> 발표 자료 준비 -> 발표까지의 전반적인 흐름이 아쉬웠다. 여기서 '아쉬웠다'의 의미는 더 잘할 수 있었는데 시간 부족으로 하지 못해 아쉬웠다는 의미다. 내 접근으로 더 촘촘하게 준비해 발표를 했더라면 더 좋은 성과가 있지 않았을까 싶다. 해당 데이터로 보여주고 싶었던 건 많았는데 보여주지 못해 아쉽다.
2. 데이터 분석에 대한 지식 및 실력 부족
다시 생각해 보면 접근은 나쁘지 않았다고 생각한다. 각 시도별로 성인병 발병률을 확인해서 이에 대해 얘기하려 했다. 나의 계획은 지방에서 성인병 발병률이 높고, 그에 대해 의료 불균형, 더 나아가 디지털 헬스케어가 필요한 이유에 대해 말하는 것이었다.
그러나 발표 자료와 발표 준비, "나의 분석정보를 어떻게 보여줄 것인가," 이 부분에서 너무 부족했다. 데이터 분석에 대한 지식이 부족했고, 대회에서 제공한 PowerBI 툴을 배우고 응용하는 데에만 정신이 쏠려 있었다. 꼭 해당 툴을 이용하지 않아도 좋은 시각화 자료를 뽑아냈으면 됐는데 말이다.
그리고 발표할 때에는 나의 raw 한 코드를 넣을 필요는 없다는 것을 알았다. 코드를 깔끔하게 리팩터링 하고 ppt에 첨부할 시간에 분석에 대한, 발표에 대한 근거부터 결론까지, 큰 흐름을 만들어나가는 것이 중요함을 깨달았다.
그래서 이런 부족한 점을 보완하고자 대회 다음날, 바로 데이터 분석 서적을 구매했다.
정말 많이, 열심히 공부해야겠다는 생각이 들었다.
3. 나의 인사이트를 어떻게 전달할 것인가? 근거가 탄탄한가?
나와 같은 접근으로 발표한 팀이 2팀 더 있었다. 모두 결론은 의료 불균형이었다. 이 두 팀 중 한 팀은 대상팀이기도 하다. 그러나 이 접근으로 발표한 팀에게 공통적으로 들어왔던 피드백이 있었다. 과연 각 시도별 성인병 발병률을 확인해서 '의료 불균형'이라는 결론을 이끌어낼 수 있는가? 였다. 대상팀의 경우 깔끔한 발표자료와 짜임새 있는 발표로 이 의문점에 대한 감점을 많이 줄인 것 같다.
다시 와서 생각해 보니 인구통계학적 요인들을 고려했으면 성인병 발병률과 의료 불균형을 연결 지었을 수 있었을 것이다.