본문 바로가기

KHUDA

KHUDA Data buisenss 02

11.데이터 전처리와 파생변수 생성 

 

chapter 11-1 결측값 처리 

 

대부분의 데이터는 결측값(missing value)나 이상치가 없는 경우가 드물다. 그러므로 데이터 탐색 단계에서 결측값을 처리해야 한다. 

완전 무작위 결측( MCMR )
이름 그대로 순수하게 결측값이 무작위로 발생한 경우 
결측치 데이터를 제거해도 편향은 발생하지 않음.
무작위 결측(MAR)
다른 변수의 특성에 의해 결측치가 체계적으로 발생한 경우 
결측값은 데이터 수집 장치에 특성에 영향을 받음
비무작위 결측(NMAR)
결측값들이 해당 변수 자체의 특성을 가지고 있는 경우
그 값이 실제로 무엇인지 확인할 수 없으므로 비무작위 결측을 구분하기 어렵다 
ex) 고객 소득 변수에서 결측값 대부분이 소득이 적어서 소득을 공개하기 꺼려해서 결측이 발생한 경우

따라서 이런 결측치를 해소하기위해 결측값 처리 방법이 많이 있다. 

표본 제거 방법( Completes analysis)
가장 간단한 처리 방법으로 결측값이 심하게 많은 변수를 제거하거나 결측값이 포함된 행을 제외하고 데이터 분석을 하는 것

전체 데이터 결측값 비율이 10%일때 사용한다. 하지만 필드에선 데이터 결측값 비율이 더 높다. 이런상황에서는 편향이 발생할 수 있으므로 주의해야한다. 

평균 대치법(Mean Imputation)
결측값을 제외한 온전한 값들의 평균을 구한다음, 그 평균 값을 결측값에 대해 대치하는 것이다. 
간단하지만, 평균을 사용하기 때문에 통계량의 표준오차가 왜곡되고, 축소되어 나타남.
표본제거방법과 평균 대치법은 완전 무작위 결측이 아닌경우 적절하지 않다. 

또 데이터가 시계열성을 가지고 있다면, 보간법을 사용하는 것이 효과적이다. 

ex) 20일의 판매 대금은 19,21일의 판매 금액과 비슷할 것으로 기대 

회귀 대치법
결측값 변수와 다른 변수 사이의 괸계성을 고려하여 결측값을 계산하면 보다 합리적으로 결측값을 처리할 수 있다.
ex) 연령 변수를 대치하기 위해 연 수입 변수를 사용하는 것 
# 결측값 수만 확인
df.isnull().sum()
# 결측값 시각화 - 전체 컬럼의 결측값 시각화

# 결측값 영역 표시
msno.matrix(df)
plt.show()

# 결측값 막대 그래프
msno.bar(df)
plt.show()
 

 

# 결측값이 아닌 빈 문자열이 있는지 확인

def is_emptystring(x):
    return x.eq('').any()
 
df.apply(lambda x:is_emptystring(x))
# 결측값 표본 제거

# 모든 컬럼이 결측값인 행 제거
df_drop_all = df.dropna(how='all')

#  세개 이상의 컬럼이 결측값인 행 제거
df_drop_3 = df.dropna(thresh=3)

#  특정 컬럼(temp)이 결측값인 행 제거
df_drop_slt = df.dropna(subset=['temp'])

# 한 컬럼이라도 결측치가 있는 행 제거
df_drop_any = df.dropna(how='any')

df_drop_any.isnull().sum()
df_mean_all = df.fillna(df.mean())

# 평균값 대치 - 컬럼 지정
df_mean_slt = df.fillna({'temp':df['temp'].mean()})

# 중앙값 대치 - 전체 컬럼
df_median_all = df.fillna(df.median())

# 중앙값 대치 - 컬럼 지정
df_median_slt = df.fillna({'temp':df['temp'].median()})

# 최빈값 대치 - 전체 컬럼
df_mode_all = df.fillna(df.mode())

# 최빈값 대치 - 컬럼 지정
df_mode_slt = df.fillna({'temp':df['temp'].mode()})

# 최댓값 대치 - 전체 컬럼
df_max_all = df.fillna(df.max())

# 최댓값 대치 - 컬럼 지정
df_max_slt = df.fillna({'temp':df['temp'].max()})

# 최솟값 대치 - 전체 컬럼
df_min_all = df.fillna(df.min())

# 최솟값 대치 - 컬럼 지정
df_min_slt = df.fillna({'temp':df['temp'],'hum':df['hum'].min()})


df_min_slt.isnull().sum()

 

chapter 11.2 이상치 처리 

 

이상치 
일부 관측치의 값이 전체 데이터의 범위에서 크게 벗어난 아주 작거나 극단적으로 큰 값을 갖는 것을 말한다.
분산을 과도하게 증가, 모델링의 정확도 감소 
그러나 전체 데이터의 양이 많아지면 통곗값에 미치는 영향력이 줄어들어 이상치 제거의 필요성이 낮아진다.

 

이상치 제거 방법 

  • 해당 이상치 제거 - 추정치의 분산은 감소하지만, 실젯값을 과장하여 편향을 일으킨다. 
  • 관측값 변경 - 하한, 상한값 결정 후 하한값보다 작으면 하한값으로 대체, 상한값과 큰 경우도 같은 과정
  • 가중치 조정 - 이상치의 영향을 감소시키는 가중치 조정

이상치 선정 방법

  • 박스 플롯 상 분류된 극단치 그대로 선정 
  • 임의의 허용 범위를 설정하여 벗어나는 자료를 이상치로 정의 
중위수 절대 편차(MAD : Madian Absolute Deviation) 
평균은 이상치에 통계량에 민감하기 때문에 이상치에보다 강건한 중위수와 중위수 절대 편차를 사용한다.

하지만 통계치를 통한 이상치 탐색은 위험하다 -> 도메인 지식을 잘 알아햐 한다. 

분석도메인에 따라 이상치가 중요한 분석 요인일 수 있다. 

 

# BMI 컬럼의 박스플롯 시각화를 통한 이상치 확인

plt.figure(figsize = (8, 6))
sns.boxplot(y = 'BMI', data = df)
plt.show()
 

3 IQR을 넘어가는 관측치가 다수 있다. 

# BMI 컬럼의 이상치 제거 (IQR*3)

# Q!, Q3 범위 정의
Q1 = df['BMI'].quantile(0.25)
Q3 = df['BMI'].quantile(0.75)
IQR = Q3 - Q1    #IQR 범위.
rev_range = 3  # 제거 범위 조절 변수 설정

# 이상치 범위 설정
filter = (df['BMI'] >= Q1 - rev_range * IQR) & (df['BMI'] <= Q3 + rev_range *IQR)
df_rmv = df.loc[filter]
print(df['BMI'].describe())
print(df_rmv['BMI'].describe())
# 이상치 제거 후 박스플롯 시각화

plt.figure(figsize = (8, 6))
sns.boxplot(y = 'BMI', data = df_rmv)
plt.show()

박스 플롯의 이상치가 많이 줄어든 것을 확인할 수 있다.

# 이상치 IQR*3 값으로 대치

# 이상치 대치 함수 설정
def replace_outlier(value):
    Q1 = df['BMI'].quantile(0.25)
    Q3 = df['BMI'].quantile(0.75)
    IQR = Q3 - Q1    #IQR 범위.
    rev_range = 3  # 제거 범위 조절 변수 설정

    if ((value < (Q1 - rev_range * IQR))):
        value = Q1 - rev_range * IQR
    if ((value > (Q3 + rev_range * IQR))):
        value = Q3 + rev_range * IQR
#         value = df['BMI'].median() # 중앙값 대치
    return value
df['BMI'] = df['BMI'].apply(replace_outlier)

print(df['BMI'].describe())

 이상치를 상하한선 값으로 대치한다. 

# 이상치 IQR*3 값으로 대치

# 이상치 대치 함수 설정
def replace_outlier(value):
    Q1 = df['BMI'].quantile(0.25)
    Q3 = df['BMI'].quantile(0.75)
    IQR = Q3 - Q1    #IQR 범위.
    rev_range = 3  # 제거 범위 조절 변수 설정

    if ((value < (Q1 - rev_range * IQR))):
        value = Q1 - rev_range * IQR
    if ((value > (Q3 + rev_range * IQR))):
        value = Q3 + rev_range * IQR
#         value = df['BMI'].median() # 중앙값 대치
    return value
df['BMI'] = df['BMI'].apply(replace_outlier)

print(df['BMI'].describe())

이상치를 처리하는 과정에서 무한 루프에 빠진 것 같다. 

 

def replace_outlier(value):
    Q1 = df['BMI'].quantile(0.25)
    Q3 = df['BMI'].quantile(0.75)
    IQR = Q3 - Q1    #IQR 범위.
    rev_range = 3  # 제거 범위 조절 변수 설정

    if ((value <= (Q1 - rev_range * IQR))):
        value = Q1 - rev_range * IQR
    if ((value >= (Q3 + rev_range * IQR))):
        value = Q3 + rev_range * IQR
    return value

# 'BMI' 열의 각 값에 대해 이상치 대치 함수를 적용하여 이상치 대체
df['BMI'] = df['BMI'].apply(replace_outlier)

# 이상치 대치 후 'BMI' 열의 요약 통계 출력
print(df['BMI'].describe())

12분 걸려서 실행 완 

 

chapter 11-3 변수 구간화 

이산형 변수를 범주형 변수로 비지니스적 상황에 맞도록 변환 시킴으로써 데이터의 해석이나 예측, 분류 모델을 의도에 맞도록 유도할 수 있는 것 

 

평활화(smoothing)
변수의 값을 일정한 폭이나 빈도로 구간을 나눈 후, 각 구간 안에 속한 데이터 값을 평균, 중앙, 경계값 등으로 변환
구간화할 변수 : 2,3,7,14,16,16,17,23,26,27,31,36
1. 동일한 폭(10)으로 변수 구간화 
구간1(1~10) : 2,3,7
구간2(11~20) : 14,16,16,17
구간3(20~30) : 23,26,27
구간4(30~40) : 31,36
2. 동일한 빈도수 (4)로 변수 구간화 
구간1(4): 2,3,7,14
구간2(4): 16,16,17,23
구간3(4) : 26,27,32,35

변수값이 효과적으로 구간화됐는지는 WOE, IV 값등을 통해 추정 가능

iv 값이 높을수록 TF 구분을 잘 할 수있는 정보량이 많다. 

변수가 종속변수를 제대로 설명할 수 있도록 구간화가 잘되면 IV 값이 높아진다. 

 

# BMI 컬럼 분포 시각화

%matplotlib inline
sns.displot(df['BMI'],height = 5, aspect = 3)

60이후로 관측치가 적다.

# 임의로 단순 구간화

df1 = df.copy() # 데이터셋 복사

# 구간화용 빈 컬럼 생성 - 생략해도 되지만 바로 옆에 붙여 보기 위함
df1.insert(2, 'BMI_bin', 0)

df1.loc[df1['BMI'] <= 20, 'BMI_bin'] = 'a'
df1.loc[(df1['BMI'] > 20) & (df1['BMI'] <= 30), 'BMI_bin'] = 'b'
df1.loc[(df1['BMI'] > 30) & (df1['BMI'] <= 40), 'BMI_bin'] = 'c'
df1.loc[(df1['BMI'] > 40) & (df1['BMI'] <= 50), 'BMI_bin'] = 'd'
df1.loc[(df1['BMI'] > 50) & (df1['BMI'] <= 60), 'BMI_bin'] = 'e'
df1.loc[(df1['BMI'] > 60) & (df1['BMI'] <= 70), 'BMI_bin'] = 'f'
df1.loc[df1['BMI'] > 70, 'BMI_bin'] = 'g'

df1.head()

20부터 10 단위로 70까지 구간 설정 

# 구간화 변수 분포 시각화

sns.displot(df1['BMI_bin'],height = 5, aspect = 3)

df1.insert(3, 'BMI_bin2', 0) # 구간화용 빈 컬럼 생성

df1['BMI_bin2'] = pd.cut(df1.BMI, bins=[0, 20, 30, 40, 50, 60, 70, 95]
                         , labels=['a', 'b', 'c', 'd', 'e', 'f', 'g'])

df1.head()
# BMI_bin2 구간 별 관측치 수 집계

df1.BMI_bin2.value_counts().to_frame().style.background_gradient(cmap='winter')

# BMI_bin3 분포 시각화

sns.displot(df1['BMI_bin3'],height = 5, aspect = 3)

구간화가 일정하게 이루어진것을 확인할 수 있다. 

# WOE를 사용한 변수 구간화

df2 = df.copy()  # 데이터셋 복사


# xverse 함수 적용을 위한 더미변수 변환
df2=pd.get_dummies(df)

# 구간화 할 컬럼(X), 기준 컬럼(y) 지정
X = df2[['PhysicalHealth']]
y = df2[['KidneyDisease_Yes']]

y = y.T.squeeze() # 차원 축소

# WOE 모델 설정 및 적용
clf = WOE()
clf.fit(X, y)

# 구간 기준점 및 eight of Evidence 값 테이블 생성
a=clf.woe_df

#Information Value 데이블 생성
b=clf.iv_df

a.head()

WOE를 활용해 최적의 범주 기준이 산출된다. 

# Information Value 확인

b.head()

 

IV 기준을 통해 확인하면 0.308이다. 

 

chapter 11-5 모델 성능 향상을 위한 파생 변수 생성 

파생변수 
원래 있던 변수들을 조합하거나 함수를 적용하여 새로 만들어낸 변수를 뜻한다. 
데이터의 특성을 이용하여 분석 효율을 높인다.
그래서 전체 데이터에 대한 파악이 중요할 뿐 아니라 해당 비지니스 도메인에 대한 충분한 이해가 수반됨. 
# 구매 상품당 가격 컬럼 생성
df['Unit_amount'] = df['Sales_Amount']/df['Quantity']

# 총 구매가격 컬럼 생성
df['All_amount'] = \
df[['Quantity', 'Sales_Amount']].apply(lambda series: series.prod(), axis=1)

df.tail()
# 방법1.Sales_Amount 컬럼 로그 적용 (+1)
df['Sales_Amount_log'] = preprocessing.scale(np.log(df['Sales_Amount']+1))

# 방법2.Sales_Amount 컬럼 로그 적용 (+1)
df['Sales_Amount_log2'] = df[['Sales_Amount']].apply(lambda x: np.log(x+1))    

# Sales_Amount 컬럼 제곱근 적용 (+1)
df['Sales_Amount_sqrt'] = np.sqrt(df['Sales_Amount']+1)

# Sales_Amount 컬럼 제곱 적용
df['Sales_Amount_pow'] = pow(df[['Sales_Amount']],2)

df.tail()
# date 컬럼 날짜형식 변환
df['Date2']= pd.to_datetime(df['Date'], infer_datetime_format=True)

# 연도 컬럼 생성
df['Year'] = df['Date2'].dt.year

# 월 컬럼 생성
df['Month'] = df['Date2'].dt.month

#연월별, 고객별 매출 합계, 평균 컬럼 생성
df_sm = df.groupby(['Year',
                    'Month',
                    'Customer_ID'])['Sales_Amount'].agg(['sum','mean']).reset_index()

# 기존 일별 테이블에 평균 테이블 조인
df2 = pd.merge(df, df_sm, how='left')

df2.head()
# 4주 뒤 시점 컬럼 생성
df2['Date2_1_m'] = df2['Date2'] + timedelta(weeks=4)

# # 4주 뒤 시점연도 컬럼 생성
df['Year_1_m'] = df2['Date2_1_m'].dt.year

# # 4주 뒤 시점월 컬럼 생성
df['Month_1_m'] = df2['Date2_1_m'].dt.month

# 4주 전 구매금액 연월별, 고객별 매출 평균 컬럼 생성
df_Mn_1 = df.groupby(['Year_1_m',
                      'Month_1_m',
                      'Customer_ID'])['Sales_Amount'].agg(['sum',
                                                           'mean']).reset_index()

# 조인을 위한 컬럼명 변경
df_Mn_1.rename(columns={'Year_1_m':'Year',
                        'Month_1_m':'Month',
                        'sum':"sum_1_m",
                        'mean':'mean_1_m'}, inplace=True)

df2 = pd.merge(df2, df_Mn_1, how='left')

df2.head()

 

chapter 11-6 슬라이딩 윈도우 데이터 가공

 슬라이딩 윈도우
실시간 네트위크 패킷 데이터 처리 기법 
데이터를 겹쳐 나눔으로써 전체 데이터가 증가하는 원리를 차용한 것 
이 방식의 특징은 각각의 데이터 조각들이 서로 겹치며 데이터가 전송되는 것이다. 

데이터를 쪼개서 보내는 이유는 패킷의 전송을 확인받지 않고도 곧바로 다음 패킷을 보낼 수 있어 네트워크를 효율적으로 사용할 수 있기 때문이다. 

 

예측모델에서 유용하게 사용된다 슬라이싱 윈도우 방법을 활용하면 많은 분석 데이터셋을 확보하고 학습데이터의 최근성을 가질 수 있다. 

 

 

# 슬라이딩 윈도우 형태로 변환

m_col = ["M{}".format(i) for i in range(6)]   # M0~M5 목록 생성
df_li = []   # 임시테이블 저장할 목록

for n, ym in enumerate(ym_li):  # YM_M0 ~ YM_M5 반복
# STD_YM_M0 변수 기준 M0~M5 & 구매금액 0원 초과        
    tmp = df_raw[(df_raw[ym].isin(m_col)) & (df_raw['sale_amt'] > 0)]
# YM_M0 기준 pivot
    tmp = tmp.pivot_table(index='cust_id',
                          columns=ym, values='sale_amt',
                          aggfunc='sum')
   
    # M0~M12 중 누락된 컬럼 생성
    # 추후 테이블 union을 위해 pivot시 누락된 컬럼을 별도로 생성해줌
    missing_col = list(set(m_col) - set(tmp.columns))
    for col in missing_col :
        tmp[col] = 0
   
    # 컬럼이름 변경
    tmp.columns = [f'slae_amt_{c}' for c in tmp.columns] # 생략 가능
   
    tmp['MM_DIFF'] = ym
    tmp = tmp.fillna(0)

    df_li.append(tmp)

final_df = pd.concat(df_li).reset_index()
final_df.head()
# 특정 고객의 시점 별 형태 확인
df1 = final_df[(final_df['cust_id']=='AFG6825009314')]
# 마지막 시점의 데이터 형태 확인
final_df.tail(10)

 

chapter 11-7 범주형 변수의 가변수 처리 

가변수 처리 (Dummy variable)
범주형 변수를 0과 1 의 값을 가지는 변수로 변환해 주는 것을 뜻한다. 

가변수를 만드는 이유는 범주형 변수는 사용할 수 없고 연속형 변수만 사용할 수 있는 분석기법을 사용하기 위함이다. 

이런 형태를 불리언 변수라고 한다. 

 

선형회귀나 로지스틱 회귀분석 등의 회귀분석은 기본적으로 연속형 변수만 사용할 수 있다. 

비흡연은 0, 흡연은 1로 바꿔준다. 

일반적으로 해당 안됨은 0, 해당됨은 1로 처리한다. 

# 변경할 컬럼 범주 별 분포 확인 시각화
fig, ax = plt.subplots(nrows=2)

# Compute Type 컬럼 범주 별 분포
sns.countplot(x="Compute Type", data=df, ax=ax[0])

# OS 컬럼 범주 별 분포
sns.countplot(x="OS", data=df, ax=ax[1])

plt.show()

df1 = pd.get_dummies(df)

df1.head()
df2 = pd.get_dummies(df, columns = ['Compute Type', 'OS'])

df2.head()

 

chapter 11-8 클래스 불균형 문제 해결을 위한 언더샘플링과 오버샘플링 

 

일반적인 기계학습 분류 모델은, 적은 비중의 클래스 든 큰 비중의 클래스 든 중요도에 차별을 두지 않고 전체적으로 분류를 잘 하도록 학습된다. 

EX) 9:1의 비율을 가질 때 1의 비중을 가진 데이터의 분류가 잘 안되도 9가 잘된다면 전체적인 분류 정확도는 높아진다. 

데이터 불균형 문제 해결 방법
가중치 벨런싱
모델 자체에 중요도가 높은 크랠스에 정확도 가중치를 주어, 특정 클래스의 분류 정확도가 높아지도록 조정하는것 
전체 정확도를 높이는 방향으로 학습된다. 
중요도가 높은 클래스를 잘못 분류하면 더 큰 손실을 계산하도록 조정하는 것이다. 

언더 샘플링 
큰 비중의 클래스 데이터를 작은 비중의 클래스 데이터만큼만 추출하여 학습시키는 것 
EX) 랜덤 언더샘플링, eASYeNSEMBLE 기법, CNN (k-근접 이웃 기법 차용)

오버 샘플링
클래스의 관측치를 복제하여 더 많은 관측치로 학습하는 것 
오버샘플링을 적용할 떄에는 먼저 학습 셋과 테스트 셋을 분리한 다음에 적용해야한다. 그렇지 않으면 학습 셋과 동일한 데이터가 들어가서 과적합을 유발하기 때문이다. 
EX) 랜덤 오버샘플링, Synthetic Minority Over-sampling Techique(SMOTE), ADASYN기법 
X = df2.drop(['Purchased'], axis=1)
y = df2[['Purchased']]

X_train, X_test, y_train, y_test = train_test_split(
    X,y,test_size=0.25,random_state=10)

X_train.head()

X_train_under, y_train_under = RandomUnderSampler(
    random_state=0).fit_resample(X_train,y_train)

print('RandomUnderSampler 적용 전 학습셋 변수/레이블 데이터 세트: '
      , X_train.shape, y_train.shape)
print('RandomUnderSampler 적용 후 학습셋 변수/레이블 데이터 세트: '
      , X_train_under.shape, y_train_under.shape)
print('RandomUnderSampler 적용 전 레이블 값 분포: \n'
      , pd.Series(y_train['Purchased']).value_counts())
print('RandomUnderSampler 적용 후 레이블 값 분포: \n'
      , pd.Series(y_train_under['Purchased']).value_counts())

# 오버샘플링 적용

smote = SMOTE(k_neighbors = 2, random_state=0)
oversample = SMOTE()

X_train_over,y_train_over = smote.fit_resample(X_train,y_train)
print('SMOTE 적용 전 학습용 변수/레이블 데이터 세트: '
      , X_train.shape, y_train.shape)
print('SMOTE 적용 후 학습용 변수/레이블 데이터 세트: '
      , X_train_over.shape, y_train_over.shape)
print('SMOTE 적용 전 레이블 값 분포: \n'
      , pd.Series(y_train['Purchased']).value_counts())
print('SMOTE 적용 후 레이블 값 분포: \n'
      , pd.Series(y_train_over['Purchased']).value_counts())
# 오버샘플링 적용 후 Purchased 컬럼 클래스 분포 시각화
sns.countplot(x="Purchased", data=y_train_over)

plt.show()

 

chapter 11-9 데이터 거리 측정 방법 

 

데이터 거리란 관측치 A를 기준으로, B와 C 중 어느 관측치가 더 가까이 있는가를 판단하기 위한것이다. 

유클리드 거리 
피타고라스 정리를 활용한 것이다. 관측치 간의 직선거리를 측정하는 것이어서 실제 거리를 사용하기 때문에 합당한 데이터 거리 측정 방법으로 인정된다. 
맨해튼 거리 
L1 norm 이라고 불리며, 보통 딥 러닝 분야에서 정규화를 할때 L1,L2 라는 용어로 데이터(벡터) 간의 거릴 구한다. 
민코프스키 거리 
옵션값을 설정하여 거리 기준을 조정할 수 있는 거리 측정 방법이다. 
수식에서 P값을 1로 설정하면 맨해튼 거리와 동일하고, 2로 설정하면 유클리드 거리와 동일하다. 
쳬비쇼프 거리 
민코프스키 거리의 P를 무한대로 설정한 것
군집간의 최대 거리를 구할 때 사용된다. 계산값이 0일수록 유사한 것 이다.
마할라노비스 거리 
유클리드 거리에 공분산을 고려한 거리 측정 방법이다. 
변수 내 분산과 변수 간 공분산을 모두 반영하여 A와 B 간 거리를 계산한다. 
마할라노비스 거리는 X와 Y의 공분산을 고려하여 거리를 측정한다.
코사인거리 
코사인 유사도는 벡터사이의 각도만으로 두 점 간의 유사도를 측정 
EX) 'BTS'로 검색을 했는데, 전체 5000개 단어 중 5개 단어가 'BTS'인 문서보다. 1000개 단어 중 4개 단어가 'BTS'인 문서가 더 적합할 수 있기 때문이다. 

'KHUDA' 카테고리의 다른 글

KHUDA Data business 02 practice  (1) 2024.03.26
KHUDA Data business 02 심화발제  (0) 2024.03.20
KHUDA Data buiseness 01 practice  (3) 2024.03.18
KHUDA Data buisness 01  (0) 2024.03.13
KHUDA ML 세션 5주차  (1) 2024.02.27