Qiita
Python
matplotlib
Collection

Qiitaの随時更新記事はいつ頃更新されなくなるか

動機

Qiitaを書いていると、昔の記事を更新するか新しい記事にして過去記事を参照するか悩みます。
そういった将来の追加作業を見越してタイトルに【随時更新】なんて入れてみるものの、結局業務や興味から外れて更新しない…なんてことも…

一体他の皆さんはどれぐらい更新宣言した記事を保守しているのか!
ということを調べてみました。

結論

おおざっぱに見ると、
三分の一が更新しない
三分の一が一ヶ月以内に更新しなくなる
三分の一が一ヶ月過ぎても更新した
雰囲気です。
想像よりちゃんと更新されていました。

コード

import qiita2
import datetime as dt
import numpy as np
import statistics
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import collections

qiita2.wait_seconds = 0

def maintenance_period(item):
    created = dt.datetime.strptime(item['created_at'], '%Y-%m-%dT%H:%M:%S%z')
    updated = dt.datetime.strptime(item['updated_at'], '%Y-%m-%dT%H:%M:%S%z')
    return (updated - created).days

items = qiita2.items()
print('投稿数:', len(items))

days = list(map(maintenance_period, items))
print(days)

print('平均値:', np.mean(days))
print('中央値:', np.median(days))
print('最頻値:', statistics.mode(days))
print('最短値:', min(days))
print('最長値:', max(days))
print('標準偏差:', statistics.stdev(days))

# 集計
count = collections.Counter(days)
# ソートして日数と重複数に分ける
sorted_count = sorted(count.items())
print('最終更新間隔と出現数:', sorted_count)
period = [c[0] for c in sorted_count]
number = [c[1] for c in sorted_count]

# グラフ化 めちゃへた
plt.bar(period, number)
plt.yticks([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100])

plt.tick_params(labelsize=4) # 1区切りでメモリが重なるんだけど…
plt.savefig('fig.png', dpi=200)

出力

投稿数: 247
[0, 8, 4, 20, 21, 26, 0, 8, 1, 33, 7, 0, 33, 0, 0, 8, 0, 3, 0, 5, 44, 31, 56, 38, 0, 0, 0, 0, 0, 0, 84, 15, 61, 0, 0, 0, 0, 23, 0, 0, 0, 0, 116, 117, 0, 18, 69, 0, 42, 126, 116, 174, 0, 0, 123, 82, 0, 0, 128, 33, 40, 0, 0, 168, 10, 0, 0, 0, 0, 0, 216, 107, 7, 0, 0, 0, 10, 12, 8, 0, 0, 7, 0, 107, 11, 9, 14, 54, 0, 175, 0, 0, 0, 0, 6, 264, 6, 0, 0, 149, 189, 0, 12, 3, 0, 119, 0, 341, 65, 271, 0, 0, 9, 216, 27, 27, 0, 0, 3, 2, 97, 10, 3, 23, 5, 42, 0, 107, 20, 0, 0, 0, 0, 56, 0, 3, 499, 177, 43, 0, 526, 12, 1, 2, 6, 37, 0, 15, 0, 0, 379, 0, 0, 19, 0, 354, 0, 0, 529, 0, 0, 157, 0, 283, 0, 563, 598, 0, 0, 223, 6, 22, 2, 1, 350, 665, 739, 137, 47, 578, 210, 368, 30, 231, 0, 771, 786, 88, 0, 0, 110, 0, 726, 0, 22, 0, 0, 26, 414, 0, 2, 59, 133, 143, 0, 118, 22, 0, 152, 461, 914, 1024, 765, 6, 604, 7, 75, 186, 594, 2, 1, 0, 5, 0, 449, 0, 0, 391, 72, 6, 962, 22, 359, 0, 0, 113, 0, 0, 954, 5, 1290, 112, 0, 2, 57, 937, 0]
平均値: 107.12145748987854
中央値: 7.0
最頻値: 0
最短値: 0
最長値: 1290
標準偏差: 219.49035903300216
最終更新間隔と出現数: [(0, 95), (1, 4), (2, 6), (3, 5), (4, 1), (5, 4), (6, 6), (7, 4), (8, 4), (9, 2), (10, 3), (11, 1), (12, 3), (14, 1), (15, 2), (18, 1), (19, 1), (20, 2), (21, 1), (22, 4), (23, 2), (26, 2), (27, 2), (30, 1), (31, 1), (33, 3), (37, 1), (38, 1), (40, 1), (42, 2), (43, 1), (44, 1), (47, 1), (54, 1), (56, 2), (57, 1), (59, 1), (61, 1), (65, 1), (69, 1), (72, 1), (75, 1), (82, 1), (84, 1), (88, 1), (97, 1), (107, 3), (110, 1), (112, 1), (113, 1), (116, 2), (117, 1), (118, 1), (119, 1), (123, 1), (126, 1), (128, 1), (133, 1), (137, 1), (143, 1), (149, 1), (152, 1), (157, 1), (168, 1), (174, 1), (175, 1), (177, 1), (186, 1), (189, 1), (210, 1), (216, 2), (223, 1), (231, 1), (264, 1), (271, 1), (283, 1), (341, 1), (350, 1), (354, 1), (359, 1), (368, 1), (379, 1), (391, 1), (414, 1), (449, 1), (461, 1), (499, 1), (526, 1), (529, 1), (563, 1), (578, 1), (594, 1), (598, 1), (604, 1), (665, 1), (726, 1), (739, 1), (765, 1), (771, 1), (786, 1), (914, 1), (937, 1), (954, 1), (962, 1), (1024, 1), (1290, 1)]

グラフ

fig.png

(なんの役にも立たない…)

動作検証環境

  • docker python:latest
  • Python 3.7.0
  • qiita2.py (手動コピペ)
pip list
Package         Version
--------------- ---------
certifi         2018.4.16
chardet         3.0.4
cycler          0.10.0
docutils        0.14
idna            2.7
kiwisolver      1.0.1
matplotlib      2.2.2 # 手動install
numpy           1.14.5 # 手動install
pip             10.0.1
pyparsing       2.2.0
python-dateutil 2.7.3
pytz            2018.5
requests        2.19.1 # 手動install
setuptools      39.2.0
six             1.11.0
statistics      1.0.3.5 # 手動install
urllib3         1.23
wheel           0.31.1

何を作るか考えよう

今回は記事の更新を調べるスクリプトです。
Qiita APIでは投稿にcreated_atupdated_atがあるので、この差分を見れば更新されていることがわかるでしょう。

残念ながら更新回数やリビジョン数はAPIからはとれないので、
わかることは投稿した記事が最後に更新されたのはどれぐらい経ってか程度になりそうです。
Webをスクレイピングすれば各リビジョンがわかりますが、そこまではしません。(まとめ方も思いつかないですし)

どうやって作るか考えよう

さて、作りたいものを思いついたので、どうやって作るか考えます。
今回はグラフにした方がわかりやすそうなので、最近よく見かけるmatplotlib?がグラフを作りやすいのかもしれません。
となると使用言語はPythonですね。
取得用とグラフ化用に言語を分ける意味もあまりないですし。

開発環境を作ろう

残念ながら普段使う環境はpython 2.7で、Ubuntuでもないのでpython3が同居していません。
ライブラリのバージョンでひっかかるのも嫌なので、どうせなら3で書きたいところ
ぐぐるとpyenvを使ったちょっと面倒な方法ばかり出るので、今回はしかたなく古い知識しか持ってないdockerでたてます。

今回は
DockerでPython3.6の環境構築!matplotlibインストールで詰まった話とかも - Qiita
を見てつくりました。
matplotlibインストールはこちらではエラーがでませんでした。

yum install docker
service docker start
docker pull python:3
docker run -it --name test python /bin/bash

記事を取得しよう

そういえば今までpythonでQiitaの記事を取得したことがありませんでした。
ライブラリ使うかいちから組むか…
と最近あった記事を参考にしようとすると

Qiita殿堂入り記事ランキングを作った物語 - Qiita

既にこの時点で、物凄く長くなりそうな予感がするので、興味のある人がいるならば、後で書く。
またはもう別記事にする。

pythonでQiitaAPIv2を叩く。取得したjsonをパースする。
qiita2というpython用のラッパーを使用させていただく。(感謝
ポイントは、一度に取得できるデータ量は1万件までというAPIの制限があること。
その制限をクリアするために、ライブラリ側にも手を入れつつ、頑張る
月次で取得すればある程度良いのだが、一部に月次で1万件を超えてしまう箇所有(泣。
ユーザのcontributionは取得出来ない、などのいくつかのAPI仕様も理解する必要あり。

qiita2というのが良さそう。
データ分析系の記事って考察が主でパク参考にしたいコードが無いときが結構ある印象覚えたり。

Qiitaの投稿をガーッと取得するバッチ処理用QiitaAPI Pythonラッパー - Qiita

Python触らない勢でもなんとか読めそうな点と、1ファイル・コピペOKな部分が気に入って他を見ずに決定。
さらに言えば、ライブラリを使っても結局手動でする必要が多いページング処理を自動でしてくれるのはとても嬉しい。

殿堂入り記事を見ると、流用するにはちょっと手入れが必要そうなんですが…

今回は全記事を取得するのではなく、タイトルに随時更新が入る記事がほしいので、直接URL定数に入れてみました。

qiita2.py
- URL_ITEMS     = "https://qiita.com/api/v2/items"
+ URL_ITEMS     = "https://qiita.com/api/v2/items?query=title:随時更新"

query以外にも追加するgetパラメーターはあるのですが…そこはqiita2.pyかrequestかがうまく処理してくれたようです。
これでqiita2.items()で絞り込みした記事を取得できました。

初投稿日と更新日の差分をとってみよう

記事がとれたのでcreated_atupdated_atを計算してみましょう。
他の言語と似ているならば、

  1. stringを日付型に変換
  2. 日付型同士を加減算するメソッドで処理
  3. 日付差分型を日数にしてstring出力

ができれば良さそうデス。
投稿日の更新は桁落ちするでしょうが、許容します。

参考はこのあたり

日付け差分日数取得
def maintenance_period(item):
    created = dt.datetime.strptime(item['created_at'], '%Y-%m-%dT%H:%M:%S%z')
    updated = dt.datetime.strptime(item['updated_at'], '%Y-%m-%dT%H:%M:%S%z')
    return (updated - created).days

全記事に適用するのでmapします。

によると、なんとなくイテレーターよりリストのほうが簡単そうなので変換しちゃいます。

mapを適用してリスト化
days = list(map(maintenance_period, items))

数学的なあれこれを計算する

数学出来ないプログラマでも、こーいうときに平均値を使っては良くないことはなんとなく知っている。
「よくそうなる」ということなら最頻値?をとればよく更新されなくなる日付けがわかるかも?1

参考は

最頻値とか計算
print('平均値:', np.mean(days))
print('中央値:', np.median(days))
print('最頻値:', statistics.mode(days))
print('最短値:', min(days))
print('最長値:', max(days))
print('標準偏差:', statistics.stdev(days))
出力
平均値: 107.12145748987854
中央値: 7.0
最頻値: 0
最短値: 0
最長値: 1290
標準偏差: 219.49035903300216

でも正直

標準偏差がとても大きいということは、
「この日数が一番よく更新されて、その前後は以降だんだん更新数が減っていく」
という釣り鐘図にならず、標本がバラバラで傾向が無い。
という漠然な大意として見てよろしいでしょうか?

グラフ化する

グラフが…ぜんぜんわからん…

えーと、日数がxで投稿数がyにするグラフ、ということはなんとなくわかる。
じゃあdaysの重複をカウントしたものにしてやればいい?
pythonに連想配列あったっけ?
matplotlib見ていると、xとyは別々のリストっぽいけど紐づけどうするんだ…?
とすごく混乱していまいた。
できれば自動でリストの同じ値の出現数をy軸として計算するグラフがほしいのです。

結果、最初はリストをそのままヒストグラムに渡していたんですが、なにか非常に大味だし間違ってそう。

fig2.png

そして細かくするとグラフがおかしい。90近く出るはずのバーがなく、全部低空飛行している。

ヒストグラフ図が用途として正しいのかもよくわからなかったので(多分に間違っていたと思う)、結局手動でカウントしてxとyのデータを用意して棒グラフに書くことに

matplotlib参考

集計・ソート・棒グラフ参考

参考あげましたが、最終的には

の「百人一首のひらがなをカウントする」がほぼ求めているものでした。

グラフ化
# 集計
count = collections.Counter(days)
# ソートして日数と重複数に分ける
sorted_count = sorted(count.items())
print('最終更新間隔と出現数:', sorted_count)
period = [c[0] for c in sorted_count]
number = [c[1] for c in sorted_count]


# グラフ化
plt.bar(period, number)
plt.yticks([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 19, 100])

plt.tick_params(labelsize=4) # 1区切りでメモリが重なるんだけど…
plt.savefig('fig.png', dpi=200)

ただ単にグラフにしただけではあまり役立ちそうにないですね…
もうちょっと一週間,一ヶ月単位ぐらいでまとめたほうがわかりやすそうです?
が、グラフ化は力尽きたので…。
配列操作だけでできそう?

わかりやすくグラフを作るのにもセンスが必要で、それが壊滅的に欠けていることがわかりました。


0が突出していて見づらいグラフなので、中略できればよいのですが、matplotlibのみではできないようです。
もししたいならこちらの記事を参考に。

ギャップのあるデータを途中を省略してプロットする - もうカツ丼はいいよな

では0だけ除くと見やすいグラフになるかと言うと…

fig5.png

x軸問題が残りました。

画像をとってくる

開発環境はdockerのCLIなので、よく見かけるplt.show()はおそらく使えないでしょう。
グラフをファイル出力して、さらにコンテナからホストにファイルを送る必要があります(今回はdockerのフォルダマウント(-v)を使っていなかった)

参考

今回ホスト側もCLIなので、毎回

  1. コンテナで実行
  2. ホストにグラフコピー
  3. メインのWindowsにグラフコピー&確認

という手間が発生しました。コンテナから直接Windowsに転送できるか調べたほうが良かった反省点です。

未完走の感想

慣れの問題だと思いますが、グラフ化がいろいろ躓いて辛く、
適当に書いてもそれなりに作れるエクセルのグラフが恋しくなりました。

もうちょっと数学とか作図に慣れ親しんで、ゴールのグラフを思い描いてから初めたほうがよかったと思いました。設計不足。


  1. 区間でとらなければこれもあまり良くないですね。0になる可能性が高すぎると予測できるべきでした。