파이썬 객체를 일정한 규칙(규약)을 따라서 (a) 효율적으로 저장하거나 스트림으로 전송할 때 파이썬 객체의 데이터를 줄로 세워 저장하는 것을 직렬화(serialization) 라고 하고, (b) 이렇게 직렬화된 파일이나 바이트를 원래의 객체로 복원하는 것을 역직렬화(de-serialization)라고 합니다. 


직렬화, 역직렬화를 하는데 Pickle, JSON, YAML 등 여러가지 방법이 있습니다. Pickle 방식은 사람이 읽을 수 없는 반면 저장이나 전송이 효율적입니다. 대신 JSON, YAML 방식은 저장이나 전송이 Pickle 보다는 덜 효율적이지만 사람이 읽을 수 있는 가독성이 좋은 장점이 있습니다. (* 파이썬에서 JSON 데이터 읽고 쓰기: https://rfriend.tistory.com/474)


이번 포스팅에서는 Pickle 방식으로 이진 파일이나 바이트 객체로 직렬화하고, 이를 다시 역직렬화해서 불러오는 방법에 대해서 소개하겠습니다. Pickle 방식에서는 직렬화(serialization)는 Pickling 이라고도 하며, 역직렬화(deserialization)는 Unpickling 이라고도 합니다. 


파이썬에서 Pickle 방식으로 직렬화, 역직렬화하는데는 파이썬으로 작성되고 파이썬 표준 라이브러리에 포함되어 있는 pickle module, C로 작성되어 매우 빠르지만 Subclass 는 지원하지 않는 cPickle module 의 두 가지 패키지를 사용할 수 있습니다. (* source : https://pymotw.com/2/pickle/)


파이썬의 Pickle 방식으로 직렬화할 수 있는 객체는 모든 파이썬 객체를 망라합니다. (정수, 실수, 복소소, 문자열, 블리언, 바이트 객체, 바이트 배열, None, 리스트, 튜플, 사전, 집합, 함수, 클래스, 인스턴스 등)


이번 포스팅에서는 Python 3.7 버전(protocol = 3)에서 pickle 모듈을 사용하여 


(1-1) 파이썬 객체를 직렬화하여 이진 파일(binary file)에 저장하기 : pickle.dump()

(1-2) 직렬화되어 있는 이진 파일로 부터 파이썬 객체로 역직렬화하기: pickle.load()


(2-1) 파이썬 객체를 직렬화하여 메모리에 저장하기: pickle.dumps()

(2-2) 직렬화되어 있는 바이트 객체(bytes object)를 파이썬 객체로 역직렬화: pickle.loads()


로 나누어서 각 방법을 소개하겠습니다. 


pickle.dump()와 pickle.dumps() 메소드, 그리고 pickle.load()와 pickle.loads() 메소드의 끝에 's'가 안붙고 붙고의 차이를 유심히 봐주세요. 


 구분

 Binary File on disk

Bytes object on memory 

 직렬화

(serialization)

 with open("file.txt", "wb") as MyFile:

     pickle.dump(MyObject, MyFile)

 pickle.dumps(MyObject, MyBytes)

 역직렬화

(deserialization)

 with open("file.txt", "rb") as MyFile:

     MyObj2 = pickle.load(MyFile)

 MyObj2 = pickle.loads(MyBytes)




직렬화, 역직렬화를 할 때 일정한 규칙을 따라야 하는데요, 파이썬 버전별로 Pickle Protocol Version 은 아래에 정리한 표를 참고하시기 바랍니다. 그리고 상위 버전의 Pickle Protocol Version에서 저장한 경우 하위 버전에서 역직렬화할 수 없으므로 주의가 필요합니다. (가령 Python 3.x에서 Protocol = 3 으로 직렬화해서 저장한 파일을 Python 2.x 에서 Protocol = 2 버전으로는 역직렬화 할 수 없습니다.)




먼저, 예제로 사용할 정수, 텍스트, 실수, 블리언 고유 자료형을 포함하는 파이썬 사전 객체 (python dictionary object)를 만들어보겠습니다. 



MyObject = {'id': [1, 2, 3, 4], 

           'name': ['KIM', 'CHOI', 'LEE', 'PARK'], 

           'score': [90.5, 85.7, 98.9, 62.4], 

           'pass_yn': [True, True, True, False]}





 (1-1) 파이썬 객체를 직렬화하여 이진 파일(binary file)에 저장하기 : pickle.dump()



import pickle


with open("serialized_file.txt", "wb") as MyFile:

    pickle.dump(MyObject, MyFile, protocol=3)



with open("serialized_file.txt", "wb") as MyFile 함수를 사용해서 "serialized_file.txt" 라는 이름의 파일을 이진 파일 쓰기 모드 ("wb") 로 열어 놓습니다. (참고로, with open() 은 close() 를 해줄 필요가 없습니다.)


pickle.dump(MyObject, MyFile) 로 위에서 만든 MyObject 사전 객체를 MyFile ("serialized_file.txt") 에 직렬화해서 저장합니다. 


Python 3.7 버전에서 작업하고 있으므로 protocol=3 으로 지정해줬는데요, Python 3.0~3.7 버전에서는 기본값이 protocol=3 이므로 안써줘도 괜찮습니다. 


현재의 작업 폴더에 가보면 "serialized_file.txt" 파일이 생성이 되어있을 텐데요, 이 파일을 클릭해서 열어보면 아래와 같이 사람이 읽을 수 없는 binary 형태로 저장이 되어 있습니다. (만약 사람이 읽을 수 있고 가독성이 좋은 저장, 전송을 원한다면 JSON, YAML 등을 사용해서 직렬화 하면됨)





 (1-2) 직렬화되어 있는 이진 파일로 부터 파이썬 객체로 역직렬화: pickle.load()



import pickle


with open("serialized_file.txt", "rb") as MyFile:

    UnpickledObject = pickle.load(MyFile)



UnpickledObject

[Out]:
{'id': [1, 2, 3, 4],
 'name': ['KIM', 'CHOI', 'LEE', 'PARK'],
 'score': [90.5, 85.7, 98.9, 62.4],
 'pass_yn': [True, True, True, False]}


with open("serialized_file.txt", "rb") as MyFile 를 사용하여 위의 (1-1)에서 파이썬 사전 객체를 직렬화하여 이진 파일로 저장했던 "serialized_file.txt" 파일을 이진 파일 읽기 모드 ("rb") 로 MyFile 이름으로 열어 놓습니다. 


UnpickledObject = pickle.load(MyFile) 로 앞에서 열어놓은 MyFile 직렬화된 파일을 역직렬화(de-serialization, unpickling, decoding) 하여 UnpickledObject 라는 이름의 파이썬 객체를 생성합니다. 


이렇게 만든 UnpickledObject 파이썬 객체를 호출해보니 다시 사람이 읽을 수 있는 사전 객체로 다시 잘 복원되었음을 알 수 있습니다. 




 (2-1) 파이썬 객체를 직렬화하여 메모리에 Bytes object로 저장하기pickle.dumps()



import pickle 


MyBytes = pickle.dumps(MyObject, protocol=3)



# unreadable bytes object

MyBytes

[Out]:

b'\x80\x03}q\x00(X\x02\x00\x00\x00idq\x01]q\x02(K\x01K\x02K\x03K\x04eX\x04\x00\x00\x00nameq\x03]q\x04(X\x03\x00\x00\x00KIMq\x05X\x04\x00\x00\x00CHOIq\x06X\x03\x00\x00\x00LEEq\x07X\x04\x00\x00\x00PARKq\x08eX\x05\x00\x00\x00scoreq\t]q\n(G@V\xa0\x00\x00\x00\x00\x00G@Ul\xcc\xcc\xcc\xcc\xcdG@X\xb9\x99\x99\x99\x99\x9aG@O333333eX\x07\x00\x00\x00pass_ynq\x0b]q\x0c(\x88\x88\x88\x89eu.'



위의 (1-1)이 파이썬 객체를 이진 파일(binary file) 로 로컬 디스크에 저장하였다면, 이번 (2-1)은 pickle.dumps(object_name, protocol=3) 을 사용해서 메모리에 Bytes object로 직렬화해서 저장하는 방법입니다. pickle.dumps() 메소드의 제일 뒤에 's'가 추가로 붙어있는 것 유의하세요. 


이렇게 직렬화해서 저장한 Bytes object의 경우 사람이 읽을 수 없는 형태입니다. (반면, 컴퓨터한테는 데이터를 저장하기에 더 효율적인 형태)




 (2-2) 직렬화되어 있는 바이트 객체를 파이썬 객체로 역직렬화: pickle.loads()



import pickle


MyObj2 = pickle.loads(MyBytes)


MyObj2

[Out]:
{'id': [1, 2, 3, 4],
 'name': ['KIM', 'CHOI', 'LEE', 'PARK'],
 'score': [90.5, 85.7, 98.9, 62.4],
 'pass_yn': [True, True, True, False]}

 


위의 (2-1)에서 직렬화하여 저장한 바이트 객체 MyBytes 를 pickle.loads() 메소드를 사용하여 역직렬화하여 MyObj2 라는 이름의 파이썬 객체를 생성한 예입니다.  


* reference: https://docs.python.org/3.7/library/pickle.html

* pickle and cPicklehttps://pymotw.com/2/pickle/


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요

지난번 포스팅에서는 파일을 열고(open), 파일을 읽고(read), 쓰고(write), 닫기(close)를 하는 방법을 소개하였습니다.  


이번 포스팅에서는 Python 에서 


(1) 문자열로 이루어진 리스트를 텍스트 파일에 쓰기

    - (1-1) 한 줄씩 쓰기 : write() 메소드 (for loop 문과 함께 사용)

    - (1-2) 한꺼번에 모든 줄 쓰기 : writelines() 메소드


(2) 텍스트 파일을 읽어오기

    - (2-1) 텍스트를 한 줄씩 읽어오기 : readline() 메소드 (while 문과 함께 사용)

    - (2-2) 텍스트를 한꺼번에 모두 읽어오기 : readlines() 메소드


에 대해서 소개하겠습니다. 



 (1-1) 문자열로 이루어진 리스트를 한 줄씩 텍스트 파일에 쓰기 : write() 메소드




예제로 사용할 텍스트는 영화 매트릭스(movie Matrix)에서 모피어스가 네오에게 말했던 대사 (Morpheus Quotes to Neo) 중 몇 개로 이루어진 파이썬 리스트입니다. '\n'으로 줄을 구분하였습니다. 



matrix_quotes = ["The Matrix is the world that has been pulled over your eyes to blind you from the truth.\n", 

                 "You have to let it all go, Neo. Fear, doubt, and disblief. Free your mind.\n", 

                 "There is a difference between knowing the path and walking the path.\n", 

                 "Welcom to the desert of the real!\n"]

 


* image source: https://www.elitecolumn.com/morpheus-the-matrix-quotes/morpheus-the-matrix-quotes-4/


위의 텍스트 문자열로 이루어진 'matrix_quotes' 리스트를 'matrix_quotes.txt'라는 이름의 파일에 한 줄씩 써보도록 하겠습니다. 먼저 with open('matrix_quotes.txt', 'w') 함수를 사용해서 'matrix_quotes.txt'라는 이름의 파일을 '쓰기 모드('w')'로 열고, 모두 4줄로 이루어진 리스트이므로 for loop 순환문을 이용해서 한줄 씩 line 객체로 가져온 다음에 f.write(line) 메소드로 한 줄씩(line by line) 'matrix_quotes.txt' 텍스트 파일에 써보겠습니다. 



with open('matrix_quotes.txt', 'w') as f:

    for line in matrix_quotes:

        f.write(line)



* 참고로, with open() 함수를 사용하면 close() 를 해주지 않아도 알아서 자동으로 close()를 해줍니다. 

* with open('matrix_quotes.txt', 'w') 에서 'w' 는 쓰기 모드를 의미합니다. 



현재 Jupyter notebook을 열어서 작업하고 있는 폴더에 'matrix_quotes.txt' 텍스트 파일이 생성되었음을 확인할 수 있습니다. 





for loop 문과 write() 메소드를 사용해서 텍스트를 파일에 쓴 'matrix_quotes.txt' 파일을 더블 클릭해서 열어보니 Python list 의 내용과 동일하고, 줄 구분도 '\n'에 맞게 잘 써진 것을 알 수 있습니다. 






 (1-2) 문자열로 이루어진 리스트의 모든 줄을 텍스트 파일에 쓰기: writelines() 메소드 


그런데 위의 write() 메소드의 경우 여러 개의 줄이 있을 경우 for loop 반복 순환문을 함께 써줘야 하는 불편함이 있습니다. 이때 writelines() 메소드를 사용하면 리스트 안의 모든 줄을 한꺼번에 텍스트 파일에 써줄 수 있어서 편리합니다.  위의 (1-1)과 동일한 작업을 writelines() 메소드를 사용해서 하면 아래와 같습니다. 



matrix_quotes = ["The Matrix is the world that has been pulled over your eyes to blind you from the truth.\n", 

                 "You have to let it all go, Neo. Fear, doubt, and disblief. Free your mind.\n", 

                 "There is a difference between knowing the path and walking the path.\n", 

                 "Welcom to the desert of the real!\n"]



with open('maxtix_quotes_2.txt', 'w') as f:

    f.writelines(matrix_quotes)

 



Jupyter notebook을 작업하고 있는 폴더에 가서 보니 'matrix_quotes_2.txt' 파일이 새로 잘 생성되었습니다. 






  (2-1) 텍스트 파일을 한 줄씩 읽어오기 : readline() 메소드


이제는 방금전 (1-1)에서 만든 'matrix_quotes.txt' 텍스트 파일로 부터 텍스트를 한 줄씩 읽어와서 matrix_quotes_list 라는 이름의 파이썬 리스트를 만들어보겠습니다. 




readline() 메소드는 텍스트 파일을 '\n', '\r', '\r\n' 과 같은 개행 문자(newline) 를 기준으로 한 줄씩 읽어오는 반면에 vs. readlines() 메소드는 텍스트 파일 안의 모든 텍스트를 한꺼번에 읽어오는 차이가 있습니다.  따라서 한 줄씩만 읽어오는 readline() 메소드를 사용해서 텍스트 파일 내 모든 줄을 읽어오려면 while 순환문을 같이 사용해주면 됩니다. 



with open('matrix_quotes.txt', 'r') as text_file:

    matrix_quotes_list = []

    line = text_file.readline()

    

    while line != '':

        matrix_quotes_list.append(line)

        line = text_file.readline()

 



matrix_quotes_list

[Out]: 
['The Matrix is the world that has been pulled over your eyes to blind you from the truth.\n',
 'You have to let it all go, Neo. Fear, doubt, and disblief. Free your mind.\n',
 'There is a difference between knowing the path and walking the path.\n',
 'Welcom to the desert of the real!\n',
 '']



* 참고로 with open('matrix_quotes.txt', 'r') 에서 'r'은 '읽기 모드'를 의미합니다. 




  (2-2) 텍스트 파일을 모두 한꺼번에 읽어오기 : readlines() 메소드


이번에는 readlines() 메소드를 사용해서 'matrix_quotes.txt' 텍스트 파일 안의 모든 줄을 한꺼번에 읽어와서 matrix_quotes_list_2 라는 이름의 파이썬 리스트를 만들어보겠습니다.  readlines() 메소드는 한꺼번에 텍스트를 읽어오기 때문의 위의 (2-1) 의 readline()메소드와 while 순환문을 함께 쓴 것도다 코드가 간결합니다. 



with open('matrix_quotes.txt', 'r') as text_file:

    matrix_quotes_list_2 = text_file.readlines()



matrix_quotes_list_2

[Out]: 
['The Matrix is the world that has been pulled over your eyes to blind you from the truth.\n',
 'You have to let it all go, Neo. Fear, doubt, and disblief. Free your mind.\n',
 'There is a difference between knowing the path and walking the path.\n',
 'Welcom to the desert of the real!\n']

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 파이썬으로 


(1) 파일을 열고, 데이터를 쓰고, 파일을 닫기

(2) 파일을 열고, 데이터를 읽고, 파일을 닫기

(3) with open(file_name) as file_object: 로 파일 열고, 데이터를 읽고, 자동으로 파일 닫기

(4) open() error 처리 옵션 설정


하는 방법을 소개하겠습니다. 




파이썬으로 파일을 열 때 open() 함수를 사용하는데요, 1개의 필수 매개변수와 7개의 선택적 매개변수를 가지며, 파일 객체를 반환합니다. open() 함수의 mode 는 'w', 'r', 'x', 'a', '+', 'b', 't'의 7개 모드가 있으며, 위의 이미지 우측에 있는 표를 참고하시기 바랍니다. ('r' 읽기용으로 파일 열기't' 텍스트 모드로 열기가 기본 설정값입니다)



open(

  file,                 # 필수 매개변수, 파일의 경로

  mode = 'r',       # 선택적 매개변수, 'rt' (읽기용, text mode)가 기본값

  buffering = -1,   # 선택적 매개변수, 버퍼링 정책 (0 : binary mode 시 버퍼링 미수행, 

                        # 1: 텍스트 모드 시 개행문자(\n)을 만날 때까지 버퍼링)

  encoding = None, # 선택적 매개변수, 문자 인코딩 방식 (예: 'utf-8', 'cp949', 'latin' 등)

  errors = None,   # 선택적 매개변수, 텍스트 모드 시 에러 처리 (예: 'ignore' 에러 무시)

  newline = None, # 선택적 매개변수, 줄바꿈 처리 (None, '\n', '\r', '\r\n')

  closefd = True,   # 선택적 매개변수, False 입력 시 파일 닫더라도 파일 기술자 계속 열어둠

  opener = None) # 선택적 매개변수, 파일을 여는 함수를 직저 구현 시 사용

 




  (1) 파일을 열고 --> 파일에 데이터를 쓰고 --> 파일을 닫기


(1-1) MyFile = open('myfile.txt', 'w') : open() 함수를 사용하여 'myfile.txt' 파일을 'w' 모드 (쓰기용으로 파일 열기, 파일이 존재하지 않으면 새로 생성, 파일이 존재하면 파일 내용을 비움(truncate)) 열어서 MyFile 객체에 할당하고,  

(1-2) MyFile.write('data') : MyFile 객체에 'data'를 쓴 후에, 

(1-3) MyFile.close() : 파일을 닫습니다. 자원 누수 방지를 위해 마지막에는 꼭 파일을 닫아(close) 주어야 합니다. 


참고로, Windows 10 OS의 기본 encoding 은 'cp949' 입니다. 



# Open and Write

MyFile = open('myfile.txt', 'w')

MyFile.write('Open, read, write and close a file using Python')

MyFile.close()


# encoding = 'cp949' for Windows10 OS

MyFile

[Out] <_io.TextIOWrapper name='myfile.txt' mode='w' encoding='cp949'>

 



현재 작업 경로에 위에서 만든 'myfile.txt' 파일이 생성이 되어서 존재하는지 확인해보겠습니다. 



import os

os.listdir(os.getcwd())

[Out]:

['myfile.txt', 'Python_read_write_file.ipynb']

 




  (2) 파일을 열고 --> 파일의 데이터를 읽고 --> 파일을 닫기


(2-1) MyFile = open('myfile.txt', 'r') : 이번에는 (1)번에서 데이터를 써서 만들어놓은 'myfile.txt' 파일을 open() 함수의 'r' 모드 (읽기용으로 파일 열기, default) 로 열어서 MyFile 객체에 할당하고, 

(2-2) MyString = MyFile.read() : 'myfile.txt' 파일을 읽어서 MyString 객체에 데이터를 저장하고, print(MyString) 로 인쇄를 한 후, 

(2-3) MyFile.close() : 파일을 닫습니다. 자원 누수 방지를 위해 마지막에는 꼭 파일을 닫아(close) 주어야 합니다. 


# Open and Read

MyFile = open('myfile.txt', 'r')


# equivalent to the above

#MyFile = open('myfile.txt', 'rt')

#MyFile = open('myfile.txt', 't')

#MyFile = open('myfile.txt')


MyString = MyFile.read()

print(MyString)


[Out]: Open, read, write and close a file using Python


# close the 'MyFile' file

MyFile.close()



open() 함수의 7개 mode 중에서 'r' 읽기용으로 열기와 't' 텍스트 모드로 열기가 기본값이므로 위의 open('myfile.txt', 'r') 은 open('myfile.txt', 'rt'), open('myfile.txt', 't'), open('myfile.txt') 와 동일한 코드입니다. 




  (3) with open(파일 이름) as 파일 객체: 를 사용해서 파일 열기


with open() as 문을 사용하여 파일을 열면 마지막에 close() 함수를 명시적으로 써주지 않아도 자동으로 파일이 닫힙니다. 위의 (2) 번 코드를 with open(file_name) as file_object: 문으로 바꿔주면 아래와 같습니다. 



# Using the with statement the file is automatically closed

with open('myfile.txt', 'r') as MyFile:

    MyString = MyFile.read()

    print(MyString)

    # no need for MyFile.close()


[Out]: Open, read, write and close a file using Python

 




  (4) 텍스트 모드에서 인코딩, 디코딩 에러 발생 시 처리 : errors


가령, 파일에서 데이터를 읽거나 쓰는 와중에 에러가 발생했을 시 무시하고 싶다면 open('file.txt', 'rw', errors = 'ignore') 라고 설정을 해주면 됩니다. 


errors 매개변수 

error 처리 방법 

 'strict'

 인코딩 에러 발생 시 ValueError 예외

 'ignore'

 에러 무시

 'replace'

 대체 기호 삽입 (예: "?")

 'surrogateescape'

 U+DC80~U+DCFF 사이의 유니코드 사용자 자유 영역의 잘못된 바이트를 code points 로 나타냄

 'xmlcharrefreplace'

 파일을 쓸 때 파일에 기록하려는 텍스트 안의 지정된 인코딩에서 지원되지 않는 문자를 &#NNN; 의 XML 문자 참조로 바꿔서 기록

 'backslashreplace'

 파일을 쓸 때 현재 인코딩에서 지원되지 않는 문자를 역슬래시(back slash, \)로 시작되는 escape sequence로 바꿔서 기록



다음번 포스팅에서는 줄 단위로 텍스트 파일을 읽고 쓰는 방법을 소개하겠습니다. 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)


Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 연속형 변수를 여러개의 구간별로 구분하여 범주형 변수로 변환(categorization of a continuous variable by multiple bins) 하는 두가지 방법을 비교하여 설명하겠습니다. 


(1) np.digitize(X, bins) 를 이용한 연속형 변수의 여러개 구간별 범주화

(2) pd.cut(X, bins, labels) 를 이용한 연속형 변수의 여러개 구간별 범주화 





np.digitize(X, bins)와 pd.cut(X, bins, labels)  함수가 서로 비슷하면서도 사용법에 있어서는 모든 면에서 조금씩 다르므로 각 함수의 syntax에 맞게 정확하게 확인하고서 사용하기 바랍니다. 



[ np.digitize()와 pd.cut() 비교 ]


 구분

 np.digitize(X, bins)

pd.cut(X, bins, labels) 

 bins=[start, end]

 [포함, 미포함)

 (미포함, 포함)

 bin 구간 대비 

 작거나 큰 수

 bin 첫 구간 보다 작으면 [-inf, start)

 --> 자동으로 '1'로 digitize 


 bin 마지막 구간 보다 크면 [end, inf)

 --> 자동으로 bin 순서에 따라 digitize

 bin 첫번째 구간보다 작으면 --> NaN


 bin 마지막 구간보다 크면 --> Nan

 label

 0, 1, 2, ... 순서의 양의 정수 자동 설정

 사용자 지정 가능 (labels option)

 반환 (return)

 numpy array

 a list of categories with labels




  (1) np.digitize(X, bins) 를 이용한 연속형 변수의 여러개 구간별 범주화


먼저 예제로 사용할 간단한 pandas DataFrame을 만들어보겠습니다. 



import pandas as pd

import numpy as np


df = pd.DataFrame({'col': np.arange(10)})

df

col
00
11
22
33
44
55
66
77
88
99





이제 np.digitize(X, bins=[0, 5, 8]) 함수를 사용해서 {[0, 5), [5, 8), [8, inf)} 구간 bin 별로 {1, 2, 3} 의 순서로 양의 정수를 자동으로 이름을 부여하여 'grp_digitize'라는 이름의 새로운 칼럼을 df DataFrame에 만들어보겠습니다. 


참고로 '(' 또는 ')'는 미포함 (not included), '[' 또는 ']' 보호는 포함(included)을 나타냅니다. 



bins=[0, 5, 8]


# returns numpy array

np.digitize(df['col'], bins)

[Out]: array([1, 1, 1, 1, 1, 2, 2, 2, 3, 3])


df['grp_digitize'] = np.digitize(df['col'], bins)

df

[Out]:

colgrp_digitize
001
111
221
331
441
552
662
772
883
993

 





  (2) pd.cut(X, bins, labels) 를 이용한 연속형 변수의 여러개 구간별 범주화 


이번에는 pd.cut(X, bins=[0, 5, 8]) 을 이용하여 {(0, 5], (5, 8]} 의 2개 구간별로 범주화해보겠습니다. array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 의 각 원소가 어느 bin에 속하는지를 나타내는 category 리스트를 반환합니다. 



import pandas as pd

import numpy as np


df = pd.DataFrame({'col': np.arange(10)})


# pd.cut(미포함, 포함]

bins=[0, 5, 8]


# returns a list of catogiries with labels

pd.cut(df["col"], bins=bins)

[Out]:

0 NaN 1 (0.0, 5.0] 2 (0.0, 5.0] 3 (0.0, 5.0] 4 (0.0, 5.0] 5 (0.0, 5.0] 6 (5.0, 8.0] 7 (5.0, 8.0] 8 (5.0, 8.0] 9 NaN Name: col, dtype: category Categories (2, interval[int64]): [(0, 5] < (5, 8]]


 



위 (1)번의 np.digitize() 가 [포함, 미포함) 인 반면에 pd.cut()은 (미포함, 포함]으로 정반대입니다. 


위 (1)번의 np.digitize() 가 bin 안의 처음 숫자보다 작거나 같은 값에 자동으로 '1'의 정수를 부여하고, bin 안의 마지막 숫자보다 큰 값에 대해서는 bin 순서에 따라 자동으로 digitze 정수를 부여하는 반면에, pd.cut()은 bin 구간에 없는 값에 대해서는 'NaN'을 반환하고 bin 구간 내 값에 대해서는 사용자가 labels=['a', 'b'] 처럼 입력해준 label 값을 부여해줍니다. 



df['grp_cut'] = pd.cut(df["col"], bins=bins, labels=['a', 'b'])

df

[Out]:

colgrp_digitizegrp_cut
001NaN
111a
221a
331a
441a
552a
662b
772b
883b
993NaN





이렇게 연속형 변수를 범주형 변수로 변환을 한 후에 'col' 변수에 대해 groupby('grp_cut') 로 그룹별 합계(sum by group)를 집계해 보겠습니다. 



df.groupby('grp_cut')['col'].sum()

[Out]:
grp_cut
a    15
b    21
Name: col, dtype: int64




'grp_cut' 기준 그룹('a', 'b')별로 합(sum), 개수(count), 평균(mean), 분산(variance) 등의 여러개 통계량을 한번에 구하려면 사용자 정의 함수를 정의한 후에 --> df.groupby('grp_cut').apply(my_summary) 처럼 apply() 를 사용하면 됩니다. 그룹별로 통계량을 한눈에 보기에 좋도록 unstack()을 사용해서 세로로 길게 늘어선 결과를 가로로 펼쳐서 제시해보았습니다. 



# UDF of summary statistics

def my_summary(x):

    result = {

        'sum': x.sum()

        'count': x.count()

        'mean': x.mean()

        'variance': x.var()

    }

    return result



df.groupby('grp_cut')['col'].apply(my_summary).unstack()

[Out]:
sumcountmeanvariance
grp_cut
a15.05.03.02.5
b21.03.07.01.0




많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)


Posted by R Friend Rfriend

댓글을 달아 주세요

  1. 덕담 2020.10.26 00:45  댓글주소  수정/삭제  댓글쓰기

    저,,age 분포가 21-85 인데 age를 20-30, 31-40, 41-50, 50+ 네 그룹으로 분휴하려면 50+이상인 경우에 대해서는 명령어를 어떻게 써야 할지요?

    • R Friend Rfriend 2020.10.26 01:12 신고  댓글주소  수정/삭제

      안녕하세요.
      age 배열을 4개의 그룹으로 범주화한다고 했을 때 np.digitize()와 pd.cut()의 경우 범주 구간의 시작과 끝의 포함, 미포함이 서로 다르므로 이를 신경써줘야 합니다.

      import numpy as np
      import pandas as pd

      # (1) np.digitize() 이용 시
      a_digitize = np.digitize(a, bins=[20, 31, 41, 51])

      # (2) pd.cut() 이용 시
      a_cut = pd.cut(a, bins=[20, 30, 40, 50, 100])

지난번 포스팅에서는 무작위로 데이터셋을 추출하여 train set, test set을 분할(Train set, Test set split by Random Sampling)하는 방법을 소개하였습니다. 


이번 포스팅에서는 데이터셋 내 층(stratum) 의 비율을 고려하여 층별로 구분하여 무작위로 train set, test set을 분할하는 방법(Train, Test set Split by Stratified Random Sampling)을 소개하겠습니다. 


(1) sklearn.model_selection.train_test_split 함수를 이용한 Train, Test set 분할

     (층을 고려한 X_train, X_test, y_train, y_test 반환) 


(2)sklearn.model_selection.StratifiedShuffleSplit 함수를 이용한 Train, Test set 분할

    (층을 고려한 train/test indices 반환 --> Train, Test set indexing)

참고로 단순 임의 추출(Simple Random Sampling), 체계적 추출(Systematic Sampling), 층화 임의 추출(Stratified Random Sampling), 군집 추출(Cluster Sampling), 다단계 추출(Multi-stage Sampling) 방법에 대한 소개는 https://rfriend.tistory.com/58 를 참고하세요.

 




  (1) sklearn.model_selection.train_test_split 함수를 이용한 Train, Test set 분할

      (층을 고려한 X_train, X_test, y_train, y_test 반환)


먼저 간단한 예제로 사용하기 위해 15행 2열의  X 배열, 15개 원소를 가진 y 배열 데이터셋을 numpy array 를 이용해서 만들어보겠습니다. 그리고 앞에서 부터 5개의 관측치는 '0' 그룹(층), 6번째부터 15번째 관측치는 '1' 그룹(층)에 속한다고 보고, 이 정보를 가지고 있는 'grp' 리스트도 만들겠습니다. 



import numpy as np


X = np.arange(30).reshape(15, 2)

X

[Out]: array([[ 0, 1], [ 2, 3], [ 4, 5], [ 6, 7], [ 8, 9], [10, 11], [12, 13], [14, 15], [16, 17], [18, 19], [20, 21], [22, 23], [24, 25], [26, 27], [28, 29]])


y = np.arange(15)

y

[Out]:

array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])


# stratum (group)

grp = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

grp

[Out]:
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]





이제 scikit-learn model_selection 클래스에서 train_test_split 함수를 가져와서 X_train, X_test, y_train_y_test 데이터셋을 분할해 보겠습니다. 

- X와 y 데이터셋이 따로 분리되어 있는 상태에서 처음과 두번째 위치에 X, y를 각각 입력해줍니다. 

- test_size에는 test set의 비율을 입력하고 stratify에는 층 구분 변수이름을 입력해주는데요, 이때 각 층(stratum, group) 별로 나누어서 test_size 비율을 적용해서 추출을 해줍니다.

- shuffle=True 를 지정해주면 무작위 추출(random sampling)을 해줍니다. 만약 체계적 추출(systematic sampling)을 하고 싶다면 shuffle=False를 지정해주면 됩니다. 

- random_state 는 재현가능성을 위해서 난수 초기값으로 아무 숫자나 지정해주면 됩니다. 



# returns X_train, X_test, y_train, y_test dataset

from sklearn.model_selection import train_test_split


X_train, X_test, y_train, y_test = train_test_split(X, 

                                                    y, 

                                                    test_size=0.2, 

                                                    shuffle=True,

                                                    stratify=grp

                                                    random_state=1004)


print('X_train shape:', X_train.shape)

print('X_test shape:', X_test.shape)

print('y_train shape:', y_train.shape)

print('y_test shape:', y_test.shape)

[Out]:
X_train shape: (12, 2)
X_test shape: (3, 2)
y_train shape: (12,)
y_test shape: (3,)





아래는 X_train, y_train, X_test, y_test 로 각각 분할된 결과입니다. 



X_train

[Out]:
array([[12, 13],
       [ 8,  9],
       [28, 29],
       [ 0,  1],
       [10, 11],
       [ 6,  7],
       [ 2,  3],
       [18, 19],
       [20, 21],
       [22, 23],
       [26, 27],
       [14, 15]])


y_train

[Out]: 
array([ 6,  4, 14,  0,  5,  3,  1,  9, 10, 11, 13,  7])



X_test

[Out]:
array([[16, 17],
       [ 4,  5],
       [24, 25]])



y_test

[Out]: array([ 8,  2, 12])






  (2) sklearn.model_selection.StratifiedShuffleSplit 함수를 이용한 Train, Test set 분할

       (층을 고려한 train/test indices 반환 --> Train, Test set indexing)


(2-1) numpy array 예제


위의 train_test_split() 함수가 X, y를 input으로 받아서 각 층의 비율을 고려해 무작위로 X_train, X_test, y_train, y_test 로 분할된 데이터셋을 반환했다고 하며, 이번에 소개할 StratfiedShuffleSplit() 함수는 각 층의 비율을 고려해 무작위로 train/test set을 분할할 수 있는 indices 를 반환하며, 이 indices를 이용해서 train set, test set을 indexing 하는 작업을 추가로 해줘야 합니다. 위의 (1)번 대비 좀 불편하지요? (대신 이게 k-folds cross-validation 할 때n_splits 를 가지고 층화 무작위 추출할 때는 위의 (1)번 보다 편리합니다)


1개의 train/ test set 만을 분할하므로 n_splits=1 로 지정해주며, test_size에 test set의 비율을 지정해주고, random_state에는 재현가능성을 위해 난수 초기값으로 아무값이 지정해줍니다. 


train_idx, test_idx 를 반환하므로 for loop문을 사용해서 X_train, X_test, y_train, y_test를 X와 y로 부터 indexing해서 만들었습니다. 



i# Stratified ShuffleSplit cross-validator 

# provides train/test indices to split data in train/test sets.

from sklearn.model_selection import StratifiedShuffleSplit


split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=1004)


for train_idx, test_idx in split.split(X, grp):

    X_train = X[train_idx]

    X_test = X[test_idx]

    y_train = y[train_idx]

    y_test = y[test_idx]

 



X_train, y_train, X_test, y_test 값을 확인해보면 아래와 같은데요, 이는 random_state=1004로 (1)번과 같게 설정해주었기때문에 (1)번의 train_test_split() 함수를 사용한 결과와 동일한 train, test set 데이터셋이 층화 무작위 추출법으로 추출되었습니다. 



X_train

[Out]:

array([[12, 13], [ 8, 9], [28, 29], [ 0, 1], [10, 11], [ 6, 7], [ 2, 3], [18, 19], [20, 21], [22, 23], [26, 27], [14, 15]])



y_train

[Out]: array([ 6, 4, 14, 0, 5, 3, 1, 9, 10, 11, 13, 7])



X_test

[Out]:
array([[16, 17],
       [ 4,  5],
       [24, 25]])



y_test

[Out]: array([ 8,  2, 12])





(2-2) pandas DataFrame 예제


위의 (2-1)에서는 numpy array를 사용해서 해보았는데요, 이번에는 pandas DataFrame에 대해서 StratifiedShuffleSplit() 함수를 사용해서 층화 무작위 추출법을 이용한 Train, Test set 분할을 해보겠습니다. 


먼저, 위에서 사용한 데이터셋과 똑같이 값으로 구성된, x1, x2, y, grp 칼럼을 가진 DataFrame을 만들어보겠습니다. 



import pandas as pd

import numpy as np


X = np.arange(30).reshape(15, 2)

y = np.arange(15)


df = pd.DataFrame(np.column_stack((X, y)), columns=['X1','X2', 'y'])

df

X1X2y
0010
1231
2452
3673
4894
510115
612136
714157
816178
918199
10202110
11222311
12242512
13262713
14282914



df['grp'] = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

df

X1X2ygrp
00100
12310
24520
36730
48940
5101151
6121361
7141571
8161781
9181991
102021101
112223111
122425121
132627131
142829141





이제 StratifiedShuffleSplit() 함수를 사용해서 층의 비율을 고려해서(유지한채) 무작위로 train set, test set DataFrame을 만들어보겠습니다. 



from sklearn.model_selection import StratifiedShuffleSplit


split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=1004)


for train_idx, test_idx in split.split(df, df["grp"]):

    df_strat_train = df.loc[train_idx]

    df_strat_test = df.loc[test_idx]


 




층 내 class의 비율을 고려해서 층화 무작위 추출된 DataFrame 결과는 아래와 같습니다. 



df_strat_train

X1X2ygrp
6121361
48940
142829141
00100
5101151
36730
12310
9181991
102021101
112223111
132627131
7141571



df_strat_test

X1X2ygrp
8161781
24520
122425121






정말로 각 층 내 계급의 비율(percentage of samples for each class)이 train set, test set에서도 유지가 되고 있는지 확인을 해보겠습니다. 



df["grp"].value_counts() / len(df)

[Out]:

1 0.666667 0 0.333333 Name: grp, dtype: float64



df_strat_train["grp"].value_counts() / len(df_strat_train)

[Out]:
1    0.666667
0    0.333333
Name: grp, dtype: float64


df_strat_test["grp"].value_counts() / len(df_strat_test)

[Out]:

1 0.666667 0 0.333333 Name: grp, dtype: float64




pandas DataFrame에 대한 무작위 표본 추출 방법https://rfriend.tistory.com/602 를 참고하세요.


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)

Posted by R Friend Rfriend

댓글을 달아 주세요

  1. researcher 2020.08.26 15:16  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 개발자님, 위의 글이 많은 도움 되었습니다.
    이를 참고로 해서, 훈련데이터와 테스트데이터를 무작위로 추출하는 방식으로 아래와 같이 코딩했는데(성공적으로 수행됨), 이걸 순차적 추출로 바꾸려면 어느 부분을 고쳐야 할까요?
    여러가지 시도를 해봤는데, 자꾸 막힙니다 ㅜ
    지도 부탁 드립니다.

    from sklearn.model_selection import train_test_split
    from sklearn.model_selection import StratifiedShuffleSplit

    sss = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)

    for train_index, test_index in sss.split(X_past, y_past):
    X_train, X_test = X_past.iloc[train_index,], X_past.iloc[test_index,]
    y_train, y_test = y_past[train_index], y_past[test_index]

    • R Friend Rfriend 2020.08.26 19:57 신고  댓글주소  수정/삭제

      안녕하세요 Researcher님,

      아래 코드처럼 shuffle=False 로 수정하시면 됩니다. (shuffle=True는 무작위로 섞은 후 표본 추출, shuffle=False 는 그대로 둔 상태에서 표본 추출)

      from sklearn.model_selection import train_test_split

      X_train, X_test, y_train, y_test = train_test_split(X,
      y,
      test_size=0.2,
      shuffle=False, # <--- 이 부분 False 로 설정
      stratify=grp,
      random_state=1004)

  2. researcher 2020.08.27 09:41  댓글주소  수정/삭제  댓글쓰기

    인사이트 공유해주셔서 감사드립니다^^

JAVA, C++, C#, Python, PHP 등을 사용하는 SW 개발자 중에 객체 지향 프로그래밍(Object-Oriented Programming, OOP)과 클래스(Class)를 모르는 분은 없을 것입니다. 


반면에, 통계와 데이터 분석을 전공하신 분들 중에는 객체 지향 프로그래밍과 클래스에 대해서 모르는 분이 상당히 많을 것이라고 보며, 아마도 '내가 개발자도 아닌데 객체 지향 프로그래밍과 클래스를 왜 알아야 해? 사용자 정의함수 만들어 쓸 수 있으면 되는거 아닌가?' 라고 생각하며 아예 선을 그어버리고 외면하는 분도 계실 듯 합니다. (제가 예전에 이랬거든요. ^^;)


그런데 통계/머신러닝 모델을 운영 시스템에 반영하기 위해 개발자와 협업하다보면 클래스 얘기가 나오고, 또 딥러닝 공부하다보니 자꾸 클래스와 맞닥드리게 되어서 이참에 클래스 공부도 하고 블로그에 정리도 하게 되었습니다. 


이번 포스팅에서는 통계와 분석업무 하시는 분들을 독자로 가정하고 파이썬3의 클래스(Class in Python 3)에 대해서 소개하겠습니다. 


1. 객체 지향 프로그래밍은 무엇이며, 클래스는 무엇인가? 

2. 왜 객체지향 프로그램이 필요한가?

3. 클래스와 인스턴스는 어떻게 만드는가? 

4. 클래스 상속이란 무엇인가? 

5. 클래스와 함수의 차이점은 무엇인가


ps. Private member, 상속의 super(), 다중 상속, decorator, iterator, generator, 추상 기반 클래스(abstract base class), Overriding 등의 세부 심화 내용은 이번 포스팅에서는 제외하겠으며, 파이썬 프로그래밍 개발 관련 전문 서적이나 사이트를 참고하시기 바랍니다. 



  1. 객체 지향 프로그래밍은 무엇이며, 클래스는 무엇인가? 


위키피디아에서 객체 지향 프로그래밍을 어떻게 정의하고 있는지 찾아보았습니다. 


객체 지향 프로그래밍(Object-oriented programming, OOP)은 객체(Objects) 개념에 기반한 프로그래밍 패러다임으로서, 변수(field) 혹은 속성(attribute), 특성(property)이라고 알려진 데이터(data)와 프로시져(procedures) 또는 메소드(methods) 형태의 코드(code) 를 가지고 있다. (Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data, in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods).)


객체의 프로시져를 통해 객체의 연관되어 있는 데이터 변수들에 접근하고 변경할 수 있는 특성이 있다. (객체는 "this" 또는 "self"의 표기를 가지고 있다) (A feature of objects is an object's procedures that can access and often modify the data fields of the object with which they are associated (objects have a notion of "this" or "self").)


객체 지향 프로그래밍에서 컴퓨터 프로그램은 서로 상호작용하는 객체들을 만듦으로써 설계한다. 객체 지향 프로그래밍 언어는 다양하지만 가장 일반적인 것은 클래스 기반(class-based) 의 언어이다. 클래스 기반이란 클래스의 인스턴스가 객체가 되고 유형(type)을 결정하는 것을 의미한다. (In OOP, computer programs are designed by making them out of objects that interact with one another. OOP languages are diverse, but the most popular ones are class-based, meaning that objects are instances of classes, which also determine their types.)


* source: Wikipedia


말이 좀 어려운데요, 한번 더 정리를 해보자면, 

  • 객체(objects) = data (fields, attributes, properties) + code (procedures, methods)
  • code(procedures, methods) 를 통해 data(fields, attributes, properties)에 접근하고 변경 가능
  • class 로 data와 code를 겹합하며, 클래스를 구체화(실체화)한 인스턴스는 객체가 됨


Python도 객체 지향 프로그래밍을 지원하는 대표적인 언어이며, 위의 위키피디아 정의와 동일하게 객체는 속성(attribute)과 기능(method), 다른 말로 표현하면 객체는 변수(filed)와 함수(function)로 구성됩니다. 이들 속성(attribute)(또는 변수(field))와 기능(method)(또는 함수(function))을 클래스(class)를 사용해서 하나로 묶어줍니다. 클래스를 실체화하여 인스턴스(instance) 객체를 생성합니다. (아래 3번의 실제 예를 살펴보면 이해하기에 더 쉬울 것입니다)





  2. 왜 객체 지향 프로그램이 필요한가?


객체 지향 프로그래밍을 하면 객체(변수 + 함수) 내의 응집력은 강하지만 객체 간의 응집력은 약하게 하여 소프트웨어의 개발, 유지보수, 업그레이드를 보다 쉽게 할 수 있도록 해주는 장점이 있습니다. 


"객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다. 또한 프로그래밍을 더 배우기 쉽게 하고 소프트웨어 개발과 보수를 간편하게 하며, 보다 직관적인 코드 분석을 가능하게 하는 장점을 갖고 있다."

* source: wikipedia


"같은 목적과 기능을 위해 객체로 묶인 코드 요소(변수, 함수)들은 객체 내부에서만 강한 응집력을 발휘하고 객체 외부에 주는 영향은 줄이게 됩니다. 코드가 객체 위주로 (또는 순수하게 객체로만) 이루어질 수 있도록 지향하는 프로그래머의 노력은 코드의 결합도를 낮추는 결과를 낳게 됩니다."

* source: '뇌를 자극하는 파이썬3', 박상현 지음, 한빛미디어


아래의 클래스를 어떻게 만들고, 상속(inheritance)은 무엇인지를 알면 응집력에 대한 위의 내용이 좀더 이해가 쉬울 것입니다. 



  3. 클래스(class)와 인스턴스(instance)는 어떻게 만드는가? 




-- 클래스 정의 (create a Class) -- 

(a) 클래스 정의는 class 키워드 (keyword) 로 시작하고, 다음에 클래스 이름을 써주며, 뒤에 콜론(':')을 써줍니다. 


(b) 클래스 이름은 보통 대문자(capital letter)로 시작합니다. 

            아래 예) class PersonalInfo:


(c) 클래스의 코드 블락에는 

    (c-1) 클래스 전체에 공통으로 사용하는 클래스 속성(class attribute), 

            아래 예) nationality = "Korean"

   (c-2) 인스턴스별로 객체를 초기화해서 사용하는 인스턴스 속성(instance attributes)

            아래 예) def __init__(self, name, age):

                             self.name = name

                             self.age = age

   (c-3) 기능을 수행하는 인스턴스 메소드(instance methods) 를 정의합니다. 

            아래 예) def getPersonalInfo(self):

                             print("Name:", self.name)


(d) 인스턴스 객체의 속성을 초기화해주는 역할은 def __init__(): 의 마법 메소드(magic method) 를 사용합니다. 


(e) 'self'는 메소드가 소속되어 있는 객체를 의미합니다. ( C++, C#, JAVA 의 'this'와 동일한 역할)


-- 인스턴스 객체 생성 (create an instance object) --

(g) 클래스 이름(변수 값) 생성자로 클래스를 구체화/ 상세화한 인스턴스(instance)를 정의합니다. 

          아래 예) personal_choi = PersonalInfo('CK Choi', 25)




# class definition starts with 'class' keyword

# class name starts with the CAPITAL LETTER usually

class PersonalInfo:

    

    # Class attribute

    nationality = "Korean"

    

    # Initalizer, Instance attributes

    def __init__(self, name, age):

        self.name = name

        self.age = age

        

    # instance method 1

    def getPersonalInfo(self):

        print("Name:", self.name)

        print("Age:", self.age)

        

    # instance method 2

    def ageGroup(self):

        if self.age < 30:

            return "under 30"

        else:

            return "over 30"

        

    # instance method 3

    def FirstName(self):

        print(self.name.split(' ')[0])

    

    # instance method 4

    def LastName(self):

        print(self.name.split(' ')[1])

 




아래 코드는 클래스 생성 시점에 메모리에 같이 저장이 되고, 클래스 전체에서 공유하는 속성인 클래스 속성(class attribute) 값을 조회한 결과입니다. '클래스이름.속성이름'  의 형태로 조회합니다. 



# get class attribute

PersonalInfo.nationality

[Out]:'Korean'

 




아래의 코드는 클래스를 상세화/구체화하여 인스턴스 객체를 생성한 예입니다. 인스턴스 객체의 속성과 메소드는 인스턴스 객체를 생성하는 시점에 메모리에 저장이 됩니다.  


인스턴스 객체를 생성할 때마다 __init__(self) 의 마법 메소드(magic method)가 속성 값을 초기화(initialization) 해줍니다. 인스턴스 속성은 '인스턴스 객체 이름'에 점('.')을 찍고 뒤에 '속성 이름'을 써주면 조회할 수 있습니다.  

    아래 예) personal_choi.name


인스턴스 메소드 호출은 인스턴스 객체 이름에 점('.')을 찍고 뒤에 '메소드 이름()'을 써주면 됩니다. 

    아래 예) personal_choi.getPersonalInfo()


인스턴스 객체 1 (instance object 1)

인스턴스 객체 2 (instance object 2)


# instance

personal_choi = PersonalInfo('CK Choi', 25)


# get instance attribute

personal_choi.name

[Out]: 'CK Choi'


# instance method 1

personal_choi.getPersonalInfo()

[Out]:
Name: CK Choi
Age: 25


# instance method 2

personal_choi.ageGroup()

[Out]: 'under 30'


# instance method 3

personal_choi.FirstName()

[Out]: CK


# instance method 4

personal_choi.LastName()

[Out]: Choi



# instance

personal_park = PersonalInfo('SJ Park', 33)


# get instance attribute

personal_park.name

[Out]: 'SJ Park'

# instance method 1 personal_choi.getPersonalInfo()

[Out]: 
Name: SJ Park
Age: 33


# instance method 2

personal_park.ageGroup()

[Out]: 'over 30'

# instance method 3

personal_park.FirstName()

[Out]: SJ


# instance method 4

personal_park.LastName()

[Out]: Park





  4. 클래스 상속이란 무엇인가? 





부모가 자식에게 재산을 상속하듯이, 클래스도 클래스가 다른 클래스에게 상속을 해줄 수 있습니다. 상속을 해주는 클래스를 부모 클래스(Parent Class) 혹은 기반 클래스(Base Class) 라고 하며, 상속을 받는 클래스를 자식 클래스(Child Class) 혹은 파생 클래스(Derived Class)라고 합니다. 도형으로 표기할 때는 바로 위 그림의 예시처럼 '자식 클래스'에서 시작해서 '부모 클래스'로 향하는 화살표를 그려줍니다. 


부모 클래스를 생성한 후에 자식 클래스 이름 끝의 괄호() 안에 부모 클래스의 이름을 적어주면 자식 클래스가 부모 클래스의 모든 데이터 속성과 메소드를 유산으로 상속받아 부모 클래스처럼 역할을 할 수가 있습니다. 

    위의 그림 예) class ChildClass(ParentClass): 

                             pass



위의 '3. 클래스는 어떻게 만드는가?'에서 들었던 예제 클래스 PersonalInfo 를 부모 클래스로 하고, 이를 상속받은 자식 클래스를 class ContactInfo(PersonalInfo):  로 아래에 만들어보겠습니다. 부모 클래스에서 상속받은 데이터 속성, 메소드 외에 def getContactInfo(self, cellphone, city): 로 인스턴스 메소드를 추가로 정의해주었습니다. 



# Child(derived) class (inherits from PersonalInfo() parent(base) class)

class ContactInfo(PersonalInfo):

    def getContactInfo(self, cellphone, city):

        print("Name:", self.name)

        print("Age:", self.age)

        print("Celluar Phone:", cellphone)

        print("City:", city)

 



아래에는 contact_lee = ContactInfo('SH Lee', 41) 으로 위의 자식 클래스를 상세화한 contact_lee 라는 이름의 인스턴스 객체를 만들었습니다. 아래에 보시다시피 getPersonalInfo(), ageGroup(), FirstName(), LastName() 등의 부모 클래스에서 정의했던 인스턴스 메소드를 자식 클래스로 부터 생성한 인스턴스에서도 동일하게 사용할 수 있음을 알 수 있습니다. 



contact_lee = ContactInfo('SH Lee', 41)


# instance method from Parent class

contact_lee.getPersonalInfo()

[Out]:

Name: SH Lee Age: 41


# instance method from Parent class

contact_lee.ageGroup()

[Out]: 'over 30'


# instance method from Parent class

contact_lee.FirstName()

[Out]: SH


# instance method from Parent class

contact_lee.LastName()

[Out]: Lee


# -- instance method from Child class

contact_lee.getContactInfo('011-1234-5678', 'Seoul')

[Out]: 
Name: SH Lee
Age: 41
Celluar Phone: 011-1234-5678
City: Seoul





  5. 클래스와 함수의 차이점은 무엇인가


코드의 재사용성(reusability) 측면에서는 클래스와 함수가 유사한 측면도 있습니다만, 클래스와 함수는 엄연히 다릅니다. 클래스의 이해를 돕기 위해 마지막으로 한번 더 정리하는 차원에서 클래스와 함수의 차이점을 비교해보자면요, 



  • 클래스(class)는 같은 목적과 기능을 위해 '데이터 속성(변수)'과 '기능(함수)'를 결합해 놓은 집합체/그룹으로서, 객체 지향 프로그래밍에서 인스턴스 객체를 생성하는데 사용이 됩니다. 
  • vs. 함수(function)는 특정 기능/과업을 수행하는 코드 블록입니다. (A function is a unit of code devoted to carrying out a specific task)

클래스(class)가 함수(function) 보다 더 광범위한 개념이며, 함수는 클래스의 부분집합입니다. 


많은 도움이 되었기를 바랍니다.

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



[Reference]

1. '뇌를 자극하는 파이썬3', 박상현 지음, 한빛미디어

2. Object-Oriented Programming in Python 3, by Real Python

3. Object-Oriented Programming, Wikipedia




Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 1. plt.subplot() 2. plt.subplots() 를 이용해서 


(1) (nrows=1, ncols=3)개의 복수의 하위 막대 그래프를 그리는 방법

     (multiple subplots of bar plots using plt.subplot())

(2) 복수의 막대 그래프 간에 y 축의 단위를 고정하는 방법 (fix y axis scale)

(3) 복수의 막대 그래프 간에 y 축의 이름을 공유하는 방법 (share y axis label)

(4) 복수의 하위 그래프 간 여백을 작게 하여 밀착해서 그리는 방법 (plots in tight layout)


을 소개하겠습니다. 


그리고 마지막에는 복수의 옆으로 누운 막대 그래프 (multiple horizontal bar plots) 를 그리는 방법을 소개하겠습니다. 


먼저 예제로 사용할 간단한 DataFrame을 만들어보겠습니다. 



import numpy as np

import pandas as pd

import matplotlib.pyplot as plt


# make a sample DataFrame

grp = ['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c']

col = ['x1', 'x2', 'x3'] * 3

val = [3.5, 4.2, 2.9, 0.5, 0.2, 0.3, 1.5, 2.5, 2.6]


df = pd.DataFrame({'grp': grp, 

                  'col': col, 

                  'val': val})


df

grpcolval
0ax13.5
1ax24.2
2ax32.9
3bx10.5
4bx20.2
5bx30.3
6cx11.5
7cx22.5
8cx32.6

 




  1. plt.subplot() 으로 복수의 막대 그래프 그리기


(1-1) (행 1개, 열 3개) 로 구성된 복수의 하위 막대 그래프 그리기 

        (multiple subplots of bar plots using plt.subplot())


plt.subplot(nrow, ncol, position) 의 형태로 행과 열의 위치에 여러개의 하위 그래프를 그릴 수 있습니다. 


아래의 예제에서는 고쳤으면 하는 3가지 문제점이 보이네요. 

첫째 문제는 Y 축의 단위(y axis scale)가 서로 다르다 보니 비교를 하기가 힘듭니다. 

둘째 문제는 X축과 Y축의 이름(label)과 단위 눈금(ticks)이 반복해서 나타나서 지저분해 보입니다. 

셋째 문제는 복수의 하위 그래프 간 간격이 떨어져 있어서 좀 작게 보입니다. 


아래에 차례대로 이들 문제를 해결해 보겠습니다. 



# (1-1) multiple bar plots with different y axis scale

plt.rcParams['figure.figsize'] = [12, 6]


for i, grp in enumerate(['a', 'b', 'c']):

    df_i = df[df['grp'] == grp]

    plt.subplot(1, 3, i+1)

    plt.bar(df_i['col'], df_i['val'], color='blue', alpha=0.5)

    plt.title('Group: %s' %grp, fontsize=18)

    plt.xlabel('X variable', fontsize=14)

    plt.ylabel('Value', fontsize=14)

    plt.xticks(fontsize=12)

    plt.yticks(fontsize=12)






(1-2) 복수의 막대 그래프 간에 y 축의 단위를 고정하는 방법 (fix y axis scale)


plt.ylim(min, max) 의 형태로 y 축의 단위를 설정 또는 고정 (set/ fix y axis scale) 할 수 있습니다. 



 # (1-2) Set fixed y axis scale and Share it together

plt.rcParams['figure.figsize'] = [12, 6]

max_val = np.ceil(max(df['val']))


for i, grp in enumerate(['a', 'b', 'c']):

    df_i = df[df['grp'] == grp]

    plt.subplot(1, 3, i+1)

    plt.bar(df_i['col'], df_i['val'], color='blue', alpha=0.5)

    plt.title('Group: %s' %grp, fontsize=18)

    plt.xlabel('X variable', fontsize=14)

    plt.ylabel('Value', fontsize=14)

    plt.xticks(fontsize=12)

    plt.yticks(fontsize=12)

    # set fixed y axis scale

    plt.ylim(0, max_val)





(1-3) 복수의 막대 그래프 간에  X, Y 축의 이름을 공유하는 방법 (share X and Y axis label)


if 조건문을 사용하여 X축의 이름(X label)은 중간 위치 (index = 1)의 하위 그래프만 나타나게 하고, Y축의 이름(Y label)은 첫번째 그래프(index = 0) 에만 나타나게 하면 X축과 Y축의 이름을 서로 공유한 것과 같은 효과를 낼 수 있습니다. 



# (1-3) Display only 1 X and Y label

plt.rcParams['figure.figsize'] = [12, 6]

max_val = np.ceil(max(df['val']))


for i, grp in enumerate(['a', 'b', 'c']):

    df_i = df[df['grp'] == grp]

    plt.subplot(1, 3, i+1)

    plt.bar(df_i['col'], df_i['val'], color='blue', alpha=0.5)

    plt.title('Group: %s' %grp, fontsize=18)

    

    # display only 1 X and Y label

    if i == 1:

        plt.xlabel('X variable', fontsize=14)

    if i == 0:

        plt.ylabel('Value', fontsize=14)

    if i != 0:

        plt.yticks([])

    

    plt.xticks(fontsize=12)

    plt.yticks(fontsize=12)

    plt.ylim(0, max_val)





(1-4) 복수의 하위 그래프 간 여백을 작게 하여 밀착해서 그리는 방법 (plots in tight layout)


plt.tight_layout() 메소드를 이용하여 복수의 하위 그래프 간 여백 간격을 좁게하여 밀집된 형태로 좀더 크게 복수의 그래프를 그릴 수 있습니다. 



# (1-4) Display multiple plots in Tight Layout

plt.rcParams['figure.figsize'] = [12, 6]

max_val = np.ceil(max(df['val']))


for i, grp in enumerate(['a', 'b', 'c']):

    df_i = df[df['grp'] == grp]

    plt.subplot(1, 3, i+1)

    plt.bar(df_i['col'], df_i['val'], color='blue', alpha=0.5)

    plt.title('Group: %s' %grp, fontsize=18)

    

    # display only 1 X and Y label

    if i == 1:

        plt.xlabel('X variable', fontsize=14)

    if i == 0:

        plt.ylabel('Value', fontsize=14)

    if i != 0:

        plt.yticks([])

    

    plt.xticks(fontsize=12)

    plt.yticks(fontsize=12)

    plt.ylim(0, max_val)


# display in tight layout

plt.tight_layout()





  2. plt.subplots() 로 복수의 막대 그래프 그리기


plt.subplots() 는 위의 plt.subplot() 보다 좀더 적은 코드로 깔끔하게 복수의 하위 막대그래프를 그릴 수 있습니다. 

  • (nrows, ncols) 로 하위 그래프를 그릴 행과 열을 지정해줍니다. 
  • sharey=True 로 y 축 공유, sharex=True 로 복수 그래프 간 x 축을 공유할 수 있습니다. (편리!)
  • if 조건문과 함께 사용해서 ax.set_ylabel(), ax.set_xlabel() 로 y축과 x축의 이름을 하나만 나타나게 하였습니다. 
  • plt.tight_layout() 로 복수의 하위 그래프 간 여백을 좁게 해서 그래프를 그렸습니다. 


# (2-1) Multiple bar plots

# (2-2) Use fixed y axis scale

# (2-3) Display only 1 X and Y label

# (2-4) Display multiple plots in Tight Layout

fig, axes = plt.subplots(nrows=1

                         , ncols=3

                         , sharey=True

                         , figsize=(12,6))


grp = ['a', 'b', 'c']


for i, ax in enumerate(axes):

    df_i = df[df['grp'] == grp[i]]

    ax.bar(df_i['col'], df_i['val'], color='blue', alpha=0.5)

    ax.set_title("Group: {grp}".format(grp=grp[i]), fontsize=18)

    if i == 0:

        ax.set_ylabel("Value", fontsize=14)

    if i == 1:

        ax.set_xlabel("X variable", fontsize=14)

        

plt.tight_layout()





  3. 복수의 옆으로 누운 막대 그래프 (multiple horizontal bar plot) 그리기


ax.barh() 를 사용하여 옆으로 누운 막대 그래프 (horizontal bar plot)을 그렸으며, 복수개의 하위 그래프 그리는 방법은 2번에서 소개한 plt.subplots() 함수를 차용하였습니다. 


이때 옆으로 누운 막대 그래프이기 때문에, 가독성을 높이기 위해서 그룹의 이름을 제목(title)이 아니라 Y 축 이름(y label) 에 표시를 하였습니다. 



# Horizontal Multiple Bar Plots using plt.subplots()

fig, axes = plt.subplots(nrows=3

                         , ncols=1

                         , sharex=True

                         , figsize=(8,10))


grp = ['a', 'b', 'c']


for i, ax in enumerate(axes):

    df_i = df[df['grp'] == grp[i]]

    ax.barh(df_i['col'], df_i['val'], color='blue', alpha=0.5)

    #ax.set_title("Group: {grp}".format(grp=grp[i]), fontsize=18)

    ax.set_ylabel("Group: {grp}".format(grp=grp[i]), fontsize=18)

    if i == 2:

        ax.set_xlabel("Value", fontsize=14)

        

plt.tight_layout()


 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 선형회귀모형에 대한 각 관측치별 변수별 기여도 분석 (each variable contribution per each observations)에 대해서 소개하겠습니다. 


변수 중요도 (variable importance, feature importance)가 전체 관측치를 사용해 적합한 모델 단위의 변수별 (상대적) 중요도를 나타내는 것이라면, 이번 포스팅에서 소개하려는 관측치별 변수별 기여도(민감도)는 개별 관측치 단위에서 한개의 칼럼이 모델 예측치에 얼마나 기여를 하는지를 분석해보는 것입니다. 


선형회귀 모형을 예로 들면, 변수 중요도는 회귀모형의 회귀 계수라고 할 수 있겠구요, 관측치별 변수별 기여도는 특정 관측치의 특정 칼럼값만 그대로 사용하고 나머지 변수값에는 '0'을 대입해서 나온 예측치라고 할 수 있겠습니다. 변수 중요도가 Global 하게 적용되는 model weights 라고 한다면, 관측치별 변수별 기여도는 specific variable's weight * value 라고 할 수 있겠습니다. 




변수 중요도를 분석했으면 됐지, 왜 관측치별 변수별 기여도 분석을 할까 궁금할 수도 있을 텐데요, 관측치별 변수별 기여도 분석은 특정 관측치별로 개별적으로 어떤 변수가 예측치에 크게 영향을 미쳤는지 파악하기 위해서 사용합니다. (동어반복인가요? ^^;) 


모델의 변수별 가중치(high, low)와 각 관측치별 변수별 값(high, low)의 조합별로 아래와 같이 관측치별 변수별 예측치에 대한 기여도가 달라지게 됩니다. 가령, 관측치별 변수별 예측치에 대한 기여도가 높으려면 모델의 가중치(중요도)도 높아야 하고 동시에 개별 관측치의 해당 변수의 관측값도 역시 높아야 합니다. 



관측치별 변수별 기여도 분석은 복잡하고 어려운 이론에 기반을 둔 분석이라기 보다는 단순한 산수를 반복 연산 프로그래밍으로 계산하는 것이므로 아래의 예를 따라가다 보면 금방 이해할 수 있을 것이라고 생각합니다. 



예를 들어보기 위해 공개된 전복(abalone) 데이터를 사용하여 전체 무게(whole weight)를 예측하는 선형회귀모형을 적합하고, 관측치별로 각 변수별 예측치에 기여한 정도를 분석해보겠습니다. 



  1. 선형회귀모형에 대한 관측치별 변수별 기여도(민감도) 분석 

    (Sensitivity analysis for linear regression model)



(1) abalone dataset 을 pandas DataFrame으로 불러오기



import numpy as np

import pandas as pd


url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data'

abalone = pd.read_csv(url

                      , sep=','

                      , header=None

                      , names=['sex', 'length', 'diameter', 'height', 

                               'whole_weight', 'shucked_weight', 'viscera_weight',

                               'shell_weight', 'rings']

                     , index_col=None)


abalone.head()

[Out]:

sexlengthdiameterheightwhole_weightshucked_weightviscera_weightshell_weightrings
0M0.4550.3650.0950.51400.22450.10100.15015
1M0.3500.2650.0900.22550.09950.04850.0707
2F0.5300.4200.1350.67700.25650.14150.2109
3M0.4400.3650.1250.51600.21550.11400.15510
4I0.3300.2550.0800.20500.08950.03950.0557




(2) X, y 변수 데이터셋 생성 (creating X and y dataset)


예측하려는 목표변수 y 로는 '전체 무게(whole weight)' 변수를 사용하겠으며, 설명변수 X 에는 'sex', 'length', 'diameter', 'height', 'shell_weight', 'rings' 의 6개 변수를 사용하겠습니다. 이중에서 'sex' 변수는 범주형 변수이므로 가변수(dummy variable)로 변환하였습니다. 



# transformation of categorical variable to dummy variable

abalone['sex'].unique()

[Out]: array(['M', 'F', 'I'], dtype=object)


abalone['sex_m'] = np.where(abalone['sex'] == 'M', 1, 0)

abalone['sex_f'] = np.where(abalone['sex'] == 'F', 1, 0)


# get X variables

X = abalone[["sex_m", "sex_f", "length", "diameter", "height", "shell_weight", "rings"]]


import statsmodels.api as sm

X = sm.add_constant(X) # add a constant to model

print(X)


[Out]:
Index(['const', 'sex_m', 'sex_f', 'length', 'diameter', 'height',
       'shell_weight', 'rings'],
      dtype='object')

      const  sex_m  sex_f  length  diameter  height  shell_weight  rings
0       1.0      1      0   0.455     0.365   0.095        0.1500     15
1       1.0      1      0   0.350     0.265   0.090        0.0700      7
2       1.0      0      1   0.530     0.420   0.135        0.2100      9
3       1.0      1      0   0.440     0.365   0.125        0.1550     10
4       1.0      0      0   0.330     0.255   0.080        0.0550      7
...     ...    ...    ...     ...       ...     ...           ...    ...
4172    1.0      0      1   0.565     0.450   0.165        0.2490     11
4173    1.0      1      0   0.590     0.440   0.135        0.2605     10
4174    1.0      1      0   0.600     0.475   0.205        0.3080      9
4175    1.0      0      1   0.625     0.485   0.150        0.2960     10
4176    1.0      1      0   0.710     0.555   0.195        0.4950     12

[4177 rows x 8 columns]


# get y value

y = abalone["whole_weight"]

 



Train:Test = 8:2 의 비율로 데이터셋을 분할 (Split train and test set)하겠습니다. 



# train, test set split

from sklearn.model_selection import train_test_split


X_train, X_test, y_train, y_test = train_test_split(X, 

                                                    y, 

                                                    test_size=0.2, 

                                                    random_state=2004)

print('Shape of X_train:', X_train.shape

print('Shape of X_test:', X_test.shape

print('Shape of y_train:', y_train.shape

print('Shape of y_test:', y_test.shape)

[Out]:

Shape of X_train: (3341, 8) Shape of X_test: (836, 8) Shape of y_train: (3341,) Shape of y_test: (836,)





(3) 선형회귀모형 적합 (fitting linear regression model)


이번 포스팅이 선형회귀모형에 대해서 설명하는 것이 목적이 아니기 때문에 회귀모형을 적합하는데 필요한 가정사항 진단 및 조치생략하고, 그냥 statsmodels.api 라이브러리를 이용해서 회귀모델을 적합해서 바로 민감도 분석(Sensitivity analysis)으로 넘어가겠습니다


이번 포스팅의 주제인 '관측치별 변수별 기여도 분석'에서 사용하는 모델은 어떤 알고리즘도 전부 가능하므로 개별 관측치별 변수별 영향력을 해석을 하는데 유용하게 사용할 수 있습니다. (가령, 블랙박스 모형인 Random Forest, Deep Learning 등도 가능)



# multivariate linear regression model

import statsmodels.api as sm


lin_reg = sm.OLS(y_train, X_train).fit()


lin_reg.summary()

OLS Regression Results
Dep. Variable:whole_weightR-squared:0.942
Model:OLSAdj. R-squared:0.942
Method:Least SquaresF-statistic:7738.
Date:Fri, 17 Jan 2020Prob (F-statistic):0.00
Time:20:06:12Log-Likelihood:2375.3
No. Observations:3341AIC:-4735.
Df Residuals:3333BIC:-4686.
Df Model:7
Covariance Type:nonrobust
coefstd errtP>|t|[0.0250.975]
const-0.36090.015-23.7950.000-0.391-0.331
sex_m0.03490.0066.0390.0000.0240.046
sex_f0.02170.0063.4930.0000.0100.034
length1.25390.10711.7240.0001.0441.464
diameter0.07490.1350.5540.580-0.1900.340
height0.38800.0884.4100.0000.2160.561
shell_weight2.43840.03864.8020.0002.3652.512
rings-0.01540.001-18.4230.000-0.017-0.014
Omnibus:698.165Durbin-Watson:1.978
Prob(Omnibus):0.000Jarque-Bera (JB):4041.763
Skew:0.866Prob(JB):0.00
Kurtosis:8.102Cond. No.866.


# prediction

predicted = lin_reg.predict(X_test)

actual = y_test


act_pred_df = pd.DataFrame({'actual': actual

                            , 'predicted': predicted

                            , 'error': actual - predicted})


act_pred_df.head()

[Out]:

actualpredictederror
34730.0455-0.0947030.140203
35230.09700.0206040.076396
18620.51850.690349-0.171849
29661.48201.3589500.123050
6590.95851.137433-0.178933



# Scatter Plot: Actual vs. Predicted

import matplotlib.pyplot as plt


plt.plot(act_pred_df['actual'], act_pred_df['predicted'], 'o')

plt.xlabel('actual', fontsize=14)

plt.ylabel('predicted', fontsize=14)

plt.show()


# RMSE (Root Mean Squared Error)

from sklearn.metrics import mean_squared_error

from math import sqrt


rmse = sqrt(mean_squared_error(actual, predicted))

rmse

[Out]: 0.11099248621173345





(4) 관측치별 변수별 예측모델 결과에 대한 기여도 분석 (contribution per each variables from specific observation)


아래 예에서는 전체 836개의 test set 관측치 중에서 '1번 관측치 (1st observation)' 의 8개 변수별 기여도를 분석해보겠습니다. 



X_test.shape

[Out]: (836, 8)


# get 1st observation's value as an example

X_i = X_test.iloc[0, :]

X_i

[Out]:

const           1.000
sex_m           0.000
sex_f           0.000
length          0.210
diameter        0.150
height          0.055
shell_weight    0.013
rings           4.000
Name: 3473, dtype: float64




관측치별 변수별 기여도(민감도, variable's contribution & sensitivity) 분석의 핵심 개념은 전체 데이터를 사용해서 적합된 모델에 특정 관측치의 변수별 값을 변수별로 순서대로 돌아가면서 해당 변수 측정값은 그대로 사용하고 나머지 변수들의 값은 '0'으로 대체해서 예측을 해보는 것입니다. 아래 코드의 for i, j in enumerate(X_i): X_mat[i, i] = j 부분을 유심히 살펴보시기 바랍니다. 



# all zeros matrix with shape of [col_num, col_num]

X_mat = np.zeros(shape=[X_i.shape[0], X_i.shape[0]])

X_mat

[Out]:
array([[0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.]])


# fill only 1 variable's value and leave '0' for the others

for i, j in enumerate(X_i):

    X_mat[i, i] = j


X_mat

[Out]:
array([[1.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ],
       [0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ],
       [0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ],
       [0.   , 0.   , 0.   , 0.21 , 0.   , 0.   , 0.   , 0.   ],
       [0.   , 0.   , 0.   , 0.   , 0.15 , 0.   , 0.   , 0.   ],
       [0.   , 0.   , 0.   , 0.   , 0.   , 0.055, 0.   , 0.   ],
       [0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.013, 0.   ],
       [0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 4.   ]])




바로 위에서 만든 X_mat 행렬 (1번 관측치의 각 변수별로 하나씩 실제 값을 사용하고, 나머지는 '0'으로 대체)을 사용해서 (3)에서 적합한 선형회귀모형으로 y값을 예측(lin_reg.predict(X_mat))을 하면 우리가 원하는 '1번 관측치의 개별 변수별 y값 예측에 대한 기여도'를 구할 수 있습니다. 


개별 변수별 y 예측값에 대한 기여도를 기준으로 내림차순 정렬해서 DataFrame을 만들고, 가로로 누운 막대그래프도 그려보았습니다. 


# sensitivity analysis

sensitivity_df = pd.DataFrame({'x': X_test.iloc[0, :]

                               , 'contribution_x': lin_reg.predict(X_mat)}).\

                 sort_values(by='contribution_x', ascending=False)


sensitivity_df

[Out]:

xcontribution_x
length0.2100.263315
shell_weight0.0130.031699
height0.0550.021341
diameter0.1500.011237
sex_m0.0000.000000
sex_f0.0000.000000
rings4.000-0.061437
const1.000-0.360857



horizontal bar plot by column's contribution

sensitivity_df['contribution_x'].plot(kind='barh', figsize=(10, 5))

plt.title('Sensitivity Analysis', fontsize=16)

plt.xlabel('Contribution', fontsize=16)

plt.ylabel('Variable', fontsize=16)

plt.yticks(fontsize=14)

plt.show()


물론, 당연하게 관측치별 변수별 기여도 (민감도) 분석에서 나온 결과를 전부 다 합치면 애초의 모형에 해당 관측치의 전체 변수별 값을 넣어서 예측한 값과 동일한 결과가 나옵니다. 



# result from sum of contribution analysis

sum(sensitivity_df['contribution_x'])

[Out]: -0.09470251191012563


# result from linear regression model's prediction

lin_reg.predict(X_test.iloc[0, :].to_numpy())

[Out]: array([-0.09470251])

 




(5) 관측치별 변수별 예측 기여도 (민감도) 분석을 위한 사용자 정의 함수


관측치별 변수별 기여도 분석 결과를 --> pandas DataFrame으로 저장하고, --> 기여도별로 정렬된 값을 기준으로 옆으로 누운 막대그래프를 그려주는 사용자 정의함수를 만들어보겠습니다. 



# UDF for contribution(sensitivity) analysis per each variables

def sensitivity_analysis(model, X, idx, bar_plot_yn):

    

    import numpy as np

    import pandas as pd

    import matplotlib.pyplot as plt

    import statsmodels.api as sm

    pd.options.mode.chained_assignment = None

    

    # get one object's X values

    X_i = X.iloc[idx, :]

    

    # make a matrix with zeros with shape of [num_cols, num_cols]

    X_mat = np.zeros(shape=[X_i.shape[0], X_i.shape[0]])

    

    # fil X_mat with values from one by one columns, leaving the ohters zeros

    for i, j in enumerate(X_i):

        X_mat[i, i] = j

    

    # data frame with contribution of each X columns in descending order

    sensitivity_df = pd.DataFrame({'idx': idx

                                   , 'x': X_i

                                   , 'contribution_x': model.predict(X_mat)}).\

                    sort_values(by='contribution_x', ascending=True)

    

    # if bar_plot_yn == True then display it

    col_n = X_i.shape[0]

    if bar_plot_yn == True:

        sensitivity_df['contribution_x'].plot(kind='barh', figsize=(10, 0.7*col_n))

        plt.title('Sensitivity Analysis', fontsize=18)

        plt.xlabel('Contribution', fontsize=16)

        plt.ylabel('Variable', fontsize=16)

        plt.yticks(fontsize=14)

        plt.show()

    

    return sensitivity_df



# check UDF

sensitivity_df = sensitivity_analysis(model=lin_reg, X=X_test, idx=0, bar_plot_yn=True)

sensitivity_df




아래는 위에서 정의한 sensitivity_analysis() 사용자 정의 함수에서 bar_plot_yn=False 로 설정을 해서 옆으로 누운 막대그래프는 그리지 말고 기여도 분석 결과 DataFrame만 반환하라고 한 경우입니다. 



# without bar plot (bar_plot_yn=False)

sensitivity_df = sensitivity_analysis(model=lin_reg, X=X_test, idx=0, bar_plot_yn=False)

sensitivity_df

[Out]:

idxxcontribution_x
length00.2100.263315
shell_weight00.0130.031699
height00.0550.021341
diameter00.1500.011237
sex_m00.0000.000000
sex_f00.0000.000000
rings04.000-0.061437
const01.000-0.360857





(6) 다수의 관측치에 대해 '개별 관측치별 변수별 예측 기여도 분석'


위에서 부터 10개의 관측치를 선별해서, 개별 관측치별 각 변수별로 위의 (5)번에서 정의한 sensitivity_analysis() 사용자정의함수와 for loop 반복문을 사용해서 변수 기여도를 분석해보겠습니다. (단, 변수 기여도 막대 그래프는 생략)



# calculate sensitivity of each columns of the first 10 objects using for loop


# blank DataFrame to save the sensitivity results together

sensitivity_df_all = pd.DataFrame()

to_idx = 10


for idx in range(0, to_idx):

    sensitivity_df_idx = sensitivity_analysis(model=lin_reg

                                              , X=X_test

                                              , idx=idx

                                              , bar_plot_yn=False)

    

    sensitivity_df_all = pd.concat([sensitivity_df_all, sensitivity_df_idx], axis=0)

    

    print("[STATUS]", idx+1, "/", to_idx, "(", 100*(idx+1)/to_idx, "%) is completed...")


[STATUS] 1 / 10 ( 10.0 %) is completed...
[STATUS] 2 / 10 ( 20.0 %) is completed...
[STATUS] 3 / 10 ( 30.0 %) is completed...
[STATUS] 4 / 10 ( 40.0 %) is completed...
[STATUS] 5 / 10 ( 50.0 %) is completed...
[STATUS] 6 / 10 ( 60.0 %) is completed...
[STATUS] 7 / 10 ( 70.0 %) is completed...
[STATUS] 8 / 10 ( 80.0 %) is completed...
[STATUS] 9 / 10 ( 90.0 %) is completed...
[STATUS] 10 / 10 ( 100.0 %) is completed...

 



결과를 한번 확인해볼까요?



sensitivity_df_all[:20]

[Out]:

idxxcontribution_x
length00.21000.263315
shell_weight00.01300.031699
height00.05500.021341
diameter00.15000.011237
sex_m00.00000.000000
sex_f00.00000.000000
rings04.0000-0.061437
const01.0000-0.360857
length10.26000.326009
shell_weight10.03050.074372
height10.07000.027161
diameter10.20500.015357
sex_m10.00000.000000
sex_f10.00000.000000
rings14.0000-0.061437
const11.0000-0.360857
length20.52000.652018
shell_weight20.18400.448668
height20.11000.042682
diameter20.41000.030713




 2. 로지스틱 회귀모형에 대한 관측치별 변수별 민감도 분석

   (Sensitivity analysis for Logistic Regression model) 


제가 서두에서 민감도분석에 어떤 모델도 가능하도고 말씀드렸습니다. 그러면 이번에는 같은 abalone 데이터셋에 대해 목표변수로 전체무게(whole weight)가 평균보다 크면 '1', 평균보다 작으면 '0' 으로 범주를 구분한 후에, 이를 이진 분류(binary classification)할 수 있는 로지스틱 회귀모형(Logistic Regression)을 적합한 후에 --> 민감도 분석을 적용해보겠습니다. 


먼저, y_cat 이라는 범주형 변수를 만들고, train/ test set으로 분할을 한 후에, 로지스틱 회귀모형(Logistic Regression)을 적합후, 이진분류를 할 수 있는 확률을 계산(확률이 0.5보다 크면 '1'로 분류)해보겠습니다. 



# make a y_category variable: if y is greater or equal to mean, then 1

y_cat = np.where(abalone["whole_weight"] >= np.mean(abalone["whole_weight"]), 1, 0)

y_cat[:20]

[Out]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])


cat_class, counts = np.unique(y_cat, return_counts=True)

dict(zip(cat_class, counts))

[Out]: {0: 2178, 1: 1999}


# train, test set split

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, 

                                                    y_cat, 

                                                    test_size=0.2, 

                                                    random_state=2004)


# fitting logistic regression

import statsmodels.api as sm

pd.options.mode.chained_assignment = None


logitreg = sm.Logit(y_train, X_train)

logitreg_fit = logitreg.fit()


print(logitreg_fit.summary())

[Out]:

Optimization terminated successfully. Current function value: 0.108606 Iterations 11 Logit Regression Results ============================================================================== Dep. Variable: y No. Observations: 3341 Model: Logit Df Residuals: 3333 Method: MLE Df Model: 7 Date: Fri, 17 Jan 2020 Pseudo R-squ.: 0.8431 Time: 21:00:24 Log-Likelihood: -362.85 converged: True LL-Null: -2313.0 Covariance Type: nonrobust LLR p-value: 0.000 ============================================================================= coef std err z P>|z| [0.025 0.975] -------------------------------------------------------------------------------- const -35.3546 2.219 -15.935 0.000 -39.703 -31.006 sex_m 1.3064 0.250 5.217 0.000 0.816 1.797 sex_f 1.1418 0.258 4.420 0.000 0.635 1.648 length 36.6625 5.348 6.855 0.000 26.180 47.145 diameter 11.7960 6.160 1.915 0.055 -0.277 23.869 height 7.6179 2.011 3.789 0.000 3.677 11.559 shell_weight 38.0930 3.286 11.593 0.000 31.653 44.533 rings -0.1062 0.038 -2.777 0.005 -0.181 -0.031 =============================================================================


# prediction

test_prob_logitreg = logitreg_fit.predict(X_test)

test_prob_logitreg.head()

[Out]:
3473    9.341757e-12
3523    2.440305e-10
1862    1.147617e-02
2966    9.999843e-01
659     9.964952e-01
dtype: float64




다음으로 위의 선형회귀모형에 대한 민감도 분석 사용자정의 함수를 재활용하여 로지스틱 회귀모형에도 사용가능하도록 일부 수정해보았습니다. 


아래의 사용자 정의 함수는 로지스틱 회귀모형 적합 시 statsmodels 라이브러리를 사용한 것으로 가정하고 작성하였습니다. 만약 로지스틱 회귀모형의 model 적합에 from sklearn.linear_model import LogisticRegression 의 라이브러리를 사용하였다면 '#'으로 비활성화해놓은 부분을 해제하여 사용하면 됩니다. (predict_proba(X_mat)[:, 1] 의 부분이 달라서 그렇습니다)



# UDF for contribution(sensitivity) analysis per each variables

# task: "LinearReg" or "LogitReg"

def sensitivity_analysis_LinearReg_LogitReg(task, model, X, idx, bar_plot_yn):

    

    import numpy as np

    import pandas as pd

    import matplotlib.pyplot as plt

    import statsmodels.api as sm

    pd.options.mode.chained_assignment = None

    

    # get one object's X values

    X_i = X.iloc[idx, :]

    

    # make a matrix with zeros with shape of [num_cols, num_cols]

    X_mat = np.zeros(shape=[X_i.shape[0], X_i.shape[0]])

    

    # fil X_mat with values from one by one columns, leaving the ohters zeros

    for i, j in enumerate(X_i):

        X_mat[i, i] = j

        

    # data frame with contribution of each X columns in descending order

    sensitivity_df = pd.DataFrame({

        'idx': idx

        , 'task': task

        , 'x': X_i

        , 'contribution_x': model.predict(X_mat)     

    })

    

#     # ==== Remark =====

#     # if you used LogisticRegressionsklearn from sklearn.linear_model

#     # then use codes below

#     if task == "LinearReg":

#         sensitivity_df = pd.DataFrame({

#             'idx': idx

#             , 'task': task

#             , 'x': X_i

#             , 'contribution_x': model.predict(X_mat) 

#         })

        

#     elif task == "LogitReg":

#         sensitivity_df = pd.DataFrame({

#             'idx': idx

#             , 'task': task

#             , 'x': X_i

#             , 'contribution_x': model.predict_proba(X_mat)[:,1] 

#         })

#     else:

#         print('Please choose task one of "LinearReg" or "LogitReg"...')

    

    

    sensitivity_df = sensitivity_df.sort_values(by='contribution_x', ascending=True)

    

    # if bar_plot_yn == True then display it

    col_n = X_i.shape[0]

    if bar_plot_yn == True:

        sensitivity_df['contribution_x'].plot(kind='barh', figsize=(10, 0.7*col_n))

        plt.title('Sensitivity Analysis', fontsize=18)

        plt.xlabel('Contribution', fontsize=16)

        plt.ylabel('Variable', fontsize=16)

        plt.yticks(fontsize=14)

        plt.show()

    

    return sensitivity_df.sort_values(by='contribution_x', ascending=False)



# apply sensitivity analysis function on 1st observation for Logistic Regression

sensitivity_analysis_LinearReg_LogitReg(task="LogitReg"

                                        , model=logitreg_fit

                                        , X=X_test

                                        , idx=0

                                        , bar_plot_yn=True)




많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요

  1. Stat_in_KNU 2020.06.08 16:21 신고  댓글주소  수정/삭제  댓글쓰기

    선생님 안녕하십니까? 데이터분석전문가 시험을 준비중인 학생입니다. 시험 관련 자료를 찾던 중 우연히 들어왔는데 예전에도 꽤 방문한 흔적이 있던 블로그 더군요 매번 도움 받을 수 있어서 감사한 마음입니다. 다름이 아니라, 제가 공부한 것들을 정리하는 용도로 블로그를 운영하고있는데 선생님의 게시글을 참고해서 올리고 싶습니다. 블로그를 통한 수익은 전혀 없으며 조회수 또한 거의 없습니다. 혹시 괜찮으시다면 제 블로그에 포스팅해도될까요?

지난번 포스팅에서는 

 - 시계열의 구성 요인 (time series component factors) (https://rfriend.tistory.com/509)과 

 - 시계열 분해 (time series decomposition) (https://rfriend.tistory.com/510

에 대해서 소개하였습니다. 


이번 포스팅에서는 직관적이어서 이해하기 쉽고 (ARIMA 대비 상대적으로요), 또 시계열 자료의 구성요소가 변동이 느리거나 매우 규칙적(periodic)일 때 예측 정확도도 높아서 시계열 자료 예측에 실무적으로 많이 사용하는 지수 평활법 (exponential smoothing) 에 대해서 소개하겠습니다. 


지수 평활법의 기본 개념은 "최근 관측한 값에 높은 가중치를 주고, 먼 과거의 관측치에는 낮은 관측치를 주어 가중 평균값을 구한는 것" 입니다. 이때 가중치는 현재로 부터 과거로 갈 수록 지수적으로 감소(exponential decay)시켜서 차등적으로 주는 평활 함수(smoothing function)을 사용하는 것에서 "지수(exponential)" 이름을 따왔습니다. 그리고 여러개의 관측치를 모아서 가중 평균을 사용하기 때문에 "평활(smoothing)"되는 효과가 있어서, 이 둘을 합쳐서 "지수 평활법(exponential smoothing)"이라고 하는 것입니다. 


이전 Python 으로 차수 m인 단순이동평균(simple moving average) 구하는 방법을 포스팅한적이 있는데요, 단순이동평균은 가중치가 1/m 로 모두 같은 형태인 (m차 관측치만 이동해서 고려하는) 지수 평활법의 특수한 경우로 볼 수도 있겠습니다. 


지수 평활법의 기법을 결정하는데는 시도표 (time series plot)를 그려보고 아래의 3가지를 고려합니다. 



[ 시계열 특성에 따른 지수평활법 기법 결정 ]


(1) 시계열 자료에 추세(Trend)가 있는지, 추세가 있다면 1차 선형인지 아니면 비선형 인가? 

    - 추세 없음 (No Trend)                  --> Simple Exponential Smoothing

    - 1차 선형 추세 (Linear Trend)         --> Two Parameter Exponential Smoothing

    - 2차 비선형 추세 (Quadratic Trend) --> Three Parameter Smoothing


(2) 시계열 자료에 계절성(Seasonality)이 있는가? 

    - 계절성 없음 (No Seasonality)

    - 계정성 있음 (with Seasonality)  --> Winters' Method


(3) 시계열 자료의 계절성이 시간이 지남에 따라 고정(fixed)되어 있는지 아니면 확산(increasing)되는가? 

    - 고정(상수) 계절 변동 (fixed seasonal variation) --> Additive Model

    - 확산 계절 변동 (increasing seasonal variation)  --> Multiplicative Model



위의 설명을 각 시계열 자료 패턴별로 시도표 예시와 함께 decision tree 형태로 구분해서 지수 평활법 (exponential smoothing) 기법을 짝지어보면 아래와 같습니다. 



물론 위와 같이 분석가가 눈으로 시도표를 보고 나서 적합한 지수 평활법 기법을 선택하는 것이 아니라, 분석 시스템 성능이 뒷받침이 되고 분석 시간이 오래 소요되어도 문제가 없다면, 혹은 사람 개입 없이 자동화를 하고자 한다면 모든 지수 평활법 기법을 적용해보고 이들 모델 긴 적합도 평가 통계량(가령, RMSE, MAPE, MAE 등)을 비교해 본 후 적합이 가장 잘 된 것을 선택할 수도 있겠습니다.



[ 지수 평활법 기법 별 수식 ]


를 지수 평활법으로 예측하고자 하는 t 시점의 시계열 값이라고 하고, 를 t 시점의 계절 요소값, 를 t 시점에서의 오차라고 했을 때, 각 지수평활법 기법별로 모형의 수식을 표기해보면 아래와 같습니다. 지수평활법 기법 간 수식을 비교해보면 좀더 잘 이해할 수 있을 것입니다. 


  • 추세와 계절성이 모두 없는 단순 지수 평활법
    (Simple Exponential Smoothing)



  • 추세는 없고 고정계절변동이 있는 가법 윈터스 방법
     (Additive Winters' Method Exponential Smoothing with No Trend)



  • 추세는 없고 승법 윈터스 방법
    (Multiplicative Winters' Method Exponential Smoothing with No Trend)



  • 1차 선형 추세는 있고 계절성은 없이중 지수 평활법
    (Two Parameter Exponential Smoothing)



  • 1차 선형 추세와 고정계절변동이 있는 가법 윈터스 지수 평활법
    (Additive Winters' Method Exponential Smoothing with Linear Trend)



  • 1차 선형 추세와 확산계절변동이 있는 승법 원터스 지수 평활법
    (Multiplicative Winters' Method Exponential Smoothing with Linear Trend)



  • 2차 비선형 추세는 있고 계절성은 없는 삼중 지수 평활법
    (Three Parameter Exponential Smoothing)



  • 2차 비선형 추세와 고정계절변동이 있는 가법 윈터스 지수 평활법
    (Additive Winters' Method Exponential Smoothing with Quadratic Trend)



  • 2차 비선형 추세와 확산계절변동이 있는 승법 윈터스 지수 평활법
    (Multiplicative Winters' Method Exponential Smoothing with Quadratic Trend)

    : 


다음번 포스팅에서는 Python과 R로 단순 지수 평활법으로 시계열 자료를 예측하는 방법을 소개하겠습니다. 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)


Posted by R Friend Rfriend

댓글을 달아 주세요

지난번 포스팅에서는 시계열 자료의 구성 요인 (time series component factors)으로서 추세 요인, 순환 요인, 계절 요인, 불규칙 요인에 대해서 소개하였습니다. 


지난번 포스팅에서는 가법 모형으로 가상의 시계열 자료를 만들었다면(time series composition), ==> 이번 포스팅에서는 반대로 시계열 분해(time series decomposition)를 통해 시계열 자료를 추세(순환)(Trend), 계절성(Seasonality), 잔차(Residual)로 분해를 해보겠습니다. 


시계열 분해는 직관적으로 이해하기 쉽고 구현도 쉬워서 시계열 자료 분석의 고전적인 방법론이지만 지금까지도 꾸준히 사용되는 방법론입니다. 시계열 분해의 순서와 방법은 대략 아래와 같습니다. 


(1) 시도표 (time series plot)를 보고 시계열의 주기적 반복/계절성이 있는지, 가법 모형(additive model, y = t + s + r)과 승법 모형(multiplicative model, y = t * s * r) 중 무엇이 더 적합할지 판단을 합니다. 


(가법 모형을 가정할 시)

(2) 시계열 자료에서 추세(trend)를 뽑아내기 위해서 중심 이동 평균(centered moving average)을 이용합니다. 


(3) 원 자료에서 추세 분해값을 빼줍니다(detrend). 그러면 계절 요인과 불규칙 요인만 남게 됩니다. 


(4) 다음에 계절 주기 (seasonal period) 로 detrend 이후 남은 값의 합을 나누어주면 계절 평균(average seasonality)을 구할 수 있습니다. (예: 01월 계절 평균 = (2020-01 + 2021-01 + 2022-01 + 2023-01)/4, 02월 계절 평균 = (2020-02 + 2021-02 + 2022-02 + 2023-02)/4). 


(5) 원래의 값에서 추세와 계절성 분해값을 빼주면 불규칙 요인(random, irregular factor)이 남게 됩니다. 



시계열 분해 후에 추세와 계절성을 제외한 잔차(residual, random/irregular factor) 가 특정 패턴 없이 무작위 분포를 띠고 작은 값이면 추세와 계절성으로 모형화가 잘 되는 것이구요, 시계열 자료의 특성을 이해하고 예측하는데 활용할 수 있습니다. 만약 시계열 분해 후의 잔차에 특정 패턴 (가령, 주기적인 파동을 그린다거나, 분산이 점점 커진다거나 등..) 이 존재한다면 잔차에 대해서만 다른 모형을 추가로 적합할 수도 있겠습니다. 



예제로 사용할 시계열 자료로서 '1차 선형 추세 + 4년 주기 순환 + 1년 단위 계절성 + 불규칙 noise' 의 가법 모형 (additive model)으로 시계열 데이터를 만들어보겠습니다. 



import numpy as np

import pandas as pd

import matplotlib.pyplot as plt


dates = pd.date_range('2020-01-01', periods=48, freq='M')

dates

[Out]:

DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31', '2020-04-30', '2020-05-31', '2020-06-30', '2020-07-31', '2020-08-31', '2020-09-30', '2020-10-31', '2020-11-30', '2020-12-31', '2021-01-31', '2021-02-28', '2021-03-31', '2021-04-30', '2021-05-31', '2021-06-30', '2021-07-31', '2021-08-31', '2021-09-30', '2021-10-31', '2021-11-30', '2021-12-31', '2022-01-31', '2022-02-28', '2022-03-31', '2022-04-30', '2022-05-31', '2022-06-30', '2022-07-31', '2022-08-31', '2022-09-30', '2022-10-31', '2022-11-30', '2022-12-31', '2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30', '2023-05-31', '2023-06-30', '2023-07-31', '2023-08-31', '2023-09-30', '2023-10-31', '2023-11-30', '2023-12-31'], dtype='datetime64[ns]', freq='M')

# additive model: trend + cycle + seasonality + irregular factor


timestamp = np.arange(len(dates))

trend_factor = timestamp*1.1

cycle_factor = 10*np.sin(np.linspace(0, 3.14*2, 48))

seasonal_factor = 7*np.sin(np.linspace(0, 3.14*8, 48))

np.random.seed(2004)

irregular_factor = 2*np.random.randn(len(dates))


df = pd.DataFrame({'timeseries': trend_factor + cycle_factor + seasonal_factor + irregular_factor, 

                   'trend': trend_factor, 

                   'cycle': cycle_factor, 

                   'trend_cycle': trend_factor + cycle_factor,

                   'seasonal': seasonal_factor, 

                   'irregular': irregular_factor},

                   index=dates)


df

[Out]:

timeseriestrendcycletrend_cycleseasonalirregular
2020-01-312.5961190.00.0000000.0000000.0000002.596119
2020-02-296.7461601.11.3321982.4321983.5656840.748278
2020-03-318.1121002.22.6406474.8406476.136825-2.865371
2020-04-308.2559413.33.9020217.2020216.996279-5.942358
2020-05-3116.8896554.45.0938349.4938345.9043271.491495
2020-06-3016.1823575.56.19483911.6948393.1655361.321981
2020-07-3114.1280876.67.18540913.785409-0.4561870.798865
2020-08-3111.9433137.78.04788615.747886-3.9506710.146099
2020-09-309.7280958.88.76689217.566892-6.343231-1.495567
2020-10-3112.4834899.99.32961219.229612-6.9665330.220411
2020-11-3012.14180811.09.72601320.726013-5.646726-2.937480
2020-12-3115.14333412.19.94902922.049029-2.751930-4.153764
2021-01-3121.77451613.29.99468423.1946840.910435-2.330604
2021-02-2828.43289214.39.86216424.1621644.318862-0.048134
2021-03-3132.35058315.49.55383224.9538326.5226690.874082
2021-04-3030.59655616.59.07518425.5751846.907169-1.885797
2021-05-3132.51052317.68.43475326.0347535.3651181.110653
2021-06-3030.42551918.77.64395526.3439552.3266241.754939
2021-07-3124.30095819.86.71689026.516890-1.360813-0.855119
2021-08-3120.45091720.95.67008226.570082-4.668691-1.450475
2021-09-3018.87088122.04.52219526.522195-6.674375-0.976939
2021-10-3121.32631023.13.29369026.393690-6.8184381.751059
2021-11-3022.90244824.22.00646926.206469-5.0606991.756678
2021-12-3126.62057825.30.68347825.983478-1.8914262.528526
2022-01-3127.62649926.4-0.65169625.7483041.8054040.072791
2022-02-2831.85892327.5-1.97525325.5247474.9986701.335506
2022-03-3135.93046928.6-3.26359825.3364026.7977043.796363
2022-04-3030.17787029.7-4.49376225.2062386.700718-1.729087
2022-05-3130.01616530.8-5.64381625.1561844.7347640.125217
2022-06-3026.59172931.9-6.69325825.2067421.448187-0.063200
2022-07-3121.11848133.0-7.62337925.376621-2.242320-2.015820
2022-08-3116.63603134.1-8.41759925.682401-5.307397-3.738973
2022-09-3017.68261335.2-9.06175926.138241-6.892132-1.563496
2022-10-3121.16329836.3-9.54437526.755625-6.5545090.962182
2022-11-3022.45567237.4-9.85684427.543156-4.388699-0.698786
2022-12-3126.91952938.5-9.99359528.506405-0.998790-0.588086
2023-01-3133.96462339.6-9.95219129.6478092.6697021.647112
2023-02-2837.45977640.7-9.73336930.9666315.5935590.899586
2023-03-3140.79376641.8-9.34103132.4589696.9572571.377540
2023-04-3043.83841542.9-8.78217134.1178296.3804333.340153
2023-05-3141.30178044.0-8.06675135.9332494.0239751.344556
2023-06-3039.21786645.1-7.20752637.8924740.5451470.780245
2023-07-3135.12550246.2-6.21981339.980187-3.085734-1.768952
2023-08-3133.84192647.3-5.12121942.178781-5.855940-2.480916
2023-09-3038.77051148.4-3.93132944.468671-6.9928031.294643
2023-10-3137.37121649.5-2.67135646.828644-6.179230-3.278198
2023-11-3046.58763350.6-1.36376049.236240-3.6421420.993536
2023-12-3146.40332651.7-0.03185351.668147-0.089186-5.175634





  (1) Python을 이용한 시계열 분해 (Time series decomposition using Python)


Python의 statsmodels 라이브러리를 사용해서 가법 모형(additive model) 가정 하에 시계열 분해를 해보겠습니다. 



from statsmodels.tsa.seasonal import seasonal_decompose


ts = df.timeseries

result = seasonal_decompose(ts, model='additive')


plt.rcParams['figure.figsize'] = [12, 8]

result.plot()

plt.show()





원래의 시계열 구성요소(추세+순환, 계절성, 불규칙 요인)와 시계열 분해(time series decomposition)를 통해 분리한 추세(&순환), 계절성, 잔차(불규칙 요인)를 겹쳐서 그려보았습니다. (즉, 원래 데이터의 추세요인과 시계열 분해를 통해 분리한 추세를 겹쳐서 그려보고, 원래 데이터의 계절요인과 시계열 분해를 통해 분리한 계절을 겹쳐서 그려보고, 원래 데이터의 불규칙 요인과 시계열 분해를 통해 분리한 잔차를 겹쳐서 그려봄) 


원래의 데이터와 얼추 비슷하게, 그럴싸하게 시계열 분해를 한 것처럼 보이지요?   



# ground truth & timeseries decompostion all together

# -- observed data

plt.figure(figsize=(12, 12))

plt.subplot(4,1, 1)

result.observed.plot()

plt.grid(True)

plt.ylabel('Observed', fontsize=14)


# -- trend & cycle factor

plt.subplot(4, 1, 2)

result.trend.plot()        # from timeseries decomposition

df.trend_cycle.plot()     # groud truth

plt.grid(True)

plt.ylabel('Trend', fontsize=14)


# -- seasonal factor

plt.subplot(4, 1, 3)

result.seasonal.plot()  # from timeseries decomposition

df.seasonal.plot()        # groud truth

plt.grid(True)

plt.ylabel('Seasonality', fontsize=14)


# -- irregular factor (noise)

plt.subplot(4, 1, 4)

result.resid.plot()    # from timeseries decomposition

df.irregular.plot()    # groud truth

plt.grid(True)

plt.ylabel('Residual', fontsize=14)


plt.show()




원래의 관측치(observed), 추세(trend), 계절성(seasonal), 잔차(residual) 데이터 아래처럼 시계열 분해한 객체에서 obsered, trend, seasonal, resid 라는 attributes 를 통해서 조회할 수 있습니다. 


Observed

 Trend ( & Cycle)

 print(result.observed)

[Out]:
2020-01-31     2.596119
2020-02-29     6.746160
2020-03-31     8.112100
2020-04-30     8.255941
2020-05-31    16.889655
2020-06-30    16.182357
2020-07-31    14.128087
2020-08-31    11.943313
2020-09-30     9.728095
2020-10-31    12.483489
2020-11-30    12.141808
2020-12-31    15.143334
2021-01-31    21.774516
2021-02-28    28.432892
2021-03-31    32.350583
2021-04-30    30.596556
2021-05-31    32.510523
2021-06-30    30.425519
2021-07-31    24.300958
2021-08-31    20.450917
2021-09-30    18.870881
2021-10-31    21.326310
2021-11-30    22.902448
2021-12-31    26.620578
2022-01-31    27.626499
2022-02-28    31.858923
2022-03-31    35.930469
2022-04-30    30.177870
2022-05-31    30.016165
2022-06-30    26.591729
2022-07-31    21.118481
2022-08-31    16.636031
2022-09-30    17.682613
2022-10-31    21.163298
2022-11-30    22.455672
2022-12-31    26.919529
2023-01-31    33.964623
2023-02-28    37.459776
2023-03-31    40.793766
2023-04-30    43.838415
2023-05-31    41.301780
2023-06-30    39.217866
2023-07-31    35.125502
2023-08-31    33.841926
2023-09-30    38.770511
2023-10-31    37.371216
2023-11-30    46.587633
2023-12-31    46.403326
Freq: M, Name: timeseries, 
dtype: float64

print(result.trend)

[Out]

2020-01-31          NaN
2020-02-29          NaN
2020-03-31          NaN
2020-04-30          NaN
2020-05-31          NaN
2020-06-30          NaN
2020-07-31    11.994971
2020-08-31    13.697685
2020-09-30    15.611236
2020-10-31    17.552031
2020-11-30    19.133760
2020-12-31    20.378094
2021-01-31    21.395429
2021-02-28    22.173782
2021-03-31    22.909215
2021-04-30    23.658616
2021-05-31    24.475426
2021-06-30    25.402005
2021-07-31    26.124056
2021-08-31    26.510640
2021-09-30    26.802553
2021-10-31    26.934270
2021-11-30    26.812893
2021-12-31    26.549220
2022-01-31    26.256876
2022-02-28    25.965319
2022-03-31    25.756854
2022-04-30    25.700551
2022-05-31    25.675143
2022-06-30    25.668984
2022-07-31    25.945528
2022-08-31    26.442986
2022-09-30    26.878992
2022-10-31    27.650819
2022-11-30    28.690242
2022-12-31    29.686565
2023-01-31    30.796280
2023-02-28    32.096818
2023-03-31    33.692393
2023-04-30    35.246385
2023-05-31    36.927214
2023-06-30    38.744537
2023-07-31          NaN
2023-08-31          NaN
2023-09-30          NaN
2023-10-31          NaN
2023-11-30          NaN
2023-12-31          NaN 

Freq: M, Name: timeseries,

dtype: float64 



 Seasonality 

 Residual (Noise)

print(result.seasonal)

[Out]:
2020-01-31    1.501630
2020-02-29    5.701170
2020-03-31    8.768065
2020-04-30    6.531709
2020-05-31    5.446174
2020-06-30    2.002476
2020-07-31   -1.643064
2020-08-31   -6.011071
2020-09-30   -7.807785
2020-10-31   -5.858728
2020-11-30   -5.849710
2020-12-31   -2.780867
2021-01-31    1.501630
2021-02-28    5.701170
2021-03-31    8.768065
2021-04-30    6.531709
2021-05-31    5.446174
2021-06-30    2.002476
2021-07-31   -1.643064
2021-08-31   -6.011071
2021-09-30   -7.807785
2021-10-31   -5.858728
2021-11-30   -5.849710
2021-12-31   -2.780867
2022-01-31    1.501630
2022-02-28    5.701170
2022-03-31    8.768065
2022-04-30    6.531709
2022-05-31    5.446174
2022-06-30    2.002476
2022-07-31   -1.643064
2022-08-31   -6.011071
2022-09-30   -7.807785
2022-10-31   -5.858728
2022-11-30   -5.849710
2022-12-31   -2.780867
2023-01-31    1.501630
2023-02-28    5.701170
2023-03-31    8.768065
2023-04-30    6.531709
2023-05-31    5.446174
2023-06-30    2.002476
2023-07-31   -1.643064
2023-08-31   -6.011071
2023-09-30   -7.807785
2023-10-31   -5.858728
2023-11-30   -5.849710
2023-12-31   -2.780867
Freq: M, Name: timeseries, 
dtype: float64

print(result.resid)

[Out]:
2020-01-31         NaN
2020-02-29         NaN
2020-03-31         NaN
2020-04-30         NaN
2020-05-31         NaN
2020-06-30         NaN
2020-07-31    3.776179
2020-08-31    4.256699
2020-09-30    1.924644
2020-10-31    0.790186
2020-11-30   -1.142242
2020-12-31   -2.453893
2021-01-31   -1.122544
2021-02-28    0.557940
2021-03-31    0.673303
2021-04-30    0.406231
2021-05-31    2.588922
2021-06-30    3.021039
2021-07-31   -0.180034
2021-08-31   -0.048653
2021-09-30   -0.123887
2021-10-31    0.250769
2021-11-30    1.939265
2021-12-31    2.852225
2022-01-31   -0.132007
2022-02-28    0.192434
2022-03-31    1.405550
2022-04-30   -2.054390
2022-05-31   -1.105152
2022-06-30   -1.079730
2022-07-31   -3.183983
2022-08-31   -3.795884
2022-09-30   -1.388594
2022-10-31   -0.628793
2022-11-30   -0.384861
2022-12-31    0.013830
2023-01-31    1.666713
2023-02-28   -0.338212
2023-03-31   -1.666692
2023-04-30    2.060321
2023-05-31   -1.071608
2023-06-30   -1.529146
2023-07-31         NaN
2023-08-31         NaN
2023-09-30         NaN
2023-10-31         NaN
2023-11-30         NaN
2023-12-31         NaN 

Freq: M, Name: timeseries,

dtype: float64 




# export to csv file

df.to_csv('ts_components.txt', sep=',', index=False)

 





  (2) R을 이용한 시계열 분해 (Time series Decomposition using R)


위에서 가법 모형을 적용해서 Python으로 만든 시계열 자료를 text 파일로 내보낸 후, 이를 R에서 읽어서 시계열 분해 (time series decomposition)를 해보겠습니다. 


ts_components.txt



# read time series text file

df <- read.table('ts_components.txt', sep=',', header=T)

head(df)

A data.frame: 6 × 6
timeseriestrendcycletrend_cycleseasonalirregular
<dbl><dbl><dbl><dbl><dbl><dbl>
2.5961190.00.0000000.0000000.0000002.5961193
6.7461601.11.3321982.4321983.5656840.7482781
8.1121002.22.6406474.8406476.136825-2.8653712
8.2559413.33.9020217.2020216.996279-5.9423582
16.8896554.45.0938349.4938345.9043271.4914946
16.1823575.56.19483911.6948393.1655361.3219812

 



이렇게 불러와서 만든 df DataFrame의 칼럼 중에서 시계열 분해를 할 'timeseries' 칼럼만을 가져와서 ts() 함수를 사용하여 1년 12개월 이므로 frequency =  12로 설정해 R의 시계열 자료 형태로 변환합니다. 


그 다음에 decompose() 함수를 사용하여 시계열 분해를 하는데요, 이때 가법 모형 (additive model)을 적용할 것이므로 decompose(ts, "additive") 라고 설정해줍니다. 


시계열 분해를 한 결과를 모아놓은 리스트 ts_decompose 객체를 프린트해보면 원래의 값 $x, 계절 요인 $seasonal, 추세(&순환) 요인 $trend, 불규칙 요인 $random 분해값이 순서대로 저장되어 있음을 알 수 있습니다. 



# transforming data to time series with 12 months frequency

ts <- ts(df$timeseries, frequency = 12) # 12 months


# time series decomposition

ts_decompose <- decompose(ts, "additive") # additive model


# decomposition results

ts_decompose

[Out]:

$x Jan Feb Mar Apr May Jun Jul 1 2.596119 6.746160 8.112100 8.255941 16.889655 16.182357 14.128087 2 21.774516 28.432892 32.350583 30.596556 32.510523 30.425519 24.300958 3 27.626499 31.858923 35.930469 30.177870 30.016165 26.591729 21.118481 4 33.964623 37.459776 40.793766 43.838415 41.301780 39.217866 35.125502 Aug Sep Oct Nov Dec 1 11.943313 9.728095 12.483489 12.141808 15.143334 2 20.450917 18.870881 21.326310 22.902448 26.620578 3 16.636031 17.682613 21.163298 22.455672 26.919529 4 33.841926 38.770511 37.371216 46.587633 46.403326 $seasonal Jan Feb Mar Apr May Jun Jul 1 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 2 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 3 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 4 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 Aug Sep Oct Nov Dec 1 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 2 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 3 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 4 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 $trend Jan Feb Mar Apr May Jun Jul Aug 1 NA NA NA NA NA NA 11.99497 13.69769 2 21.39543 22.17378 22.90922 23.65862 24.47543 25.40200 26.12406 26.51064 3 26.25688 25.96532 25.75685 25.70055 25.67514 25.66898 25.94553 26.44299 4 30.79628 32.09682 33.69239 35.24639 36.92721 38.74454 NA NA Sep Oct Nov Dec 1 15.61124 17.55203 19.13376 20.37809 2 26.80255 26.93427 26.81289 26.54922 3 26.87899 27.65082 28.69024 29.68657 4 NA NA NA NA $random Jan Feb Mar Apr May Jun 1 NA NA NA NA NA NA 2 -1.12254398 0.55793990 0.67330316 0.40623129 2.58892205 3.02103851 3 -0.13200705 0.19243403 1.40555039 -2.05439035 -1.10515213 -1.07973023 4 1.66671294 -0.33821202 -1.66669164 2.06032097 -1.07160802 -1.52914637 Jul Aug Sep Oct Nov Dec 1 3.77617915 4.25669908 1.92464365 0.79018590 -1.14224232 -2.45389325 2 -0.18003389 -0.04865275 -0.12388741 0.25076875 1.93926482 2.85222467 3 -3.18398336 -3.79588443 -1.38859434 -0.62879275 -0.38486059 0.01383049 4 NA NA NA NA NA NA $figure [1] 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 [8] -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 $type [1] "additive" attr(,"class") [1] "decomposed.ts"

 




위의 분해 결과가 숫자만 잔뜩 들어있으니 뭐가 뭔지 잘 눈에 안들어오지요?  그러면 이제 원래의 값 (observed)과 시계열 분해된 결과인 trend, seasonal, random 을 plot() 함수를 사용하여 다같이 시각화해보겠습니다. 



# change plot in jupyter

library(repr)


# Change plot size to 12 x 10

options(repr.plot.width=12, repr.plot.height=10)


plot(ts_decompose)

 



위의 분해 결과를 Trend (ts_decompose$trend), Seasonal (ts_decompose$seasonal), Random (ts_decompose$random) 의 각 요소별로 나누어서 시각화해볼 수도 있습니다. 



# change the plot size

options(repr.plot.width=12, repr.plot.height=5)


# Trend

plot(as.ts(ts_decompose$trend))

# Seasonality

plot(as.ts(ts_decompose$seasonal))

# Random (Irregular factor)

plot(as.ts(ts_decompose$random))




많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요