Home DACON 병원 개업/폐업 분류 예측 경진대회
Post
Cancel

DACON 병원 개업/폐업 분류 예측 경진대회

DACON 병원 개/폐업 분류 예측 경진대회

DACON 병원 개/폐업 분류 예측 경진대회

EDA를 좀 더 꼼꼼하고 자세하게 하자..
Catboost를 사용해보는것도 괜찮았을꺼 같기도하고..

사용 라이브러리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import koreanize_matplotlib

from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix, f1_score, roc_auc_score, mean_squared_error, mean_absolute_error, r2_score
from sklearn.ensemble import RandomForestClassifier

from lightgbm import LGBMClassifier
import lightgbm as lgbm
from xgboost import XGBClassifier
import xgboost as xgb

import warnings
warnings.filterwarnings("ignore")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def eval_CM(y_test, y_pred=None, show_cm=0):
    confusion = confusion_matrix(y_test, y_pred)
    acc = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    if show_cm:
        print(f"정확도: {acc:.4f}\n정밀도: {precision:.4f}\n재현율: {recall:.4f}\nF1: {f1:.4f}")
    else:
        print(confusion)
        print(f"정확도: {acc:.4f}\n정밀도: {precision:.4f}\n재현율: {recall:.4f}\nF1: {f1:.4f}")

def reg_score(y_true, y_pred):
    MSE = mean_squared_error(y_true, y_pred)
    RMSE = np.sqrt(mean_squared_error(y_true,y_pred))
    MAE = np.mean( np.abs((y_true - y_pred) / y_true) )
    NMAE = mean_absolute_error(y_true, y_pred)/ np.mean( np.abs(y_true) )
    MAPE = np.mean( np.abs((y_true - y_pred) / y_true) ) *100
    R2 = r2_score(y_true, y_pred)
    
    print(f"MSE: {np.round(MSE, 3)}\nRMSE: {np.round(RMSE, 3)}\nMAE: {np.round(MAE, 3)}\nNMAE: {np.round(NMAE, 3)}\nMAPE: {np.round(MAPE, 3)}\nR2: {np.round(R2, 3)}")

Data Load

1
2
3
4
import glob

path = glob.glob("data/*")
path
1
2
3
4
5
['data\\pre_test.csv',
 'data\\pre_train.csv',
 'data\\submission_sample.csv',
 'data\\test.csv',
 'data\\train.csv']
1
2
3
train, test = pd.read_csv(path[4], encoding="cp949"), pd.read_csv(path[3], encoding="cp949")

train.shape, test.shape
1
((301, 58), (127, 58))

EDA 및 전처리

기본 정보

1
2
display(train.head())
display(test.head())
inst_idOCsidosggopenDatebedCountinstkindrevenue1salescost1sga1...debt2liquidLiabilities2shortLoan2NCLiabilities2longLoan2netAsset2surplus2employee1employee2ownerChange
01openchoongnam7320071228175.0nursing_hospital4.217530e+090.03.961135e+09...7.589937e+082.228769e+080.000000e+005.361169e+083.900000e+082.619290e+091.271224e+0962.064.0same
13opengyeongnam3219970401410.0general_hospitalNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaN801.0813.0same
24opengyeonggi8920161228468.0nursing_hospital1.004522e+09515483669.04.472197e+08...0.000000e+000.000000e+000.000000e+000.000000e+000.000000e+000.000000e+000.000000e+00234.01.0same
37openincheon14120000814353.0general_hospital7.250734e+100.07.067740e+10...3.775501e+101.701860e+109.219427e+092.073641e+101.510000e+101.295427e+107.740829e+09663.0663.0same
49opengyeongnam3220050901196.0general_hospital4.904354e+100.04.765605e+10...5.143259e+103.007259e+101.759375e+102.136001e+101.410803e+105.561941e+069.025550e+09206.0197.0same

5 rows × 58 columns

inst_idOCsidosggopenDatebedCountinstkindrevenue1salescost1sga1...debt2liquidLiabilities2shortLoan2NCLiabilities2longLoan2netAsset2surplus2employee1employee2ownerChange
02NaNincheon13919981125.0300.0general_hospital6.682486e+100.000000e+006.565709e+10...5.540643e+105.068443e+103.714334e+104.720000e+094.690000e+091.608540e+108.944587e+09693693same
15NaNjeju14920160309.044.0hospital3.495758e+100.000000e+003.259270e+10...6.730838e+104.209828e+102.420000e+102.521009e+101.830000e+103.789135e+090.000000e+00379371same
26NaNjeonnam10319890427.0276.0general_hospital2.326031e+102.542571e+092.308749e+10...0.000000e+002.777589e+102.182278e+100.000000e+000.000000e+000.000000e+001.638540e+10NaNNaNNaN
38NaNbusan7120100226.0363.0general_hospital0.000000e+000.000000e+000.000000e+00...1.211517e+109.556237e+094.251867e+092.558931e+090.000000e+003.914284e+100.000000e+00760760same
410NaNjeonbuk2620040604.0213.0general_hospital5.037025e+100.000000e+004.855803e+10...4.395973e+107.535567e+093.298427e+093.642417e+102.134712e+102.574488e+101.507269e+10437385same

5 rows × 58 columns

  • inst_id - 각 파일에서의 병원 고유 번호
  • OC – 영업(1)/폐업(0) 분류
  • sido – 병원의 광역 지역 정보
  • sgg – 병원의 시군구 자료
  • openDate – 병원 설립일
  • bedCount - 병원이 갖추고 있는 병상의 수
  • instkind – 병원, 의원, 요양병원, 한의원, 종합병원 등 병원의 종류
    • 종합병원 : 입원환자 100명 이상 수용 가능
    • 병원 : 입원 환자 30명 이상 100명 미만 수용 가능
    • 의원 : 입원 환자 30명 이하 수용 가능
    • 한방 병원(한의원) : 침술과 한약으로 치료하는 의료 기관
  • revenue1 – 매출액, 2017(회계년도)년 데이터를 의미함
  • salescost1 – 매출원가, 2017(회계년도)년 데이터를 의미함
  • sga1 - 판매비와 관리비, 2017(회계년도)년 데이터를 의미함
  • salary1 – 급여, 2017(회계년도)년 데이터를 의미함
  • noi1 – 영업외수익, 2017(회계년도)년 데이터를 의미함
  • noe1 – 영업외비용, 2017(회계년도)년 데이터를 의미함
  • Interest1 – 이자비용, 2017(회계년도)년 데이터를 의미함
  • ctax1 – 법인세비용, 2017(회계년도)년 데이터를 의미함
  • Profit1 – 당기순이익, 2017(회계년도)년 데이터를 의미함
  • liquidAsset1 – 유동자산, 2017(회계년도)년 데이터를 의미함
  • quickAsset1 – 당좌자산, 2017(회계년도)년 데이터를 의미함
  • receivableS1 - 미수금(단기), 2017(회계년도)년 데이터를 의미함
  • inventoryAsset1 – 재고자산, 2017(회계년도)년 데이터를 의미함
  • nonCAsset1 – 비유동자산, 2017(회계년도)년 데이터를 의미함
  • tanAsset1 – 유형자산, 2017(회계년도)년 데이터를 의미함
  • OnonCAsset1 - 기타 비유동자산, 2017(회계년도)년 데이터를 의미함
  • receivableL1 – 장기미수금, 2017(회계년도)년 데이터를 의미함
  • debt1 – 부채총계, 2017(회계년도)년 데이터를 의미함
  • liquidLiabilities1 – 유동부채, 2017(회계년도)년 데이터를 의미함
  • shortLoan1 – 단기차입금, 2017(회계년도)년 데이터를 의미함
  • NCLiabilities1 – 비유동부채, 2017(회계년도)년 데이터를 의미함
  • longLoan1 – 장기차입금, 2017(회계년도)년 데이터를 의미함
  • netAsset1 – 순자산총계, 2017(회계년도)년 데이터를 의미함
  • surplus1 – 이익잉여금, 2017(회계년도)년 데이터를 의미함
  • revenue2 – 매출액, 2016(회계년도)년 데이터를 의미함
  • salescost2 – 매출원가, 2016(회계년도)년 데이터를 의미함
  • sga2 - 판매비와 관리비, 2016(회계년도)년 데이터를 의미함
  • salary2 – 급여, 2016(회계년도)년 데이터를 의미함
  • noi2 – 영업외수익, 2016(회계년도)년 데이터를 의미함
  • noe2 – 영업외비용, 2016(회계년도)년 데이터를 의미함
  • interest2 – 이자비용, 2016(회계년도)년 데이터를 의미함
  • ctax2 – 법인세비용, 2016(회계년도)년 데이터를 의미함
  • profit2 – 당기순이익, 2016(회계년도)년 데이터를 의미함
  • liquidAsset2 – 유동자산, 2016(회계년도)년 데이터를 의미함
  • quickAsset2 – 당좌자산, 2016(회계년도)년 데이터를 의미함
  • receivableS2 - 미수금(단기), 2016(회계년도)년 데이터를 의미함
  • inventoryAsset2 – 재고자산, 2016(회계년도)년 데이터를 의미함
  • nonCAsset2 – 비유동자산, 2016(회계년도)년 데이터를 의미함
  • tanAsset2 – 유형자산, 2016(회계년도)년 데이터를 의미함
  • OnonCAsset2 - 기타 비유동자산, 2016(회계년도)년 데이터를 의미함
  • receivableL2 – 장기미수금, 2016(회계년도)년 데이터를 의미함
  • Debt2 – 부채총계, 2016(회계년도)년 데이터를 의미함
  • liquidLiabilities2 – 유동부채, 2016(회계년도)년 데이터를 의미함
  • shortLoan2 – 단기차입금, 2016(회계년도)년 데이터를 의미함
  • NCLiabilities2 – 비유동부채, 2016(회계년도)년 데이터를 의미함
  • longLoan2 – 장기차입금, 2016(회계년도)년 데이터를 의미함
  • netAsset2 – 순자산총계, 2016(회계년도)년 데이터를 의미함
  • surplus2 – 이익잉여금, 2016(회계년도)년 데이터를 의미함
  • employee1 – 고용한 총 직원의 수, 2017(회계년도)년 데이터를 의미함
  • employee2 – 고용한 총 직원의 수, 2016(회계년도)년 데이터를 의미함
  • ownerChange – 대표자의 변동

컬럼명 패턴이, 1이 붙으면 2017년 데이터, 2가 붙으면 2016년 데이터임

1
_ = train.hist(bins=50, figsize=(20, 18))

png

결측치 확인 및 이상치 처리

1
2
3
4
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(20, 7))
sns.heatmap(train.isnull(), ax=ax[0]).set_title("Train Missing")
sns.heatmap(test.isnull(), ax=ax[1]).set_title("Test Missing")
plt.show()

png

OC - 영업/폐업 분류
1
train["OC"].isnull().sum(), test["OC"].isnull().sum()
1
(0, 127)

OC는 예측해야하는 값이므로 test에는 결측치가 존재하는 것이 당연함

1
_ = sns.countplot(x=train["OC"]).set_title("Train - OC")

png

1
train["OC"].value_counts(normalize=True)*100
1
2
3
open      95.016611
 close     4.983389
Name: OC, dtype: float64

오버 샘플링을 고려해 볼 만함

sido - 광역 지역 정보
1
train["sido"].isnull().sum(), test["sido"].isnull().sum()
1
(0, 0)
1
2
3
4
fig, ax = plt.subplots(1, 2, figsize=(28, 8))
sns.countplot(data=train, x="sido", ax=ax[0]).set_title("Train - sido")
sns.countplot(data=test, x="sido", ax=ax[1]).set_title("Test - sido")
plt.show()

png

광역 지역을 좀 더 일반화 시키는 것이 좋을꺼 같다는 생각을 함

1
set(train["sido"].value_counts().index) - set(test["sido"].value_counts().index)
1
{'gangwon', 'gwangju'}
1
train[train["sido"]=='gangwon'].shape, test[test["sido"]=='gangwon'].shape
1
((10, 58), (0, 58))
1
train[train["sido"]=='gwangju'].shape, test[test["sido"]=='gwangju'].shape
1
((2, 58), (0, 58))

강원도와 광주의 경우 train에는 존재하지만 test에는 존재하지 않음

1
set(test["sido"].value_counts().index) - set(train["sido"].value_counts().index)
1
{'jeju'}
1
train[train["sido"]=='jeju'].shape, test[test["sido"]=='jeju'].shape
1
((0, 58), (3, 58))

반대로, 제주도는 test에는 존재하지만 train에는 존재하지 않음
먼저, 북과 남을 합쳐 도로 만들어 개수를 줄여봄

1
2
3
4
5
6
# ~남 ~북을 제거, ex. 충남 -> 충 / 충북 -> 충
train["sido"] = train["sido"].str.replace("nam|buk", "")
test["sido"] = test["sido"].str.replace("nam|buk", "")
# 인천과 경기를 묶어줌
train["sido"] = train["sido"].str.replace("gyeonggi|incheon", "gyeon-in")
test["sido"] = test["sido"].str.replace("gyeonggi|incheon", "gyeon-in")

특별시나 광역시도 그냥 도로 편입 시킬까하다 진행하지 않음
보기 좋게 한글명으로 변환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
train["sido"] = train["sido"].replace({"busan": "부산",
                                        "choong": "충청도",
                                        "daegu": "대구",
                                        "daejeon": "대전",
                                        "gangwon": "강원도",
                                        "gwangju": "광주",
                                        "gyeon-in": "경인",
                                        "gyeong": "경상도",
                                        "jeju": "제주도",
                                        "jeon": "전라도",
                                        "sejong": "세종",
                                        "seoul": "서울",
                                        "ulsan": "울산"})
test["sido"] = test["sido"].replace({"busan": "부산",
                                        "choong": "충청도",
                                        "daegu": "대구",
                                        "daejeon": "대전",
                                        "gangwon": "강원도",
                                        "gwangju": "광주",
                                        "gyeon-in": "경인",
                                        "gyeong": "경상도",
                                        "jeju": "제주도",
                                        "jeon": "전라도",
                                        "sejong": "세종",
                                        "seoul": "서울",
                                        "ulsan": "울산"})
1
2
3
4
fig, ax = plt.subplots(1, 2, figsize=(28, 8))
sns.countplot(data=train, x="sido", ax=ax[0]).set_title("Train - sido")
sns.countplot(data=test, x="sido", ax=ax[1]).set_title("Test - sido")
plt.show()

png

TrainTest에 공통적으로 존재하지 않는 데이터도 있어서, 모델 생성 및 평가시 조금 문제가 있었음
앞으로는 이런 부분을 염두하고 좀 더 EDA 및 전처리를 해야 할 듯..

sgg - 시군구 자료
1
train["sgg"].isnull().sum(), test["sgg"].isnull().sum()
1
(0, 0)
openDate - 병원 설립일

데이터 타입을 datetime으로 변환하고 년/월만 남김

1
train["openDate"].isnull().sum(), test["openDate"].isnull().sum()
1
(0, 1)
1
test["openDate"] = test["openDate"].fillna(0)
1
2
train["openDate"] = pd.to_datetime(train["openDate"].astype("str"), format="%Y/%m/%d")
test["openDate"] = pd.to_datetime(test["openDate"].astype("int").astype("str"), format="%Y/%m/%d", errors="coerce")
1
2
3
4
5
6
7
train["open_year"] = train["openDate"].dt.year
train["open_month"] = train["openDate"].dt.month
test["open_year"] = test["openDate"].dt.year
test["open_month"] = test["openDate"].dt.month

train.drop(columns="openDate", axis=1, inplace=True)
test.drop(columns="openDate", axis=1, inplace=True)
1
2
3
4
fig, ax = plt.subplots(1, 2, figsize=(32, 8))
sns.countplot(data=train, x="open_year", ax=ax[0]).set_title("Train - Year")
sns.countplot(data=test, x="open_year", ax=ax[1]).set_title("Test - Year")
plt.show()

png

1
2
3
4
fig, ax = plt.subplots(1, 2, figsize=(32, 8))
sns.countplot(data=train, x="open_month", ax=ax[0]).set_title("Train - Month")
sns.countplot(data=test, x="open_month", ax=ax[1]).set_title("Test - Month")
plt.show()

png

bedCount - 병원이 갖추고 있는 병상의 수
1
train["bedCount"].isnull().sum(), test["bedCount"].isnull().sum()
1
(5, 8)

결측치는 0으로 처리하는 방법도 있으나, 병원의 종류를 나타내는 instkind와 관련이 있을것이라고 생각함
침상의 수를 구간별로 나누고, 없는 부분도 정보로써 활용해볼 예정

의원은 30병상 미만의 의료기관입니다.
30~100병상 미만을 ‘병원’이라고 합니다.
100~500병상 미만이면서 일정 수의 진료과목이 있고 진료과목마다 전문의를 두는 등의 특정 조건을 충족하면 종합병원으로 분류
500병상 이상이면서 특정 조건을 충족하면 상급종합병원 자격이 됨

이라는 뉴스 기사를 참고해 이용했음

1
_ = sns.histplot(data=train, x="bedCount")

png

1
2
3
4
5
def bedCount2band(num):
    if num<30: return "의원"
    elif 30<=num<100: return "병원"
    elif 100<=num<500: return "종합병원"
    elif num>=500: return "상급종합병원"
1
2
train["bedCount"] = train["bedCount"].apply(bedCount2band)
test["bedCount"] = test["bedCount"].apply(bedCount2band)
1
2
3
4
fig, ax = plt.subplots(1, 2, figsize=(20, 5))
sns.countplot(data=train, x="bedCount", ax=ax[0]).set_title("Train - bedCount")
sns.countplot(data=test, x="bedCount", ax=ax[1]).set_title("Test - bedCount")
plt.show()

png

instkind - 병원의 종류
1
train["instkind"].isnull().sum(), test["instkind"].isnull().sum()
1
(1, 2)
1
2
display(train[train["instkind"].isnull()])
display(test[test["instkind"].isnull()])
inst_idOCsidosggbedCountinstkindrevenue1salescost1sga1salary1...shortLoan2NCLiabilities2longLoan2netAsset2surplus2employee1employee2ownerChangeopen_yearopen_month
193281close경인12NoneNaN305438818.022416139.0467475340.0254868810.0...0.00.00.00.00.015.015.0change201212

1 rows × 59 columns

inst_idOCsidosggbedCountinstkindrevenue1salescost1sga1salary1...shortLoan2NCLiabilities2longLoan2netAsset2surplus2employee1employee2ownerChangeopen_yearopen_month
120413NaN경인168병원NaN5.583625e+087.443415e+075.482900e+082.826852e+08...0.00.000000e+000.000000e+000.00.02121sameNaNNaN
125430NaN제주도76NoneNaN4.892710e+104.157148e+104.721485e+091.514547e+09...0.02.871805e+102.563120e+10-205062936.00.0363343same2001.02.0

2 rows × 59 columns

1
train["instkind"].unique()
1
2
3
array(['nursing_hospital', 'general_hospital', 'hospital',
       'traditional_clinic', 'clinic', 'traditional_hospital',
       'dental_clinic', nan], dtype=object)
float형 변수

결측치는 -999로 대체
해당 부분도 시간적 여유를 가지고 이상치 탐색 및 결측치 처리를 좀 더 했어야했음..

1
2
3
same_col = ["inst_id", "OC", "sido", "sgg", "bedCount", "instkind"]
in_col_train = train.columns.tolist()[6:]
in_col_test = test.columns.tolist()[6:]
1
2
3
4
temp = train[in_col_train].replace(0, -999)
temp = temp.fillna(-999)

pre_train = pd.concat([train[same_col], temp], axis=1)
1
2
3
4
temp = test[in_col_test].replace(0, -999)
temp = temp.fillna(-999)

pre_test = pd.concat([test[same_col], temp], axis=1)

ownerChangenan유지

1
2
3
4
5
6
pre_train["ownerChange"] = pre_train["ownerChange"].replace(-999, np.nan)
pre_test["ownerChange"] = pre_test["ownerChange"].replace(-999, np.nan)

# pre_train.to_csv("data/pre_train.csv", index=False)
# pre_test.to_csv("data/pre_test.csv", index=False)
train, test = pre_train, pre_test

범주형 변수 -> 수치형 변수

OC, sido, bedCount, instkind, ownerChange를 변경해줘야함

1
obj2num = ["OC", "sido", "bedCount", "instkind", "ownerChange"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# train
temp_arr = []
for col in obj2num:
    temp_arr.append(pd.get_dummies(train[col], drop_first=True))

temp = pd.concat(temp_arr, axis=1)
df_train = pd.concat([temp, train.drop(columns=obj2num, axis=1)], axis=1) 

# test
temp_arr = []
for col in obj2num:
    temp_arr.append(pd.get_dummies(test[col], drop_first=True))

temp = pd.concat(temp_arr, axis=1)
df_test = pd.concat([temp, test.drop(columns=obj2num, axis=1)], axis=1) 

Train - Data Split

1
2
3
4
5
6
label = "open"
feature_names = df_train.columns.tolist()
feature_names.remove(label)
feature_names.remove("dental_clinic")
feature_names.remove("경상도")
feature_names.remove("광주")
1
df_test.drop(columns="제주도", axis=1, inplace=True)

공통으로 존재하지 않는 피처라 제외하고 모델 생성시 이용

1
2
df_test["employee1"] = df_test["employee1"].str.replace(",","").astype("float")
df_test["employee2"] = df_test["employee2"].str.replace(",","").astype("float")

EDA시 데이터 타입을 확인해주지 못했어서..

1
2
3
X_train, X_valid, y_train, y_valid = train_test_split(df_train[feature_names], df_train[label], test_size=0.15, stratify=df_train[label])

print(f"X_train: {X_train.shape}\ny_train: {y_train.shape}\nX_valid: {X_valid.shape}\ny_valid: {y_valid.shape}")
1
2
3
4
X_train: (255, 72)
y_train: (255,)
X_valid: (46, 72)
y_valid: (46,)

Random Forest

1
2
3
4
5
clf_rf = RandomForestClassifier()

clf_rf.fit(X_train, y_train)

pred_rf = clf_rf.predict(X_valid)
1
eval_CM(y_valid, pred_rf, 1)
1
2
3
4
정확도: 0.9565
정밀도: 0.9565
재현율: 1.0000
F1: 0.9778
1
2
3
plt.figure(figsize=(23, 15))
_ = sns.barplot(x=clf_rf.feature_importances_, y=clf_rf.feature_names_in_)
plt.show()

png

XGBoost

1
2
3
4
5
clf_xgb = XGBClassifier()

clf_xgb.fit(X_train, y_train)

pred_xgb = clf_xgb.predict(X_valid)
1
eval_CM(y_valid, pred_xgb, 1)
1
2
3
4
정확도: 0.9565
정밀도: 0.9565
재현율: 1.0000
F1: 0.9778
1
2
3
_ = xgb.plot_importance(clf_xgb)
fig = _.figure
fig.set_size_inches(23, 10)

png

LGBM

1
2
3
4
5
clf_lgbm = LGBMClassifier()

clf_lgbm.fit(X_train, y_train)

pred_lgbm = clf_lgbm.predict(X_valid)
1
eval_CM(y_valid, pred_lgbm, 1)
1
2
3
4
정확도: 0.9565
정밀도: 0.9565
재현율: 1.0000
F1: 0.9778
1
_ = lgbm.plot_importance(clf_lgbm, figsize=(23, 10))

png

마무리

EDA를 좀 더 자세하게 진행하는 연습을하자
제출시, RF와 XGB 모델은 pubplic: 87 / private: 82.8로 유사한 결과가 나왔고,
LGBM은 pubplic은 비슷했지만, private: 84.3로 가장 점수가 높았다.
Stacking 모델도 생성하기는 했으나, 제출 제한 횟수가 있어 평가를하지는 못했다.

This post is licensed under CC BY 4.0 by the author.

멋쟁이 사자처럼 AI Shcool 9주차

22년 7월 1주차 주간 회고

Comments powered by Disqus.