別記事「エクセルではできない?PythonでグラフをGIFアニメーション化してみた」では、日本の月別婚姻数の統計データを、PythonでGIFアニメーション化させてみた結果を、実際のPythonコードも交えて紹介していきました。
その記事の中で、下記GIFアニメーションは、グラフを2回に分けて描画させ作っていることと、その方法に至るまでに苦戦したことを書きました。
ということで、今回の記事では
- グラフのGIFアニメーション化で、グラフをなぜ2回描画させたのかを知りたい
- プログラミングをするうえで計算量なんて気にしたことがなかった
という人に向けて、グラフのGIFアニメーションで2回グラフを描画させた2つの理由について紹介します。
PythonのグラフのGIFアニメーション化でグラフを2回描画させた2つの理由
GIFアニメーションはどのようなグラフで構成されているか
はじめに、冒頭のGIFアニメーション化されたグラフは、どのようなグラフで構成されているかを紹介します。
①ベースとなるグラフ
一つ目のグラフは、ベースとなるグラフです。
別記事でGIFアニメーション化したグラフを作る際のプログラムのポイントのひとつとして、
・アニメーションのベース(背景)となるグラフを描画させる。
と書きました。
このポイントは、別記事で紹介しているコードで言うと、次の記述にあたります。
# 1月~12月の月別婚姻数の描画
for i in range(1,13): # 1から12までを繰り返す
month = '{}月'.format(i) # 1月から12月の文字列を用意する
ax.plot(x, df[month], color="gray", zorder=1,linestyle="dashed") # 折れ線グラフを描画する
そして、このコードで描画させるグラフは次のようなグラフとなります。
このグラフは、折れ線グラフを灰色×点線で表現させただけのグラフとなります。そして、このグラフを用意することが、本記事で紹介するGIFアニメーション化したグラフ描画におけるひとつのポイントとなってきます。
②アニメーション用グラフ
二つ目のグラフは、アニメーション用のグラフです。
別記事で、グラフを作る別のポイントの一つとして、
・for文を用いて、アニメーション用のグラフを繰り返し描画させる。
と書きました。
このポイントは、別記事で紹介しているコードで言うと、次の記述にあたります。
# 1月~12月の月別婚姻数のアニメ用グラフの描画
for j in range(1,13): # 1から12までを繰り返す
month = '{}月'.format(j) # 1月から12月の文字列を用意する
my_line, = ax.plot(x, df[month], color="red",zorder=2, linewidth = 4.0) # 折れ線グラフを描画する
my_text = ax.text(x[0], df[month][0], month, color="red", fontsize=16, ha='right') # 折れ線グラフに添える月情報を描画する
artists.append([my_line, my_text, my_xlabel]) # リストにハイライトさせた月毎のグラフを加えていく
そして、このコードで描画させるグラフは次のようなグラフとなります。
アニメーションで強調して表現したい、各月の折れ線をハイライトさせたグラフです。
③ベースグラフとアニメーショングラフの重ね合わせ
①はアニメーションのないグラフに対して、②はアニメーション化されたグラフとなります。
そして、この2つのグラフを重ね合わせて描画させることで、最終的に目的のGIFアニメーションを作り出しています。
では、「なぜ、わざわざ2つのグラフを用意をし、その2つのグラフの重ね合わせによってグラフのGIFアニメーション化をしたのか?」を、次に紹介していいます。
2つのグラフの重ね合わせをおこなった2つの理由
まず、matplotlibによるグラフのGIFアニメーション化を紹介した記事は多くあります。
そして著者自身も、様々なサイトのやりかたを参考にしながら、グラフのGIFアニメーション化にトライしました。
しなしながら、最終的には次の2つの理由から、今回の2つのグラフを重ね合わせる手段に落ち着きました。
- ラベル情報を含めたグラフのGIFアニメーションの生成がうまくいかない。
- グラフ描画のためのfor文が2重ループ構造となり、計算量がn^2(nの2乗)となる。
理由①:GIFアニメの生成がうまくいなかい
一つ目の、
- ラベル情報を含めたグラフのGIFアニメーションの生成がうまくいかない。
というのは、次のコードにある軸ラベルをGIFアニメーションにうまく含めることができなかったためです。
# グラフの軸ラベル等の設定
plt.title("月別婚姻数 (1947年~2019年)",size=16)
plt.ylabel("婚姻数",size=14)
plt.xticks(x[::], rotation=90, size='small') # 3つおきにラベルを表示する。
plt.xlabel("年",size=14)
plt.grid(True)
調べてみると、ラベルなどのグラフを構成する要素は、次のコードにあるように、各要素をリストで持たせることで解消できるようです。
しかし、著者は、その方法でうまく軸ラベルの描画が出来なかったこともあり、最終的には、GIFアニメーションのベースとグラフを描画させる方法をとりました。
※なお、グラフのラベルをGIFアニメで表示させるための、もっとスマートな方法はあると思います。
理由②:計算量がn^2 (nの2乗)となる。
二つ目の理由、
- グラフ描画のためのfor文が2重ループ構造となり、計算量がn^2(nの2乗)となる。
については、著者は、理由①の課題が解決した後、月毎に赤色でハイライトさせるプログラムを、次のコードで実現させていました。
for i in range(1,13):
for j in range(1,13):
month = '{}月'.format(j)
if i==j:
my_line, = ax.plot(x, df[month], color="red", zorder=2, linewidth = 4.0)
my_text = ax.text(x, df[month][0], month, color="red", fontsize=16, ha='right')
else:
ax.plot(x, df[month], color="gray", zorder=1, linestyle="dashed")
artists.append([my_line, my_text])
コードを見ていただくと分かる通り、このコードは、ひとつのfor文の中に、もうひとつのfor文が存在します。
外側のfor文は、1月から12月まで注目する月を順に指定しています。そして、内側のfor文で、注目している月を赤線で描き、それ以外の月を灰色線で描います。
この例では、外側のfor文で12回の繰り返し、内側のfor文でも12回の繰り返し処理となるため、合計12回×12回=144回の繰り返し処理でグラフ描画を実現させています。
一方で、最終的なコードは、ベースとなるグラフとアニメーション用グラフの描画を2つのfor文で分けているため、結果的に12回+12回=24回の繰り返し処理でグラフ描画を実現しています。
つまり、144回の繰り返し処理を24回の繰り返し処理に削減させるため、グラフの描画を2回に分けたのです。
プログラミング実装において計算量は注意が必要
for文の中にfor文を置いて計算させる場合、その計算量(時間計算量)はn^2(nの2乗)となります。
今回の例のようにfor文で繰り返し処理させる数が12(n=12)である場合は、まだその計算量は程度問題として気にならないですが、この繰り返し処理させる数は大きくなるに従い、その計算量が指数的に増大していきます。
そのため、何気なく、プログラミングでやりたいことを実現するための記述方法として、for文の中にfor文を用いることは十分考えられる話しかとは、計算量が増大し、場合によっては求めている時間で計算が完了しない可能性を秘めているため、注意が必要と言えます。
なお、このコンピューターの計算量については、大学のコンピューターサイエンス(情報学)関連の講義で必ず学ぶ基礎的な内容です。
プログラミングをやる人にとっては教養レベルの内容となるため、知らない方は、「バブルソート」「マージソート」「クイックソート」などの計算量の違いについて説明しているサイトなどを見てみてください。
for文の組み方による実行時間の違いについて
なお、今回の2つのグラフ描画における実行時間の違いについても、軽く確認してみたので紹介しておきます。
- 独立したfor文が2つの場合(24回繰り返し):0.09sec
- for文の中にfor文がある場合(144回繰り返し):0.39sec
結果は、繰り返し回数が24回から144回と6倍であるのに対して、実行時間は約4.3倍の違いとなりました。
実行時間計測はグラフ描画の部分だけを指定し計測をしましたが、単純に実行時間が6倍という結果とはなりませんでした。
しかしながら、for文の組み方次第で、実行時間(計算量)が変わることは確かなので、Pythonで繰り返し処理をさせる場合は、留意しておくべきポイントと言う結論には変わりありません。
(おまけ)処理速度が上がるリスト内包表記について
今回のプログラムではfor文で繰り返し処理を実現させていましたが、この繰り返し処理に対しても記述の仕方で実行速度に差が生じると言われています。
たとえば、あるリストに対して10回繰り返し処理でプロットを描画させる場合を考えます。
for文で繰り返し処理をさせる場合、本記事で紹介したように、次ような記述で表現できます。
for i in range(0,10):
plot(X[i])
しかし、次のような書き方に変えることで、プログラムの実行速度が上がることが、matplotlibの公式ページ上で紹介されています。
plot(sum[x+[None] for x in X],[]))
この記述は「リスト内包表記」と呼ばれています。
昔ながらのC言語の書き方に慣れている人には、とっつきにくい記述方法かもしれませんが、コード量の観点でも実行速度の観点でもメリットがあることから、実務でpythonでコードを書く人であれば、習得して損はない表記法と言えます。
まとめ
今回の記事では、グラフのGIFアニメーションで2回グラフを描画させた2つの理由について紹介してきました。
一つ目の理由は、グラフのラベル等の情報をGIFアニメーション上でうまく表示させるためであり、二つ目の理由は、プログラムの計算量を減らすためでした。
また、for文の組み方でプログラムの計算量が変わることを紹介し、それはプログラミングを行う人にとっては教養レベルの話であることも紹介しました。
コンピューターの性能が大幅に向上していることや、扱うデータ量がそれほど多くない場合は、コンピューターの計算量はそれほど意識しなくても困らないかもしれません。しかし、大量のデータを扱う場合や、自らアルゴリズムを作る場合は、注意すべきポイントとなるため、小さなプログラムを作る場合でも、計算量を意識して実装する習慣を身に付けていくのがいいのかもしれません。
じゃあ