8 분 소요

본 게시물은 ADP 실기의 연습문제 풀이에 대한 것이다.

머신러닝

1. 데이터 전처리 및 군집 생성

1.1 결측치를 확인하고 제거

import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

data = pd.read_csv("https://raw.githubusercontent.com/ADPclass/ADP_book_ver01/main/data/26_problem1.csv")
data

data_desc = data.describe()
data_desc

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

data.info()
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   ID                   2240 non-null   int64  
 1   Year_Birth           2240 non-null   int64  
 2   Marital_Status       2240 non-null   object 
 3   Income               2216 non-null   float64
 4   Kidhome              2240 non-null   int64  
 5   Teenhome             2240 non-null   int64  
 6   MntWines             2240 non-null   int64  
 7   MntFruits            2240 non-null   int64  
 8   MntMeatProducts      2240 non-null   int64  
 9   MntFishProducts      2240 non-null   int64  
 10  MntSweetProducts     2240 non-null   int64  
 11  NumDealsPurchases    2240 non-null   int64  
 12  NumWebPurchases      2240 non-null   int64  
 13  NumCatalogPurchases  2240 non-null   int64  
 14  NumStorePurchases    2240 non-null   int64  
 15  NumWebVisitsMonth    2240 non-null   int64  
dtypes: float64(1), int64(14), object(1)

📍 데이터 탐색 결과

  1. missing value: Income에 결측치 존재
  2. outlier: 편차가 큰 Income에 대해 이상치 탐색 필요
  3. scaling: 각 컬럼의 max 값이 상이하기 때문에 차이가 클 경우 스케일링 고려
  4. feature type: Kidhome, Teenhome, Marital_Status은 categorical feature 나머지는 numerical feature

👀 결측치 탐색

df_isna = pd.DataFrame(data.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(data)))
    print("describe")
    print(data[null_col].describe())
***************
결측치 컬럼 : Income
결측치 비율 : 1.0714285714285714%
describe
count      2216.000000
mean      52247.251354
std       25173.076661
min        1730.000000
25%       35303.000000
50%       51381.500000
75%       68522.000000
max      666666.000000

☀️ 결측치 처리

  • 약 1%의 데이터가 결측치로 확인됨
  • 이상치가 있을 수 있으므로 모델링을 통한 제거 보다는 mean, median 등을 통한 대체
income_mean = data['Income'].median()
data['Income'].fillna(income_mean, inplace=True)
data.isna().sum()
ID                     0
Year_Birth             0
Marital_Status         0
Income                 0
Kidhome                0
Teenhome               0
MntWines               0
MntFruits              0
MntMeatProducts        0
MntFishProducts        0
MntSweetProducts       0
NumDealsPurchases      0
NumWebPurchases        0
NumCatalogPurchases    0
NumStorePurchases      0
NumWebVisitsMonth      0

1.2 이상치 제거

👀 데이터 시각화

# 수치형
numerical_columns = ['Year_Birth', 'Income',
       'MntWines', 'MntFruits', 'MntMeatProducts', 'MntFishProducts',
       'MntSweetProducts', 'NumDealsPurchases', 'NumWebPurchases',
       'NumCatalogPurchases', 'NumStorePurchases', 'NumWebVisitsMonth']
# 범주형
categorical_columns = ['Kidhome', 'Teenhome','Marital_Status']

👀 수치형-히스토그램

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

for i, column in enumerate(numerical_columns):

    row = i // 4
    col = i % 4
    data[column].plot(kind='hist', bins=20, ax=axes[row, col], color='skyblue', edgecolor='black')
    axes[row, col].set_title(f'Histogram of {column}')
    axes[row, col].set_xlabel(column)
    axes[row, col].set_ylabel('Frequency')

plt.tight_layout()

plt.show()

👀 수치형-박스플롯

fig, axes = plt.subplots(nrows=3, ncols=4, figsize=(15, 8))

for i, column in enumerate(numerical_columns):

    row = i // 4
    col = i % 4
    sns.boxplot(x=data[column], ax=axes[row, col], color='skyblue')
    axes[row, col].set_title(f'Box plot of {column}')
    axes[row, col].set_xlabel(column)
    axes[row, col].set_ylabel('Frequency')

plt.tight_layout()
plt.show()

👀 범주형-막대그래프

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

for i, column in enumerate(categorical_columns):

    data[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()

📍 이상치 탐색을 위한 시각화 결과

  • Year_Birth는 1900 이전은 이상치로 판단
  • Income은 평균보다 많이 떨어진 지점이 있으므로 IQR을 통해 제거
  • 나머지 컬럼들은 이상치가 연속적이므로 제거를 위해서는 현업의 의견이 필요
data = data[data.Year_Birth > 1900].reset_index(drop=True)

☀️ 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=data, column='Income')
data.iloc[outlier_idx]

  • 666666을 제외하고는 비슷한 값을 가지므로 고소득층이라 생각해도 무방
data = data[data.Income < 666666]

1.3 Kmeans, DBSCAN의 방식으로 군집화 수행

☀️ 군집화를 위한 범주형 변수처리

  • categorical columns 중 Marital_Status는 범주형 변수 처리가 필요
  • 순서가 없는 컬럼이기에 one-hot encoding 수행
df_encoded = pd.get_dummies(data, columns=['Marital_Status'], prefix=['Category'])
df_encoded

☀️ Kmeans

  • 최적의 군집을 찾기 위한 elbow 기법
from sklearn.cluster import KMeans

def elbow(X):
    sse = []
    for i in range(1, 11):
        km = KMeans(n_clusters=i, random_state=1)
        km.fit(X)
        sse.append(km.inertia_)

    # SSE를 그래프로 시각화하여 최적 클러스터 수 결정
    plt.plot(range(1, 11), sse, marker='o')
    plt.xlabel('The Number of Clusters')
    plt.ylabel('SSE')
    plt.title('Elbow Method for Optimal k')
    plt.show()

X = np.array(df_encoded[['Year_Birth', 'Income', 'Kidhome', 'Teenhome', 'MntWines',
       'MntFruits', 'MntMeatProducts', 'MntFishProducts', 'MntSweetProducts',
       'NumDealsPurchases', 'NumWebPurchases', 'NumCatalogPurchases',
       'NumStorePurchases', 'NumWebVisitsMonth', 'Category_Absurd',
       'Category_Alone', 'Category_Divorced', 'Category_Married',
       'Category_Single', 'Category_Together', 'Category_Widow',
       'Category_YOLO']])

elbow(X)

km = KMeans(n_clusters =3, random_state=1)
km.fit(X)
data['label_kmeans'] = km.labels_

☀️ DBSCAN

from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaled_data = scaler.fit_transform(X)

dbscan = DBSCAN(eps=5, min_samples=2)  # eps와 min_samples는 조정 가능
dbscan.fit(scaled_data)

cluster_labels = dbscan.labels_

data['label_dbscan'] = cluster_labels
data.label_dbscan.value_counts()
 0    2136
 2      77
 5       4
-1       3
 3       3
 6       3
 1       2
 4       2
 7       2
 8       2
 9       2

👀 군집화 시각화-빈도수

fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(15, 4))

for i, column in enumerate(['label_kmeans', 'label_dbscan']):

    data[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()

👀 군집 시각화

sns.pairplot(data[['label_kmeans', 'Income', 'Year_Birth', 'MntMeatProducts']],
            diag_kind='kde',
            hue="label_kmeans", 
            corner =True, 
            palette='bright')
plt.show()

👀 군집 시각화-PCA

from sklearn.decomposition import PCA

X = df_encoded.drop(columns=['ID']).reset_index(drop=True)

# PCA 모델 생성 및 적합
pca = PCA(n_components=2)  # 주성분 2개 선택
pca.fit(X)

# 주성분과 주성분의 설명된 분산 비율 확인
print("주성분 (Principal Components):")
print(pca.components_)
print("\n주성분의 설명된 분산 비율 (Explained Variance Ratio):")
print(pca.explained_variance_ratio_)

# 데이터를 주성분으로 변환
X_pca = pca.transform(X)

# 주성분으로 변환된 데이터 시각화
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=data['label_kmeans'])
plt.xlabel('PC 1 (Principal Component 1)')
plt.ylabel('PC 2 (Principal Component 2)')
plt.title('PCA Plot')
plt.show()
주성분의 설명된 분산 비율 (Explained Variance Ratio):
[9.99799806e-01 1.33989352e-04]

📍 군집화 결과

  • dbscan은 군집화가 골고루 되지 않음을 확인
  • Kmeans를 통한 군집화는 분포가 균일
  • PCA를 통해 확인해 보아도 구분이 잘 되어있음을 확인

2. 군집분석

2.1 군집의 특성 분석

👀 그룹별 데이터 확인

data_group = df_encoded.drop(columns=['ID']).groupby('label').mean()
data_group

fig, axes = plt.subplots(4, 6, figsize=(16, 12))

columns = data_group.columns.to_list()

for i, column in enumerate(columns):
    row = i // 6
    col = i % 6
    sns.barplot(x=data_group.index, y=column, data=data_group, ax=axes[row, col])
    axes[row, col].set_title(f'Average {column}')
    axes[row, col].set_xlabel("data_group")
    axes[row, col].set_ylabel(f'Average {column}')

plt.tight_layout()
plt.show()

📍 그룹별 데이터 특성

  1. 고객 정보 데이터
    • Income: 그룹 0 > 2> 1 순으로 소득의 차이가 있음
    • Kidhome, Teenhome: 그룹 1에는 아이가 2그룹에는 청소년이 있는 고객이 많이 분포됨
    • Marital_Status: 그룹 0에만 Absurd가 있으며 Alone에는 그룹 0이 없음, 그룹 2에만 YOLO가 존재
  2. 소비 제품 데이터
    • 그룹 0은 대부분의 소비 제품에서 높은 비율을 차지
  3. 구매 채널 데이터
    • 그룹 0은 다양한 구매채널을 활용하나 웹페이지 방문 횟수는 적음
    • 그룹 1, 2는 웹사이트를 방문하는 비중은 높으나 직접 구매로 이루어지지는 않음

📍 그룹별 데이터 해석

  • 그룹 0은 높은 소득을 기반으로 많은 소비를 함
  • 그룹 1은 상대적으로 젊은 고객이 주를 이루며 웹 사이트 방문을 많이하나 소득이 낮아 구매 비율이 낮음
  • 그룹 2는 그룹 1과 2의 중간 정도의 특성을 나타내는 고객으로 보임

2.2 군집별 상품 추천

👀 그룹별 구매 비율

# 각 열의 비율 계산
df_mnt = data_group[["MntWines","MntFruits","MntMeatProducts","MntFishProducts","MntSweetProducts"]]
df_ratios = df_mnt.divide(df_mnt.sum(axis=1), axis=0)
df_ratios

📍 그룹별 데이터 해석

  • 그룹 0: 와인 추천
  • 그룹 1: 고기 추천
  • 그룹 2: 와인 추천

2.3 특정 고객 대상 상품 추천 (ID=10870)

data[data.ID == 10870]

data_group.iloc[2,:]
Year_Birth              1966.114957
Income                 52364.688504
Kidhome                    0.412855
Teenhome                   0.815822
MntWines                 290.609394
MntFruits                 19.034611
MntMeatProducts          101.075402
MntFishProducts           25.189122
MntSweetProducts          18.262052
NumDealsPurchases          3.100124
NumWebPurchases            4.744129
NumCatalogPurchases        2.254635
NumStorePurchases          6.065513
NumWebVisitsMonth          5.692213
Category_Absurd            0.000000
Category_Alone             0.001236
Category_Divorced          0.117429
Category_Married           0.372064
Category_Single            0.203956
Category_Together          0.259580
Category_Widow             0.043263
Category_YOLO              0.002472

📍 데이터에 기반한 상품 추천

  • 해당 고객은 그룹 2에 속하므로 와인을 추천
  • 웹에서 구매하는 비중이 그룹 2의 평균보다 높으므로 웹 홍보 전략 수행

통계분석

1. 가설 검정 (1)

한 공장에서 생산된 제품에서 최근 추정 불량률은 90%였다. 오차의 한계가 5% 이하가 되도록 하는 최소 표본 사이즈를 구하시오

✏️ 문제 정의

  • 단일 집단의 모비율 신뢰구간 표본 크기를 구하는 문제
  • 추정 불량률 90%, 오차한계 5%
  • 가정: 신뢰구간 95%
p = 0.9
e = 0.05
confidence = 0.95

from scipy.stats import norm

# 정규 분포의 평균과 표준 편차
mu = 0
sigma = 1

two_side_alpha = (1 - confidence)/2
two_side = 1 - two_side_alpha

one_side_alpha = 1 - confidence
one_side = 1 - one_side_alpha

two_side_value = norm.ppf(two_side, loc=mu, scale=sigma)
one_side_value = norm.ppf(one_side, loc=mu, scale=sigma)

print(f"Z 분포 95% 양측검정 백분위수: {two_side_value}")
Z 분포 95% 양측검정 백분위수: 1.959963984540054
n = two_side_value**2 * (p*(1-p)) / e**2
print("최소 표본 수 :", int(np.ceil(n)))
최소 표본 수 : 139

2. 가설 검정 (2)

아래 그래프는 A, B, C 자치구별 H 의원에 대한 찬성, 반대 투표 결과이다. 자치구별 지지율이 같은지에 대해서 검정하시오.

df = pd.DataFrame({'A':[176, 124], 'B':[193, 107], 'C':[159, 141],'gubun':['agree','disagree']})
df = df.set_index('gubun').T
df

✏️ 문제 정의

  • 범주형 변수에 대한 빈도수를 사용한 분석 ☞ 교차분석
  • 지지율은 비율이기에 적합성 검정

📍 가설 수립

  • H0: 지지율이 다르다고 할 수 없음
  • H1: 지지율이 다르다고 할 수 있음
df['support'] = df.agree / (df.agree + df.disagree)
df['support']
A    0.586667
B    0.643333
C    0.530000
from scipy.stats import chisquare
ratio = df.support.sum()/3
chi = chisquare(df['support'], f_exp=[ratio, ratio, ratio])
chi
Power_divergenceResult(statistic=0.010946969696969688, pvalue=0.9945414673770099)

📍 적합도 검정 해석

  • p-value가 0.05보다 크므로 귀무가설 채택
  • 자치구별 지지율이 다르다고 할 수 없음

3. 가설 검정(3)

A학교 남녀 학생들의 평균 혈압 차이가 있는지 여부에 대한 검정하시오

  • (단, 남학생과 여학생의 혈압 데이터는 정규분포를 따르며 등분산임을 가정)
import pandas as pd
data = pd.read_csv("https://raw.githubusercontent.com/ADPclass/ADP_book_ver01/main/data/26_problem6.csv")
data

✏️ 문제 정의

  • 평균 혈압 차이 ☞ 두개 집단 차이 가설검정
  • 정규성과 등분산성을 가정하고 있음 ☞ 분산이 같은 독립표본 t-test

3.1 남녀 학생들의 평균 혈압 차이가 있는지 가설 수립

📍 가설 수립

  • H0: 남녀 학생 평균 혈압 차이는 없음
  • H1: 남녀 학생 평균 혈압 차이는 있음

3.2 검정통계량을 구하고 판단

from scipy import stats

# 독립표본 데이터 설정
group1 = np.array(data[data.gender=='male'].pressure)
group2 = np.array(data[data.gender=='female'].pressure)

# 독립표본 t-검정 수행
result = stats.ttest_ind(a=group1, b=group2, equal_var=False)

# 결과 출력
print(result)
Ttest_indResult(statistic=1.3495215111176884, pvalue=0.19643211975943328)

📍 t-test 해석

  • p-value가 0.05보다 크기에에 귀무가설 채택
  • 남녀 학생 평균 혈압 차이는 없음

3.3 평균 혈압차의 신뢰구간을 구하고 결과가 3.2의 결과를 지지하는지 설명

✏️ 문제 정의

  • 모평균 차이 신뢰구간 - 모분산을 모르는 경우
  • 소표본에 대한 신뢰구간
  • 신뢰구간은 95% 설정
mu_male = data[data.gender == 'male'].pressure.mean()
mu_female = data[data.gender == 'female'].pressure.mean()
n_male = len(data[data.gender == 'male'])
n_female = len(data[data.gender == 'female'])
dof = n_male + n_female -2
std_male = data[data.gender == 'male'].pressure.std()
std_female = data[data.gender == 'female'].pressure.std()

# 결합 분산 구하기
def get_sp(n1, n2, s1, s2):
    under = n1+n2-2
    over1 = (n1-1) * s1**2
    over2 = (n2-1) * s2**2
    return np.sqrt((over1 + over2)/under)
  
sp = get_sp(n_male, n_female, std_male, std_female)

# 분포 값 구하기
from scipy.stats import t

confidence = 0.95

two_side_alpha = (1 - confidence)/2
two_side = 1 - two_side_alpha

two_side_value = t.ppf(two_side, df=dof)

print(f"t 분포 95% 양측검정 백분위수: {two_side_value}")
t 분포 95% 양측검정 백분위수: 2.0686576104190406
mu_diff = mu_male - mu_female
sqrt = np.sqrt((1/n_male) +  (1/n_female))
under = mu_diff - two_side_value * sp * sqrt
over = mu_diff + two_side_value * sp * sqrt

print("표본 평균 차이: ", mu_diff)
print("신뢰구간: ", under, over)
표본 평균 차이: 5.296527777777783
신뢰구간: -2.635362714413926 13.228418269969492

📍 신뢰구간 해석

  • 보유한 데이터의 평균의 차이가 신뢰구간 안에 들어감
  • 3.2의 혈압 차이가 없음에 부합

4. 시계열 분석

다음은 1월부터 9월까지의 은의 가격이다.

import pandas as pd
import numpy as np
data = pd.read_csv("https://raw.githubusercontent.com/ADPclass/ADP_book_ver01/main/data/26_problem4.csv")
data

4.1 은의 가격 및 이동평균값 3이 설정된 시계열 그래프

ma_data = data.transpose( )
ma_data.columns = ["month_price"]
ma_data['ma_3'] = ma_data['month_price'].rolling(window=3).mean()
ma_data.fillna(0, inplace=True)
ma_data

plt.plot(ma_data.index, ma_data['month_price'][:],c='blue',marker='o')
plt.plot(ma_data.index[2:], ma_data['ma_3'][2:],c='red')

4.2 1월 대비 9월의 은의 가격은 몇 % 올랐는가?(소수점 두번째 자리에서 반올림)

  • 대비 증감률: (비교-기준)/기준 * 100
  • A 대비 B ☞ A가 기준
m9 = ma_data["month_price"][-1]
m1 = ma_data["month_price"][0]
print("증감률 {}%".format(round((m9-m1)/m1*100,1)))
증감률 158.5%

댓글남기기