

딥러닝에서는 특성 추출(feature extraction)까지 컴퓨터가 하기 때문에 지금 실습에서 데이터 탐색 & 특성 추출 과정을 잘 알아둬야 함!
# 요일별 식수 확인
# 요일에 따른 중식계, 석식계 식수 평균 구하기
# groupby() 사용
df_day = data.groupby("요일")[["중식계", "석식계"]].mean()
# 요일 순으로 정렬: 월화수목금
df_day = pd.DataFrame(df_day, index=("월", "화", "수", "목", "금"))
# 한글 폰트 자동 설정 라이브러리: koreanize_matplotlib
!pip install koreanize-matplotlib
import koreanize_matplotlib
# 요일별 중식계 평균 식수
plt.bar(df_day.index, df_day["중식계"])
plt.ylim(600,)
# 월요일에는 중식계에 대한 식수가 가장 많음
# 초반에는 회사에 출근하여 구내식당을 이용하는 직원이 많으나
# 후반에는 식수가 감소 -> 연차, 반차 사용 또는 다른 식당을 이용하지 않을까?

# 요일별 석식계 평균 식수
plt.bar(df_day.index, df_day["석식계"])
plt.ylim(300,)
# 월: 석식계의 식수가 높음 -> 월요일에 늦게까지 일하는 직원이 많은듯 보임
# 화, 목: 업무 진행
# 수: 석식계 식수 감소 -> 주중의 중반임을 감안하여 직원들의 피로도가 높아 야근을 적게 하지 않을까?
# 금: 석식계 식수 감소 -> 사회적 약속이 많은 요일, 외부에서 식사

# 월별 식수 평균 확인
data["month"] = data["일자"].dt.month
# 월별 중식계, 석식계 식수 평균 확인 → 그래프 →해석
df_month = data.groupby("month")[["중식계", "석식계"]].mean()
# 월별 중식계 평균 식수
plt.bar(df_month.index, df_month["중식계"])
plt.ylim(800,) # 범위를 제한하여 그래프의 특이점 확인하는 과정이 필요
# 1, 2, 3월: 가장 많은 중식 식수 -> 신입사원 입사? 영향이 있지 않을까?
# 6, 7, 8월: 여름 휴가, 11, 12월: 겨울 휴가
# 9, 10월: 또 상승 -> 여름 휴가로 인한 지출 높아짐, 지출 제한을 위해 구내식당을 사용하지 않을까?
# 식수의 빈도수 확인을 통해 월별, 요일별 식수 변동 패턴을 고려하여 재고를 조절할 수 있도록 해줘야 함

# 월별 석식계 평균 식수
plt.bar(df_month.index, df_month["석식계"])
plt.ylim(400,)

# 참고: 코랩에 설치된 폰트 목록 확인
!ls /usr/share/fonts/truetype/
!sudo apt-get install -y font-nanum
!sudo fc-cache -fv
!rm ~/.cach/matplotlib -rf
plt.rcParams["font.family"] = "NanumBarunGothic"
plt.rcParams["axes.unicode_minus"]=False
!pip install koreanize-matplotlib
import koreanize_matplotlib
# 한글 폰트 설정
import matplotlib.font_manager as fm
import matplotlib as mpl
# 폰트 파일 경로
font_path = "./data/fonts/NanumFont/NanumGothic.ttf"
# 폰트 매니저에 직접 추가
fm.fontManager.addfont(font_path)
# 폰트 이름 확인
font_name = fm.FontProperties(fname=font_path).get_name()
print(font_name)
# rcParams에 적용
mpl.rc("font", family=font_name)
# 마이너스 표시
mpl.rc("axes", unicode_minus=False)

참고: 코랩은 리눅스 운영 체제입니다.

| 메서드 | 설명 |
|---|---|
| .str.split() | 문자열을 특정 구분자로 분리한다. |
| .str.contains() | 특정 문자열이 포함되어 있는지 확인한다. |
| .str.replace() | 문자열을 다른 값으로 대체한다. |
| .str.startswith() | 특정 문자열로 시작하는지 확인한다. |
| .str.endswith() | 특정 문자열로 끝나는지 확인한다. |
| .str.len() | 문자열의 길이를 반환한다. |
| .str.strip() | 양쪽 공백을 제거한다. |
| .str.upper() | 대문자로 변환한다. |
| .str.lower() | 소문자로 변환한다. |
| .str.extract() | 정규 표현식을 이용하여 패턴 추출한다. |
data["조식메뉴"][0]
모닝롤/찐빵 우유/두유/주스 계란후라이 호두죽/쌀밥 (쌀:국내산) 된장찌개 쥐어채무침 포기김치 (배추,고추가루:국내산)
*로 변환data["조식메뉴"][0].replace(' ', '*')
모닝롤/찐빵**우유/두유/주스*계란후라이**호두죽/쌀밥*(쌀:국내산)*된장찌개**쥐어채무침**포기김치*(배추,고추가루:국내산)*
# 전체 메뉴에 적용
data["조식메뉴"] = data["조식메뉴"].str.replace(" ", ' ')
data["중식메뉴"] = data["중식메뉴"].str.replace(" ", ' ')
data["석식메뉴"] = data["석식메뉴"].str.replace(" ", ' ')
# 확인
data["조식메뉴"][0]
모닝롤/찐빵 우유/두유/주스 계란후라이 호두죽/쌀밥 (쌀:국내산) 된장찌개 쥐어채무침 포기김치 (배추,고추가루:국내산)
# 전체 메뉴에 적용
data["조식메뉴"] = data["조식메뉴"].str.split(' ')
data["중식메뉴"] = data["중식메뉴"].str.split(' ')
data["석식메뉴"] = data["석식메뉴"].str.split(' ')
# 확인
data["조식메뉴"][0]
['모닝롤/찐빵',
'우유/두유/주스',
'계란후라이',
'호두죽/쌀밥',
'(쌀:국내산)',
'된장찌개',
'쥐어채무침',
'포기김치',
'(배추,고추가루:국내산)',
'']
향후 진행되는 '텍스트 마이닝' 과정에서 좀 더 상세하게 다룰 예정이니 여기서는 특징만 알고 넘어가기



TF: 단어가 각 문서에서 발생한 빈도
DF: 단어가 등장한 문서의 수
적은 문서에서 상대적으로 많이 발견될수록 가치 있는 정보
많은 문서에서 자주 등장하는 단어일수록 일반적인 단어
단어가 특정 문서에서만 나타나는 희소성을 반옇가이 위해 TF에 DF의 역수(IDF)를 곱한 값을 사용
# 자연어 처리는 시간이 부족해서 생략
# 추후 텍스트 마이닝에서 다룰 예정
data2 = pd.read_csv("./data/Cafeteria_preprocess.csv", encoding="UTF-8")
# 메인 메뉴별 중식계 식수 확인
df_lunch = pd.DataFrame(data2.groupby("중식메뉴_Main")["중식계"].mean())
df_lunch10 = df_lunch.sort_values(by="중식계", ascending=False).head(10)
df_lunch10.plot(kind="barh")
plt.xlim(1300)

# 석식 확인
df_dinner = pd.DataFrame(data2.groupby("석식메뉴_Main")["석식계"].mean())
df_dinner10 = df_dinner.sort_values(by="석식계", ascending=False).head(10)
df_dinner10.plot(kind="barh")
plt.xlim(700)

→ 전처리가 잘못 된 것 같다는 생각:
FT-IDF가 상대적으로 보기 때문에 실곤약초무침이 사실 메인이 아니라 서브 메뉴인데 빈도수 문제로 메인메뉴에 선정될 수 있음
(TF-IDF는 단어에 많이 쓰이지 메뉴를 전처리하는 방식은 아니기 때문)
하지만 단순 빈도수보다는 낫기 때문에 해당 전처리 방식을 사용한 것

# 불필요한 컬럼 삭제
# 이미 유의미한 데이터를 추출한 컬럼
data2.drop(["일자", "조식메뉴", "중식메뉴", "석식메뉴"], axis=1, inplace=True)
# 수치형 데이터 타입, 범주형 데이터 타입 분리 (for 스케일링, 인코딩)
numeric_list = [] # 수치형 컬럼명 담을 list
categorical_list = [] # 범주형 컬럼명 담을 list
for i in data2.columns:
if data2[i].dtype == 'O':
categorical_list.append(i)
else:
numeric_list.append(i)
# 숫자 데이터 → 스케일링
# 데이터의 분포 확인을 위한 히스토그램 출력해보기
data2[numeric_list].hist(figsize=(10, 10))
plt.show()

| 항목 | 분포 형태 | 설명 |
|---|---|---|
| 본사정원수 | 다중 피크를 가진 비정규 분포 | 특정 구간에서 집중되는 경향이 있으며, 전체 범위가 비교적 넓음 |
| 본사휴가자수 | 오른쪽으로 치우친 비대칭 분포 | 대부분의 값이 0에서 200 사이에 집중되어 있으며, 일부 높은 값들이 존재함 |
| 본사출장자수 | 정규분포에 가까움 | 값이 평균을 중심으로 어느 정도 고르게 분포되어 있음 |
| 본사시간외근무명령서승인건수 | 오른쪽으로 치우친 비대칭 분포 | 대부분의 값이 0에서 400 사이에 집중되어 있으며, 일부 높은 값들이 존재 |
| 현본사소속재택근무자수 | 매우 오른쪽으로 치우친 비대칭 분포 | 대부분의 값이 0에 매우 가깝게 집중되어 있으며, 일부 높은 값들이 존재함 |
| 중식계 | 정규분포에 가까움 | 값이 평균을 중심으로 고르게 분포되어 있음 |
| 석식계 | 정규분포에 가까움 | 값이 평균을 중심으로 고르게 분포되어 있음 |
| month | 균등 분포 | 각 월이 조금씩 차이가 있으나, 균등하게 분포되어 있음 |
# 스케일링
from sklearn.preprocessing import MinMaxScaler
# 스케일러 객체 생성
scaler = MinMaxScaler()
# 스케일러 학습 및 변환
df_scaled = data2.copy()
# 스케일러 적용
data2[numeric_list] = scaler.fit_transform(df_scaled[numeric_list])
data2[categorical_list]

# 요일 원핫 인코딩
data2 = pd.get_dummies(data2, columns=["요일"], dtype="int64")
# categorical_list에서 3개의 메인 메뉴만 추출
categorical_list[1:]
# 반복문을 활용하여 모두 딕셔너리로 변경
for col in categorical_list[1:]:
freq_map = data2[col].value_counts().to_dict()
data2[f"{col}_encoded"] = data2[col].map(freq_map)
# Main 텍스트 데이터 삭제
data2.drop(columns=["조식메뉴_Main", "중식메뉴_Main", "석식메뉴_Main"], inplace=True)
from sklearn.cluster import KMeans
# 모델 객체 생성
km_model = KMeans(
n_clusters=3 # 클러스터의 수 (군집의 수) 설정 (기본값: 8)
, random_state=10 # 고정 규칙 → 초기 중심점을 랜덤으로 결정하기 때문에 재현성을 위해 고정해 주면 좋음
, n_init = 10 # 내부적으로 10번의 학습 진행 → SSE가 가장 낮은 값을 최종 결과로 활용
, max_iter=300 # 중심점의 이동 횟수(하나의 n_init마다 300번 움직인다는 의미)
)
# 학습
km_model.fit(data2)
# 군집 결과 확인하기
print("클러스터의 개수:", km_model.n_clusters)
print("클러스터 레이블:", km_model.labels_)
print("클러스터 레이블의 고유값:", np.unique(km_model.labels_))
# 각 클러스터에 분류된 데이터 포인트 수 확인
unique, counts = np.unique(km_model.labels_, return_counts=True)
cluster_counts = dict(zip(unique, counts))
print("클러스터별 데이터 포인트 수:", cluster_counts)
print("클러스터별 데이터 포인트 수:", np.bincount(km_model.labels_))
클러스터의 개수: 3
클러스터 레이블: [1 2 0 ... 1 1 1]
클러스터 레이블의 고유값: [0 1 2]
클러스터별 데이터 포인트 수: {np.int32(0): np.int64(200), np.int32(1): np.int64(490), np.int32(2): np.int64(472)}
클러스터별 데이터 포인트 수: [200 490 472]
# 군집화 후 각 중심점에서 군집의 데이터 간 거리를 제곱하여 합산: SSE
km_model.inertia_ # 출력: 3534.3412440404613
# 오류라고 생각하면 안 됨! (오류 개념 아님)
# 데이터 포인트틀이 중심에 얼마나 더 가까워졌는지를 확인하는 수치값
# 클러수터 수 범위 설졍
c_r = range(1, 9+1)
# 반복문을 활용하여 각 클러스터 개수에 따른 SSE를 누적
# 리스트명 = [실행문장 for i in range()]
kmeans_fit = [KMeans(n_clusters=k,n_init=20, max_iter=300, random_state=7).fit(data2) for k in c_r]
kmeans_sse = [model.inertia_ for model in kmeans_fit]
# 한 번에 쓸 수도 있음
# sse_list = [KMeans(n_clusters=k, random_state=10).fit(data2).inertia_ for k in c_r]
plt.figure(figsize=(8, 3.5))
plt.plot(c_r, kmeans_sse, marker='o', color="purple")
plt.xlabel("클러스터 수")
plt.ylabel("SSE")
plt.show()

# 10개의 모델들 중에서 클러스터의 개수가 3인 모델을 가져와 실루엣 지수 확인
kmeans_fit[2]
# 실루엣 지수 확인
from sklearn.metrics import silhouette_score
silhouette_score(data2, kmeans_fit[2].labels_) # 출력: np.float64(0.29610377253257636)
# 1에 가까울수록 데이터 포인트가 잘 군집화 되고, 다른 군집과는 잘 분리됨
# 0에 가까울수록 데이터포인트가 클러스터 경계에 위치하고 있음을 의미
# -1에 가까울수록 데이터 포인트가 잘 못 군집화 되어 있음을 의미
# 실루엣 지수 시각화
sil_score = [silhouette_score(data2, model.labels_) for model in kmeans_fit[1:]]
# 군집을 1로 하는 건 군집화가 아니므로 0번 인덱스 모델은 가져오면 안 됨
plt.plot(range(2, 9+1), sil_score, marker='o')
plt.grid()
plt.show()

import scipy.cluster.hierarchy as sch
plt.figure(figsize=(12,8))
dend = sch.dendrogram(sch.linkage(data2, method="ward"))
plt.xlabel("samples")
plt.show()

# 이해를 돕기 위한 예시
mglearn.plots.plot_dbscan()
min_samples: 2 eps: 1.000000 cluster: [-1 0 0 -1 0 -1 1 1 0 1 -1 -1]
min_samples: 2 eps: 1.500000 cluster: [0 1 1 1 1 0 2 2 1 2 2 0]
min_samples: 2 eps: 2.000000 cluster: [0 1 1 1 1 0 0 0 1 0 0 0]
min_samples: 2 eps: 3.000000 cluster: [0 0 0 0 0 0 0 0 0 0 0 0]
min_samples: 3 eps: 1.000000 cluster: [-1 0 0 -1 0 -1 1 1 0 1 -1 -1]
min_samples: 3 eps: 1.500000 cluster: [0 1 1 1 1 0 2 2 1 2 2 0]
min_samples: 3 eps: 2.000000 cluster: [0 1 1 1 1 0 0 0 1 0 0 0]
min_samples: 3 eps: 3.000000 cluster: [0 0 0 0 0 0 0 0 0 0 0 0]
min_samples: 5 eps: 1.000000 cluster: [-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]
min_samples: 5 eps: 1.500000 cluster: [-1 0 0 0 0 -1 -1 -1 0 -1 -1 -1]
min_samples: 5 eps: 2.000000 cluster: [-1 0 0 0 0 -1 -1 -1 0 -1 -1 -1]
min_samples: 5 eps: 3.000000 cluster: [0 0 0 0 0 0 0 0 0 0 0 0]

from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=0.5, min_samples=4)
data2["dbscan_cluster"] = dbscan.fit_predict(data2)
data2["dbscan_cluster"].value_counts()
# -1: 노이즈 포인트
# 적절한 하이퍼파라미터를 찾는 게 중요함
count
dbscan_cluster
-1 639
4 45
8 44
27 44
18 42
44 36
0 13
15 10
12 10
29 10
7 9
32 9
36 9
2 9
13 9
1 8
10 8
25 8
22 8
11 7
37 7
47 7
34 7
48 7
28 7
23 7
39 6
20 6
30 6
6 6
35 6
52 5
40 5
5 5
17 5
14 5
16 5
53 5
46 5
51 5
55 4
50 4
21 4
9 4
54 4
26 4
3 4
19 4
24 4
41 4
31 4
38 4
33 4
43 4
45 4
42 4
49 4
dtype: int64
MinPts 설정하기
Eps 설정하기
DBSCAN 강의
파이썬 사이킷런 DBSCAN 군집화 과정
dbscan = DBSCAN(eps=3, min_samples=17)
cluster, counts = np.unique(dbscan.fit_predict(data2), return_counts=True)
cluster, counts
(array([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
array([ 22, 673, 50, 52, 116, 19, 42, 102, 23, 27, 36]))
dbscan = DBSCAN(eps=2, min_samples=17)
cluster, counts = np.unique(dbscan.fit_predict(data2), return_counts=True)
cluster, counts
(array([-1, 0, 1, 2, 3, 4, 5]),
array([307, 644, 45, 44, 42, 44, 36]))
dbscan = DBSCAN(eps=3, min_samples=17)
cluster, counts = np.unique(dbscan.fit_predict(data2), return_counts=True)
cluster, counts # dict(zip(cluster, counts))로 묶어도 OK
data2["dbscan_cluster"] = dbscan.fit_predict(data2)
pd.plotting.scatter_matrix(
data2[numeric_list] # 문제 데이터(좌표 역할)
, figsize=(10,10) # 그래프의 크기
, c=data2["dbscan_cluster"] # 클래스별 색상 지정
, alpha=0.5 # 그래프 산점도 점 투명도 → 겹치는 걸 보여주려고
)
plt.show()
