6-9주차는 머신러닝 팀프로젝트와 발표가 있었던 주간이었습니다. 저희 조는 <유투브 뷰티 마케팅 영상의 반응율 예측>이라는 주제로 프로젝트를 진행했고, 발표 이후 디벨롭 과정을 거쳐 <유튜브 마케팅 영상의 조회수 예측 - 뷰티 쇼츠 영상을 중심으로>라는 새로운 주제로 다가오는 7월 학회 발표를 준비하고 있습니다.
지난 개인 EDA 프로젝트 발표 이후 작성한 회고와 마찬가지로 이번 팀 프로젝트 회고 역시 잘한 점과 개선할 점을 중점적으로 작성해 볼텐데요. 개인 프로젝트와 조금 다르게 이번 포스팅에서는 팀 프로젝트를 진행하면서 어떤 문제상황이 있었는지, 그리고 부족했던 점을 어떻게 개선했는지를 중점적으로 적어보도록 하겠습니다.


 

1. 문제의식

저희 조는 유투브 뷰티 광고 영상의 메타데이터를 수집해서 조회수, 좋아요 수, 댓글 수로 구성된 '반응률'이라는 마케팅 지표를 예측하는 프로젝트를 진행했는데요. 뷰티 업계와 광고주에게 어필할 수 있는 실용적인 프로젝트라는 믿음은 있었지만, 프로젝트에 기승전결이 있는지, 주제와 결론이 부합하는지에 대해서는 프로젝트를 진행하는 내내 의문이 들었어요. 그러다 보니 발표 자료를 구성하면서 급히 짜맞추고 끼워맞추는 부분이 다수 발생했고, 프로젝트의 완성도가 성에 차지 않았습니다. 그 이유를 돌아보니 다음과 같습니다.
첫째, 프로젝트를 진행하는 내내 세부 주제를 계속해서 조금씩 틀고 변경하게 되면서 전체적인 흐름과 구성에 일관성이 없었습니다. 저희 팀은 막연히 '유투브 광고에 관련된 무언갈 하자'라는 생각으로 데이터를 수집하고 들여다 보면서 발표 직전에야 '뷰티 광고 영상'을 타겟으로 하자는 결론을 내릴 수 있었는데요. 로우 데이터를 분석하며 프로젝트 아이디어를 얻는 것이 때로는 좋은 방법이 될 수는 있겠지만, 제한된 시간 안에 기승전결을 도출해야 했던 이번 과제에 있어서는 다소 효율적이지 못한 접근 방법이었다고 생각합니다.
둘째, 어떠한 프로젝트를 진행할 때에는 기존에 어떤 선행 연구가 이루어졌는지에 대한 데이터 분석이 필수적으로 이루어져야 하는데, 저희 조는 시간에 쫓기면서 이 점을 간과했습니다. 그러다 보니 유투브의 뷰티 마케팅 영상에 대한 연구사례가 있는지 없는지조차 알지 못했고, 선행 연구에 비교했을 때 우리 프로젝트에는 어떤 차별점이 있는지조차 파악할 수 없었습니다. 내 프로젝트가 가지는 강점과 의의에 대해 스스로 설득이 되지 않으니 프로젝트 막바지로 갈수록 '이게 맞나?'라는 생각이 들 수밖에 없었습니다.
셋째, 소통의 부족으로 유기적인 팀플레이가 다소 부족했고 역할 분담이 효율적으로 이루어지지 못했습니다. 저희 팀원 모두 열의가 가득했고 개별 능력치도 월등히 좋았던지라 이 점이 더욱 아쉬웠어요. 모두 적극적으로 프로젝트에 참여했고, 열린 마음으로 소통을 하려고 했지만, 저를 포함한 팀원 모두가 데이터 기반의 소통을 해본 경험이 부족했던 것이 원인이라고 생각하고 있습니다. 


2. 개선방향

프로젝트를 진행하면서 겪은 시행착오를 통해 얻은 값진 교훈을 토대로, 저는 저의 팀 프로젝트 결과물을 다음과 같이 디벨롭하고 있습니다.

  1. 로우 데이터를 분석하면서 뷰티 카테고리의 영상 데이터 분포도가 운동, 패션, 테크, 반려동물, 등의 다른 카테고리와 다양한 차별점이 있는 점을 발견했는데요, 특히 타 카테고리와 달리 뷰티 카테고리의 광고 영상은 평균 2.5배 이상 쇼츠 영상의 비율이 많은 것을 확인할 수 있었습니다. 이를 통해 유튜브의 뷰티 광고 영상 중에서도 쇼츠 영상을 타겟팅하여 연구를 진행하면 의의가 있을 것이라고 생각했습니다.
  2. 유투브 영상의 조회수 예측에 관한 선행 연구, 숏폼 광고 영상에 관한 선행 연구, 유튜브 뷰티 카테고리 영상에 관한 선행 연구를 최대한 많이 조사했습니다. 그 결과, 유투브 영상의 조회수 예측에 관한 선행 연구는 활발히 이루어진 바 있으나 뷰티 광고 영상만을 타겟팅한 연구 사례는 존재하지 않으므로 우리 연구에 확실한 차별점이 있을 뿐더러 뷰티 광고 업계에 필요한 인사이트를 제공하는 선구자 역할을 할 수 있다는 근거를 확보했습니다.
  3. 기존 프로젝트에서는 마케팅 수치 중 하나인 반응률이라는 지표를 예측하는 것을 목표로 했습니다. 하지만 광고 업계에서 반응률이라는 지표가 하나의 통일된 공식으로 존재하지 않으며 업체마다 필요에 따라 수정해서 쓰는 관례가 있음을 확인했습니다. 게다가 '반응률'이라는 수치가 무엇을 의미하는지 비교할 수 있는 대상이나 선행 연구사례가 없어 해석이 난해한 점을 고려해 조회수 예측이라는 전통적인 모델을 구성하는 것으로 방향을 수정하기로 결정했습니다. 예측한 조회수는 누구나 직관적으로 받아들이고 해석하기 쉬운 장점이 있습니다.
  4. 기존 프로젝트에서는 영상의 조회수, 구독자수, 길이, 태그 빈도 등 메타데이터만을 수집하고 분석하여 모델을 구축했습니다. 그러다보니 영상 내부의 시청각 데이터에 관한 고려를 전혀 하지 못했을 뿐더러, 인플루언서로서 영향력을 가지는 유투버에 대한 피처 추출도 간과하게 되었습니다. 이번 프로젝트에서는 뷰티 업계에서 전문성을 가진 인플루언서로 판단될 만한 국내 뷰티 유투버 192명을 직접 선정하고, 이 유투버들의 광고 영상의 메타데이터와 함께 시청각 데이터를 수집하고 분석하여 모델을 구축했습니다.
  5. 기존 프로젝트는 발표자료를 잘 구성하여 취업을 위한 포트폴리오로 제작하는것을 목표로 삼았습니다. 그러나 1-4와 같은 일련의 데이터 기반의 의사결정을 통해 우리의 연구가 학술지로서 충분한 가치를 가진다는 합리적인 판단을 내리게 되었고, 새로 디벨롭하는 프로젝트는 학회에 발표하는 저널 형식으로 완성하기로 결정했습니다. 저희 팀은 6월 중순까지 프로젝트를 마무리하여 7월 학회에 발표하는 것을 목표로 하고 있습니다.

3. 마무리

이번 프로젝트에서 '데이터 기반의 의사결정'을 통한 프로젝트 기획과 구성이 중요하다는 정말 값진 교훈을 얻었습니다. 우리는 협업을 하면서 끊임 없이 서로를 설득시키는 과정을 반복합니다. 때로는 그 설득이 내가 옳다일 수도 있고, 네가 틀렸다일 수도 있겠죠. 나를 이해하지 못하는 팀원을 설득하는 일, 이해가 되지 않는 팀원의 말을 계속해서 듣는 일은 생각보다도 더 어렵고 소모적입니다. 때로는 일을 시작하기도 전에 말을 하다가 진이 다 빠지기도 하고요.이럴 때 필요한 것이 바로 데이터입니다. 번지르르한 말 천 마디보다 하나의 데이터가 더 큰 설득력을 가질 수 있습니다.우리는 데이터를 기반으로 의사소통을 하고, 의사결정을 내릴 수 있어야 한다는 걸, 이번 프로젝트를 통해 깨달았어요. 이렇게 얻은 깨달음을 토대로 더 나은 방향으로 팀 프로젝트를 디벨롭할 수 있어서 정말 감사하게 생각하고 있습니다. 제가 구상한 개선 방향을 믿고 따라와주는 멋진 팀원들에게 감사하다고 말하고 싶어요.
추가로 말씀드리자면, 지난 회고부터 틈틈이 언급했던 대로, 모든 KDT 교육과정이 대부분 비슷하겠지만, ASAC 빅데이터 분석과정은 특히 교육과정이 매우 타이트합니다. 그럼에도 불구하고 수업만 듣는다면 남는 게 없어요. 내가 무엇을 배우고 공부하는지 뭐라도 흔적을 남기고 아웃풋을 만들기 위해서는 수업 이상으로 틈틈이 꾸준히 노력해야 합니다. 더불어 본인에게 부족한 부분은 스스로 파악하고 자기주도적으로 보충해야 하고, 그와 동시에 포트폴리오도 구축해야 하며, 본인이 희망하는 필드에 걸맞는 자격을 갖추기 위한 추가 공부도 진행해야 합니다. 저는 그렇게 살고 있어요. 하루 24시간이 모자랄 만큼 바쁘지만, 그래도 누가 떠먹여주는 것보다 내가 스스로 개척해나가는 미래가 더욱 값지고 빛날 것이라는 일념으로 즐겁게 생활하고 있습니다.


다음 회고에는 딥러닝 프로젝트 회고를 가지고 오겠습니다 :) 감사합니다.
 

 

7주차 월요일, 드디어 개인 EDA 프로젝트 발표가 있었는데요. 주제 구상 - 데이터 수집, 분석, 시각화 - 인사이트 도출 - 관련 논문 분석 - 모델링 후 논문 검증 - 인사이트 도출 - 발표자료 구성 - 그리고 이 모든 작업을 몇 번이고 되돌아 반복하는 일련의 과정을 통해, 정말 많은 것을 배우고 성장할 수 있었던 기회였습니다.

이번 포스팅에서는 개인 EDA 프로젝트를 준비하면서 스스로 기특하고 자랑스러웠던 점, 그리고 스스로 확인한 나의 강점을 먼저 작성해 보겠습니다. 그리고 이어서 개선할 점을 이어서 적어보겠습니다.


칭찬할 점

첫째, 끊임 없이 피드백을 찾아 나섰습니다.

저희 ASAC 강사님께서는 이번 개인 EDA 발표를 준비하는 25명의 동기들을 모두 혼자서 멘토링하시느라 정말 바쁘셨는데요. 다르게 말씀드리면, 이번 프로젝트에서 강사님 1분을 제외하고는 조언을 구할 다른 멘토가 단 한 분도 계시지 않았습니다. 강사님께서는 몸이 열 개라도 모자라 보이셨고 야근을 자주 하셨어요. 그렇게 원하는 만큼 피드백을 얻기 어려웠던 (나름 열악했던) 상황 속에서, 저는 계속해서 현직자들과 네트워킹을 시도하며 프로젝트에 대한 가이드를 스스로 찾아 나섰습니다. 주로 일일 세미나를 오셨던 강사분들께 부탁을 드렸는데요. 쉬는 시간에 냅다 노트북을 가지고 가서 프로젝트를 보여드리고 조언을 구했습니다. 아니면 이메일이나 링크드인으로 자기 소개를 보내고 강의에 대해 느낀점을 말씀드린 뒤 정중하게 피드백 부탁을 드렸습니다.

쉬운 일은 아니었습니다. 바쁘신 분들께 초면에 그런 부탁을 드린다는 게 생각보다 많은 용기가 필요했어요. 제가 부족한 만큼 정성스럽고 따뜻한 피드백보다는 타격감 있는 직설적인 피드백들을 주로 받게 되었고, 그래서 자존심도 속도 많이 상하기도 했는데요. 그래도 정말 감사한 마음으로 모두 수용했고, 피드백을 찾아 나서는 일을 절대 멈추지 않았습니다. 나 혼자였다면 몰랐거나 흐린눈 하며 내버려 뒀을 어설프고 엉성한 부분들을 열심히 뜯어고치고 보완하면서 프로젝트의 완성도를 올릴 수 있었습니다.

문제상황을 적극적으로 해결하고자 전문가에게 도움을 청할 용기를 낸 나 자신을 정말 칭찬하고 싶습니다. 이번 프로젝트를 통해 네트워킹의 중요성을 다시한 번 확인했습니다. 사람들이 왜 협업을 통해 일을 진행하는지 알 수 있었던 좋은 기회였어요. 앞으로도 이렇게 스스로 배움을 찾아 나서는 적극적인 자세를 통해 더 많이 성장하고 발전하는 제가 되겠다고 다짐했어요. 

둘째, 어려운 과제 속에서 포기하거나 타협하지 않았습니다. 원하는 목표를 달성할 수 있었던 가장 큰 이유였습니다.

저는 25명의 동기들 중에서 이번 개인 EDA 프로젝트에 유일하게 AI 모델링 작업을 포함하여 발표했습니다. 아직 머신러닝 수업을 듣기 전이었지만, 제가 데이터 사이언스와 엔지니어링을 통한 문제해결에 큰 뜻과 욕심이 있다는 걸 아시는 강사님께서 제가 원하는 방향으로 프로젝트를 수립하도록 적극적으로 도와주셨기 때문에 가능한 일이였습니다. 또, CNN을 통한 Image Classification이 프로젝트의 메인 소재였는데, 평소 열심히 Neural Network의 원리를 탐구하고 Tensorflow Keras나 PyTorch와 같은 프레임워크들을 적극적으로 연습하고 실습한 덕분에 원하는 대로 프로젝트를 준비할 수 있었습니다.

딥러닝 과정에서 원하는 목표 성능이 확보 될 때 까지 여러 방법으로 모델을 수정하고 학습시키고 평가하는 일을 수없이 반복했습니다. 마음처럼 일이 풀리지 않아 마음이 흔들릴 때마다 매번 잘 다잡으려고 노력했어요. 그렇게 복잡하지 않고 간단할 거라고 생각했던 것도 예상대로 진행되지 않는 것을 보면서 실무에서는 더 어려운 일이 많이 일어나겠구나 싶었습니다. 여하간, 힘들고 답답할 때마다 타협하지 않고 목표한 일들을 무사히 수행해낸 나의 집념에 박수를 보내고 싶습니다.

앞으로 더 열심히 공부하고 지식 수준을 높여서 더 어렵고 복잡한 모델도 학습시켜 보고 싶습니다. 인공지능은 정말 너무 어려운데, 어려운 만큼 또 너무 재밌어요. 현재 ASAC 과정에서 머신 러닝 모델링 방법들을 여러 가지 배우기 시작했는데요. 평소 몰두했던 인공신경망(Neural Network) 뿐만 아니라 다른 모델링 학습 법도 이렇게 다양했다니 매일이 놀라움의 연속이더라고요. 즐겁게 배우고 있어요. 언제나 새로운 것들을 proactive한 자세로 배우고 탐구하는 제가 되도록 하겠습니다.


개선할 점

데이터 분석의 전문성을 높이기 위해서는 통계학적 지식을 보완할 필요가 있음을 깨달았습니다.

이번 프로젝트는 주제의 특성상 저의 도메인 지식을 적극 활용할 수 있었던 덕분에 통계학적 접근이 크게 필요하지 않았다고 생각했는데요. 그럼에도 불구하고 통계학적으로 분석한 근거가 함께 수반되었다면 더 논리적인 프로젝트 진행이 가능했겠다는 아쉬움이 남습니다. 

저는 지금 고려사이버대학교 소프트웨어공학과 학생으로 이번 3학년 1학기에 확률과 통계 수업을 듣고 있는데요, 수업 내용을 다시 한 번 정리하고 복습해서 다음 프로젝트에 꼭 적용해 보기로 했습니다. 또, 이번 여름 계절 학기에도 통계 관련 수업을 선택해서 연속적으로 수업을 듣기로 했습니다. 앞으로 통계학적 지식을 보충해서  자신뿐만 아니라 다른 사람들 역시 데이터 기반의 의사결정을 내릴 수 있도록 돕는 데이터 전문가가 될 수 있도록 노력하겠습니다.


 

이것으로 첫 번째 프로젝트(개인 EDA) 에 대한 회고 작성을 마치도록 하겠습니다. 개인 프로젝트 깃허브 링크가 궁금하시다면 댓글을 달아주시거나 niceonesuri@gmail.com으로 연락주세요. 감사합니다.

 

 

오늘 포스팅에는 유명한 Kaggle 신용카드 사기 감지 데이터셋(Credit Card Fraud Detection)을 가지고 데이터 전처리/분석/머신러닝을 하는 과정을 기록할 것입니다. 데이터 EDA를 진행하고 적절한 전처리를 해준 후 머신러닝 모델링을 수행하고 성능 지표를 비교하는 일련의 과정을 전부 담을 예정인데요, 의식의 흐름대로 작성할 예정이라 중간 중간 Tmi도 많고 삽질하는 내용까지도 필터링 없이 기록할 것임을 미리 알려드립니다.

 

1. 데이터 불러오기, 컬럼/결측치/데이터 타입 확인

https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud/data

 

Credit Card Fraud Detection

Anonymized credit card transactions labeled as fraudulent or genuine

www.kaggle.com

!kaggle datasets download -d mlg-ulb/creditcardfraud
!unzip "/content/creditcardfraud.zip"

먼저 캐글 신용카드 사기 감지 데이터셋을 다운로드받아서 가지고 옵니다. 저는 API Command를 복사하여 실행하고 코랩에 다운받아진 파일을 unzip해주는 형식으로 간단히 데이터를 불러왔습니다. 이렇게 하면 파일을 직접 다운로드해서 가져오는것보다 훨씬 빠릅니다.

 준비된 데이터프레임에 총 31개의 column, 284807개의 row를 확인했습니다.

card.info()

info 메소드를 통해 컬럼의 데이터타입과 결측치 여부를 확인했는데, 다행히 모든 컬럼에 결측치 없이 데이터가 잘 들어가 있었고, 정수타입의 Class 컬럼을 제외한 모든 열은 float64 타입임을 확인했습니다. 컬럼명에 들어있는 V1~V28의 경우 어떤 속성인지 알 수는 없고, Amount는 해당 row의 결제 금액, Class는 정상/사기 이진 분류(binary classification) 결과(label) 컬럼에 해당합니다.

만약 object 타입의 컬럼이 있었다면 라벨 인코딩(Label Encoding)이나 원핫 인코딩(One-Hot Encoding) 등의 작업을 통해 값을 숫자로 변환하는 작업이 필요합니다. 다행히 모두 숫자로 이루어져 있기 때문에 따로 인코딩 작업은 들어가지 않아도 될 것 같습니다 :-)

또, NaN 등의 결측치가 있는 경우 적절한 근거를 가지고 대표값으로 결측치를 채워 넣거나 해당 row를 삭제하는 등의 작업을 통해 결측치를 제거해 주어야 합니다. 다행히도 우리의 친절한 캐글 신용카드 데이터셋은 우리에게 그런 노가다를 요구하지 않고 있습니다... (흡족)

 

2. 결제 금액 분포도 그래프로 확인

결제 금액을 나타내는 Amount 컬럼의 분포도를 그래프로 확인해 보겠습니다.

import seaborn as sns
import matplotlib.pyplot as plt

plt.style.use('fivethirtyeight')
plt.figure(figsize = (10, 4))
plt.xticks(range(0, 40000, 1000), rotation = 45)
sns.histplot(card['Amount'], bins = 100, kde = True)
plt.show()

Tmi지만 저는 matplotlib의 테마 중에서 fivethirtyeight를 가장 좋아합니다. 왜인지는 모르겠습니다. 그래서 웬만하면 그래프를 그릴 때 fivethirtyeight으로 테마를 변경한 다음에 그래프를 뽑는 편입니다. 이런 각박한 작업 속에서도 내 취향이란 걸 반영할 수 있다는 게 전 재밌더라구요^_^...;;;

plt.figure(figsize = (8, 4))
plt.xlim(0, 1000)
plt.xticks(range(0, 1000, 50), rotation = 45)
sns.histplot(card['Amount'], bins = 500, kde = True)
plt.show()

다시 본론으로 돌아와서... 0부터 약 500달러 미만의 결제 금액이 차지하는 비율이 압도적으로 많은 것을 확인할 수 있었습니다. 이 그래프를 확인한 후 저는

과하게 치우친 Amount 컬럼의 값을 로그변환하여 보정해 주면 모델의 성능 지표가 상승할 것이다

라는 첫 번째 가설을 세우게 됩니다.

 

3. Amount 컬럼 박스 플롯 그리기

결제 금액 분포도를 살펴본 이후 저는 Amount 컬럼의 박스 플롯을 그려서 이상치를 확인해 봐야겠다는 생각이 들었는데요.

sns.boxplot(data = card, y = 'Amount')

쩝... 네... 첫 번째 삽질 보여드립니다. 방금 위에서 데이터가 쏠려 있음을 확인해놓고 박스플롯이 예쁘게 그려지리라고 생각한 제가 좀 바보같네요. 결제 금액의 박스 플롯은 큰 의미가 없는 것 같으니 다음으로 데이터 프레임의 상관 계수를 확인해 보도록 하겠습니다.

 

4. 상관 계수 시각화하기

card.corr()

corr() 메소드를 이용해서 모든 컬럼 사이의 상관계수를 나타내어 보았습니다. 한 눈에 들어오질 않으니 일단 쉐입을 확인해 볼게요...

card_corr = card.corr()
card_corr.shape
# (31, 31)

31개의 컬럼 사이의 상관계수가 (31, 31) 정사각 쉐입의 데이터프레임으로 이쁘게 반환되었습니다. 저는 저렇게 e어쩌고로 나타내진 숫자값들은 봐도 봐도 적응이 안되더라구요. 어쨌든 이 상태로는 전체 분포를 한 눈에 알아보기 힘들기 때문에 seaborn의 heatmap을 이용해서 그래프로 시각화를 해 보도록 하겠습니다.

두 번째 Tmi... 저는 또 이렇게 색상이 중요한 그래프를 그릴 때 괜히 컬러 팔레트 고르느라 1-2분을 더 낭비하는 것을 좋아합니다.. 역시 각박한 일상 속에서 찾아내는 저만의 소소한 작은 행복입니다 (^_^*) 이번에는 따뜻한 색감의 노랑-브릭 계열의 팔레트를 골라 보았습니다.

plt.figure(figsize = (8, 8))
sns.heatmap(card_corr, cmap = "YlOrBr")
plt.tight_layout()
plt.savefig("corr.png")

그래프를 큼직하게 뽑아서 괜히 savefig를 이용해 png파일로 다운로드까지 해 보았습니다. 이것도 자주 안하면 계속 까먹어요. ㅋ

상관관계 히트맵에서 양의 상관관계가 높을수록 색깔이 진한 갈색에 가깝고, 음의 상관관계가 높을수록 연한 노란색에 가깝습니다. 그래프의 변두리에 희끗희끗하게 보이는 밝은 부분들이 음의 상관관계가 높은 지점입니다. 

이 때, 레이블에 해당하는 Class컬럼과 가장 낮은 상관관계를 보이는 컬럼은 V14와 V17인것으로 확인되는데요.

이렇게 레이블과 관계 없는 컬럼의 이상치를 제거하면 모델의 성능 지표가 상승할 것이다

라는 두 번째 가설을 세우게 됩니다. 

 

5. [before] baseline 모델링

(5-1) train, test 구분하고 레이블 비율 확인해 보기

card.drop('Time', axis = 1, inplace = True)

먼저 큰 의미가 없는 Time 컬럼은 drop을 통해 삭제를 해 주고 시작하겠습니다.

def train_test(df):
    card_copy = card.copy()
    X_features = card_copy.iloc[:, :-1] # label에 해당하는 마지막 'Class'컬럼 제외
    y_label = card_copy.iloc[:, -1]     # label에 해당하는 마지막 'Class'컬럼만

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X_features,
                                                        y_label,
                                                        test_size = 0.2,
                                                        random_state = 1004,
                                                        stratify = y_label)
    
    return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = train_test(card)

card 데이터프레임의 마지막 'Class' 컬럼을 기준으로 feature과 label을 구분했구요. 싸이킷런의 train_test_split을 통해 8:2의 비율로 train과 test를 분할했습니다. 3번째 Tmi...인데.. 저는 random_state로 항상 1004(천사)를 사용합니다. ^^;;;;;

len(X_train), len(X_test), len(y_train), len(y_test)
# (227845, 56962, 227845, 56962)

이렇게 나눠졌구요. 그럼 데이터에서 레이블 비율(0과 1의 비율)이 어떻게 되는지 살펴볼까요?

y_train.value_counts(normalize = True).apply(lambda x: str(round(x * 100,4)) + '%')
y_test.value_counts(normalize = True).apply(lambda x: str(round(x * 100,4)) + '%')

와우! 사기에 해당하는 1번 레이블이 약 0.17%에 해당하는 극소수로 확인되었습니다. 하긴, 사기 거래가 2~30%씩 차지하고 있으면 그것도 말이 안되겠네요. 저는 이렇게 뭐든 시각화하면서 가지고 있는 데이터와 친숙해지는 작업이 꼭 필요하다고 생각합니다. 그렇지 않으면 의미 없는 숫자놀음에 그치게 된다고 생각해요,,, 어쨌거나 저쨌거나 이 비율 확인 후 저는 

평가 지표(metrics)로 Accuracy가 아닌 다른 지표들을 전부 다 확인할 필요가 있음

이라는 결론을 내렸고, 다양한 평가 지표를 모두 확인해 보기로 결정했습니다.

(5-2) 모델 학습 후 예측 성능 평가

def train_eval(model, f_tr = None, f_test= None, t_tr= None, t_test= None):
    # 모델 학습
    model.fit(f_tr, t_tr)

    # 예측 
    pred = model.predict(f_test)
    pred_proba = model.predict_proba(f_test)[:, 1]

    # 평가 (1) confusion matrix 출력
    from sklearn.metrics import confusion_matrix
    confusion = confusion_matrix(t_test, pred)
    print("----- confusion matrix -----")
    print(confusion)
    
    # 평가 (2) accuracy / precision / recall / f1 / roc_score 출력
    from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
    accuracy = accuracy_score(t_test, pred)
    precision = precision_score(t_test, pred)
    recall = recall_score(t_test, pred)
    f1 = f1_score(t_test, pred)
    roc_auc = roc_auc_score(t_test, pred_proba)
    print("----- evaluation score -----")
    print(f"accuracy : {accuracy:.4f}")
    print(f"precision : {precision:.4f}")
    print(f"recall : {recall:.4f}")
    print(f"f1 : {f1:.4f}")
    print(f"ROC_SCORE : {roc_auc:.4f}")

 

먼저 baseline 모델을 학습시키고 예측을 수행한 다음 confusion matrix와 여러 평가 지표를 출력하는 함수를 작성했습니다.

from lightgbm import LGBMClassifier
from xgboost import XGBClassifier

lgbm = LGBMClassifier(n_estimators = 1000,
                      num_leaves = 64,
                      n_jobs = -1,
                      boost_from_average = False)

xgb = XGBClassifier(n_estimators = 1000,
                    learning_rate = 0.05,
                    max_depth = 3, 
                    eval_metric = 'logloss')

이후 XGBM, LightGBM 모델 2가지를 학습하고 예측 평가를 수행했습니다.

train_eval(lgbm, X_train, X_test, y_train, y_test)
train_eval(xgb, X_train, X_test, y_train, y_test)

(좌) LGBM / (우) XGB

결과값은 위와 같이 나왔는데요. Accuracy는 대체로 동일하고, precision과 recall, f1은 LightBGM 모델이 더 높았고, roc_auc 점수는 XGB 모델이 더 높게 측정되었습니다.

 

6. [after] 가설 2가지 적용하기

이 번엔 데이터 분석을 통해 세웠던 2가지의 가설을 검증해보는 작업을 수행해 보겠습니다.

레이블과 관계 없는 컬럼의 이상치를 제거하고 Amount 컬럼의 값을 로그변환하여 보정한다면 모델의 성능 지표를 상승시킬 수 있을 것이다

(6-1) 이상치 찾아내기

def find_outliers(df, column):
    # 해당 컬럼의 사기 데이터 series 선언
    frauds = df[df['Class'] == 1][column]
    
    # IQR 구하기
    import numpy as np
    Q1 = np.percentile(frauds.values, 25)
    Q3 = np.percentile(frauds.values, 75)
    IQR = Q3 - Q1

    # 1QR에 1.5를 곱해서 최댓값, 최솟값 구하기
    lowest_value = Q1 - IQR * 1.5
    highest_value = Q3 + IQR * 1.5

    # 이상치 데이터 찾아서 인덱스만 리스트로 뽑아내기
    outliers = frauds[(frauds < lowest_value) | (frauds > highest_value)].index

    # 이상치 인덱스 리스트 리턴
    return outliers

데이터 프레임과 원하는 컬럼명을 인자로 받아서 해당 컬럼의 이상치 인덱스를 리스트로 반환하는 함수를 작성했습니다 :) 처음부터 바로 함수를 작성한 건 아니고, 컬럼 하나를 지정해서 하나하나 실행해본 다음 깔끔하게 함수로 정리하는 작업을 통해 함수를 완성했습니다.

find_outliers(card, "V14")
# Index([8296, 8615, 9035, 9252], dtype='int64')

find_outliers(card, "V17")
# Index([], dtype='int64')

아까 히트맵을 통해 확인했을 때 Class컬럼과 가장 낮은 상관관계를 보이는 컬럼은 V14와 V17인것으로 확인되었었는데요, find_outliers 함수를 통해 확인해 본 결과 V14 컬럼의 경우 이상치가 4개 발견되었고, V17 컬럼의 경우 이상치가 발견되지 않았습니다 :)

 따라서 저는 V14 컬럼의 이상치 데이터 4개를 삭제해 보겠습니다.

 

(6-2) 이상치 제거, 로그 변환 후 train, test 분리

def train_test_no_outliers(df):
    card_copy = df.copy()
    # outlier 제거
    outliers = find_outliers(card_copy, "V14")
    card_copy.drop(outliers, axis = 0, inplace = True)

    # Amount 컬럼 로그변환
    import numpy as np
    card_copy['Amount'] = np.log1p(card_copy['Amount'])
    
    # 데이터셋 나누기
    X_features = card_copy.iloc[:, :-1] # label에 해당하는 마지막 'Class'컬럼 제외
    y_label = card_copy.iloc[:, -1]     # label에 해당하는 마지막 'Class'컬럼만

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X_features,
                                                        y_label,
                                                        test_size = 0.2,
                                                        random_state = 1004,
                                                        stratify = y_label)
    
    return X_train, X_test, y_train, y_test

X_train_af, X_test_af, y_train_af, y_test_af = train_test_no_outliers(card)

train, test 분리하는 함수에 Amount 컬럼 로그변환과 outlier rows 제거하는 과정을 추가한 다음 분리 작업을 실시했습니다.

(6-3) 모델 학습 후 예측 성능 평가

train_eval(lgbm, X_train_af, X_test_af, y_train_af, y_test_af)
train_eval(xgb, X_train_af, X_test_af, y_train_af, y_test_af)

(좌) LGBM / (우) XGB

다음과 같이 결과가 나왔습니다.

 

7.  모델 4개 비교

지금까지 학습시킨 모델들의 성능 지표를 표로 작성하여 한눈에 비교해 보겠습니다.

  Baseline(XGB) Baseline(LGBM) After(XGB) After(LGBM)
Accuracy 0.9996 0.9996 0.9996 0.9996
Precision 0.9419 0.9405 0.9405 0.9302
Recall 0.8265 0.8061 0.8061 0.8163
F1 0.8804 0.8681 0.8681 0.8696
ROC Score 0.9853 0.9863 0.9882 0.9793

저는 이상치 제거와 로그 변환의 전처리 과정을 거친 다음 학습한 After 모델들의 모든 성능 지표가 다 개선될 것이라고 생각했습니다. 하지만 실습 결과 Precision / Recall / F1의 경우 오히려 Baseline의 수치가 더 높게 나온것을 확인할 수 있었습니다. 역시 인생이란 건 그렇게 호락호락하지 않습니다. 원하는 대로 결과가 안 나와서 좀 찝찝하긴 한데요...

다만, After XGB 모델 ROC-AUC 스코어의 경우 Baseline 모델들보다 점수가 더 상승한 것을 확인할 수 있었습니다. 이번에 다룬 Kaggle 신용카드 사기감지 데이터셋의 경우, 기존에 미리 확인한 대로 전체 데이터의 약 0.17%만이 사기에 해당하는 극도로 치우친 값을 가지고 있었습니다. 이렇게 과하게 편향된 데이터셋의 경우 ROC-AUC 스코어가 유의미하므로, 만약 4가지 모델 중 한 가지를 골라야 한다면 저는 ROC-AUC 스코어가 가장 높게 나온 After(XGB) 모델을 고르는 것도 나쁘지 않겠다는 생각을 했습니다.

 

8. 마무리

이번 포스팅은 여기에서 실습을 마치도록 할텐데요. 위의 4가지 모델에서 성능 지표를 더 올리기 위해서는 0.17%에 해당하는 사기 데이터를 펌핑해서 수를 늘려 주는 방법을 사용하면 좋을 것 같습니다. 마음같아서는 지금 당장 하고싶지만 할일이 많아서 일단 여기에서 끊겠습니다만, 좀 찝찝하기 때문에 시간여유가 생기면 모델 개선 작업을 추가로 수행해서 추가 포스팅을 하도록 하겠습니다.

이번 실습을 통해서 앞으로 다가올 머신러닝 팀프로젝트에 어떤 식으로 데이터 EDA를 진행하고 모델링을 개선해나갈 수 있을 지 좋은 가이드라인이 되었습니다. 

읽어주셔서 감사합니다 :-)

 

 

 

지난 글에서 서울특별시 공중화장실 공중데이터를 판다스 데이터프레임으로 만들고 간단히 정제작업을 해 보았는데요. 정제한 데이터프레임을 가지고 태블로를 이용해서 아주 간단히만 시각화 작업을 진행해 보았습니다.

 

 

 

대시보드 구성 방법

  1. 서울시의 25개 구별 공중화장실 수 합계를 계산하여 그 수를 비교할 수 있도록 시각화했습니다.
  2. 대시보드의 왼쪽에는 지도를 배치하여 화장실의 수를 원의 크기와 색깔로 직관적으로 파악할 수 있도록 구성했습니다
    • 지도를 확대하면 보이지 않는 레이블을 모두 확인할 수 있어요.
    • 화장실 수가 많을수록 원의 크기가 큽니다.
    • 화장실 수가 많을수록 원의 색깔이 진합니다.
  3. 대시보드의 오른쪽에는 가로막대그래프를 배치하여 수치별로 좀더 직관적인 비교가 가능하도록 구성했습니다.
    • 오른쪽의 비교 파라미터를 이용해서 비교선을 100단위로 조절하면서 이동시킬 수 있어요.
    • 비교선을 움직이면서 비교선을 기준으로 색깔이 바뀌는 것을 확인할 수 있어요.

 

태블로 퍼블릭으로 보러가기 (클릭)

 

지하철

지하철

public.tableau.com

클릭하시면 태블로 퍼블릭 웹사이트에서 인터렉티브하게 직접 결과를 조절해 보실 수 있습니다.

 


 

 

<시각화 이후 생각해볼만한 것들>

 

1. 구별 화장실 수와 구별 면적의 관계는 어떻게 되는가?
2. 구별 화장실 수와 지하철 역의 개수의 상관관계가 있는가?
3. 구별 화장실 수와 구별 인구 수의 관계는 어떻게 되는가? 인구와 구별 화장실 총 수는 비례하는가?
4. 서울특별시 구별 장애인 화장실 데이터를 따로 구할 수 있는가? 구할 수 있다면, 전체 화장실과 장애인 화장실의 비율을 비교해 보자.
5. 상업 단지와 화장실 수의 상관관계가 있는가?

6. 관광 구역과 화장실 수의 상관관계가 있는가?

7. 공공화장실 중에서 지하철 역 화장실의 개수를 특정할 수 있는가? 있다면 그 비율은 어떻게 되는가?

8. 지하철 역 화장실 개수를 구할 수 있다면, 지하철 노선별 유동인구 데이터와 병합하여 화장실의 갯수가 적절하게 비치되어 있는지 비교해보자.

 

 

 

여기까지입니다 :-) 좀더 생각해볼 수 있을것 같지만 본격적인 개인 EDA 프로젝트 준비를 위해서 이번엔 이정도로 간단히만 포스팅을 마치려고 합니다. 쉽고 짧은 작업이었지만 맘에드는 공공데이터를 구하고, 정제하고, 시각화 후 생각해볼거리 도출까지 은근 시간이 걸렸네요. 다음엔 더 능숙하고 멋진 프로젝트를 가져와서 공유해보겠습니다. 감사합니다. :)

 

 


 

 

(+) 판다스 데이터프레임 CSV파일로 내보내기 한 후 태블로에 불러온 과정

 

t.to_csv("toilet_df.csv")

 

먼저 to_csv() 메소드를 통해 간단하게 csv파일로 내보내기를 해주었습니다. 저는 구글 코랩에서 실습을 진행했습니다.

 

 

 

 

 

구글 코랩의 왼쪽 파일메뉴에서 간단하게 바로 다운로드를 해서 다운로드 폴더에 넣어 주었는데요. 로컬 환경에 따로 다운로드하지않고 태블로 퍼블릭 환경에서 구글 드라이브를 연동해서 바로 오픈해도 됩니다.

 

 

 

 

 

태블로 퍼블릭 프로그램을 실행하고 로컬 환경에 다운로드한 toilet_df.csv 파일을 오픈했습니다.

 

 

 

따로 사용하지 않을 예정인 산지, 부지번 컬럼은 숨기기(Hide) 해주었습니다. 이후 시트 2개에서 작업을 하고 대시보드 1개에서 두개를 합쳐주는 방식으로 간단히 끝을 내주었습니다.

 

 

 

 
서울시 공중화장실 공공데이터를 가지고 아주 간단한 데이터 시각화, 분석 실습을 해 보려고 합니다.
먼저 이번 포스팅에는 파이썬 pandas 라이브러리를 이용해서 데이터 전처리 작업한 것을 간단히 정리해 보았습니다. 
데이터 시각화, 분석은 태블로 프로그램을 이용하여 마친 뒤 다음 포스팅에 이어서 올리도록 하겠습니다.
 
 
https://data.seoul.go.kr/dataList/OA-1370/S/1/datasetView.do

열린데이터광장 메인

데이터분류,데이터검색,데이터활용

data.seoul.go.kr

 
사용한 데이터 링크입니다.
 


 

1. pandas : 필요없는 컬럼 삭제, 인덱스 지정

import pandas as pd

t = pd.read_csv("toilet.csv", engine='python', encoding = "cp949")
t

 
pd.read_csv를 이용해서 데이터를 불러오는데 한글 깨짐이 좀 있어서 encoding = "cp949"를 이용해주니 깔끔하게 불러오기가 잘 되었습니다.

 
먼저 value_counst()를 이용해서 대략적으로 확인해보니 별 다른 정보가 담겨있지 않은 관계로 새주소명, 생성일 컬럼은 삭제하기로 결정했습니다. 또, 고유번호에 중복값이 없는것을 확인한 관계로 고유번호를 인덱스로 지정해주겠습니다.

# 새주소명, 생성일 컬럼 드랍(삭제)
t = t.drop('새주소명', axis=1)
t.drop('생성일', axis = 1, inplace = True)

# 고유번호 컬럼 중복값 없는지 확인
len(t['고유번호'].unique()) == len(t['고유번호'])  # True

# 고유번호 인덱스화
t.set_index('고유번호', inplace = True)

 
필요없는 컬럼을 삭제하고 인덱스를 고유번호로 바꾸어 주니 어느정도 보는 게 깔끔해졌습니다.
이제 구명과 법정동명을 확인해보려고 하는데요.
 
 

2. 구명

t['구명'].value_counts()

 
value_counts()를 이용해서 확인해 보니

  1. 끝에 '구'가 붙어 있지 않은 구
  2. 오타 작렬한 구
  3. 빌딩이 왜 여기서 나와? 갈암구는 또 어디야? 갈현송방차풀소는 뭐야?

이것들을 해결해줘야 할 것 같습니다.
먼저 구가 안붙어있는 것들에 구를 붙여줘 보기로 했습니다. (예:노원 > 노원구)

t['구명'] = t['구명'].apply(lambda x: x + '구' 
                          if x in ['동작', '금천', '강서', '양천', '노원', '관악', '영등포', '서대문'] 
                          else x)

 
'구명'이 동작/금천/강서/양천/노원/관악/영등포/서대문 중 하나인 경우
컬럼에 apply와 lambda 함수를 이용해서 끝에 '구'를 붙여 줬습니다. 해당사항이 없는 경우는 그냥 놔두도록 처리했습니다.

t['구명'].value_counts()


그 외 갈현송방차풀소~남서울빌딩에 해당하는 row들은 그냥 제거하겠습니다.

# 이것들에 해당하는 '구명'을 가진 row들을 제외한(~) 줄만 남겨서 t에 재할당
t = t[~t['구명'].isin(['갈현송방차풀소', '송북구', '송파ㅜ', '영등로구', 
                      '영등표구', '송파두성빌딩', '갈암구', '구로수', '남서울빌딩'])]
len(t['구명'].value_counts().index)
# 25

 
혹시 몰라 확인해 보니 총 25개의 구가 있는것이 잘 확인되었습니다. 검색해보니 서울에는 25개 자치구와 426개 행정동이 있다고 하네요! 서울 살면서도 계속 까먹어요... 상식으로 외워둬야지.
 
법정동은 426개를 일일이 확인하기 불가능 + 의미가 없는 것 같아서 일단 놔두도록 하겠습니다. 다음 포스팅에는 태블로를 이용하여 간단한 시각화를 해서 가져오도록 하겠습니다!
 

 

 


 
파이썬 requests, BeautifulSoup 라이브러리를 이용한 웹크롤링 후 데이터분석 실습을 해보았습니다 :-) 
 
연금복권720+은 제가 한달에 2-3회정도 꾸준하게 구매하는 최애 복권인데요. 슬프게도 지금까지 제대로 당첨된 적은 단 한번도 없지만, 앞으로도 저는 꾸준히 구매를 할 예정인 아주아주 매력적인 복권입니다. 1등에 당첨이 되면 (세전) 700만원을 매월 20년동안 수령할 수 있어요. 동행복권 온라인 사이트에서 간단히 온라인 구매를 할 수도 있구요. 1등 번호는 온라인 1명, 오프라인 1명 총 2명이 당첨될 수 있습니다. 자세한 복권 구조는 동행복권 홈페이지를 참고해 보시구요.
 
복권의 경우 통계를 공부해보신 분들께는 아주 친숙한 소재이실텐데요. (저는 아닙니다.ㅋㅋㅋ) 동행복권 사이트에서는 복권 당첨번호를 엑셀파일로도 제공하고 통계 자료를 따로 분석해서 메뉴도로 제공하고 있습니다. 다만 저는 철저히 requests 라이브러리를 이용한 웹크롤링에 익숙해지기 위해서 엑셀 파일이나 통계자료를 건드리지 않고 처음부터 끝까지 혼자 힘으로 본 실습을 했습니다! 
 

 
[참고] 본 포스팅은 수리링 본인의 공부 기록을 목적으로 작성하였습니다. 해당 라이브러리에 대해 전혀 모르시는 분께서 보면서 따라하시기엔 많이 불친절하게 느껴질 수 있습니다. 참고하시고 봐 주시면 감사드리겠습니다 :-)
 
[참고] 본 포스팅은 책, 강의, 다른 사람의 포스팅을 참고하지 않은 스스로의 창작물입니다! 참고하여 포스팅 하시는 경우 출처 밝혀주심 감사드리겠습니다!
 


 
[실습 목차]

  1. 206회차로 모의 실습
  2. 원하는 회차 구간을 입력하면 모든 정보를 담아 데이터프레임으로 리턴하는 함수 작성
  3. 데이터프레임으로 간단한 데이터분석 (은근 재밌으니 귀찮으시면 이것만 보고 가세요...^^)

1-1. 숨은 URL 찾아내기

 
동행복권 사이트의 회차별 당첨번호 페이지(클릭)에 가 봅니다.
 

 
 
회차 바로가기 메뉴를 통해 원하는 회차를 선택해서 당첨 결과를 볼 수 있었습니다.
 

 
 
그런데 기본 URL에 회차 정보가 드러나지 않고 숨어 있어요. 206회를 조회해도, 200회를 조회해도 계속 같은 URL이 유지됩니다. 따라서 회차를 특정하여 정보를 뽑아낼 수가 없는 상황입니다. 우리는 회차를 조회할 수 있는 상세URL을 알아내야 해요.
 
문제상황을 해결하기 위해 크롬 웹브라우저의 inspection(개발자 도구) 메뉴의 Network 탭을 확인해 봅시다.
 

 
 
위와 같이 네트워크 탭을 켜둔 상태로 조회 버튼을 눌러봅니다. Name 탭의 맨 첫 번째 gameResult 어쩌구를 클릭한 다음 Payload를 확인합니다. (누가 봐도 수상한) Round: 206 이라는 정보를 확인했습니다. 기존 url 뒤에 &Round=206을 붙여 주면 될 것 같다는 합리적 의심을 해봅니다.
 

https://dhlottery.co.kr/gameResult.do?method=win720&amp;amp;Round=205

 
 
주소 뒤에 &Round=205 를 붙여넣고 엔터를 치니 205회 당첨결과 페이지로 잘 이동합니다 ㅎㅎ 찾았다 요놈! 이제 상세 url주소를 찾았으니 코드를 작성하면서 원하는 데이터를 뽑아내 보겠습니다.
 


 

1-2. requests, BeautifulSoup 라이브러리

* 본 실습에서 해당 라이브러리에 대한 상세 설명은 생략합니다

import requests
from bs4 import BeautifulSoup as BS

 
먼저 requests와 BeautifulSoup 라이브러리를 임포트해줍니다.

url = "https://dhlottery.co.kr/gameResult.do?method=win720&Round=206"
res = requests.get(url)
soup = BS(res.text, "html.parser")

 
우리가 찾아낸 url을 선언해 준 다음 차례대로 라이브러리에 넣어서 html 자료를 뽑아냅니다.

 
soup을 실행해 보니 html 정보가 잘 들어왔습니다 :)
저는 html 코드를 하나하나 뜯어보면서 원하는 정보를 뽑아내 봤어요.

nums = rows[0].find_all("span", {"class":"num"})

#조, 당첨번호
group = int(nums[0].find("span").text)
n_1 = int(nums[1].find("span").text)
n_2 = int(nums[2].find("span").text)
n_3 = int(nums[3].find("span").text)
n_4 = int(nums[4].find("span").text)
n_5 = int(nums[5].find("span").text)
n_6 = int(nums[6].find("span").text)

print(f"{group}조 {n_1}, {n_2}, {n_3}, {n_4}, {n_5}, {n_6}")
# '3조 4, 8, 9, 0, 7, 5'

 
먼저 제일 중요한 1등 조, 6개의 당첨번호를 뽑아봤습니다. 

# 등위(등수명) 
# rows[0]이므로 첫번째 1등을 구함 -> 나중에 인덱스를 바꾸어 다른 등수의 이름도 구할 수 있음
rank = rows[0].find_all("td")[0].text
rank
# '1등'

 
등수명도 뽑아봤어요. 이정도는 그냥 작성해도 되지만 연습삼아서 뽑아봤습니다 :)

# 당첨결과(매)
rank_counts = int(rows[0].find_all("td", {"class":"ta_right"})[-1].text.strip())
rank_counts
# 2

 
1등의 당첨 매수를 뽑아봤습니다. 206회차는 1등이 2명입니다. 연금복권 1등은 온라인/오프라인 각1명씩 최대 2명이 나올 수 있습니다. 가끔 1등이 1명밖에 없을 때도 많아요. 아주 드물게 0명일 때도 있는 거 같아요.

# 보너스 당첨번호 6자리
bonus_nums = []
for i in range(6):
    bonus_num = rows[7].find_all("span", {"class" : "num"})[i].find("span").text
    bonus_num = int(bonus_num)
    bonus_nums.append(bonus_num)

print(bonus_nums)
# [5, 8, 7, 6, 9, 5]

 
보너스 당첨번호 6자리도 뽑아봤습니다.

# 보너스 당첨결과(매)
bonus_counts = int(rows[7].find_all("td", {"class":"ta_right"})[-1].text.strip())
bonus_counts
# 10

 
10명이나 당첨됐네요.
 


 

2-1. 회차를 입력할 수 있는 함수로 작성해보기

206회차를 가지고 적당히 연습을 해 봤으니, 원하는 회차를 입력하면 하나씩 모두 조회해서 딕셔너리로 담아 리턴하는 함수를 작성해 보았습니다.

def win720(round):
	# 입력받은 회차 번호로 url을 만들고 정보를 받아냅니다.
    url = f"https://dhlottery.co.kr/gameResult.do?method=win720&Round={round}"
    res = requests.get(url)
    soup = BS(res.text, "html.parser")
    rows = soup.find("tbody").find_all("tr")
	
    # data_dict에 앞으로 하나씩 정보를 추가할 겁니다. 먼저 라운드 값을 첫 번째로 넣어줬습니다.
    data_dict = {"round":round}
    nums = rows[0].find_all("span", {"class":"num"})

    # 1등 조, 당첨번호
    group = int(nums[0].find("span").text)
    n_1 = int(nums[1].find("span").text)
    n_2 = int(nums[2].find("span").text)
    n_3 = int(nums[3].find("span").text)
    n_4 = int(nums[4].find("span").text)
    n_5 = int(nums[5].find("span").text)
    n_6 = int(nums[6].find("span").text)

    data_dict["group"] = group
    data_dict["n_1"] = n_1
    data_dict["n_2"] = n_2
    data_dict["n_3"] = n_3
    data_dict["n_4"] = n_4
    data_dict["n_5"] = n_5
    data_dict["n_6"] = n_6

    # 1-7등 당첨자수
    for i in range(7):
        rank_counts = rows[i].find_all("td", {"class":"ta_right"})[-1].text.strip()
        rank_counts = re.sub(",","", rank_counts)
        rank_counts = int(rank_counts)
        column_name = f"rank{i+1}"
        data_dict[column_name] = rank_counts

    # 보너스 당첨번호 6개
    for i in range(6):
        bonus_num = rows[7].find_all("span", {"class" : "num"})[i].find("span").text
        column_name = f"bonus_{int(i)+1}"
        data_dict[column_name] = bonus_num

    # 보너스 당첨자수
    bonus_counts = int(rows[7].find_all("td", {"class":"ta_right"})[-1].text.strip())
    data_dict["bonus"] = bonus_counts

    return data_dict

 
더럽게 길지만 그래도 잘 작동했습다^_^;;;; 
너무 길어져서 쓰면서 불길했는데 그래도 오류 수정 2-3번만에 원하는 대로 값이 나와서 다행이였어요

 
205회차로 테스트를 해 봤는데요. 조, 1등 넘버 6자리, 등수별 당첨매수, 보너스 번호 6자리, 보너스 당첨매수가 딕셔너리로 제대로 들어온 것을 확인했습니다 :) 이게 뭐라고 너무 재밌었어요 (ㅋㅋㅋㅋ)
 


 

2-2. 회차 구간을 설정하고 데이터프레임을 리턴하는 함수 작성하기

 
위에서 작성한 win720()함수를 가지고 원하는 회차 구간의 모든 정보를 담은 데이터프레임을 반환하는 함수를 작성해 주었습니다.

def lucky_chart(start, end):
    lucky_results = []

    for i in range(start, end+1):
        win = win720(i)
        values = list(win.values())
        print(values)
        print(len(values))
        lucky_results.append(values)

    columns = list(win.keys())
    print(columns)
    print(len(columns))
    
    import pandas as pd
    df = pd.DataFrame(lucky_results, columns = columns)
    
    return df.set_index("round")

 
중간 중간에 있는 print 함수들은 제가 함수를 작성하면서 중간 과정을 시각화하기 위해 굳이 넣어줬구요, 깔끔하게 없애줘도 됩니다.

history = lucky_chart(190, 206)

 
190회부터 206회차까지 럭키차트 함수를 돌려보았습니다.

 
요런식으로 진행상황을 시각화 하기 위해 print 함수를 넣어줬습니다. (중간에 오류가 있었어서 저런식으로 시각화 하면서 수정해줬어요!)
 

history

 
알흠다운 판다스 데이터프레임이 완성되었어요 ❤️
 


 

3. 간단 데이터분석

마지막으로 데이터분석은 1회차부터 206회차까지로 구간을 늘려서 진행했습니다! (1등은 제껍니다.)
 

  • 1등 조 비율이 어떻게 될까?
# 조별 value_counts() 구하기
group_counts = history['group'].value_counts()

# matplotlib 임포트
import matplotlib.pyplot as plt

# 차트 작성
plt.pie(group_counts,
        labels = group_counts.index,
        shadow = True,
        autopct = '%1.2f%%')
plt.title("Rank 1 groups ratio")
plt.legend()
plt.show()

 
아주.. 흥미로운.. 결과입니댜... 연금복권720+ 1회차부터 206회차까지 모든 데이터들을 살펴본 결과... 지금까지 가장 많은 1등을 배출한 조는 4조였습니다. 4조 > 1조 > 3조 > 5조 > 2조 순이네요. 
 
연금복권을 아는 분들께서는 이해를 하실텐데, 저는 혹시라도 번호 6개를 다 맞췄지만 조가 다를 때 2등이라도 당첨되도록 + 혹시라도 1등이 되면 2등도 동시 당첨되도록(ㅋㅋㅋ) 번호 6개를 고르고 나면 모든 조(1~5조)로 총 5줄(5,000원)을 구매해버립니다. 솔직히 이게 더 확률이 낮을 것 같기는 한데.......... 만약 특정 조를 골라서 구매해야 한다면 앞으로 저는 1조 또는 4조를 고르겠습니댜.
 

  • 지금까지 역대 2등은 몇명이 나왔을까?
import seaborn as sns

sns.set_style("darkgrid")
sns.set_palette("bright")

sns.barplot(history["rank2"].value_counts())
history['rank2'].agg(['min', 'max', 'mean'])
# min     0.0
# max     8.0
# mean    4.5

 
1등에 가려 2등의 당첨자 수는 사실 잘 확인해 본 적이 없는데요. 2등에 당첨되면 매달 100만원씩 10년간 수령할 수 있거든요. 2등이라도 당첨시켜 주신면 제가 굉장히 감사할텐데요. 몇 명이나 당첨되나 보니, 역대 2등 당첨자 수는 최대 8명, 최소 0명(ㅋㅋㅋㅋㅋㅋ), 평균 4.5명이 나왔다고 합니다. 그래프로 확인해 보니 2등이 한 명도 나오지 않은 회차가 20번이 넘네요? 실화냐?
 

  • 그럼 1등이 한 번도 안 나온 회차도 있을까?
history['rank1'].value_counts()
# 1    101
# 2     63
# 0     42
sns.countplot(data = history,
              x = 'rank1',
              legend = 'full')

아니 미친... 1등이 0명인 회차가 40회가 넘는다고? 1등이 2명 다 나온 적보다 1명밖에 안 나온 적이 더 많다고? 여러분 빨리 연금복권 사세요! 저거 다 우리돈이라고(흥분)
 

  • 1번부터 6번까지 각 자리마다 번호가 몇번씩 나왔을까
df = pd.DataFrame()

for i in range(1, 7):
    col = f"n_{i}"
    df[col] = history[col].value_counts().sort_index()

df
for i in range(1, 7):
    col = f"n_{i}"
    print(f"{i}번째 자리에서 가장 많이 나온 숫자는 {df[col].idxmax()}")

 
각 자리에서 가장 많이 나온 숫자는 순서대로 4 - 4 - 9 - 0 - 5 - 6 이었습니다. 이게 엄청 큰 의미가 있을지는 모르겠지만, 해당 자리에 어떤 숫자를 고를지 고민되신다면 이 정보도 참고해 봐도 좋을 것 같습니다.

for i in range(1, 7):
    col = f"n_{i}"
    print(f"{i}번째 자리에서 가장 조금 나온 숫자는 {df[col].idxmin()}")

 
반대로 각 자리별로 가장 조금 나온 숫자도 구해봤어요. 두 정보를 종합하면, 숫자 0 4 7 9가 자주 보이네요. 반대로 0 4 7 9를 제외한 1 2 3 5 6 8 을 고르는 것도 안전하게 갈 수 있는(?) 방법일 수도 있을 것 같고.. 복권의 세계는 정말 어렵네요.
 
tmi지만 저는 항상 첫자리를 6으로 구매를 하는데, 첫자리 6은 꼴찌 0에 이어서 두 번째로 나온 횟수가 적네요. 전략을 바꿔야 하나.... 고민이 되지만 (ㅋㅋㅋㅋㅋㅋㅋㅋㅋ) 복권 통계는 재미일 뿐이라고 생각합니다.
 
 
 
 
여기까지 간단 분석을 마쳐 보겠습니다! :-)
감사합니다.
 

 
 

 

 

안녕하세요, 데이터 사이언티스트를 꿈꾸는 수리링입니다.

저는 2024년 내일 배움 카드 KDT 훈련과정인 ASAC 빅데이터 분석 과정 5기에 지원하고 합격하였습니다. 2024년 3월 20부터 과정이 시작되었는데요. 평소에 노션으로 공부 내용을 정리하고 관리해 왔지만, 모두에게 더 쉽게 공유할 수 있는 자료가 되었으면 하는 마음에 이번 과정의 회고는 블로그에 남기려고 합니다.

SK플래닛에서 주관하는 ASAC 빅데이터 분석 과정은 데이터 사이언스와 인공지능 분야까지 배울 수 있는 오프라인 교육과정입니다. 매니저님께 질문해 보니 이번 5기 모집은 어느 때보다 경쟁률이 치열했다고 합니다. 총 28명을 모집하는 데 약 150명 이상의 지원자가 몰렸다고 하는데요. 오프라인 면접과 코딩 테스트를 통해 선별이 된 인원이 모인 만큼 동기들과 함께 좋은 커뮤니티를 형성할 수 있을 거라는 큰 기대를 가지고 있습니다.


 

[1] 지원 계기

나중에 따로 제 소개 글을 써볼 텐데, 먼저 간단히 말씀드리면 저는 공대를 나오지 않은 비전공자입니다. 2023년 11월 말부터 최근까지 약 3개월간 독학으로 코드를 공부했습니다. 첫 시작으로 파이썬 책을 통해 기초 문법을 배웠는데요. 이후로는 주로 유데미에서 인터넷 강의를 들었습니다. 나중에 유데미 강의 후기도 적어보겠습니다. 어쨌든 인터넷에 양질의 자료가 많아 혼자서 공부하는 데 큰 무리는 없었다는 말씀을 드리고 싶습니다.

공부가 참 재밌더라고요. 솔직한 심정으로는 계속해서 자기주도적으로, 혼자! 학습하고 싶었습니다. 저는 평소에도 개인주의적 성향이 강한 편이거든요. 하지만 현실적으로 접근하기로 했습니다. 언젠가 취업을 해야 한다면 나와 비슷한 동료들과 커뮤니티를 형성하여 자극받고 성장할 수 있는 환경을 조성하는 것이 좋을 테니까요. 또 팀 프로젝트를 진행하고 포트폴리오를 작성하기 위해서는 개인플레이로는 분명한 한계가 있겠다고 판단이 되었습니다. 그래서 KDT 국비 교육과정을 탐색하기 시작했습니다.

여러 가지 프로그램 중에서 ASAC 빅데이터 과정을 고른 이유는 다음과 같습니다.

  1. 눈살이 찌푸려지는 과대광고를 하지 않는다.
    • 어느 정도 사회생활을 해 보셨다면 다들 아시겠지만, 이런 정부 사업은 돈이 됩니다. 그래서 다양한 기관들이 비슷한 교육과정을 만들고 진행하고 있어요. 대부분의 프로그램들은 지원자를 모집하기 위해 광고를 합니다.
    • 저는 너무 과하게 공격적으로 광고하는 기관들을 선택지에서 배제했습니다. 다들 먹고 살자고 하는 것은 맞긴 하지만, 그래도 본질을 잊고 수익 창출에만 혈안이 된 단체를 신뢰할 수는 없었어요. 내 소중한 6개월!
  2. 온라인이 아닌 9-6 오프라인 교육과정이다.
    • 사람에 따라 온라인 교육을 선호할 수는 있겠습니다만, 저는 온라인 교육과정은 전부 배제했습니다. 저는 경기 외곽에 살기때문에 서울에 있는 어느 과정에 등록해도 출퇴근이 편도 1시간 이상 걸립니다. 그럼에도 불구하고 무조건 오프라인을 원했습니다.
    • 온라인 교육이랍시고 미리 찍어둔 동영상 대충 보게 할 거면, 유데미 강의 듣는 게 낫다고 생각했습니다.
    • 오프라인 과정만이 줄 수 있는 긴장감, 리듬감, 커뮤니티를 원했습니다.
  3. 선발 과정에 면접, 코딩 테스트가 있다.
    • 선별된 인원과 함께할 수 있으니까요! 정말 중요하게 생각했던 부분입니다.
    • 형식적으로나마 면접, 코딩테스트가 있지만 사실상 필터링 없이 누구나 들어갈 수 있는 교육과정은 배제했습니다.

​이러한 이유로 ASAC을 고르게 되었어요. 그렇다고 해서 이 과정에 엄청난 확신이 있었던 건 아니긴 했습니다만, 그래도 느낌이 제일 괜찮았습니다. (...)

 

[2] 면접, 코딩 테스트 후기

테스트는 간단한 파이썬 기본 문법 테스트로 이루어졌습니다. 구글 시트에 설문 형태로 문제가 주어졌고요. 프로그래머스나 리트 코드 같은 코테 문제 풀이는 안 했습니다. 정말 기초 기초 쌩 기초 문법이 전부였어서 평소 코드를 쓰고 공부하는 사람이었으면 틀릴 수가 없는... 그런 쉬운 문제들로만 구성이 되었습니다. 그래서 쉽게 풀었고요. 아마 합격한 분들은 테스트 문제들 거의 다 맞히지 않았을까? 싶어요.

면접은 2인 1조로 봤습니다. 면접관은 두 분이셨고요. 미리 제출했던 지원서를 보면서 이것저것 질문하셨고, 저는 '공부를 혼자서 체계적으로 잘 하고 계시는데 우리 과정에 지원한 이유는 무엇인지', '집이 먼데 출퇴근이 어렵지는 않겠는지', 등등 어려운 질문은 딱히 안 하셔서 솔직하게 답변드리고 잘 마무리했습니다. 

 

[3] 지원서에는 무엇을 썼는가

문항이 세세하게 기억나지는 않습니다만. 다음과 같은 내용을 적었던 것이 기억납니다.

  1. 이직을 결심하게 된 이유, 그리고 데이터 분석에 관심을 가지게 된 이유
  2. 지금까지 혼자서 공부해온 내용 나열
  3. 앞으로 프로그램에 어떤 자세로 얼마나 열심히 참여할 것인지 나의 열정과 포부 보여주기

공부한 내용은 다음과 같이 작성했습니다.

  1. 파이썬
  2. HTML, CSS, JavaScript, Booststrap 5
  3. 파이썬 Numpy, Pandas, Matplotlib, Seaborn 라이브러리
  4. R
  5. MySQL
  6. 깃/깃허브, Linux Commandline
  7. 파이썬 Tensorflow 
  8. 해커 랭크, 프로그래머스 코딩 테스트 병행 중

블로그나 깃허브, 노션 등 개인용 포트폴리오가 있으면 주소를 적으라고 해서 저의 개인 노션 페이지 주소를 첨부했습니다. 저는 노션에 지금까지 제가 공부한 모든 내용을 전부 깔끔하게 정리하고 있습니다. 정리를 하면서 공부하는 것과 아닌 것에는 하늘과 땅만큼의 차이가 있더라고요. 특이한 점은 모든 정리를 영어로 하고 있다는 건데, 아무래도 유데미 강의를 들으며 평소 영어로 공부하다 보니 자연스럽게 그렇게 되었습니다. 이게 어필이 되었을지는 모르겠습니다.

[4] 마무리

비록 경쟁을 통해 선발되어 학생의 입장으로 과정에 임하게 되었긴 하지만, 저 또한 이들이 준비한 교육과정을 평가하는 동등한 관계에 있다고 생각합니다. 그래서 교육에 참여하는 동안 누구보다 열심히 하되, 독학이 낫겠다는 확실한 판단이 서는 순간에는 가차없이 중도 하차할 생각도 하고 있어요. 변태 같지만.... 그럴 일이 없도록 탄탄하고 만족스러운 과정으로 저를 웃고 울게 해 주었으면 좋겠네요 아삭! ^_^

 

앞으로 매주 솔직한 회고를 올리겠습니다 :)

+ Recent posts