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()
id | title | postUrl | visibility | categoryId | comments | trackbacks | date | |
---|---|---|---|---|---|---|---|---|
0 | 205 | [취준생의 데이터 분야의 커리어 고민 3] 엔지니어가 되자 | https://dailyheumsi.tistory.com/205 | 20 | 864097 | 4 | 0 | 2020-03-01 21:07:57 |
1 | 204 | [취준생의 데이터 분야의 커리어 고민 2] 분석으로 취업은 힘들다 | https://dailyheumsi.tistory.com/204 | 20 | 864097 | 7 | 0 | 2020-02-26 18:07:20 |
2 | 203 | 스프링 부트를 활용한 간단한 웹 사이트 | https://dailyheumsi.tistory.com/203 | 20 | 801880 | 0 | 0 | 2020-02-24 00:55:55 |
3 | 202 | [스프링 프레임워크 핵심 기술] AOP | https://dailyheumsi.tistory.com/202 | 20 | 874866 | 0 | 0 | 2020-02-23 23:36:01 |
4 | 201 | [디자인 패턴 9편] 구조 패턴, 프록시(Proxy) | https://dailyheumsi.tistory.com/201 | 20 | 855210 | 0 | 0 | 2020-02-23 21:49:14 |
# 자료형을 살펴보면 다음과 같다.
df_posts.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 193 entries, 0 to 192 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 193 non-null object 1 title 193 non-null object 2 postUrl 193 non-null object 3 visibility 193 non-null object 4 categoryId 193 non-null object 5 comments 193 non-null object 6 trackbacks 193 non-null object 7 date 193 non-null object dtypes: object(8) memory usage: 12.2+ KB
# 일부 컬럼들의 자료형을 바꿔준다.
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
총 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()
id | name | parent | label | entries | entriesInLogin | |
---|---|---|---|---|---|---|
0 | 804355 | 언젠가 또 기억하고 싶은거 | 864096 | 일상, 생각, 경험/언젠가 또 기억하고 싶은거 | 1 | 1 |
1 | 864096 | 일상, 생각, 경험 | 일상, 생각, 경험 | 10 | 10 | |
2 | 854906 | 데이터 시각화 | 815369 | 공부하며 적어놓기 1/데이터 시각화 | 13 | 14 |
3 | 855210 | 빽 투더 기본기 | 800526 | 취업과 기본기 튼튼/빽 투더 기본기 | 29 | 29 |
4 | 882621 | 자바로 개발하기 | 882620 | 공부하며 적어놓기 2/자바로 개발하기 | 2 | 3 |
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()
categoryId | category_1 | category_2 | |
---|---|---|---|
0 | 804355 | 일상, 생각, 경험 | 언젠가 또 기억하고 싶은거 |
1 | 864096 | 일상, 생각, 경험 | 일상, 생각, 경험 |
2 | 854906 | 공부하며 적어놓기 1 | 데이터 시각화 |
3 | 855210 | 취업과 기본기 튼튼 | 빽 투더 기본기 |
4 | 882621 | 공부하며 적어놓기 2 | 자바로 개발하기 |
# 카테고리1 (큰 카테고리) 에 해당하는 카테고리 수
df_categories['category_1'].nunique()
8
# 카테고리2 (작은 카테고리) 에 해당하는 카테고리 수
df_categories['category_2'].nunique()
20
# category 데이터프레임을 기존 posts 데이터프레임과 합침.
df_posts = pd.merge(df_posts, df_categories, how='left')
df_posts.head()
id | title | postUrl | visibility | categoryId | comments | trackbacks | date | year | month | day | category_1 | category_2 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 205 | [취준생의 데이터 분야의 커리어 고민 3] 엔지니어가 되자 | https://dailyheumsi.tistory.com/205 | 20 | 864097 | 4 | 0 | 2020-03-01 21:07:57 | 2020 | 3 | 1 | 일상, 생각, 경험 | 그냥 얘기 |
1 | 204 | [취준생의 데이터 분야의 커리어 고민 2] 분석으로 취업은 힘들다 | https://dailyheumsi.tistory.com/204 | 20 | 864097 | 7 | 0 | 2020-02-26 18:07:20 | 2020 | 2 | 26 | 일상, 생각, 경험 | 그냥 얘기 |
2 | 203 | 스프링 부트를 활용한 간단한 웹 사이트 | https://dailyheumsi.tistory.com/203 | 20 | 801880 | 0 | 0 | 2020-02-24 00:55:55 | 2020 | 2 | 24 | 프로젝트들 | 프로젝트들 |
3 | 202 | [스프링 프레임워크 핵심 기술] AOP | https://dailyheumsi.tistory.com/202 | 20 | 874866 | 0 | 0 | 2020-02-23 23:36:01 | 2020 | 2 | 23 | 공부하며 적어놓기 2 | 웹 백엔드 with 자바 |
4 | 201 | [디자인 패턴 9편] 구조 패턴, 프록시(Proxy) | https://dailyheumsi.tistory.com/201 | 20 | 855210 | 0 | 0 | 2020-02-23 21:49:14 | 2020 | 2 | 23 | 취업과 기본기 튼튼 | 빽 투더 기본기 |
이제 포스트 데이터에 카테고리가 추가되었으므로, 월별 카테고리 글 개수를 살펴보자.
# 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])
HBox(children=(IntProgress(value=0, max=205), HTML(value='')))
df_detail_posts = pd.DataFrame(detail_posts, columns=['id', 'content'])
df_detail_posts.head()
id | content | |
---|---|---|
0 | 4 | <p>1 * 2 * .. * n 과 같은 꼴을 팩토리얼이라 하고, 기호로 n! 이라... |
1 | 5 | <p cid="n0" mdtype="paragraph" class="md-end-b... |
2 | 6 | <p>당신이 컴퓨터공학과 출신이거나 혹은 신입 개발자 취업 준비를 해왔다면, 수 많... |
3 | 8 | <p>만약에, 면접장에서 큐를 구현해보라는 말을 들으면 어떻게 해야할까? 이런 기본... |
4 | 9 | <p>[##_Image|kage@JBi9h/btqzyhu4JHV/S8kXHSV2Rk... |
df_detail_posts.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 184 entries, 0 to 183 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 184 non-null int64 1 content 184 non-null object dtypes: int64(1), object(1) memory usage: 3.0+ KB
# 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', ' ')
/Users/heumsi/anaconda3/envs/py36/lib/python3.6/site-packages/bs4/__init__.py:181: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently. The code that caused this warning is on line 193 of the file /Users/heumsi/anaconda3/envs/py36/lib/python3.6/runpy.py. To get rid of this warning, change code that looks like this: BeautifulSoup(YOUR_MARKUP}) to this: BeautifulSoup(YOUR_MARKUP, "lxml")
# 이를 다시 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("[]().,?!@#$%^&*~+-/<>\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)
공부하며 적어놓기 1
공부하며 적어놓기 2
취업과 기본기 튼튼
구글 애널리틱스에서 받은 데이터를 불러오자.
df_pv = pd.read_excel('data/ga_pv.xlsx', sheet_name="데이터세트1")
df_pv.head()
페이지 | 페이지뷰 수 | 순 페이지뷰 수 | 평균 페이지에 머문 시간 | 방문수 | 이탈률 | 종료율(%) | 페이지 값 | |
---|---|---|---|---|---|---|---|---|
0 | /33 | 12220 | 7687 | 42.677364 | 7666 | 0.472998 | 0.617512 | 0 |
1 | /36 | 10331 | 5862 | 49.533925 | 5670 | 0.335979 | 0.542058 | 0 |
2 | /67 | 6175 | 3455 | 50.803914 | 3434 | 0.326150 | 0.553198 | 0 |
3 | /105 | 5871 | 2582 | 74.029271 | 2552 | 0.160658 | 0.429739 | 0 |
4 | /85 | 4867 | 2710 | 137.404666 | 2584 | 0.400542 | 0.489213 | 0 |
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()
id | 페이지뷰 수 | 순 페이지뷰 수 | 평균 페이지에 머문 시간 | 방문수 | 이탈률 | 종료율(%) | 페이지 값 | title | category_1 | category_2 | comments | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 33 | 12220 | 7687 | 42.677364 | 7666 | 0.472998 | 0.617512 | 0 | pip3? pip? 및 conda 내 pip 정리 | 공부하며 적어놓기 1 | 데이터 with 파이썬 | 3 |
1 | 36 | 10331 | 5862 | 49.533925 | 5670 | 0.335979 | 0.542058 | 0 | 파이썬으로 데이터 시각화하기 1편. matplotlib. | 공부하며 적어놓기 1 | 데이터 시각화 | 1 |
2 | 67 | 6175 | 3455 | 50.803914 | 3434 | 0.326150 | 0.553198 | 0 | 파이썬 정렬, 다중 조건으로 한 번에 하기. | 공부하며 적어놓기 1 | 데이터 with 파이썬 | 1 |
3 | 105 | 5871 | 2582 | 74.029271 | 2552 | 0.160658 | 0.429739 | 0 | python 멀티 프로세싱은 parmap 으로 하자. | 공부하며 적어놓기 1 | 데이터 with 파이썬 | 5 |
4 | 85 | 4867 | 2710 | 137.404666 | 2584 | 0.400542 | 0.489213 | 0 | folium 의 plugins 패키지 샘플 살펴보기 | 공부하며 적어놓기 1 | 데이터 시각화 | 4 |
df_pv.shape
(181, 12)
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()
페이지뷰 수 | 순 페이지뷰 수 | 평균 페이지에 머문 시간 | 방문수 | 이탈률 | 종료율(%) | 페이지 값 | comments | |
---|---|---|---|---|---|---|---|---|
페이지뷰 수 | 1.000000 | 0.993271 | 0.034142 | 0.991482 | 0.084858 | 0.157099 | NaN | 0.306384 |
순 페이지뷰 수 | 0.993271 | 1.000000 | 0.032642 | 0.999707 | 0.112546 | 0.177879 | NaN | 0.257182 |
평균 페이지에 머문 시간 | 0.034142 | 0.032642 | 1.000000 | 0.030804 | 0.198782 | 0.112584 | NaN | 0.117134 |
방문수 | 0.991482 | 0.999707 | 0.030804 | 1.000000 | 0.117920 | 0.184239 | NaN | 0.245301 |
이탈률 | 0.084858 | 0.112546 | 0.198782 | 0.117920 | 1.000000 | 0.848446 | NaN | -0.052540 |
종료율(%) | 0.157099 | 0.177879 | 0.112584 | 0.184239 | 0.848446 | 1.000000 | NaN | -0.069448 |
페이지 값 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
comments | 0.306384 | 0.257182 | 0.117134 | 0.245301 | -0.052540 | -0.069448 | NaN | 1.000000 |
별로 유의미한 관계는 안보이네.
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='페이지뷰 수')