Pythonのグラフ描画ライブラリmatplotlibで、時系列データ（サイト閲覧人数）を日次・週次で棒グラフにする

# まとめ
あるウェブサイトの閲覧人数を15分置きに記録したcsvファイルがある。csvファイルは日別で1ファイルになっている。
こんな:

```20180319.csv
date,time,dow,w_or_h,headcount
2018-03-19,00:00,Mon,weekday,2638
2018-03-19,00:15,Mon,weekday,2482
:
2018-03-19,23:30,Mon,weekday,2689
2018-03-19,23:45,Mon,weekday,2521
```
```20180321.csv（祝日）
date,time,dow,w_or_h,headcount
2018-03-21,00:00,Wed,holiday,2670
2018-03-21,00:15,Wed,holiday,2537
:
2018-03-21,23:30,Wed,holiday,2817
2018-03-21,23:45,Wed,holiday,2818
```

これを、
・日次の棒グラフにした。
　2018/03/19（月）のグラフ:
　![daily_figure.png](https://qiita-image-store.s3.amazonaws.com/0/150194/658025f1-3020-ca11-d334-d710837b8896.png)

・週次の棒グラフにした。
　2018/03/19（月）から2018/03/25（日）までのグラフ:
　![weekly_figure.png](https://qiita-image-store.s3.amazonaws.com/0/150194/fb6e863c-7701-425c-5942-1979a1dcd197.png)

# はじめに/背景

私がよく見るサイトに「いま何人くらいこのサイトを閲覧しているか」を示す一文があります。1分置きに更新されてるっぽい。閲覧人数を時系列で見たらどういう分布になるのか気になりました。

それを調べるため、そのページのhtmlファイルを取得し、閲覧人数を切り出してCSVファイルに記録するプログラムをPythonで書きました。さほど難しいことはしてないのでこのコードは割愛。ただ、平日と休日（土日+祝日）とで閲覧人数の分布が変わると予想されるので、「休日であるか否か」を表す情報も出力するようにしました。冒頭のcsvの`w_or_h`列です（祝日であるか否かの判定は、祝日の日づけリストをコード内にベタ書きして比較してるだけです）。

そのプログラムをラズパイに移して、crontabで15分置きに動かしています。1ヵ月ほど動かして順調にデータも溜まってきたので可視化したい。Excelで手作業でグラフを作ってもいいけど、仕事でmatplotlibを使いそうなので慣れておきたい（そのうちグラフの作成も自動で行いたい; そのあたりについても書いてないです）。

[この記事](https://qiita.com/skotaro/items/08dc0b8c5704c94eafb9) を読んでなるほどと思ったので、`plt.なんとか` は使わず、`ax.なんとか` で全部やりました。

# まずは日次の棒グラフ
各種の数値（棒グラフの太さとか）は試行錯誤で決めました。

```Python:daily_plot_headcount.py
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as dates
from datetime import datetime,timedelta

df = pd.read_csv("C:\\tmp\\20180319.csv")

# 横軸を時系列として扱うため、dateとtimeを結合し、stringからdatetimeに変換する
df["date-time"] = df["date"].str.cat(df["time"], sep=" ")
df["date-time"] = pd.to_datetime(df["date-time"])

# end_datetimeはcsvの最後の行を取る手もあるが、
# 横軸の右端に翌日の日付を表示したいのでこうした。
start_datetime = df.iloc[0]["date-time"]
end_datetime = df.iloc[0]["date-time"] + timedelta(days=+1)
title_label = start_datetime.strftime("%Y/%m/%d")

fig = plt.figure(figsize=(8,4), dpi=200)
ax = fig.add_subplot(1,1,1)

# 描画関係の設定
ax.bar(x=df["date-time"], height=df["headcount"], width=0.008, color="lightgreen", align="edge")
ax.set_xlim(start_datetime, end_datetime)
ax.set_title(title_label, fontsize=10)
ax.set_facecolor("white")
ax.grid(axis="both", which="both", linewidth=0.5, linestyle="dashed", alpha=0.5)

# 主目盛設定
# x軸目盛のラベルに時刻を表示すると補助目盛のラベルと被ってしまうので日付だけ。
ax.xaxis.set_major_locator(dates.DayLocator())
ax.xaxis.set_major_formatter(dates.DateFormatter("%m/%d %a"))
ax.tick_params(axis="x", which="major", labelsize=8)
ax.tick_params(axis="y", which="major", labelsize=8)

# 補助目盛設定
ax.xaxis.set_minor_locator(dates.HourLocator(interval=3))
ax.xaxis.set_minor_formatter(dates.DateFormatter("\n\n%H:%M"))
ax.tick_params(axis="x", which="minor", labelsize=6)

plt.savefig("daily_figure.png")
```

結果（冒頭のグラフを再掲）:
![daily_figure.png](https://qiita-image-store.s3.amazonaws.com/0/150194/658025f1-3020-ca11-d334-d710837b8896.png)

# 続いて週次の棒グラフ
平日か休日か、パッと見でわかるように背景色を変えました。

```Python:weekly_plot_headcount.py
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as dates
from datetime import datetime,timedelta

df = pd.read_csv("C:\\tmp\\20180319.csv")
df = pd.concat([df, pd.read_csv("C:\\tmp\\20180320.csv")])
df = pd.concat([df, pd.read_csv("C:\\tmp\\20180321.csv")])
df = pd.concat([df, pd.read_csv("C:\\tmp\\20180322.csv")])
df = pd.concat([df, pd.read_csv("C:\\tmp\\20180323.csv")])
df = pd.concat([df, pd.read_csv("C:\\tmp\\20180324.csv")])
df = pd.concat([df, pd.read_csv("C:\\tmp\\20180325.csv")])

# 横軸を時系列として扱うため、dateとtimeを結合し、stringからdatetimeに変換する
df["date-time"] = df["date"].str.cat(df["time"], sep=" ")
df["date-time"] = pd.to_datetime(df["date-time"])

# end_datetimeはcsvの最後の行を取る手もあるが、
# 横軸の右端に1週先の日付を表示したいのでこうした。
start_datetime = df.iloc[0]["date-time"]
end_datetime = df.iloc[0]["date-time"] + timedelta(days=+7)
title_label = start_datetime.strftime("%Y/%m/%d %H:%M:%S") + " - " + end_datetime.strftime("%Y/%m/%d %H:%M:%S")

fig = plt.figure(figsize=(18,8), dpi=200)
ax = fig.add_subplot(1,1,1)

# 背景色の調節。平日を薄い青、休日を薄い赤にしている。
# ax.set_facecolor()は範囲指定できないみたいだったので、矩形を描画するax.axvspan()を使った。
weekdays = df.loc[(df.time == "00:00") & (df.w_or_h == "weekday"), "date-time"].tolist()
holidays = df.loc[(df.time == "00:00") & (df.w_or_h == "holiday"), "date-time"].tolist()
for weekday in weekdays:
    ax.axvspan(weekday, weekday + timedelta(days=+1), facecolor="blue", alpha=0.03)
for holiday in holidays:
    ax.axvspan(holiday, holiday + timedelta(days=+1), facecolor="red", alpha=0.03)

# 描画関係の設定
ax.bar(x=df["date-time"], height=df["headcount"], width=0.015, color="lightgreen", align="edge")
ax.set_xlim(start_datetime, end_datetime)
ax.set_title(title_label, fontsize=18)
ax.set_facecolor("white")
ax.grid(axis="both", which="both", linewidth=0.5, linestyle="dashed", alpha=0.5)

# 主目盛設定
# x軸目盛のラベルに時刻を表示すると補助目盛のラベルと被ってしまうので日付だけ。
ax.xaxis.set_major_locator(dates.DayLocator())
ax.xaxis.set_major_formatter(dates.DateFormatter("%m/%d %a"))
ax.tick_params(axis="x", which="major", labelsize=12)
ax.tick_params(axis="y", which="major", labelsize=12)

# 補助目盛設定
ax.xaxis.set_minor_locator(dates.HourLocator(interval=6))
ax.xaxis.set_minor_formatter(dates.DateFormatter("\n\n%H:%M"))
ax.tick_params(axis="x", which="minor", labelsize=8)

plt.savefig("weekly_figure.png")
```

結果（冒頭のグラフを再掲）:
![weekly_figure.png](https://qiita-image-store.s3.amazonaws.com/0/150194/fb6e863c-7701-425c-5942-1979a1dcd197.png)

## 記述の順序に注意
背景色の描画に使用している`ax.axvspan()`と、棒グラフの描画に使用している`ax.bar()`の記述の順序を変えると、つまりこうすると、

```
# ax.bar()のあとにax.axvspan()を書く:

ax.bar(x=df["date-time"], height=df["headcount"], width=0.015, color="lightgreen", 

weekdays = df.loc[(df.time == "00:00") & (df.w_or_h == "weekday"), "date-time"].tolist()
holidays = df.loc[(df.time == "00:00") & (df.w_or_h == "holiday"), "date-time"].tolist()
for weekday in weekdays:
    ax.axvspan(weekday, weekday + timedelta(days=+1), facecolor="blue", alpha=0.03)
for holiday in holidays:
    ax.axvspan(holiday, holiday + timedelta(days=+1), facecolor="red", alpha=0.03)
```

グラフの手前に矩形が来るため、見た目がややアレになります:
![bad_weekly_figure.png](https://qiita-image-store.s3.amazonaws.com/0/150194/8d16e985-f6d7-c27b-a7f9-6d72f09111bd.png)

# 考察

- 平日/休日を問わず、最大値は21:00-0:00に現れる。
- 平日/休日を問わず、最小値は4:00-6:00に現れる。
  - 最小値は常に600人くらいのところにあり、それくらいのユーザが接続しっぱなしにしているらしい。
- 平日は、
  - 出勤前（7:00-9:00）にピークが現れる。休日はそうでもない。（ピーク後にそんなに閲覧人数が減らないのって…）
  - 昼休み（12:00-13:00）にピークが現れる。休日はそうでもない。（ピーク後にそんなに閲覧人数が減らないのって……）
  - 定時後（17:30-18:30）あたりから閲覧人数が増え始める。
- 祝日（03/21）は、最小値から最大値まで、おおむね単調増加しているのが面白い。

# おわりに
Pythonのグラフ描画ライブラリmatplotlibで、時系列データ（サイト閲覧人数）を日次・週次で棒グラフにしました。

日次でグラフにするのと週次でグラフにするのとでは、見栄えの意味から言って、異なる配慮が必要でした。このまま観測を続けてデータが溜まったら、月次・年次のグラフも作ってみたい。