27일차는 오전에는 머신러닝 모델 튜닝 흐름을 Optuna로 확장했고, 오후에는 직접 준비한 EDA 발표를 진행한 날이었다. 전날까지는 RandomizedSearchCV와 GridSearchCV로 하이퍼파라미터를 찾았다면, 이번에는 사람이 직접 후보값을 하나하나 나열하기보다 범위를 주고, 그 안에서 최적의 값을 탐색하도록 맡기는 방식을 봤다. 같은 Random Forest 튜닝이라도 접근 방식이 조금 달라졌다.
오후 발표는 CPI와 프랜차이즈 데이터를 활용해서 “엽떡 14,000원은 정말 비쌀까?”라는 질문을 EDA로 풀어본 내용이었다. 발표의 핵심은 가격 유지가 성장의 원인이라고 단정하는 것이 아니라, 물가 대비 가격 부담 변화와 브랜드 성과 지표가 어떤 패턴을 보이는지 탐색하는 것이었다. 발표 자료에서도 분석 질문을 상대가격 부담, 경쟁 브랜드 대비 시장 위치, 성장과 안정성, 독보적 위치의 원인 후보로 나누어 잡았다.
1. RandomizedSearchCV와 GridSearchCV 이후에 Optuna로 넘어갔다
전날에는 Random Forest의 성능을 올리기 위해 RandomizedSearchCV와 GridSearchCV를 사용했다. RandomizedSearchCV는 내가 지정한 후보 조합 중 일부를 랜덤하게 골라 실험하고, GridSearchCV는 내가 지정한 조합을 전부 실험하는 방식이었다. 이 방식은 명확하긴 하지만, 후보값을 사람이 직접 리스트로 만들어야 한다는 점이 있었다.
예를 들어 이런 방식이다.
parameters = {
"n_estimators": [30, 50, 70, 100],
"max_features": [5, 6, 7],
"max_depth": [5, 6, 7],
"min_samples_split": [7]
}
이 방식은 내가 생각한 후보 안에서만 탐색한다. 그런데 실제 최적값이 n_estimators=3591처럼 내가 직접 적지 않은 값 근처에 있을 수도 있다. 그래서 이번에는 Optuna를 이용해서 “내가 후보값을 전부 찍는 것”이 아니라, 탐색할 범위를 주고 그 안에서 더 나은 값을 찾아가게 하는 방식을 봤다. 강의에서는 Optuna도 완벽한 정답을 찾아주는 도구라기보다, 최적화할 때 사용할 수 있는 하나의 탐색 도구로 보는 것이 맞다고 설명했다.
2. Optuna는 직접 목적 함수를 만들어야 했다
Optuna를 사용할 때 가장 먼저 낯설었던 부분은 목적 함수를 직접 만들어야 한다는 점이었다. GridSearchCV처럼 모델과 파라미터 딕셔너리를 넘기면 알아서 끝나는 구조가 아니라, 어떤 파라미터를 어떤 범위에서 제안받고, 그 값으로 모델을 만들고, 어떤 점수를 반환할지를 직접 함수로 짜야 했다.
기본 구조는 이런 느낌이었다.
def rf_objective(trial):
params = {}
params["n_estimators"] = trial.suggest_int(
"n_estimators",
10,
5000
)
params["criterion"] = trial.suggest_categorical(
"criterion",
["gini", "entropy", "log_loss"]
)
params["max_depth"] = trial.suggest_int(
"max_depth",
2,
50
)
params["max_features"] = trial.suggest_int(
"max_features",
2,
X_train.shape[1]
)
params["min_samples_split"] = trial.suggest_int(
"min_samples_split",
2,
10
)
rf = RandomForestClassifier(
**params,
n_jobs=-1,
random_state=1234
)
scores = cross_val_score(
rf,
X_train,
y_train,
cv=kfold,
scoring="accuracy"
)
return scores.mean()
여기서 trial.suggest_int()는 정수 범위에서 값을 제안받는 방식이고, trial.suggest_categorical()은 정해진 후보 중 하나를 고르는 방식이다. 이 부분 때문에 Optuna 매뉴얼과 Random Forest 매뉴얼을 같이 봐야 했다. Random Forest의 어떤 파라미터가 정수인지, 실수인지, 문자열 후보인지 먼저 확인하고, 그에 맞춰 Optuna의 suggest_int, suggest_float, suggest_categorical을 골라야 하기 때문이다. 강의에서도 Optuna 쪽 trial 매뉴얼과 Random Forest 쪽 파라미터 매뉴얼을 같이 보면서 세팅해야 한다고 설명했다.
3. maximize와 minimize를 직접 정해야 했다
목적 함수를 만들고 나면, Optuna에게 이 값을 크게 만들 것인지 작게 만들 것인지 알려줘야 한다. 이번에는 accuracy를 기준으로 했기 때문에 값이 클수록 좋다. 그래서 direction="maximize"를 사용했다.
import optuna
rf_study = optuna.create_study(
direction="maximize"
)
rf_study.optimize(
rf_objective,
n_trials=20
)
만약 반환값을 1 - accuracy처럼 error로 만들었다면 방향은 minimize가 되어야 한다. 이 부분이 생각보다 중요했다. 같은 모델을 튜닝하더라도 내가 목적 함수를 무엇으로 정의했는지에 따라 최적화 방향이 달라진다.
accuracy를 반환한다
→ maximize
error를 반환한다
→ minimize
이번 실습에서는 시간 관계상 trial을 많이 돌리지는 않았지만, 구조상 trial 수를 늘리면 더 많은 후보를 탐색할 수 있다. 다만 trial 수가 늘어날수록 시간이 오래 걸린다. Random Forest 자체도 여러 tree를 만들기 때문에 무거운데, 그걸 K-Fold까지 돌리면 계산량이 커진다. 그래서 나중에 실제 프로젝트에서는 시간과 리소스를 같이 고려해야 한다는 점도 남았다.
4. Optuna는 시각화로 탐색 과정을 볼 수 있었다
Optuna를 쓰는 이유 중 하나는 최적화 과정을 시각화할 수 있다는 점이었다. plot_optimization_history를 사용하면 trial이 진행되면서 objective 값이 어떻게 변했는지 볼 수 있다.
optuna.visualization.plot_optimization_history(rf_study)
이 그래프를 보면 몇 번째 시도에서 더 좋은 값이 나왔는지, 이후에 성능이 더 올라가는지, 아니면 어느 정도 정체되는지 확인할 수 있다. 만약 그래프가 계단식으로 계속 올라가고 있다면 trial을 더 늘려볼 수도 있고, 어느 순간부터 더 이상 개선이 없다면 그쯤에서 멈출 수도 있다. 강의에서도 optimization history를 통해 더 시도할지, 멈출지 판단할 수 있다고 설명했다.
또 plot_param_importances를 사용하면 어떤 하이퍼파라미터가 objective 값에 영향을 많이 줬는지도 볼 수 있다.
optuna.visualization.plot_param_importances(rf_study)
이 부분이 RandomizedSearchCV나 GridSearchCV보다 편하게 느껴졌다. 단순히 “이 조합이 제일 좋았다”에서 끝나는 것이 아니라, 어떤 파라미터를 더 신경 써야 할지 다음 실험의 방향을 잡을 수 있기 때문이다. 예를 들어 min_samples_split의 영향이 크고 criterion의 영향이 작다면, 다음에는 criterion 후보는 줄이고 min_samples_split 주변을 더 촘촘하게 볼 수 있다.
5. Optuna가 최종 모델을 바로 주는 것은 아니었다
조금 헷갈릴 수 있는 부분은 Optuna가 최적의 모델 객체를 바로 주는 구조는 아니라는 점이었다. GridSearchCV는 best_estimator_를 통해 가장 좋은 모델을 바로 가져올 수 있었지만, Optuna는 기본적으로 좋은 파라미터 조합을 알려준다. 그래서 그 파라미터를 이용해 모델을 다시 만들어 학습해야 했다.
best_params = rf_study.best_params
rf_best = RandomForestClassifier(
**best_params,
n_jobs=-1,
random_state=1234
)
rf_best.fit(X_train, y_train)
pred = rf_best.predict(X_val)
accuracy_score(y_val, pred)
이 과정이 조금 번거롭기 때문에, 강의에서는 Optuna로 대략 좋은 범위를 찾고, 그 주변을 다시 GridSearchCV로 좁혀가며 최종 모델을 가져오는 방식도 이야기했다. 실제로는 도구 하나만 고집하기보다, Optuna, RandomizedSearchCV, GridSearchCV를 상황에 맞게 섞어 쓸 수 있어야 한다는 느낌이었다.
6. Feature Importance로 모델이 중요하게 본 변수를 확인했다
Random Forest 같은 tree 기반 모델은 feature importance를 볼 수 있다는 장점이 있다. Decision Tree 계열은 어떤 변수가 분기 기준으로 많이 쓰였고, 얼마나 분류에 기여했는지를 계산할 수 있기 때문이다. 이번 타이타닉 데이터에서는 Fare, Age, Sex, Pclass 같은 변수가 중요하게 나왔다고 설명됐다.
rf_best.feature_importances_
이 값만 보면 어떤 feature인지 바로 알기 어려우므로, 컬럼명과 함께 DataFrame으로 묶어 정렬했다.
feature_importance = pd.DataFrame(
{
"feature": X_train.columns,
"importance": rf_best.feature_importances_
}
)
feature_importance = feature_importance.sort_values(
by="importance",
ascending=True
)
feature_importance.plot(
kind="barh",
x="feature",
y="importance"
)
이 그래프는 모델이 어떤 변수를 중요하게 봤는지 확인하는 데 도움이 된다. 다만 이것도 “진짜 원인”이라고 단정하면 안 된다. 모델의 관점에서 예측에 도움이 된 변수이지, 인과관계를 증명한 것은 아니다. 그래도 다음 EDA나 feature engineering 방향을 잡는 데는 꽤 유용해 보였다.
7. 오후에는 개인 EDA 발표를 했다
오후에는 직접 준비한 EDA 발표를 진행했다. 주제는 **“CPI와 프랜차이즈 데이터를 활용한 엽떡 시장 위치 EDA: 14,000원 엽떡은 정말 비쌀까?”**였다. 발표는 500원이라는 소재에서 시작했다. 2026년에 500원은 크게 와닿지 않지만, 2012년 초등학생 시절에는 컵떡볶이나 슬러시를 먹을 수 있는 금액이었다는 비교로 물가 감각을 잡고, 같은 14,000원이라는 가격이 2012년과 2026년에 어떻게 다르게 느껴질 수 있는지로 들어갔다. 발표 자료에서도 2012년과 2026년의 500원 이미지, 그리고 2012년부터 2026년까지 14,000원으로 유지된 엽떡 가격을 시각적으로 보여줬다.
발표의 분석 질문은 네 가지였다.
1. 14,000원의 상대가격 부담은 어떻게 변했나?
2. 경쟁 브랜드 대비 어떤 시장 위치인가?
3. 성장과 안정성을 함께 보이는가?
4. 왜 독보적 위치를 갖게 되었을까?
중요했던 건 인과분석이 아니라 EDA라고 선을 그은 점이었다. 가격 유지가 성장의 원인이라고 증명하는 것이 아니라, 가격 부담 변화와 브랜드 성과 지표가 어떤 패턴을 보이는지 탐색하는 발표였다. 이 방향을 먼저 정리한 덕분에 발표의 범위가 조금 더 명확해졌다.
8. 발표 데이터는 CPI, 가격 시나리오, 가맹점 현황으로 나눴다
발표에서 사용한 데이터는 크게 세 가지였다. 첫 번째는 KOSIS 소비자물가조사의 CPI 데이터였다. 떡볶이 CPI와 음식서비스 CPI를 활용해서 14,000원의 물가 대비 부담이 어떻게 변했는지 보기 위한 데이터였다. 두 번째는 기사와 공식 홈페이지 탐색을 기반으로 한 엽떡 가격 시나리오였다. 세 번째는 공정위 가맹정보 API에서 가져온 브랜드별 가맹점 현황 데이터였다. 발표 자료에서도 각 데이터의 기간, 출처, 사용 목적을 따로 정리했다.
전처리에서는 평균매출 0값을 결측으로 처리했다. 가맹점이 있는데 평균매출이 0인 경우는 실제 매출 0이라기보다 미집계 가능성이 높다고 판단했다. 반면 계약종료·해지 수의 0은 실제로 0건일 수 있으므로 유지했다. 평균매출은 발표와 시각화 해석을 쉽게 하기 위해 억 원 단위로 바꿨다.
파생변수도 만들었다.
상대가격 부담지수
= 실제가격 / CPI 예상가격 × 100
계약종료·해지율
= 계약종료·해지수 / 가맹점수 × 100
순증감률
= (신규가맹점 - 계약종료·해지) / 가맹점수 × 100
이 파생변수들이 발표의 중심이었다. 단순히 가격과 매출을 보는 것이 아니라, 물가 대비 가격 부담, 가맹점 변동성, 확장 흐름을 따로 지표화했다는 점이 좋았다.
9. CPI 기준으로 보면 상대가격 부담은 낮아졌다
첫 번째 분석은 가격 부담이었다. 엽떡의 대표 가격은 14,000원으로 유지된 시나리오를 두고, 떡볶이 CPI와 음식서비스 CPI를 반영했을 때 예상 가격이 어떻게 달라지는지 비교했다. 발표 자료에서는 떡볶이 물가지수가 134.99까지 상승했고, 이를 반영하면 2026년 가격은 약 25,582원으로 재표현된다고 정리했다. 음식서비스 물가지수 기준으로는 124.72까지 상승했고, 반영 가격은 약 20,833원으로 계산했다.
여기서 결론은 “엽떡이 절대적으로 싸다”가 아니었다. 같은 14,000원이라도 CPI 기준으로 재표현하면 과거보다 상대적 가격 부담이 낮아졌다는 것이다.
절대가격
= 14,000원 유지
CPI 기준 예상가격
= 물가 상승을 반영한 가격
상대가격 부담
= 실제가격 / CPI 예상가격
이 표현이 발표에서 중요했다. 가격이 그대로라서 싸다는 말은 너무 단순하고, 소비자가 느끼는 부담은 물가와 소득, 대체재 가격 등 여러 요인과 연결된다. 그래서 이번 발표에서는 CPI를 기준으로 가격 부담을 재표현했다.
10. 엽떡은 소수 고매출이 아니라 대형 고매출 브랜드로 보였다
두 번째 분석은 브랜드 시장 포지션이었다. 2025년 기준 엽떡은 평균매출 8.80억, 가맹점 수 650개, 계약종료·해지율 0.15%, 순증감률 8.92%로 정리했다. 발표 자료에서도 이 수치를 2025년 엽기떡볶이 브랜드 지표로 제시했다.
여기서 단순히 평균매출만 높은 브랜드인지, 아니면 규모도 함께 큰 브랜드인지 확인하기 위해 가맹점 수와 평균매출을 함께 봤다. 매장이 적은 브랜드는 평균매출이 높게 보일 수 있기 때문이다. 발표 자료의 포지션 차트에서는 엽떡이 평균매출과 가맹점 수를 동시에 확보한 오른쪽 위 단독 포지션으로 나타났다.
평균매출 8.80억
→ 점포당 매출이 높음
가맹점 수 650개
→ 소수 점포 브랜드가 아님
오른쪽 위 포지션
→ 규모와 매출을 동시에 가진 브랜드
이 부분은 발표에서 꽤 중요한 포인트였다. 엽떡은 단순히 “매장이 많다”거나 “일부 점포 매출이 높다”가 아니라, 많은 가맹점 수를 유지하면서도 점포당 평균매출이 높은 쪽에 가까웠다.
11. 계약종료·해지율과 순증감률로 안정성도 봤다
브랜드가 크고 매출이 높아도, 계약종료·해지가 많다면 안정적인 브랜드라고 보기 어렵다. 그래서 계약종료·해지율과 순증감률을 함께 봤다.
발표 자료에서는 엽떡의 2025년 계약종료·해지율이 0.15%로 비교 브랜드 중 가장 낮은 수준이고, 순증감률은 +8.92%로 신규 가맹점 증가가 계약종료·해지를 상회한다고 정리했다.
계약종료·해지율 낮음
→ 가맹점 변동성이 낮음
순증감률 양수
→ 신규 증가가 이탈보다 많음
둘을 함께 보면
→ 안정적 확장 흐름
다만 이 지표도 실제 폐업률이라고 단정하면 안 된다. 발표 자료에서도 계약종료·해지율은 실제 폐업률이 아니라 공정위 가맹계약 기준의 변동성 지표라고 주석을 달았다. 이 부분을 넣은 건 좋았다. 지표가 의미하는 범위를 정확히 제한해야 EDA가 과장되지 않기 때문이다.
12. 발표 후 남은 점
이번 발표에서 확인한 것은 네 가지였다. CPI 기준 상대가격 부담은 낮아졌고, 평균매출과 가맹점 수 기준으로 엽떡은 대형 고매출형 브랜드에 가까웠다. 또 계약종료·해지율이 낮고 순증감률이 양수라는 점에서 가맹점 변동성도 비교적 낮게 보였다.
다만 가격 유지가 곧 성장의 원인이라고 단정할 수는 없었다. 발표 자료에서도 원가율, 광고비, 소비자 가격 인식 데이터 등을 추가해 성장 원인 후보를 검증해보고 싶다고 정리했다. 또한 직접 데이터를 수집하고 시각화하면서 데이터 분석의 중요성을 확인했고, 더 복잡한 데이터 핸들링과 통계적 분석, Tableau 시각화와 대시보드 구성도 더 공부하고 싶다고 마무리했다.
발표를 하면서 느낀 건, EDA는 결국 “내가 궁금한 질문을 데이터로 어디까지 확인할 수 있는가”를 보여주는 작업이라는 점이었다. 이번 발표는 인과관계를 증명한 분석은 아니지만, 가격 부담 변화와 브랜드 지표가 함께 움직이는 단서를 정리했다는 점에서 의미가 있었다.
마무리
27일차는 오전에는 Optuna를 활용한 하이퍼파라미터 탐색, 오후에는 개인 EDA 발표로 이어진 날이었다. Optuna에서는 직접 목적 함수를 만들고, trial.suggest_int, trial.suggest_categorical로 파라미터 범위를 지정한 뒤, cross_val_score의 평균 accuracy를 최적화 대상으로 삼았다. 이 과정에서 하이퍼파라미터 튜닝은 정답을 찾는 버튼이 아니라, 더 나은 실험 방향을 잡기 위한 도구라는 느낌이 남았다.
오후 발표는 CPI와 프랜차이즈 데이터를 활용해 엽떡의 가격 부담과 시장 위치를 탐색했다. 14,000원이라는 가격이 절대적으로 싸다는 결론이 아니라, CPI 기준으로 재표현했을 때 상대가격 부담이 낮아졌고, 동시에 엽떡이 평균매출·가맹점 수·계약종료해지율 측면에서 강한 포지션을 보인다는 흐름이었다.
발표를 준비하면서 데이터 수집, 전처리, 파생변수 생성, 시각화, 해석 범위 설정이 모두 중요하다는 걸 다시 느꼈다. 특히 “가격 유지가 성장의 원인이다”라고 단정하지 않고, 현재 데이터로 확인 가능한 패턴과 확인하지 못한 한계를 구분하는 것이 EDA 발표에서 가장 조심해야 할 부분이었다.
27일차는 Optuna로 모델 튜닝의 방향을 잡는 방법을 배우고, 직접 준비한 EDA 발표를 통해 데이터로 질문을 만들고 한계를 정리하는 과정을 경험한 날이었다.
'[SK플래닛] ASAC 빅데이터전문가 11기 > 학습기록' 카테고리의 다른 글
| [SK플래닛] ASAC 빅데이터전문가 11기 | 29일차 (0) | 2026.05.29 |
|---|---|
| [SK플래닛] ASAC 빅데이터전문가 11기 | 28일차 (0) | 2026.05.28 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 26일차 (0) | 2026.05.26 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 25일차 (1) | 2026.05.22 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 24일차 (0) | 2026.05.21 |
