HSEOM GeckoHSEOM
Instagram

흑섬 TECH 블로그 - 데이터 기반 브리딩 기술

레오파드게코 브리딩에 데이터 분석과 AI 기술을 접목합니다. Python, NumPy를 활용한 체중 관리, 성장 추이 분석, 환경 데이터 시각화 등 실무에서 직접 사용하는 기술을 일반인도 이해하기 쉽게 설명합니다.

주요 카테고리

AI 카테고리

AI와 머신러닝을 활용한 레오파드게코 브리딩 기술과 데이터 분석 방법을 공유합니다.

28개의 글이 있습니다.

[머신러닝 실전 1편] 통신사 고객 7천 명, 누가 떠날까 — EDA로 단서 찾기

처음 데이터를 받았을 때 바로 모델부터 돌렸습니다. 정확도 79%에 좋아했는데.. 자세히 보니 이탈 고객을 거의 못 잡고 있었습니다. 원인을 찾다 보니 데이터 자체에 문제가 있었습니다. 그때 깨달았습니다 — 모델 전에 데이터를 먼저 봐야 한다는 것을. 제가 했던 실수를 여러분은 안 하시도록, Telco Churn 데이터 7,043명을 직접 뜯어보면서 EDA가 왜 필수인지 확인합니다.

카테고리: AI

작성일: 2026-03-09T09:00:00

예상 읽기 시간: 18

Back to Tech
AI·18min read·

[머신러닝 실전 1편] 통신사 고객 7천 명, 누가 떠날까 — EDA로 단서 찾기

처음 데이터를 받았을 때 바로 모델부터 돌렸습니다. 정확도 79%에 좋아했는데.. 자세히 보니 이탈 고객을 거의 못 잡고 있었습니다. 원인을 찾다 보니 데이터 자체에 문제가 있었습니다. 그때 깨달았습니다 — 모델 전에 데이터를 먼저 봐야 한다는 것을. 제가 했던 실수를 여러분은 안 하시도록, Telco Churn 데이터 7,043명을 직접 뜯어보면서 EDA가 왜 필수인지 확인합니다.

시작하며 — 제가 했던 실수부터 말씀드릴게요

솔직히 말하면, 저는 머신러닝을 배우면서 꽤 큰 실수를 했습니다.ㅋㅋ
k-NN, 결정 트리, SVM, XGBoost... 기초/중급 시리즈를 거치면서 도구를 안다고 생각했습니다.
그래서 실제 데이터를 받자마자 바로 모델부터 돌려버렸습니다.

결과는요? 정확도 79%. "오, 나쁘지 않은데?" 싶었는데..
자세히 뜯어보니 이탈 고객을 거의 못 잡고 있었습니다.
원인을 파헤쳐보니 — 데이터 자체에 결측치가 있었고, 숫자여야 할 컬럼이 문자열이었습니다.

그때서야 깨달았습니다. 모델을 돌리기 전에 데이터를 먼저 들여다봐야 한다는 것을요.
이걸 EDA(Exploratory Data Analysis, 탐색적 데이터 분석)라고 합니다.
이번 실전 시리즈에서는 제가 했던 실수를 여러분은 안 하시도록, 하나의 데이터셋을 가지고 처음부터 끝까지 완성하는 과정을 함께 진행합니다.


오늘의 데이터 — 통신사 고객 이탈(Churn)

이번에 사용할 데이터는 Kaggle의 Telco Customer Churn 데이터셋입니다.
통신사 고객 7,043명의 정보가 담겨 있고, 핵심 질문은 하나입니다.

"이 고객이 이탈할 것인가, 유지할 것인가?"

7,043명 × 21개 변수 — 성별, 계약 유형, 월 요금, 재직 기간 등
타겟: Churn (Yes / No)

이 데이터셋을 고른 이유가 있습니다.
범주형(성별, 계약유형)과 수치형(요금, 재직기간)이 섞여 있고,
결측치도 있고, 불균형도 있어서 — 실전에서 맞닥뜨리는 문제를 전부 연습할 수 있습니다.
처음 이 데이터를 열어봤을 때 "이거 하나면 시리즈 전체를 커버할 수 있겠다" 싶었습니다.


EDA 프로세스 — 데이터를 받으면 제일 먼저

EDA 프로세스 흐름도 — 데이터 로드 → 기본 통계 → 분포 확인 → 상관관계 → 이상치 탐색

EDA는 거창한 게 아닙니다. 데이터를 이해하기 위한 탐색 과정입니다.
저도 처음에는 "그냥 head() 한번 찍어보면 되는 거 아니야?" 했는데,
위 5단계를 순서대로 밟아보니까 head()만으로는 절대 못 잡는 문제들이 보였습니다.

import pandas as pd
import numpy as np

# 데이터 로드
df = pd.read_csv('telco_churn.csv')

# 1단계: 기본 정보 확인
print(f"데이터 크기: {df.shape}")     # (7043, 21)
print(f"컬럼 타입:\n{df.dtypes}")
print(f"\n결측치:\n{df.isnull().sum()[df.isnull().sum() > 0]}")
print(f"\n상위 5행:\n{df.head()}")
여기서 바로 문제가 하나 보였습니다.
TotalCharges의 타입이 object(문자열)입니다. 숫자여야 하는데..
알고 보니 빈 문자열(" ")이 섞여 있어서 자동으로 숫자 변환이 안 된 것이었습니다.
처음에 이걸 모르고 그냥 넘어갔다가 나중에 모델이 에러를 뱉었습니다. 이런 걸 EDA 단계에서 잡아야 합니다.
# TotalCharges 타입 문제 확인
print(df['TotalCharges'].dtype)  # object ← 숫자여야 하는데!

# 빈 문자열 찾기
empty = df[df['TotalCharges'] == ' ']
print(f"빈 값 개수: {len(empty)}개")  # 11개

# 숫자로 변환 (빈 값 → NaN)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
print(f"결측치: {df['TotalCharges'].isnull().sum()}개")  # 11개

타겟 분포 — 이탈이 얼마나 되나?

고객 이탈 현황 — 전체 7,043명 중 유지 73.5%, 이탈 26.5% / 계약 유형별 이탈률

전체 7,043명 중 이탈은 1,869명으로 26.5%였습니다.
처음에 이 숫자를 보고 "4명 중 1명이 떠난다고?" 싶었는데 — 이게 왜 중요하냐면,

모델이 "전부 유지"라고 찍기만 해도 정확도가 73.5%가 나옵니다.
기초 6편에서 다뤘던 문제와 같습니다 — 정확도만 보면 안 됩니다.
제가 처음에 정확도 79%에 좋아했던 게 바로 이 함정이었습니다. 이탈 고객을 얼마나 잡아내는지(Recall)가 더 중요합니다.

그리고 오른쪽 차트를 보면 더 흥미로운 게 나옵니다.
월별 계약 고객의 이탈률이 42.7%로 압도적입니다.
1년 계약은 11.3%, 2년 계약은 2.8%에 불과합니다.
아직 모델을 안 만들었는데도 계약 유형이 이탈을 예측하는 강력한 단서라는 게 바로 보입니다. 이게 EDA의 힘입니다.

# 타겟 분포 확인
print(df['Churn'].value_counts())
# No     5174 (73.5%)
# Yes    1869 (26.5%)

# 계약 유형별 이탈률
contract_churn = df.groupby('Contract')['Churn'].apply(
    lambda x: (x == 'Yes').mean() * 100
)
print(contract_churn)
# Month-to-month    42.71%
# One year          11.27%
# Two year           2.83%

수치형 변수 — tenure와 월 요금

재직 기간(tenure) 히스토그램 — 이탈 고객은 1~6개월에 집중 / 월 요금 boxplot — 이탈 고객이 더 높은 요금

이 그래프를 처음 봤을 때 "아, 이거다" 싶었습니다.
왼쪽 히스토그램을 보면 이탈 고객(빨강)이 재직 기간 1~6개월에 확 몰려 있습니다.
반대로 유지 고객(민트)은 70개월 근처에 큰 봉우리가 있습니다.

해석하면 — 가입 초기 6개월이 고비라는 것입니다.
여기를 넘기면 장기 고객이 될 가능성이 높고, 못 넘기면 떠납니다.
통신사 입장에서는 "온보딩 6개월"에 집중 투자해야 한다는 인사이트가 나옵니다. 실제로 이런 분석이 마케팅 전략의 근거가 됩니다.

오른쪽 boxplot은 월 요금 비교입니다.
이탈 고객의 중앙값이 $80, 유지 고객은 $64입니다.
월 요금이 높을수록 이탈 확률이 올라갑니다. 비싼 요금제를 쓰는 고객일수록 "이 돈 내고 이 서비스?" 하면서 불만이 쌓이는 건 아닌지.. 직관적으로도 말이 됩니다.

# 이탈/유지별 tenure 통계
print(df.groupby('Churn')['tenure'].describe())
# 이탈 고객 평균: 17.98개월
# 유지 고객 평균: 37.57개월

# 이탈/유지별 월 요금 통계
print(df.groupby('Churn')['MonthlyCharges'].median())
# No     64.43
# Yes    79.65

Jupyter Notebook 실행 결과

Jupyter 셀 1 — 수치형 변수 EDA 코드 Jupyter 실행 결과 — Tenure, Monthly Charges, Total Charges 히스토그램 + 산점도

범주형 변수 — 서비스별 이탈률

서비스 유형별 이탈률 비교 — 인터넷 서비스, 결제 방법, 고령자 여부

이번에는 범주형 변수를 봤는데, 여기서도 이탈률에 꽤 큰 차이가 나타났습니다.

인터넷 서비스 — Fiber optic 사용자의 이탈률이 41.9%로 가장 높음. DSL은 19.0%

결제 방법 — 전자 수표(Electronic check) 사용자가 45.3%로 압도적. 자동이체 계열은 15~19%

고령자 여부 — 고령자 이탈률 41.7%, 일반 23.6%. 거의 2배 차이

솔직히 Fiber optic 이탈률이 높은 건 처음에 이해가 안 됐습니다. 좋은 서비스인데 왜?
곰곰이 생각해보니 고속 인터넷일수록 월 요금이 높고 → 높은 요금에 대한 기대치가 높아지면서 불만으로 이어지는 것 같았습니다.
전자 수표 사용자의 이탈률이 높은 것도 재밌는 부분인데 — 자동이체가 아니라 매번 수동 결제하는 고객이니까 "해지 허들"이 낮은 것입니다. 이런 해석을 데이터에서 끌어내는 게 EDA의 재미입니다.ㅎㅎ

# 범주형 변수별 이탈률 한 번에 확인
cat_cols = ['gender', 'SeniorCitizen', 'Partner', 'Dependents',
            'InternetService', 'Contract', 'PaymentMethod']

for col in cat_cols:
    churn_rate = df.groupby(col)['Churn'].apply(
        lambda x: (x == 'Yes').mean() * 100
    )
    print(f"\n[{col}]")
    print(churn_rate.round(1))

Jupyter Notebook 실행 결과

Jupyter 셀 2 — 범주형 변수 이탈률 분석 코드 Jupyter 실행 결과 — 계약유형, 인터넷서비스, 결제방법, 고령자, 파트너, 부양가족별 이탈률

상관관계 — 숫자로 확인하는 변수 간 관계

수치형 변수 상관관계 히트맵 — 재직기간과 이탈의 음의 상관(-0.35)

마지막으로 수치형 변수끼리의 상관관계를 히트맵으로 그려봤습니다.
여기서 앞에서 느낌적으로 봤던 패턴들이 숫자로 확인됩니다.

재직기간 ↔ 이탈: -0.35 — 오래 머물수록 이탈 확률 낮음 (가장 강한 음의 상관)

월요금 ↔ 이탈: +0.19 — 요금이 높을수록 이탈 확률 높음

재직기간 ↔ 총요금: +0.83 — 당연한 관계. 오래 있으면 총 지불액이 큼

월요금 ↔ 총요금: +0.65 — 다중공선성 주의 (중급 6편 PCA 참고)
# 상관관계 히트맵
df_num = df[['tenure', 'MonthlyCharges', 'TotalCharges', 'SeniorCitizen']].copy()
df_num['Churn'] = (df['Churn'] == 'Yes').astype(int)

corr = df_num.corr()
print(corr['Churn'].sort_values())
# tenure           -0.352
# TotalCharges     -0.198
# SeniorCitizen     0.151
# MonthlyCharges    0.193

그런데 여기서 하나 찝찝했던 게 있었습니다.
tenure와 TotalCharges의 상관관계가 0.83으로 매우 높습니다.
"이 두 변수를 그냥 같이 넣어도 되나..?" 싶었는데, 이건 3편(피처 엔지니어링)에서 다중공선성 처리할 때 다시 돌아올 내용입니다.


EDA에서 발견한 것들 — 정리

EDA 핵심 발견 4가지 요약 — 이탈률 26.5%, 월별 계약 고위험, 신규 고객 이탈 집중, 결측치 11개
1. 이탈률 26.5% — 불균형 데이터. 정확도 대신 Recall/F1으로 평가해야 함
2. 핵심 이탈 변수 — 계약 유형, 재직 기간, 월 요금, 결제 방법, 인터넷 서비스
3. 데이터 문제 — TotalCharges에 결측치 11개 + 타입 변환 필요

이게 EDA입니다. 복잡한 통계 기법이 아니라,
데이터를 눈으로 확인하고, 문제와 패턴을 먼저 파악하는 과정입니다.
저처럼 이 단계를 건너뛰고 모델부터 돌리면.. 나중에 반드시 뒤통수를 맞습니다.ㅋㅋ

다음 편(2편)에서는 여기서 발견한 문제들을 직접 처리합니다.
TotalCharges 결측치 11개를 어떻게 할지, 이상치는 어떻게 다룰지 —
결측치와 이상치 실전 처리, 같이 확인해봅시다.

#EDA#탐색적데이터분석#데이터시각화#pandas#python