Today I Learned

[내일배움캠프 QA/QC 6기] TIL #035

JiJi0406 2026. 6. 30. 23:29

001

오늘의 코드카타

46. 숫자 문자열과 영단어

 

 


문제 설명

네오와 프로도가 숫자놀이를 하고 있습니다. 네오가 프로도에게 숫자를 건넬 때 일부 자릿수를 영단어로 바꾼 카드를 건네주면 프로도는 원래 숫자를 찾는 게임입니다.

다음은 숫자의 일부 자릿수를 영단어로 바꾸는 예시입니다.

1478 → "one4seveneight"
234567 → "23four5six7"
10203 → "1zerotwozero3"

 

이렇게 숫자의 일부 자릿수가 영단어로 바뀌어졌거나, 혹은 바뀌지 않고 그대로인 문자열 s가 매개변수로 주어집니다. s가 의미하는 원래 숫자를 return 하도록 solution 함수를 완성해주세요.

 

제한사항

  • 1 ≤ s의 길이 ≤ 50
  • s가 "zero" 또는 "0"으로 시작하는 경우는 주어지지 않습니다.
  • return 값이 1 이상 2,000,000,000 이하의 정수가 되는 올바른 입력만 s로 주어집니다.

 

더보기
def solution(s):
    
    eng_to_num = {"one": "1", "two": "2", "three": "3", "four": "4", "five": "5", "six": "6", "seven": "7", "eight": "8", "nine": "9", "zero": "0"}
    
    for eng, num in eng_to_num.items():
        
        s = s.replace(eng,num)
        
    return int(s)

 

양심고백: 딕셔너리랑 for문 돌리는 것까진 잘 작성했는데, replace는 생각 못해서 찾아봄..ㅎㅎ

 

 

 

 


 

 

002

오늘 학습한 내용

 

[ 머신러닝의 이해와 라이브러리 활용 심화 ]

 

오늘까지 다 들어야 했었는데 겨우겨우 다 들었다... 너무 오래 걸렸다

다 듣긴 했는데... 흠...

 

 

 

1. 의사결정나무 (Decision Tree)

  • 의사결정규칙을 나무 구조로 나타내서 데이터를 분류/예측하는 방법
  • 루트 노드(시작점) → 리프 노드(중간/최종 노드)로 가지를 뻗어나가는 구조
  • 불순도(impurity, 보통 지니계수 0~1)가 낮아지는 방향으로 나무가 자람 → 리프 노드로 갈수록 한쪽 클래스로 쏠리게 분류됨

1) 장단점 

  • 장점: 해석 쉬움, 분류/회귀 둘 다 가능, 이상치에 견고, 스케일링 불필요
  • 단점: 나무가 너무 자라면 과대적합, 데이터 작은 변화에도 구조가 크게 바뀜(불안정성)

 

2. 실습 - 타이타닉 의사결정나무

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder
from sklearn.tree import DecisionTreeClassifier, plot_tree

titanic_df = pd.read_csv('/content/train.csv')
titanic_df.info()

X_feature = ['Pclass', 'Sex', 'Age']
# Pclass, Sex는 레이블 인코딩, Age는 결측치를 평균으로 대치할 계획

le = LabelEncoder()
titanic_df['Sex'] = le.fit_transform(titanic_df['Sex'])      # Sex를 0/1로 변환
le2 = LabelEncoder()
titanic_df['Pclass'] = le2.fit_transform(titanic_df['Pclass'])  # Pclass도 인코딩

age_mean = titanic_df['Age'].mean()
titanic_df['Age'] = titanic_df['Age'].fillna(age_mean)        # Age 결측치 평균으로 대치

X = titanic_df[X_feature]
y = titanic_df['Survived']

model_dt = DecisionTreeClassifier(max_depth = 2)
# max_depth 제한 안 하면 나무가 끝없이 자람
# 만들 때마다 살짝씩 결과가 달라질 수 있음(불안정성)
# random_state를 고정하면 평가를 일관되게 할 수 있음

model_dt.fit(X,y)

plt.figure(figsize = (10,5))
plot_tree(model_dt, feature_names=X_feature, class_names=['Not Survived', 'Survived'], filled=True)  # 트리 구조 시각화
plt.show()

 

 

 

3. 랜덤 포레스트 (Random Forest)

1) 배깅(Bagging)의 원리 

  • 의사결정나무 하나로는 과적합+불안정성 문제가 있어서, 나무를 여러 개 만들어 숲(Forest)을 만드는 아이디어로 보완
  • Bootstrapping(데이터 복원추출로 유사하지만 다른 데이터셋 여러 개 생성) + Aggregating(여러 모델 결과를 합침) = Bagging
  • 여러 나무가 각자 결과를 내고 다수결로 최종 결론을 냄 → 한 나무가 훈련 데이터에 민감했던 단점 극복

2) 장단점

  • 장점: 과적합 방지, 이상치에 견고, 스케일링 불필요, 변수 중요도(feature_importances_)로 모델 해석 가능
  • 단점: 연산 비용 크고, 앙상블이라 해석이 어려워짐

 

4. K-최근접 이웃 (KNN)

  • "유유상종" 아이디어: 주변 데이터 K개를 보고 가장 많은 클래스로 예측함
  • K를 몇으로 잡느냐, 거리를 어떻게 재느냐(보통 유클리드 거리)가 핵심 하이퍼파라미터
  • 거리 기반 알고리즘이라 표준화가 필수 !! (단위 영향을 크게 받기 때문)

1) 장단점

  • 장점: 직관적, 데이터 분포 가정 안 함, 회귀/분류 둘 다 가능
  • 단점: 차원 많아질수록 계산량 폭증, 스케일링 필요

5. 부스팅 알고리즘

  • 약한 학습기를 순차적으로 학습시키면서, 틀린 데이터에 가중치를 줘서 점점 오류를 줄여나가는 방식
  • 종류: Gradient Boosting(경사하강법으로 가중치 업데이트), XGBoost(트리 기반 앙상블, Kaggle에서 인기, 병렬학습으로 빠름), LightGBM(XGBoost보다 학습시간+메모리 적게 듦, 다만 작은 데이터에선 과적합 위험)

 

6. 실습 - 여러 모델 성능 비교 + 변수 중요도

from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score

model_lor = LogisticRegression()
model_dt = DecisionTreeClassifier(random_state=42)
model_rf = RandomForestClassifier(random_state=42)

model_lor.fit(X,y)
model_dt.fit(X,y)
model_rf.fit(X,y)

y_lor_pred = model_lor.predict(X)
y_dt_pred = model_dt.predict(X)
y_rf_pred = model_rf.predict(X)

def get_score(model_name, y_true, y_pred):
  acc = round(accuracy_score(y_true, y_pred), 3)
  f1 = round(f1_score(y_true, y_pred), 3)
  print(model_name, 'acc 스코어는:', acc, 'f1_score는:', f1)

get_score('lor', y, y_lor_pred)
get_score('dt', y, y_dt_pred)
get_score('rf', y, y_rf_pred)

model_rf.feature_importances_
# 순서대로 변수의 중요도를 나타냄. 시각화해서 확인 가능

from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import GradientBoostingClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier

model_knn = KNeighborsClassifier()
model_gbm = GradientBoostingClassifier(random_state=42)
model_xgb = XGBClassifier(random_state=42)
model_lgb = LGBMClassifier(random_state=42)

model_knn.fit(X,y)
model_gbm.fit(X,y)
model_xgb.fit(X,y)
model_lgb.fit(X,y)

y_knn_pred = model_knn.predict(X)
y_gbm_pred = model_gbm.predict(X)
y_xgb_pred = model_xgb.predict(X)
y_lgb_pred = model_lgb.predict(X)

get_score('lor', y, y_lor_pred)
get_score('dt', y, y_dt_pred)
get_score('rf', y, y_rf_pred)
get_score('knn', y, y_knn_pred)
get_score('gbm', y, y_gbm_pred)
get_score('xgb', y, y_xgb_pred)
get_score('lgb', y, y_lgb_pred)

# 조작 많이 해보면 랜덤포레스트 98점까지도 찍을 수 있음
# 모델 더 좋게 만들고 싶으면 X에 변수 더 추가해보기

 

 

7. 비지도학습 개요 & K-Means

1) 지도학습 vs 비지도학습

  • 지도학습은 X와 정답(Y)을 같이 주고 학습, 비지도학습은 정답 없이 데이터 간 유사성으로 그룹을 나눔
  • (예: 고객을 헤비유저/일반유저로 그룹화, 구매내역 기반 그룹화) → 정답이 없으니 지도학습보다 어렵고 분석가의 주관적 판단이 들어감

2) K-Means 알고리즘 

  • K개 군집 수 정하기 → 임의의 중심점 선정 → 가까운 데이터끼리 그룹화 → 그룹의 무게중심으로 중심점 이동 → 다시 그룹화 (반복)
  • 장점: 적용하기 쉬움 
  • 단점: 차원 많으면 정확도↓, K를 주관적으로 정해야 함, 이상치에 취약(평균 기반이라)

 

8. 군집평가 - 실루엣 계수

  • 정답이 없는 비지도학습이라 평가가 어려운데, 군집 간 거리는 멀고 군집 내 거리는 가까울수록 좋은 군집화
  • 실루엣 값이 1에 가까울수록 좋음, silhouette_score 함수로 평균값 산출

 

9. 실습 - Iris 데이터 K-Means 군집화

import seaborn as sns

iris_df = sns.load_dataset('iris')
iris_df.head(3)

sns.scatterplot(data=iris_df, x='sepal_length', y='sepal_width')           # 라벨 없이 본 산점도
sns.scatterplot(data=iris_df, x='sepal_length', y='sepal_width', hue='species')  # 실제 라벨(species) 있는 산점도

iris_df2 = iris_df[['sepal_length','sepal_width', 'petal_length', 'petal_width']]  # 라벨 빼고 X만 추출
iris_df2.head(3)

from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters = 3, init = 'k-means++', max_iter = 300, random_state=42)  # 군집 3개로 설정
kmeans.fit(iris_df2)

iris_df2['target'] = iris_df['species']     # 실제 라벨 따로 보관 (비교용)
iris_df2['cluster'] = kmeans.labels_        # K-means가 예측한 군집 라벨
iris_df2

plt.figure(figsize = (12,6))
plt.subplot(1,2,1)
sns.scatterplot(data = iris_df2, x='sepal_length', y='sepal_width', hue='target')
plt.title('Original')                       # 실제 종 기준 분포

plt.subplot(1,2,2)
sns.scatterplot(data = iris_df2, x='sepal_length', y='sepal_width', hue='cluster', palette='viridis')
plt.title('Clustering')                     # K-means가 나눈 군집 분포 → 둘이 얼마나 비슷한지 비교

 

10. RFM 고객 세그멘테이션

1) RFM

  • 고객 관계 관리(CRM)에서 가장 많이 쓰는 비지도학습 응용, 고객을 세 가지 기준으로 분류
  • Recency(가장 최근 구매로부터 지난 시간), Frequency(구매 횟수), Monetary(총 구매 금액)

2) 흐름

  • 데이터 로드 → EDA → 전처리(CustomerID 결측치 삭제, 취소 주문/음수 수량, 단가 제거, 영국 데이터만 추출) → RFM 가공(날짜 계산) → 스케일링 → K-means 군집화 → 평가(실루엣 점수)

11. 실습 - RFM 데이터 전처리 및 군집화

retail_df = pd.read_excel('/content/Online Retail.xlsx')
retail_df.head(3)

# 1. 데이터 EDA 및 전처리
retail_df.describe(include = 'all')

# 주문 수량, 제품 단가가 음수인 게 있어서 확인
cond1 = retail_df['Quantity'] < 0
retail_df[cond1]

# 전처리 전략: CustomerID 결측치 삭제 / Invoice가 'C'로 시작(취소건)·수량 음수·단가 음수는 모두 제외
cond_cust = (retail_df['CustomerID'].notnull())
cond_invo = (retail_df['InvoiceNo'].astype(str).str[0] != 'C')
cond_minus = (retail_df['Quantity'] > 0) & (retail_df['UnitPrice'] > 0)

retail_df_2 = retail_df[cond_cust & cond_invo & cond_minus]   # 세 조건 모두 만족하는 데이터만 남김
retail_df_2.info()

retail_df_2['Country'].value_counts()[:10]   # 국가 분포 확인 → 대부분 UK

cond_uk = (retail_df_2['Country'] == 'United Kingdom')
retail_df_2 = retail_df_2[cond_uk]           # 영국 데이터만 사용

retail_df_2['Amt'] = retail_df_2['Quantity'] * retail_df_2['UnitPrice']   # 구매 금액 컬럼 생성
retail_df_2['Amt'] = retail_df_2['Amt'].astype('int')

retail_df_2.pivot_table(index = 'CustomerID', values = 'Amt', aggfunc='sum').sort_values('Amt', ascending = False)
# 고객별 총 구매금액 확인

import datetime as dt
# 2011.12.10 기준으로 각 주문일과의 날짜 차이 계산 (+1)
# 추후 CustomerID별 최소값을 구하면 그게 Recency가 됨
retail_df_2['Period'] = (dt.datetime(2011,12,10) - retail_df_2['InvoiceDate']).apply(lambda x: x.days+1)

rfm_df = retail_df_2.groupby('CustomerID').agg({
    'Period' : 'min',        # 가장 최근 구매일 (Recency)
    'InvoiceNo' : 'count',   # 구매 횟수 (Frequency)
    'Amt' : 'sum'            # 총 구매금액 (Monetary)
})
rfm_df.columns = ['Recency', 'Frequency', 'Monetary']

sns.histplot(rfm_df['Recency'])
sns.histplot(rfm_df['Frequency'])
sns.histplot(rfm_df['Monetary'])   # 세 지표 분포 확인

# 데이터 정규화 (거리 기반 알고리즘이라 스케일링 필수)
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_features = sc.fit_transform(rfm_df[['Recency', 'Frequency', 'Monetary']])

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

Kmeans = KMeans(n_clusters = 3, random_state=42)
labels = Kmeans.fit_predict(X_features)
rfm_df['label'] = labels

silhouette_score(X_features, labels)   # 군집화 품질 점수 확인

from kmeans_visual import visualize_silhouette
visualize_silhouette([2,3,4,5,6], X_features)
# K값별로 실루엣 시각화 → 색이 5개 보여야 디테일하게 분류된 건데, 2개밖에 안 보임 → 전처리 재시도 필요

# 로그 스케일로 한번 더 전처리 (분포가 치우쳐 있어서)
import numpy as np
rfm_df['Recency_log'] = np.log1p(rfm_df['Recency'])
rfm_df['Frequency_log'] = np.log1p(rfm_df['Frequency'])
rfm_df['Monetary_log'] = np.log1p(rfm_df['Monetary'])

X_features2 = rfm_df[['Recency_log', 'Frequency_log', 'Monetary_log']]
sc2 = StandardScaler()
X_features2_sc = sc2.fit_transform(X_features2)

visualize_silhouette([2,3,4,5,6], X_features2_sc)   # 로그 변환 후 다시 실루엣 시각화로 군집 확인

 

→ RFM 군집화는 처음엔 군집이 잘 안 갈렸다가, 로그 스케일링을 추가하고 나서야 더 잘 나뉘는 걸 확인 : 전처리가 군집화 품질에 직접적인 영향을 준다!