import pandas as pd
import requests
import json
import plotly.express as px
import cufflinks as cf
from tqdm import tqdm_notebook
from bs4 import BeautifulSoup
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
cf.go_offline(connected=True)
plt.rcParams.update({
'font.family': 'AppleGothic',
'font.size': 14,
'figure.figsize': (20, 10),
})
access_token 은 아래 링크에 들어가시면 얻을 수 있습니다.
본인의 tistory app_id 와 callback_url 을 넣은 뒤에 사용하세요.
일정 시간 주기로 토큰이 파기되니 데이터 다시 로드할 때마다 다시 받아와야 합니다 ㅠㅠ (좀 귀찮.. 하지만 금방 함)
참고 :
# 위에서 얻은 토큰 값을 아래 변수에 넣어서 주석 푼 뒤 사용!
# access_token =
def get_posts_list(page):
url = "https://www.tistory.com/apis/post/list"
params = {
'output': 'json',
'access_token': access_token,
'blogName': 'dailyheumsi',
'page': page
}
return requests.get(url, params)
df_posts = pd.DataFrame()
page = 1
while True:
res = get_posts_list(page)
if res.status_code != 200:
break
res = json.loads(res.content)
if 'posts' not in res['tistory']['item']:
break
posts = res['tistory']['item']['posts']
df_posts = df_posts.append(posts, ignore_index=True)
page += 1
# 데이터 확인
df_posts.head()
# 자료형을 살펴보면 다음과 같다.
df_posts.info()
# 일부 컬럼들의 자료형을 바꿔준다.
df_posts['id'] = df_posts['id'].astype('int')
df_posts['date'] = pd.to_datetime(df_posts['date'])
df_posts[['comments', 'trackbacks']] = df_posts[['comments', 'trackbacks']].astype('int')
df_posts[['visibility', 'categoryId']] = df_posts[['visibility', 'categoryId']].astype('category')
# 공개된 글 데이터만 남겨둠.
df_posts = df_posts[df_posts['visibility'] == '20']
len(df_posts)
총 184개의 글을 올렸음.
df_posts['year'] = df_posts['date'].dt.year
df_posts['month'] = df_posts['date'].dt.month
df_posts['day'] = df_posts['date'].dt.day
# 2018-08 ~ 2020-02 까지의 데이터프레임을 하나 만들어 둠.
size_by_month = pd.DataFrame({'date': pd.date_range('2018-08', '2020-03', freq='m')})
size_by_month['year'] = size_by_month['date'].dt.year
size_by_month['month'] = size_by_month['date'].dt.month
size_by_month.drop('date', 1, inplace=True)
# 위에서 구한 df_posts 를 위 데이터프레임에 합침.
tmp = df_posts.groupby(['year', 'month']).size().reset_index()
tmp.columns = ['year', 'month', '글 개수']
size_by_month = pd.merge(size_by_month, tmp, how='left').fillna(0)
# 년/월별 글 개수를 시각화 해보자.
tmp = pd.DataFrame()
tmp['년/월'] = size_by_month.apply(lambda x: "%d/%d" %(x['year'], x['month']), axis=1)
tmp['글 개수'] = size_by_month['글 개수']
tmp.set_index('년/월', inplace=True)
tmp.iplot(mode='markers+lines')
각 글들을 카테고리로 나눠서 보면??
먼저 카테고리 id만 있으므로, 카테고리 관련 데이터를 받아오자.
def get_category_list():
url = "https://www.tistory.com/apis/category/list"
params = {
'output': 'json',
'access_token': access_token,
'blogName': 'dailyheumsi',
}
return requests.get(url, params)
# category data 를 받아옴.
res = get_category_list()
res = json.loads(res.content)
df_categories = pd.DataFrame(res['tistory']['item']['categories'])
df_categories.head()
df_categories.set_index('id', inplace=True)
categories = [] # ['id', 'category_1', 'category_2'] 의 페어 리스트를 담음.
for idx, row in df_categories.iterrows():
if row['parent'] == '':
categories.append([idx, row['name'], row['name']])
else:
categories.append([idx, df_categories.loc[row['parent'], 'name'], row['name']])
# category 와 id 를 담는 데이터프레임을 다시 구성
df_categories = pd.DataFrame(categories, columns=['categoryId', 'category_1', 'category_2'])
df_categories.head()
# 카테고리1 (큰 카테고리) 에 해당하는 카테고리 수
df_categories['category_1'].nunique()
# 카테고리2 (작은 카테고리) 에 해당하는 카테고리 수
df_categories['category_2'].nunique()
# category 데이터프레임을 기존 posts 데이터프레임과 합침.
df_posts = pd.merge(df_posts, df_categories, how='left')
df_posts.head()
이제 포스트 데이터에 카테고리가 추가되었으므로, 월별 카테고리 글 개수를 살펴보자.
# 2018-08 ~ 2020-02 까지의 데이터프레임을 하나 만들어 둠.
size_by_month = pd.DataFrame({'date': pd.date_range('2018-08', '2020-03', freq='m')})
size_by_month['year'] = size_by_month['date'].dt.year
size_by_month['month'] = size_by_month['date'].dt.month
size_by_month.drop('date', 1, inplace=True)
pvt = df_posts.pivot_table(index=['year', 'month'], columns='category_1', aggfunc='size', fill_value=0).reset_index()
size_by_month = pd.merge(size_by_month, pvt, how='left').fillna(0)
size_by_month.set_index(['year', 'month'], inplace=True)
size_by_month.iplot('area', fill=True, )
혹시 내가 자주 글을 쓰는 시간대가 있었을까?
df_posts['hour'] = df_posts['date'].dt.hour
df_posts.groupby('hour').size().iplot('bar')
카테고리별로 비율을 보면?
pvt = df_posts.pivot_table(index='hour', columns='category_1', aggfunc='size', fill_value=0)
pvt = pvt.div(pvt.sum(axis=1), axis=0)
pvt.iplot('bar', barmode='stack')
tmp = df_posts.groupby('category_1').size().reset_index()
tmp.columns = ['category', 'size']
tmp.iplot('pie', labels='category', values='size', hole=.4)
tmp = df_posts.groupby(['category_1', 'category_2']).size().reset_index()
tmp.rename({0: '포스팅 수'}, axis=1, inplace=True)
categories = ['취업과 기본기 튼튼', '공부하며 적어놓기 1', '공부하며 적어놓기 2']
tmp = tmp[tmp['category_1'].isin(categories)]
grp = tmp.groupby('category_1')
for grp_name, grp_df in grp:
size_by_cat2 = grp_df.groupby('category_2').sum().reset_index()
size_by_cat2.iplot(kind='pie', labels='category_2', values='포스팅 수', title=grp_name, hole=0.4)
원래는 아래 코드와 같이 파이 그래프 3개를 하나로 묶어서 표현하려고 했으나,
plotly subplots 에는 각각 파이마다 레전드를 달 수가 없음 (현재 지원 x)
따라서 위 같이 그냥 하나씩 그린 후에 그냥 포토샵으로 묶어야 겠다...._
엑셀로 그리니까 편하다 ㅠㅠㅠㅠ
아래 코드들 다 안씀.
# grp = tmp.groupby('category_1')
# plots = []
# for grp_name, grp_df in grp:
# size_by_cat2 = grp_df.groupby('category_2').sum().reset_index()
# plots.append(size_by_cat2.figure(kind='pie', labels='category_2', values='포스팅 수')['data'][0])
# from plotly.subplots import make_subplots
# fig = make_subplots(rows=1, cols=3, specs=[[{'type':'domain'}, {'type':'domain'}, {'type':'domain'}]]) # pie 라서 ..
# fig.add_traces(plots, rows=[1,1,1], cols=[1,2,3])
# fig.update_traces(hole=.4)
# fig.show()
# tmp = tmp[tmp['category_1'] != tmp['category_2']]
# category_1 = tmp['category_1'].unique().tolist()
# category_2 = tmp['category_2'].tolist()
# parents = len(category_1)*[""] + tmp['category_1'].tolist()
# # values = tmp.groupby('category_1')['포스팅 수'].sum().tolist() + tmp['포스팅 수'].tolist()
# values = len(category_1)*[0] + tmp['포스팅 수'].tolist()
# fig = go.Figure(go.Sunburst(
# labels=category_1+category_2,
# parents=parents,
# values=values
# ))
# fig.update_layout(margin = dict(t=0, l=0, r=0, b=0))
# fig.show()
먼저 각 포스팅 데이터를 받아오자
def get_post_content(post_id):
url = "https://www.tistory.com/apis/post/read"
params = {
'output': 'json',
'access_token': access_token,
'blogName': 'dailyheumsi',
'postId': post_id,
}
return requests.get(url, params)
# id 0 부터 끝까지 넣어보며 post 받아오기.
# 나는 마지막 포스팅 id 가 204 임을 확인함
detail_posts = []
for post_id in tqdm_notebook(range(205)):
res = get_post_content(post_id)
if res.status_code != 200:
continue
res = json.loads(res.content)
if res['tistory']['item']['visibility'] != '20':
continue
content = res['tistory']['item']['content']
detail_posts.append([post_id, content])
df_detail_posts = pd.DataFrame(detail_posts, columns=['id', 'content'])
df_detail_posts.head()
df_detail_posts.info()
# content 에서 html tag 모두 제거
def remove_html(html):
bs = BeautifulSoup(html)
return bs.get_text()
df_detail_posts['content'] = df_detail_posts['content'].apply(lambda x: remove_html(x))
df_detail_posts['content'] = df_detail_posts['content'].str.replace('\n', ' ')
# 이를 다시 df_posts 로 모아주자.
df_posts = pd.merge(df_posts, df_detail_posts, how='left')
근데 이후에 결국에 글 내용은 결국 안쓰고, 타이틀만 씀...
비율이 가장 높았던 3개의 카테고리 내 포스팅들은 어떤 주제를 가지고 있나 살펴보자.
categories = [
'공부하며 적어놓기 1',
'공부하며 적어놓기 2',
'취업과 기본기 튼튼'
]
def get_tokenized_corpus(corpus, tagger):
# 참고: https://ratsgo.github.io/korean%20linguistics/2017/03/15/words/#%EC%A3%BC%EA%B2%A9%EC%A1%B0%EC%82%AC
stopwords = list("[]().,[email protected]#$%^&*~+-/<>\n") + list(map(str, range(10))) + list("은는이가을를의과와만도로의에") + ['\xa0']
tokenized_corpus = []
for title in corpus:
# words = tagger.nouns(title) # <- 1) 성능이 너무 안나온다.
words = []
for word, tag in tagger.pos(title):
if tag in ['Foreign', 'Alpha', 'Noun']: # <- 2) 따라서 수동으로 단어들을 찾아냄.
words.append(word)
words = [word for word in words if word not in stopwords]
tokenized_corpus.append(words)
return tokenized_corpus
def draw_word_score_heatmap(topn, cmap, num_topic, lda_model, dictionary):
word_score = []
for topic_id in range(num_topic):
for word_id, score in lda_model.get_topic_terms(topic_id, topn=topn):
word_score.append([topic_id, dictionary[word_id], score])
df_topic_words = pd.DataFrame(word_score, columns=['topic_id', 'word', 'score'])
# 참고 : https://brunch.co.kr/@goodvc78/13#comment
# 참고에 해당하는 topic - score heatmap 을 만들어보려함.
labels, scores = [], []
for grp_name, grp_df in df_topic_words.groupby('topic_id'):
grp_df.sort_values('score', inplace=True, ascending=False)
labels.append(grp_df['word'].tolist())
scores.append(grp_df['score'].tolist())
tmp = pd.DataFrame(scores)
plt.figure()
ax = sns.heatmap(tmp, cmap=cmap, square=True, annot=np.array(labels), fmt='', cbar_kws={"shrink": 0.5})
plt.show()
from konlpy.tag import Okt
from gensim import corpora
from gensim.models import LdaModel
cmaps = ['Oranges', 'Blues', 'Wistia']
plots = []
for category, cmap in zip(categories, cmaps):
print(category)
# 해당 카테고리에 해당하는 데이터만 가져오기.
df_category = df_posts[df_posts['category_1'] == category]
df_category.reset_index(drop=True, inplace=True)
# title 를 기준으로 tokenized 된 corpus 얻기.
corpus = get_tokenized_corpus(df_category['title'], Okt())
# lda 모델링 전 데이터 전처리.
dictionary = corpora.Dictionary(corpus)
corpus = [dictionary.doc2bow(words) for words in corpus]
# 토픽 수는 하위 카테고리 수 만큼.
num_topics = df_category['category_2'].nunique()
# LDA 모델 구축.
lda = LdaModel(corpus, num_topics=num_topics, id2word=dictionary, passes=10)
# 토픽별 word-score 를 히트맵으로 그리기.
draw_word_score_heatmap(10, cmap, num_topics, lda, dictionary)
구글 애널리틱스에서 받은 데이터를 불러오자.
df_pv = pd.read_excel('data/ga_pv.xlsx', sheet_name="데이터세트1")
df_pv.head()
df_pv.rename({'페이지': 'id'}, axis=1, inplace=True)
df_pv['id'] = df_pv['id'].str.replace('/', '')
tmp = df_posts[['id', 'title', 'category_1', 'category_2', 'comments']]
tmp['id'] = tmp['id'].apply(str)
df_pv = pd.merge(df_pv,
tmp,
how='inner')
df_pv.head()
df_pv.shape
PV 로만 살펴보면
topn = 10
df_pv_topn = df_pv.sort_values('페이지뷰 수', ascending=False)[:topn]
df_pv_topn.set_index('title')['페이지뷰 수'].sort_values().iplot('barh')
수치형 변수들간의 상관관계를 살펴보면
df_pv.corr()
별로 유의미한 관계는 안보이네.
PV 를 카테고리별로 묶어서 봐보면 좀 압도적으로 차지하는 카테고리가 있을까?
tmp = df_pv.groupby('category_1')['페이지뷰 수'].sum().reset_index()
tmp.iplot('pie', labels='category_1', values='페이지뷰 수')
# tmp.iplot('barh')
tmp = df_pv.groupby('category_2')['페이지뷰 수'].sum().reset_index()
tmp.iplot('pie', labels='category_2', values='페이지뷰 수')