경기도 인공지능 개발 과정/Python

[Python] AIFB 강의(가입정보를 활용한 고객 데이터 분석)

agingcurve 2022. 7. 18. 17:19
반응형

0. 개요

가. 패키지 설치 및 라이브러리 호출

In [16]:
import warnings
# 불필요한 경고 출력을 방지합니다.
warnings.filterwarnings('ignore')
import subprocess
import sys
import pandas as pd
import numpy as np
import pickle
import hashlib

def install(package):
    subprocess.check_call([sys.executable, "-m", "pip", "install", package])

install("folium")
install("seaborn")
install("xgboost")

나. 강의 목차

1. 데이터 수집

가. 데이터 암호화

In [17]:
#실습 코드 - 파일을 ansan_data 데이터프레임에 로드하세요. 경로명(aidu환경): 'ansan_data.csv'
ansan_data=pd.read_csv("ansan_data.csv")
In [18]:
#head()로 상위 5개의 데이터를 살펴봅니다.
ansan_data.head()
 
데이터 전처리

※ 이 데이터는 고객을 식별할 수 없도록 사전에 처리를 거쳤습니다.

hashlib 을 이용한 데이터 암호화

IS, JOIN_SEQ는 암호화가 필요한 항목입니다.
학습데이터는 이미 암호화 되어있으나, 암호화 코드를 실행해보면서 방법을 익힙니다.

In [19]:
#암호화를 위한 함수를 정의합니다.
def encrypt(target):
    hashSHA=hashlib.sha256() #SHA256 해시 객체 생성
    hashSHA.update(str(target).encode('utf-8')) #해시 값 생성 암호화 대상이 숫자형인 경우에는 문자로 변경해야 한다.
    return hashSHA.hexdigest().upper() #해시값 반환
In [20]:
#연습문제 - encrypt 함수를 이용하여 본인 이름을 해쉬값으로 변경하세요
encrypt("성민제")
Out[20]:
'537D30E8C014ECB9EDF5BFED130C18A7C7397A359B416ADAEB2F2C58DB3E0EC0'
In [21]:
#실습 코드 - 앞서 만든 encrypt 함수를 적용하여 IS컬럼의 값을 암호화 하세요(.apply 함수를 이용하세요)
ansan_data['IS']=ansan_data['IS'].apply(encrypt)
In [22]:
ansan_data.head()
Out[22]:
DATEISTYPETYPE_DTL상세분류2JOIN_SEQPRODUCTPRODUCT_NMJOIN_DATEC_PERIOD...ROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2ROAD_NM_ADDCNTF_ADDRlonlat01234
In [23]:
#실습 코드 - 앞서 만든 encrypt 함수를 적용하여 JOIN_SEQ컬럼의 값을 암호화 하세요(.apply 함수를 이용하세요)
ansan_data['JOIN_SEQ']=ansan_data['JOIN_SEQ'].apply(encrypt)
In [24]:
ansan_data['JOIN_SEQ'].head()
Out[24]:
0    4ECD4C4BA2746463118E9B3E98C4D60E74D460B38B4BA9...
1    4C59FC639891F337F2C8FCE8F16653732306C672DF87C1...
2    708DB4F33C8CE7A1015FA79CDA42220BF52A14723F3ADE...
3    740E839B30A7BFAF81F10654A28423CDE84F6F0747CA3B...
4    936C726D2B538400D09885A481A886ADF9F0D17BC1B4A2...
Name: JOIN_SEQ, dtype: object

값이 암호화 되는 것을 확인할 수 있습니다.

In [25]:
ansan_data.head()
Out[25]:
DATEISTYPETYPE_DTL상세분류2JOIN_SEQPRODUCTPRODUCT_NMJOIN_DATEC_PERIOD...ROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2ROAD_NM_ADDCNTF_ADDRlonlat01234
 
 

나. 주소에 대한 위경도 좌표 수집

지도시각화를 위해서는 주소에 대한 좌표가 필요합니다.
Aidu 환경에서는 api 호출이 불가능하기 때문에 api호출은 실습하지 않습니다.
api로 수집한 데이터를 간단하게 파싱해보는 것만 실습하겠습니다.

In [26]:
with open('json_data.pickle', 'rb') as f:
    json_data = pickle.load(f) # 단 한줄씩 읽어옴
In [27]:
json_data[:10]
Out[27]:
[<Response [200]>,
 <Response [200]>,
 <Response [200]>,
 <Response [200]>,
 <Response [200]>,
 <Response [200]>,
 <Response [200]>,
 <Response [200]>,
 <Response [200]>,
 <Response [200]>]

<Response [200]> 각각이 json형식을 담고 있습니다.
.json() 함수로 JSON데이터에 접근이 가능합니다.

아래 결과를 보면 [] 리스트와 {} 딕셔너리로 되어있습니다. 즉, index와 key로 접근이 가능합니다.

In [28]:
json_data[0].json()
Out[28]:
 

- 도로명 주소 가져오기

한단계씩 접근해 봅니다.

In [29]:
json_data[0].json()['documents']
 
In [14]:
json_data[0].json()['documents'][0]
Out[14]:
 
In [30]:
json_data[0].json()['documents'][0]['road_address']['address_name']

 

 

- 좌표 가져오기

리스트에서 주소와 경도/위도만 가져와서 데이터프레임으로 만들어 줍니다.

I
 
In [37]:
#실습 코드 - json_data의 첫번째 요소에서 좌표를 출력해보세요(x,y)
json_data[0].json()['documents'][0]['road_address']["x"], json_data[0].json()['documents'][0]['road_address']["y"]
Out[37]:
 
In [41]:
address_data=pd.DataFrame([])
In [42]:
#실습 코드 - for문을 활용하여 도로명 주소와 주소에 대한 좌표를 읽어서 address_data에 저장하세요
for i in np.arange(len(json_data)):
    address_data=address_data.append([(json_data[i].json()['documents'][0]['road_address']['address_name'],
                                           json_data[i].json()['documents'][0]['road_address']['x'],
                                           json_data[i].json()['documents'][0]['road_address']['y'])],
                                         ignore_index=True)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-42-3cf2b658be78> in <module>
      1 #실습 코드 - for문을 활용하여 도로명 주소와 주소에 대한 좌표를 읽어서 address_data에 저장하세요
      2 for i in np.arange(len(json_data)):
----> 3     address_data=address_data.append([(json_data[i].json()['documents'][0]['road_address']['address_name'],
      4                                            json_data[i].json()['documents'][0]['road_address']['x'],
      5                                            json_data[i].json()['documents'][0]['road_address']['y'])],

IndexError: list index out of range

에러가 발생합니다. (주소에 대한 위/경도값을 api에서 가져오지 못한 경우)

In [43]:
json_data[18].json()
Out[43]:
{'documents': [],
 'meta': {'is_end': True, 'pageable_count': 0, 'total_count': 0}}
In [44]:
#실습 코드 - for문과 if문을 활용하여 도로명 주소와 주소에 대한 좌표를 읽어서 address_data에 저장하세요
for i in np.arange(len(json_data)):
    if json_data[i].json()['documents']!=[]: #주소가 조회되지 않은 경우가 존재하므로 제외함
        address_data = address_data.append([(json_data[i].json()["documents"][0]["road_address"]["address_name"],
                                             json_data[i].json()["documents"][0]["road_address"]["x"],
                                             json_data[i].json()["documents"][0]["road_address"]["y"])],
                                           ignore_index=True)
    
# 코드를 완성하세요.
In [ ]:
 
In [46]:
address_data
Out[46]:
01201234...248249250251252
경기 안산시 단원구 꽃우물길 27 126.821475069242 37.353274333416
경기 안산시 단원구 마조금길 36-11 126.582792941775 37.2236009338678
경기 안산시 단원구 선삼로5길 24 126.81262087183 37.3462208193513
경기 안산시 단원구 원당길 4-12 126.81426060651 37.3136263617497
     
     
     
     
     
     
     

253 rows × 3 columns

위와 같이 주소에 대한 좌표를 얻어서 저장할 수 있습니다.

ansan_data의 위경도 좌표는 이렇게 만들어졌습니다.

In [47]:
ansan_data.iloc[:,-3:]
Out[47]:
F_ADDRlonlat01234...177282177283177284177285177286
경기도 안산시 도로_300 41 126.812665 37.306694
경기도 안산시 도로_135 215 126.834569 37.311247
경기도 안산시 도로_124 80 126.818370 37.305672
경기도 안산시 도로_139 157 126.836629 37.305891
경기도 안산시 도로_364 29 126.838281 37.326341
... ... ...
경기도 안산시 도로_380 47 126.801111 37.316470
경기도 안산시 도로_39 326 126.786731 37.323545
경기도 안산시 도로_39 326 126.786664 37.323128
경기도 안산시 도로_39 326 126.787017 37.323861
경기도 안산시 도로_13 69 126.841267 37.317351

177287 rows × 3 columns

위와 같이 좌표를 얻어서 BIDW데이터에 붙여주었습니다.
아래가 최종 수집된 데이터입니다.

In [49]:
ansan_data.head(3)
Out[49]:
DATEISTYPETYPE_DTL상세분류2JOIN_SEQPRODUCTPRODUCT_NMJOIN_DATEC_PERIOD...ROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2ROAD_NM_ADDCNTF_ADDRlonlat012
2020-05-11 187E67F5E3D6893386AF54D8880752539691211BD52A08... 개인 _ ㆍ값없음 4ECD4C4BA2746463118E9B3E98C4D60E74D460B38B4BA9... 059Z 내선전화 2003-07-19 ㆍ값없음 ... R00050282 도로_300 아파트1067 41 0          
                                         
                                         

3 rows × 22 columns

※ 컬럼 설명

  • DATE : 데이터 생성일자
  • IS : 식별번호
  • TYPE : 고객 유형
  • TYPE_DTL : 상세 분류 코드
  • 상세분류2 : 상세 분류 코드 한글명
  • JOIN_SEQ: 가입일련번호
  • PRODUCT : 상품코드
  • PRODUCT_NM : 상품코드 한글명
  • JOIN_DATE : 가입일자
  • C_PERIOD_DATE : 약정기간
  • B_DONG : 법정동
  • B_DONG_ADD : 행정구역+법정동
  • ROAD_NO : 도로명번호
  • ROAD_NM : 도로명
  • BUILDING_NM : 건물명
  • BUILDING_NO1 : 건물 본번
  • BUILDING_NO2 : 건물 부번
  • ROAD_NM_ADD : 행정구역+도로명
  • CNT : 지표
  • F_ADDR : 도로명주소전체
  • lon: 경도
  • lat: 위도

 

2. 데이터 전처리

어떤 데이터인지 한번 살펴봅니다.

In [24]:
ansan_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 177287 entries, 0 to 177286
Data columns (total 22 columns):
 #   Column        Non-Null Count   Dtype  
---  ------        --------------   -----  
 0   DATE          177287 non-null  object 
 1   IS            177287 non-null  object 
 2   TYPE          177287 non-null  object 
 3   TYPE_DTL      177287 non-null  object 
 4   상세분류2         177287 non-null  object 
 5   JOIN_SEQ      177287 non-null  object 
 6   PRODUCT       177287 non-null  object 
 7   PRODUCT_NM    177287 non-null  object 
 8   JOIN_DATE     177228 non-null  object 
 9   C_PERIOD      177287 non-null  object 
 10  B_DONG        177287 non-null  object 
 11  B_DONG_ADD    177287 non-null  object 
 12  ROAD_NO       177287 non-null  object 
 13  ROAD_NM       177287 non-null  object 
 14  BUILDING_NM   144578 non-null  object 
 15  BUILDING_NO1  177287 non-null  int64  
 16  BUILDING_NO2  177287 non-null  int64  
 17  ROAD_NM_ADD   177287 non-null  object 
 18  CNT           177287 non-null  int64  
 19  F_ADDR        177287 non-null  object 
 20  lon           177279 non-null  float64
 21  lat           177279 non-null  float64
dtypes: float64(2), int64(3), object(17)
memory usage: 29.8+ MB

가.결측값 제거

JOIN_DATE, lon, lat에 결측치가 존재함을 알수 있습니다. 컬럼별로 아래와 같이 결측치를 처리합니다.

  • JOIN_DATE: 사용하지 않을 데이터이므로 열을 지우겠습니다.
  • BUILDING_NM : 건물명은 실제로 값이 존재하지 않을 수 있으므로 공란으로 두겠습니다.
  • lon, lat : 주소에 좌표가 없는 것은 있을 수 없으므로 해당 행을 삭제하겠습니다.
In [25]:
ansan_data.head()
Out[25]:
DATEISTYPETYPE_DTL상세분류2JOIN_SEQPRODUCTPRODUCT_NMJOIN_DATEC_PERIOD...ROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2ROAD_NM_ADDCNTF_ADDRlonlat01234
2020-05-11 187E67F5E3D6893386AF54D8880752539691211BD52A08... 개인 _ ㆍ값없음 4ECD4C4BA2746463118E9B3E98C4D60E74D460B38B4BA9... 059Z 내선전화 2003-07-19 ㆍ값없음 ... R00050282 도로_300 아파트1067 41 0   1   126.812665 37.306694
2020-05-11 1BF44EBE97B481D28E3B774E778E0E6406116EFDEC29D3... 개인 _ ㆍ값없음 4C59FC639891F337F2C8FCE8F16653732306C672DF87C1... 059Z 내선전화 2003-07-25 ㆍ값없음 ... R00050210 도로_135 상가979 215 0   1   126.834569 37.311247
2020-05-11 F2AFF7038C1740FD05445CB2E37DB5E116A459DD10BFCF... 개인 _ ㆍ값없음 708DB4F33C8CE7A1015FA79CDA42220BF52A14723F3ADE... 059Z 내선전화 2003-07-28 ㆍ값없음 ... R00050201 도로_124 아파트1046 80 0   1   126.818370 37.305672
2020-05-11 CBECCD2E091BDA7AB2395D8146F3B608FD72B6AEC54319... 개인 _ ㆍ값없음 740E839B30A7BFAF81F10654A28423CDE84F6F0747CA3B... 059Z 내선전화 2003-07-30 ㆍ값없음 ... R00050252 도로_139 상가984 157 0   1   126.836629 37.305891
2020-05-11 D6E56F9244AA80E03C48DD42AC0CE106CB353CFEC5CBFA... 개인 _ ㆍ값없음 936C726D2B538400D09885A481A886ADF9F0D17BC1B4A2... 059Z 내선전화 2003-08-04 ㆍ값없음 ... R00050219 도로_364 아파트1055 29 0   1   126.838281 37.326341

5 rows × 22 columns

i) JOIN_DATE 열을 삭제합니다.

결측치 제거하는 김에 불필요한 컬럼도 함께 삭제하겠습니다.('JOIN_DATE','DATE','C_PERIOD','B_DONG','B_DONG_ADD','ROAD_NM_ADD','CNT')

In [51]:
#실습 코드 - 'JOIN_DATE','DATE','C_PERIOD','B_DONG','B_DONG_ADD','ROAD_NM_ADD','CNT' 열을 삭제하세요 (.drop()을 사용하세요.)
ansan_data.drop(['JOIN_DATE','DATE','C_PERIOD','B_DONG','B_DONG_ADD','ROAD_NM_ADD','CNT'] ,axis=1,inplace=True)

ii) BUILDING_NM 열은 공란으로 둡니다.

In [54]:
#실습 코드 - BUILDING_NM 열의 NaN값을 확인합니다. (.isna()를 사용하세요.)
ansan_data["BUILDING_NM"].isna().sum()
#코드를 완성하세요. 
Out[54]:
32709

BUILDING_NM에 NaN이 들어가 있는 것이 보입니다. ""로 값을 변경합니다.

In [55]:
#실습 코드 - NaN을 공란으로 바꿔줍니다. (.fillna()를 사용하세요)
ansan_data['BUILDING_NM'].fillna("",inplace=True )
In [56]:
ansan_data[ansan_data['BUILDING_NM'].isna()]
Out[56]:
ISTYPETYPE_DTL상세분류2JOIN_SEQPRODUCTPRODUCT_NMROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2F_ADDRlonlat

결측값이 사라졌습니다.

iii) lon, lat은 결측값이 있는 행을 삭제합니다.

In [57]:
#실습 코드 - 결측값이 있는 행을 삭제합니다.(.dropna() 를 사용하세요)
ansan_data.dropna(axis=0,inplace=True)
In [58]:
ansan_data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 177279 entries, 0 to 177286
Data columns (total 15 columns):
 #   Column        Non-Null Count   Dtype  
---  ------        --------------   -----  
 0   IS            177279 non-null  object 
 1   TYPE          177279 non-null  object 
 2   TYPE_DTL      177279 non-null  object 
 3   상세분류2         177279 non-null  object 
 4   JOIN_SEQ      177279 non-null  object 
 5   PRODUCT       177279 non-null  object 
 6   PRODUCT_NM    177279 non-null  object 
 7   ROAD_NO       177279 non-null  object 
 8   ROAD_NM       177279 non-null  object 
 9   BUILDING_NM   177279 non-null  object 
 10  BUILDING_NO1  177279 non-null  int64  
 11  BUILDING_NO2  177279 non-null  int64  
 12  F_ADDR        177279 non-null  object 
 13  lon           177279 non-null  float64
 14  lat           177279 non-null  float64
dtypes: float64(2), int64(2), object(11)
memory usage: 21.6+ MB

이제 모든 결측치가 모두 사라졌습니다.

나. 중복 제거

데이터를 삭제한 경우 중복이 발생할 수 있습니다.

In [59]:
#실습 코드 - 데이터프레임의 중복을 제거합니다.(drop_duplicates()를 사용하세요)
ansan_data.drop_duplicates(inplace=True)
In [60]:
ansan_data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 177279 entries, 0 to 177286
Data columns (total 15 columns):
 #   Column        Non-Null Count   Dtype  
---  ------        --------------   -----  
 0   IS            177279 non-null  object 
 1   TYPE          177279 non-null  object 
 2   TYPE_DTL      177279 non-null  object 
 3   상세분류2         177279 non-null  object 
 4   JOIN_SEQ      177279 non-null  object 
 5   PRODUCT       177279 non-null  object 
 6   PRODUCT_NM    177279 non-null  object 
 7   ROAD_NO       177279 non-null  object 
 8   ROAD_NM       177279 non-null  object 
 9   BUILDING_NM   177279 non-null  object 
 10  BUILDING_NO1  177279 non-null  int64  
 11  BUILDING_NO2  177279 non-null  int64  
 12  F_ADDR        177279 non-null  object 
 13  lon           177279 non-null  float64
 14  lat           177279 non-null  float64
dtypes: float64(2), int64(2), object(11)
memory usage: 21.6+ MB

중복이 존재하지 않습니다.

다. 컬럼명 변경.

한글로된 컬럼명을 영어로 변경합니다.

In [63]:
#실습 코드 - 상세분류2 --> TYPE_DTL_NM로 컬럼명을 변경합니다.rename 함수를 이용하세요. 입력값은 {변경전컬럼명,변경후컬럼명} 이며, axis=1 옵션을 주어야 합니다.
ansan_data=ansan_data.rename(columns = {"상세분류2":"TYPE_DTL_NM"})
In [64]:
ansan_data
Out[64]:
ISTYPETYPE_DTLTYPE_DTL_NMJOIN_SEQPRODUCTPRODUCT_NMROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2F_ADDRlonlat01234...177282177283177284177285177286
187E67F5E3D6893386AF54D8880752539691211BD52A08... 개인 _ ㆍ값없음 4ECD4C4BA2746463118E9B3E98C4D60E74D460B38B4BA9... 059Z 내선전화 R00050282     41 0   126.812665 37.306694
1BF44EBE97B481D28E3B774E778E0E6406116EFDEC29D3... 개인 _ ㆍ값없음 4C59FC639891F337F2C8FCE8F16653732306C672DF87C1... 059Z 내선전화 R00050210     215 0   126.834569 37.311247
F2AFF7038C1740FD05445CB2E37DB5E116A459DD10BFCF... 개인 _ ㆍ값없음 708DB4F33C8CE7A1015FA79CDA42220BF52A14723F3ADE... 059Z 내선전화 R00050201     80 0   126.818370 37.305672
CBECCD2E091BDA7AB2395D8146F3B608FD72B6AEC54319... 개인 _ ㆍ값없음 740E839B30A7BFAF81F10654A28423CDE84F6F0747CA3B... 059Z 내선전화 R00050252     157 0   126.836629 37.305891
D6E56F9244AA80E03C48DD42AC0CE106CB353CFEC5CBFA... 개인 _ ㆍ값없음 936C726D2B538400D09885A481A886ADF9F0D17BC1B4A2... 059Z 내선전화 R00050219     29 0   126.838281 37.326341
... ... ... ... ... ... ... ...     ... ...   ... ...
D57D266B1D0A4A5C94CCCC46B37AFB6ABD92CF614E0F41... 개인사업자 90029 가치 CC9A47350AD7B49AEA972F5FACAD1409A24412CC8D91D3... 0V2101 biz_인터넷전화_A R00050226     47 0   126.801111 37.316470
11089D77B49DFE8070D277046F9E85F89169598C99A8F6... 개인 10003 MASS 8C6CC1EB8DAE63629739503738A7C66B1E07A5F6520CCD... 0501 일반전화 R00050199     326 0   126.786731 37.323545
9B3AA16230A5AFC064816A7849993340B1FA61D5ABB479... 개인 10003 MASS F267C5BD0AB55EA7D25A9EC2D47FBEB14CFC7F39ED27FD... 088M01 인터넷_A R00050199     326 0   126.786664 37.323128
1AB4BD42ED3CE52CE539BBA55C2F06E2F25AF0222BA5F8... 개인 10003 MASS D3D10D98C705895D3C54483265CB0D8B8ED7B87BE2866F... 2PH601 IPTV_A R00050199     326 0   126.787017 37.323861
272CF87992C562F626E0DCF2EEA15B2EE3404BFED94079... 개인 10003 MASS 8393029F081E3E42261F2ED82EA8FA1745EA94960F4E3F... 088L01 인터넷_E R00050331     69 0   126.841267 37.317351

177279 rows × 15 columns

라.Feature Engineering

i) Domain지식 - 데이터 삭제

  • 059Z-내선전화는 요금을 납부하지 않으며, 모회선에 종속되어 세금계산서 발급 대상이 아니므로 제외합니다.
In [36]:
#실습 코드 PRODUCT가 '059Z'인 데이터를 삭제합니다. ('059Z'가 아닌 데이터만 가져옵니다)
ansan_data=ansan_data.loc[(ansan_data['PRODUCT']!='059Z')]
In [37]:
ansan_data.head(3)
Out[37]:
ISTYPETYPE_DTLTYPE_DTL_NMJOIN_SEQPRODUCTPRODUCT_NMROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2F_ADDRlonlat210012100221003
2403D35D92928C792A2BE8391283EF704E9E7652F7D23A... 개인사업자 10019 가치 8BBB8E2A3A4BAAFA74413364738AF067A00B2EFC4BF9A4... 088M01 인터넷_A R00050230 도로_395   65 0   126.738316 37.319408
3A985D28A2D0655B534E6C479D076BA860CC1E0A6AA858... 개인사업자 10019 가치 5956A41EAD4F67FC5BA589790334CEE2207CF5FAAEAEF9... 0V0201 인터넷전화_A R00050230 도로_395   65 0   126.739371 37.318065
29A2442FCF593EA28A6A0BCD2B6B13A938180232B8B337... 개인사업자 10019 가치 B96DDD5726D3DBFFFD50CA743C2B9147AC56B15038ECB3... 0501 일반전화 R00050230 도로_395   65 0   126.738734 37.318752

ii) Domain지식 - 데이터 수정

  • 유형별 데이터를 살펴봅니다.
In [65]:
#실습 코드 - TYPE과 TYPE_DTL_NM을 .groupby() 함수를 이용해서 살펴봅니다. > count() 사용
pd.DataFrame(ansan_data.groupby(["TYPE","TYPE_DTL_NM"])['IS'].count())
Out[65]:
ISTYPETYPE_DTL_NM개인AMAPTCOREMASSㆍ값없음가치법인개인사업자AMAPTCOREMASS가치법인공공기관AMCOREMASS가치법인기타가치법인사업자AMAPTCOREMASS가치법인정식단체AMCORE가치법인
826
42223
1088
18081
19169
20571
98
5
10
554
112
15993
306
56
6536
1
12
95
1
9704
9
16349
14
8748
14779
5
640
38
1256
  • TYPE은 개인/개인사업자/공공기관/기타/법인사업자/정식단체로 분류되는데, 공공기관/기타/법인사업자/정식단체는 구분하는 것이 의미가 없으므로 "법인"으로 통합합니다.
In [66]:
ansan_data.loc[(ansan_data['TYPE']=='공공기관')|
               (ansan_data['TYPE']=='기타') |
               (ansan_data['TYPE']=='법인사업자')|
               (ansan_data['TYPE']=='정식단체'),'TYPE']
Out[66]:
21014     법인사업자
21027     법인사업자
21028     법인사업자
21029     법인사업자
21030     법인사업자
          ...  
177272    법인사업자
177273    법인사업자
177277    법인사업자
177278    법인사업자
177279    법인사업자
Name: TYPE, Length: 58243, dtype: object
In [67]:
ansan_data.loc[(ansan_data['TYPE']=='공공기관')|
               (ansan_data['TYPE']=='기타') |
               (ansan_data['TYPE']=='법인사업자')|
               (ansan_data['TYPE']=='정식단체'),'TYPE'] = '법인'
  • 명의자(TYPE)는 개인이지만, 실제로는 사업자가 연결되어있을 수 있습니다. 고객세부유형을 보면, 가치/AM/CORE/법인은 실제로는 사업자일 것입니다. 실제 데이터를 살펴보면, "가치"는 개인사업자이고, AM/CORE도 기업의 입점업체이거나, 지점으로 개인사업자일 것으로 보이므로, 해당 건들을 전부 개인사업자로 변경하겠습니다.

※고객 세부유형의 의미: MASS(일반개인고객)/ 가치(소상공인) / AM(대기업) /CORE(중견기업) / 법인(미분류 법인)

In [68]:
#실습 코드 - 앞의 코드를 사용하여 TYPE이 '개인'이면서 TYPE_DTL_NM이 'AM','CORE','가치','법인'인 경우 '개인사업자'로 변경해줍니다.
ansan_data.loc[(ansan_data["TYPE"] == "개인") 
               & ((ansan_data['TYPE_DTL_NM']=='AM') 
               |(ansan_data['TYPE_DTL_NM']=='CORE')
               |(ansan_data['TYPE_DTL_NM']=='가치')
               |(ansan_data['TYPE_DTL_NM']=='법인')),'TYPE'] = '개인사업자'
  • TYPE_DTL_NM 이 ㆍ값없음 인 경우도 살펴보겠습니다.
    BUILDING_NM을 보니 개인사업자입니다. 개인사업자로 변경하겠습니다.
In [70]:
ansan_data.loc[ansan_data['TYPE_DTL_NM']=='ㆍ값없음']
Out[70]:
ISTYPETYPE_DTLTYPE_DTL_NMJOIN_SEQPRODUCTPRODUCT_NMROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2F_ADDRlonlat01234...1916419165191664889048891
187E67F5E3D6893386AF54D8880752539691211BD52A08... 개인 _ ㆍ값없음 4ECD4C4BA2746463118E9B3E98C4D60E74D460B38B4BA9... 059Z 내선전화 R00050282     41 0   126.812665 37.306694
1BF44EBE97B481D28E3B774E778E0E6406116EFDEC29D3... 개인 _ ㆍ값없음 4C59FC639891F337F2C8FCE8F16653732306C672DF87C1... 059Z 내선전화 R00050210     215 0   126.834569 37.311247
F2AFF7038C1740FD05445CB2E37DB5E116A459DD10BFCF... 개인 _ ㆍ값없음 708DB4F33C8CE7A1015FA79CDA42220BF52A14723F3ADE... 059Z 내선전화 R00050201     80 0   126.818370 37.305672
CBECCD2E091BDA7AB2395D8146F3B608FD72B6AEC54319... 개인 _ ㆍ값없음 740E839B30A7BFAF81F10654A28423CDE84F6F0747CA3B... 059Z 내선전화 R00050252     157 0   126.836629 37.305891
D6E56F9244AA80E03C48DD42AC0CE106CB353CFEC5CBFA... 개인 _ ㆍ값없음 936C726D2B538400D09885A481A886ADF9F0D17BC1B4A2... 059Z 내선전화 R00050219     29 0   126.838281 37.326341
... ... ... ... ... ... ... ...     ... ...   ... ...
970FE6C3311886B570B67ABBED6FF32B6025A1594B920B... 개인 _ ㆍ값없음 516D03DF33AF3E723D2288FDAAC53EEE30C8DA18E532FF... 059Z 내선전화 R00050268     49 0   126.811308 37.303106
A49DCE9AC6AB3E3013F7C2CC9B2A1D9F07A1A793C43EF4... 개인 _ ㆍ값없음 F50CF2DEF9D1449EC91BFB6D4E0D86CFBA6B4E13003CBB... 059Z 내선전화 R00050210     245 0   126.837719 37.311212
C5048D7B1F232A7FF919D0893746DFBC9DC48F55F95D09... 개인 _ ㆍ값없음 5E6A68690AB43507B80C89C22F5D4F088CA87F7EE66171... 059Z 내선전화 R00050212     45 0   126.825612 37.309489
B359AA8A0C05054D60A12D15B8DAB421731B8442658270... 개인 _ ㆍ값없음 D20A733ACE41147B1B4D0764E94C0E05353C321435C67E... 088M01 인터넷_A R00050332     38 0   126.834456 37.316762
7D9DF6E18137AD4927E9DABC461F5BAF039CA7E99C71C5... 개인 _ ㆍ값없음 06FAF031F8C8C5A1555CE0E656D701F60829A37DCA295A... 2PHA01 IPTV_B R00050332     38 0   126.835585 37.317674

19169 rows × 15 columns

In [71]:
#실습 코드 - TYPE이 '개인'이면서(&) TYPE_DTL_NM이 '값없음'인 경우 개인사업자로 변경합니다.
ansan_data.loc[(ansan_data["TYPE"] == "개인") & (ansan_data["TYPE_DTL_NM"]=='ㆍ값없음'),'TYPE']='개인사업자'
In [72]:
ansan_data.loc[ansan_data['TYPE_DTL_NM']=='ㆍ값없음']
Out[72]:
ISTYPETYPE_DTLTYPE_DTL_NMJOIN_SEQPRODUCTPRODUCT_NMROAD_NOROAD_NMBUILDING_NMBUILDING_NO1BUILDING_NO2F_ADDRlonlat01234...1916419165191664889048891
187E67F5E3D6893386AF54D8880752539691211BD52A08... 개인사업자 _ ㆍ값없음 4ECD4C4BA2746463118E9B3E98C4D60E74D460B38B4BA9... 059Z 내선전화 R00050282     41 0   126.812665 37.306694
1BF44EBE97B481D28E3B774E778E0E6406116EFDEC29D3... 개인사업자 _ ㆍ값없음 4C59FC639891F337F2C8FCE8F16653732306C672DF87C1... 059Z 내선전화 R00050210     215 0   126.834569 37.311247
F2AFF7038C1740FD05445CB2E37DB5E116A459DD10BFCF... 개인사업자 _ ㆍ값없음 708DB4F33C8CE7A1015FA79CDA42220BF52A14723F3ADE... 059Z 내선전화 R00050201     80 0   126.818370 37.305672
CBECCD2E091BDA7AB2395D8146F3B608FD72B6AEC54319... 개인사업자 _ ㆍ값없음 740E839B30A7BFAF81F10654A28423CDE84F6F0747CA3B... 059Z 내선전화 R00050252     157 0   126.836629 37.305891
D6E56F9244AA80E03C48DD42AC0CE106CB353CFEC5CBFA... 개인사업자 _ ㆍ값없음 936C726D2B538400D09885A481A886ADF9F0D17BC1B4A2... 059Z 내선전화 R00050219     29 0   126.838281 37.326341
... ... ... ... ... ... ... ...     ... ...   ... ...
970FE6C3311886B570B67ABBED6FF32B6025A1594B920B... 개인사업자 _ ㆍ값없음 516D03DF33AF3E723D2288FDAAC53EEE30C8DA18E532FF... 059Z 내선전화 R00050268     49 0   126.811308 37.303106
A49DCE9AC6AB3E3013F7C2CC9B2A1D9F07A1A793C43EF4... 개인사업자 _ ㆍ값없음 F50CF2DEF9D1449EC91BFB6D4E0D86CFBA6B4E13003CBB... 059Z 내선전화 R00050210     245 0   126.837719 37.311212
C5048D7B1F232A7FF919D0893746DFBC9DC48F55F95D09... 개인사업자 _ ㆍ값없음 5E6A68690AB43507B80C89C22F5D4F088CA87F7EE66171... 059Z 내선전화 R00050212     45 0   126.825612 37.309489
B359AA8A0C05054D60A12D15B8DAB421731B8442658270... 개인사업자 _ ㆍ값없음 D20A733ACE41147B1B4D0764E94C0E05353C321435C67E... 088M01 인터넷_A R00050332     38 0   126.834456 37.316762
7D9DF6E18137AD4927E9DABC461F5BAF039CA7E99C71C5... 개인사업자 _ ㆍ값없음 06FAF031F8C8C5A1555CE0E656D701F60829A37DCA295A... 2PHA01 IPTV_B R00050332     38 0   126.835585 37.317674

19169 rows × 15 columns

  • 삭제된 데이터가 있으니 index를 리셋합니다.
In [73]:
#실습 코드 - index를 리셋하세요
ansan_data.reset_index(drop=True,inplace=True)
  • 전처리가 완료되었습니다.
In [74]:
ansan_data.groupby('TYPE')['IS'].count()
Out[74]:
TYPE
개인       60304
개인사업자    58732
법인       58243
Name: IS, dtype: int64

마.데이터 저장

In [75]:
#실습 코드 - to_csv 함수를 이용하여 ansan_data 를 CSV파일로 저장합니다. 경로명: 'ansan_data_pre.csv'
ansan_data.to_csv('ansan_data_pre.csv')

3. 데이터 시각화

가. Seaborn Barplot을 통한 탐색

  • seaborn으로 barplot을 그려서 상품의 분포를 탐색해보겠습니다.

i) 한글 폰트 설정

In [76]:
from matplotlib import pyplot as plt
import seaborn as sns
In [77]:
import matplotlib.font_manager as fm

어떤 폰트가 설치되어있는지 확인합니다.

In [78]:
fm.get_fontconfig_fonts()
Out[78]:
['/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
 '/usr/share/fonts/truetype/nanum/NanumGothicCoding-Bold.ttf',
 '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
 '/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf',
 '/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf',
 '/usr/share/fonts/truetype/nanum/NanumGothicCoding.ttf',
 '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf',
 '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf']

NanumGothicCoding이라는 폰트를 쓰겠습니다.

In [79]:
plt.rc('font',family = 'NanumGothicCoding')

캔버스 사이즈도 키워줍니다.

In [80]:
plt.rcParams["figure.figsize"]=(12,9)

ii) barplot

  • 고객유형과 상품별로 groupby를 해봅니다.
In [81]:
pd.DataFrame(ansan_data.groupby(['TYPE','PRODUCT_NM'])['PRODUCT'].count()).sort_values(by=['TYPE','PRODUCT'],
            ascending=False)
Out[81]:
PRODUCTTYPEPRODUCT_NM법인biz_일반전화_A일반전화전용회선_Abiz_인터넷전화_Bbiz_일반전화_B......개인상품_38상품_40상품_45상품_49상품_81
18810
8716
4848
3994
3706
...
1
1
1
1
1

414 rows × 1 columns

In [89]:
#실습 코드 - TYPE과 PRODUCT_NM기준으로 groupby 하고, type과 product 값 기준으로 내림차순 정렬(sort_values), index는 리셋 하세요(reset_index)
prod_stat=pd.DataFrame(ansan_data.groupby(["TYPE","PRODUCT_NM"])['PRODUCT'].count()).sort_values(by=["TYPE","PRODUCT"],
            ascending=False).reset_index()
prod_stat
Out[89]:
TYPEPRODUCT_NMPRODUCT01234...409410411412413
법인 biz_일반전화_A 18810
법인 일반전화 8716
법인 전용회선_A 4848
법인 biz_인터넷전화_B 3994
법인 biz_일반전화_B 3706
... ... ...
개인 상품_38 1
개인 상품_40 1
개인 상품_45 1
개인 상품_49 1
개인 상품_81 1

414 rows × 3 columns

  • 고객/상품별 건수가 컬럼명이 PRODUCT이니 혼동이 옵니다. 컬럼명을 변경해주겠습니다.
In [90]:
prod_stat = prod_stat.rename(columns={'PRODUCT':'CNT'})
prod_stat
Out[90]:
TYPEPRODUCT_NMCNT01234...409410411412413
법인 biz_일반전화_A 18810
법인 일반전화 8716
법인 전용회선_A 4848
법인 biz_인터넷전화_B 3994
법인 biz_일반전화_B 3706
... ... ...
개인 상품_38 1
개인 상품_40 1
개인 상품_45 1
개인 상품_49 1
개인 상품_81 1

414 rows × 3 columns

  •  
  • seaborn.barplot Parameters
    x, y: x축, y축이 되는 feature를 넣어줍니다.
    data : 그래프를 그릴 데이터
  • subplot(m,n,p): m행 n열로 그래프를 배치합니다. p는 mxn 그리드 중 현재 그리는 그래프의 위치를 의미합니다.
In [118]:
#실습 코드 - x축은 PRODUCT_NM, y축은 CNT가 되도록 개인, 개인사업자, 법인에 대하여 barplot 그래프를 그리세요.
i=1
for c_type in ['개인','개인사업자','법인']:
    plt.subplot(3,1,i)
    graph_data=prod_stat[prod_stat['TYPE']==c_type]
    #barplot을 그리세요.#
    sns.barplot(x="PRODUCT_NM" ,y="CNT", data=graph_data)
    i=i+1
    
plt.show()

상품이 매우 많다는 것만 알겠습니다. 조금씩 고쳐보겠습니다.

iii) 상품 통합

상품이 너무 많으니, 비중이 작은 상품을 하나로 묶어서 "기타 상품"으로 처리하겠습니다.

상품명을 기타함수로 변경하는 함수를 정의합니다.

In [92]:
#대상이 되는 Dataframe과 기타상품을 구분하는 임계값을 입력으로 받습니다.
def etctoetc(df,threshold):
    total_cnt = df['CNT'].sum()
    cnt=0
    for n in df.index:
        cnt=cnt+df.loc[n,'CNT'] #루프를 돌때마다 cnt의 값을 PRODUCT만큼 증가시킴
        if(cnt > total_cnt*threshold): #cnt의 값이 total_cnt*threshold보다 크면
            df.loc[n:,'PRODUCT_NM'] = '기타상품' #n번 인덱스 이후의 PRODUCT_NM을 '기타상품'으로 상품명을 변경
            break 
    #기타상품으로 변경된 상품을 하나로 합쳐줌        
    df = pd.DataFrame(df.groupby(['TYPE','PRODUCT_NM'])['CNT'].sum()).reset_index()
    #데이터를 정렬할 때 기타상품이 가장 뒤에 오도록 수정   
    df2 = df.loc[df['PRODUCT_NM']=='기타상품',:]    
    df3 = df.loc[df['PRODUCT_NM']!='기타상품',:].sort_values(by=['CNT'], ascending=False)    
    df4= pd.concat([df3,df2],sort=False).reset_index(drop=True)
        
    return df4

위 함수를 적용해서 다시 그려보겠습니다.

In [93]:
#실습 코드 - x축은 PRODUCT_NM, y축은 CNT가 되도록 개인, 개인사업자, 법인에 대하여 barplot 그래프를 그리세요.
i=1
for c_type in ['개인','개인사업자','법인']:
    plt.subplot(3,1,i)
    graph_data=etctoetc(prod_stat[prod_stat['TYPE']==c_type],0.9)
    sns.barplot(x="PRODUCT_NM" ,y="CNT", data=graph_data)
    i=i+1
    
plt.show()

iv) 그래프 수정

  • 상품명이 겹쳐보입니다. 이럴 때는 가로 그래프로 그리면 됩니다.
  • 간격이 좁습니다. 조금 그래프를 키우겠습니다.
  • title 추가합니다.
In [95]:
#실습 코드 - y축은 PRODUCT_NM, x축은 CNT가 되도록 개인, 개인사업자, 법인에 대하여 barplot 그래프를 그리세요.
plt.rcParams["figure.figsize"]=(12,15)

i=1
for c_type in ['개인','개인사업자','법인']:
    plt.subplot(3,1,i)
    plt.title(c_type+' 주요 사용 상품')
    graph_data=etctoetc(prod_stat[prod_stat['TYPE']==c_type],0.9)
     #barplot을 그리세요.#
    sns.barplot(x="PRODUCT_NM" ,y="CNT", data=graph_data)
        
    i=i+1

plt.show()

개인은 Mass상품, 개인사업자는 Biz 상품과 Mass상품 혼용, 법인은 Biz 상품 위주로 사용하여
고객유형 별로 사용하는 상품에 차이가 있음을 확인할 수 있습니다.

나. Folium을 이용한 지도시각화

이제 우리가 가진 데이터를 지도위에 올려보겠습니다.

i) Folium 기초

일단 지도를 불러와야 합니다.
지도는 folium 패키지를 사용하여 불러오겠습니다. (google/kakao/naver/공공 API에서 지도를 불러올 수도 있겠지만, Aidu에서 외부연동이 불가능합니다.)

In [96]:
import folium

지도의 중심점을 위경도 좌표로 지정하고, 지도의 배율을 지정하겠습니다. (작을 수록 저배율) 우리가 가진 데이터의 기술통계를 살펴보고, 위도(lat)와 경도(lon)의 평균값을 지도의 중심점으로 삼고, 지도의 배율은 14로 하겠습니다.

In [97]:
ansan_data.describe()
Out[97]:
BUILDING_NO1BUILDING_NO2lonlatcountmeanstdmin25%50%75%max
177279.000000 177279.000000 177279.000000 177279.000000
147.501768 0.618827 126.807102 37.318794
199.578856 3.144173 0.033800 0.013132
1.000000 0.000000 126.344671 37.110933
32.000000 0.000000 126.790920 37.310979
80.000000 0.000000 126.813747 37.318522
171.000000 0.000000 126.829125 37.326330
4933.000000 90.000000 127.259800 37.407577
  • 아래와 같이 지도의 중심점을 설정하면, 안산시 단원구 지도가 나오는 것을 볼수 있습니다.
In [102]:
f_lon=126.813232
f_lat=37.318579

Parameters
location(tuple or list, default None) – 위도, 경도로 지도의 중심점을 지정합니다.

zoom_start (int, default 10) – 지도의 배율 지정, 숫자가 커질수록 배율이 낮음

 

#실습 코드 - 지도를 그리세요 (구글 지도에서 본인이 살고 있는 집 주소를 검색하고 좌표를 찾아서 지도에 띄워 보세요(구글맵 이용))
map = folium.Map(location=[f_lat,f_lon],default=5)
map

 

 

folium 패키지는 기존적으로 Open Street Map 기반이지만, Stamen Terrain’, ‘Stamen Toner’, ‘Mapbox Bright’, 와 ‘Mapbox Control room tiles’ 형식을 지정할 수 있습니다.

(Mapbox 의 경우에는 API key가 필요)

In [119]:
map_ST = folium.Map(location=[f_lat,f_lon], zoom_start=14, tiles='Stamen Terrain')
In [120]:
map_ST

ii) 지도위에 데이터 올리기

우리는 다양한 지도중에서도 HeatMap을 사용합니다.
Heatmap은 아래와 같이 데이터의 밀도와 분포를 표현하는데 적합합니다

 

 

from folium.plugins import HeatMap

개인/개인사업자/법인으로 구분합니다.

In [122]:
ansan_map_1=ansan_data.loc[ansan_data['TYPE']=='개인',:]
ansan_map_2=ansan_data.loc[ansan_data['TYPE']=='개인사업자',:]
ansan_map_3=ansan_data.loc[ansan_data['TYPE']=='법인',:]

Parameter
data (list of points of the form [lat, lng] or [lat, lng, weight]) - numpy.array 또는 list로 입력
min_opacity (default 1.) – 불투명도
max_zoom (default 18) – 최대 밀도에 도달하는 Zoom 레벨
radius (int, default 25) – 포인트의 반경

  • 개인의 분포를 확인합니다.
In [123]:
heat_data=np.array([ansan_map_1['lat'],ansan_map_1['lon']])
heat_data=heat_data.transpose()  #array의 행열 전환
In [124]:
heat_data
Out[124]:
array([[ 37.32558955, 126.82132713],
       [ 37.31990483, 126.83285282],
       [ 37.31900046, 126.83144718],
       ...,
       [ 37.32312757, 126.78666357],
       [ 37.32386146, 126.78701723],
       [ 37.31735054, 126.84126659]])
In [132]:
map = folium.Map(location=[f_lat,f_lon], zoom_start=13)
HeatMap(heat_data,min_opacity=0.2,max_val=1,max_zoom=25,radius=25).add_to(map)
Out[132]:
<folium.plugins.heat_map.HeatMap at 0x7fec18fc7e80>
In [126]:
map

 

 

개인은 아파트단지 등 주택지역에 많이 분포하는 것을 볼수 있습니다.

  • 개인사업자의 분포를 확인합니다
In [133]:
#실습 코드 - 개인에 대해 지도에 heatmap을 그렸던 코드를 참고하여 개인사업자의 데이터를 만듭니다.
heat_data=np.array([ansan_map_2['lat'],ansan_map_2['lon']])
heat_data=heat_data.transpose()  #array의 행열 전환
In [134]:
#실습 코드 - 개인에 대해 지도에 heatmap을 그렸던 코드를 참고하여 개인사업자의 분포를 표시합니다.
map = folium.Map(location=[f_lat,f_lon], zoom_start=13)
HeatMap(heat_data,min_opacity=0.2,max_val=1,max_zoom=25,radius=25).add_to(map)
Out[134]:
<folium.plugins.heat_map.HeatMap at 0x7fec18a1f0b8>
In [129]:
map

 

  • 법인의 분포를 확인합니다
In [135]:
#실습 코드 - 개인에 대해 지도에 heatmap을 그렸던 코드를 참고하여 법인의 데이터를 만듭니다.
heat_data=np.array([ansan_map_3['lat'],ansan_map_3['lon']])
heat_data=heat_data.transpose()  #array의 행열 전환
In [136]:
#실습 코드 - 개인에 대해 지도에 heatmap을 그렸던 코드를 참고하여 법인의 분포를 표시합니다.
map = folium.Map(location=[f_lat,f_lon], zoom_start=13)
HeatMap(heat_data,min_opacity=0.2,max_val=1,max_zoom=25,radius=25).add_to(map)
Out[136]:
<folium.plugins.heat_map.HeatMap at 0x7fec18fb6588>
In [137]:
map

 

4. 모델링

가. Train/Test 데이터 분할

학습에 사용할 데이터만 남기겠습니다.

In [138]:
ansan_data_T=ansan_data.loc[:,['TYPE','PRODUCT_NM','ROAD_NM','BUILDING_NO1','BUILDING_NO2']]
ansan_data_T.head()
Out[138]:
TYPEPRODUCT_NMROAD_NMBUILDING_NO1BUILDING_NO201234
개인사업자 내선전화 도로_300 41 0
개인사업자 내선전화 도로_135 215 0
개인사업자 내선전화 도로_124 80 0
개인사업자 내선전화 도로_139 157 0
개인사업자 내선전화 도로_364 29 0

데이터를 학습에 사용할 데이터와 예측을 위한 데이터로 나누겠습니다.
데이터를 분할 할 때는 Pandas의 sample함수를 이용해서 Train 셋과 Test 셋을 9:1로 분리하겠습니다.

※ 원칙적으로 학습에 사용할 데이터와 예측을 위한 데이터는 전처리 전에 분리해야 합니다.
강의에서 전처리 후에 데이터를 분할한 것은 학습의 편의를 위해서입니다.

In [139]:
#Train set의 크기 계산
N=int(len(ansan_data_T) * 0.9)
In [140]:
#sample 함수를 이용해 Train_set을 분리하여 저장
train_data=ansan_data_T.sample(n=N)
In [ ]:
 
In [141]:
#Train_set을 제외한 나머지를 Test_set으로 저장
test_data=ansan_data_T.drop(ansan_data_T.index[train_data.index])

나. Label Encoding

문자로된 범주형 컬럼들을 숫자로 변경합니다.

In [142]:
train_data
Out[142]:
TYPEPRODUCT_NMROAD_NMBUILDING_NO1BUILDING_NO212757187517109667365610150...17387314261613834476026112303
법인 biz_일반전화_A 도로_109 45 0
개인 일반전화 도로_165 9 0
개인사업자 내선전화 도로_71 9 0
개인 IPTV_E 도로_271 462 0
개인사업자 내선전화 도로_24 16 0
... ... ... ... ...
법인 biz_인터넷전화_A 도로_157 234 0
법인 일반전화 도로_146 167 10
법인 상품_153 도로_271 809 0
개인 인터넷_A 도로_190 19 0
법인 일반전화 도로_35 88 0

159551 rows × 5 columns

i) Label Encoding 대상 추출 및 변환

In [143]:
#Label Encoding 대상 컬럼 지정
le_columns=train_data.columns
In [144]:
le_columns
Out[144]:
Index(['TYPE', 'PRODUCT_NM', 'ROAD_NM', 'BUILDING_NO1', 'BUILDING_NO2'], dtype='object')

범주형 데이터의 범주에 변경이 발생하는 경우가 있습니다.
이런 경우에는 아래와 같이 예외처리를 해주어야 합니다.

In [149]:
from sklearn.preprocessing import LabelEncoder

# #실습 코드 - LabelEncoder를 객체로 생성한 후 , fit( ) 과 transform( ) 으로 label 인코딩 수행. 
le = LabelEncoder()

for column in le_columns:
    le.fit(train_data[column]) #fitting 
    train_data[column]=le.fit_transform(train_data[column]) #encoding 적용
    if column =='TYPE':
        print(le.classes_)
    #train_data에 없는 label이 test_data에 있을 수 있으므로 아래 코드가 필요하며, test_data는 fit 없이 transform만 해야함
    for label in np.unique(test_data[column]):
        if label not in le.classes_: # unseen label 데이터인 경우( )
            le.classes_ = np.append(le.classes_, label) # 미처리 시 ValueError발생
    test_data[column]=le.transform(test_data[column])  #encoding 적용       
[0 1 2]
In [150]:
train_data.shape, test_data.shape
Out[150]:
((159551, 5), (17728, 5))

※ 모델링 과정에서 Tree 계열 모델을 사용하기 때문에, 교육 중에 One-Hot Encoding은 진행하지 않습니다.
범주형 데이터의 범주가 많은 경우 Tree 계열 모델의 학습속도가 느립니다.

Label Encoding한 데이터를 구분해서 저장합니다.

In [151]:
train_data.shape
Out[151]:
(159551, 5)

다. Train data와 Validation data 분할

Label Encoding 데이터 분할

  • train vs validation 데이터 분리 (feature label 분리)

test_size: test_size의 크기
random_state: random_state를 동일하게 유지해야 일정한 데이터가 나옴
stratify: label을 균등하게 분포하도록 함

In [249]:
train_data
Out[249]:
TYPEPRODUCT_NMROAD_NMBUILDING_NO1BUILDING_NO212757187517109667365610150...17387314261613834476026112303
2 18 12 44 0
0 211 74 8 0
1 21 385 8 0
0 4 190 347 0
1 21 156 15 0
... ... ... ... ...
2 16 65 226 0
2 211 53 166 10
2 73 190 391 0
0 205 102 18 0
2 211 276 87 0

159551 rows × 5 columns

In [250]:
y = train_data.iloc[:,0]
In [252]:
#실습 코드 사이킷런의 train_test_split로 데이터를 train셋과 validation셋으로 분리하세요. (feature와 label을 입력값으로 받습니다. staratify에는 label 값이 들어갑니다.)
from sklearn.model_selection import train_test_split

x_train, x_val, y_train, y_val = train_test_split(train_data.iloc[:,1:],train_data.iloc[:,0] ,test_size=0.2,random_state=21,stratify=train_data.iloc[:,0])

x_train.shape, x_val.shape, y_train.shape, y_val.shape
Out[252]:
((127640, 4), (31911, 4), (127640,), (31911,))
  • test 데이터 feature label 분리
In [253]:
test_data
Out[253]:
TYPEPRODUCT_NMROAD_NMBUILDING_NO1BUILDING_NO21524282930...177260177262177275177276177277
1 21 102 18 0
1 21 385 68 0
1 21 391 165 0
1 21 292 29 0
1 21 41 122 0
... ... ... ... ...
2 16 190 394 0
2 211 345 23 0
0 211 320 289 0
0 205 320 289 0
0 0 320 289 0

17728 rows × 5 columns

In [254]:
x_test=test_data.iloc[:,1:]
y_test=test_data.iloc[:,0]
In [255]:
x_test
Out[255]:
PRODUCT_NMROAD_NMBUILDING_NO1BUILDING_NO21524282930...177260177262177275177276177277
21 102 18 0
21 385 68 0
21 391 165 0
21 292 29 0
21 41 122 0
... ... ... ...
16 190 394 0
211 345 23 0
211 320 289 0
205 320 289 0
0 320 289 0

17728 rows × 4 columns

라. 알고리즘 별 학습과 검증

※ 모델별 성능을 저장하기 위한 함수 선언

아래 함수는 외우실 필요없이 저장해두고 Copy 해서 사용하시면 됩니다.

In [256]:
import matplotlib.pyplot as plt
import seaborn as sns

my_predictions = {}

colors = ['r', 'c', 'm', 'y', 'k', 'khaki', 'teal', 'orchid', 'sandybrown',
          'greenyellow', 'dodgerblue', 'deepskyblue', 'rosybrown', 'firebrick',
          'deeppink', 'crimson', 'salmon', 'darkred', 'olivedrab', 'olive', 
          'forestgreen', 'royalblue', 'indigo', 'navy', 'mediumpurple', 'chocolate',
          'gold', 'darkorange', 'seagreen', 'turquoise', 'steelblue', 'slategray', 
          'peru', 'midnightblue', 'slateblue', 'dimgray', 'cadetblue', 'tomato'
         ]


#acc를 구해서 시각화해줌
def acc_eval(name_, pred, actual):
    global predictions
    global colors

    acc = (pred==actual).mean()
    my_predictions[name_] = acc


    y_value = sorted(my_predictions.items(), key=lambda x: x[1], reverse=False)  # 정확도 내림차순으로 sort
    
    df = pd.DataFrame(y_value, columns=['model', 'acc'])
    #print(df)
    min_ = df['acc'].min() -0.5
    max_ = 1.2
    
    length = len(df)
    
    plt.figure(figsize=(10, length))
    ax = plt.subplot()
    ax.set_yticks(np.arange(len(df)))
    ax.set_yticklabels(df['model'], fontsize=15)
    bars = ax.barh(np.arange(len(df)), df['acc'])
    
    for i, v in enumerate(df['acc']):
        idx = np.random.choice(len(colors))
        bars[i].set_color(colors[idx])
        ax.text(v+0.1, i, str(round(v, 3)), color='k', fontsize=15, fontweight='bold')
        
    plt.title('Accuracy', fontsize=18)
    plt.xlim(min_,max_)
    
    plt.show()
    
#실수로 잘못 넣은 경우 해당 모델을 삭제
def remove_model(name_):
    global my_predictions
    try:
        del my_predictions[name_]
    except KeyError:
        return False
    return True

#출처: 패스트캠퍼스-직장인을위한 파이썬 데이터분석(이경록) 발췌

i) Decision Tree

Parameter
min_samples_split : 내부 노드를 분할하는데 필요한 최소 샘플 수 (작을 수록 과적합 가능성이 큼)
min_samples_leaf: 리프노드의 최소 샘플 수 (작을 수록 과적합 가능성이 큼)
max_features : 최상의 분할을 찾을 때 고려할 기능의 수
max_depth : 트리의 최대 깊이
max_leaf_nodes : 리프노드의 개수

Step 0. 라이브러리 로딩

In [257]:
#실습 코드 - sklearn의 DecisionTreeClassifier를 로딩합니다.
from sklearn.tree import DecisionTreeClassifier

Step 1. 모델 생성

In [258]:
#실습 코드 - 모델을 생성합니다. 
model_dt = DecisionTreeClassifier(min_samples_split=3,
                                  min_samples_leaf=1,
                                  max_features=None,
                                  max_depth=None,
                                  max_leaf_nodes=None
                                 )

Step 2. 모델 학습

In [259]:
#실습 코드 - 모델을 학습합니다.
model_dt.fit(x_train,y_train)
Out[259]:
DecisionTreeClassifier(min_samples_split=3)

Step 3. 결과 검증

In [260]:
#실습 코드 - 결과를 예측합니다. 
pred_dt=model_dt.predict(x_test)
In [261]:
#실습 코드 - acc_eval로 결과를 시각화 합니다.
acc_eval('Decision Tree',pred_dt,y_test)

Step 4. Tree의 구조 확인

In [262]:
n_nodes = model_dt.tree_.node_count
children_left = model_dt.tree_.children_left
children_right = model_dt.tree_.children_right
feature = model_dt.tree_.feature
threshold = model_dt.tree_.threshold

node_depth = np.zeros(shape=n_nodes, dtype=np.int64)
is_leaves = np.zeros(shape=n_nodes, dtype=bool)
stack = [(0, 0)]  # start with the root node id (0) and its depth (0)
while len(stack) > 0:
    # `pop` ensures each node is only visited once
    node_id, depth = stack.pop()
    node_depth[node_id] = depth

    # If the left and right child of a node is not the same we have a split
    # node
    is_split_node = children_left[node_id] != children_right[node_id]
    # If a split node, append left and right children and depth to `stack`
    # so we can loop through them
    if is_split_node:
        stack.append((children_left[node_id], depth + 1))
        stack.append((children_right[node_id], depth + 1))
    else:
        is_leaves[node_id] = True

print("The binary tree structure has {n} nodes and has "
      "the following tree structure:\n".format(n=n_nodes))
for i in range(n_nodes):
    if is_leaves[i]:
        print("{space}node={node} is a leaf node.".format(
            space=node_depth[i] * "\t", node=i))
    else:
        print("{space}node={node} is a split node: "
              "go to node {left} if X[:, {feature}] <= {threshold} "
              "else to node {right}.".format(
                  space=node_depth[i] * "\t",
                  node=i,
                  left=children_left[i],
                  feature=feature[i],
                  threshold=threshold[i],
                  right=children_right[i]))

iii) Random Forest

Parameter
n_estimators: 결정트리의 수 (bagging에 들어가는 트리의 수)
min_samples_split : 내부 노드를 분할하는데 필요한 최소 샘플 수 (작을 수록 과적합 가능성이 큼)
min_samples_leaf: 리프노드의 최소 샘플 수 (작을 수록 과적합 가능성이 큼)
max_features : 최상의 분할을 찾을 때 고려할 기능의 수
max_depth : 트리의 최대 깊이
max_leaf_nodes : 리프노드의 개수

Step 0. 라이브러리 로딩

In [264]:
#실습 코드 - sklearn에서 RandomForestClassifier를 로드합니다.
from sklearn.ensemble import RandomForestClassifier

Step 1. 모델 생성

In [265]:
#실습 코드 - 모델을 생성하고, 파라미터를 설정합니다.
model_rf= RandomForestClassifier(n_jobs=-1,n_estimators=300,
                                 min_samples_split=2,
                                 min_samples_leaf=1,
                                 max_features='auto',
                                 max_depth=None,
                                 max_leaf_nodes=None)

Step 2. 모델 학습

In [266]:
#실습 코드 - 학습을 진행합니다.
model_rf.fit(x_train,y_train)
Out[266]:
RandomForestClassifier(n_estimators=300, n_jobs=-1)

Step 3. 결과 검증

In [267]:
#실습 코드  - 결과를 예측합니다. 
pred_rf=model_rf.predict(x_test)
In [268]:
#실습 코드  - 결과를 시각화 합니다.
acc_eval('Random Forest',y_test,pred_rf) #기본

iv) XGBoost

Step 0. 라이브러리 로딩

In [187]:
install("xgboost")
In [270]:
#실습코드 - xgboost를 불러옵니다.
import xgboost as xgb

Step 1. 모델 생성

In [271]:
#실습코드 - 모델을 생성합니다. 학습파라미터는 따로 설정하겠습니다.
model_xgb = xgb.XGBClassifier()

Step 2. 모델 학습

- sklearn 래퍼 xgboost 적용

sklearn의 기본 estimator를 이용해 만들어져 앞서 공부한 알고리즘과 사용법이 동일합니다.
그러나, 이 경우 xgboost 학습 성능이 느리기 때문에, 학습이 오래 걸리는 경우에는 사용하기 어렵습니다.

- python 래퍼 xgboost 적용

sklearn을 통하지 않고, 직접 호출을 하면 성능이 더 좋습니다. 이 경우에는 xgboost 공유의 데이터형식인 DMatrix로 변환해주어야 합니다.

In [272]:
#실습 코드 dtrain을 참고해서 validation과 test 데이터도 생성합니다.
# 넘파이 형태의 학습 데이터 세트와 테스트 데이터를 DMatrix로 변환하는 예제
dtrain = xgb.DMatrix(data=x_train, label = y_train)
dval = xgb.DMatrix(data=x_val, label = y_val)
dtest = xgb.DMatrix(data=x_test, label = y_test)

1. Tree Boost Parameter

  • eta: learning rate
  • num_boost_around: 생성할 weak learner의 수
  • min_child_weight: 관측치에 대한 가중치 합의 최소로 과적합 조절 용도 (값이 크면 과적합이 감소)
  • gamma:리프 노드에서 추가분할을 만드는데 필요한 최소 손실감소 값(값이 크면 과적합이 감소)
  • max_depth:Tree 의 최대 깊이(너무 크면 과적합)
  • sub_sample:훈련 중 데이터 샘플링 비율 지정(과적합 제어)
  • colsample_bytree: 열의 서브 샘플링 비율

2. Learning Task Parameter

  • objective
      reg:linear : 회귀  
      binary:logistic : 이진분류  
      multi:softmax : 다중분류, 클래스 반환  
      multi:softprob : 다중분류, 확률 반환  
  • eval_metric
      rmse : Root Mean Squared Error
      mae : mean absolute error
      logloss : Negative log-likelihood
      error : binary classification error rate
      merror : multiclass classification error rate
      mlogloss: Multiclass logloss
      auc: Area Under Curve       
In [273]:
#실습 코드 - 파라미터를 설정합니다.
params = {'eta' : 0.3,
          'max_depth' : 3,
          'min_child_weight':1,
          'gamma':0,
          'subsample':1,
          'colsample_bytree':1,
         'objective':'multi:softmax',
         'eval_metric':'mlogloss',
         'num_class':3}
In [274]:
import time
start=time.time()

early_stopping_rounds 옵션은 XGBoost의 파라미터가 아니고 학습 수행시의 파라미터이므로 XGBClassifier()가 아니라 train()안에 넣어주어야 합니다.
안그러면 아래와 같은 warning이 나오면서 옵션이 사용되지 않습니다.
Parameters: { early_stopping_rounds } might not be used.

In [275]:
#실습 코드
# train 데이터 세트는 'train', validation 데이터 세트는 'eval' 로 명기
wlist = [(dtrain, 'train'), (dval,'eval')]
# 하이퍼 파라미터와 early stopping 파라미터를 train() 함수의 파라미터로 전달
model_xgb = xgb.train(params = params, dtrain=dtrain, num_boost_round=70, evals=wlist,early_stopping_rounds=20,)
 
In [276]:
(time.time()-start)/60
Out[276]:
3.010620717207591

Step 3. 결과 검증

In [277]:
#실습 코드 - 결과 예측
pred_xgb=model_xgb.predict(dtest)
In [278]:
#실습 코드 -  결과 시각화
acc_eval('XGBoost',y_test,pred_xgb) #기본

v) Catboost

※도큐먼트: https://catboost.ai/

Parameter
Catboost는 Parameter에 민감하지 않으므로 별도로 설정하지 않겠습니다.

Step 0. 라이브러리 로딩

In [279]:
install('catboost')
In [280]:
#실습 코드
import time
start=time.time()

from catboost import CatBoostClassifier, Pool

Step 1. 모델 생성

In [281]:
#실습 코드 - 모델을 생성하고, 파라미터를 설정합니다.
cat = CatBoostClassifier(learning_rate=0.3,iterations=3000)

Step 2. 모델 학습

In [283]:
#실습 코드 - Pool 함수로 데이터셋을 지정합니다. 반드시 label을 지정해 주어야 합니다.
train_dataset = Pool(data=x_train,label=y_train)  #feature와 label 지정
eval_dataset = Pool(data=x_val,label=y_val)   #feature와 label 지정

cat.fit(train_dataset, eval_set=eval_dataset,early_stopping_rounds=20)

 

learning curve 그리기

In [284]:
# retrieve performance metrics
results = cat.evals_result_
# plot learning curves
plt.plot(results['learn']['MultiClass'], label='train')
plt.plot(results['validation']['MultiClass'], label='test')
# show the legend
plt.legend()
# show the plot
plt.show()
In [285]:
(time.time()-start)/60
Out[285]:
1.836858073870341

Step 3. 결과 검증

In [286]:
#실습 코드  - 결과를 예측합니다. 
pred_cat=cat.predict(eval_dataset) #기본
In [287]:
pred_cat
Out[287]:
array([[0],
       [2],
       [0],
       ...,
       [1],
       [1],
       [0]])
In [288]:
y_test
Out[288]:
15        1
24        1
28        1
29        1
30        1
         ..
177260    2
177262    2
177275    0
177276    0
177277    0
Name: TYPE, Length: 17728, dtype: int64
In [289]:
pred_cat=pred_cat.reshape(1,-1)[0]
In [290]:
pred_cat
Out[290]:
array([0, 2, 0, ..., 1, 1, 0])
In [291]:
#실습 코드  - 결과를 시각화 합니다.
acc_eval('Catboost',y_val,pred_cat) #기본

마. 평가지표 활용

i) Confusion Matrix

In [292]:
from sklearn.metrics import confusion_matrix, plot_confusion_matrix

plot=plot_confusion_matrix(cat, x_test,y_test, 
                           display_labels=['개인','개인사업자','법인'],
                           cmap=plt.cm.Blues)

plot.ax_.set_title('Confusion Matrix')
                           
Out[292]:
Text(0.5, 1.0, 'Confusion Matrix')

ii) Accuracy/Presision/recall/f1_score

In [297]:
from sklearn.metrics import precision_score, recall_score
from sklearn.metrics import f1_score, accuracy_score
def metric_func(actual,pred):
    print("accuracy_score:",accuracy_score(actual,pred))
    print("precision_score:",precision_score(actual,pred,average=None))
    print("recall_score:",recall_score(actual,pred,average=None))
    print("f1_score:",f1_score(actual,pred,average=None))

 

metric_func(y_test,pred_cat)

 

iii) ROC Curve & AUC

from sklearn.metrics import roc_curve, auc
#Parameter
#y_pred: 원핫인코딩된 2차원 이상의 numpy.array
#y_true: 실제 값
#y_label:label값(list)

def plot_multiclass_roc(y_pred, y_true, y_label):
    
    fpr =dict()
    tpr =dict()
    roc_auc = dict()
    
    y_test_dummies = pd.get_dummies(y_true, drop_first=False).values
    for i in range(len(y_label)):
        fpr[i], tpr[i], _ = roc_curve(y_test_dummies[:,i],y_pred[:,i])
        roc_auc[i] = auc(fpr[i], tpr[i])
        
    fig, ax = plt.subplots(figsize=(17,10))
    ax.plot([0,1],[0,1],'k--')
    ax.set_xlim([0.0,1.0])
    ax.set_ylim([0.0,1.05])
    ax.set_xlabel('False Positive Rate')
    ax.set_ylabel('True Positive Rate')
    ax.set_title('ROC')
    for i in range(len(y_label)):
        ax.plot(fpr[i],tpr[i], label = 'ROC curve (area = %0.2f) for label %s' %(roc_auc[i], y_label[i]))
    ax.legend(loc='best')
    ax.grid(alpha=.4)
    sns.despine()
    plt.show()
    
In [301]:
pred_cat
Out[301]:
array([0, 2, 0, ..., 1, 1, 0])
In [302]:
y_test
Out[302]:
15        1
24        1
28        1
29        1
30        1
         ..
177260    2
177262    2
177275    0
177276    0
177277    0
Name: TYPE, Length: 17728, dtype: int64
In [303]:
plot_multiclass_roc(pd.get_dummies(pred_cat).to_numpy(),y_test,['개인','개인사업자','법인'])
pd.get_dummies(pred_cat).to_numpy()
Out[304]:
array([[1, 0, 0],
       [0, 0, 1],
       [1, 0, 0],
       ...,
       [0, 1, 0],
       [0, 1, 0],
       [1, 0, 0]], dtype=uint8)

ROC Curve는 개인/개인사업자/법인의 예측 확률을 Threshold로 사용하기 때문에 원핫 벡터 형태의 데이터를 넣으면 그림처럼 각이 생기게 됩니다.

그래서 predict_proba를 쓰면 확률로 표시되는 값을 얻을 수 있고, 이 값을 이용하면 동일한 결과를 얻을 수 있습니다.

In [305]:
pred_cat_proba = cat.predict_proba(x_test)
In [306]:
pred_cat_proba
Out[306]:
array([[1.12419020e-03, 9.98722088e-01, 1.53722265e-04],
       [2.31345713e-03, 9.97055494e-01, 6.31048586e-04],
       [3.90836336e-03, 9.95607040e-01, 4.84596253e-04],
       ...,
       [2.11195572e-01, 6.33518012e-01, 1.55286416e-01],
       [2.45785784e-01, 6.14803218e-01, 1.39410999e-01],
       [2.82041819e-01, 6.18208448e-01, 9.97497334e-02]])
In [307]:
plot_multiclass_roc(pred_cat_proba,y_test,['개인','개인사업자','법인'])

좀더 부드러운 곡선의 ROC Curve를 얻었습니다.