데이터 전처리 및 시각화

기계학습, 파이프라인 저장과 streamlit 입력폼 생성

myun0506 2026. 1. 23. 00:55

[ 전처리 / 시각화 세션 5-1 ]

- 데이터: https://www.kaggle.com/datasets/joniarroba/noshowappointments?resource=download

- 결측치 비율 파악

print("\n--- missing rate(top) ---")
missing = df.isna().mean().sort_values(ascending=False)
print(missing.head(10))

- 컬럼 전처리

# 1) 컬럼명 정리: 소문자/공백 제거/하이픈 -> 언더바
df2.columns = (
    df2.columns
      .str.strip()
      .str.lower()
      .str.replace("-", "_", regex=False)
      .str.replace(" ", "_", regex=False)
)
df2.columns

# 2) 날짜(datetime) 변환
df2["scheduledday"] = pd.to_datetime(df2["scheduledday"], errors="coerce", utc=True)
df2["appointmentday"] = pd.to_datetime(df2["appointmentday"], errors="coerce", utc=True)

# 3) 타겟 변환: no_show (Yes=1, No=0)
df2["no_show"] = df2["no_show"].map({"Yes": 1, "No": 0})
위 코드를 실수로 두번 실행하게되면 이미 1,0으로 바뀐 값들을 다시 map을 실행하는 것이므로 1,0 값이 NaN으로 변함

 

# 날짜 기준 wait_days (0일 포함) 2016-04-29 18:38:08+00:00 → 2016-04-29 18:38:08
scheduled_date = df2["scheduledday"].dt.tz_convert(None).dt.normalize()
appointment_date = df2["appointmentday"].dt.tz_convert(None).dt.normalize()
df2["wait_days"] = (appointment_date - scheduled_date).dt.days
  • .dt.tz_convert(None)
    • 서로 다른 시간대 기준을 통일하거나, 시간대 정보가 불필요한 연산에서 오버헤드를 줄임
  • .dt.normalize()
    • 시, 분, 초 데이터를 00:00:00으로 초기화하여 오직 '날짜' 정보만 남김
    • 만약 normalize()를 하지 않는다면
      • 당일 예약 시 음수값이 발생하는 논리적 오류 발생
    • dt.date를 사용해 Object 타입이 되었는데 그 이후 다시 datetime64로 바꾸려면 문자열 파싱이나 객체 순회 과정을 거쳐야 하는데, 이는 대용량 데이터에서 상당한 컴퓨팅 자원을 소모함
    • → normalize()로 시간을 0으로 밀어버리더라도 datetime64라는 규격(dtype)을 유지하는 것이 CPU 연산과 메모리 참조 효율성 면에서 압도적으로 유리함
구분 dt.normalize() dt.date
결과 타입 datetime64[ns] (Pandas 고유 타입) object (Python date 객체)
시간 정보 00:00:00 으로 남겨둠  완전히 제거됨
연산 편의성  .dt 접근자를 계속 사용 가능함  .dt 접근자 사용 불가 (추가 변환 필요)
시각화  대부분의 라이브러리에서 시계열로 인식 일반 객체나 문자열로 인식될 수 있음
메모리 저장 방식 Pandas의 datetime64 타입은 내부적으로 64비트 정수 형태로 메모리에 연속적으로 저장 Python 객체로 변환되면 각 행이 Python의 Object 타입을 참조하게 되어 메모리 오버헤드가 발생하고 연산 속도가 수십 배 느려짐
  • 호환성 측면에서 Timestamp가 유리한 이유
    • 표준화된 규격이기 때문!
    • 시각화 측면 (Matplotlib, Seaborn)
      • 시계열 축(x축)을 그릴 때 datetime64는 자동으로 '월, 일, 시간' 단위로 축을 최적화하여 보여줌
      • but Object(date) 타입은 이를 단순한 문자열(String)로 인식하여 축이 겹치거나 정렬이 꼬이는 문제가 발생함
    • 머신러닝 (Scikit-learn)
      • 모델은 숫자형 데이터를 학습함
      • datetime64는 내부적으로 정수형이기 때문에 wait_days와 같은 수치 데이터로 변환하기 매우 용이하지만,
      • Python 객체는 모델이 직접 읽을 수 없음
  •  A-B
    • timedelta 객체를 생성하여 두 시점 사이의 물리적 간격을 계산
  • .dt.days
    • timedelta에서 '일(day)' 단위의 정수값만 추출하여 수치형 변수로 변환

  • wait_days 열이 0보다 작은 것은 정상이 아니므로 데이터 확인 후 제거

  • 이상치 제거 후 wait_days 열 확인

- 노쇼 비율 막대그래프로 시각화

 

- 두 집단(no_show vs show) 간의 대기 기간 분포 차이

 

 

- 나이 구간별로 age 열 나누기

 

- 요일별 노쇼율

# 다음 단계 3) 시각화 #3 — 요일별 노쇼율 (표 + 막대그래프)
# 아래 셀 실행하고, 출력 표에서 상위 1개 요일 / 하위 1개 요일만 보내주세요.
df2["appt_dow"] = df2["appointmentday"].dt.day_name()
dow_rate = df2.groupby("appt_dow")["no_show"].mean().sort_values(ascending=False)
print(dow_rate.round(4))
plt.figure(figsize=(8,4))
dow_rate.plot(kind="bar")
plt.title("No-show Rate by Appointment Day of Week")
plt.ylabel("No-show rate")
plt.show()

- 오타 있던 컬럼명 수정

- EDA 요약표 생성

  • agg(n="size", no_show_rate="mean")
    • 그룹화된 데이터에 대해 두 가지 연산을 수행함과 동시에
    • 그 결과값들이 담길 열 이름을 지정하는 역할
  • min_n=200
    • 표본 수가 적을 때 발생하는 '소수의 법칙'에 의한 왜곡을 방지하기 위해
    • 표본이 충분할 때 (n>=200) 값을 리턴하도록 조건 추가
# 다음 셀: EDA 요약표 자동 생성
# 나이 구간(없으면 생성)
if "age_group" not in df2.columns:
    bins = [-1, 0, 12, 18, 30, 45, 60, 75, 110, 200]
    labels = ["0", "1-12", "13-18", "19-30", "31-45", "46-60", "61-75", "76+", "110+"]
    df2["age_group"] = pd.cut(df2["age"], bins=bins, labels=labels)
def rate_table(col, min_n=200):
    tmp = df2.groupby(col)["no_show"].agg(n="size", no_show_rate="mean").reset_index()
    tmp["no_show_rate(%)"] = (tmp["no_show_rate"] * 100).round(2)
    tmp = tmp.sort_values("no_show_rate(%)", ascending=False)
    return tmp[tmp["n"] >= min_n]
cols = ["gender", "sms_received", "scholarship", "hypertension", "diabetes", "alcoholism", "handcap", "age_group"]
for c in cols:
    print(f"\n=== {c} ===")
    display(rate_table(c, min_n=200))

- 타입 정리 (0/1 컬럼을 "정수/범주형"으로)

  • 0/1로 되어있던 나머지 컬럼은 모두 정수형으로 타입 변환
  • F/M로 되어있던 'gender' 컬럼은 이를 범주형으로 타입 변환 (.astype("category"))

 

 

- 나이구간별(행) x 고혈압(열) 노쇼율 피벗

 

- 상관관계 히트맵 + "중복(높은 상관)" 컬럼 후보 제거

  • df2.select_dtypes(include["int64", "float64"])
    • 특정 데이터 타입을 가진 열들만 필터링하여 새로운 데이터 프레임을 반환
    • 목적
      • 상관계수 계산은 '숫자'간의 연산이므로, 문자열(object)이나 범주형(category) 데이터가 섞여있으면 오류가 나거나 계산이 불가능함
      • 이를 사전에 방지하기 위해 숫자형만 골라내는 과정임
    • 자주 쓰는 옵션
      • include="number" 를 쓰면 모든 숫자형(int, float)을 한 번에 가져올 수 있음
  • num_df.corr(numeric_only=True)
    • 열 간의 피어슨 상관계수를 계산하여 행렬 형태로 반환함
    • numeric_only=True
      • 데이터프레임 내에 숫자가 아닌 데이터가 섞여있을 경우, 
      • 이를 자동으로 제외하고 숫자 데이터로만 상관 행렬을 만들겠다는 명시적 설정
  • heatmap()
    • corr
      • 계산된 상관행렬 데이터
    • annot=True
      •  각 셀 안에 실제 상관계수 수치를 표시할지 여부
    • fmt=".2f"
      • 수치 표시 형식 (소수점 둘째 자리까지 표시)
    • cmap="coolwarm"
      • 색상 테마 (양수는 빨간색(warm), 음수는 파란색(cool)으로 표현)
    • square=True
      • 각 셀의 모양을 정사각형으로 고정하여 가독성 향상
# A. 상관관계 히트맵 + “중복(높은 상관)” 컬럼 후보 제거
# A-1) 숫자형 컬럼만 뽑고 상관행렬 확인
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

num_df = df2.select_dtypes(include=["int64", "float64"]).copy()

# 타겟 포함한 숫자형 컬럼 목록 확인
print("numeric columns:", num_df.columns.tolist())
corr = num_df.corr(numeric_only=True)
plt.figure(figsize=(10, 7))
sns.heatmap(corr, annot=True, fmt=".2f", cmap="coolwarm", square=True)
plt.title("Correlation Heatmap (Numeric Features)")
plt.show()

 

- 기계학습

 

- Train/Test 분리

 

- 인코딩 + 정규화 + 로지스틱 회귀 모델 학습 / 평가

# 다음 단계: Step 3 (인코딩 + 정규화 + 로지스틱 회귀 모델 학습/평가)
# 아래 셀을 그대로 실행하고, 출력(특히 confusion_matrix, classification_report)을 캡처해서 올려주세요.

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, classification_report

# 숫자/범주 컬럼 분리
num_cols = X_train.select_dtypes(include=["int64","float64","int32","float32"]).columns.tolist()
cat_cols = [c for c in X_train.columns if c not in num_cols]
print("num_cols:", num_cols)
print("cat_cols:", cat_cols)
preprocess = ColumnTransformer(
    transformers=[
        ("num", StandardScaler(), num_cols),
        ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols)
    ]
)

model = Pipeline(steps=[
    ("prep", preprocess),
    ("clf", LogisticRegression(max_iter=1000))
])

model.fit(X_train, y_train)
pred = model.predict(X_test)
print("confusion_matrix:\n", confusion_matrix(y_test, pred))
print("\nclassification_report:\n", classification_report(y_test, pred, digits=4))

 

- 가상 데이터로 예측하기

 

- streamlit 입력폼 제작

import joblib, json
import pandas as pd
from pandas.api.types import is_numeric_dtype
# 1) 파이프라인 저장 (전처리+모델 통째로)
joblib.dump(model_bal, "./data/no_show_pipeline.joblib")
# 2) Streamlit 입력폼을 쉽게 만들기 위한 메타 저장
feature_columns = X_train.columns.tolist()
schema = {}
defaults = {}
for col in feature_columns:
    s = X_train[col]
    if is_numeric_dtype(s):
        schema[col] = {"type": "num"}
        defaults[col] = float(s.median())  # 기본값: 중앙값
    else:
        schema[col] = {
            "type": "cat",
            # 옵션이 너무 많으면 UI가 무거워져서 최대 200개만 (원하면 늘리세요)
            "options": sorted(s.dropna().astype(str).unique().tolist())[:200]
        }
        mode = s.dropna().astype(str).mode()
        defaults[col] = mode.iloc[0] if len(mode) else ""
meta = {
    "feature_columns": feature_columns,
    "schema": schema,
    "defaults": defaults,
    "label_map": {0: "Show", 1: "No-show"}  # 본인 기준에 맞게 유지
}
with open("./data/no_show_meta.json", "w", encoding="utf-8") as f:
    json.dump(meta, f, ensure_ascii=False, indent=2)
print("Saved:", "/data/no_show_pipeline.joblib", "/data/no_show_meta.json")

 

- streamlit 실행

  • vscode에서 app.py가 있는 디렉토리까지 이동한 후 streamlit run app.py 실행