2024년 1학기 텍스트 데이터 분석 과목을 수강하면서 진행했던 프로젝트에 대해 리뷰해보려고 한다.
해당 프로젝트는 데이터 분석 프로젝트이고 분야는 NLP, 데이터 분석이 되겠다.
이번 포스팅에서는 주제 선정 배경, 데이터 수집, 데이터 전처리까지의 내용을 담았다.
https://github.com/junhoeKu/Game-Company-Analysis.github.io
주제 선정 배경
처음에 과제를 받았을 때 어떤 주제를 선정해야 할지 막막했다.
어떤 주제든 좋으니 텍스트 데이터 최소 15,000건 이상 수집, 적절한 전처리 기법 적용, 분석 방법 최소 2가지 활용해서 코드와 분석 결과를 담은 보고서를 작성할 것
심지어 동아리 프로젝트, 학회 활동 등 여러 활동과 시기가 겹쳐 절대적으로 시간이 부족했다.
그래서 최대한 단순하게 주제를 잡아보려고 시도했고 팀원과 상의한 끝에 둘 다 좋아하는 게임을 주제로 잡았고나중에 게임사에 취직할 수도 있으니 미리 선행조사 느낌으로 국내 주요 게임사에 대해 조사해보자고 결정하였다.따라서 게임 업계 내에서 각 게임사의 특징, 전략과 각 기업의 사회적 가치 등을 파악하고 게임 업계의 전체적인 동향 분석까지 해보는 것이 이번 프로젝트의 목적이다.
필자가 조사한 기업은 총 5곳 크래프톤, 넥슨, 넷마블, 엔씨소프트, 라이엇이다.선정 기준은 매출액 및 기업 규모를 고려해 선정했고 라이엇이 포함된 이유는 단순히 필자가 롤을 즐겨 하기 때문이다.
데이터 수집
각 게임사의 신작 출시 정보를 비롯한 주가, 실적 등 공식적인 내용들은 뉴스 기사에 대다수 포함될 것으로 판단하였고 게임사별 게임에 대한 사용자들의 리뷰, 채용공고, 복지 등의 비공식적인 내용들은 블로그 포스팅에 포함될 것으로 판단해 뉴스와 블로그 API를 활용해 데이터를 수집하였다.
1) API를 활용한 데이터 수집
네이버 검색 API 일일 제한량이 1000개로 제한되어 있어 검색어를 늘려 데이터를 수집하고자 했다.검색어는 게임사, 게임, 주가, 채용, 복지 키워드를 추가해 구성하였다.아래는 네이버 뉴스 API를 활용한 데이터 수집 코드이다.블로그 데이터 수집은 news url에서 blog url로 수정한 후 예외처리를 살짝 다르게 해서 진행하였다.
import urllib.request
import urllib.parse
## API 인증 정보
client_id =
client_secret =
## 검색 기준 설정
n_display = 100 ## API 최대 요청 가능 수
news_list = [] ## 결과를 저장할 리스트
base_url = 'https://openapi.naver.com/v1/search/news.json' ## 네이버 뉴스 base url
queries_krafton = ['크래프톤', '크래프톤 게임', '크래프톤 주가', '크래프톤 채용', '크래프톤 복지']
queries_nexon = ['넥슨', '넥슨 게임즈', '넥슨 주가', '넥슨 채용', '넥슨 복지']
queries_netmable = ['넷마블', '넷마블 게임', '넷마블 주가', '넷마블 채용', '넷마블 복지']
queries_ncsoft = ['엔씨소프트', '엔씨소프트 게임', '엔씨소프트 주가', '엔씨소프트 채용', '엔씨소프트 복지']
queries_riot = ['라이엇게임즈', '라이엇코리아', '라이엇게임즈 주가', '라이엇게임즈 채용', '라이엇게임즈 복지']
## 각 검색어에 대해 처리
for query in queries_krafton:
start_index = 1
while start_index <= 1000: ## 최대 1000개 기사 수집
encQuery = urllib.parse.quote(query)
url = f"{base_url}?query={encQuery}&display={n_display}&start={start_index}&sort=sim"
request = urllib.request.Request(url)
request.add_header("X-Naver-Client-Id", client_id)
request.add_header("X-Naver-Client-Secret", client_secret)
with urllib.request.urlopen(request) as response:
rescode = response.getcode()
if rescode == 200:
response_body = response.read()
search_results = eval(response_body.decode('utf-8'))
items = search_results['items']
## 네이버 뉴스만 추출 --> 'naver.com'를 포함하는 경우만
for item in items:
if 'naver.com' in item['link']:
news_list.append(item)
else:
print(f"Error Code: {rescode}")
start_index += n_display ## 다음 페이지 인덱스 증가
## 수집된 기사 수 출력
print(f"Collected {len(news_list)} articles that include 'naver.com' in the link.")
2) 데이터프레임화 및 데이터 전처리
API를 활용해 각 게임사별 최소 5,000개 이상의 데이터를 수집하였고 조사한 게임사가 5곳이니 약 25,000개 이상의 데이터를 수집하였다. 하지만 링크가 중복되는 경우나 너무 오래된 기사, 의미 없는 글 등은 제거해 데이터 품질을 올려야 한다.
아래는 데이터프레임화 및 데이터 전처리하는 코드이다.
import pandas as pd
pd.set_option('display.max_colwidth', 1000) ## 최대 1000개의 텍스트까지 보는 코드
news = pd.DataFrame(news_list)
blog = pd.DataFrame(blog_list)
news.shape, blog.shape
## 중복 링크 제거 및 링크 column 클렌징
news = news.drop_duplicates(subset='link', keep='first')
news.link = news.link.apply(lambda x : str(x).replace('\\', ''))
blog = blog.drop_duplicates(subset='link', keep='first')
blog.link = blog.link.apply(lambda x : str(x).replace('\\', ''))
## 뉴스와 블로그 중 너무 오래된 글은 제거하고자 2015년 이상의 글만 수집
news.pubDate = news['pubDate'].apply(lambda x: pd.to_datetime(x))
news['year'] = news.pubDate.dt.year
blog.postdate = blog['postdate'].apply(lambda x: pd.to_datetime(x))
blog['year'] = blog.postdate.dt.year
news = news.loc[news.year >= 2015]
blog = blog.loc[blog.year >= 2015]
3) Selenium을 활용한 추가 데이터 수집
네이버 검색 API를 통한 데이터 수집은 본문 내용에 있어서 길이 제한 때문에 모든 정보를 가져올 수 없다.
따라서 본문 내용 전체를 수집하기 위해서 Selenium 패키지를 활용해 추가 데이터 수집하였다.
아래는 Selenium을 활용한 데이터 수집 코드이다.
blog 본문도 url과 태그만 조금 수정해 그대로 사용하였다.
progress 변수로 현재 어느 정도 진척이 되고 있는지 나타내었다. (6시간 동안 노트북 끄지도 못하고 이동도 못하고.. ㅠ)
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
import random
driver = webdriver.Chrome()
driver.implicitly_wait(3)
news_contents = []
total_links = len(news.link) ## 전체 링크 개수
for index, i in enumerate(news.link):
driver.get(i)
time.sleep(random.uniform(1, 2)) ## 1~2초 사이의 균등분포로 time sleep 설정
progress = (index + 1) / total_links * 100 ## 진행 정도를 알아보고자 progress 변수 생성
## 페이지 장르(경제 / 스포츠 등)에 따라서 태그가 달라지는 경우가 있어 다르게 크롤링 진행
try:
if 'n.news.naver.com' in i:
tmp = driver.find_elements(By.ID, 'newsct_article') ## 태그 탐색
news_contents.append(tmp[0].text) if tmp else news_contents.append('사라진 페이지')
else:
tmp = driver.find_elements(By.ID, 'comp_news_article') ## 태그 탐색
news_contents.append(tmp[0].text) if tmp else news_contents.append('사라진 페이지')
except IndexError:
news_contents.append('사라진 페이지')
print(f"Progress: {progress:.2f}% Complete") ## 진행 정도 출력
driver.quit()
print("본문 크롤링이 완료되었습니다.")
news['body'] = news_contents ## news 데이터프레임의 body column에 내용을 할당
데이터 전처리
텍스트 데이터 전처리할 때 어떤 전처리 기법이 있는지 궁금한 사람은 아래 링크를 참고하길 바란다.
https://dangingsu.tistory.com/26
1) Data Load & Data Merge
데이터를 모두 불러온 다음 게임사별 분석을 진행하기 위해 라벨링 작업을 추가하였다.그리고 연도별 게임사 분석이나 월별 분석이 어떻게 달라지는 지도 알아보고자 초기에 Date 변수도 포함하였는데 이 부분은 슬라이싱에서 제거하였다.
import pandas as pd
import re
pd.set_option('display.max_colwidth', 200)
krafton_news = pd.read_csv('krafton_news.csv')
krafton_blog = pd.read_csv('krafton_blog.csv')
nexon_news = pd.read_csv('nexon_news.csv')
nexon_blog = pd.read_csv('nexon_blog.csv')
ncsoft_blog = pd.read_csv('ncsoft_blog.csv')
ncsoft_news = pd.read_csv('ncsoft_news.csv')
netmarble_blog = pd.read_csv('netmable_blog.csv')
netmarble_news = pd.read_csv('netmable_news.csv')
riot_blog = pd.read_csv('riot_blog.csv')
riot_news = pd.read_csv('riot_news.csv')
## 각 게임사별 라벨링 작업 진행
krafton_news['category'] = 'krafton_news'
krafton_blog['category'] = 'krafton_blog'
nexon_news['category'] = 'nexon_news'
nexon_blog['category'] = 'nexon_blog'
ncsoft_news['category'] = 'ncsoft_news'
ncsoft_blog['category'] = 'ncsoft_blog'
netmarble_news['category'] = 'netmarble_news'
netmarble_blog['category'] = 'netmarble_blog'
riot_news['category'] = 'riot_news'
riot_blog['category'] = 'riot_blog'
## 각 게임사별 데이터프레임 concat을 위해 slicing
krafton_news = krafton_news[['title', 'body', 'link', 'category']]
krafton_blog = krafton_blog[['title', 'body', 'link', 'category']]
nexon_news = nexon_news[['title', 'body', 'link', 'category']]
nexon_blog = nexon_blog[['title', 'body', 'link', 'category']]
ncsoft_news = ncsoft_news[['title', 'body', 'link', 'category']]
ncsoft_blog = ncsoft_blog[['title', 'body', 'link', 'category']]
netmarble_news = netmarble_news[['title', 'body', 'link', 'category']]
netmarble_blog = netmarble_blog[['title', 'body', 'link', 'category']]
riot_news = riot_news[['title', 'body', 'link', 'category']]
riot_blog = riot_blog[['title', 'body', 'link', 'category']]
df = pd.concat([krafton_news, krafton_blog, nexon_news, nexon_blog,
ncsoft_news, ncsoft_blog, netmarble_news, netmarble_blog,
riot_news, riot_blog], axis = 0)
2) Data Preprocessing
전처리 기법으로는 불용어 제거, 결측치 제거, 중복링크 제거, 클렌징, 정규화 등을 적용하였다.
더 세부적인 각 작업들은 주석처리하였으니 참고하고 아래는 전처리 코드이다.화살표는 상승, 하락의 의미를 담고 있고 상승과 하락은 긍정 / 부정의 중요한 표현이기에 남겨놓았다.
## 결측치 제거
df = df.loc[(df.title.notnull()) & (df.body.notnull())]
## 문서의 내용이 없는 경우 제거
df = df.loc[df.body != '사라진 페이지']
## title 불용어 제거
pattern1 = re.compile(r'<[^>]*>')
df.title = df.title.apply(lambda x: pattern1.sub('', x)) ## <b> 태그 제거
df.title.replace('↑', '상승', regex = True, inplace = True)
df.title.replace('quot', ' ', regex = True, inplace = True) ## 인용구 제거
df.title.replace('amp', ' ', regex = True, inplace = True) ## & 마크 제거
df.title = df.title.str.replace('[^가-힣0-9a-zA-Z.,?!()\[\] ]', '', regex=True) ## 한글, 숫자, 영어, 공백, 문장부호, 괄호까지만 남기고 제거
df.title = df.title.str.replace(r'http[^[\(\sㄱ-ㅎㅏ-ㅣ가-힣]+', ' ', regex=True) ## 제목에 http로 시작하는 링크가 있는 경우 제거
df.title = df.title.str.replace(r'www[^[\(\sㄱ-ㅎㅏ-ㅣ가-힣]+', ' ', regex=True) ## 제목에 www로 시작하는 링크가 있는 경우 제거
df.title = df.title.str.replace(r'blog[^[\(\sㄱ-ㅎㅏ-ㅣ가-힣]+', ' ', regex=True) ## 제목에 blog로 시작하는 링크가 있는 경우 제거
df.title = df.title.apply(lambda x: re.sub(r'\(\s*\)', ' ', x)) ## 텍스트 없이 괄호만 남은 경우에 괄호 제거
## body 불용어 제거
df.body.replace('\n', ' ', regex = True, inplace = True) ## 줄바꿈 문자 제거
df.body.replace('↑', '상승', regex = True, inplace = True)
df.body.replace('↓', '하락', regex = True, inplace = True)
df.body = df.body.str.replace('[^가-힣0-9a-zA-Z.,?!%()\[\] ]', ' ', regex=True) ## 한글, 숫자, 영어, 공백, 문장부호, 괄호까지만 남기고 제거
df.body = df.body.str.replace(r'http[^[\(\sㄱ-ㅎㅏ-ㅣ가-힣]+', ' ', regex=True) ## 제목에 http로 시작하는 링크가 있는 경우 제거
df.body = df.body.str.replace(r'www[^[\(\sㄱ-ㅎㅏ-ㅣ가-힣]+', ' ', regex=True) ## 제목에 www로 시작하는 링크가 있는 경우 제거
df.body = df.body.str.replace(r'blog[^[\(\sㄱ-ㅎㅏ-ㅣ가-힣]+', ' ', regex=True) ## 제목에 blog로 시작하는 링크가 있는 경우 제거
df.body = df.body.apply(lambda x: re.sub(r'\(\s*\)', ' ', x)) ## 텍스트 없이 괄호만 남은 경우에 괄호 제거
## 게임사별 중복 링크 제거
## 첫 번째 값을 남기고 나머지를 제거했는데 먼저 나올수록 연관도가 높은 글이라고 생각해 이렇게 전처리했습니다
df = df.drop_duplicates(subset='link', keep='first')
## '라이엇 게임즈'랑 '라이엇게임즈'가 같은 의미라서 통합 작업
df['title'] = df.title.replace('라이엇게임즈', '라이엇 게임즈', regex = True)
df['body'] = df.body.replace('라이엇게임즈', '라이엇 게임즈', regex = True)
이상으로 게임사 텍스트 분석 프로젝트에 대한 첫 번째 포스팅을 마치려고 한다.
다음 포스팅은 실질적인 분석 내용을 담을 예정이다.
'프로젝트' 카테고리의 다른 글
[프로젝트] 2024 하반기 ICT 학점연계 프로젝트 인턴십 합격 후기 (0) | 2024.08.14 |
---|---|
[프로젝트] 국내 주요 게임사 텍스트 데이터 분석 프로젝트 리뷰 (2) (4) | 2024.07.12 |
[프로젝트] 자전거 교통사고 원인 분석 프로젝트 리뷰 (0) | 2024.07.09 |
[프로젝트] 딥러닝을 활용한 치매 예측 및 예방 프로젝트 리뷰 (0) | 2024.07.04 |
[프로젝트] 데이콘 주관 도배하자 질의응답 처리 언어모델 개발 공모전 리뷰 (2) | 2024.02.26 |