こんにちは、ほけきよです。
以前、SEO対策ということで、内部リンクの可視化についての記事を書きました。
コレだけでも、十分に参考になるのですが、ほら、ひとつ気になることがないですか?
- 孤島に浮かぶ記事はどうやって補足すればええんや
これ考えるのは人間のお仕事ですよね。
….
けどやっぱりめんどくさい!!
というわけで、自動化しましょうね。
記事の類似度を出す
記事の類似度を出す方法って、いろいろあるんですよ。大別すると二種
- 記事の文章内容から類似度算出
- 読者の行動から類似度算出
今回は二つ目『読者の行動から類似度算出』方式でやります。*1
バスケット分析
「おむつとビール」の法則って聞いたことないですか?オムツと一緒にビールが買われるっていうアレです。
情報マネジメント用語辞典:おむつとビール(おむつとびーる) - ITmedia エンタープライズ
これに使われるのがバスケット分析。
「同じ組み合わせで買われたことが多いもの同士は関連が深いだろう」 という考えに基づいています。
たとえば、ここにAさん、Bさん、Cさん、Dさんがいるとします。 それぞれ下図のような商品を買いました。
これを、縦軸を顧客、横軸を商品にとります。すると、各商品が誰に買われたかがわかります。
各商品において、どれが似ているかを数学的に計算して、 顧客の購買履歴から商品の関連性を算出してやろうということです。
いわゆる『共起性』を見るのです。より詳しくはこちらの記事でどうぞ!
バスケット分析をブログに応用
この考えをブログに使ってみたのが今回のアレ。
- 商品が記事
- 顧客は読者
- 購入の有無はブクマ
スパム的なブクマカやファンは中身よりほかの目的でブックマークしている可能性があるので、その影響が薄くなるように、少しだけ細工をしたものを使っています。(下図)
これの内積を用いて、関連度を抽出しています!
コピペで一発!記事抽出コード
今までどおりpythonを使って、記事抽出のコードを書きました。下記コードをコピペして、related_article.py
とかで保存してください。
*3
#coding: utf-8 from bs4 import BeautifulSoup import urllib from urllib import request import csv from argparse import ArgumentParser import json import numpy as np from sklearn import manifold import matplotlib.pyplot as plt def extract_urls(root_url): """ トップページを指定すると、ブログ内に存在するurlをすべて抜き出してくれる """ is_articles = True page = 1 urls = [] titles = [] # writer = csv.writer(f, lineterminator='\n') # 改行コード(\n)を指定しておく while is_articles: try: html = request.urlopen("{}/archive?page={}".format(root_url, page)) except urllib.error.HTTPError as e: # HTTPレスポンスのステータスコードが404, 403, 401などの例外処理 print(e.reason) break except urllib.error.URLError as e: # アクセスしようとしたurlが無効なときの例外処理 print(e.reason) break soup = BeautifulSoup(html, "html.parser") articles = soup.find_all("a",class_="entry-title-link") for article in articles: titles.append(article.text) urls.append(article.get("href")) if len(articles) == 0: # articleがなくなったら終了 is_articles = False page += 1 return titles, urls def get_bookmarks(url): """ はてブ情報を取得 """ data = request.urlopen("http://b.hatena.ne.jp/entry/json/{}".format(url)).read().decode("utf-8") try: info = json.loads(data.strip('(').rstrip(')')) except: info = json.loads(bytes(data).strip(b'(').rstrip(b')'), "r") try: return info["bookmarks"] except: return 0 def make_matrix(urls, save): users = [] for url in urls: bookmarks = get_bookmarks(url) if bookmarks != 0: for bookmark in bookmarks: user = bookmark["user"] if user not in users: users.append(user) # bookmark_matrix作成 M = np.zeros((len(urls),len(users))) for i, url in enumerate(urls): bookmarks = get_bookmarks(url) if bookmarks != 0: for bookmark in bookmarks: j = users.index(bookmark["user"]) M[i][j] += 1 # 保存したければする if save: with open("data.csv","w") as f: writer = csv.writer(f, lineterminator='\n') a = ["USER"] a.extend(urls) writer.writerow(a) MT = M.T for i, user in enumerate(users): m = [user] m.extend(MT[i]) with open("data.csv","a") as f: writer = csv.writer(f, lineterminator='\n') writer.writerow(m) # ブクマ一回勢を消す MT = M.T # print(MT.shape) MT_filter = [] for e in MT: if e.sum() > 1: e /= e.sum() MT_filter.append(e) MT_filter = np.array(MT_filter) M_filter = MT_filter.T print(M_filter.shape) return M_filter def calc_dist(M_filter,alpha=0.05): confidences = np.zeros( (len(M_filter),len(M_filter)) ) for i, article0 in enumerate(M_filter): for j, article1 in enumerate(M_filter): a0a1 = np.zeros(len(article0)) for l,(u0, u1) in enumerate(zip(article0, article1)): a0a1[l] = u0*u1 if article0.sum() == 0 or i==j: confidences[i][j] = 0 else: confidences[i][j] = a0a1.sum()#/article0.sum() # symmetricな計量の場合 dist = 1-np.power(confidences,0.04) return dist if __name__ == '__main__': parser = ArgumentParser() parser.add_argument("-u", "--url", type=str, required=True,help="input your url") parser.add_argument("-r", "--rank", type=int, required=True,help="input num of related articles") parser.add_argument("-s", "--save_matrix", action="store_true", default=False, help="save matrix default:False") parser.add_argument("-m", "--mds", action="store_true", default=False, help="show MSD scatter default:False") args = parser.parse_args() save = args.save_matrix mds = args.mds n = args.rank titles, urls = extract_urls(args.url) alpha = 0.05 # userリスト作成 M_filter = make_matrix(urls, save=False) # csvにする(url編) with open("related_articles_url.csv","w") as f: writer = csv.writer(f, lineterminator='\n') a = ["original"] a.extend(range(n)) writer.writerow(a) # csvにする(タイトル編) with open("related_articles_title.csv","w") as f: writer = csv.writer(f, lineterminator='\n') a = ["original"] a.extend(range(n)) writer.writerow(a) confidence = np.zeros(len(M_filter)) for i, article0 in enumerate(M_filter): for j, article1 in enumerate(M_filter): a0a1 = np.zeros(len(article0)) for k,(u0, u1) in enumerate(zip(article0, article1)): a0a1[k] = u0*u1 if article0.sum() == 0: confidence[j] = 0 else: confidence[j] = a0a1.sum()/article0.sum() index = confidence.argsort()[::-1] print(titles[i],":",urls[i]) related_article_url = [urls[i]] related_article_title = [titles[i]] related_num = ["#"] # 追加 for i in index[1:n]: related_article_url.append(urls[i]) related_article_title.append(titles[i]) related_num.append(confidence[i]) print("\t",confidence[i],titles[i],":",urls[i]) with open("related_articles_url.csv","a") as f: writer = csv.writer(f, lineterminator='\n') writer.writerow(related_article_url) writer.writerow(related_num) with open("related_articles_title.csv","a") as f: writer = csv.writer(f, lineterminator='\n') try: writer.writerow(related_article_title) except: continue writer.writerow(related_num) # confidence heatmap作る if mds: dist = calc_dist(M_filter, alpha) mds = manifold.MDS(n_components=2, dissimilarity="precomputed") pos = mds.fit_transform(dist) plt.scatter(pos[:,0], pos[:,1], marker="x", alpha=0.5) plt.show()
実行は下記コマンドです。保存場所で実行しましょう
python related_article.py -u http://www.procrasist.com -r 10 -s -m
-u
自分のURLを入れる-r
関連記事を何個まで出すか-s
関連記事リストを保存するかどうか-m
2次元にマッピングして表示してみるかどうか
各記事に対してタイトル、URLが関連度の大きい順に表示されます。 また、実行結果がcsvに保存されるようになっています
data.csv
: 顧客と商品のマトリックス(上で表示しているようなやつ)related_article_url.csv
: 関連記事のURLリストrelated_article_title.csv
: 関連記事のタイトルリスト*4
... 1ヶ月書き続ければやっぱりぶち上がるの?ブログ月報(´ε` ) : http://www.procrasist.com/entry/2016/10/31/200000 0.171028210043 ブログは一年続くの?読者数は?2万件のはてなブログで分析する : http://www.procrasist.com/entry/blog-analyzer 0.149092859153 帰省・上京の新幹線の中で聞きたい曲9選 : http://www.procrasist.com/entry/2016/12/30/200000 0.0273836825332 【ブログ運営報告】前月比3倍!得意領域に力を入れた、やってて良かった7ヶ月目。 : http://www.procrasist.com/entry/7th-month 0.0258411085419 【運営報告】バズとGoogle様と5ヶ月目の私 : http://www.procrasist.com/entry/5th-month 0.0238548574645 【コードで一発】ブログ最適化/SEO対策で面倒なことは全てPythonにやらせよう : http://www.procrasist.com/entry/python-blog-optimization 0.0238548574645 DeepLearning系ライブラリ、Kerasがあまりにも便利だったので使い方メモ : http://www.procrasist.com/entry/2017/01/07/154441 0.0175295395488 最近のyoutuberが鉄球を熱しすぎな件について : http://www.procrasist.com/entry/2016/12/04/200000 0.0165658732392 【成人式】真のヤンキーとは?ヤンキーのなり方を考えてみる! : http://www.procrasist.com/entry/2017/01/09/234532 0.0165658732392 【武井壮最強説】武井壮が本当に『百獣の王』だと思うワケ : http://www.procrasist.com/entry/takeiso 【書評】『金持ち父さん貧乏父さん』新社会人への『お金』の講義 : http://www.procrasist.com/entry/2016/10/29/000000 0.217741935484 ブログは一年続くの?読者数は?2万件のはてなブログで分析する : http://www.procrasist.com/entry/blog-analyzer 0.217741935484 ダリ展行ってきた。天才過ぎた作品タイトルランキング : http://www.procrasist.com/entry/dali 0.217741935484 勇者ヨシヒコが始まったので、メレブの今までの呪文を振り返ってみる! : http://www.procrasist.com/entry/2016/10/08/170000 0.00477897252091 ライブが最高!Fall Out Boy (FOB) の紹介とオススメ曲! : http://www.procrasist.com/entry/2016/11/07/200000 0.00477897252091 毎日のパソコン生活を快適にする事前設定集 : http://www.procrasist.com/entry/pctips 0.00477897252091 1ヶ月書き続ければやっぱりぶち上がるの?ブログ月報(´ε` ) : http://www.procrasist.com/entry/2016/10/31/200000 0.00477897252091 Pentatonixという最強のプロアカペラ集団。おすすめ曲は? : http://www.procrasist.com/entry/2016/11/01/200000 0.00477897252091 整体師が教えてくれた4つの肩こり・目のかすみ対策 : http://www.procrasist.com/entry/2016/11/04/200000 0.00477897252091 【ハイスタ, NOB, ...】青春を彩るメロコア・パンクロックバンド達 : http://www.procrasist.com/entry/2016/11/06/200000 ...
なお、これらの類似度を元に、2次元空間にマッピングすることもできます*5*6。こんな感じ!
僕の場合は、
- ブログ系の記事
- 日記的な雑記
- テクノロジー系
でブクマしてくれる方がちょっとずつ違うんだなぁっとわかったりしました!
まとめ
いかがでしたか? ブックマーク数がある程度ある人は、こういう分析方法も有効だと思います!
試してみたいけど環境が作れない&なんかエラー出るって人は声かけてくださいな。可能な限り対応します。
*1:記事内容から類似度算出も実装してるんですが、Windowsでするには面倒(mecabをwindowsに入れるのがゴリ面倒)なものになったので、先に2つ目から紹介します。
*3:そのうちこの機能もall in oneのほうに入れておきます。
【コードで一発】ブログ最適化/SEO対策で面倒なことは全てPythonにやらせよう - プロクラシスト
*4:特殊な文字コードが入っている場合は保存されません。仕様です。
*5:多次元尺度法を使ってます。
*6:グラフに関しては詳しくはhokekiyooのGithubページの実装をご覧ください。以前記事にも書いています。
ipywidgetsとbokehで『jupyter』の更なる高みへ 【インタラクティブなグラフ描画】 - プロクラシスト