은행의 전화 마케팅에 대해 고객의 반응 여부에 대해서 여러가지 질문에 따라서 분석해 보는 것입니다.
상당히 흥미롭고, 추리를 하지 않고, 모의고사처럼 질문하는 것에 대한 답을 하면 된다는 것에 아, 이렇게도 학습할 수 있구나 생각이 들었습니다.
분석을 재현!
원 분석은 아래와 같고,
Reference Analysis
모의고사 1회차 — DataManim
https://www.datamanim.com/dataset/practice/q1.html
Data Location : https://raw.githubusercontent.com/Datamanim/datarepo/main/bank/train.csv
해결해야 할 질문들은 아래와 같습니다.
Q01) 마케팅 응답 고객들의 나이를 10살 단위로 변환 했을 때, 가장 많은 인원을 가진 나이대는? (0~9 : 0 , 10~19 : 10)
Q02) 마케팅 응답 고객들의 나이를 10살 단위로 변환 했을 때, 가장 많은 나이대 구간의 인원은 몇명인가?
Q03) 나이가 25살 이상 29살 미만인 응답 고객들중 housing컬럼의 값이 yes인 고객의 수는?
Q04) numeric한 값을 가지지 않은 컬럼들중 unique한 값을 가장 많이 가지는 컬럼은?
Q05) balance 컬럼값들의 평균값 이상을 가지는 데이터를 ID값을 기준으로 내림차순 정렬했을때 상위 100개 데이터의 balance값의 평균은?
Q06) 가장 많은 광고를 집행했던 날짜는 언제인가? (데이터 그대로 일(숫자),달(영문)으로 표기)
Q07) 데이터의 job이 unknown 상태인 고객들의 age 컬럼 값의 정규성을 검정하고자 한다. 샤피로 검정의 p-value값을 구하여라
Q08) age와 balance의 상관계수를 구하여라
Q09) 응답 y 변수와 education 변수는 독립인지 카이제곱검정을 통해 확인하려한다. p-value값을 출력하라
Q10) 각 job에 따라 divorced/married 인원의 비율을 확인 했을 때 그 값이 가장 높은 값은?
Q11) 예측 모형을 만들어라.
ㅎㅎ 꽤나 질문이 많군요. 일단 질문들을 보면, 전체적인 줄거리로는
마케팅 응답에 대한 가장 많은 고객에 대한 분석, 카테고리 Feature에 대해서 Unique값을에 대한 분석, 누가 예금을 많이 했는지? 언제 마케팅을 많이 했는지? 예금과 나머지 Feature들의 상관성등이 궁금한가 봅니다. 뭐, 궁금하다니까 한번 해 보죠 머.
일단~! 데이터를 다운로드하고 보자
!wget https://raw.githubusercontent.com/Datamanim/datarepo/main/bank/train.csv
--2023-07-18 21:15:36-- https://raw.githubusercontent.com/Datamanim/datarepo/main/bank/train.csv raw.githubusercontent.com (raw.githubusercontent.com) 해석 중... 185.199.111.133, 185.199.110.133, 185.199.108.133, ... 다음으로 연결 중: raw.githubusercontent.com (raw.githubusercontent.com)|185.199.111.133|:443... 연결했습니다. HTTP 요청을 보냈습니다. 응답 기다리는 중... 200 OK 길이: 1085379 (1.0M) [text/plain] 저장 위치: `train.csv.6' train.csv.6 100%[===================>] 1.03M --.-KB/s / 0.1s 2023-07-18 21:15:37 (9.04 MB/s) - `train.csv.6' 저장함 [1085379/1085379]
import pandas as pd
ixlsx = "train.csv"
df_raw = pd.read_csv(ixlsx)
df_raw.head()
ID | age | job | marital | education | default | balance | housing | loan | contact | day | month | campaign | pdays | previous | poutcome | y | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 13829 | 29 | technician | single | tertiary | no | 18254 | no | no | cellular | 11 | may | 2 | -1 | 0 | unknown | no |
1 | 22677 | 26 | services | single | secondary | no | 512 | yes | yes | unknown | 5 | jun | 3 | -1 | 0 | unknown | no |
2 | 10541 | 30 | management | single | secondary | no | 135 | no | no | cellular | 14 | aug | 2 | -1 | 0 | unknown | no |
3 | 13689 | 41 | technician | married | unknown | no | 30 | yes | no | cellular | 10 | jul | 1 | -1 | 0 | unknown | no |
4 | 11304 | 27 | admin. | single | secondary | no | 321 | no | yes | unknown | 2 | sep | 1 | -1 | 0 | unknown | no |
df_raw.info()
RangeIndex: 12870 entries, 0 to 12869 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ID 12870 non-null int64 1 age 12870 non-null int64 2 job 12870 non-null object 3 marital 12870 non-null object 4 education 12870 non-null object 5 default 12870 non-null object 6 balance 12870 non-null int64 7 housing 12870 non-null object 8 loan 12870 non-null object 9 contact 12870 non-null object 10 day 12870 non-null int64 11 month 12870 non-null object 12 campaign 12870 non-null int64 13 pdays 12870 non-null int64 14 previous 12870 non-null int64 15 poutcome 12870 non-null object 16 y 12870 non-null object dtypes: int64(7), object(10) memory usage: 1.7+ MB
Null은 없는 것 같군요? 아름다운 데이터네요.
이제는 눈감고도 데이터를 로드할 수 있겠죠.
자, 처음 질문부터 격파해 나갈까요?
Q01) 마케팅 응답 고객들의 나이를 10살 단위로 변환 했을 때, 가장 많은 인원을 가진 나이대는? (0~9 : 0 , 10~19 : 10)
Q02) 마케팅 응답 고객들의 나이를 10살 단위로 변환 했을 때, 가장 많은 나이대 구간의 인원은 몇명인가?
흐흐 그러면 일단 나이를 10살 단위로 변환해야하겠군요.
import math
df_raw['age'].apply(lambda row : (math.floor(row/10)*10)).value_counts()
30 5056 40 3198 50 2244 20 1638 60 460 70 193 80 57 10 18 90 6 Name: age, dtype: int64
보니까, 30대가 가장 많군요? 5056명이 응답했습니다. 왜 원래 마케팅 응답을 30대가 많이 했을까요? 출퇴근하는 직장인 비중이 높은 30대와 40대의 경우 주말에 비해 평일 응답률이 낮은 게 일반적이라는 것이 정설인데 말이죠. 그러면 주말에 이벤트를 한 것일까요? 흠.
Q03) 나이가 25살 이상 29살 미만인 응답 고객들중 housing컬럼의 값이 yes인 고객의 수는?
두 번째 질문은 뜬금없긴 한데, 일단 얼마나 되나 볼까요?
len(df_raw[(df_raw['age']>=25) & (df_raw['age']<29) & (df_raw['housing']=='yes')])
504
504명인데, 이게 어느 정도인지 감이 안오니까, 25~29세 사이의 사람이 몇명인지나 일단 보시죠. s
len(df_raw[(df_raw['age']>=25) & (df_raw['age']<29)])
1007
504/1007 # 전체 중 어느 정도?
0.5004965243296922
50% 정도가 housing이 Yes로군요? 오호. 은행으로서는 꽤나 흥미로운 결과가 되겠군요. 이게 어떤 마케팅이었는지도 설명이 있었다면 매우 흥미로운 결과를 추리할 수도 있겠습니다만.
어쨌든, 다음 질문
Q04) numeric한 값을 가지지 않은 컬럼들중 unique한 값을 가장 많이 가지는 컬럼은?
이라면, numeric한 column만 일단 찾아서 처리해 보자면
df_raw.info() # object들이 numeric하지 않은 Feature이다.
RangeIndex: 12870 entries, 0 to 12869 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ID 12870 non-null int64 1 age 12870 non-null int64 2 job 12870 non-null object 3 marital 12870 non-null object 4 education 12870 non-null object 5 default 12870 non-null object 6 balance 12870 non-null int64 7 housing 12870 non-null object 8 loan 12870 non-null object 9 contact 12870 non-null object 10 day 12870 non-null int64 11 month 12870 non-null object 12 campaign 12870 non-null int64 13 pdays 12870 non-null int64 14 previous 12870 non-null int64 15 poutcome 12870 non-null object 16 y 12870 non-null object dtypes: int64(7), object(10) memory usage: 1.7+ MB
dic_cols = {}
for column in df_raw.columns: # 각 컬럼을 돌면서
if df_raw[column].dtype!='int' : # 셀수 있는 것이 아닌 것들의
dic_cols.update({column: df_raw[column].nunique()}) # unique값들을 세고
dft_cols = pd.DataFrame().from_dict(dic_cols, orient='index').sort_values(by=0, ascending=False) # 그걸 데이터프레임으로 만듦
display(dft_cols.head)
max_index = dft_cols[0].max()
dft_cols[dft_cols[0]==max_index] # 동률을 다 찾아서 출력
0 | |
---|---|
job | 12 |
month | 12 |
Job이랑 Month가 12개로 제일 많은 종류를 자랑하는데요, month야 12개월이니까 12개이고요, Job은 12가지나 있네요. 머머 있는지나 한번 보고 지나갈까요?
df_raw['job'].value_counts()
management 2858 blue-collar 2571 technician 2141 admin. 1464 services 1043 retired 770 self-employed 454 unemployed 414 entrepreneur 383 student 358 housemaid 334 unknown 80 Name: job, dtype: int64
꽤 많네요. 그중에서도 management와 blue-collar가 상위를 랭크하는군요. 사회에서의 비율과 비슷한 것 같은데, 의외로 창업자가 꽤 되는군요?
자, 그러면 5번째 문제!
Q05) balance 컬럼값들의 평균값 이상을 가지는 데이터를 ID값을 기준으로 내림차순 정렬했을때 상위 100개 데이터의 balance값의 평균은?
자, 이걸 하고 싶다면, 일단 balance 컬럼의 평균값을 구하고, 그걸 ID기준으로 내림차순 정렬한 후에 뭔가 보면 되겠군요! ㅎㅎ
df_raw[df_raw['balance']>=df_raw['balance'].mean()].sort_values(by='ID', ascending=False)[:100]['balance'].mean()
3473.73
아니... 이걸 해서 알아낼 수 있는 정보가 음... 잘 모르겠네요? 왜 해보라는 건지..는 모르겠습니다만. 차라리 balance의 상위 100개를 보면 몰라도 ID를 상위 100로 보면. 뭘 알아낼 수가 있는 건지는.. 사실 모르겠습니다만, 어쨌든 이 값은 3473.73달러입니다. 얼마 되지 않는군요?
그러면 6번째! 문제를 격파!
Q06) 가장 많은 광고를 집행했던 날짜는 언제인가? (데이터 그대로 일(숫자),달(영문)으로 표기)
dft_max = df_raw.groupby(['month', 'day'])['ID'].count().sort_values(ascending=False)
display(dft_max)
dft_max.index[0]
month day may 15 301 14 283 13 257 7 239 nov 21 221 ... mar 19 1 oct 4 1 dec 16 1 sep 5 1 dec 30 1 Name: ID, Length: 303, dtype: int64
('may', 15)
흠~ 사실 질문이 좀 잘못되었는데, 광고를 집행한 횟수와 응답수는 비례할 수는 있지만, 꼭 응답수가 많다고 하여 집행 횟수가 많다고 할 수 없습니다. 질문이 좀 이상한 것 같긴 한데, 어쨌든 5월 15일이 제일 많았군요?
그러면 7번째 질문!
Q07) 데이터의 job이 unknown 상태인 고객들의 age 컬럼 값의 정규성을 검정하고자 한다. 샤피로 검정의 p-value값을 구하여라
햐. age만 가지고 정규성을 검정하는 것 이거 별거 아니죠~
from scipy import stats
dft_job_unknown_age = df_raw[df_raw['job']=='unknown']['age'].copy()
shapiro_test = stats.shapiro(dft_job_unknown_age)
shapiro_test
ShapiroResult(statistic=0.9784717559814453, pvalue=0.1961131989955902)
p value를 보니, 정규성을 만족하는군요? 이건 우리 많이 해봤잖아요? age가 실제로 어떤 분포인지 한번 볼까요?
dft_job_unknown_age.hist()
조금~ 좌우 대칭인 것이 대충~ 비슷해 보이기도 하는군요?
8번째 질문도 한번 해 보죠
Q08) age와 balance의 상관계수를 구하여라
넹.
df_raw['age'].corr(df_raw['balance'])
0.10198734763981504
해석을 해 보자면, 나이와 예금액이 선형관계를 갖기에는 너무 상관이 적습니다. ㅠㅠ 그래도 0.5는 될 줄 알았는데, 그렇지는 않군요.
그러면, 9번째 질문을 한번 볼까요?
Q09) 응답 y 변수와 education 변수는 독립인지 카이제곱검정을 통해 확인하려한다. p-value값을 출력하라
오, 이것은 꽤나 재미있는 도전이군요. 당연히 빈도표를 만들어야 하겠군요!
cross_table = pd.crosstab(df_raw['education'], df_raw['y'])
cross_table
y | no | yes |
---|---|---|
education | ||
primary | 1424 | 456 |
secondary | 4555 | 1813 |
tertiary | 2559 | 1516 |
unknown | 365 | 182 |
자, 이제 education에 따라서 답변이 달라지는지에 대한 독립성 검정입니닷
from scipy.stats import chi2_contingency
chi2 , p ,dof, expected = chi2_contingency(cross_table)
p
7.901201277473551e-29
어허~ p value를 보니까, 이거 독립이 아니군요? 그러니까 education이 응답에 영향을 미칩니다. 서로 독립이 아니라는 뜻입니다.
교육수준과 예금이 영향을 미치는 관계라는 건데, 이거 많이 흥미롭군요.
그러면, 10번 질문으로 가 볼까요!
Q10) 각 job에 따라 divorced/married 인원의 비율을 확인 했을 때 그 값이 가장 높은 값은? (groupby를 이용할 것)
job에 따라서 grouping을 하고, 그룹에 대해서 인원의 비율을 보고 값을 가져오면 되겠군요? 후후
그 전에 divoced/marred가 어디 있는건가.. marital에 있나 봅니다. 확인!
df_raw['marital'].value_counts()
married 7490 single 3905 divorced 1475 Name: marital, dtype: int64
자, 그렇군요! 그러면 그룹을 만들어서 함 보시죠! (먼저 cross table로 정답을 확인!)
(참고로 pivot_table은 value column이 있는 경우에 매우 편리합니다)
pd.crosstab(df_raw['job'], df_raw['marital']) # 결과를 미리 본다면
marital | divorced | married | single |
---|---|---|---|
job | |||
admin. | 207 | 762 | 495 |
blue-collar | 205 | 1775 | 591 |
entrepreneur | 44 | 272 | 67 |
housemaid | 49 | 245 | 40 |
management | 323 | 1580 | 955 |
retired | 157 | 572 | 41 |
self-employed | 39 | 284 | 131 |
services | 138 | 584 | 321 |
student | 0 | 24 | 334 |
technician | 246 | 1115 | 780 |
unemployed | 62 | 219 | 133 |
unknown | 5 | 58 | 17 |
dft_job_marital = df_raw.groupby(['job','marital']).size().reset_index()
dft_job_marital.head()
job | marital | 0 | |
---|---|---|---|
0 | admin. | divorced | 207 |
1 | admin. | married | 762 |
2 | admin. | single | 495 |
3 | blue-collar | divorced | 205 |
4 | blue-collar | married | 1775 |
그룹을 만들긴 했는데, 이걸 각 job에 대해서 결혼여부에 대한 count를 만들어야 하겠는데, column이 여러개라서 pivot이 유리하겠습니다.
dft_jm = dft_job_marital.pivot_table(index='job', columns='marital', values=0).reset_index()
dft_jm.head()
marital | job | divorced | married | single |
---|---|---|---|---|
0 | admin. | 207.0 | 762.0 | 495.0 |
1 | blue-collar | 205.0 | 1775.0 | 591.0 |
2 | entrepreneur | 44.0 | 272.0 | 67.0 |
3 | housemaid | 49.0 | 245.0 | 40.0 |
4 | management | 323.0 | 1580.0 | 955.0 |
이제 각 행에 대해서 divoced와 married만 따로 비율을 구하는 과정을 거쳐야하겠군요.
dft_jm['ratio'] = dft_jm['divorced'] / dft_jm['married']
dft_jm.head()
marital | job | divorced | married | single | ratio |
---|---|---|---|---|---|
0 | admin. | 207.0 | 762.0 | 495.0 | 0.271654 |
1 | blue-collar | 205.0 | 1775.0 | 591.0 | 0.115493 |
2 | entrepreneur | 44.0 | 272.0 | 67.0 | 0.161765 |
3 | housemaid | 49.0 | 245.0 | 40.0 | 0.200000 |
4 | management | 323.0 | 1580.0 | 955.0 | 0.204430 |
이중에 비율이 가장 큰 것을 찾아라 했으니! sorting!
dft_jm.sort_values(by='ratio', ascending=False)[:2]
marital | job | divorced | married | single | ratio |
---|---|---|---|---|---|
10 | unemployed | 62.0 | 219.0 | 133.0 | 0.283105 |
5 | retired | 157.0 | 572.0 | 41.0 | 0.274476 |
unemployed가 결혼 상태에 비해서 이혼 비율이 제일 높군요? 어흙. 여튼 예측모형을 제외한 나머지 질문에 대해서 답을 열심히 했습니다. 이제까지의 질문이 예측모형을 만드는데 도움이 될까요? 정리를 한번 해 보죠.
Q01) 마케팅 응답 고객들의 나이를 10살 단위로 변환 했을 때, 가장 많은 인원을 가진 나이대는? (0~9 : 0 , 10~19 : 10)
Q02) 마케팅 응답 고객들의 나이를 10살 단위로 변환 했을 때, 가장 많은 나이대 구간의 인원은 몇명인가?
30대가 가장 가장 많았고요
Q03) 나이가 25살 이상 29살 미만인 응답 고객들중 housing컬럼의 값이 yes인 고객의 수는? 꽤 많았습니다.
50%정도?
Q04) numeric한 값을 가지지 않은 컬럼들중 unique한 값을 가장 많이 가지는 컬럼은?
job이었는데 12개 종류였습니다. 이게 좀 뭔가 나중에 예측할 때 critical한 요소가 되지 않을까 생각이 듭니다.
Q05) balance 컬럼값들의 평균값 이상을 가지는 데이터를 ID값을 기준으로 내림차순 정렬했을때 상위 100개 데이터의 balance값의 평균은?
이건.. 어디다 쓰죠..?
Q06) 가장 많은 광고를 집행했던 날짜는 언제인가? (데이터 그대로 일(숫자),달(영문)으로 표기)
5월 15일?
Q07) 데이터의 job이 unknown 상태인 고객들의 age 컬럼 값의 정규성을 검정하고자 한다. 샤피로 검정의 p-value값을 구하여라
정규성을 만족하더라.
Q08) age와 balance의 상관계수를 구하여라
상관계수는 0.1정도로 그다지 크지는 않았다. 이것은 알고 있으면 좋은 점?
Q09) 응답 y 변수와 education 변수는 독립인지 카이제곱검정을 통해 확인하려한다. p-value값을 출력하라
서로 독립이 아니더라. 나이보다는 교육수준이 더 관련이 있어 보입니다.
Q10) 각 job에 따라 divorced/married 인원의 비율을 확인 했을 때 그 값이 가장 높은 값은?
미취업 상태의 사람들이 이혼률이 더 높더라. 일반적인 상식과 비슷한 듯 합니다.
네, 뭐 어러가지 살펴보았는데, 이렇게 알아본 것 분석결과가 얼마나 도움이 될지 모르겠지만, 예측 모형을 만들어 보려고 합니다. 종속변수가 y/n인 Logistic 이거든요? 어떤 걸로 하면 좋을지 생각해 볼까요? Random Forest로 먼저 한번 해보고요. 또 고려해 보죠. 머
필요한 데이터를 선별하기 위한 Feature Engineering을 좀 해보도록 하시죵.
먼저, 데이터의 type을 좀 먼저보고요, object와 int를 나눠서 생각해 보는 겁니다~!
df_raw.info()
RangeIndex: 12870 entries, 0 to 12869 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ID 12870 non-null int64 1 age 12870 non-null int64 2 job 12870 non-null object 3 marital 12870 non-null object 4 education 12870 non-null object 5 default 12870 non-null object 6 balance 12870 non-null int64 7 housing 12870 non-null object 8 loan 12870 non-null object 9 contact 12870 non-null object 10 day 12870 non-null int64 11 month 12870 non-null object 12 campaign 12870 non-null int64 13 pdays 12870 non-null int64 14 previous 12870 non-null int64 15 poutcome 12870 non-null object 16 y 12870 non-null object dtypes: int64(7), object(10) memory usage: 1.7+ MB
object로 된 Feature들이 Label과 독립인지 확인도 해 보시겠습니담.
df_preprocess = df_raw.copy()
df_preprocess['str_campaign'] = df_preprocess['campaign'].astype('str')
coi = ['job', 'marital', 'education', 'default', 'housing', 'loan', 'contact', 'str_campaign', 'poutcome']
cot = ['y']
for feature in coi:
contingency_table = pd.crosstab(df_preprocess[feature], df_preprocess['y'])
chi2 , p ,dof, expected = chi2_contingency(contingency_table)
print(f"Feature {feature.upper()}: Chi-square value = {chi2}, p-value = {p}")
if p<0.05 :
print(f"{feature.upper()} is SIGNIFICANT")
else : print(f"{feature.upper()} is insignificant")
# 숫자로 된 것들은 등급처럼 만들어 둡시다.
df_preprocess.drop('str_campaign', axis=1, inplace=True)
df_preprocess['age'] = df_preprocess['age'].apply(lambda row: math.floor(row/10))
df_preprocess['balance'] = df_preprocess['balance'].apply(lambda row: math.floor(row/100))
Feature JOB: Chi-square value = 445.6149545516124, p-value = 1.2347805365874967e-88 JOB is SIGNIFICANT Feature MARITAL: Chi-square value = 112.78861212877688, p-value = 3.2230279036603607e-25 MARITAL is SIGNIFICANT Feature EDUCATION: Chi-square value = 133.87601246919445, p-value = 7.901201277473551e-29 EDUCATION is SIGNIFICANT Feature DEFAULT: Chi-square value = 15.034769518623209, p-value = 0.0001055485658896835 DEFAULT is SIGNIFICANT Feature HOUSING: Chi-square value = 481.2516893583131, p-value = 1.141246856051449e-106 HOUSING is SIGNIFICANT Feature LOAN: Chi-square value = 115.12933233581707, p-value = 7.372869938892642e-27 LOAN is SIGNIFICANT Feature CONTACT: Chi-square value = 660.7373487393592, p-value = 3.3320209821978656e-144 CONTACT is SIGNIFICANT Feature STR_CAMPAIGN: Chi-square value = 239.55703590479362, p-value = 2.541932665635015e-32 STR_CAMPAIGN is SIGNIFICANT Feature POUTCOME: Chi-square value = 1352.8799481901515, p-value = 4.940174194847549e-293 POUTCOME is SIGNIFICANT
아니 이게 뭐랍니까? 모든 Feature가 Label y와 독립이 아니군요? 히익? 뭔가 영향을 끼치나 봅니다. 이러면 없앨 수 있는 후보지가 일단은 없게 되는데 말이죠..
그럼 숫자로 된 것들은 분포가 어떤가 한번 보시죠.
df_preprocess['previous'].value_counts().plot()
으니~ 이건 또 멈니꺄~? 역시나 너무 0쪽에 몰려 있군요. 이럴 때는 둘중에 하나를 선택하는 편이 좋겠죠. 그게 뭐냐. Log를 씌우던지, 아니면 4이상은 하나로 묶던지. 그런데, previous feature가 무엇을 의미하는지를 몰라서 이거 어떻게 하는게 좋을지 모르겠는데, 일단 제 생각에는 왠만해야 되는데, 너무 몰려 있어서 제외하는 편이 낫지 않을까 생각합니다. 헤헤
그리고 또 볼만한 것인 pdays라는 건데, 이건 또 뭔가 싶군요.
df_preprocess['pdays'].value_counts().sort_index(ascending=True).plot()
아니, 이것도 뭐 0에 대부분의 데이터가 있습니다. 이게 의미가 있는 건지 모르겠는데, 그러면 0이냐 아니냐 정도로 하면 어떨까 생각합니다.
그리고, 캠페인과 job도 종류가 엄청 많았잖아요? 캡페인 먼저 한번 보고요!
df_preprocess['campaign'].value_counts().plot()
df_preprocess['campaign'].value_counts()[:10]
1 5222 2 3493 3 1556 4 990 5 458 6 337 7 190 8 152 9 87 10 72 Name: campaign, dtype: int64
그냥 이런 걸 보면 1,2,3,4 는 그대로 두고, 그외로 묶으면 좋을 것 같긴한데, 그렇게 일단 한번 해 보겠습니답
그리고, 마지막으로 job을 좀 볼텐데, 이거는 group별로 label y가 어떤 식으로 분포하는 지 한번 보겠습니다.
dft_job = df_preprocess.groupby(['job', 'y']).size().unstack()
dft_job['%yes'] = dft_job['yes'] / (dft_job['no'] + dft_job['yes']) * 100
dft_job.sort_values(by="%yes", ascending=False)
y | no | yes | %yes |
---|---|---|---|
job | |||
student | 152 | 206 | 57.541899 |
retired | 378 | 392 | 50.909091 |
unemployed | 260 | 154 | 37.198068 |
management | 1876 | 982 | 34.359692 |
self-employed | 310 | 144 | 31.718062 |
unknown | 55 | 25 | 31.250000 |
admin. | 1007 | 457 | 31.215847 |
technician | 1511 | 630 | 29.425502 |
services | 769 | 274 | 26.270374 |
housemaid | 247 | 87 | 26.047904 |
entrepreneur | 292 | 91 | 23.759791 |
blue-collar | 2046 | 525 | 20.420070 |
특이하게 학생하고 퇴직자들에게 yes가 많고, 나머지는 no가 많군요? 어허. 여튼 job이 뭔가 Label y를 예측할 떄 중요한 feature가 아닐까 생각이 강하게 드는군요! 후후. 생각해 보니 balance가 Label y와 어떤 관계인지 한번 궁금하군요?
dft_balance = df_preprocess.groupby(['balance', 'y']).size().unstack().plot()
print(dft_balance)
AxesSubplot(0.125,0.125;0.775x0.755)
아니 이게 머랍니까? 흠냐. balance가 참으로 특이한 분포를 갖는군요. out시키는 편이 낫겠습니다.
그러면 이번엔 마지막으로 label y가 imbalance한지도 한번 보시죠.
df_preprocess['y'].value_counts().plot(kind='bar')
엥. 불균형이 좀 심하네요. 이거는 나중에 train 데이터를 만들 때 균형을 맞춰야 하겠어요. 이건 다음의 stratify로 해결하겠습니다.
아, 그리고 한가지 더 개인적인 의견으로는 마케팅을 시행한 month와 day는 예측모형에 넣는 것이 적절하지 못하다는 생각이 듭니다. (아무리 생각해도 말이죠)
자, 그러면 일단 예측모형으로 RandomForest 모형을 이용해서 학습을 시켜보겠습니다.
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from imblearn.under_sampling import *
from sklearn.feature_selection import SelectKBest, f_classif
import numpy as np
import matplotlib.pyplot as plt
# ID는 제거
df_refined = df_preprocess.drop(columns ='ID').copy()
# Feature Selection
coi = ['age', 'job', 'marital', 'education', 'default', 'contact', 'day', 'month', 'pdays', 'previous', 'y']
cor = [x for x in df_refined.columns if x not in coi]
df_refined = df_refined.drop(columns=cor)
# Feature 변형
if 'previous' in coi :
df_refined['previous'] = df_refined['previous'].apply(lambda row: 0 if row==0 else 1)
if 'pdays' in coi :
df_refined['pdays'] = df_refined['pdays'].apply(lambda row: 0 if row==0 else 1)
if 'campaign' in coi :
df_refined['campaign'] = df_refined['campaign'].apply(lambda row: row if row>4 else 5)
df_refined['campaign'] = df_refined['campaign'].astype('str')
# y Label을 빼고,
y_train = df_refined.pop('y')
# Random Forest니까, drop없이 (Reference Class 없이) 그냥 모두 dummy화
x_train_raw = df_refined.copy()
x_train = pd.get_dummies(x_train_raw)
# train과 test를 분리!
# y_train의 불균형(Data Imbanace)을 stratify로 해소
x_train,x_test,y_train,y_test = train_test_split(x_train, y_train, stratify=y_train, test_size=0.3, random_state=4)
print(len(x_train))
print(len(x_test))
model = RandomForestClassifier()
# 학습! 가즈아!
model.fit(x_train, y_train)
# 예츠윽!
y_pred = model.predict(x_test)
# 결과 확인
print('훈련세트 정확도: {:.3f}' .format(model.score(x_train, y_train)))
print('테스트세트 정확도: {:.3f}' .format(model.score(x_test, y_test)))
9009 3861 훈련세트 정확도: 0.950 테스트세트 정확도: 0.727
음.. 결과가 accuracy가 72.7% 정도군요. 이거 만족할 만한 수준은 아니긴 한데.. 그럼 다른 모형으로도 해보죠. xgboost모형을 가져다가 해봐욥.
from xgboost import XGBClassifier
# xgboost는 자동으로 labeling안해주니까, label y를 dummy encoding!
y_train = pd.get_dummies(y_train, drop_first=True)
y_test = pd.get_dummies(y_test, drop_first=True)
model = XGBClassifier(random_state=11)
# 학습 가즈아!
model.fit(x_train, y_train)
y_pred = model.predict(x_test)
# 성능을 확인
print('훈련세트 정확도: {:.3f}' .format(model.score(x_train, y_train)))
print('테스트세트 정확도: {:.3f}' .format(model.score(x_test, y_test)))
훈련세트 정확도: 0.856 테스트세트 정확도: 0.775
오, xgboost가 77.5%로 조금 더 낫군요? 오홓? 이게 최선입니까? 증말로? 이번엔 신경망 모형으로 학습을 한번 해보죠. 여기에서 포기하기에는 너무 아쉽잖아요?
import numpy as np
from tensorflow import keras # tensorflow에 비해서 keras버전이 너무 높을 때에는 tensorflow에서 직접 import!
from tensorflow.keras import layers
from tensorflow.python.client import device_lib
# print(device_lib.list_local_devices())
hidden_nodes = 6 #len(x_train.columns)
def build_model() :
model = keras.Sequential([
layers.Dense(hidden_nodes, activation='relu', input_shape=[len(x_train.columns)]),
layers.Dense(38, activation='relu'),
layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model = build_model()
# Overfitting 되는 것을 방지하기 위해 early_stop! 설정.
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)
class PrintDot(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
if epoch % 100 == 0: print('## %d ## '%(epoch), end='')
history = model.fit(
x_train, y_train,
epochs=300, batch_size=100, validation_split = 0.2, verbose=0,
callbacks=[early_stop, PrintDot()])
## 0 ## ## 100 ##
loss, accuracy = model.evaluate(x_test, y_test, verbose=0) # evaluate는 자동으로 이진화 해서 비교해 줌
loss, accuracy
(0.5346112819231721, 0.76016575)
참고로 evaluate를 사용하지 않고 accuracy를 확인하는 방법은! 다음과 같습니다.
from sklearn.metrics import accuracy_score
# 모델이 예측한 출력
y_pred = model.predict(x_test)
display(y_pred)
# 출력을 이진화하여 0 또는 1로 변환
y_pred_binary = (y_pred > 0.5).astype(int)
# 정확도 계산
accuracy = accuracy_score(y_test, y_pred_binary)
accuracy
array([[0.1002959 ], [0.7983074 ], [0.10381174], ..., [0.15230204], [0.22632809], [0.19036685]], dtype=float32)
0.7601657601657602
accuary가 76.0%정도 하네요. 그러면 학습과정도 눈으로 보면 매우 도움이 되겠습니다. 그려봐요!
hist = pd.DataFrame(history.history)
hist['epoch'] = history.epoch
hist.tail()
loss | accuracy | val_loss | val_accuracy | epoch | |
---|---|---|---|---|---|
106 | 0.500487 | 0.776329 | 0.504779 | 0.783019 | 106 |
107 | 0.498914 | 0.777855 | 0.504862 | 0.784684 | 107 |
108 | 0.498912 | 0.778410 | 0.504304 | 0.782464 | 108 |
109 | 0.498502 | 0.777855 | 0.506623 | 0.780244 | 109 |
110 | 0.500490 | 0.777300 | 0.505452 | 0.784684 | 110 |
import matplotlib.pyplot as plt
# 6 훈련 과정 시각화 (정확도)
plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.title('Model accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend(['Train', 'Test'], loc='upper left')
plt.show()
# 7 훈련 과정 시각화 (손실)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Model loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend(['Train', 'Test'], loc='upper left')
plt.show()
네. 휴. 3개의 모형을 모두 학습 시켜봤는데, 결과를 살펴보면 (accuracy)
Randomforest는 72.7%정도, xgboost는 77.5%정도, 심층신경망은 76% 정도입니다.
이렇다는 의미는 당연하겠지만, 두가지로 해석할 수 있는데,
1) 예측을 위한 Feature Engineering이 덜 되었다.
2) 마케팅 응답이 현재 쌓아놓은 데이터와 관련성이 많이 없다.
사실 고백하건데, (굉장히 별거 아닌 것 처럼 진행해 왔지만) Feature Selection을 위해 넣기 빼기등을 여러가지 시도해 봤고요, 80%이상의 정확도를 확보하지 못했습니다. 더 좋은 방법이 있을 수도 있겠지만, 결론은 마케팅 응답이 현재 쌓아놓은 데이터와 관련성이 72~78%정도이다 라고 결론 맺는 편이 낫겠습니다.라고 주장하는 바입니다.
이번 분석은 이정도에서 마무리 짓는 것으로 해 보겠습니다.

그러니까 더 양질의 데이터 - 여기에서 양질의 데이터란 Label y를 더 잘 예측할 수 있는 주요 Feature와 꺠끗한 데이터 - 가 확보된다면 더 좋은 성능을 얻을 수 있을 것이라 생각합니다. 헤헤.

사실상 77.5%정도의 정확도로 실전에서 사용하기에는 무리가 있다고 생각합니다. 그러니까, 이건 데이터를 더 쌓기도 해야하고, Feature를 사전에 설계해서 만드는 편이 더 좋은 결과를 가져올 수 있지 않을까 생각합니다. 아쉽네요.


댓글