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

# 動機

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

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

# 結論

* タイトルに`随時更新`を含む記事は現在247件
* うち全体の1/3以上の95件が`更新なしor当日更新のみ`で最頻値
* 中央値は7
* [もっとも更新間隔が長い(古い記事を更新した)期間は1290日。約3年前の記事が**随時更新**されている。](https://qiita.com/ux_design_tokyo/items/21c6ecda2ac9b123b814))

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

# コード

```py
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](https://qiita-image-store.s3.amazonaws.com/0/62354/b1ebefda-c42d-e0ae-8127-2fd51b5b22e9.png)

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

# 動作検証環境

* docker python:latest
* Python 3.7.0
* qiita2.py (手動コピペ)

```:pip&nbsp;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_at`と`updated_at`があるので、この差分を見れば更新されていることがわかるでしょう。

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

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

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

# 開発環境を作ろう

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

今回は
[DockerでPython3.6の環境構築！matplotlibインストールで詰まった話とかも - Qiita](https://qiita.com/po3rin/items/45ba92e482efcf883325)
を見てつくりました。
matplotlibインストールはこちらではエラーがでませんでした。

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

# 記事を取得しよう

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

[Qiita殿堂入り記事ランキングを作った物語 - Qiita](https://qiita.com/youwht/items/0a12079671750c39fbfa#qiitaapiv2%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E9%81%8E%E5%8E%BB%E6%8A%95%E7%A8%BF%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%96%E5%BE%97)
>既にこの時点で、物凄く長くなりそうな予感がするので、興味のある人がいるならば、後で書く。
またはもう別記事にする。

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

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

[Qiitaの投稿をガーッと取得するバッチ処理用QiitaAPI Pythonラッパー - Qiita](https://qiita.com/tag1216/items/7e23630d97293e35ea4c)

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

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

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

```diff: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_at`と`updated_at`を計算してみましょう。
他の言語と似ているならば、

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

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

参考はこのあたり

* [\[Python3\]日付の差分を取得する - Qiita](https://qiita.com/wonder_zone/items/a15ee2760a1d9e796b67)
* [ISO8601表記でJST現在時刻を取得 - Qiita](https://qiita.com/yohhoy/items/77880f234634ec54598a)
* [8.1. datetime — 基本的な日付型および時間型 — Python 3.6.5 ドキュメント](https://docs.python.jp/3/library/datetime.html)
* [%Y-%m-%dT%H:%M:%S　の T って何なの - Rails etc... Memo](http://d.hatena.ne.jp/noazoh/20140718/1405676120)

```py:日付け差分日数取得
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します。

* [Python3におけるmap/filterの使い方 - どこかに向かうらしい話](http://hiroto1979.hatenablog.jp/entry/2016/02/24/020534)

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

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

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

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

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

参考は

* [Pythonで平均、中央値、最頻値、分散、標準偏差を算出 | note.nkmk.me](https://note.nkmk.me/python-statistics-mean-median-mode-var-stdev/)
* [データの最頻値をNumPyで求める - Irohabook](http://www.irohabook.com/numpy-statistics-mode)
* [平均値・中央値・最頻値をpythonで求める | kote2.tokyo](http://kote2.tokyo/blog/2017/01/22/mean-median-mode/)
* [配列の要素の平均を求めるNumPyのaverage関数とmean関数の使い方 - DeepAge](https://deepage.net/features/numpy-average.html)
* [【数式なしで見てわかる】標準偏差がどうしてもわからない人へ【卒論・修論執筆者向け】 - 草薙の研究ログ](http://kusanagi.hatenablog.jp/entry/2015/12/13/191845)

```py:最頻値とか計算
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
```

でも正直

* [標準偏差計算 、分散を計算します](https://www.easycalculation.com/ja/statistics/standard-deviation.php)
* 
へリストの`[]`を外して入れれば事足りました。

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

# グラフ化する

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

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

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

![fig2.png](https://qiita-image-store.s3.amazonaws.com/0/62354/0a1f7466-81db-ab3a-fc54-bc68f7e9858b.png)

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

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

matplotlib参考

* [matplotlib入門 - りんごがでている](http://bicycle1885.hatenablog.com/entry/2014/02/14/023734)
* [とりあえず描く — matplotlib 1.0 documentation](http://yubais.net/doc/matplotlib/introduction.html)

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

* [棒グラフ — matplotlib 1.0 documentation](http://yubais.net/doc/matplotlib/bar.html)
* [PythonのCounterでリストの各要素の出現個数をカウント | note.nkmk.me](https://note.nkmk.me/python-collections-counter/)
* [\[Python\] コレクション型をソート - Qiita](https://qiita.com/ysk24ok/items/b546471c37b2f443f4c7)

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

* [matplotlibで棒グラフ - Remrinのpython攻略日記](http://python-remrin.hatenadiary.jp/entry/2017/04/28/150338)

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

```py:グラフ化
# 集計
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のみではできないようです。
もししたいならこちらの記事を参考に。

[ギャップのあるデータを途中を省略してプロットする - もうカツ丼はいいよな](http://rion778.hatenablog.com/entry/2014/12/08/235237)

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

![fig5.png](https://qiita-image-store.s3.amazonaws.com/0/62354/62d4574b-4543-e8f8-fb35-31e6241fc2eb.png)

x軸問題が残りました。


# 画像をとってくる

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

参考

* [とりあえず描く — matplotlib 1.0 documentation # グラフを画像として保存](http://yubais.net/doc/matplotlib/introduction.html#id6)
* [Dockerでホストとコンテナ間でのファイルコピー - Qiita](https://qiita.com/gologo13/items/7e4e404af80377b48fd5)
* [Matplotlibでグラフのサイズを変更 - 西尾泰和のはてなダイアリー](http://d.hatena.ne.jp/nishiohirokazu/20111121/1321849806)

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

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

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


# 未完走の感想

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

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