Today I Learned

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

JiJi0406 2026. 6. 29. 21:56

001

오늘의 코드카타

44. 최소직사각형

 

 


문제 설명

명함 지갑을 만드는 회사에서 지갑의 크기를 정하려고 합니다. 다양한 모양과 크기의 명함들을 모두 수납할 수 있으면서, 작아서 들고 다니기 편한 지갑을 만들어야 합니다. 이러한 요건을 만족하는 지갑을 만들기 위해 디자인팀은 모든 명함의 가로 길이와 세로 길이를 조사했습니다.

아래 표는 4가지 명함의 가로 길이와 세로 길이를 나타냅니다.

 

명함 번호 가로 길이 세로 길이
1 60 50
2 30 70
3 60 30
4 80 40

 

가장 긴 가로 길이와 세로 길이가 각각 80, 70이기 때문에 80(가로) x 70(세로) 크기의 지갑을 만들면 모든 명함들을 수납할 수 있습니다. 하지만 2번 명함을 가로로 눕혀 수납한다면 80(가로) x 50(세로) 크기의 지갑으로 모든 명함들을 수납할 수 있습니다. 이때의 지갑 크기는 4000(=80 x 50)입니다.

모든 명함의 가로 길이와 세로 길이를 나타내는 2차원 배열 sizes가 매개변수로 주어집니다. 모든 명함을 수납할 수 있는 가장 작은 지갑을 만들 때, 지갑의 크기를 return 하도록 solution 함수를 완성해주세요.

 

제한사항

  • sizes의 길이는 1 이상 10,000 이하입니다.
  • sizes의 원소는 [w, h] 형식입니다.
  • w는 명함의 가로 길이를 나타냅니다.
  • h는 명함의 세로 길이를 나타냅니다.
  • w와 h는 1 이상 1,000 이하인 자연수입니다.

 

더보기
def solution(sizes):
    return max(max(size) for size in sizes) * max(min(size) for size in sizes)
    # max(size)들 중의 max  * min(size)들 중의 max

 

리스트 컴프리헨션 공부했던 거 생각하고 한줄로 간단하게 풀이

이런 풀이를 제너레이터 표현식이라고 한다더라

 

 

45. 시저 암호

 


문제 설명

어떤 문장의 각 알파벳을 일정한 거리만큼 밀어서 다른 알파벳으로 바꾸는 암호화 방식을 시저 암호라고 합니다. 예를 들어 "AB"는 1만큼 밀면 "BC"가 되고, 3만큼 밀면 "DE"가 됩니다. "z"는 1만큼 밀면 "a"가 됩니다. 문자열 s와 거리 n을 입력받아 s를 n만큼 민 암호문을 만드는 함수, solution을 완성해 보세요.

 

제한사항

  • 공백은 아무리 밀어도 공백입니다.
  • s는 알파벳 소문자, 대문자, 공백으로만 이루어져 있습니다.
  • s의 길이는 8000이하입니다.
  • n은 1 이상, 25이하인 자연수입니다.

 

더보기
def solution(s, n):
    big = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    small = "abcdefghijklmnopqrstuvwxyz"
    answer = [] 

    for char in s:
        if char == " ":
            answer.append(" ")
            
        elif char in big:
            # 대문자일 때 현재 인덱스 찾기
            idx = big.index(char)
            new_idx = (idx + n) % len(big)
            answer.append(big[new_idx])
            
        elif char in small:
            # 소문자일 때 현재 인덱스 찾기
            idx = small.index(char)
            new_idx = (idx + n) % len(big)
            answer.append(small[new_idx])

    return "".join(answer)

 

 

 


 

 

002

오늘 학습한 내용

 

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

 

1. 결측치 & 인코딩 (이론)

1) 이상치 처리 (IQR)

  • Q1, Q3 구해서 IQR(=Q3-Q1) 계산 →  Q3+1.5×IQR보다 크거나 / Q1-1.5×IQR보다 작으면 이상치로 봄

 

2) 결측치 처리

  • 수치형은 평균값(또는 중앙값 : 평균이 대표성 X) 대치, 범주형은 최빈값 대치
  • 간단하게는 dropna(행/열 삭제), fillna(특정 값 대치)로 처리
  • sklearn으로 하면 SimpleImputer(평균/중앙/최빈값), IterativeImputer(회귀 기반 대치), KNNImputer(KNN 기반 대치)도 있음 (뒤 두 개는 자세히 몰라도 됨)

 

3) 레이블 인코딩 (Label Encoding)

  • 문자열 범주를 순서대로 숫자로 바꾸는 거 (1등급→0, 2등급→1, 3등급→2)
  • 모델이 처리하기 쉬운 수치형으로 바뀌는 장점 있지만, 실제로는 순서 의미 없는데 모델이 "크기 차이"로 잘못 해석할 위험 o
  • LabelEncoder의 fit/transform/fit_transform/inverse_transform 메소드, classes_ 속성으로 학습한 범주 확인 가능

 

4) 원-핫 인코딩 (One-Hot Encoding)

  • 각 범주를 독립적인 이진 벡터로 변환 (빨강→[1,0,0], 파랑→[0,1,0])
  • 순서 의미 없는 명목형 데이터에 권장, 단점은 범주 개수 많으면 차원이 폭증해서(차원의 저주) 모델 복잡도↑, 과적합 유발 가능

 

2. 실습 - 결측치/인코딩 적용해보기

Age 결측치는 SimpleImputer로 학습시켜보고, Sex는 레이블 인코딩, Embarked는 원-핫 인코딩으로 변환해서 데이터프레임에 합쳐봄

from sklearn.impute import SimpleImputer

si = SimpleImputer()              # 평균값으로 대치하는 imputer 생성
si.fit(titanic_df[['Age']])       # Age 컬럼 기준으로 평균값 학습

from sklearn.preprocessing import LabelEncoder, OneHotEncoder
le = LabelEncoder()               # 레이블 인코더 생성
oe = OneHotEncoder()              # 원-핫 인코더 생성

le.fit(titanic_df[['Sex']])       # Sex 범주를 숫자로 매핑하도록 학습
titanic_df['Sex_le'] = le.transform(titanic_df[['Sex']])  # Sex를 숫자로 변환해서 새 컬럼 생성

oe.fit(titanic_df[['Embarked']])  # Embarked 범주 학습

embarked_csr = oe.transform(titanic_df[['Embarked']])     # 원-핫 인코딩 변환 (희소행렬로 반환됨)
embarked_csr_df = pd.DataFrame(embarked_csr.toarray(), columns=oe.get_feature_names_out())  # 희소행렬 → 일반 배열로 바꿔서 데이터프레임화
embarked_csr_df.head(3)

pd.concat([titanic_df, embarked_csr_df], axis=1).head(3)  # 원본 데이터에 원-핫 인코딩 결과 옆으로 합치기

 

3. 스케일링 (이론)

1) 표준화 (Standardization) 

  • 평균 빼고 표준편차로 나눠서 평균 0, 표준편차 1로 맞춤
  • StandardScaler 사용, 이상치 있거나 분포 치우쳤을 때 유용함. 단점은 최소-최대 범위가 정해지지 않음

2) 정규화 (Normalization)

  • 데이터를 0~1 사이로 맞춤 (최소값 0, 최대값 1)
  • MinMaxScaler 사용, 범위가 명확한 장점 있지만 이상치에 영향 많이 받음 (이상치 없을 때는 유용)

3) 로버스트 스케일링 (Robust Scaling)

  • 중앙값과 IQR을 이용해서 스케일링 → 이상치 영향에 덜 민감함 (표준화/정규화보다는 덜 쓰이긴 함, 자세히 몰라도 됨)

 

4. 실습 - 스케일링 적용 (실습)

Age는 MinMaxScaler로, Fare는 StandardScaler로 스케일링하고 히스토그램으로 분포 확인함

from sklearn.preprocessing import StandardScaler, MinMaxScaler
mm_sc = MinMaxScaler()            # 정규화 스케일러
sd_sc = StandardScaler()          # 표준화 스케일러

titanic_df['Age_mean_mm_sc'] = mm_sc.fit_transform(titanic_df[['Age_mean']])  # Age 정규화 후 새 컬럼 생성

titanic_df['Fare_sd_sc'] = sd_sc.fit_transform(titanic_df[['Fare']])  # Fare 표준화 후 새 컬럼 생성

sns.histplot(titanic_df['Age_mean_mm_sc'], bins = 100)  # 정규화된 Age 분포 확인
sns.histplot(titanic_df['Fare_sd_sc'])                   # 표준화된 Fare 분포 확인

 

 

5. 과적합 & 전체 모델링 프로세스 (이론)

1) 과적합(Overfitting)

  • 데이터를 너무 과하게 학습해서 그 문제만 잘 맞추고 새 데이터는 제대로 못 맞추는 현상
  • 모델이 너무 복잡하면 과대적합, 너무 단순하면 과소적합 (둘 다 문제)
  • 원인: 모델 복잡도, 데이터 양 부족, 학습 반복 너무 많음(딥러닝), 데이터 불균형(예: 정상95:암환자5)

2) train_test_split 

  • 학습 데이터(Train)로 모델 학습시키고, 테스트 데이터(Test)로 평가하는 게 기본
  • test_size/train_size(비율), shuffle(섞기), random_state(랜덤 고정값, 매번 같은 결과 보려면 고정해야 함)
  • 반환값 순서 중요: X_train, X_test, y_train, y_test (원본 데이터에 바로 덧붙이지 말고 새 컬럼명으로 가공하는 게 좋다고 함)

 

6. 실습 

가족 수 변수 만들고

이상치(Fare 512 이상) 제거

결측치 처리

스케일링

인코딩까지 함수로 만들어서 한 번에 적용

로지스틱 회귀 모델 학습하고 평가

 

전체 프로세스

  • 데이터 로드&분리 → EDA(분포·이상치 확인) → 전처리(결측치 처리, 인코딩, 스케일링) → 모델 수립 → 평가
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib as plt

train_df_2 = train_df.copy()      # 원본 보존하려고 복사본으로 작업

def get_family(df):
    df['Family'] = df['SibSp'] + df['Parch'] + 1   # 형제자매+부모자녀+본인 = 가족 수 변수 생성
    return df
get_family(train_df_2).head(3)

train_df_2 = train_df_2[train_df_2['Fare']<512]    # Fare 512 이상은 이상치로 보고 제거
train_df_2.shape

# 결측치 처리, 수치형(Age)은 평균, 범주형(Embarked)은 'S'로 대체
def get_non_missing(df):
    Age_mean = train_df_2['Age'].mean()             # train 기준으로 평균 계산 (test에도 동일하게 적용하기 위함)
    Fare_mean = train_df_2['Fare'].mean()
    df['Age'] = df['Age'].fillna(Age_mean)           # Age 결측치를 평균으로 대치
    #train 데이터에는 필요하지않으나 test데이터에 결측치 존재해서 추가 
    df['Fare'] = df['Fare'].fillna(Fare_mean)        # Fare 결측치를 평균으로 대치
    df['Embarked'] = df['Embarked'].fillna('S')      # Embarked 결측치는 최빈값인 'S'로 대치
    return df

def get_numeric_sc(df):
    # sd_sc: Fare, mm_sc: Age, Family
    from sklearn.preprocessing import StandardScaler, MinMaxScaler
    sd_sc = StandardScaler()
    mm_sc = MinMaxScaler()

    sd_sc.fit(train_df_2[['Fare']])                  # train 기준으로만 fit (test는 transform만)
    df['Fare_sd_sc'] = sd_sc.transform(df[['Fare']])
    
    mm_sc.fit(train_df_2[['Age','Family']])          # Age, Family 한번에 정규화 학습
    df[['Age_mm_sc', 'Family_mm_sc']] = mm_sc.transform(df[['Age','Family']])                           
    return df

def get_category(df):
    from sklearn.preprocessing import LabelEncoder, OneHotEncoder
    le = LabelEncoder()    # Pclass용
    le2 = LabelEncoder()   # Sex용
    oe = OneHotEncoder()   # Embarked용
    
    le.fit(train_df_2[['Pclass']])
    df['Pclass_le'] = le.transform(df['Pclass'])     # Pclass를 레이블 인코딩
    
    le2.fit(train_df_2[['Sex']])
    df['Sex_le'] = le2.transform(df['Sex'])          # Sex를 레이블 인코딩
    
    #index reset을 하기위한 구문
    df = df.reset_index(drop=True)                   # concat 시 인덱스 꼬임 방지
    oe.fit(train_df_2[['Embarked']])
    embarked_csr = oe.transform(df[['Embarked']])
    embarked_csr_df = pd.DataFrame(embarked_csr.toarray(), columns = oe.get_feature_names_out())
    df = pd.concat([df, embarked_csr_df], axis = 1)  # 원-핫 인코딩 결과를 옆으로 합치기
    return df

train_df_2 = get_category(train_df_2)

def get_model(df):
    from sklearn.linear_model import LogisticRegression
    model_lor = LogisticRegression()
    X = df[['Age_mm_sc','Fare_sd_sc','Family_mm_sc', 'Pclass_le', 'Sex_le', 'Embarked_C', 'Embarked_Q', 'Embarked_S']]  # 전처리 끝난 변수들만 모델 입력으로 사용
    y = df[['Survived']]
    return model_lor.fit(X,y)                        # 학습된 모델 반환

model_output = get_model(train_df_2)
model_output

X = train_df_2[['Age_mm_sc','Fare_sd_sc','Family_mm_sc', 'Pclass_le', 'Sex_le', 'Embarked_C', 'Embarked_Q', 'Embarked_S']]
y_pred = model_output.predict(X)                     # train 데이터로 예측 (자체 평가용)

# 평가
from sklearn.metrics import accuracy_score, f1_score
print(accuracy_score(train_df_2['Survived'],y_pred))
print(f1_score(train_df_2['Survived'],y_pred))

# test 데이터에도 같은 전처리 함수들을 그대로 적용
test_df_2 = get_family(test_df)
test_df_2 = get_non_missing(test_df_2)
test_df_2 = get_numeric_sc(test_df_2)
test_df_2 = get_category(test_df_2)

test_X = test_df_2[['Age_mm_sc','Fare_sd_sc','Family_mm_sc', 'Pclass_le', 'Sex_le', 'Embarked_C', 'Embarked_Q', 'Embarked_S']]

y_test_pred = model_output.predict(test_X)           # test 데이터로 최종 예측

# Kaggle에 제출
sub_df = pd.read_csv('C:/Users/USER/OneDrive/바탕 화면/Python_study/titanic/gender_submission.csv')  # 제출 양식 불러오기

sub_df['Survived'] = y_test_pred                     # 예측 결과를 제출 양식에 채우기
sub_df.to_csv('./result.csv', index = False)         # 최종 제출 파일 저장

 

7. 교차 검증 (이론)

1) 교차검증이 필요한 이유?

  • train_test_split으로 한 번만 나누면, 그 고정된 테스트셋에만 맞춰질 위험(과적합)이 있음 → 이걸 보완하려고 나온 방법

2) K-Fold Validation

  • Train 데이터를 K개로 나눠서, 한 덩어리씩 돌아가며 검증용으로 쓰고 나머지로 학습 (Split1~K까지 반복 후 최종 평가)
  • 데이터가 부족할 때 특히 유용
  • KFold, StratifiedKFold(불균형 데이터일 때 사용) 함수 있음

3) GridSearch (지금은 자세히 몰라도 됨)

  • 하이퍼파라미터(사람이 직접 정하는 값)를 여러 조합으로 자동 실험해주는 도구라고만 알아두기