아이티윌_데이터 분석 55기/강의내용 필기_분석(Python)

#6 교호작용 및 효과 검증

ecosso 2026. 6. 9. 18:18

[ 과제5. 교호작용 효과 검증 ]

# 1. 데이터 로딩 및 설명
import pandas as pd
pd.set_option('display.max_column', None)
df = pd.read_csv('telecom_churn.csv')

 

# X, y 분리 / 노이즈 변수 제거
X = df.drop(columns=['churn','phone number'])
y = df['churn']

# 변수 추가
X['total minutes'] = X.loc[:, X.columns.str.contains('minutes')].sum(axis=1)   # 총 통화시간
X['total calls'] = X.loc[:, X.columns.str.contains('calls')].sum(axis=1)       # 총 통화건수
X['total charge'] = X.loc[:, X.columns.str.contains('charge')].sum(axis=1)     # 총 요금

# 노이즈 변수 제거
X = X.drop(columns=['state','area code','total eve calls','total calls'])


# 2. 라벨 인코딩
obj_cols = X.select_dtypes(include='object').columns               # 문자형 컬럼만 선택 -> 라벨인코딩
num_cols = X.select_dtypes(exclude='object').columns            # 숫자형 컬럼만 선택 -> 스케일링

from sklearn.preprocessing import LabelEncoder
for colname in obj_cols:
    m_ec = LabelEncoder()
    X[colname] = m_ec.fit_transform(X[colname])                 # X에서 문자형 컬럼을 라벨인코딩 변환 후 바로 덮어쓰기
   


# 3. 스케일링
from sklearn.preprocessing import MinMaxScaler
X_sc = X.copy()                                                 # X와 동일한 X_sc copy 데이터 생성
for colname in num_cols:
    m_sc = MinMaxScaler()
    X_sc[colname] = m_sc.fit_transform(X_sc[[colname]])        


# 4. 교호작용 추가
X_poly = X.copy()
X_sc_poly = X_sc.copy()

X_poly['intl_plan_x_charge'] = X['international plan'] * X['total intl charge']

X_sc_poly['intl_plan_x_charge'] = X['international plan'] * X['total intl charge']
m_sc = MinMaxScaler()
X_sc_poly['intl_plan_x_charge'] = m_sc.fit_transform(X_sc_poly[['intl_plan_x_charge']])       


# 5. train/test 분리
from sklearn.model_selection import train_test_split
train_x, test_x, train_x_sc, test_x_sc, train_x_poly, test_x_poly, train_x_sc_poly, test_x_sc_poly, train_y, test_y = train_test_split(X, X_sc, X_poly, X_sc_poly, y, random_state=0)


# 6. 모델링 비교
# 5-1) 랜덤포레스트
from sklearn.ensemble import RandomForestClassifier
m_rf = RandomForestClassifier(random_state=0)
m_rf.fit(train_x,train_y)
print('RF 훈련 원본 점수 :', m_rf.score(train_x,train_y))
print('RF 평가 원본 점수 :', m_rf.score(test_x,test_y))

m_rf = RandomForestClassifier(random_state=0)
m_rf.fit(train_x_poly,train_y)
print('RF 훈련(교호작용) 원본 점수 :', m_rf.score(train_x_poly,train_y))
print('RF 평가(교호작용) 원본 점수 :', m_rf.score(test_x_poly,test_y))


# 5-2) SVM
from sklearn.svm import SVC
m_svm = SVC()
m_svm.fit(train_x_sc,train_y)
print('SVM 훈련 원본 점수 :', m_svm.score(train_x_sc,train_y))
print('SVM 평가 원본 점수 :', m_svm.score(test_x_sc,test_y))

m_svm = SVC()
m_svm.fit(train_x_sc_poly,train_y)
print('RF 훈련(교호작용) 원본 점수 :', m_svm.score(train_x_sc_poly,train_y))
print('RF 평가(교호작용) 원본 점수 :', m_svm.score(test_x_sc_poly,test_y))

 

RF 훈련 원본 점수 : 1.0
RF 평가 원본 점수 : 0.9808153477218226
RF 훈련(교호작용) 원본 점수 : 1.0
RF 평가(교호작용) 원본 점수 : 0.9820143884892086
SVM 훈련 원본 점수 : 0.9279711884753902
SVM 평가 원본 점수 : 0.935251798561151
RF 훈련(교호작용) 원본 점수 : 0.9263705482192878
RF 평가(교호작용) 원본 점수 : 0.9376498800959233

 ▲ 결론) 국제전화 가입여부 X 국제전화 요금의 교호작용 효과가 있는 것으로 확인되었다.

  국제전화 요금제를 가입한 고객 중 요금이 올라갈수록 이탈율이 올라갈 것으로 예상된다.


과제6) 세그먼트 분석 (이탈)

# 국제전화 요금제에 가입한 대상에 대한 추가 연구

# Step1) 국제전화 요금제에 가입한 대상만 추출

df_yes = df.loc[df['international plan'] == 'yes', :]

df
Out[45]: 
     state  account length  area code phone number international plan  \
0       KS             128        415     382-4657                 no   
1       OH             107        415     371-7191                 no   
2       NJ             137        415     358-1921                 no   
3       OH              84        408     375-9999                yes   
4       OK              75        415     330-6626                yes   
   ...             ...        ...          ...                ...   
3328    AZ             192        415     414-4276                 no   
3329    WV              68        415     370-3271                 no   
3330    RI              28        510     328-8230                 no   
3331    CT             184        510     364-6381                yes   
3332    TN              74        415     400-4344                 no   

     voice mail plan  number vmail messages  total day minutes  \
0                yes                     25              265.1   
1                yes                     26              161.6   
2                 no                      0              243.4   
3                 no                      0              299.4   
4                 no                      0              166.7   
             ...                    ...                ...   
3328             yes                     36              156.2   
3329              no                      0              231.1   
3330              no                      0              180.8   
3331              no                      0              213.8   
3332             yes                     25              234.4   

      total day calls  total day charge  total eve minutes  total eve calls  \
0                 110             45.07              197.4               99   
1                 123             27.47              195.5              103   
2                 114             41.38              121.2              110   
3                  71             50.90               61.9               88   
4                 113             28.34              148.3              122   
              ...               ...                ...              ...   
3328               77             26.55              215.5              126   
3329               57             39.29              153.4               55   
3330              109             30.74              288.8               58   
3331              105             36.35              159.6               84   
3332              113             39.85              265.9               82   

      total eve charge  total night minutes  total night calls  \
0                16.78                244.7                 91   
1                16.62                254.4                103   
2                10.30                162.6                104   
3                 5.26                196.9                 89   
4                12.61                186.9                121   
               ...                  ...                ...   
3328             18.32                279.1                 83   
3329             13.04                191.3                123   
3330             24.55                191.9                 91   
3331             13.57                139.2                137   
3332             22.60                241.4                 77   

      total night charge  total intl minutes  total intl calls  \
0                  11.01                10.0                 3   
1                  11.45                13.7                 3   
2                   7.32                12.2                 5   
3                   8.86                 6.6                 7   
4                   8.41                10.1                 3   
                 ...                 ...               ...   
3328               12.56                 9.9                 6   
3329                8.61                 9.6                 4   
3330                8.64                14.1                 6   
3331                6.26                 5.0                10   
3332               10.86                13.7                 4   

      total intl charge  customer service calls  churn  
0                  2.70                       1  False  
1                  3.70                       1  False  
2                  3.29                       0  False  
3                  1.78                       2  False  
4                  2.73                       3  False  
                ...                     ...    ...  
3328               2.67                       2  False  
3329               2.59                       3  False  
3330               3.81                       2  False  
3331               1.35                       2  False  
3332               3.70                       0  False  

[3333 rows x 21 columns]

 

# Step2) DT를 사용한 요금구간 찾기 (max_depth = 1)

pd.cut?

Signature:
pd.cut(
    x,
    bins,
    right: 'bool' = True,
    labels=None,
    retbins: 'bool' = False,
    precision: 'int' = 3,
    include_lowest: 'bool' = False,
    duplicates: 'str' = 'raise',
    ordered: 'bool' = True,
)
Docstring:
Bin values into discrete intervals.

 

*** 구간화 (수치형 -> 범주형)

# 방법1) 직접 조건별로 구간 설정

# 방법2) pd.cut

pd.cut(                                     
       x,                                   # 대상
       bins,                                # 구간값 (구간 수 또는 구간 경계 전달 가능)
       right: 'bool' = True,                # 오른쪽 닫힘 여부
       labels = None,                       # 각 구간 이름
       include_lowest : 'bool' = False,     # 최솟값을 첫 번째 구간에 포함시킬지 여부
       )

import numpy as np
a1 = np.arange(1,101)

 

# 4개 그룹으로 분할
pd.cut(a1, 4)  # ~초과, ~ 이하

pd.cut(a1, 4)  # ~초과, ~ 이하
Out[51]: 
[(0.901, 25.75], (0.901, 25.75], (0.901, 25.75], (0.901, 25.75], (0.901, 25.75], ..., (75.25, 100.0], (75.25, 100.0], (75.25, 100.0], (75.25, 100.0], (75.25, 100.0]]
Length: 100
Categories (4, interval[float64, right]): [(0.901, 25.75] < (25.75, 50.5] < (50.5, 75.25] <
                                           (75.25, 100.0]]

 

# 4개 그룹에 이름을 부여

pd.cut(a1, 4, labels = ['A', 'B', 'C', 'D'])

pd.cut(a1, 4, labels = ['A', 'B', 'C', 'D'])
Out[52]: 
['A', 'A', 'A', 'A', 'A', ..., 'D', 'D', 'D', 'D', 'D']
Length: 100
Categories (4, object): ['A' < 'B' < 'C' < 'D']

 

# 각 구간의 경계를 직접 설정 (1 초과 25 이하)

pd.cut(a1, [1, 25, 50, 75, 100])

pd.cut(a1, [1, 25, 50, 75, 100])
Out[53]: 
[NaN, (1.0, 25.0], (1.0, 25.0], (1.0, 25.0], (1.0, 25.0], ..., (75, 100], (75, 100], (75, 100], (75, 100], (75, 100]]
Length: 100
Categories (4, interval[int64, right]): [(1, 25] < (25, 50] < (50, 75] < (75, 100]]

 

▲ 첫 번째 그룹에 1이 포함되지 않는 문제가 발생한다.

 

pd.cut(a1, [0, 25, 50, 75, 100])

pd.cut(a1, [0, 25, 50, 75, 100])
Out[54]: 
[(0, 25], (0, 25], (0, 25], (0, 25], (0, 25], ..., (75, 100], (75, 100], (75, 100], (75, 100], (75, 100]]
Length: 100
Categories (4, interval[int64, right]): [(0, 25] < (25, 50] < (50, 75] < (75, 100]]

 

# 데이터 나누기

charge_bins = pd.cut(df['total intl charge'], bins = 3)
df.groupby(['international plan', charge_bins], observed = True)['churn'].agg(['count','mean'])

df.groupby(['international plan', charge_bins], observed = True)['churn'].agg(['count','mean'])
Out[58]: 
                                      count      mean
international plan total intl charge                 
no                 (-0.0054, 1.8]       306  0.104575
                   (1.8, 3.6]          2356  0.113328
                   (3.6, 5.4]           348  0.135057
yes                (-0.0054, 1.8]        24  0.250000
                   (1.8, 3.6]           251  0.330677
                   (3.6, 5.4]            48  1.000000

 

▲ 국제전화 요금제에 가입하지 않은 사람(no)은 일반적인 국제전화비가 증가해도 상대적으로 민감하지 않다.

     국제전화 요금제에 가입한 사람(yes)은 일반적인 국제전화비가 증가하면 이탈률이 올라간다.

 

from sklearn.tree import DecisionTreeClassifier
m_dt = DecisionTreeClassifier(max_depth=1, random_state=0)
m_dt.fit(df[['total intl charge']], df['churn'])
threshold = m_dt.tree_.threshold[0]

m_dt.tree_.threshold[0]
Out[76]: np.float64(3.549999952316284)

 

# Step3) 위에서 찾은 임계값으로 집단을 나눈 뒤 각각의 이탈율을 확인
high = df_yes.loc[df_yes['total intl charge'] >= threshold, 'churn']
low = df_yes.loc[df_yes['total intl charge'] <= threshold, 'churn']

print('요금이 3.549 이상인 고객 수 :', len(high))
print('요금이 3.549 이상인 고객 이탈율 :', high.mean())

high = df_yes.loc[df_yes['total intl charge'] >= threshold, 'churn']
low = df_yes.loc[df_yes['total intl charge'] <= threshold, 'churn']

print('요금이 3.549 이상인 고객 수 :', len(high))
print('요금이 3.549 이상인 고객 이탈율 :', high.mean())
요금이 3.549 이상인 고객 수 : 57
요금이 3.549 이상인 고객 이탈율 : 1.0

 

# 모자이크 플롯

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Patch

groups = {
    f'low\n(< {threshold:.2f})':   low,
    f'high\n(>= {threshold:.2f})': high,
}

total_n = len(df_yes)
gap = 0.02

fig, ax = plt.subplots(figsize=(8, 5))

x_cursor = 0
for label, series in groups.items():
    n = len(series)
    churn_rate = series.mean()
    width = (n / total_n) - gap

    ax.add_patch(Rectangle((x_cursor, 0), width, churn_rate,
                            color='#E05C5C', ec='white', lw=1.5))
    ax.add_patch(Rectangle((x_cursor, churn_rate), width, 1 - churn_rate,
                            color='#5B8DB8', ec='white', lw=1.5))

    if churn_rate > 0.05:
        ax.text(x_cursor + width/2, churn_rate/2,
                f'{churn_rate:.1%}', ha='center', va='center',
                color='white', fontweight='bold', fontsize=11)
    else:
        ax.text(x_cursor + width/2, churn_rate + 0.04,
                f'{churn_rate:.1%}', ha='center', va='bottom',
                color='#E05C5C', fontweight='bold', fontsize=11)

    if (1 - churn_rate) > 0.05:
        ax.text(x_cursor + width/2, churn_rate + (1 - churn_rate)/2,
                f'{1-churn_rate:.1%}', ha='center', va='center',
                color='white', fontweight='bold', fontsize=11)
    else:
        ax.text(x_cursor + width/2, churn_rate - 0.04,
                f'{1-churn_rate:.1%}', ha='center', va='top',
                color='#5B8DB8', fontweight='bold', fontsize=11)

    ax.text(x_cursor + width/2, -0.07,
            f'{label}\nn={n}', ha='center', va='top', fontsize=10)

    x_cursor += width + gap

ax.set_xlim(0, 1)
ax.set_ylim(-0.15, 1.05)
ax.set_xticks([])
ax.set_yticks([0, 0.25, 0.5, 0.75, 1.0])
ax.set_yticklabels(['0%', '25%', '50%', '75%', '100%'])
ax.set_ylabel('Churn Rate', fontsize=11)
ax.set_title(f'Mosaic Plot — International Plan (threshold: {threshold:.2f})',
             fontsize=12, fontweight='bold', pad=15)

legend = [Patch(color='#E05C5C', label='Churn (1)'),
          Patch(color='#5B8DB8', label='Stay (0)')]
ax.legend(handles=legend, fontsize=10,
          bbox_to_anchor=(1.02, 1.0), loc='upper left', borderaxespad=0)

ax.spines[['top', 'right', 'bottom']].set_visible(False)
plt.tight_layout()
plt.show()