워싱턴 D.C.의 자전거 공유 서비스 운영팀은
시간대·계절·날씨에 따라 달라지는 자전거 수요를 미리 파악해 운영 계획을 세우려고 한다.
여러분에게는 2011년과 2012년의 시간별 자전거 대여량과 기상·계절 정보가 주어진다.
여러분은 데이터 분석가로서 다음 질문에 답해야 한다.
2011년 데이터로 학습한 머신러닝 모델은
2012년의 시간당 자전거 대여량을 얼마나 잘 예측할 수 있는가?
이 프로젝트의 목표는 가장 높은 점수를 만드는 것이 아니다.
데이터를 이해하고 → 분석을 설계하고 → 모델을 비교하고 →
새로운 데이터에서 평가하고 → 결과의 의미와 한계를 판단하는 것
이 핵심이다.
3.1프로젝트에서 만들어야 할 것¶
프로젝트가 끝나면 다음 네 가지가 남아야 한다.
분석 질문과 설계
무엇을 예측하는가?
어떤 변수를 사용하고 어떤 변수는 제외하는가?
왜 2011년과 2012년을 다르게 사용하는가?
데이터 탐색 결과
자전거 수요가 언제, 어떤 조건에서 달라지는가?
모델링 전에 알아야 할 데이터의 특징은 무엇인가?
모델 비교와 최종 평가
단순한 모델과 더 유연한 모델을 비교한다.
모델 선택과 최종 평가를 구분한다.
최종 판단
어떤 모델을 선택할 것인가?
어디까지 믿을 수 있는가?
실제 운영에서는 어떻게 활용할 수 있는가?
어떤 한계가 남아 있는가?
3.21. 문제를 데이터의 언어로 바꾼다¶
먼저 코드를 실행하지 말고 다음을 확인한다.
샘플: 한 시간의 관측값
타깃: 시간당 전체 자전거 대여량
cnt특성 후보: 시간, 계절, 월, 요일, 휴일, 근무일, 날씨, 온도, 체감온도, 습도, 풍속
문제 유형: 타깃이 수치이므로 회귀
과거 데이터: 2011년
미래 데이터: 2012년
이번 프로젝트는 데이터를 무작위로 섞어 나누기보다
과거의 자료로 학습하고 미래의 자료에서 확인하는 상황
을 그대로 사용한다.
3.32. 데이터를 불러와 구조를 확인한다¶
아래 셀은 프로젝트 진행을 위한 제공 코드다.
코드를 외우는 것이 목적은 아니다.
실행한 뒤 데이터의 행과 열이 무엇을 의미하는지 확인한다.
Source
# UCI Bike Sharing 데이터 불러오기
# 처음 실행할 때 인터넷 연결이 필요하다.
try:
from ucimlrepo import fetch_ucirepo
except ImportError:
!pip -q install ucimlrepo
from ucimlrepo import fetch_ucirepo
bike = fetch_ucirepo(id=275)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
parts = []
if bike.data.features is not None:
parts.append(bike.data.features.copy())
if bike.data.targets is not None:
parts.append(bike.data.targets.copy())
if getattr(bike.data, "ids", None) is not None:
parts.append(bike.data.ids.copy())
df = pd.concat(parts, axis=1)
df = df.loc[:, ~df.columns.duplicated()]
print("데이터 크기:", df.shape)
df.head()df.info()df.isna().sum().sort_values(ascending=False).head(20)3.3.1데이터에서 확인할 것¶
다음 질문에 짧게 답한다.
전체 샘플 수와 변수 수는 얼마인가?
결측치는 있는가?
숫자로 저장되어 있지만 실제로는 범주를 나타내는 변수는 무엇인가?
날짜와 시간은 어떤 변수에 들어 있는가?
cnt이외에 대여량과 직접 관련된 열은 무엇인가?
관찰 기록
3.43. 높은 성능보다 먼저 누수를 막는다¶
원자료에는 세 개의 대여량 열이 있다.
casual: 비등록 사용자의 대여 건수registered: 등록 사용자의 대여 건수cnt: 전체 대여 건수
그리고 다음 관계가 성립한다.
cnt = casual + registered
따라서 cnt를 예측하면서 casual과 registered를 입력에 사용하면
모델에게 사실상 정답을 알려주는 셈이다.
이것이 **데이터 누수(data leakage)**다.
또한 다음 변수도 기본 모델의 입력에서는 제외한다.
instant: 단순한 행 번호dteday: 날짜 문자열 자체yr: 2011년과 2012년을 나누는 용도로만 사용
성능이 높다고 좋은 분석인 것은 아니다.
어떤 정보를 모델에게 주었는지를 먼저 확인해야 한다.
df[[c for c in ["casual", "registered", "cnt"] if c in df.columns]].head()3.4.1프로젝트 기록 ② — 특성 결정¶
사용할 특성 후보
제외할 변수와 이유
| 변수 | 제외하는 이유 |
|---|---|
instant | |
dteday | |
casual | |
registered | |
yr |
이 표를 채운 뒤 모델링으로 넘어간다.
3.54. 2012년은 지금부터 최종 평가 데이터로 보존한다¶
yr는 다음을 뜻한다.
0→ 2011년1→ 2012년
이 프로젝트에서는
2011년: 탐색·학습·모델 선택
2012년: 최종 평가
로 역할을 나눈다.
2012년의 정답을 계속 확인하면서 모델을 바꾸면
2012년은 더 이상 완전히 새로운 평가 데이터라고 보기 어렵다.
따라서 먼저 2011년과 2012년을 분리한다.
df_2011 = df.loc[df["yr"] == 0].copy()
df_2012 = df.loc[df["yr"] == 1].copy()
print("2011:", df_2011.shape)
print("2012:", df_2012.shape)3.65. 모델보다 먼저 2011년 데이터를 탐색한다¶
이제부터의 EDA는 2011년 데이터만 사용한다.
먼저 타깃 cnt의 분포를 본다.
질문: 대부분의 시간대에서 대여량은 어느 정도이며, 매우 높은 수요도 자주 나타나는가?
df_2011["cnt"].hist(bins=30)
plt.xlabel("Hourly bike rentals")
plt.ylabel("Frequency")
plt.title("Distribution of hourly bike rentals: 2011")
plt.show()hourly = df_2011.groupby("hr")["cnt"].mean()
hourly.plot(marker="o")
plt.xlabel("Hour")
plt.ylabel("Mean rentals")
plt.title("Mean bike rentals by hour: 2011")
plt.show()3.6.2평일과 비근무일의 시간대별 모습은 같을까?¶
전체 평균만 보면 서로 다른 이용 목적이 섞여 있을 수 있다.
출퇴근 수요가 존재한다면 workingday에 따라 시간대별 모습이 달라질 수 있다.
hour_work = (
df_2011.groupby(["hr", "workingday"])["cnt"]
.mean()
.unstack()
)
hour_work.columns = ["Non-working day", "Working day"]
hour_work.plot()
plt.xlabel("Hour")
plt.ylabel("Mean rentals")
plt.title("Hourly rentals by working-day status: 2011")
plt.show()3.6.3학생이 하나의 질문을 더 만든다¶
아래 변수 중 하나 이상을 이용해 자신의 EDA 질문을 만든다.
seasonmnthweathersittempatemphumwindspeed
예:
“날씨 상태에 따라 평균 대여량은 얼마나 다른가?”
“온도와 자전거 대여량 사이에는 어떤 관계가 보이는가?”
아래 셀에 필요한 분석 코드를 작성한다.
AI에게 코드를 제안받아도 되지만, 왜 이 그래프나 요약이 필요한지는 학생이 설명해야 한다.
# TODO: 자신이 정한 EDA 질문을 확인하는 코드 작성3.6.4프로젝트 기록 ③ — EDA에서 발견한 것¶
다음 형식으로 세 가지를 기록한다.
관찰:
모델링과 연결:
관찰:
모델링과 연결:
관찰:
모델링과 연결:
단순히 “그래프가 증가한다”라고 적는 데서 끝내지 않는다.
그래서 어떤 변수가 예측에 유용할 것 같은가?
모델이 어떤 관계를 표현해야 할 것 같은가?
까지 연결한다.
3.76. 특성과 타깃을 확정한다¶
이제 앞에서 정한 원칙에 따라 입력 특성과 타깃을 만든다.
숫자로 저장되어 있다고 해서 모두 연속적인 양을 뜻하는 것은 아니다.
예를 들어
hr=23이hr=1보다 23배 큰 시간이라는 뜻은 아니다.season=4가season=2보다 두 배 큰 계절이라는 뜻도 아니다.
따라서 다음 변수는 기본 분석에서 범주형 변수로 다룬다.
season, mnth, hr, holiday, weekday, workingday, weathersit
target = "cnt"
drop_cols = [
c for c in ["instant", "dteday", "casual", "registered", "yr", target]
if c in df.columns
]
feature_cols = [c for c in df.columns if c not in drop_cols]
categorical_cols = [
c for c in [
"season", "mnth", "hr", "holiday",
"weekday", "workingday", "weathersit"
]
if c in feature_cols
]
numeric_cols = [
c for c in feature_cols
if c not in categorical_cols
]
print("사용 특성:", feature_cols)
print("범주형:", categorical_cols)
print("수치형:", numeric_cols)3.87. 모델 선택과 최종 평가는 분리한다¶
여기서 중요한 문제가 하나 있다.
2012년은 최종 평가에 남겨 두었다.
그렇다면 선형회귀와 Random Forest 중 어떤 모델을 고를 때 무엇을 사용해야 할까?
2012년을 보면서 고르면 안 된다.
그래서 2011년을 시간 순서대로 다시 나누어
앞부분 → 모델 학습
뒷부분 → 모델 선택용 검증
에 사용한다.
모델을 선택한 뒤에는 2011년 전체로 다시 학습하고,
2012년을 한 번만 최종 평가한다.
학습 → 검증 → 최종 테스트
의 역할을 구분하는 연습이다.
Source
# 2011년 자료를 시간 순서대로 정렬한다.
df_2011_ordered = df_2011.sort_values(["dteday", "hr"]).copy()
split_idx = int(len(df_2011_ordered) * 0.8)
dev_df = df_2011_ordered.iloc[:split_idx].copy()
val_df = df_2011_ordered.iloc[split_idx:].copy()
X_dev = dev_df[feature_cols]
y_dev = dev_df[target]
X_val = val_df[feature_cols]
y_val = val_df[target]
print("모델 학습용:", X_dev.shape)
print("모델 선택용 검증:", X_val.shape)3.98. 전처리는 모델과 함께 묶는다¶
기본 전처리는 다음과 같다.
수치형 변수 → 결측치 처리 + 표준화
범주형 변수 → 결측치 처리 + 원-핫 인코딩
현재 UCI 자료에는 결측치가 없더라도
일관된 분석 절차를 만들기 위해 전처리를 명시한다.
중요한 것은 클래스 이름을 외우는 것이 아니다.
어떤 종류의 데이터에 어떤 처리가 왜 필요한가?
를 설명할 수 있어야 한다.
Source
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
numeric_pipe = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
categorical_pipe = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore"))
])
preprocess = ColumnTransformer([
("num", numeric_pipe, numeric_cols),
("cat", categorical_pipe, categorical_cols)
])3.109. 단순한 모델과 더 유연한 모델을 비교한다¶
두 모델을 사용한다.
3.10.1선형회귀¶
단순하고 해석하기 쉬운 기준 모델이다.
3.10.2Random Forest¶
선형회귀보다 더 복잡한 관계를 표현할 수 있다.
이 프로젝트에서는 Random Forest의 내부 원리를 깊게 다루지 않는다.
지금은 다음 질문에 집중한다.
더 복잡한 모델이 실제로 새로운 시점에서도 더 잘 예측하는가?
Source
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
linear_model = Pipeline([
("prep", preprocess),
("model", LinearRegression())
])
rf_model = Pipeline([
("prep", preprocess),
("model", RandomForestRegressor(
n_estimators=200,
random_state=42,
n_jobs=-1
))
])3.1110. 무엇으로 모델을 비교할까?¶
세 가지 지표를 함께 본다.
MAE: 예측이 실제 대여량에서 평균적으로 몇 대 정도 벗어나는가?
RMSE: 큰 오차에 더 큰 벌점을 주었을 때 오차는 어느 정도인가?
R²: 대여량의 변동을 모델이 어느 정도 설명하는가?
공식을 외우기보다
이 숫자가 운영자의 언어로 무엇을 의미하는가?
를 설명하는 것이 중요하다.
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
def regression_scores(model, X, y):
pred = model.predict(X)
return {
"MAE": mean_absolute_error(y, pred),
"RMSE": np.sqrt(mean_squared_error(y, pred)),
"R2": r2_score(y, pred)
}
models = {
"LinearRegression": linear_model,
"RandomForest": rf_model
}
validation_rows = []
for name, model in models.items():
model.fit(X_dev, y_dev)
train_scores = regression_scores(model, X_dev, y_dev)
val_scores = regression_scores(model, X_val, y_val)
validation_rows.append({
"Model": name,
"Train MAE": train_scores["MAE"],
"Validation MAE": val_scores["MAE"],
"Train RMSE": train_scores["RMSE"],
"Validation RMSE": val_scores["RMSE"],
"Train R2": train_scores["R2"],
"Validation R2": val_scores["R2"],
})
validation_results = pd.DataFrame(validation_rows).set_index("Model")
validation_results3.11.1프로젝트 기록 ④ — 모델 선택¶
검증 결과를 보고 답한다.
1. 검증 RMSE가 더 낮은 모델은 무엇인가?
2. 훈련 성능과 검증 성능의 차이는 어느 모델에서 더 큰가?
3. 단순히 훈련 성능이 가장 좋은 모델을 고르면 안 되는 이유는 무엇인가?
4. 최종적으로 어떤 모델을 선택할 것인가?
5. 그 선택의 근거를 3~5문장으로 설명하라.
3.1211. 모델을 확정한 뒤 2012년을 처음 평가한다¶
이제 모델 선택이 끝났다.
선택한 모델을 2011년 전체 데이터로 다시 학습한 다음
2012년에서 최종 성능을 확인한다.
아래 셀의 chosen_model_name을 자신이 선택한 모델로 수정한다.
가능한 값:
"LinearRegression""RandomForest"
from sklearn.base import clone
chosen_model_name = "RandomForest" # TODO: 자신의 선택에 맞게 수정
X_2011 = df_2011[feature_cols]
y_2011 = df_2011[target]
X_2012 = df_2012[feature_cols]
y_2012 = df_2012[target]
final_model = clone(models[chosen_model_name])
final_model.fit(X_2011, y_2011)
train_2011_scores = regression_scores(final_model, X_2011, y_2011)
test_2012_scores = regression_scores(final_model, X_2012, y_2012)
pd.DataFrame({
"2011 train": train_2011_scores,
"2012 final test": test_2012_scores
})3.12.12012년 성능을 현실의 언어로 읽는다¶
숫자만 옮겨 적지 않는다.
예를 들어 MAE가 45라면 다음과 같이 해석할 수 있다.
“2012년의 시간당 대여량을 예측할 때 실제 대여량과 평균적으로 약 45대 정도 차이가 났다.”
자신의 결과를 같은 방식으로 해석한다.
MAE 해석
RMSE 해석
R² 해석
2011년과 2012년 성능 차이에 대한 해석
pred_2012 = final_model.predict(X_2012)
plt.scatter(y_2012, pred_2012, alpha=0.15)
plt.xlabel("Actual rentals in 2012")
plt.ylabel("Predicted rentals")
plt.title("Actual vs predicted bike rentals: 2012")
plt.show()3.13.1언제 많이 틀리는가?¶
예측 결과도 다시 하나의 데이터로 만들 수 있다.
전체 평균 오차만 보지 말고 시간대별 오차를 확인한다.
운영팀의 입장에서는 출퇴근 시간에 크게 틀리는 모델과
새벽 시간에 크게 틀리는 모델의 의미가 다를 수 있다.
errors_2012 = pd.DataFrame({
"hr": X_2012["hr"].to_numpy(),
"actual": y_2012.to_numpy(),
"predicted": pred_2012
})
errors_2012["error"] = (
errors_2012["actual"] - errors_2012["predicted"]
)
errors_2012["abs_error"] = errors_2012["error"].abs()
hour_error = errors_2012.groupby("hr")["abs_error"].mean()
hour_error.plot(marker="o")
plt.xlabel("Hour")
plt.ylabel("Mean absolute error")
plt.title("Prediction error by hour: 2012")
plt.show()3.13.2프로젝트 기록 ⑤ — 오류 분석¶
그래프를 보고 답한다.
오차가 큰 시간대는 언제인가?
그 시간대에서 수요 자체가 높은가?
운영자가 전체 RMSE만 보았다면 놓칠 수 있었던 사실은 무엇인가?
추가로 어떤 집단이나 조건별 오류를 확인해 보고 싶은가?
df.groupby("yr")["cnt"].agg(["mean", "median", "std"])3.14.1월별 수요도 비교한다¶
monthly = (
df.groupby(["yr", "mnth"])["cnt"]
.mean()
.unstack(0)
)
monthly.columns = ["2011", "2012"]
monthly.plot(marker="o")
plt.xlabel("Month")
plt.ylabel("Mean hourly rentals")
plt.title("Monthly bike demand: 2011 vs 2012")
plt.show()두 해의 수요 수준이나 월별 모습이 다르다면
2011년에 학습한 관계가 2012년에서 완전히 그대로 유지되지 않을 수 있다.
이것은 단순히 “모델이 나쁘다”라는 말보다 더 중요한 해석이다.
미래의 데이터가 과거와 달라지면
모델의 일반화 성능도 달라질 수 있다.
따라서 2012년에서 성능이 낮아졌다면
모델이 충분히 유연하지 않았는가?
2011년에 지나치게 맞춰졌는가?
2012년의 수요 수준이나 관계가 달라졌는가?
중요한 변수가 데이터에 빠져 있는가?
를 함께 생각해야 한다.
3.1514. 예측 관계와 인과관계를 구분한다¶
모델이 hr, temp, workingday 같은 정보를 유용하게 사용했다고 해도
다음 두 문장은 같은 뜻이 아니다.
“온도 정보는 자전거 대여량 예측에 유용했다.”
“온도가 높아지면 자전거 대여가 증가한다.”
첫 번째는 예측 관계에 대한 문장이다.
두 번째는 인과관계를 주장할 수 있다.
관찰 데이터와 예측 모델만으로는
인과관계를 쉽게 결론 내릴 수 없다.
프로젝트 보고서에서는
모델이 예측에 어떤 정보를 활용했는가
와
현실에서 무엇이 원인인가
를 구분한다.
3.1615. AI에게 맡길 수 있는 것과 학생이 판단할 것¶
AI는 다음을 잘할 수 있다.
EDA 코드 제안
전처리 코드 작성
모델 코드 작성
성능 표 정리
그래프 작성
결과 설명 초안 작성
하지만 다음 판단은 그대로 맡기면 안 된다.
casual,registered를 사용해도 되는가?2011년과 2012년을 무작위로 섞어도 되는가?
어떤 지표를 중요하게 볼 것인가?
검증 데이터와 최종 테스트 데이터의 역할은 무엇인가?
높은 성능을 믿어도 되는가?
결과를 인과관계로 말할 수 있는가?
실제 운영에 사용할 만한 모델인가?
AI의 코드를 검토하는 것보다
AI가 제안한 분석의 논리를 검토하는 것이 더 중요하다.
3.16.1AI 검토 활동¶
현재까지의 결과를 AI에게 제공하고 다음을 요청한다.
“이 분석 결과를 바탕으로 워싱턴 D.C. 자전거 공유 서비스 운영자에게
5~7문장의 결론을 작성해줘.”
AI가 작성한 결론에서 다음과 같은 문제가 있는지 찾는다.
2013년 이후에도 같은 성능을 보장한다고 말한다.
높은 상관이나 모델의 활용을 인과관계로 해석한다.
가장 복잡한 모델이 반드시 최고라고 주장한다.
2012년 한 번의 평가로 일반화가 완전히 검증되었다고 말한다.
전체 성능만 보고 시간대별 실패를 무시한다.
AI의 문장 중 수정한 문장
왜 수정했는가?
수정한 문장
3.1716. 최종 결론을 작성한다¶
좋은 결론은 성능 숫자를 나열하는 것으로 끝나지 않는다.
다음 순서로 작성한다.
3.17.1분석 질문에 대한 직접적인 답¶
2011년 데이터로 학습한 모델은 2012년의 자전거 수요를 __________ 정도로 예측했다.
3.17.2선택한 모델과 근거¶
3.17.3핵심 성능¶
3.17.4실제 운영에서의 활용 가능성¶
3.17.5모델이 특히 주의해야 할 상황¶
3.17.6분석의 한계 두 가지 이상¶
3.17.7다음 분석에서 추가하고 싶은 것¶
3.18프로젝트 최종 제출물¶
제출물은 실행 가능한 노트북 + 2쪽 이내의 요약 보고서로 한다.
보고서에는 다음 내용이 반드시 포함되어야 한다.
3.18.11. 문제와 데이터¶
분석 목적
타깃
사용한 특성
제외한 변수와 이유
3.18.22. EDA¶
핵심 발견 2~3개
각 발견이 모델링과 어떻게 연결되는지 설명
핵심 그래프 최대 2개
3.18.33. 분석 설계¶
2011년과 2012년의 역할
모델 선택용 검증과 최종 평가의 구분
전처리 방법
3.18.44. 모델 비교¶
최소 2개 모델
검증 결과
선택한 모델과 이유
3.18.55. 최종 평가와 오류 분석¶
2012년 MAE, RMSE, R²
2011년과 2012년 성능 차이
한 가지 이상의 조건별 오류 분석
3.18.66. 판단과 한계¶
분석 질문에 대한 최종 답
실제 활용 가능성
한계 최소 2개
AI가 제안했지만 수정하거나 받아들이지 않은 내용 최소 1개
3.19평가 기준¶
| 영역 | 배점 | 확인할 내용 |
|---|---|---|
| 문제와 데이터 이해 | 4 | 타깃·특성·누수·분석 설계를 적절히 설명했는가? |
| 분석 방법 | 4 | EDA·전처리·모델 비교·평가 절차가 타당한가? |
| 결과 해석 | 5 | 지표와 그래프를 현실의 언어로 정확히 해석했는가? |
| 판단 | 4 | 모델 선택과 최종 결론에 근거가 있는가? |
| 검증과 한계 | 3 | AI 결과 검토, 일반화, 한계를 적절히 다루었는가? |
| 합계 | 20 |
코드의 길이나 화려함 자체는 평가의 중심이 아니다.
무엇을 했는지보다 왜 그렇게 했는지를 설명할 수 있어야 한다.
3.20개인 확인 문제¶
프로젝트를 마친 뒤 다음 질문에 코드를 보지 않고 답할 수 있어야 한다.
왜
cnt를 예측할 때casual,registered를 사용할 수 없는가?왜 2011년과 2012년을 무작위로 섞지 않았는가?
2012년을 모델 선택에 반복해서 사용하면 왜 문제가 되는가?
숫자로 저장된
hr를 범주형 변수로 처리한 이유는 무엇인가?MAE와 RMSE는 각각 무엇을 알려주는가?
훈련 성능이 매우 좋고 검증 성능이 낮다면 무엇을 의심해야 하는가?
전체 RMSE가 좋아도 시간대별 오류를 확인해야 하는 이유는 무엇인가?
어떤 특성이 예측에 중요하다고 해서 그 변수가 원인이라고 말할 수 없는 이유는 무엇인가?
2011년과 2012년의 수요 수준이 다르다면 일반화에 어떤 문제가 생길 수 있는가?
이 프로젝트에서 AI가 대신하기 어려운 가장 중요한 판단은 무엇인가?
3.21프로젝트를 마치며¶
이 프로젝트에서 배워야 할 핵심은
선형회귀나 Random Forest의 사용법 자체가 아니다.
처음에는 다음 질문 하나로 시작했다.
2011년 데이터로 2012년의 자전거 수요를 예측할 수 있을까?
그 질문에 답하기 위해 우리는
문제 정의 → 데이터 이해 → 누수 점검 → 2011년 EDA →
모델 학습과 검증 → 모델 선택 → 2012년 최종 평가 →
오류 분석 → 해석과 판단
의 과정을 거쳤다.
다음 프로젝트에서도 이 흐름은 반복된다.
달라지는 것은 데이터와 모델이 아니라
어떤 질문을 하고, 어떤 결과를 믿을 것인가를 판단하는 상황이다.