Machine Learning/Real Analysis

로지스틱 회귀 모델링 <전처리>

Frisbeen 2026. 4. 22. 05:34

[#6] 최종 변수 선택 정책 확정

3~5편에서 변수별 신호 강도와 다중공선성 이슈를 파악했습니다. 6편에서는 모델에 실제로 넣을 변수를 최종 확정합니다.

변수 선택은 신중해야한다.

지금까지 분석한 변수는 총 9개입니다. 그런데 이걸 전부 다 넣으면 안 됩니다. 이유는 두 가지입니다.

첫째, 다중공선성 — 서로 같은 정보를 담은 변수를 둘 다 넣으면 모델이 혼란스러워집니다. Fare와 Pclass, FamilySize와 IsAlone이 대표적입니다.

둘째, 노이즈 — 신호가 약한 변수를 억지로 넣으면 모델이 의미 없는 패턴까지 학습하려다 성능이 오히려 떨어집니다.

따라서 "쓸 것"과 "버릴 것"을 명확히 가르고 근거를 남겨야 합니다.


Step 1. 첫번째 제외 결정 — Fare vs Pclass

4편에서 Fare의 생존율 패턴이 Pclass와 거의 동일하다는 것을 확인했습니다. 실제 상관관계를 수치로 확인합니다.

fare_pclass_corr = train[["Fare", "Pclass"]].corr().round(3)
fare_pclass_corr

 

Fare 1.000 -0.550
Pclass -0.550 1.000

 

corr()의 결과값 즉, 상관계수가 -0.55입니다. Pclass 숫자가 낮을수록(1등석일수록) Fare가 높아지는 관계가 수치로 확인됩니다.

여기서 저는 Pclass를 선택합니다. 이유는 Pclass가 이산형(1/2/3)이라 로지스틱 회귀에 그대로 쓰기 쉽고,

무엇보다 Fare는 분포가 심하게 치우쳐 있어 로그 변환까지 해야 하는 번거로움이 있기 때문입니다.

Fare (skew=4.79, kurtosis=33.40)

💡 결정: Fare 제외, Pclass 유지


Step 2. 두번째 제외 결정 — FamilySize vs IsAlone

FamilySize에서 IsAlone을 만들었으므로 둘은 완전히 연관되어 있습니다. 어느 쪽이 생존 신호를 더 깔끔하게 담고 있는지 비교합니다.

familysize_corr = train[["FamilySize", "IsAlone", "Survived"]].corr().round(3)
familysize_corr

 

FamilySize 1.000 -0.650 0.017
IsAlone -0.650 1.000 -0.203
Survived 0.017 -0.203 1.000

핵심인 Survived와의 상관계수를 관찰해보겠습니다.

  • FamilySize ↔ Survived: 0.017 (거의 0, 신호 없음)
  • IsAlone ↔ Survived: -0.203 (음수 = 혼자일수록 생존율 낮음, 신호 있음)

FamilySize가 생존율과 역U자형 관계여서 상관계수로는 신호가 잡히지 않는 것이라고도 볼 수있습니다. 상관관계가 옅습니다. 따라서 IsAlone이 "혼자냐 아니냐"라는 핵심 신호를 더 명확하게 전달합니다.

💡 결정: FamilySize 제외, IsAlone 유지


 

Step 3. 제외 결정 — Embarked

3편에서 Embarked의 생존율 차이가 Pclass의 교란 효과 때문임을 확인했습니다.

Pclass를 통제하기 위해 같은 Pclass안에서 Embarked의 값들을 비교해보겠습니다.

embarked_controlled = (
    train.groupby(["Embarked", "Pclass"])["Survived"]
    .mean()
    .round(3)
    .unstack()
)
embarked_controlled

 

C 0.694 0.529 0.378
Q 0.500 0.667 0.375
S 0.582 0.463 0.189

같은 Pclass 안에서도 Embarked별 차이가 조금 있긴 합니다.

그런데 Q의 경우 2등석 샘플이 6명(0.667은 4명 생존)으로 너무 적어 신뢰하기 어렵습니다.

전반적으로 Pclass를 통제하면 Embarked의 패턴을 얻기 힘들어서 신호가 크게 약해진다는 것을 알 수 있습니다.

Embarked 제외. 신호가 독립적이지 않고 샘플 수도 불균형.


Step 4. HasCabin 독립 신호 검증

HasCabin이 Pclass와 강하게 연관되어 있다는 건 확인했습니다. 그러나 Pclass를 통제한 상태에서도 HasCabin이 추가 신호를 주는지 확인합니다.

hascabin_controlled = (
    train.groupby(["HasCabin", "Pclass"])["Survived"]
    .mean()
    .round(3)
    .unstack()
)
hascabin_controlled.index = ["기록 없음 (0)", "기록 있음 (1)"]
hascabin_controlled

 

기록 없음 (0) 0.461 0.464 0.242
기록 있음 (1) 0.672 0.583 0.333

같은 Pclass 안에서도 HasCabin이 1인 경우 생존율이 일관되게 높습니다. 1등석에서도 기록 있는 승객(67.2%) vs 없는 승객(46.1%)의 차이가 납니다.

이는 Pclass가 같더라도 HasCabin이 추가적인 정보를 담고 있다는 의미라고 볼 수 있습니다. 아마 1등석 안에서도 더 고급 객실을 배정받은 승객과 그렇지 않은 승객 사이의 차이가 반영된 것으로 해석할 수 있습니다.

💡 결정: HasCabin은 모델 feature에 포함. Pclass와 겹치지만 독립적인 신호 존재 확인.


Step 5. 최종 변수 목록 확정

로지스틱 회귀 모델을 사용할 것이기에, 범주형 변수인 Sex는 원-핫 인코딩으로 전처리할 예정입니다. 순서형 변수인 Pclass는 숫자가 커질수록 생존율이 일관되게 낮아지는 선형 관계가 확인됐기 때문에 그대로 사용합니다. 나머지 이진형 변수들(IsChild, IsAlone, HasCabin)은 이미 0/1로 수치화되어 있어 추가 전처리 없이 그대로 사용합니다.

final_features = pd.DataFrame(
    "feature": ["Sex", "Pclass", "IsChild", "IsAlone", "HasCabin"],
    "type": ["범주형", "순서형", "이진형", "이진형", "이진형"],
    "preprocessing_needed": [
        "원-핫 인코딩",
        "그대로 사용 (1/2/3)",
        "그대로 사용 (0/1)",
        "그대로 사용 (0/1)",
        "그대로 사용 (0/1)"
    ],
    "missing_handling": [
        "결측 없음",
        "결측 없음",
        "Age 결측 → IsChild=0 처리 (177건)",
        "결측 없음",
        "결측 없음 (결측 자체가 0)"
    ]
})
final_features.to_csv(OUTPUT_DIR / "19_final_features.csv", index=False)
final_features

 

Sex 범주형 원-핫 인코딩 결측 없음
Pclass 순서형 그대로 사용 (1/2/3) 결측 없음
IsChild 이진형 그대로 사용 (0/1) Age 결측 → IsChild=0 처리 (177건)
IsAlone 이진형 그대로 사용 (0/1) 결측 없음
HasCabin 이진형 그대로 사용 (0/1) 결측 없음 (결측 자체가 0)

Step 6. 제외 변수

첫째, 다중공선성으로 제외한 변수들 — Fare, FamilySize

Fare는 Pclass와 상관계수가 -0.55로 강하게 연관되어 있어 중복 정보를 담고 있다고 판단했습니다. 둘 중 Pclass를 선택한 이유는 Pclass가 이산형(1/2/3)이라 전처리 부담이 적고, Fare는 분포가 심하게 치우쳐 있어 로그 변환까지 필요하기 때문입니다.

FamilySize는 IsAlone과 완전히 연관된 변수입니다. IsAlone을 FamilySize로부터 만들었기 때문입니다. Survived와의 상관계수도 0.017로 거의 0에 가까워 신호가 미약했고, IsAlone이 같은 정보를 더 명확하게 전달하기 때문에 FamilySize는 제외합니다.

 

둘째, 파생변수로 대체된 변수들 — Age, SibSp, Parch

Age는 연속값 그대로는 신호가 약했고, "어린이냐 아니냐"라는 핵심 신호만 IsChild로 추출했기 때문에 원본은 제외합니다. SibSp와 Parch는 FamilySize로 합산된 뒤 IsAlone으로 이진화되었으므로 원본을 따로 넣을 필요가 없습니다.

 

셋째, 신호가 없거나 구조화가 어려운 변수들 — Embarked, Name, Ticket, PassengerId

Embarked는 Pclass를 통제했을 때 독립적인 신호가 소멸했고 샘플 불균형도 있어 제외합니다. Name은 텍스트라 구조화가 어렵고, 호칭(Mr, Mrs 등)을 파생변수로 뽑는 방법도 있지만 이번 분석 범위에서는 제외합니다. Ticket과 PassengerId는 단순 식별자로 생존과 무관합니다.

excluded_features = pd.DataFrame({
    "feature": ["Fare", "FamilySize", "Embarked", "Age (원본)", "SibSp", "Parch", "Name", "Ticket", "PassengerId"],
    "reason": [
        "Pclass와 상관계수 -0.55, 중복 정보",
        "IsAlone과 중복, Survived 상관계수 0.017로 신호 미약",
        "Pclass 통제 시 독립 신호 소멸, 샘플 불균형",
        "IsChild로 대체, 연속값 그대로는 신호 약함",
        "FamilySize로 합산 후 IsAlone으로 이진화",
        "FamilySize로 합산 후 IsAlone으로 이진화",
        "텍스트, 구조화 어려움 (호칭 파생은 고급 과정)",
        "식별자, 생존과 무관",
        "식별자, 생존과 무관"
    ]
})
excluded_features.to_csv(OUTPUT_DIR / "20_excluded_features.csv", index=False)
excluded_features

 

Fare Pclass와 상관계수 -0.55, 중복 정보
FamilySize IsAlone과 중복, Survived 상관계수 0.017로 신호 미약
Embarked Pclass 통제 시 독립 신호 소멸, 샘플 불균형
Age (원본) IsChild로 대체, 연속값 그대로는 신호 약함
SibSp FamilySize로 합산 후 IsAlone으로 이진화
Parch FamilySize로 합산 후 IsAlone으로 이진화
Name 텍스트, 구조화 어려움
Ticket 식별자, 생존과 무관
PassengerId 식별자, 생존과 무관
   

[#7] 전처리 파이프라인 구현

6편에서 최종 변수 5개(Sex, Pclass, IsChild, IsAlone, HasCabin)를 확정했습니다. 7편에서는 이 변수들을 모델에 실제로 입력할 수 있는 형태로 만드는 전처리 파이프라인을 구현합니다.

전처리 과정을 왜 파이프라인으로 만드는가 - Test의 눈치를 봐야한다.

전처리를 그냥 코드로 쭉 작성해도 되는데, 굳이 파이프라인으로 묶는 이유가 있습니다.

train 데이터로 전처리를 하고, 나중에 test 데이터에도 똑같이 적용해야 합니다. 코드를 따로따로 작성하면 실수할 가능성이 높고, 나중에 test 데이터에 빠뜨리거나 다르게 적용하는 실수가 생깁니다.

 

파이프라인은 "전처리 과정을 하나의 객체로 묶어서" train에 fit하고, test에는 그대로 transform만 하면 되는 구조입니다. 실수 없이 동일한 전처리를 보장할 수 있습니다.

“train에 계산한 기준 자체를 test에도 자동으로 똑같이 적용해주는 구조”를 만드는 것이 파이프라인 구축의 목적입니다.

# train 전처리
train["Age"].fillna(train["Age"].mean())
train["Embarked"].fillna("S")
# ... 10줄

# test 전처리 (실수)
test["Age"].fillna(test["Age"].mean())  # ← 실수로 test 평균 씀
test["Embarked"].fillna("S")
# ... 똑같이 10줄 또 씀

# train에서 기준값 먼저 저장


Step 1. 파생변수 생성 함수

지금까지 만든 파생변수들을 함수로 묶습니다. train과 test 양쪽에 동일하게 적용하기 위함입니다.

def make_features(df):
    df = df.copy()
    
    # IsChild: Age 결측은 자동으로 False → 0 (성인)
    df["IsChild"] = (df["Age"] < 16).astype(int)
    
    # FamilySize → IsAlone
    df["FamilySize"] = df["SibSp"] + df["Parch"] + 1
    df["IsAlone"] = (df["FamilySize"] == 1).astype(int)
    
    # HasCabin: Cabin 결측 여부 이진화
    df["HasCabin"] = df["Cabin"].notna().astype(int)
    
    return df

train = make_features(train)
test = make_features(test)

df.copy()를 쓰는 이유는 원본 데이터를 건드리지 않기 위해서입니다.

함수 안에서 컬럼을 추가하면 원본 DataFrame도 바뀌는 경우가 있어서, 복사본을 만들어 작업하는 것이 안전합니다.


Step 2. 최종 feature 선택

FEATURES = ["Sex", "Pclass", "IsChild", "IsAlone", "HasCabin"]
TARGET = "Survived"

X_train = train[FEATURES]
y_train = train[TARGET]

X_test = test[FEATURES]

6편에서 확정한 변수 목록을 코드로 고정합니다. FEATURES 를 상수로 따로 빼두는 이유는, 나중에 변수를 추가하거나 빼고 싶을 때 이 한 줄만 수정하면 아래 코드 전체에 자동으로 반영되기 때문입니다.


Step 3. 전처리 파이프라인 구성

6편에서 확정한 전처리 방향을 파이프라인으로 구현합니다.

  • Sex → 원-핫 인코딩
  • Pclass, IsChild, IsAlone, HasCabin → 그대로 사용
# 원-핫 인코딩이 필요한 컬럼
categorical_features = ["Sex"]

# 그대로 사용할 컬럼
passthrough_features = ["Pclass", "IsChild", "IsAlone", "HasCabin"]

preprocessor = ColumnTransformer(
    transformers=[
        ("cat", OneHotEncoder(drop="first", sparse_output=False), categorical_features),
        ("num", "passthrough", passthrough_features)
    ]
)

ColumnTransformer가 하는 일

컬럼마다 다른 전처리를 적용할 수 있게 해주는 도구입니다. Sex는 원-핫 인코딩, 나머지는 그대로 통과(passthrough)시키는 구조를 활용했습니다.

OneHotEncoder의 drop="first"

원-핫 인코딩을 하면 Sex가 Sex_male, Sex_female 두 컬럼으로 바뀝니다.

그런데 둘 중 하나가 1이면 나머지는 자동으로 0이라 두 컬럼이 완전히 중복입니다. drop="first"는 첫 번째 컬럼을 버려서 중복을 제거하여 모델의 효율성을 올리기에 활용하였습니다.

결과적으로 Sex_female 하나만으로 표현이 가능합니다.


Step 4. 전처리 적용 및 결과 확인

X_train_processed = preprocessor.fit_transform(X_train)
X_test_processed = preprocessor.transform(X_test)

# 컬럼명 확인
cat_cols = preprocessor.named_transformers_["cat"].get_feature_names_out(categorical_features)
all_cols = list(cat_cols) + passthrough_features

print("전처리 후 컬럼:", all_cols)
print("X_train shape:", X_train_processed.shape)
print("X_test shape:", X_test_processed.shape)

Sex_female Pclass IsChild IsAlone HasCabin

0.0 3 0 0 0
1.0 1 0 0 1
1.0 3 0 1 0
1.0 1 0 0 1
0.0 3 0 1 0

fit_transform과 transform의 차이가 중요합니다.

  • fit_transform(X_train) → train 데이터로 전처리 방법을 학습하고 동시에 적용
  • transform(X_test) → 학습된 전처리를 test 데이터에 그대로 적용만

test 데이터는 학습의 대상이 전혀 아니기에 fit_transform을 쓰면 안 됩니다.

est 데이터 기준으로 새로 학습하면 train과 다른 기준이 적용되어 결과가 달라집니다.


Step 5. 전처리 결과 저장

preprocessor.fit_transform() 결과는 numpy배열로 나오기에, 표처럼 생겼지만 컬럼며이 없는 숫자 덩어리로 학습된 전처리 데이터가 나오기에, DataFrame로 감쌉니다.

또한 정답 컬럼을 같이 추가하여, 나중에 CSV를 열었을때, 탑승객의 생존여부를 같이 볼 수 있게 합니다.

X_train_df = pd.DataFrame(X_train_processed, columns=all_cols)
X_train_df["Survived"] = y_train.values

X_train_df.to_csv(OUTPUT_DIR / "21_X_train_processed.csv", index=False)
X_train_df.head()
0.0 3 0 0 0
1.0 1 0 0 1
1.0 3 0 1 0
1.0 1 0 0 1
0.0 3 0 1 0

 

모든 컬럼이 숫자로 변환되었습니다. 이제 로지스틱 회귀 모델에 바로 입력하여 예측이 가능해진 상태입니다.