7 분 소요

본 게시물은 ADP 실기의 23회차 문제 풀이에 대한 것이다.

머신러닝

1. 온, 습, 조도, CO2 농도에 따른 객실의 사용 유무 판별

1.1 데이터 EDA 수행 후 의미있는 탐색

☀️ 데이터 개요

import pandas as pd
import numpy as np
df =pd.read_csv('https://raw.githubusercontent.com/Datamanim/datarepo/main/adp/23/problem1.csv')
df.head()

df_desc = df.describe()
df_desc

pd.DataFrame(df_desc.loc['std',:]).sort_values(by=['std'], ascending=False)

df.info()
Data columns (total 7 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   date           17910 non-null  object 
 1   Temperature    17910 non-null  float64
 2   Humidity       17910 non-null  float64
 3   Light          17910 non-null  float64
 4   CO2            17889 non-null  float64
 5   HumidityRatio  17910 non-null  float64
 6   Occupancy      17910 non-null  int64  
dtypes: float64(5), int64(1), object(1)

📍 데이터 탐색 결과

  1. target: Occupancy
  2. missing value: CO2
  3. outlier: CO2, Light 두 변수의 편차가 큰 것으로 보아 outlier가 존재할 수 있음
  4. scaling: 서로간의 데이터 범주가 다르기 때문에 스케일링 고려
  5. feature type: 모두 float type이며 target인 Occupancy categorical type
  6. feature eng: date 변수를 새로운 변수로 활용하여 요일을 추가할 수 있을 것 같음

☀️ 날짜 요일 변환

from datetime import datetime as dt

df['day'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
df['weekday'] = df.apply(lambda x: dt.weekday(x['day']), axis=1)
df['is_weekend'] = df['weekday'].apply(lambda x: 1 if x >= 5 else 0)
df.drop(columns=['day', 'date'], inplace=True)

☀️ 데이터 시각화

categorical_columns = ['Occupancy', 'weekday', 'is_weekend']
numerical_columns = ['Temperature', 'Humidity', 'Light', 'CO2', 'HumidityRatio']

👀 수치형-히스토그램

import seaborn as sns
import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=1, ncols=5, figsize=(15, 3))

for i, column in enumerate(numerical_columns):
    ax = axes[i]

    df[column].plot(kind='hist', bins=20, ax=ax, color='skyblue', edgecolor='black')
    ax.set_title(f'Histogram of {column}')
    ax.set_xlabel(column)
    ax.set_ylabel('Frequency')

plt.tight_layout()
plt.show()

👀 수치형-박스플롯

fig, axes = plt.subplots(nrows=1, ncols=5, figsize=(16, 2))

for i, column in enumerate(numerical_columns):

    ax = axes[i]
    sns.boxplot(x=df[column], ax=ax, color='skyblue')
    ax.set_title(f'Box plot of {column}')
    ax.set_xlabel(column)
    ax.set_ylabel('Frequency')

plt.tight_layout()
plt.show()

👀 범주형-막대그래프

fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 3))

for i, column in enumerate(categorical_columns):

    df[column].value_counts().sort_index().plot(kind='bar', ax=axes[i], color='skyblue')
    axes[i].set_title(f'Bar Plot of {column}')
    axes[i].set_xlabel(column)
    axes[i].set_ylabel('Frequency')

plt.tight_layout()
plt.show()

👀 그룹별 데이터

  • classification 문제이기 때문에 그룹별로 데이터를 시각화하여 파악
group_class = df.groupby('Occupancy').mean()
group_class

fig, axes = plt.subplots(1, 5, figsize=(15, 3))

columns = group_class.columns.difference(['is_weekend', 'weekday']).to_list()

for i, column in enumerate(columns):
    ax = axes[i]
    sns.barplot(x=group_class.index, y=column, data=group_class, ax=ax)
    ax.set_title(f'Average {column}')
    ax.set_xlabel("Occupancy")
    ax.set_ylabel(f'Average {column}')

plt.tight_layout()
plt.show()

group_counts = df.groupby(['is_weekend', 'Occupancy']).size().unstack(fill_value=0)

# 파이 그래프 그리기
fig, axes = plt.subplots(1, len(group_counts), figsize=(15, 4))

for i, (is_weekend, counts) in enumerate(group_counts.iterrows()):
    ax = axes[i]
    counts.plot(kind='pie', autopct='%1.1f%%', ax=ax)
    ax.set_title(f'is_weekend = {is_weekend}')
    ax.set_ylabel('')
    ax.legend(title='Occupancy', labels=counts.index)

plt.tight_layout()
plt.show()

group_counts = df.groupby(['Occupancy', 'weekday']).size().unstack(fill_value=0)

# 파이 그래프 그리기
fig, axes = plt.subplots(1, len(group_counts), figsize=(15, 4))

for i, (occupancy, counts) in enumerate(group_counts.iterrows()):
    ax = axes[i]
    counts.plot(kind='pie', autopct='%1.1f%%', ax=ax)
    ax.set_title(f'Occupancy = {occupancy}')
    ax.set_ylabel('')
    ax.legend(title='Weekday', labels=counts.index)

plt.tight_layout()
plt.show()

📍시각화 결과

  • 히스토그램 및 박스플롯을 확인한 결과 Light에 이상치가 있을 수 있음
  • 나머지 이상치들은 연속해서 분포되어 있기에 현업의 의견 없이 쉽게 제거할 수 없을 것으로 판단
  • 객실 사용 유무에 따라 light와 co2는 확실한 평균의 차이가 있음
  • 주말에 사용중인 경우는 없고 평일에 비슷한 비율로 사용하는 것으로 보임

1.2 결측치를 대체하는 방식 선택하고 근거제시, 대체 수행

👀 결측치 탐색

df_isna = pd.DataFrame(df.isna().sum(), columns=['null_cnt'])
df_isna

for null_col in df_isna[df_isna.null_cnt > 0].index.to_list():
    print("*"*15)
    print("결측치 컬럼 :", null_col)
    print("결측치 비율 : {}%".format(df_isna.loc[null_col].null_cnt *100/ len(df)))
    print("describe")
    print(df[null_col].describe())
***************
결측치 컬럼 : CO2
결측치 비율 : 0.11725293132328309%
describe
count    17889.000000
mean       647.700865
std        285.997340
min        412.750000
25%        453.000000
50%        532.666667
75%        722.000000
max       2076.500000

📍 결측치 대체 방식 선택

  • CO2의 결측치가 0.11%이기 때문에 제거하여도 무방해 보이나 대체하기로 함
  • 결측치는 CO2 연속형 변수
  • 따라서 평균 대체, 모델을 사용한 대체 등이 가능
  • 나머지 데이터들의 데이터가 모두 있기에 KNN 모델을 사용해 결측치를 대체하도록 함
before_fill_df = df[df.isna().any(axis=1)].copy()

from sklearn.impute import KNNImputer

KNN_data = df.copy()
imputer = KNNImputer()
df_filled = imputer.fit_transform(KNN_data)
df_filled = pd.DataFrame(df_filled, columns=KNN_data.columns)
df[KNN_data.columns] = df_filled
df.isnull().sum()
Temperature      0
Humidity         0
Light            0
CO2              0
HumidityRatio    0
Occupancy        0
weekday          0
is_weekend       0
df.loc[before_fill_df.index]

1.3 추가적으로 데이터의 질 및 품질관리를 향상시킬만한 내용 작성

  • Light에 결측치가 있는 것으로 보임
  • 극단적으로 큰값과 음수가 있음
  • IQR 방식으로 제거해 주는 것을 수행하면 데이터의 질이 향상 될 것
def detect_outliers(df=None, column=None, weight=1.5):
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    IQR_weight = IQR*weight
    outlier_idx = df[(df[column] < Q1 - IQR_weight) | (df[column] > Q3 + IQR_weight)].index
    return outlier_idx

outlier_idx = detect_outliers(df=df, column='Light')
df.iloc[outlier_idx]

df.iloc[outlier_idx].Occupancy.value_counts()
1.0    2099
0.0    1684

📍 이상치 판단 기준

  • 이를 모두 제거하면 Occupancy가 1인 데이터를 거의 모두 지우게 됨
  • 음수와 가장 큰 값을 제거하는 방식으로 이상치 제거
# 기준 0
df = df[df.Light >= 0 ].reset_index(drop=True)

# 기준 99.9%
percentile = 99.9  
threshold = np.percentile(df['Light'], percentile)
df = df[df['Light'] <= threshold]

2. 데이터 불균형 처리

2.1 데이터에 불균형이 있는지 확인, 불균형 판단 근거 작성

👀 시각화

sns.countplot(x='Occupancy', data =df)
plt.title('count plot of Occupancy', fontsize =14)
plt.show()
ratio0 = round(len(df[df['Occupancy']==0])/len(df)*100, 2)
ratio1 = round(len(df[df['Occupancy']==1])/len(df)*100, 2)
print('0 비율: {}%'.format(ratio0))
print('1 비율: {}%'.format(ratio1))

0 비율: 88.43%
1 비율: 11.57%
  • 데이터의 비율이 약 9:1 이므로 불균형이라고 판단

4.2 Oversampling

📍 Random Oversampling

from imblearn.over_sampling import RandomOverSampler, SMOTE
import time

X = df[df.columns.difference(['Occupancy'])]
y = df['Occupancy']
start = time.time()
# Random Oversampling
ros = RandomOverSampler(random_state =42)
X_ro, y_ro = ros.fit_resample(X, y)
print("time :", time.time() - start)
time : 0.006474018096923828

📍 SMOTE

start = time.time()
sm = SMOTE(random_state =42)
X_sm, y_sm = sm.fit_resample(X, y)
print("time :", time.time() - start)
time : 0.01717376708984375

5. Modeling

5.1 원본 및 오버샘플링 수행 데이터 셋으로 모델링 수행

📍 Origin Dataset

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report

start = time.time()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size =0.2, stratify =y, random_state =100)
clf = RandomForestClassifier(n_estimators =100, min_samples_split =10)
clf.fit(X_train, y_train)
print('train 정확도 :', clf.score(X_train, y_train), '\n')
pred=clf.predict(X_test)
print(classification_report(y_test, pred))
print('test 정확도 :', clf.score(X_test, y_test), '\n')
print("time :", time.time() - start)
train 정확도 : 0.997124824684432 

              precision    recall  f1-score   support

         0.0       1.00      0.99      1.00      3153
         1.0       0.94      0.99      0.97       412

    accuracy                           0.99      3565
   macro avg       0.97      0.99      0.98      3565
weighted avg       0.99      0.99      0.99      3565

test 정확도 : 0.9918653576437587 

time : 0.6616039276123047

📍 Random Oversampling Dataset

start = time.time()
X_ro_train, X_ro_test, y_ro_train, y_ro_test = train_test_split(X_ro, y_ro, test_size =0.2, stratify =y_ro, \
                                                                random_state =100)
clf_ro = RandomForestClassifier(n_estimators =100, min_samples_split=10, random_state =100)
clf_ro.fit(X_ro_train, y_ro_train)
print('train 정확도 :', clf_ro.score(X_ro_train, y_ro_train), '\n')
pred_ro=clf_ro.predict(X_ro_test)
print(classification_report(y_ro_test, pred_ro))
print('test 정확도 :', clf_ro.score(X_ro_test, y_ro_test), '\n')
print("time :", time.time() - start)
train 정확도 : 0.9992466296590008 

              precision    recall  f1-score   support

         0.0       1.00      0.99      1.00      3153
         1.0       0.99      1.00      1.00      3153

    accuracy                           1.00      6306
   macro avg       1.00      1.00      1.00      6306
weighted avg       1.00      1.00      1.00      6306

test 정확도 : 0.9955597843323819 

time : 1.362199068069458

📍 SMOTE Dataset

start = time.time()
X_sm_train, X_sm_test, y_sm_train, y_sm_test = train_test_split(X_sm, y_sm, test_size =0.2, stratify =y_sm, \
                                                                random_state =100)
clf_sm = RandomForestClassifier(n_estimators =100, min_samples_split=10, random_state =234)
clf_sm.fit(X_sm_train, y_sm_train)
print('train 정확도 :', clf_sm.score(X_sm_train, y_sm_train), '\n')
pred_sm=clf_sm.predict(X_sm_test)
print(classification_report(y_sm_test, pred_sm))
print('test 정확도 :', clf_sm.score(X_sm_test, y_sm_test), '\n')
print("time :", time.time() - start)
train 정확도 : 0.9984932593180016 

              precision    recall  f1-score   support

         0.0       1.00      0.99      0.99      3153
         1.0       0.99      1.00      0.99      3153

    accuracy                           0.99      6306
   macro avg       0.99      0.99      0.99      6306
weighted avg       0.99      0.99      0.99      6306

test 정확도 : 0.9946083095464637 

time : 1.8415770530700684

통계분석

1. 가설검정 (1)

공장에서는 진공관 수명이 1만 시간이라고 주장하여 품질관리팀에서 12개 샘플을 뽑았음 유의수준 5%에서 부호 검정하시오

import pandas as pd
df =pd.read_csv('https://raw.githubusercontent.com/Datamanim/datarepo/main/adp/23/problem2.csv')
df.head()

✏️ 문제 정의

  • 모평균 1만시간
  • 샘플사이즈 12
  • 유의수준 5%
  • 부호 검정

1.1 가설수립

  • H0: 진공관 수명의 중위수는 1만 시간
  • H1: 진공관 수명의 중위수는 1만 시간이 아님

1.2 유효한 데이터 개수

  • 부호검정에서 중위수와 동일한 값은 불필요한 데이터
sample_data=df['life span']
df_fillter = df[df['life span'] != np.median(sample_data)]
print("유효 데이터:", sum(effective_data))
유효 데이터: 8

1.3 검정통계량 및 연구가설 채택 여부를 작성

import numpy as np 
from scipy import stats

data = df['life span'].tolist()
# 중앙값
median_value = np.median(data)

# 부호검정
# 중앙값보다 큰 데이터의 개수
above = np.sum(data > median_value)
# 중앙값보다 작은 데이터의 개수
below = np.sum(data < median_value)

# 이항 검정
p_value = stats.binom_test(min(above, below), n=above + below, p=0.5) 
print('p-value: ', p_value)
p-value:  0.7265625

혹은

import statsmodels.stats.descriptivestats as smsd

# 두 집단의 데이터
group1 = df['life span']

# 부호검정 수행
statistic, p_value = smsd.sign_test(group1 - 10000)

print("검정 통계량:", statistic)
print("p-value:", p_value)

alpha = 0.05  # 유의수준 설정

if p_value < alpha:
    print("귀무가설을 기각합니다. 두 집단의 중앙값이 다릅니다.")
else:
    print("귀무가설을 기각하지 않습니다. 두 집단의 중앙값이 같습니다.")
검정 통계량: -1.0
p-value: 0.7265625
귀무가설을 기각하지 않습니다. 두 집단의 중앙값이 같습니다.

📍 검정 결과

  • P-value가 0.05보다 높기에 귀무가설 채택
  • 중위수는 1만 시간이다

2. 가설검정 (2)

학과별 학점 분포 인원수 표가 있다. 학과와 성적이 관계있는지를 검정하라

df = pd.DataFrame({'사회과학':[15,60,24], '자연과학':[25,69,5],'공학':[10,77,13], 'score':['1.5-2.5','2.5-3.5','3.5-4.5']})
df = df.set_index(['score'])
df

✏️ 문제 정의

  • 학과-범주형, 성적-범주형의 상관관계
  • 교차분석의 독립성검정 수행

2.1 가설수립

  • H0: 학과와 성적은 독립
  • H1: 학과와 성적은 독립이 아님

2.2 기대값 및 검정통계량, 가설선택

from scipy.stats import chi2_contingency
chi, p, df, expect = chi2_contingency(df)
print('Statistic:', chi)
print('p-value:',p)
print('df:',df)
print('expect: \n', expect)
Statistic: 22.136920195949322
p-value: 0.00018822647762421383
df: 4
expect: 
 [[16.61073826 16.61073826 16.77852349]
 [68.43624161 68.43624161 69.12751678]
 [13.95302013 13.95302013 14.09395973]]

📍 검정 결과

  • p-value < 0.05이므로 귀무가설 기각
  • 독립이 아니며 상관관계가 있음

3. 시계열 분석

import pandas as pd
df =pd.read_csv('https://raw.githubusercontent.com/Datamanim/datarepo/main/adp/23/problem3_covid2.csv')
df.head()

3.1 데이터는 일자별 각 나라의 일일 확진자수를 나타낸다. 각 나라의 일자별 누적확진자 수를 나타내는 데이터 프레임을 생성

df_new = df.groupby(by=['location','date']).sum()
df_new

3.2 1에서 구한 데이터를 각 나라별로 acf값을 구하고(lag는 50개까지 구하고 첫번째 값을 제외하라) 국가를 기준으로 유클리디안 거리를 기준으로 클러스터링을 진행 후 계층적 군집 분석을 위해 덴드로그램 작성

import statsmodels.api as sm

df_idx = df_new.reset_index()
df_acf = pd.DataFrame()

for loc in df['location'].unique():
    np.random.seed(0)

    x = df_idx[df_idx.location == loc][['date', 'new_cases']].set_index('date')

    acf_result = sm.tsa.stattools.acf(x, adjusted=False, 
                                  nlags=50, qstat=False, 
                                  fft=True, alpha=None, 
                                  bartlett_confint=True, missing='none')
    df_t = pd.DataFrame({'acf':acf_result[1:]})
    df_t = df_t.T
    df_t['location'] = loc
    df_t = df_t.set_index(['location'])
    
    df_acf = pd.concat([df_acf, df_t])
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
single = linkage(df_acf, metric='euclidean', method='ward')

plt.figure(figsize=(10,3))
dendrogram(single, orientation='top', labels=df_acf.index.tolist(), distance_sort='descending'
          , color_threshold=4.0, show_leaf_counts=True)
plt.axhline(y=20, color='r', linewidth=1)
plt.show()

댓글남기기