이번 포스팅에서는 Python numpy 의 배열 원소의 순서를 거꾸로 뒤집기 (how to reverse the python numpy array) 하는 두가지 방법을 소개하겠습니다. 1D array의 원소를 뒤집는 것은 간단한데요, 2D 이상의 다차원 배열(multi-dimensional array)의 경우 좀 복잡하므로 간단한 예를 들어 유형별로 설명을 해보겠습니다.  


(1) x[::-1] 를 이용해서 배열 뒤집기 (mirror 반환하기)

(2) np.flip(x) 를 이용해서 배열 뒤집기





   1 차원 numpy 배열을 뒤집기 (how to reverse 1D numpy array?)


먼저 예제로 사용할 간단한 1차원 numpy 배열을 만들어보겠습니다. 



import numpy as np


# 1D array

arr_1d = np.arange(5)

arr_1d 

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




다음으로, 1차원 numpy 배열을 (1) x[::-1] 방법과, (2) np.flip(x) 방법을 이용하여 뒤집어보겠습니다. 


 (1) x[::-1]

(2) np.flip(x)


# returns a view in reversed order

arr_1d[::-1]

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


# 1D array in reversed order using np.flip()

np.flip(arr_1d)

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





  2 차원 numpy 배열을 뒤집기 (how to reverse 2D numpy array?)


2차원 이상의 numpy 배열 뒤집기는 말로 설명하기가 좀 어렵고 복잡합니다. 왜냐하면 배열의 차원(축, axis) 을 무엇으로 하느냐에 따라서 뒤집기의 기준과 기대하는 결과의 모습(reversed output) 달라지기 때문입니다. 따라서 아래에는 2차원 numpy 배열에 대해서 3가지 경우의 수에 대해서 각각 x[::-1] 과 np.flip(x) 을 사용한 방법을 소개하였으니 원하는 뒤집기 output 에 맞게 선택해서 사용하시기 바랍니다. 


먼저 예제로 사용할 2차원 numpy 배열(2D numpy array)을 만들어보겠습니다. 



import numpy as np


# 2D array

arr_2d = np.arange(10).reshape(2, 5)

arr_2d

[Out]:
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])




이제 2차원 numpy 배열을 한번 뒤집어 볼까요? 



(2-1) axis = 0 기준으로 2차원 numpy 배열 뒤집기 (how to reverse numpy 2D array by axis=0)


 (1) x[::-1]

 (2) np.flip(x, axis=0)


# returns a view in reversed order by axis=0

arr_2d[::-1]

[Out]:

array([[5, 6, 7, 8, 9], [0, 1, 2, 3, 4]])


# reverse 2D array by axis 0

np.flip(arr_2d, axis=0)

[Out]:
array([[5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4]])




(2-2) axis = 1 기준으로 2차원 numpy 배열 뒤집기 (how to reverse numpy 2D array by axis=1)


 (1) x[:, ::-1]

 (2) np.flip(x, axis=1)


# returns a view of 2D array by axis=1

arr_2d[:, ::-1]

[Out]:
array([[4, 3, 2, 1, 0],
       [9, 8, 7, 6, 5]])


# reverse 2D array by axis 1

np.flip(arr_2d, axis=1) 

[Out]:
array([[4, 3, 2, 1, 0],
       [9, 8, 7, 6, 5]])




(2-3) axis =0 & axis = 1 기준으로 2차원 numpy 배열 뒤집기 (revserse numpy 2D array by axis=0 &1)


 (1) x[:, ::-1][::-1]

 (2) np.flip(x)


# returns a view

arr_2d[:, ::-1][::-1]

[Out]:

array([[9, 8, 7, 6, 5], [4, 3, 2, 1, 0]])


# 2D array

np.flip(arr_2d)

[Out]:
array([[9, 8, 7, 6, 5],
       [4, 3, 2, 1, 0]])



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

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

728x90
반응형
Posted by Rfriend
,

 2019년 12월에 중국 우한에서 신종 코로나 바이러스 감염증 환자가 발생한 이래, 국내에서도 최근 신종 코로나 바이러스로 언론은 연일 기사를 쏟아내고 있고, 정치인들은 정쟁의 요소로 삼아 서로 남탓 하기에 여념이 없습니다. 


 언론은 정보를 수집/편집/전달하는데 있어 중요한 역할을 하며, 정치는 한정된 국가 자원의 배분에 대한 협의와 의사결정을 하는 곳이므로 매우 중요합니다. 이번 포스팅에서는 도를 넘어선 언론과 자유한국당의 언행에 문제제기를 하고자 합니다. 



  1. 신종 코로나 바이러스 감염증에 대한 언론의 행태는 어떠하며, 무엇이 문제인가?


 일부 언론은 이번 신종 코로나 바이러스 감염증에 대한 보도 시 (1) 과도한 불안을 조장하고, (2) 정부에 대해 국민이 불신하게끔 언론 플레이를 하고 있으며, (3) 중국인에 대한 혐오를 부추기고 있습니다. 



 조선일보는 2020.01.28일 (a) '지도에도 없는 샛길로 우한 탈출... 우리 차 뒤로 수십대가 따라왔다'(박수찬 특파원)는 기사와 (b) '5일간 돌아다닌 2명... 방역 뚫렸다' (김철중 의학전문기자, 정석우 기자) 는 두 개의 기사를 실었습니다. 한쪽에서는 국내 방역 뚫린것을 개탄하면서, 다른 한쪽에서는 중국의 방역을 뚫고 다닌 기자의 기사를 나란히 전하는 자가당착에 빠졌습니다. 박수찬 특파원이 탈출하면서 이를 기사화한 것도 어이가 없는 일인데, 이것을 조선일보 데스크에서는 스크리닝없이 그대로 기사화를 했습니다. 



만약에 중국 우한지역에서 중국 기자 한명이 국내의 정식 검역 절차를 무시하고 비공식적인 루트를 통해서 국내에 몰래 잠입해왔다고 가정을 해보세요. 그리고 이걸 중국 기자가 기사를 썼고, 중국의 언론사가 이를 기사화했다고 가정해보세요. 만약 이랬다면 아마도 우리나라 전체가 발칵 뒤집혔을 겁니다. 




 아래 기사는 조선일보의 2020.01.29일 기사 '과도한 불안 갖지말라 했던 문대통령 "과하다 할 정도로 대응하라"'(안준용 기자) 라는 제목의 기사입니다. 2020.01.26일 문 대통령의 대국민메시지인 "정부를 믿고 과도한 불안을 갖지 말 것을 당부드린다"는 말씀은 말 그대로 '대 국민'을 향한 메시지 였으며, 잘못된게 없습니다. 그리고 2020.01.29일의 "선제적 조치가 조금 과하다는 평가가 있을 정도로 강력하고 발 빠르게 시행돼야 한다"고 문 대통령께서 말씀하신 것은 정부의 의료/방역 담당자들에게 지시를 하신 것으로서 역시 잘못된게 하나도 없습니다. 그런데 조선일보는 대국민 메시지와 정부 의료/방역 담당자에게 지시한 내용을 교묘히 섞어서 제목을 가지고 말장난을 하고 있습니다. 마치 문재인 정부가 우왕좌왕하고 있는 것처럼 보이게 하려고 말이지요. 




 중앙일보는 2020.01.28일 '단독'으로 "[단독] '전세기 철수' 우한 교민, 2주간 천안 2곳에 격리한다"(김기환 기자)는 기사를 냈고, 다음날인 2020.01.29일 "천안 --> 아산, 진천... 느닷없는 변심이 '우한 갈등' 키웠다"(김기환 기자)는 기사를 냈습니다. 두 기사 모두 김기환 기자가 작성했는데요, 김기환 기자는 자기가 정부에서 검토 중인 내용을 정부 허락도 없이 단독으로 '천안 2곳에 격리한다'는 기사를 내보내서 천안 시민을 불안에 떨게 선동을 해놓고 (정부는 검토 중이었고, 민감한 사안이라 지역을 밝힐 수 없다고 했었음), 그 다음날에는 뻔뻔하게 '정부가 천안에서 느닷없이 변심하여 아산, 진천으로 변경해 우한 갈등을 키웠다'라고 갈등의 원인을 정부로 떠넘기고 있습니다. 01.28일 단독 기사 이후에 여러 언론사에서도 동시다발적으로 '천안' 격리시설 기사를 내보냈고, 천안은 난리가 났습니다. 01.29일에는 '아산, 진천'에 트랙터가 등장하고 또 난리가 났구요. 



 2020.01.29일의 정부 보도참고자료에 보면, “각 시설의 수용능력, 인근지역 의료시설의 위치, 공항에서 시설간의 이동거리, 지역안배 등을 고려하여 선정하였다. 당초 대형시설 한곳에서 지낼 수 있도록 하고자 하였으나 귀국 희망 국민수가 처음 150여 명 수준에서 700여 명 이상으로 증가하고, 감염가능성을 원천적으로 차단하기 위해 1인 1실(별도 화장실 포함) 방역원칙에 따라 방역통제가 가능한 시설로 선정하게 되었다”고 하였습니다. 그런데 중앙일보(김기환 기자)는 천안 시민의 반발에 정부가 못이겨서 아산, 진천으로 격리 지역/시설을 바꾼 것처럼 해놓고 결과적으로 아산, 진천 시민의 성화를 부추기고 있습니다. 


 우한에 있는 우리 교민 700여명을 전세기로 데리고 오는 것은 국가가 마땅히 해야할 일입니다. 그리고 국내 시민들의 감염 방지를 위해서 잠복기를 고려해 14일간 1인 1실방에서 격리 수용하는 것도 현실적인 방안이라고 생각합니다.(미국은 비행기 격납고, 호주는 섬, 일본은 자가 격리) 그러면 국내의 국가 시설 중에 앞서의 정부 보도참고자료에 나온 기준에 따라 적합한 시설을 정했으면 그에 따라 격리하고, 이후에 철저히 소독하면 되는 것입니다. 공기 중 전염은 안일어나고 침이나 분비물에 의한 감염만 일어나므로 무증상자만 버스로 태워서 격리 시설로 이동해서 격리한다면 지역사회에 전염 전파될 위험은 거의 없습니다. 이걸 중앙일보(김기환 기자)는 [단독]을 제목에 달고 처음으로 보도하고, 또 다음날 '정부의 변심' 운운하며 정부 욕을 하고 있습니다. 김기환 기자님, 님이 무슨 잘못을 한건지 찬찬히 생각해보세요. 중앙일보 편집실장님, 언론의 책임이라는게 무엇인지 고민해보시기 바랍니다. 



 중앙일보는 2020.01.29일 '[현장에서] 전세기도 마스크 지원도 일본보다 한발 늦은 정부' (서유진 기자) 기사를 냈습니다. 저는 정부가 우리 국민을 위해 전세기를 투입하기로 한 결정도, 그리고 중국에 마스크를 지원하기로 한 결정도 잘한 것이라고 생각합니다. 일본보다 이틀 늦은 것을 가지고 또 중앙일보는 한국 정부를 어떻게든 깍아내리기에 여념이 없습니다.





 헤럴드경제는 2020.01.29일자 기사에서 "[르포] 대림동 차이나타운 가보니... 가래침 뱉고, 마스크 미착용 '위생불량 심각'"(윤호 기자, 유동현 수습기자, 신주희 수습기자(joohee@heraldcorp.com)) 이라는 기사를 냈습니다. 


 대림동 차이나타운의 중국인 또는 화교 중에서 중국 우한에서 최근에 온 사람은 얼마나 되는지 확인해보는게 먼저일텐데요, 이런 언급은 없고 '위생불량 심각'이라고 꼬리표를 달아 놓았습니다. 이 기사 내용 중에 보면 "노상에는 고기, 순대, 탕후루(각종 열매를 꼬치에 꿰어 사탕물을 묻혀 굳힌 중국 전통 과자), 도넛 등 음식 대부분이 바깥에 진열돼 있었다. 맨손으로 길거리에 진열돼 있는 탕후루를 만지는 관광객과 묵을 만지는 상인들도 눈에 띄었다. 대림중앙시장 공영 주차장 쪽 흡연금지 구역에서는 중년 남성들이 모여 담배를 피운 후 가래침을 길바닥에 뱉는 경우가 다반사였다. 일부 행인은 마스크를 착용하고 있지만 중국인 또는 화교처럼 보이는 사람 중에는 마스크를 착용하는 비율이 극히 낮았다." 라고 되어있는데요, 이는 한국인이 운영하는 시장의 모습과도 크게 달라보이지 않습니다. 그냥 국내 중국인에 대한 혐오를 조장하는 매우 나쁜 기사입니다. 




 아래 조선일보 2020.01.30일자 기사 "[사설] '격리 시설' 여 지역서 야 지역으로 변경, 왜 일을 키우나' 라는 사설에서 아예 대놓고 여/야 정치 싸움으로 변질을 시켜놓고 이번 신종 코로나 바이러스 감염증 사태를 오는 총선과 연결시키려고 하는게 아닌가 하는 의심마저 들게 하고 있습니다. 





  2. 신종 코로나 바이러스 감염증에 대한 자유한국당의 행태는 어떠하며, 무엇이 문제인가?


 정부가 이번 신종 코로나 바이러스 감염증과 같은 전염병 국가 비상사태에 제대로 대응하기 위해서는 인력과 장비 지원이 필요합니다. 그런데 보건복지부가 요청한 현장 검역 인력 증원 예산을 자유한국당의 공무원 증원 반대로 인해 2017년 27명, 2018년 25명, 2019년 3명 등 총 55명의 현장검역인력 증원 예산이 삭감되었습니다. (* 출처 : '현장은 전쟁터라는데 정치권은 ‘책임’ 공방', 의약뉴스, 2020.01.30 게다가 저소득층에 대한 마스크 지원금도 자유한국당의 반대 때문에 삭감되었구요. 


 우리는 이와 비슷한 장면을 2018년 국회예산안 통과 시 본 적이 있습니다. 소방관, 경찰, 사회복지사, 집배원, 해경, 119구조대 등 민생과 밀접한 관련이 있는 공무원을 충원하기 위한 정부와 여당의 예산안에 자유한국당은 “노는 공무원 왜 늘리냐”며 반대를 했었습니다. 그리고 2019년 4월에 속초 산불이 났을 때 문재인 정부는 전국의 소방차를 총 출동시키는 지시를 내렸었고 소방관들은 헌신적으로 산불을 진화하여 피해를 줄였다는 것도 우리는 잘 알고 있습니다. 물론 자유한국당 의원들이 국회에서 하라는 일은 안하고 반대를 위한 반대만 하며 놀고 있다는 것도 잘 알고 있고 말이지요. 




 아래는 조선일보 2020-01-29일 기사에 난 "한국당 "중국인 입국금지하고 중 관광객 돌려보내야"" 라는 제목의 기사입니다. 황교안 대표는 당대표 및 최고위원.중진의원 연석회의에서 “중국인 입국 금지 청원자가 삽시간에 50만명이나 돌파한 사실을 정부는 무겁게 받아들여야 한다. 문재인 정권의 고질적인 중국 눈치 보기에 국민 불신은 더욱 깊어진다”고 말했다고 합니다. 굉장히 무책임한 발언이고 중국인 혐오를 부채질하는 위험한 발언입니다. 


세계보건기구 WHO는 ‘국경 폐쇄나 여행·무역 제한 시 비공식적인 국경 이동을 유발해 오히려 감염병 확산 가능성을 높일 수 있다’고 지적하고 있습니다. 그리고 외교, 정치, 경제 등이 긴밀하게 연결되어 있는 한중 관계를 고려할 때 "중국인 입국금지와 중 관광객 돌려보내기" 결정은 결코 쉽게 내릴 수 있는 것이 아닙니다. 중국인 입국을 금지한 북한이야 검역을 할 역량도, 또 방역이 한번 뚫리면 대처할 능력도 없기 때문에 아주 예외적이며, 북한 이외에 현재까지는 중국인 입국금지 및 중국 관광객 돌려보내기를 결정한 국가는 없습니다. 

 이명박 정부 때 국내에서 신종플루 환자가 74만명에 달했고, 박근혜 정부 때 메르스 환자가 186명으로 세계 2위 메르스 발병국이었을 때 전세계 어느 나라도 한국 국민의 입국을 거부한 적이 없습니다. 그리고 한국의 메르스 환자를 중국 정부는 1억원을 들여 치료를 해주었고, 이에 대해 한국정부에 비용을 청구하지 않았었습니다. 




 그리고 저는 2015.08월에 "한-중, 한-미 무역량으로 본 김무성 새누리당 대표의 "중국보다 미국" 발언의 위험성"이라는 블로그 포스팅에서 이미 2003년도부터 한-중 무역량이 한-미, 한-일 무역량을 압지르고 현재는 배 이상의 차이로 한-중 무역량이 증가하였음을 언급한 적이 있습니다. 중국인 입국금지나 국내 체류중인 중국인 관광객의 본국 송환은 결코 쉽게 결정할 수 있는 문제가 아니며, 신종 코로나 바이러스 감염증의 확산 추세를 지켜보고 WHO의 권고도 들어보면서 외교, 정치, 경제도 종합적으로 고려해서 신중하게 결정해야할 사안입니다. 


 중국에 마스크 200만개 지원하는 방침에 대해서도 "공포에 휩싸인 자국민을 지원해야 할 정부가 이런 행동을 한다. 어느 나라 대통령이냐"고 조경태 최고위원이 말했다고 합니다. 어떻게 생각하시는지요? 저는 한국 정부가 이번에 지원하는 것이 중국 내 전염의 확산을 막는데 일조를 해서 최인접국인 우리나라에도 방역 측면에서 좋고, 인도적 차원에서도 의례 해야하는 일이고, 어려울 때 중국을 돕는게 양국 간 우호적인 관계를 이어가는데 도움이 된다고 보기에 문재인 정부가 아주 잘했다고 생각합니다. 



 

황교안 대표는 "청와대가 우한 폐렴 차단보다 반중 정서 차단에 급급한 건 아닌가, 지금 청와대가 우한 폐렴 명칭이나 고치고 있는데, 거기에 신경 쓸 만큼 여유로운 상황이 아니다"라고 지적했습니다. 그러면서 자유한국당은 "중국 우한 폐렴"이라는 명칭을 사용하고 있습니다. (* 왼쪽 사진출처: 오마이뉴스)

 세계보건기구(WHO)는 2015년 표준 지침을 통해 국가, 경제, 국민 등에 부정적 영향을 미칠 수 있기 때문에 지리적 위치, 사람 이름, 동물ㆍ식품 종류 등이 포함된 병명을 사용하지 말라고 권고한 바 있다. 이에 청와대는 세계보건기구(WHO)의 지침에 따라 WHO가 명명한 '신종 코로나 바이러스 감염증'이라는 병명을 사용하고 있습니다. 

 자유한국당이 세계보건기구(WHO)의 권고를 무시하고 중국인 혐오를 부추길 수 있는 "우한 폐렴"을 굳이 사용하는 것을 보면 공당으로서, 또 제1 야당으로서 사회적 책임이라는 걸 알고는 있는건지 의심이 듭니다. 이렇게 부추긴 중국인 혐오는 돌고 돌아 언젠가는 한국인 혐오로 돌아오는 날이 있을 겁니다. 그때 자유한국당은 뭐라 말하겠습니까? (이미 미국, 유럽에서는 아시아인에 대한 기피와 혐오 확산 중... -_-;)



 결정적으로 자유한국당은 이명박 정부 시절 신종플루 방역에 실패하여 74만명의 감염자가 나왔었고, 박근혜 정부 시절 '메르스 세계 2위 발병국'이라는 타이틀을 안겨준 여당이었습니다. 자유한국당이 여당으로 있던 이명박, 박근혜 정부 시절에 대한 부끄러움을 안다면 자중하시기 바랍니다. 




 아래의 중앙일보 2020.01.30일 기사 ""우린 국민이 아닌 것 같다"...진천, 아산 덮친 우한폐렴 공포" (이태윤 기자)는 매우 자국적인 제목 "우린 국민 아니라는 거?"과 함께 700여명의 반대시민들의 집회 사진을 실었습니다. 이 사진의 중앙에 플랭카드를 들고 있는 두 명중 오른쪽에 머리에 빨간 띠를 두리고 있는 분이 자유한국당 소속의 전 음성군수이고 이번 총선의 자유한국당 예비 후보라고 하네요. 



 저는 분명 진천, 아산 그리고 천안에도 역지사지의 심정으로 우한에서 전세기를 타고 귀국하는 우리 국민에게 기꺼이 격리 시설을 사용하도록 내주고 아무런 탈 없이 무사히 지내다가 나중에 집으로 귀가할 수 있기를 바라는 성숙한 시민들도 많이 계실 것이라고 생각합니다. 그런데 언론의 뉴스에는 위의 사진과 같이 자유한국당원을 중심으로 한 "우한 송환 교민 진천 격리수용 결사반대" 시위대는 모습만 자꾸 비추면서 "우린 국민이 아닌 것 같다"는 자극적인 제목까지 붙여서 보도를 하니 진천, 아산, 천안 시민들의 전체 여론인 양 갈등을 부추깁니다. 




  3. 신종 코로나 바이러스 전염병 대처는 과도한 불안감과 막연한 공포가 아니라 신뢰와 협조가 필요


"누가" 말하고 있는지를 보면 "무슨 의도"를 가지고 그 말을 그 시점에 하는지 짐작할 수 있습니다. 


자유한국당은 지난 박근혜 정부 때 메르스 대처를 잘못하는 바람에 국정 지지율이 29%까지 추락했던 적이 있습니다. 자유한국당은 이번 정부가 신종 코로나 바이러스 감염증 방역에 실패하여 국민들의 현 정부에 대한 원성이 높아져야 자기들이 오는 국회의원 선거에 유리하고 또 다음 대선에 유리하다고 보고 있을 것입니다. (국민들의 건강은 어찌되든지 간에요... -_-;)


언론사들은 클릭을 유도할 수 있는 자극적이고 선동적인 제목의 기사를 남발하고 있으며, 특히 조중동을 위시한 보수언론은 자유한국당과 같은 속셈으로 이번 정부가 신종 코로나 바이러스 감염증에 대해 무능하게 대처하고 있다는 프레임으로 국민들을 호도하고 있습니다. 그래야 조중동이 지지하는 자유한국당에게 선거에 유리할 것이기 때문입니다. 


자유한국당은 이번 신종 코로나 바이러스 감염증 비상사태를 정쟁에 사용하지 말길 바랍니다. 그리고 언론도 불안과 지역갈등을 부추기지 말고 언론 본연의 제 역할을 해주기 바랍니다. 


아래에 소개한 2020.01.30일에 있었던 '신종 코로나 바이러스 감염증 대응 종합점검회의'에서 문재인 대통령님의 모두 발언 내용처럼, "신종 코로나로부터 우리 자신을 지킬 수 있는 무기는 공포와 혐오가 아니라 신뢰와 협력"일 것입니다.


- 신종 코로나바이러스 감염증 대응 종합점검회의 시 문재인 대통령님의 모두발언 - 오늘 회의는 신종 코로나바이러스 감염증에 대한 대책들을 종합적으로 점검하고 논의하기 위해 소집했습니다. 시·도지사님들도 화상 연결로 참석했습니다. 감사합니다. 감염병의 확산을 막고, 민생경제에 미치는 부정적 영향을 최소화하기 위해 중앙정부와 지자체 간에 긴밀히 소통하면서 협력을 강화해 주시기 바랍니다. 오늘부터 중국 우한에 고립된 우리 교민 700여 명의 귀국이 시작됩니다. 실제 도착은 내일부터 하게 될 것으로 보입니다. 협조해 주신 항공사와 승무원들께 감사드립니다. 우리 국민이 어디에 있든 국민의 생명과 안전을 지키는 것은 국가의 당연한 책무입니다. 현재까지 현지 교민 가운데 감염증 확진자나 의심환자는 없는 것으로 파악되고 있습니다. 또한, 교민들은 중국 정부와의 협의에 따라 검역 후 증상이 없는 경우에만 임시항공편에 탑승하고, 귀국 후에는 일정 기간 외부와 격리된 별도의 시설에서 생활하며 검사받게 됩니다. 귀국 교민들의 안전은 물론, 완벽한 차단을 통해 지역사회의 감염을 예방하기 위한 조치입니다. 정부는 임시생활시설이 운영되는 지역 주민들의 불안을 이해합니다. 그에 대한 대책을 충분히 세우고 있고, 걱정하시지 않도록 정부가 빈틈없이 관리할 것입니다. 이해와 협조를 당부 드리며, 불안해하시지 않아도 된다는 것을 거듭 약속드립니다. 또한, 중국에 남게 되는 교민들에 대하서도 중국당국과 계속 협력해 나가겠습니다. 국민안전에는 타협이 있을 수 없습니다. 모든 상황에 대비해야 하고, 필요한 모든 조치를 다 취해야 합니다. 선제적 예방조치는 빠를수록 좋고, 과하다 싶을 만큼 강력해야 합니다. 정부와 지자체의 대응 역량을 최대한으로 끌어올려 2차 감염의 방지에 총력을 기울여야 할 것입니다. 이미 시행되고 있는 우한 지역 입국자 전수조사도 신속히 진행하고, 그 경과와 결과를 투명하게 알리기 바랍니다. 연락이 닿지 않는 분들은 자진하여 신고해 주실 것을 당부 드립니다. 증상이 있거나, 확진 환자와 접촉했던 분들에 대해서는 모니터링과 관리체계를 한층 더 강화할 필요가 있습니다. 지역의료기관의 진료와 신고체계 점검, 확산에 대비한 지역별 선별진료소와 격리병상 확충, 필요한 인력과 물품의 확보도 속도를 내주기 바랍니다. 중국 외에도 여러 나라에서 확진 환자가 발생하고 있기 때문에 바이러스 유입 경로가 다양해질 수 있습니다. 이 경우까지를 대비해 모든 공항과 항만에 대한 검역 강화 조치를 강구해 주기 바랍니다. 우리가 맞서야 할 것은 바이러스만이 아닙니다. 과도한 불안감, 막연한 공포와 단호하게 맞서야 합니다. 정부가 가장 정확한 정보를 가장 신속하게 제공할 수 있습니다. 국민의 일상생활이 위축되거나 불필요한 오해와 억측이 생기지 않도록 필요한 모든 정보를 투명하고, 신속하게, 국민의 시각에서 최대한 상세하게 공개하기 바랍니다. 특별히, 가짜뉴스에 대한 엄정한 대응을 강조합니다. 아무리 우수한 방역체계도 신뢰 없이는 작동하기 어렵습니다. 확산하는 신종 감염병에 맞서 범국가적 역량을 모아야 할 때 불신과 불안을 조장하는 가짜뉴스의 생산과 유포는 방역을 방해하고 국민의 안전을 저해하는 중대한 범죄행위입니다. 관계 부처는 표현의 자유를 넘는 가짜뉴스에 대해 각별한 경각심을 갖고 단호하게 대처해 주기 바랍니다. 언론의 역할도 중요합니다. 신종 코로나를 빠르게 극복할 수 있도록 힘을 모아주시기 바랍니다. 정치권도 이 문제에서만큼은 정쟁을 자제해 주시길 요청 드립니다. 한편으로, 우려되는 부분이 과도한 경제 심리 위축입니다. 불안감 때문에 정상적인 경제활동까지 영향을 받는 일이 없도록 해야 합니다. 경제부총리를 중심으로 모든 부처가 경제상황 관리에 만전을 기해 줄 것을 각별히 당부 드립니다. 국내외 금융시장 불안, 수출·투자·소비 등 우리 경제에 미치는 영향에 대한 종합적인 점검과 대책이 필요합니다. 지역경제와 관광·숙박 등 서비스업종의 어려움도 커질 수 있습니다. 지자체와 함께 지역별·업종별 파급효과를 세밀히 살펴보고, 행정적․재정적 지원이 필요한 부분에 대해서는 최대한 신속하고, 충분한 규모의 지원 대책을 마련해주기 바랍니다. 한편, 중국 내 신종 코로나 상황이 진정될 때까지 현지 진출 우리 기업들의 어려움도 커질 것으로 예상됩니다. 관계기관과 현지 기업, 경제단체들 간 소통 채널을 만들고, 피해가 최소화될 수 있도록 적극 지원해 주기 바랍니다. 국민 여러분께도 당부 드리겠습니다. 신종 코로나로부터 우리 자신을 지킬 수 있는 무기는 공포와 혐오가 아니라 신뢰와 협력입니다. 우리는 세계 최고 수준의 방역 역량을 가지고 있습니다. 과거의 사례에서 축적된 경험도 있습니다. 또한, 정부가 최선을 다하고 있습니다. 국민들과 지역사회가 협력해 주신다면 충분히 극복해낼 수 있습니다. 정부는 자자체와 함께 정부의 일을 철저히 하고, 국민 개개인은 예방 행동수칙을 철저히 지킨다면 우리는 신종 코로나 피해를 최소화하면서 넘어설 수 있습니다. 우리 국민의 성숙한 역량을 믿고 정부도 더욱 최선을 다하겠습니다.


* 영상보기: https://youtu.be/MG9bh_GMdks



 마지막으로, 공항과 출입거점에서 현장 검역에 구슬 땀을 흘리고 계신 검역 종사자 분들, 역학조사 하시는 분들, 1339번 상담해주시는 분들, 병원과 격리시설에서 환자를 치료하고 격리된 교민들을 진찰하실 의료진 분들, 소독과 방역을 담당하신 분들, 우한의 교민들을 한국으로 데려오는 전세기에 자원해주신 대한항공 승무원분들, 중국 내 교민 지원해주시는 재외공관/외교관 분들, 정부 대응을 총괄하는 질병관리본부 담당자 분들, 그리고 신종 코로나 바이러스의 RNA 분석을 통해 발병원인과 치료/백신을 연구하고 성과를 공유하며 어느때보다도 발빠르게 움직이고 있는 전세계의 Bioinformatics 연구 전문가들에게 감사의 말씀을 전합니다. 정말 고맙습니다. 당신들이 박수 받아야 할 진정한 영웅들입니다. 

728x90
반응형
Posted by Rfriend
,

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




728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 R을 사용하여 예측이나 분류 모델링을 할 때 기본적으로 필요한 두가지 작업인 


(1) DataFrame을 Train set, Test set 으로 분할하기 

     (Split a DataFrame into Train and Test set)

   - (1-1) 무작위 샘플링에 의한 Train, Test set 분할 

             (Split of Train, Test set by Random Sampling)

   - (1-2) 순차 샘플링에 의한 Train, Test set 분할 

             (Split of Train, Test set by Sequential Sampling)

   - (1-3) 층화 무작위 샘플링에 의한 Train, Test set 분할 

             (Split of Train, Test set by Stratified Random Sampling)


(2) 여러개의 숫자형 변수를 가진 DataFrame을 표준화하기 

      (Standardization of Numeric Data)

   - (2-1) z-변환 (z-transformation, standardization)

   - (2-2) [0-1] 변환 ([0-1] transformation, normalization)


(3) 여러개의 범주형 변수를 가진 DataFrame에서 가변수 만들기 

      (Getting Dummy Variables)


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



예제로 사용할 Cars93 DataFrame을 MASS 패키지로 부터 불러오겠습니다. 변수가 무척 많으므로 예제를 간단하게 하기 위해 설명변수 X로 'Price', 'Horsepower', 'RPM', 'Length', 'Type', 'Origin' 만을 subset 하여 가져오고, 반응변수 y 로는 'MPG.highway' 변수를 사용하겠습니다. 



# get Cars93 DataFrame from MASS package

library(MASS)

data(Cars93)

str(Cars93)

'data.frame': 93 obs. of 27 variables: $ Manufacturer : Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 4 4... $ Model : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1... $ Type : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3... $ Min.Price : num 12.9 29.2 25.9 30.8 23.7 14.2 19.9 22.6 26.3 33 ... $ Price : num 15.9 33.9 29.1 37.7 30 15.7 20.8 23.7 26.3 34.7 ... $ Max.Price : num 18.8 38.7 32.3 44.6 36.2 17.3 21.7 24.9 26.3... $ MPG.city : int 25 18 20 19 22 22 19 16 19 16 ... $ MPG.highway : int 31 25 26 26 30 31 28 25 27 25 ... $ AirBags : Factor w/ 3 levels "Driver & Passenger",..: 3 1 2 1 2... $ DriveTrain : Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 3... $ Cylinders : Factor w/ 6 levels "3","4","5","6",..: 2 4 4 4 2 2 4... $ EngineSize : num 1.8 3.2 2.8 2.8 3.5 2.2 3.8 5.7 3.8 4.9 ... $ Horsepower : int 140 200 172 172 208 110 170 180 170 200 ... $ RPM : int 6300 5500 5500 5500 5700 5200 4800 4000 4800... $ Rev.per.mile : int 2890 2335 2280 2535 2545 2565 1570 1320 1690... $ Man.trans.avail : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 1 1 1 1... $ Fuel.tank.capacity: num 13.2 18 16.9 21.1 21.1 16.4 18 23 18.8 18 ... $ Passengers : int 5 5 5 6 4 6 6 6 5 6 ... $ Length : int 177 195 180 193 186 189 200 216 198 206 ... $ Wheelbase : int 102 115 102 106 109 105 111 116 108 114 ... $ Width : int 68 71 67 70 69 69 74 78 73 73 ... $ Turn.circle : int 37 38 37 37 39 41 42 45 41 43 ... $ Rear.seat.room : num 26.5 30 28 31 27 28 30.5 30.5 26.5 35 ... $ Luggage.room : int 11 15 14 17 13 16 17 21 14 18 ... $ Weight : int 2705 3560 3375 3405 3640 2880 3470 4105 3495... $ Origin : Factor w/ 2 levels "USA","non-USA": 2 2 2 2 2 1 1... 

$ Make : Factor w/ 93 levels "Acura Integra",..: 1 2 4 3 5 6 7 9 8 10 ...



X <- subset(Cars93, select=c('Price', 'Horsepower', 'RPM', 'Length', 'Type', 'Origin'))

head(X)

A data.frame: 6 × 6
PriceHorsepowerRPMLengthTypeOrigin
<dbl><int><int><int><fct><fct>
15.91406300177Smallnon-USA
33.92005500195Midsizenon-USA
29.11725500180Compactnon-USA
37.71725500193Midsizenon-USA
30.02085700186Midsizenon-USA
15.71105200189MidsizeUSA



table(X$Origin)

USA non-USA 48 45



y <- Cars93$MPG.highway

y

  1. 31
  2.  
  3. 25
  4.  
  5. 26
  6.  
  7. 26
  8.  
  9. 30
  10.  
  11. 31
  12.  
  13. 28
  14.  
  15. 25
  16.  
  17. 27
  18.  
  19. 25
  20.  
  21. 25
  22.  
  23. 36
  24.  
  25. 34
  26.  
  27. 28
  28.  
  29. 29
  30.  
  31. 23
  32.  
  33. 20
  34.  
  35. 26
  36.  
  37. 25
  38.  
  39. 28
  40.  
  41. 28
  42.  
  43. 26
  44.  
  45. 33
  46.  
  47. 29
  48.  
  49. 27
  50.  
  51. 21
  52.  
  53. 27
  54.  
  55. 24
  56.  
  57. 33
  58.  
  59. 28
  60.  
  61. 33
  62.  
  63. 30
  64.  
  65. 27
  66.  
  67. 29
  68.  
  69. 30
  70.  
  71. 20
  72.  
  73. 30
  74.  
  75. 26
  76.  
  77. 50
  78.  
  79. 36
  80.  
  81. 31
  82.  
  83. 46
  84.  
  85. 31
  86.  
  87. 33
  88.  
  89. 29
  90.  
  91. 34
  92.  
  93. 27
  94.  
  95. 22
  96.  
  97. 24
  98.  
  99. 23
  100.  
  101. 26
  102.  
  103. 26
  104.  
  105. 37
  106.  
  107. 36
  108.  
  109. 34
  110.  
  111. 24
  112.  
  113. 25
  114.  
  115. 29
  116.  
  117. 25
  118.  
  119. 26
  120.  
  121. 26
  122.  
  123. 33
  124.  
  125. 24
  126.  
  127. 33
  128.  
  129. 30
  130.  
  131. 23
  132.  
  133. 26
  134.  
  135. 31
  136.  
  137. 31
  138.  
  139. 23
  140.  
  141. 28
  142.  
  143. 30
  144.  
  145. 41
  146.  
  147. 31
  148.  
  149. 28
  150.  
  151. 27
  152.  
  153. 28
  154.  
  155. 26
  156.  
  157. 38
  158.  
  159. 37
  160.  
  161. 30
  162.  
  163. 30
  164.  
  165. 43
  166.  
  167. 37
  168.  
  169. 32
  170.  
  171. 29
  172.  
  173. 22
  174.  
  175. 33
  176.  
  177. 21
  178.  
  179. 30
  180.  
  181. 25
  182.  
  183. 28
  184.  
  185. 28




  (1) DataFrame을 Train set, Test set 으로 분할하기 (Split a DataFrame into Train and Test set)


(1-1) 무작위 샘플링에 의한 Train, Test set 분할 (Split of Train, Test set by Random Sampling)


간단하게 일회성으로 무작위 샘플링 하는 것이면 sample() 함수로 난수를 생성해서 indexing을 해오면 됩니다. 

(* 참고 : https://rfriend.tistory.com/58)



# (1) index for splitting data into Train and Test set

set.seed(1004) # for reprodicibility

train_idx <- sample(1:nrow(X), size=0.8*nrow(X), replace=F) # train-set 0.8, test-set 0.2

test_idx <- (-train_idx)


X_train <- X[train_idx,]

y_train <- y[train_idx]

X_test <- X[test_idx,]

y_test <- y[test_idx]


print(paste0('X_train: ', nrow(X_train)))

print(paste0('y_train: ', length(y_train)))

print(paste0('X_test: ', nrow(X_test)))

print(paste0('y_test: ', length(y_test)))

[Out]:

[1] "X_train: 74" [1] "y_train: 74" [1] "X_test: 19" [1] "y_test: 19"





(1-2) 순차 샘플링에 의한 Train, Test set 분할 (Split of Train, Test set by Sequential Sampling)


시계열 분석을 할 경우 시간 순서(timestamp order)를 유지하는 것이 필요하므로 (1-1)의 무작위 샘플링을 하면 안되며, 시간 순서를 유지한 상태에서 앞서 발생한 시간 구간을 training set, 뒤의(미래의) 시간 구간을 test set 으로 분할합니다. 



# sequential sampling

test_size <- 0.2

test_num <- ceiling(nrow(X) * test_size)

train_num <- nrow(X) - test_num


X_train <- X[1:train_num,]

X_test <- X[(train_num+1):nrow(X),]

y_train <- y[1:train_num]

y_test <- y[(train_num+1):length(y)]





(1-3)  층화 무작위 샘플링에 의한 Train, Test set 분할 (Split of Train, Test set by Stratified Random Sampling)


위의 (1-1)과 (1-2)에서 소개한 무작위 샘플링, 순차 샘플링을 사용한 train, test set split 을 random_split() 이라는 사용자 정의함수(user-defined function)으로 정의하였으며, 층화 무작위 샘플링(stratified random sampling)을 사용한 train_test_split() 사용자 정의 함수도 이어서 정의해 보았습니다. (python sklearn의 train_test_split() 함수의 인자, 반환값이 유사하도록  정의해보았습니다) (* 참고 : https://rfriend.tistory.com/58)



# --- user-defined function of train_test split with random sampling

random_split <- function(X, y

                         , test_size

                         , shuffle

                         , random_state) {

    

    test_num <- ceiling(nrow(X) * test_size)

    train_num <- nrow(X) - test_num

    

    if (shuffle == TRUE) {

        # shuffle == True

        set.seed(random_state) # for reprodicibility

        test_idx <- sample(1:nrow(X), size=test_num, replace=F)

        train_idx <- (-test_idx)

            

        X_train <- X[train_idx,]

        X_test <- X[test_idx,]

        y_train <- y[train_idx]

        y_test <- y[test_idx]

    } else {

        # shuffle == False

        X_train <- X[1:train_num,]

        X_test <- X[(train_num+1):nrow(X),]

        y_train <- y[1:train_num]

        y_test <- y[(train_num+1):length(y)]

    }

    

    return (list(X_train, X_test, y_train, y_test))

}



# --- user defined function of train_test_split() with statified random sampling

train_test_split <- function(X, y

                             , test_size=0.2

                             , shuffle=TRUE

                             , random_state=2004

                             , stratify=FALSE, strat_col=NULL){

                        

    if (stratify == FALSE){ # simple random sampling

        split <- random_split(X, y, test_size, shuffle, random_state)

        X_train <- split[1]

        X_test  <- split[2]

        y_train <- split[3]

        y_test  <- split[4]

    } else { # --- stratified random sampling

        strata <- unique(as.character(X[,strat_col]))

        X_train <- data.frame()

        X_test  <- data.frame()

        y_train <- vector()

        y_test  <- vector()

        for (stratum in strata){

            X_stratum <- X[X[strat_col] == stratum, ]

            y_stratum <- y[X[strat_col] == stratum]

            split_stratum <- random_split(X_stratum, y_stratum, test_size, shuffle, random_state)

            X_train <- rbind(X_train, data.frame(split_stratum[1]))

            X_test  <- rbind(X_test,  data.frame(split_stratum[2]))

            y_train <- c(y_train, unlist(split_stratum[3]))

            y_test  <- c(y_test,  unlist(split_stratum[4]))

        }

    }

    return (list(X_train, X_test, y_train, y_test))

}

 



위에서 정의한 train_test_splie() 사용자 정의 함수를 사용하여 'Origin' ('USA', 'non-USA' 의 두 개 수준을 가진 요인형 변수) 변수를 사용하여 층화 무작위 샘플링을 통한 train, test set 분할 (split of train and test set using stratified random sampling in R) 을 해보겠습니다, 



split_list <- train_test_split(X, y

                               , test_size=0.2

                               , shuffle=TRUE

                               , random_state=2004

                               , stratify=TRUE, strat_col='Origin')


X_train <- data.frame(split_list[1])

X_test  <- data.frame(split_list[2])

y_train <- unlist(split_list[3])

y_test  <- unlist(split_list[4])



print(paste0('Dim of X_train: ', nrow(X_train), ', ', ncol(X_train)))

print(paste0('Dim of X_test:  ', nrow(X_test), ', ', ncol(X_test)))

print(paste0('Length of y_train: ', length(y_train)))

print(paste0('Length of y_test:  ', length(y_test)))

[Out]:
[1] "Dim of X_train: 74, 6"
[1] "Dim of X_test:  19, 6"
[1] "Length of y_train: 74"
[1] "Length of y_test:  19"



X_test

A data.frame: 19 × 6
PriceHorsepowerRPMLengthTypeOrigin
<dbl><int><int><int><fct><fct>
448.0815500168Smallnon-USA
233.92005500195Midsizenon-USA
398.4555700151Smallnon-USA
4012.5905400164Sportynon-USA
329.11725500180Compactnon-USA
538.3825000164Smallnon-USA
4510.01246000172Smallnon-USA
9020.01345800180Compactnon-USA
4212.11025900173Smallnon-USA
1616.31704800178VanUSA
720.81704800200LargeUSA
1140.12956000204MidsizeUSA
739.0745600177SmallUSA
1213.41105200182CompactUSA
823.71804000216LargeUSA
239.2926000174SmallUSA
1716.61654000194VanUSA
7411.11105200181CompactUSA
1415.11604600193SportyUSA


table(X$Origin)

[Out]: USA non-USA 48 45



table(X_test$Origin)

[Out]: USA non-USA 10 9


y_test

  1. [Out]: 33
  2.  
  3. 25
  4.  
  5. 50
  6.  
  7. 36
  8.  
  9. 26
  10.  
  11. 37
  12.  
  13. 29
  14.  
  15. 30
  16.  
  17. 46
  18.  
  19. 23
  20.  
  21. 28
  22.  
  23. 25
  24.  
  25. 41
  26.  
  27. 36
  28.  
  29. 25
  30.  
  31. 33
  32.  
  33. 20
  34.  
  35. 31
  36.  
  37. 28





참고로 (1-1) 무작위 샘플링에 의한 Train, Test set 분할을 위의 (1-3)에서 정의한 train_test_split() 사용자 정의 함수를 사용해서 하면 아래와 같습니다. (shuffle=TRUE)



# split of train, test set by random sampling using train_test_split() function

split_list <- train_test_split(X, y

                               , test_size=0.2

                               , shuffle=TRUE

                               , random_state=2004

                               , stratify=FALSE)


X_train <- data.frame(split_list[1])

X_test  <- data.frame(split_list[2])

y_train <- unlist(split_list[3])

y_test  <- unlist(split_list[4])




참고로 (1-2) 순차 샘플링에 의한 Train, Test set 분할을 위의 (1-3)에서 정의한 train_test_split() 사용자 정의 함수를 사용해서 하면 아래와 같습니다. (shuffle=FALSE)



# split of train, test set by sequential sampling using train_test_split() function

split_list <- train_test_split(X, y

                               , test_size=0.2

                               , shuffle=FALSE

                               , random_state=2004

                               , stratify=FALSE)


X_train <- data.frame(split_list[1])

X_test  <- data.frame(split_list[2])

y_train <- unlist(split_list[3])

y_test  <- unlist(split_list[4])

 




  (2) 여러개의 숫자형 변수를 가진 DataFrame을 표준화하기 (Standardization of Nuemric Data)


(2-1) z-변환 (z-transformation, standardization)


X_train, X_test 데이터셋에서 숫자형 변수(numeric variable)와 범주형 변수(categorical varialble)를 구분한 후에, 숫자형 변수로 이루어진 DataFrame 에 대해서 z-표준화 변환 (z-standardization transformation)을 해보겠습니다. (* 참고 : https://rfriend.tistory.com/52)


여러개의 변수를 가진 DataFrame이므로 X_mean <- apply(X_train_num, 2, mean) 로 Train set의 각 숫자형 변수별 평균을 구하고, X_stddev <- apply(X_train_num, 2, sd) 로 Train set의 각 숫자형 변수별 표준편차를 구했습니다. 


그리고 scale(X_train_num, center=X_mean, scale=X_stddev) 로 Train set의 각 숫자형 변수를 z-표준화 변환을 하였으며, scale(X_test_num, center=X_mean, scale=X_stddev) 로 Test set의 각 숫자형 변수를 z-표준화 변환을 하였습니다. 


이때 조심해야 할 것이 있는데요, z-표준화 변환 시 사용하는 평균(mean)과 표준편차(standard deviation)는 Train set으로 부터 구해서 --> Train set, Test set 에 적용해서 z-표준화를 한다는 점입니다. 왜냐하면 Test set는 미래 데이터(future data), 볼 수 없는 데이터(unseen data) 이므로, 우리가 알 수 있는 집단의 평균과 표준편차는 Train set으로 부터만 얻을 수 있기 때문입니다.  (많은 분석가가 그냥 Train, Test set 구분하기 전에 통채로 scale() 함수 사용해서 표준화를 한 후에 Train, Test set으로 분할을 하는데요, 이는 엄밀하게 말하면 잘못된 순서입니다)



# split numeric, categorical variables

X_train_num <- X_train[, c('Price', 'Horsepower', 'RPM', 'Length')]

X_train_cat <- X_train[, c('Type', 'Origin')]

X_test_num  <- X_test[ , c('Price', 'Horsepower', 'RPM', 'Length')]

X_test_cat  <- X_test[ , c('Type', 'Origin')]


# (1) Z Standardization

# (1-1) using scale() function

X_mean   <- apply(X_train_num, 2, mean)

X_stddev <- apply(X_train_num, 2, sd)


print('---- Mean ----')

print(X_mean)

print('---- Standard Deviation ----')

print(X_stddev)

[Out]:
[1] "---- Mean ----"
     Price Horsepower        RPM     Length 
  20.22703  146.08108 5278.37838  183.67568 
[1] "---- Standard Deviation ----"
     Price Horsepower        RPM     Length 
  9.697073  51.171149 594.730345  14.356620 



X_train_scaled <- scale(X_train_num, center=X_mean, scale = X_stddev)

head(X_train_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
1-0.44621989-0.11883811.7177896-0.46498935
41.801881070.50651430.37264220.64947906
51.007827061.21003570.70892910.16189913
41-0.044036690.27200720.8770725-0.60429791
43-0.28122166-0.11883810.54078560.09224485
46-1.05465089-1.05686670.4567139-1.23118639


# note that 'mean' and 'stddev' are calculated using X_train_num dataset (NOT using X_test_num)

X_test_scaled <- scale(X_test_num, center=X_mean, scale = X_stddev)

head(X_test_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
44-1.2608987-1.27183150.3726422-1.0918778
21.41001031.05369760.37264220.7887876
39-1.2196491-1.77993030.7089291-2.2760005
40-0.7968411-1.09595120.2044988-1.3704949
30.91501560.50651430.3726422-0.2560265
53-1.2299615-1.2522893-0.4680750-1.3704949



# combine X_train_scaled, X_train_cat

X_train_scaled <- cbind(X_train_num_scaled, X_train_cat)


# combine X_trest_scaled, X_test_cat

X_test_scaled <- cbind(X_test_num_scaled, X_test_cat)





(2-2) [0-1] 변환 ([0-1] transformation, normalization)


각 숫자형 변수별 최소값(min)과 최대값(max)을 구해서 [0-1] 사이의 값으로 변환해보겠습니다. 

(* 참고 : https://rfriend.tistory.com/52)



# (2) [0-1] Normalization

# 0-1 transformation

X_max <- apply(X_train_num, 2, max)

X_min <- apply(X_train_num, 2, min)

X_train_num_scaled <- scale(X_train_num, center = X_min, scale = (X_max - X_min))

X_test_num_scaled <- scale(X_test_num, center = X_min, scale = (X_max - X_min))


head(X_train_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
10.155963300.32489450.92592590.4615385
40.555963300.45991560.62962960.6666667
50.414678900.61181430.70370370.5769231
410.227522940.40928270.74074070.4358974
430.185321100.32489450.66666670.5641026
460.047706420.12236290.64814810.3205128



head(X_test_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
440.011009170.075949370.62962960.3461538
20.486238530.578059070.62962960.6923077
390.01834862-0.033755270.70370370.1282051
400.093577980.113924050.59259260.2948718
30.398165140.459915610.62962960.5000000
530.016513760.080168780.44444440.2948718


# combine X_train_scaled, X_train_cat

X_train_scaled <- cbind(X_train_num_scaled, X_train_cat)


# combine X_trest_scaled, X_test_cat

X_test_scaled <- cbind(X_test_num_scaled, X_test_cat)





 (3) 여러개의 범주형 변수를 가진 DataFrame에서 가변수 만들기 (Getting Dummy Variables) 


(3-1) caret 패키지의 dummyVars() 함수를 이용하여 DataFrame 내 범주형 변수로부터 가변수 만들기



library(caret)


# fit dummyVars()

dummy <- dummyVars(~ ., data = X_train_cat, fullRank = TRUE)


# predict (transform) dummy variables

X_train_cat_dummy <- predict(dummy, X_train_cat)

X_test_cat_dummy <- predict(dummy, X_test_cat)


head(X_train_cat_dummy)

A matrix: 6 × 6 of type dbl
Type.LargeType.MidsizeType.SmallType.SportyType.VanOrigin.non-USA
001001
010001
000001
010001
010001
010000


head(X_test_cat_dummy)

A matrix: 6 × 6 of type dbl
Type.LargeType.MidsizeType.SmallType.SportyType.VanOrigin.non-USA
75000100
76010000
77100000
78000001
79001000
80001001





(3-2) 조건문 ifelse() 함수를 이용하여 수작업으로 가변수 만들기 

        (creating dummy variables manually using ifelse())


아무래도 (3-1)의 caret 패키지를 이용하는 것 대비 수작업으로 할 경우 범주형 변수의 개수와 범주형 변수 내 class 의 종류 수가 늘어날 수록 코딩을 해야하는 수고가 기하급수적으로 늘어납니다. 그리고 범주형 변수나 class가 가변적인 경우 데이터 전처리 workflow를 자동화하는데 있어서도 수작업의 하드코딩의 경우 에러를 야기하는 문제가 되거나 추가적인 비용이 될 수 있다는 단점이 있습니다. 


범주형 변수 내 범주(category) 혹은 계급(class)이 k 개가 있으면 --> 가변수는 앞에서 부터 k-1 개 까지만 만들었습니다. (회귀모형의 경우 dummy trap 을 피하기 위해)



# check level (class) of categorical variables

unique(X_train_cat$Type)

  1. [Out]: Small
  2.  
  3. Midsize
  4.  
  5. Compact
  6.  
  7. Large
  8.  
  9. Sporty
  10.  
  11. Van

unique(X_train_cat$Origin)

  1. [Out]: non-USA
  2.  
  3. USA


# get dummy variables from train set

X_train_cat_dummy <- data.frame(

    type_small = ifelse(X_train_cat$Type == "Small", 1, 0)

    , type_midsize = ifelse(X_train_cat$Type == "Midsize", 1, 0)

    , type_compact = ifelse(X_train_cat$Type == "Compact", 1, 0)

    , type_large = ifelse(X_train_cat$Type == "Large", 1, 0)

    , type_sporty = ifelse(X_train_cat$Type == "Sporty", 1, 0)

    , origin_nonusa = ifelse(X_train_cat$Origin == "non-USA", 1, 0)

)


head(X_train_cat_dummy)

A data.frame: 6 × 6
type_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl>
100001
010001
001001
010001
010001
010000


# get dummy variables from test set

X_test_cat_dummy <- data.frame(

    type_small = ifelse(X_test_cat$Type == "Small", 1, 0)

    , type_midsize = ifelse(X_test_cat$Type == "Midsize", 1, 0)

    , type_compact = ifelse(X_test_cat$Type == "Compact", 1, 0)

    , type_large = ifelse(X_test_cat$Type == "Large", 1, 0)

    , type_sporty = ifelse(X_test_cat$Type == "Sporty", 1, 0)

    , origin_nonusa = ifelse(X_test_cat$Origin == "non-USA", 1, 0)

)


head(X_test_cat_dummy)

A data.frame: 6 × 6
type_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl>
000010
010000
000100
001001
100000
100001





  (4) 숫자형 변수와 범주형 변수 전처리한 데이터셋을 합쳐서 Train, Test set 완성하기



# combine X_train_scaled, X_train_cat

X_train_preprocessed <- cbind(X_train_num_scaled, X_train_cat_dummy)

head(X_train_preprocessed)

A data.frame: 6 × 10
PriceHorsepowerRPMLengthtype_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
0.15596330.34693880.92592590.4615385100001
0.48623850.59183670.62962960.6923077010001
0.39816510.47755100.62962960.5000000001001
0.55596330.47755100.62962960.6666667010001
0.41467890.62448980.70370370.5769231010001
0.15229360.22448980.51851850.6153846010000


 

# combine X_trest_scaled, X_test_cat

X_test_preprocessed <- cbind(X_test_num_scaled, X_test_cat_dummy)

head(X_test_preprocessed)

A data.frame: 6 × 10
PriceHorsepowerRPMLengthtype_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
750.188990830.428571430.29629630.70512821000010
760.203669720.591836730.44444440.69230769010000
770.311926610.469387760.37037040.46153846000100
780.390825690.346938780.81481480.55128205001001
790.067889910.122448980.44444440.44871795100000
800.018348620.073469390.66666670.06410256100001




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

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



728x90
반응형
Posted by 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()


 



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

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



728x90
반응형
Posted by 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)




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

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



728x90
반응형
Posted by Rfriend
,

이 포스팅을 쓰고 있는 오늘은 2020년 1월 5일입니다. 2019년 9월 6일 조국 전 법무부 장관에 대한 인사청문회가 있던 날 밤에 검찰이 그 당시 후보자의 배우자에 대해 전격 기소를 한지 딱 4달째가 되는 날입니다. 


모든 기업/기관이 한해를 마무리하고 새해를 맞이하기 전에 한 해 업무에 대한 성과 평가 (투입한 인력/시간 등의 비용 대비 성과, 산출물)를 합니다. 그리고 성과 평가 결과에 기초해서 조직개편/ 인사발령을 진행합니다. 


조국 전 장관 인사청문회로부터 4개월째 되는 오늘, 조국 전 법무부 장관에 대한 검찰 수사에는 얼마나 많은 비용이 들었을까 하는 의문이 드는 시점입니다. 수사 결과는 재판을 통해서 법원에서 판결로서 차차 밝혀질 터이니 기다려보기로 하구요, 이번 포스팅에서는 비용 부분에 대해서만 추정을 해보았습니다. 



검찰 수사에 드는 비용으로 (1) 인건비, (2) 운영/관리비, (3) 외주 영역비의 3개 주제 영역을 대상으로 하여 추정을 하였습니다. 


(1) 인건비를 추정하기 위해서 (1-1) 누가 (검사, 수사관), (1-2) 얼마나 오랜 기간 동안 수사에 참여했으며, (1-3) 단위 시간 당 임금은 얼마인지에 대해서 아래와 같이 가정을 하여 진행했습니다. 


- (1-1) 누가: 검색을 해봐도 정확한 인원수 나와있는 기사를 찾을 수가 없네요. 아래 중앙일보 기사를 보니 서울중앙지검 특수2부 외에 특수 1, 3, 4부와 형사부 및 강력부 소속 일부 검사도 투입되었다고 하니 최소 20명은 넘을 거 같습니다. 


검찰은 또 조 장관 관련 전담 수사팀인 기존의 서울중앙지검 특수2부 외에 특수1·3·4부와 형사부 및 강력부 소속 일부 검사도 투입했다. 수사팀 검사 숫자만 수십명에 달하는 것으로 전해졌다.

[출처: 중앙일보] '조국 수사' 윤석열 총력전…지방검사도 서울로 차출됐다, 2019.09.19

https://news.joins.com/article/23581397 


그리고 https://tip.daum.net/question/116636877 의 Q&A 답변 글에 보니 수사관이 100여명에 달한다는 의견도 있습니다. 암튼 검찰청에서 공식적으로 확인해준 투입 인력에 대한 내용이 현재는 없는 듯 하니 Case1, Case2, Case3으로 시나리오 기반으로 추정을 해봤습니다. 


(Case 1) 검찰청장 1명, 검사 20명, 수사관 50명

(Case 2) 검찰청장 1명, 검사 30명, 수사관 75명

(Case 3) 검찰청장 1명, 검사 40명, 수사관 100명



- (1-2) 얼마나 오랜 기간 동안: 2019-09-06 일 검찰의 조국 전 장관 배우자에 대한 기소가 있는 날을 기점으로 해서 이 글을 쓰는 오늘까지 4개월간 수사에 참여했다고 가정했습니다. 


수사 인력 중에는 처음부터가 아니라 중간에 증원 투입된 인력도 있을 텐데요, 2019-09-06일 이전에도 검찰에서 당시 조국 후보자를 수사했기에 2019-09-06일에 기소할 수 있었을 것이므로, 이 둘을 서로 상쇄해서 모든 인력이 다 2019-09-06일 부터 시작해서 4개월이라고 일단 가정하겠습니다. 



- (1-3) 단위 시간당 임금은 얼마: 검사와 수사관 별로 직급/연봉이 전부 다 다를텐고, 직급별로 몇 명이 수사에 투입되었는지 알 수 없는 관계로 대략의 월급 평균 추정치로 검찰청장 1,200만원, 검사 8백만원, 수사관 2백 50만원을 가정하였습니다. 


https://m.blog.naver.com/wintergalaxy/221265003251 의 내용에 따르면 검찰청장의 경우 2018년 기본급이 8,009,400 원이라고 하네요. 기본급 외에 수당 4백만원 추가해서 1,000만원이라고 가정했습니다. 검찰의 명운이 걸린 수사인 만큼 full time으로 수사 지휘를 했다고 가정했습니다. 


2018년도 초임 검사 4호봉(사법연수원 수료, 군필 기준)의 세전 월급 합계가 6,114,375원 (기본급 4,019,000원, 정근수당 미지급, 수사지도수당 100,000원, 관리업무수당 361,710원, 봉급조정수당 70,332원, 정액급식비 130,000원, 직급보조비 500,000원, 직무성과급 933,333원) 이라고 나오네요. 설마 조국 전 장관 수사에 초임 검사 위주로 투입했을거 같지는 않구요, 베테랑 검사들을 많이 포진시켰을 것으로 예상이 되고, 2019년 물가상승률 3~5% 반영한 연봉 인상도 고려해서 전체 평균 월급을 8백만원으로 가정하였습니다. 


수사관의 경우 http://blog.daum.net/_blog/BlogTypeView.do?blogid=0Xtsi&articleno=205 에 의하면 9호봉 수사관의 월 실수령액이 180~190만원 정도라고 나오네요(너무 적은거 같은데 이게 맞는지 모르겠습니다). 수사관들도 고참 수사관들을 많이 투입하고 야근에 주말 근무도 상당히 했을거라고 가정하고 수사관 인당 평균 월급을 250만원으로 가정했습니다. 


이렇게 가정하면 (1) 인건비의 추정액은 Case 1일 경우 약 11.8억원, Case 2일 경우 약 17.5억원, Case 3일 경우 23.2억원이 나옵니다. 



(2) 운영/관리비는 보통 일반 기업에서는 인건비의 1.5배~2배를 가정하는 거 같습니다. (저는 이 가정에 대한 학문적, 실증적 근거를 가지고 있지 않습니다. 사회 초년생 때 "네 연봉의 최소 3배는 벌어야지 회사는 손실 안본다"는 선배의 말이 기억나서요... -_-;)  일단은 운영/관리비를 인건비의 1.5배로 가정을 했습니다. 



(3) 외주용역비(Case 1)은 1억원, (Case 2)는 1.5억원, (Case 3)은 2억원을 가정했습니다.  PC, 핸드폰 포렌식이라든지 (사모펀드 등...) 금융 수사 관련해서 외부 전문업체에게 외주용역을 주었을 것이라고 가정했습니다. 



이렇게 (1) 인건비, (2) 운영/관리비, (3) 외주용역비에 대해서만 많은 가정을 두고 3개의 시나리오별로 비용을 추정을 해보았을 때 


  • Case 1) 인건비 11.8억원 + 운영/관리비 17.7억원 + 외주용역비 1억원
    = 총
    추정 비용 30.5억원

  • Case 2) 인건비 17.5억원 + 운영/관리비 26.3억원 + 외주용역비 1.5억원
    = 총 추정 비용 45.3억


  • Case 3) 인건비 23.2억원 + 운영/관리비 34.8억원 + 외주용역비 2억원
    = 총 추정 비용 60억원


국가 세금으로 월급 받으면서 국민에게 봉사해야 할 공무원인 검찰이 이렇게 큰 비용을 들여서 조국 전 법무부 장관을 수사했고, 해가 바뀌어서 2020년이 밝았는데도 아직도 계속 수사를 하고 있으니 비용은 계속 증가할 것입니다. 


이 나왔습니다. 아래는 엑셀로 계산한 표 화면캡쳐한 내용입니다. 





이 외에 특수 1/2/3/4부의 수십명 검사와 또 더 많은 수의 수사관을 조국 전 법무부 장관 수사에 올인함으로 인해서 다른 중대한 수사에 인력 부족으로 차질을 빚은 기회비용(opportunity cost)은 또 얼마나 클지 가늠이 안됩니다. 


조국 전 법무부 장관과 가족/친척에 대한 수사가 마무리 되면 법무부에서는 총 투입 인원 수, 기간, 비용에 대해서 정식으로 국민에게 브리핑을 해주면 좋겠습니다. 그리고 이만큼 투입할 만한 가치가 있었는지 평가를 해보는 자리가 마련되기를 바랍니다. 



2019년 6월 말부터 조국 전 장관에 대한 검찰의 잔인한 수사와 언론의 광기어린 보도로 인해 우리 국민이 지불해야 했던 사회적 비용도 엄청날 것입니다. 


가령, 서초동에 9월달 부터 매주 토요일마다 참여하고 있는 검찰개혁 촉구 집회에 참여하는 국민들의 비용만 보수적으로 추정해보면요, 참가 누적 인원 30만명 x 회당 참여 시간 4시간 (집회 참여 2시간 + 교통 왕복 2시간) x 1시간 당 비용 1만원 = 총 120 억원의 금액이 나옵니다. 


그리고 집회에는 참가하지 않았지만 대한민국 국민들이 조국 전 법무부 장관과 가족에 대한 뉴스 기사를 읽고, 기사에 달린 댓글도 읽고, SNS에 포스팅을 하기도 하고, ... 우리 사람에게 가장 희귀하고 비싼 자원인 "시간(time)"과 "관심(attention)" 까지 비용으로 환산하면 정말 어마어마할 것 같습니다.  



물론, 이 와중에 대다수의 가증스런 언론은 검증되지도 않은 수십만 건의 조국 전 장관 관련 기사를 남발하면서 '클릭 장사'로 특수를 누렸을 것 같습니다 (MBC는 제외요. 거듭난 MBC는 칭찬해주고 싶습니다! PD수첩 흥하세요~!). 그리고 광화문에서 열린 보수단체 집회에서는 참가한 노인들이 일당으로 몇 만원씩 받았다고 하니 그분들도 이참에 용돈 좀 벌었을 거 같습니다. 



조국 전 장관과 가족에 대한 재판이 하루 빨리 진행이 되고 사실관계가 조속히 밝혀지길 바랍니다.  공수처법이 작년 말에 통과되었고, 이제 검경수사권 조정안도 국회에서 반드시 통과되기를 기원합니다.  검찰은 이번 조국 전 장관과 가족에 대한 재판 결과가 나오면 이에 대한 책임을 지기 바랍니다. 특히 검찰청장과 수사에 참여한 검사장들은 특별히 책임을 지기 바랍니다. 


왕족시대도 아닌 21세기에 한 가족을 멸문지화에 이를 정도로 잔인하게 수사를 해서 다수의 국민이 '무소불위의 견제받지 않은 검찰을 가만 두면 안되겠구나' 하고 자각하게끔 해준 현 검찰 수뇌부의 공로는 인정해야 하는 아이러니, 참 슬프네요. 


견디기 힘든 고통을 감내해주신 조국 전 장관님과 그 가족에게 큰 빚을 졌기에 미안하고, 또 고맙습니다. 

728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 

 - 시계열의 구성 요인 (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)"이라고 하는 것입니다. 

 

 

[ 지수 분포의 확률밀도함수 곡선 ]

probability density function of exponential distribution

 

이전 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)

    : 

 

다음번 포스팅에서는 시계열 예측 모형의 적합도를 평가하는 지표(goodness-of-fit measures for time series models) (https://rfriend.tistory.com/667) 를 살펴보고, 이어서 Python 으로 단순 지수 평활법으로 시계열 자료를 예측하고 모델 성능을 비교평가하는 방법(https://rfriend.tistory.com/671)을 소개하겠습니다. 

 

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

이번 포스팅이 도움이 되었다면 아래의 '공감~

'를 꾹 눌러주세요. :-)

 

 

728x90
반응형
Posted by 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]:
  timeseries trend cycle trend_cycle seasonal irregular
2020-01-31 2.596119 0.0 0.000000 0.000000 0.000000 2.596119
2020-02-29 6.746160 1.1 1.332198 2.432198 3.565684 0.748278
2020-03-31 8.112100 2.2 2.640647 4.840647 6.136825 -2.865371
2020-04-30 8.255941 3.3 3.902021 7.202021 6.996279 -5.942358
2020-05-31 16.889655 4.4 5.093834 9.493834 5.904327 1.491495
2020-06-30 16.182357 5.5 6.194839 11.694839 3.165536 1.321981
2020-07-31 14.128087 6.6 7.185409 13.785409 -0.456187 0.798865
2020-08-31 11.943313 7.7 8.047886 15.747886 -3.950671 0.146099
2020-09-30 9.728095 8.8 8.766892 17.566892 -6.343231 -1.495567
2020-10-31 12.483489 9.9 9.329612 19.229612 -6.966533 0.220411
2020-11-30 12.141808 11.0 9.726013 20.726013 -5.646726 -2.937480
2020-12-31 15.143334 12.1 9.949029 22.049029 -2.751930 -4.153764
2021-01-31 21.774516 13.2 9.994684 23.194684 0.910435 -2.330604
2021-02-28 28.432892 14.3 9.862164 24.162164 4.318862 -0.048134
2021-03-31 32.350583 15.4 9.553832 24.953832 6.522669 0.874082
2021-04-30 30.596556 16.5 9.075184 25.575184 6.907169 -1.885797
2021-05-31 32.510523 17.6 8.434753 26.034753 5.365118 1.110653
2021-06-30 30.425519 18.7 7.643955 26.343955 2.326624 1.754939
2021-07-31 24.300958 19.8 6.716890 26.516890 -1.360813 -0.855119
2021-08-31 20.450917 20.9 5.670082 26.570082 -4.668691 -1.450475
2021-09-30 18.870881 22.0 4.522195 26.522195 -6.674375 -0.976939
2021-10-31 21.326310 23.1 3.293690 26.393690 -6.818438 1.751059
2021-11-30 22.902448 24.2 2.006469 26.206469 -5.060699 1.756678
2021-12-31 26.620578 25.3 0.683478 25.983478 -1.891426 2.528526
2022-01-31 27.626499 26.4 -0.651696 25.748304 1.805404 0.072791
2022-02-28 31.858923 27.5 -1.975253 25.524747 4.998670 1.335506
2022-03-31 35.930469 28.6 -3.263598 25.336402 6.797704 3.796363
2022-04-30 30.177870 29.7 -4.493762 25.206238 6.700718 -1.729087
2022-05-31 30.016165 30.8 -5.643816 25.156184 4.734764 0.125217
2022-06-30 26.591729 31.9 -6.693258 25.206742 1.448187 -0.063200
2022-07-31 21.118481 33.0 -7.623379 25.376621 -2.242320 -2.015820
2022-08-31 16.636031 34.1 -8.417599 25.682401 -5.307397 -3.738973
2022-09-30 17.682613 35.2 -9.061759 26.138241 -6.892132 -1.563496
2022-10-31 21.163298 36.3 -9.544375 26.755625 -6.554509 0.962182
2022-11-30 22.455672 37.4 -9.856844 27.543156 -4.388699 -0.698786
2022-12-31 26.919529 38.5 -9.993595 28.506405 -0.998790 -0.588086
2023-01-31 33.964623 39.6 -9.952191 29.647809 2.669702 1.647112
2023-02-28 37.459776 40.7 -9.733369 30.966631 5.593559 0.899586
2023-03-31 40.793766 41.8 -9.341031 32.458969 6.957257 1.377540
2023-04-30 43.838415 42.9 -8.782171 34.117829 6.380433 3.340153
2023-05-31 41.301780 44.0 -8.066751 35.933249 4.023975 1.344556
2023-06-30 39.217866 45.1 -7.207526 37.892474 0.545147 0.780245
2023-07-31 35.125502 46.2 -6.219813 39.980187 -3.085734 -1.768952
2023-08-31 33.841926 47.3 -5.121219 42.178781 -5.855940 -2.480916
2023-09-30 38.770511 48.4 -3.931329 44.468671 -6.992803 1.294643
2023-10-31 37.371216 49.5 -2.671356 46.828644 -6.179230 -3.278198
2023-11-30 46.587633 50.6 -1.363760 49.236240 -3.642142 0.993536
2023-12-31 46.403326 51.7 -0.031853 51.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()     # ground 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()        # ground 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()    # ground 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
timeseries trend cycle trend_cycle seasonal irregular
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
2.596119 0.0 0.000000 0.000000 0.000000 2.5961193
6.746160 1.1 1.332198 2.432198 3.565684 0.7482781
8.112100 2.2 2.640647 4.840647 6.136825 -2.8653712
8.255941 3.3 3.902021 7.202021 6.996279 -5.9423582
16.889655 4.4 5.093834 9.493834 5.904327 1.4914946
16.182357 5.5 6.194839 11.694839 3.165536 1.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))


 

 

 

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

이번 포스팅이 도움이 되었다면 아래의 '공감~

'를 꾹 눌러주세요. :-)

 

 

 

728x90
반응형
Posted by Rfriend
,

그동안 여러 포스팅에 나누어서 Python pandas 라이브러리에서 사용할 수 있는 시계열 데이터 처리 함수, 메소드, attributes 들에 대해서 소개했습니다. 


이번 포스팅에서는 시계열(Time series)의 4가지 구성 요인(추세 요인, 순환 요인, 계절 요인, 불규칙 요인)에 대해서 소개하겠습니다. 


시계열 구성요인 간의 결합 방식에 따라서 (1) 구성요인 간 독립적이라고 가정하여 각 구성요인을 더하는 가법 모형 (additive model)과, (2) 구성요인 간 독립적이지 않고 상호작용을 한다고 가정하여 구성요인 간 곱해주는 승법 모형 (multiplicative model)으로 구분할 수 있습니다. 


  • 시계열 가법 모형 (time series additive model) 
    = 추세 요인(trend factor) + 순환 요인(cycle factor) + 계절 요인(seasonal factor)
     + 불규칙 요인(irregular/random factor) 


  • 시계열 승법 모형 (time series multiplicative model)
    = 추세 요인 * 순환 요인 x 계절 요인 x 불규칙 요인

    :



이번 포스팅에서는 이해하기 쉬운 가법 모형(additive model)을 가상으로 만든 예제 데이터를 가지고 설명해보겠습니다. 



[ 시계열 구성 요인 (Time Series Component Factors) ]





시계열의 4가지 구성 요인인 추세 요인, 순환 요인, 계절 요인, 불규칙 요인을 차례대로 설명해보겠습니다.  

(* Reference: '한국의 경기순환 분석', 김혜원, 2004, http://kostat.go.kr/attach/journal/9-1-4.PDF)


(1) 추세 요인 (Trend factor) 은 인구의 변화, 자원의 변화, 자본재의 변화, 기술의 변화 등과 같은 요인들에 의해 영향을 받는 장기 변동 요인으로서, 급격한 충격이 없는 한 지속되는 특성이 있습니다. "10년 주기의 세계경제 변동 추세" 같은 것이 추세 요인의 예라고 할 수 있습니다. 


(2) 순환 요인 (Cycle factor) 은 경제활동의 팽창과 위축과 같이 불규칙적이며 반복적인 중기 변동요인을 말합니다. 주식투자가들이 "건설업/반도체업/조선업 순환주기"를 고려해서 투자한다고 말하는게 좋은 예입니다. 

만약 관측한 데이터셋이 10년 미만일 경우 추세 요인과 순환 요인을 구분하는 것이 매우 어렵습니다. 그래서 관측기간이 길지 않을 경우 추세와 순환 요인을 구분하지 않고 그냥 묶어서 추세 요인이라고 분석하기도 합니다. 


(3) 계절 요인 (Seasonal factor)  12개월(1년)의 주기를 가지고 반복되는 변화를 말하며, 계절의 변화, 공휴일의 반복, 추석 명절의 반복 등 과 같은 요인들에 의하여 발생합니다. 


(4) 불규칙 요인 (Irregular / Random factor, Noise) 은 일정한 규칙성을 인지할 수 없는 변동의 유형을 의미합니다. 천재지변, 전쟁, 질병 등과 같이 예 상할 수 없는 우연적 요인에 의해 발생되는 변동을 총칭합니다. 불규칙변동 은 경제활동에 미미한 영향을 미치기도 하지만 때로는 경제생활에 지대한 영향을 주기도 합니다. 



위의 설명에 대한 이해를 돕기 위하여 Python으로 위의 추세 요인, 순환 요인, 계절 요인, 불규칙 요인을 모두 더한 가법 모형의 시계열 자료(Yt = Tt + Ct + St + It)를 가상으로 만들어보겠습니다. 



import numpy as np

import pandas as pd


# DatetiemIndex

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, 

                   'seasonal': seasonal_factor, 

                   'irregular': irregular_factor},

                   index=dates)


df

[Out]:

timeseriestrendcycleseasonalirregular
2020-01-312.5961190.00.0000000.0000002.596119
2020-02-296.7461601.11.3321983.5656840.748278
2020-03-318.1121002.22.6406476.136825-2.865371
2020-04-308.2559413.33.9020216.996279-5.942358
2020-05-3116.8896554.45.0938345.9043271.491495
2020-06-3016.1823575.56.1948393.1655361.321981
2020-07-3114.1280876.67.185409-0.4561870.798865
2020-08-3111.9433137.78.047886-3.9506710.146099
2020-09-309.7280958.88.766892-6.343231-1.495567
2020-10-3112.4834899.99.329612-6.9665330.220411
2020-11-3012.14180811.09.726013-5.646726-2.937480
2020-12-3115.14333412.19.949029-2.751930-4.153764
2021-01-3121.77451613.29.9946840.910435-2.330604
2021-02-2828.43289214.39.8621644.318862-0.048134
2021-03-3132.35058315.49.5538326.5226690.874082
2021-04-3030.59655616.59.0751846.907169-1.885797
2021-05-3132.51052317.68.4347535.3651181.110653
2021-06-3030.42551918.77.6439552.3266241.754939
2021-07-3124.30095819.86.716890-1.360813-0.855119
2021-08-3120.45091720.95.670082-4.668691-1.450475
2021-09-3018.87088122.04.522195-6.674375-0.976939
2021-10-3121.32631023.13.293690-6.8184381.751059
2021-11-3022.90244824.22.006469-5.0606991.756678
2021-12-3126.62057825.30.683478-1.8914262.528526
2022-01-3127.62649926.4-0.6516961.8054040.072791
2022-02-2831.85892327.5-1.9752534.9986701.335506
2022-03-3135.93046928.6-3.2635986.7977043.796363
2022-04-3030.17787029.7-4.4937626.700718-1.729087
2022-05-3130.01616530.8-5.6438164.7347640.125217
2022-06-3026.59172931.9-6.6932581.448187-0.063200
2022-07-3121.11848133.0-7.623379-2.242320-2.015820
2022-08-3116.63603134.1-8.417599-5.307397-3.738973
2022-09-3017.68261335.2-9.061759-6.892132-1.563496
2022-10-3121.16329836.3-9.544375-6.5545090.962182
2022-11-3022.45567237.4-9.856844-4.388699-0.698786
2022-12-3126.91952938.5-9.993595-0.998790-0.588086
2023-01-3133.96462339.6-9.9521912.6697021.647112
2023-02-2837.45977640.7-9.7333695.5935590.899586
2023-03-3140.79376641.8-9.3410316.9572571.377540
2023-04-3043.83841542.9-8.7821716.3804333.340153
2023-05-3141.30178044.0-8.0667514.0239751.344556
2023-06-3039.21786645.1-7.2075260.5451470.780245
2023-07-3135.12550246.2-6.219813-3.085734-1.768952
2023-08-3133.84192647.3-5.121219-5.855940-2.480916
2023-09-3038.77051148.4-3.931329-6.9928031.294643
2023-10-3137.37121649.5-2.671356-6.179230-3.278198
2023-11-3046.58763350.6-1.363760-3.6421420.993536
2023-12-3146.40332651.7-0.031853-0.089186-5.175634



위에서 추세 요인(trend factor) + 순환 요인(cycle factor) + 계절 요인(seasonal factor) + 불규칙 요인(irregular factor, noise) 을 더해서 만든 시계열 가법 모형 (time series additive model) 자료를 아래에 시각화보았습니다. 


아래의 시도표 (time series plot)를 보면 '양(+)의 1차 선형 추세 (linear trend)', '1년 단위의 계절성(seasonality)', 그리고 불규칙한 잡음(noise)을 눈으로 확인할 수 있습니다. (기간이 4년으로서 길지 않다보니 추세와 순환 요인을 구분하기는 쉽지가 않지요?)



# Time series plot

import matplotlib.pyplot as plt


plt.figure(figsize=[10, 6])

df.timeseries.plot()

plt.title('Time Series (Additive Model)', fontsize=16)

plt.ylim(-12, 55)

plt.show()




(1) 위에서 가법 모형으로 가상의 시계열 자료를 만들 때 사용했던 '1차 선형 추세 요인 (trend factor)' 데이터를 시각화하면 아래와 같습니다. 



# -- Trend factor

#timestamp = np.arange(len(dates))

#trend_factor = timestamp*1.1


plt.figure(figsize=[10, 6])

df.trend.plot()

plt.title('Trend Factor', fontsize=16)

plt.ylim(-12, 55)

plt.show()



(2) 위의 가법 모형으로 가상의 시계열 자료를 만들 때 사용했던 '4년 주기의 순환 요인 (cycle factor) '자료를 시각화하면 아래와 같습니다. 



# -- Cycle factor

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


plt.figure(figsize=[10, 6])

df.cycle.plot()

plt.title('Cycle Factor', fontsize=16)

plt.ylim(-12, 55)

plt.show()



(3) 위에서 가법 모형으로 가상의 시계열 자료를 만들 때 사용했던 '1년 주기의 계절 요인 (seasonal factor)' 자료를 시각화하면 아래와 같습니다. 



# -- Seasonal factor

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


plt.figure(figsize=[10, 6])

df.seasonal.plot()

plt.title('Seasonal Factor', fontsize=16)

plt.ylim(-12, 55)

plt.show()




(4) 위에서 가법 모형으로 가상의 시계열 자료를 만들 때 사용했던 '불규칙 요인 (irregular factor)' 자료를 시각화하면 아래와 같습니다. 



# -- Irregular/ Random factor

#np.random.seed(2004)

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


plt.figure(figsize=[10, 6])

df.irregular.plot()

plt.title('Irregular Factor', fontsize=16)

plt.ylim(-12, 55)

plt.show()


 



추세 요인(trend factor), 순환 요인 (cycle factor), 계절 요인(seasonal factor), 불규칙 요인(irregular factor)와 이를 모두 합한 시계열 자료를 모두 모아서 하나의 그래프에 시각화하면 아래와 같습니다. 



# All in one: Time series = Trend factor + Cycle factor + Seasonal factor + Irregular factor


from pylab import rcParams

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

df.plot()

plt.ylim(-12, 55)

plt.show()

 



다음 포스팅에서는 위에서 만든 가상의 시계열 데이터를 Python과 R을 사용해서 시계열 구성요인 별로 분해(time series decomposition)를 해보겠습니다. (https://rfriend.tistory.com/509)


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

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



728x90
반응형
Posted by Rfriend
,