파이썬 requests, BeautifulSoup 라이브러리를 이용한 웹크롤링 후 데이터분석 실습을 해보았습니다 :-)
연금복권720+은 제가 한달에 2-3회정도 꾸준하게 구매하는 최애 복권인데요. 슬프게도 지금까지 제대로 당첨된 적은 단 한번도 없지만, 앞으로도 저는 꾸준히 구매를 할 예정인 아주아주 매력적인 복권입니다. 1등에 당첨이 되면 (세전) 700만원을 매월 20년동안 수령할 수 있어요. 동행복권 온라인 사이트에서 간단히 온라인 구매를 할 수도 있구요. 1등 번호는 온라인 1명, 오프라인 1명 총 2명이 당첨될 수 있습니다. 자세한 복권 구조는 동행복권 홈페이지를 참고해 보시구요.
복권의 경우 통계를 공부해보신 분들께는 아주 친숙한 소재이실텐데요. (저는 아닙니다.ㅋㅋㅋ) 동행복권 사이트에서는 복권 당첨번호를 엑셀파일로도 제공하고 통계 자료를 따로 분석해서 메뉴도로 제공하고 있습니다. 다만 저는 철저히 requests 라이브러리를 이용한 웹크롤링에 익숙해지기 위해서 엑셀 파일이나 통계자료를 건드리지 않고 처음부터 끝까지 혼자 힘으로 본 실습을 했습니다!
[참고] 본 포스팅은 수리링 본인의 공부 기록을 목적으로 작성하였습니다. 해당 라이브러리에 대해 전혀 모르시는 분께서 보면서 따라하시기엔 많이 불친절하게 느껴질 수 있습니다. 참고하시고 봐 주시면 감사드리겠습니다 :-)
[참고] 본 포스팅은 책, 강의, 다른 사람의 포스팅을 참고하지 않은 스스로의 창작물입니다! 참고하여 포스팅 하시는 경우 출처 밝혀주심 감사드리겠습니다!
[실습 목차]
- 206회차로 모의 실습
- 원하는 회차 구간을 입력하면 모든 정보를 담아 데이터프레임으로 리턴하는 함수 작성
- 데이터프레임으로 간단한 데이터분석 (은근 재밌으니 귀찮으시면 이것만 보고 가세요...^^)
1-1. 숨은 URL 찾아내기
동행복권 사이트의 회차별 당첨번호 페이지(클릭)에 가 봅니다.
회차 바로가기 메뉴를 통해 원하는 회차를 선택해서 당첨 결과를 볼 수 있었습니다.
그런데 기본 URL에 회차 정보가 드러나지 않고 숨어 있어요. 206회를 조회해도, 200회를 조회해도 계속 같은 URL이 유지됩니다. 따라서 회차를 특정하여 정보를 뽑아낼 수가 없는 상황입니다. 우리는 회차를 조회할 수 있는 상세URL을 알아내야 해요.
문제상황을 해결하기 위해 크롬 웹브라우저의 inspection(개발자 도구) 메뉴의 Network 탭을 확인해 봅시다.
위와 같이 네트워크 탭을 켜둔 상태로 조회 버튼을 눌러봅니다. Name 탭의 맨 첫 번째 gameResult 어쩌구를 클릭한 다음 Payload를 확인합니다. (누가 봐도 수상한) Round: 206 이라는 정보를 확인했습니다. 기존 url 뒤에 &Round=206을 붙여 주면 될 것 같다는 합리적 의심을 해봅니다.
주소 뒤에 &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에 이어서 두 번째로 나온 횟수가 적네요. 전략을 바꿔야 하나.... 고민이 되지만 (ㅋㅋㅋㅋㅋㅋㅋㅋㅋ) 복권 통계는 재미일 뿐이라고 생각합니다.
여기까지 간단 분석을 마쳐 보겠습니다! :-)
감사합니다.
'Data Science > ML 머신러닝' 카테고리의 다른 글
ML | 캐글 Kaggle 신용카드 데이터 EDA + 모델링 실습 (0) | 2024.05.10 |
---|---|
ML | 파이썬 scikitlearn XGBoost 래퍼 클래스 - XGBoostClassifier (0) | 2024.05.08 |
ML | 파이썬 XGBoost API 사용하여 위스콘신 유방암 예측하기 (0) | 2024.05.08 |
EDA | 서울특별시 공중화장실 02 _ 태블로를 이용해 시각화하기 (0) | 2024.04.17 |
EDA | 서울특별시 공중화장실 01 _ pandas를 이용한 공공데이터 정제, 전처리하기 (1) | 2024.04.16 |