おうちだいすき

自宅警備員からデータサイエンティストになった人の雑記的ななにか。

matplotlibで時系列データをプロットするとき、軸ラベルを上手く表示させたいだけの人生だった

「年末年始はcourseraのkaggleコースを一気に受けるぞ!」と意気込むもひょんなことからmatplotlibと大格闘してしまい、色々と無に帰してしまいました。あけてましたおめでとうございます。

今回は取り扱うテーマがテーマなだけに実際のモデリングや効率良い計算のTipsにはならないです。
想定しているシーンとしては皆様がお客様に分析結果を報告する際に

「もうちょい目盛りをわかりやすくしてくれたらなぁ(ねっとり)」

と、ビンタの一発や二発お見舞いしたくなるような嫌味をかまされた際、ミーティングルームを赤く染め上げないため、皆様の清きその右手を汚さないために共有できたらなと思います。
ではよろしくお願いします。


目指したいプロット

f:id:hiro-i2:20190105161250p:plain

こんな感じです。(ざっくり)
半年おきぐらいに目盛りとうっすらなグリッド線が入っていればお客様も何かどうでもいい文句を言いたそうにはしていますが、流石に重箱の隅をつつきすぎかなと良心が現れだす頃かと思います。

利用するデータについて

知る人ぞ知る飛行機の旅客数データを使用します。
コチラをクリックするとダウンロードできます。
df.head()df.tail()はこんな感じです。
f:id:hiro-i2:20190105160907p:plain
f:id:hiro-i2:20190105160918p:plain

ご覧の通り、1949年1月から1960年12月までの旅客数があつまってます。レコード数は144です。

とりあえずプロットする

plt.figure(figsize=(12, 6)) # とりあえずいい感じのサイズ感にしてくれる
plt.plot(df.Month, df['#Passengers']); # とりあえずプロットできる


f:id:hiro-i2:20190105161003p:plain

まぁそうなりますよね。
df['Month']が文字列になっていて、TimeSeries型でないのが原因です。
変換しないままプロットすると処理に時間もかかりますし、そもそもプロットがごちゃごちゃになるので控えましょう。

TimeSeriesに変換してプロットする

df['Month'] = pd.to_datetime(df['Month'], format='%Y-%m')

補足します。
ここでformat指定しなくてもto_datetimeはdateutil.parser.parseを呼んで処理してくれます。
しかし、レコード数が増加すると処理に時間がかかってしまうのでなるべくformat指定をしてあげましょう。

ではもう一度プロットしてみましょう。コードは先ほどと同じです。

plt.figure(figsize=(12, 6))
plt.plot(df['Month'], df['#Passengers']);


f:id:hiro-i2:20190105161042p:plain

お、綺麗になりましたね。
ですが綺麗すぎるのが難点。
これだとねっとりした嫌味を言われてしまいそうです。
割ともうここでおしまいにしたい。

せめてグリッド線だけでもつけてあげる

まぁ上の状態だと流石に自分自身も説明しにくいかと思います。
不本意ではありますがグリッド線ぐらいつけてあげましょう。

fig, ax = plt.subplots(figsize=(12, 6)) # 細かい設定のためにfig と ax を指定する

plt.plot(df['Month'], df['#Passengers'])

ax.grid(True); # grid を True にする(そのまんま)

ここからmatplotlibの詳細設定に入りますので一番最初はfig, ax =としています。
コードは上から順番に

  1. 事前設定
  2. プロット
  3. 軸とかの設定とプロットの実行

みたいな感じでソースコードを区切って書いていきます。
さて、どんな感じになりましたかね。

f:id:hiro-i2:20190105161120p:plain

もう、ゴールでいいんじゃん?
頑張った。私は頑張った。
ですがお客様はふんぞりかえりながらねっとりした視線を送ってきます。
やってやろうじゃねえか。

目盛りが欲しいんだろ?ほらよ

先にプロットから出してから解説します。

f:id:hiro-i2:20190105161142p:plain

どうやって書いたのでしょう。
ここからあまり馴染みのないクラスを呼びます。
matplotlib.datesです。

import matplotlib.dates as mdates  
  
fig, ax = plt.subplots(figsize=(12, 6))  
  
months = mdates.MonthLocator() # 月の設定  
years = mdates.YearLocator() # 年の設定  
tickFmt = mdates.DateFormatter('%Y-%m') # 軸ラベルに与える表示フォーマットの指定  
  
plt.plot(df['Month'], df['#Passengers'])  
  
ax.grid(True)  
ax.xaxis.set_major_formatter(tickFmt) # おっきい目盛りの指定(そもそもが年単位になっていた)  
ax.xaxis.set_minor_locator(months) # ちっちゃい目盛りの指定  

MonthLocatorYearLocatorでmatplotlibの軸ラベルに対して「今からこの子たち使うよ」的な準備をします。
DateFormatterはその名の通り日付のフォーマットを指定して軸ラベルに書いてもらう準備をします。
set_major_formatterで目盛りの大きな区切りに与えるラベルのフォーマットを指定しています。
なのでおっきい目盛りに対して先ほどDateFormatterで指定した軸ラベルのフォーマットが与えられています。
set_minor_locator注意してください。formatterとlocatorで異なっています。
locatorは簡単にいうと「目盛り」のこと(という認識)です。
MonthLocator()をつかって、ちっちゃい目盛りをかいてね!」という指示内容です。

疲れました。
だけどお客様は半笑いと失笑を交えたなんとも言えないブッサイクな顔をしています。
右手を鎮めるためにコードを書きましょう。

ちっちゃい目盛りの数を減らしてラベルを与えてみる

今度はこんな感じを目指します。

f:id:hiro-i2:20190105161232p:plain

コードのお時間。

fig, ax = plt.subplots(figsize=(12, 6))  
  
months = mdates.MonthLocator(bymonth=7) # bymonth指定で毎年何月の目盛りをふるか指定できる  
years = mdates.YearLocator()  
tickFmt = mdates.DateFormatter('%Y-%m')  
  
plt.plot(df['Month'], df['#Passengers'])  
  
ax.grid(True)  
ax.xaxis.set_major_formatter(tickFmt)  
ax.xaxis.set_minor_formatter(tickFmt) # ちっちゃいめもりにもラベルかいてね!  
ax.xaxis.set_major_locator(years) # なくても良い(統一感なくて気持ち悪かったので書いた)  
ax.xaxis.set_minor_locator(months)  
ax.tick_params(axis='x', which='minor', rotation=45, labelsize='small') # ちっちゃいめもりのラベル設定  
plt.xlim(df['Month'].min(), df['Month'].max()) # ついでに枠いっぱいいっぱい書くようにした  
plt.xticks(rotation=45); # これ書かないとおっきい目盛りは回転しくれない  

先ほどのMonthLocatorbymonthを指定してあげる事で「毎年何月の目盛りを作ってね」という指示を出せます。
ちっちゃい目盛りに軸ラベルがふられてなかったので、set_minor_formatterで書いてもらうようにしました。
そのままだとおっきいラベルと文字サイズが同じで暑苦しい軸ラベルが出来上がるのでax.tick_paramsで軸に対する詳細設定をしています。
引数を見たらなんとなく分かると思いますが、「x軸のちっちゃい目盛りに小さめの文字サイズを45度回転してラベル表示してね」という指示です。
これだけだとおっきい目盛りが回転してくれないので最後にplt.xticks(rotation=45)で回転させてます。
もちろん、ax.tick_params(axis='x', which='major', rotation=45)でも良いですが面倒だったので楽な方を選びました。

ここまでくるとお客様も「なかなかやるじゃん」みたいな顔してるのですが「グリッド線がすくなくて見辛いなぁ(ねっとり)」と、さらに細かい注文をつけてきます。
できらぁ!!!!
f:id:hiro-i2:20190105163026j:plain

ちっちゃい目盛りにもグリッド線をつけてあげる

これを目指します。

f:id:hiro-i2:20190105161250p:plain

コードは以下の通りです。

fig, ax = plt.subplots(figsize=(12, 6))  
  
months = mdates.MonthLocator(bymonth=7)  
years = mdates.YearLocator()  
tickFmt = mdates.DateFormatter('%Y-%m')  
  
plt.plot(df['Month'], df['#Passengers'])  
  
ax.grid(which='major') # ここをいじってあげる  
ax.grid(which='minor', linestyle=':') # ちっちゃい軸に対するグリッドの設定  
ax.xaxis.set_major_formatter(tickFmt)  
ax.xaxis.set_minor_formatter(tickFmt)  
ax.xaxis.set_minor_locator(months)  
ax.xaxis.set_major_locator(years)  
ax.tick_params(axis='x', which='minor', rotation=45, labelsize='small')  
plt.xlim(df['Month'].min(), df['Month'].max())  
plt.xticks(rotation=45);  

変更箇所は2つです。
ax.grid(True)に対してwhich='major'which='minor'を各々設定する事で両方のグリッドラインが描けます。
同じグリッド線だと暑苦しくなりますのでちっちゃい目盛りのグリッド線はドットにしてあげました。

お客様は満足げです。

ほ〜らこんなこともできるんだよ(ねっとり)

今までのノウハウを使ってこんなグラフを描いてみましょう。

f:id:hiro-i2:20190105161310p:plain

四半期別に目盛りを振ってさらにちっちゃい目盛りには年を表示させない取り組みです。

fig, ax = plt.subplots(figsize=(12, 6))  
  
months = mdates.MonthLocator(bymonth=np.array([4, 7, 10])) # bymonthはnp.array指定するのがミソ  
years = mdates.YearLocator()  
majorFmt = mdates.DateFormatter('%Y_%m') # おっきい軸用フォーマット  
minorFmt = mdates.DateFormatter('_%m')  # ちっちゃい軸用フォーマット  
  
plt.plot(df['Month'], df['#Passengers'])  
  
ax.grid(True)  
ax.grid(which='minor', linestyle=':')  
ax.xaxis.set_major_formatter(majorFmt) # majorFmtを適用  
ax.xaxis.set_minor_formatter(minorFmt) # minorFmtを適用  
ax.xaxis.set_minor_locator(months)  
ax.xaxis.set_major_locator(years)  
ax.tick_params(axis='x', which='minor', rotation=90, labelsize='small') # 90度にしないとごちゃる  
plt.xlim(df['Month'].min(), df['Month'].max())  
plt.xticks(rotation=90); # 上に同じく  

コード内コメントにも書きましたが、ここでのミソはbymonthnp.arrayを適用する事です。
これ、公式ドキュメントに一切触れておらず頭抱えてました。
内部的にnp.arrayのチェックを行ってるので指定しないとエラー出します。
デフォルトではbymonth = range(1, 13)みたいなのですがなぜ。。。

[追記]
リスト表記で試したら普通に通りました。
色々試してる時に通らなかったのは何故だ。。。

何はともあれこれで四半期別のプロットも見やすく出来ました。
こうして見てみると、毎年7月くらいにピークがきているのがよく分かりますねっ!
最後はおっきい目盛り、ちっちゃい目盛りに対してフォーマットを設定してあげたのも、もう1つのミソかと思ってます。

最初2行だったコードが軸の設定だけで14行追加されました😩😩😩

もうこれで文句言われないはずです。堂々とお客様にお見せしましょう。

「なんか線が多すぎてみづらいなぁ(呆れ)」

f:id:hiro-i2:20190105162438j:plain

参考サイトなど


ソースコード

GitHubを用意しました。
とりあえず動かして遊んでみたい方はデータをダウンロードして動かしてみてください。
github.com