matplotlibで時系列データをプロットするとき、軸ラベルを上手く表示させたいだけの人生だった
「年末年始はcourseraのkaggleコースを一気に受けるぞ!」と意気込むもひょんなことからmatplotlibと大格闘してしまい、色々と無に帰してしまいました。あけてましたおめでとうございます。
今回は取り扱うテーマがテーマなだけに実際のモデリングや効率良い計算のTipsにはならないです。
想定しているシーンとしては皆様がお客様に分析結果を報告する際に
「もうちょい目盛りをわかりやすくしてくれたらなぁ(ねっとり)」
と、ビンタの一発や二発お見舞いしたくなるような嫌味をかまされた際、ミーティングルームを赤く染め上げないため、皆様の清きその右手を汚さないために共有できたらなと思います。
ではよろしくお願いします。
- 目指したいプロット
- 利用するデータについて
- とりあえずプロットする
- TimeSeriesに変換してプロットする
- せめてグリッド線だけでもつけてあげる
- 目盛りが欲しいんだろ?ほらよ
- ちっちゃい目盛りの数を減らしてラベルを与えてみる
- ちっちゃい目盛りにもグリッド線をつけてあげる
- ほ〜らこんなこともできるんだよ(ねっとり)
- 参考サイトなど
- ソースコード
目指したいプロット
こんな感じです。(ざっくり)
半年おきぐらいに目盛りとうっすらなグリッド線が入っていればお客様も何かどうでもいい文句を言いたそうにはしていますが、流石に重箱の隅をつつきすぎかなと良心が現れだす頃かと思います。
利用するデータについて
知る人ぞ知る飛行機の旅客数データを使用します。
コチラをクリックするとダウンロードできます。
df.head()
とdf.tail()
はこんな感じです。
ご覧の通り、1949年1月から1960年12月までの旅客数があつまってます。レコード数は144です。
とりあえずプロットする
plt.figure(figsize=(12, 6)) # とりあえずいい感じのサイズ感にしてくれる plt.plot(df.Month, df['#Passengers']); # とりあえずプロットできる
まぁそうなりますよね。
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']);
お、綺麗になりましたね。
ですが綺麗すぎるのが難点。
これだとねっとりした嫌味を言われてしまいそうです。
割ともうここでおしまいにしたい。
せめてグリッド線だけでもつけてあげる
まぁ上の状態だと流石に自分自身も説明しにくいかと思います。
不本意ではありますがグリッド線ぐらいつけてあげましょう。
fig, ax = plt.subplots(figsize=(12, 6)) # 細かい設定のためにfig と ax を指定する plt.plot(df['Month'], df['#Passengers']) ax.grid(True); # grid を True にする(そのまんま)
ここからmatplotlibの詳細設定に入りますので一番最初はfig, ax =
としています。
コードは上から順番に
- 事前設定
- プロット
- 軸とかの設定とプロットの実行
みたいな感じでソースコードを区切って書いていきます。
さて、どんな感じになりましたかね。
もう、ゴールでいいんじゃん?
頑張った。私は頑張った。
ですがお客様はふんぞりかえりながらねっとりした視線を送ってきます。
やってやろうじゃねえか。
目盛りが欲しいんだろ?ほらよ
先にプロットから出してから解説します。
どうやって書いたのでしょう。
ここからあまり馴染みのないクラスを呼びます。
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) # ちっちゃい目盛りの指定
MonthLocator
やYearLocator
でmatplotlibの軸ラベルに対して「今からこの子たち使うよ」的な準備をします。
DateFormatter
はその名の通り日付のフォーマットを指定して軸ラベルに書いてもらう準備をします。
set_major_formatter
で目盛りの大きな区切りに与えるラベルのフォーマットを指定しています。
なのでおっきい目盛りに対して先ほどDateFormatter
で指定した軸ラベルのフォーマットが与えられています。
set_minor_locator
注意してください。formatterとlocatorで異なっています。
locatorは簡単にいうと「目盛り」のこと(という認識)です。
「MonthLocator()
をつかって、ちっちゃい目盛りをかいてね!」という指示内容です。
疲れました。
だけどお客様は半笑いと失笑を交えたなんとも言えないブッサイクな顔をしています。
右手を鎮めるためにコードを書きましょう。
ちっちゃい目盛りの数を減らしてラベルを与えてみる
今度はこんな感じを目指します。
コードのお時間。
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); # これ書かないとおっきい目盛りは回転しくれない
先ほどのMonthLocator
にbymonth
を指定してあげる事で「毎年何月の目盛りを作ってね」という指示を出せます。
ちっちゃい目盛りに軸ラベルがふられてなかったので、set_minor_formatter
で書いてもらうようにしました。
そのままだとおっきいラベルと文字サイズが同じで暑苦しい軸ラベルが出来上がるのでax.tick_params
で軸に対する詳細設定をしています。
引数を見たらなんとなく分かると思いますが、「x軸のちっちゃい目盛りに小さめの文字サイズを45度回転してラベル表示してね」という指示です。
これだけだとおっきい目盛りが回転してくれないので最後にplt.xticks(rotation=45)
で回転させてます。
もちろん、ax.tick_params(axis='x', which='major', rotation=45)
でも良いですが面倒だったので楽な方を選びました。
ここまでくるとお客様も「なかなかやるじゃん」みたいな顔してるのですが「グリッド線がすくなくて見辛いなぁ(ねっとり)」と、さらに細かい注文をつけてきます。
できらぁ!!!!
ちっちゃい目盛りにもグリッド線をつけてあげる
これを目指します。
コードは以下の通りです。
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'
を各々設定する事で両方のグリッドラインが描けます。
同じグリッド線だと暑苦しくなりますのでちっちゃい目盛りのグリッド線はドットにしてあげました。
お客様は満足げです。
ほ〜らこんなこともできるんだよ(ねっとり)
今までのノウハウを使ってこんなグラフを描いてみましょう。
四半期別に目盛りを振ってさらにちっちゃい目盛りには年を表示させない取り組みです。
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); # 上に同じく
コード内コメントにも書きましたが、ここでのミソはbymonth
にnp.array
を適用する事です。
これ、公式ドキュメントに一切触れておらず頭抱えてました。
内部的にnp.array
のチェックを行ってるので指定しないとエラー出します。
デフォルトではbymonth = range(1, 13)
みたいなのですがなぜ。。。
[追記]
リスト表記で試したら普通に通りました。
色々試してる時に通らなかったのは何故だ。。。
何はともあれこれで四半期別のプロットも見やすく出来ました。
こうして見てみると、毎年7月くらいにピークがきているのがよく分かりますねっ!
最後はおっきい目盛り、ちっちゃい目盛りに対してフォーマットを設定してあげたのも、もう1つのミソかと思ってます。
最初2行だったコードが軸の設定だけで14行追加されました😩😩😩
もうこれで文句言われないはずです。堂々とお客様にお見せしましょう。
「なんか線が多すぎてみづらいなぁ(呆れ)」
参考サイトなど
- 早く知っておきたかったmatplotlibの基礎知識、あるいは見た目の調整が捗るArtistの話
- dates(matplotlib公式)
- Date tick labels(matplotlib公式)
- matplotlib.axes.Axes.tick_params
ソースコード
GitHubを用意しました。
とりあえず動かして遊んでみたい方はデータをダウンロードして動かしてみてください。
github.com