[ 데이콘 ] 소득 예측 AI 해커톤 코드 분석(Private 1위)
[Private 1위] Target Encoding + LGBM + StratifiedKFold
노트북 링크 : https://colab.research.google.com/drive/1rVmEeZC9RgxHq4Iq6MrfrBJo2N2KAM-u?usp=sharing
데이콘 소득예측 / [Private 1위] Target Encoding + LGBM + StratifiedKFold
Colab notebook
colab.research.google.com
# 초기 설정
!pip install category-encoders
!pip install catboost
!pip install optuna
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import random
import os
import warnings
from category_encoders.target_encoder import TargetEncoder
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone
from sklearn.metrics import mean_squared_error
from lightgbm import LGBMRegressor
import optuna
from optuna.samplers import TPESampler
import seaborn as sns
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore")
def seed_everything(seed):
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
np.random.seed(seed)
seed_everything(42) # Seed 고정
1. Read data & EDA
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
submission = pd.read_csv('sample_submission.csv')
test_copy = test.copy()
# Income 분포 확인
print(f'Min Income: {train["Income"].min()}, Max Income: {train["Income"].max()}')
plt.figure(figsize=(12, 4))
data = train['Income']
sns.histplot(data, color='skyblue', kde=True)
plt.xlabel('Income')
plt.show()
# Age가 20보다 작은 경우 Income 평균
train[train['Age'] < 20].groupby('Age')['Income'].mean().reset_index().T
과연 미성년자의 수입 분포는 어떻게 될까? / 평균값 확인
=> 15세 이후로 꽤 있다
# Age가 80 이상인 경우 Income 평균
train[train['Age'] >= 80].groupby('Age')['Income'].mean().reset_index().T
과연 고령자의 수입 분포는 어떻게 될까? / 평균값 확인
=> 들쑥날쑥 하다
# Employment_Status별 Income 평균
train.groupby('Employment_Status')['Income'].mean().reset_index()
먼저 취업상태는 순서형 데이터인 것으로 보인다. 그런데 아이와 군인이 껴있다보니 순서가 애매하다
그리고 상태별로 값의 분포가 적절한지도 살펴봐야할 것으로 보인다.
예상외로 대체적으로 풀타임보다 파트타임이 더높다.
풀타임 평균보다 높은 파트타임은 어떤 직업군이지 궁금하다.
# Occupation_Status별 Income 평균
train.groupby('Occupation_Status')['Income'].mean().reset_index()
직업군에 따른 수입이다. 여기서는 따로 군인이 빠져나와 있다. => 수입이 '0'이다 ㅎㅎ
Unknown이 보인다. 이를 잘 정리해줘야 할 것으로 보인다. Unknown도 수입이 '0'이다.
# Industry_Status별 Income 평균
train.groupby('Industry_Status')['Income'].mean().reset_index()
# Education_Status별 Income 평균
train.groupby('Education_Status')['Income'].mean().reset_index()
2. EDA에 따른 피쳐수정
train = train[(train['Age'] >= 15) & (train['Income'] <= 3000)]
train = train[train['Employment_Status']!='Not Working']
train = train[train['Occupation_Status']!='Armed Forces']
train = train[train['Occupation_Status']!='Unknown']
train = train[train['Industry_Status']!='Armed Forces']
train = train[train['Industry_Status']!='Not in universe or children']
train = train[train['Education_Status']!='Children']
test = test[test['Age'] >= 15]
test = test[test['Employment_Status']!='Not Working']
test = test[test['Occupation_Status']!='Armed Forces']
test = test[test['Occupation_Status']!='Unknown']
test = test[test['Industry_Status']!='Armed Forces']
test = test[test['Industry_Status']!='Not in universe or children']
test = test[test['Education_Status']!='Children']
test_ID = test['ID']
- train = train[(train['Age'] >= 15) & (train['Income'] <= 3000)]
- 나이가 15세 이상이면서 수입이 3000 이하인 데이터만을 선택합니다.
- 이는 아마도 어린이 데이터를 제외하고, 특정 소득 수준 이하의 데이터만을 대상으로 분석하고자 하는 것으로 보입니다.
- train = train[train['Employment_Status']!='Not Working']
- '고용 상태'가 'Not Working' (비고용 상태)이 아닌 사람들만을 데이터셋에서 유지합니다.
- 이는 고용된 사람들의 데이터만 분석하고자 할 때 사용합니다.
- train = train[train['Occupation_Status']!='Armed Forces']
- '직업 상태'가 'Armed Forces' (군인)가 아닌 경우만을 필터링하여 선택합니다.
- 특정 직업군을 제외한 나머지 직업을 대상으로 분석하기 위한 조치입니다.
- train = train[train['Occupation_Status']!='Unknown']
- '직업 상태'가 'Unknown' (미상)인 데이터를 제외합니다.
- 이는 미상의 데이터가 모델에 불필요한 노이즈를 추가할 수 있기 때문입니다.
- train = train[train['Industry_Status']!='Armed Forces']
- '산업 상태'가 'Armed Forces' (군인)가 아닌 데이터만을 선택합니다.
- 이는 군인을 제외한 다른 산업에 종사하는 사람들의 데이터만을 분석하기 위함입니다.
- train = train[train['Industry_Status']!='Not in universe or children']
- '산업 상태'가 'Not in universe or children' (해당 없음 또는 어린이)가 아닌 데이터만을 선택합니다.
- 이는 실제로 산업에 종사하는 성인 인구를 대상으로 데이터를 분석하고자 할 때 사용됩니다.
- train = train[train['Education_Status']!='Children']
- '교육 상태'가 'Children' (어린이)가 아닌 사람들만을 데이터셋에서 유지합니다.
- 이는 교육 상태 데이터에서 어린이를 제외하고 분석을 진행하고자 할 때 적용됩니다.
피쳐를 지울때는 '열' 중심으로 작업하기 보다는
'행' 중심으로 데이터를 건들여봅시다.
해당 피쳐가 필요없다고 단정하기 어렵다.
2차원적으로 그래프상에서 우상향, 우하향 관계가 안보인다고 해서 관계성이 없다고 보기 어렵다.
그럼 언제 피쳐를 삭제하나요? 어떤 기준이 있나요?
# chatgpt의 답변
변수를 제거하는 결정은 데이터 과학 프로젝트에서 중요한 단계입니다.
이를 신중하게 수행하기 위해 다음과 같은 기준과 절차를 고려할 수 있습니다:
- 데이터 이해와 도메인 지식: 변수의 중요성을 이해하는 데 있어 도메인 지식이 큰 역할을 합니다. 해당 분야의 전문가 의견이나 기존 연구를 참조하여, 특정 변수가 문제 해결에 기여하는 정도를 평가할 수 있습니다.
- 통계적 분석: 변수 간의 상관관계를 분석하거나, 변수의 변동성이 결과에 미치는 영향을 평가하기 위해 기술적 통계 방법을 사용합니다. 변수가 통계적으로 유의미하지 않은 경우 제거를 고려할 수 있습니다.
- 모델 기반 평가: 다양한 머신러닝 모델을 사용하여 변수의 중요도를 평가합니다. 특히, 랜덤 포레스트나 그라디언트 부스팅과 같은 모델은 변수 중요도를 직접 제공합니다. 모델 성능에 크게 기여하지 않는 변수는 제거를 고려할 수 있습니다.
- 크로스 밸리데이션: 변수를 제거한 모델과 제거하지 않은 모델을 비교하여 크로스 밸리데이션을 수행함으로써, 변수 제거의 영향을 정량적으로 평가할 수 있습니다. 여러 데이터 세트에서 일관되게 성능 향상이 관찰되면 변수 제거를 진행할 수 있습니다.
- 차원 축소 기법: PCA(주성분 분석) 같은 차원 축소 기법을 사용하여, 데이터의 정보를 최대한 보존하면서 차원을 줄일 수 있습니다. 이는 변수 간의 관계를 더 명확하게 이해하고 중복성이 높은 변수를 식별할 때 유용합니다.
- 비즈니스 목표와 정렬: 마지막으로, 분석의 최종 목표와 데이터 변수가 어떻게 정렬되는지 고려하는 것이 중요합니다. 변수 제거 결정이 비즈니스 문제를 해결하는 데 도움이 되는지 평가해야 합니다.
변수를 제거할 때는 이러한 다양한 기준을 종합적으로 고려하며, 해당 변수가 모델의 성능이나 결과 해석에 부정적인 영향을 미칠 가능성이 낮다는 확신이 들 때 수행하는 것이 좋습니다. 데이터의 복잡성을 줄이면서도 필요한 정보를 유지하는 균형을 찾는 것이 핵심입니다.
# 불필요한 컬럼 제거
drop_cols = ['ID']
train.drop(drop_cols, axis=1, inplace=True)
test.drop(drop_cols, axis=1, inplace=True)
# 중복 데이터 제거
before_train_size = train.shape[0]
train = train.drop_duplicates()
print(f'중복 데이터 제거 개수 : {before_train_size - train.shape[0]}')
train.reset_index(drop=True, inplace=True)
중복 데이터 제거 개수 : 12
# Target Encoding
object_columns = train.select_dtypes(include='object').columns
for i in object_columns:
le = TargetEncoder()
train[i] = le.fit_transform(train[i], train['Income'])
test[i] = le.transform(test[i])
# 파생변수 생성
train['Industry_Occupation'] = train['Industry_Status'] * train['Occupation_Status']
train['Age_Household_Size_Ratio'] = train['Age'] / train['Household_Summary']
train['Race_Gender'] = train['Race'] * train['Gender']
train['Race_Hispanic_Origin'] = train['Race'] * train['Hispanic_Origin']
test['Industry_Occupation'] = test['Industry_Status'] * test['Occupation_Status']
test['Age_Household_Size_Ratio'] = test['Age'] / test['Household_Summary']
test['Race_Gender'] = test['Race'] * test['Gender']
test['Race_Hispanic_Origin'] = test['Race'] * test['Hispanic_Origin']
파생변수는 왜 만드나요?
# chatgpt의 답변
파생변수를 생성하는 과정은 데이터의 특징을 보다 잘 포착하고,
모델이 예측을 더 잘 수행하도록 돕는 중요한 단계입니다. 여기서 제시된 파생변수들을 각각 살펴보겠습니다:
- Industry_Occupation: 이 변수는 'Industry_Status'와 'Occupation_Status'의 값을 곱하여 생성합니다. 이 과정에서 산업 분야와 직업 상태 간의 상호작용을 포착하려고 합니다. 예를 들어, 특정 산업에서 일하는 사람들의 직업군이 결과 변수에 미치는 영향을 모델링할 수 있습니다.
- Age_Household_Size_Ratio: 이 변수는 'Age'를 'Household_Summary'로 나누어 생성합니다. 이 비율은 가구 내에서 개인의 연령이 가구 크기에 비해 어떤 비율을 차지하는지를 나타냅니다. 이는 연령과 가구 구성의 관계를 통해 어떤 사회경제적 동태를 반영할 수 있습니다.
- Race_Gender: 'Race'와 'Gender'의 값을 곱하여 생성합니다. 이 변수는 인종과 성별의 교차성을 나타내는데 사용될 수 있으며, 특정 인종과 성별 조합이 결과에 미치는 영향을 조사할 때 유용할 수 있습니다.
- Race_Hispanic_Origin: 마찬가지로 'Race'와 'Hispanic_Origin'을 곱하여 생성합니다. 이 파생변수는 인종적 배경과 히스패닉 출신 여부 간의 상호작용을 모델링하는 데 도움이 됩니다. 특정 인종과 히스패닉 기원이 결합된 효과를 살펴보고자 할 때 유용합니다.
각 파생변수는 특정한 상호작용이나 관계를 모델 내에서 명시적으로 표현하고자 할 때 생성됩니다. 이러한 변수들은 종종 모델의 성능을 향상시킬 수 있지만, 동시에 다중공선성이나 과적합의 위험도 증가시킬 수 있습니다. 따라서 이러한 변수들이 실제로 모델에 긍정적인 영향을 미치는지 평가하기 위해서는 교차 검증과 함께 변수의 중요도를 평가하는 것이 중요합니다.
마지막으로, 데이터 세트에서 'Race'와 'Gender' 등은 범주형 변수일 가능성이 높으므로, 곱셈 연산 대신 적절한 인코딩 방법(예: 원-핫 인코딩)을 사용하는 것이 좋습니다. 만약 위 코드가 실수로 인해 잘못 작성되었다면, 적절한 데이터 전처리 방법을 적용하는 것이 필요합니다.
3. 모델 구성 및 훈련
def skf_train_model(train, estimators):
X = train.copy()
y = X.pop('Income')
test_predictions = np.zeros((len(test))) # 예측을 누적할 배열
val_scores = [] # 각 폴드에서의 검증 세트에 대한 평가 성능을 저장할 리스트
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, valid_idx) in enumerate(skf.split(X, y)):
model = clone(estimators)
X_train, y_train = X.iloc[train_idx], y.iloc[train_idx]
X_valid, y_valid = X.iloc[valid_idx], y.iloc[valid_idx]
model.fit(X_train, y_train)
train_y_pred = model.predict(X_train)
valid_y_pred = model.predict(X_valid)
# print(f"Train RMSE Score : {mean_squared_error(y_train, train_y_pred, squared=False)}")
# print(f"Valid RMSE Score : {mean_squared_error(y_valid, valid_y_pred, squared=False)}")
# print("-" * 50)
test_predictions += model.predict(test) / 5
y_preds = model.predict(X_valid)
val_scores.append(mean_squared_error(y_valid, y_preds, squared=False))
# print(f"RMSE Score : {np.array(val_scores).mean()}")
return val_scores, test_predictions
best_params = {
'num_leaves': 50,
'max_depth': 8,
'learning_rate': 0.06996742850657808,
'n_estimators': 107,
'min_child_samples': 21,
'subsample': 0.5889374661414168,
'colsample_bytree': 0.3306647854340129,
'reg_alpha': 2.814245208464249,
'reg_lambda': 9.238104977329753
}
best_params["random_state"] = 293
best_params["verbosity"] = -1
lgbm = LGBMRegressor(**best_params)
lgbm_val_scores, lgbm_test_predictions = skf_train_model(train, lgbm)
submission.loc[submission['ID'].isin(test_ID), 'Income'] = lgbm_test_predictions
final = pd.concat([test_copy, submission[['Income']]], axis=1)
# 결과 제출(마지막 제출 전 결과값 수정)
final.loc[final['Employment_Status'] == 'Seeking Full-Time', 'Income'] = 0
final.loc[(final['Age'] >= 83) & (final['Age'] < 90), 'Income'] = 0
final.loc[final['Income'] <= 200, 'Income'] = 0
submission['Income'] = final['Income']
submission.to_csv("submission_19.csv", index=False)
# 분석 과정에서 의문점:
1. 왜 순서형 변수에 맞게 피쳐엔지니어링을 안 할까?
LightGBM과 같은 트리 기반의 모델에서 순서형 변수의 값을 바꾸지 않는 이유는 주로 이러한 모델들이 데이터의 내재된 패턴과 구조를 자동으로 학습할 수 있는 능력 때문입니다. 특히, LightGBM과 같은 그라디언트 부스팅 모델은 데이터의 분할에 있어 변수의 실제 값보다는 해당 값이 목표 변수와 어떻게 상호작용하는지를 중요시 여깁니다. 순서형 변수에 대해 몇 가지 주요 이유를 살펴보겠습니다:
- 내부적인 최적 분할: LightGBM은 이진 분할을 사용하여 각 단계에서 정보 이득을 최대화하는 방향으로 데이터를 분할합니다. 순서형 변수의 원래 값이 분할을 결정하는 데 유효한 경우, 이 값들을 변경하거나 조정할 필요가 없습니다. 모델은 이러한 값들을 기반으로 최적의 분할을 자동으로 찾아내어, 순서형 변수의 자연스러운 순서를 반영할 수 있습니다.
- 데이터의 본질 보존: 순서형 변수의 값은 특정한 순서나 등급을 나타내기 때문에, 이 값을 인위적으로 변환하면 데이터의 본질적인 의미가 왜곡될 수 있습니다. LightGBM과 같은 모델은 원본 데이터의 본질적인 특성을 유지하면서 학습하는 것이 더 효과적일 수 있습니다.
- 모델의 복잡성 관리: 변수의 값을 임의로 변경하거나 인코딩하는 것은 모델의 복잡성을 증가시키고, 과적합의 위험을 높일 수 있습니다. 트리 기반 모델은 자연스럽게 변수의 분포와 패턴을 학습하므로, 별도의 변환 없이도 효과적인 학습이 가능합니다.
- 효율성과 속도: LightGBM은 대규모 데이터를 다룰 때 매우 효율적입니다. 데이터 전처리 과정을 간소화함으로써, 데이터 로딩과 모델 학습 시간을 단축할 수 있습니다. 변수를 임의로 조정하는 과정은 이러한 효율성을 저하시킬 수 있습니다.
결론적으로, LightGBM과 같은 고급 머신러닝 모델은 데이터의 원래 특성을 잘 활용하여 강력한 예측 모델을 구축할 수 있는 능력을 갖추고 있습니다. 순서형 변수의 값을 변형하지 않고도, 변수의 자연스러운 순서와 중요성을 반영하여 효과적으로 학습할 수 있기 때문에, 별도의 값 변환은 종종 불필요하게 됩니다.
2. 그럼 원 핫 인코딩은 왜 안 할까?
LightGBM과 같은 트리 기반 알고리즘에서는 원-핫 인코딩을 특별히 권장하지 않는 경우가 많습니다. 이는 몇 가지 이유로 설명될 수 있습니다:
- 트리 분할 메커니즘: 트리 기반 모델은 범주형 변수의 각 수준을 직접적으로 핸들링할 수 있습니다. 데이터를 분할할 때, 각 범주의 효과를 직접적으로 평가하고 가장 유용한 분할을 찾을 수 있습니다. 원-핫 인코딩을 적용하면 각 범주가 별도의 변수로 분리되므로, 트리 모델이 각 범주의 정보를 통합적으로 평가하는 데 어려움이 있을 수 있습니다.
- 차원의 저주와 효율성: 원-핫 인코딩은 특히 범주의 수가 많을 때 변수의 수를 크게 증가시킵니다. 이로 인해 모델의 학습 속도와 메모리 사용량이 증가하며, 효율성이 저하될 수 있습니다. LightGBM은 특히 대용량 데이터를 효율적으로 처리하는 데 강점을 가지고 있기 때문에, 불필요하게 차원을 증가시키는 것을 피하는 것이 좋습니다.
- 과적합 방지: 원-핫 인코딩으로 생성된 많은 수의 이진 변수들은 트리 모델이 불필요하게 깊어지고 복잡해지게 만들 수 있습니다. 이는 과적합을 유발할 수 있으며, 특히 훈련 데이터에는 잘 맞지만 실제 새로운 데이터에는 잘 일반화되지 않는 문제를 일으킬 수 있습니다.
- 내장된 범주형 처리: LightGBM은 범주형 변수를 내부적으로 최적으로 처리할 수 있는 기능을 제공합니다. 이 알고리즘은 범주형 변수를 자동으로 인식하고, 효율적인 방식으로 이를 처리하여 최적의 분할을 찾습니다. 사용자가 categorical_feature 매개변수를 통해 모델에 범주형 변수를 명시할 수 있으며, 이 경우 LightGBM이 범주형 변수를 최적화하여 처리합니다.
따라서, LightGBM을 사용할 때는 원-핫 인코딩보다는 모델의 내장 기능을 활용하여 범주형 변수를 처리하는 것이 더 권장되며, 이는 모델의 성능과 효율성을 모두 개선할 수 있는 방법입니다.
3. 그럼 트리형 알고리즘은 원 핫 인코딩을 사용 안 하나?
결정트리 알고리즘은 원-핫 인코딩을 사용하지 않아도 효과적으로 작동할 수 있습니다. 이는 결정트리와 유사한 트리 기반 모델들, 특히 랜덤 포레스트나 그라디언트 부스팅 모델들이 범주형 변수를 자연스럽게 처리할 수 있는 능력을 갖고 있기 때문입니다. 그럼에도 원-핫 인코딩의 사용 여부는 특정 상황과 목적에 따라 달라질 수 있습니다. 아래는 결정트리에서 원-핫 인코딩을 사용하지 않는 이유와 사용할 경우의 고려 사항을 설명합니다:
원-핫 인코딩을 사용하지 않는 이유
- 직관적인 분할: 결정트리는 범주형 변수를 기반으로 직관적으로 트리를 분할할 수 있습니다. 각 분할에서 트리는 범주의 각 값에 대해 최적의 분할 지점을 찾으려고 시도합니다. 이는 변수의 원래 의미를 유지하며 모델이 데이터를 이해하는 데 도움이 됩니다.
- 모델 복잡도: 원-핫 인코딩은 특히 범주 수가 많을 경우 변수의 차원을 크게 늘릴 수 있습니다. 이는 결정트리가 불필요하게 복잡해지게 하고, 특히 데이터의 양에 비해 변수가 너무 많으면 과적합을 유발할 수 있습니다.
- 효율성: 원-핫 인코딩은 추가적인 계산 비용을 발생시키고, 메모리 사용량도 증가시킵니다. 결정트리는 상대적으로 간단한 알고리즘인 만큼, 효율적인 데이터 처리가 중요할 수 있습니다.
원-핫 인코딩을 고려할 수 있는 경우
- 변수의 범주 수가 매우 적은 경우: 범주의 수가 매우 적은 경우, 원-핫 인코딩이 모델에 추가적인 정보를 제공하고 모델의 성능을 개선할 수 있습니다.
- 모델의 해석성: 특정 애플리케이션에서 모델의 해석성이 중요한 경우, 원-핫 인코딩은 각 범주가 결과에 미치는 독립적인 영향을 더 명확하게 파악할 수 있게 해줍니다.
결과적으로, 결정트리와 같은 트리 기반 모델을 사용할 때는 원-핫 인코딩을 사용하기보다는 모델이 자체적으로 범주형 변수를 효과적으로 처리할 수 있도록 내버려 두는 것이 일반적으로 더 바람직합니다. 이는 모델의 성능, 효율성, 그리고 종종 해석성 면에서도 유리할 수 있습니다.
4. 그럼 선형회귀는?
선형 회귀 모델에서는 범주형 변수를 다루는 방식이 트리 기반 모델과 다릅니다. 선형 회귀 모델은 범주형 변수의 원본 형태를 직접적으로 처리할 수 없기 때문에, 이 변수들을 수치형으로 변환하는 과정이 필요합니다. 원-핫 인코딩은 선형 회귀에서 범주형 변수를 처리하는 가장 흔하고 효과적인 방법 중 하나입니다. 다음은 원-핫 인코딩을 선형 회귀에 사용하는 주요 이유입니다:
- 수치화: 선형 회귀는 수치적 데이터를 기반으로 작동합니다. 원-핫 인코딩을 통해 각 범주가 독립 변수로 변환되어 선형 회귀 모델에 포함될 수 있습니다. 이 방법으로 각 범주의 독립적인 효과를 모델링할 수 있습니다.
- 더미 변수 트랩 방지: 원-핫 인코딩을 할 때 주의할 점은 더미 변수 트랩입니다. 이는 두 개 이상의 더미 변수 간에 완벽한 다중공선성이 생기는 현상을 말합니다. 하나의 범주를 기준 범주로 설정하고 나머지 범주에 대해서만 더미 변수를 생성함으로써 이를 방지할 수 있습니다.
- 모델 해석 용이: 원-핫 인코딩을 사용하면 각 범주가 결과 변수에 미치는 영향을 분리하여 측정할 수 있습니다. 이는 모델의 해석을 돕고, 특정 범주가 결과에 미치는 영향을 명확하게 이해할 수 있게 합니다.
- 선형 관계의 가정: 선형 회귀는 입력 변수와 출력 변수 간의 선형 관계를 가정합니다. 원-핫 인코딩을 통해 범주형 변수는 이러한 선형 구조에 잘 맞도록 변환될 수 있습니다.
원-핫 인코딩을 적용할 때는 데이터의 특성과 모델의 요구사항을 고려해야 합니다. 예를 들어, 범주의 수가 매우 많은 경우, 원-핫 인코딩은 차원의 저주를 초래하고 모델의 성능을 저하시킬 수 있습니다. 이러한 경우에는 다른 인코딩 기법(예: 라벨 인코딩, 평균 인코딩, 이진 인코딩 등)을 고려할 수도 있습니다. 항상 데이터의 특성과 모델의 목적에 맞는 적절한 인코딩 방법을 선택하는 것이 중요합니다.