【matplotlib】FuncAnimationを理解しアニメーションを作成する

matplotlibでは、アニメーションを実装する方法として、ArtistAnimationとFuncAnimationの大きく2種類の方法があります。

ArtistAnimationの方が実装は簡単である反面、メモリ等のコンピュータリソースを大きく使ってしまいます。一方、今回解説するFuncAnimationは効率的にアニメーションを作成することができますが、インターフェースが少しだけ複雑で、実装するのに慣れが必要です。

今回は、matplotlibでアニメーションを実装する方法として、FuncAnimationを利用した方法を紹介します。

本記事の内容

  • FuncAnimationの仕組みとインターフェース
  • サイン波のアニメーション実装
  • 散布図のアニメーション実装
  • GIFファイルへの保存方法

FuncAnimationの仕組み

FuncAnimationのインターフェースは以下のようになっています。

from matplotlib.animation import FuncAnimation

# FuncAnimation(fig, animate, frames=range(8), interval=1000)
引数 説明
fig アニメーションの描画領域(Figure オブジェクト)
animate 各フレームで呼ばれる更新関数。Artistオブジェクトを返す
frames アニメーションのフレーム数またはイテラブル
interval フレーム間隔(ミリ秒)
init_func 初期化関数(オプション)
blit True にすると描画が高速化される

全体の仕組みをまとめると、FuncAnimationは frames で指定された回数だけ animate 関数を繰り返し呼び出し、各フレームの描画を更新することでアニメーションを実現します。

サイン波のアニメーション

最も基本的な例として、サイン波が時間とともに移動するアニメーションを作成しましょう。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

# 描画領域の準備
fig, ax = plt.subplots(figsize=(10, 4))
x = np.linspace(0, 4 * np.pi, 200)
line, = ax.plot([], [], 'b-', linewidth=2)

ax.set_xlim(0, 4 * np.pi)
ax.set_ylim(-1.5, 1.5)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("Traveling Sine Wave")
ax.grid(True, alpha=0.3)

def init():
    """初期化関数: 空のプロットを返す"""
    line.set_data([], [])
    return line,

def animate(frame):
    """更新関数: 各フレームで呼ばれる"""
    phase = frame * 0.1  # 位相をフレームごとにずらす
    y = np.sin(x - phase)
    line.set_data(x, y)
    return line,

# アニメーション作成
anim = FuncAnimation(fig, animate, init_func=init,
                     frames=100, interval=50, blit=True)
plt.tight_layout()
plt.show()

animate 関数が各フレームで呼ばれ、サイン波の位相を少しずつずらすことで、波が右方向に移動するアニメーションが実現されます。

振幅が変化するサイン波

もう少し複雑な例として、振幅が時間とともに減衰するサイン波のアニメーションを作りましょう。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig, ax = plt.subplots(figsize=(10, 5))
x = np.linspace(0, 4 * np.pi, 300)
line, = ax.plot([], [], 'b-', linewidth=2)
fill = None

ax.set_xlim(0, 4 * np.pi)
ax.set_ylim(-2, 2)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True, alpha=0.3)
time_text = ax.text(0.02, 0.95, '', transform=ax.transAxes, fontsize=12)

def init():
    line.set_data([], [])
    time_text.set_text('')
    return line, time_text

def animate(frame):
    t = frame / 20.0
    amplitude = np.exp(-0.1 * t) * 1.5
    y = amplitude * np.sin(x - t)

    line.set_data(x, y)
    time_text.set_text(f't = {t:.2f}, A = {amplitude:.2f}')
    ax.set_title(f"Damped Sine Wave (amplitude = {amplitude:.2f})")
    return line, time_text

anim = FuncAnimation(fig, animate, init_func=init,
                     frames=200, interval=50, blit=True)
plt.tight_layout()
plt.show()

散布図のアニメーション

散布図でもFuncAnimationを利用できます。ランダムウォークする点群のアニメーションを作成してみましょう。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

np.random.seed(42)

n_points = 50
n_frames = 150

# ランダムウォークの軌跡を事前計算
positions = np.zeros((n_frames, n_points, 2))
positions[0] = np.random.randn(n_points, 2)
for t in range(1, n_frames):
    positions[t] = positions[t-1] + np.random.randn(n_points, 2) * 0.1

fig, ax = plt.subplots(figsize=(8, 8))
scat = ax.scatter([], [], c='steelblue', s=30, alpha=0.7)
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)
ax.set_aspect('equal')
ax.set_title("Random Walk Particles")
ax.grid(True, alpha=0.3)
frame_text = ax.text(0.02, 0.95, '', transform=ax.transAxes, fontsize=12)

def init():
    scat.set_offsets(np.empty((0, 2)))
    frame_text.set_text('')
    return scat, frame_text

def animate(frame):
    scat.set_offsets(positions[frame])
    # 色を距離で変化させる
    distances = np.sqrt(positions[frame][:, 0]**2 + positions[frame][:, 1]**2)
    scat.set_array(distances)
    frame_text.set_text(f'Frame: {frame}')
    return scat, frame_text

anim = FuncAnimation(fig, animate, init_func=init,
                     frames=n_frames, interval=50, blit=True)
plt.tight_layout()
plt.show()

散布図の場合は set_offsets メソッドで座標を更新し、set_array メソッドで色を更新しています。

複数のサブプロットでのアニメーション

複数のサブプロットを同時にアニメーションすることも可能です。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

x = np.linspace(0, 2 * np.pi, 200)
line1, = ax1.plot([], [], 'b-', linewidth=2)
line2, = ax2.plot([], [], 'r-', linewidth=2)

ax1.set_xlim(0, 2 * np.pi)
ax1.set_ylim(-1.5, 1.5)
ax1.set_title("sin(x - t)")
ax1.grid(True, alpha=0.3)

ax2.set_xlim(0, 2 * np.pi)
ax2.set_ylim(-1.5, 1.5)
ax2.set_title("cos(x - t)")
ax2.grid(True, alpha=0.3)

def init():
    line1.set_data([], [])
    line2.set_data([], [])
    return line1, line2

def animate(frame):
    t = frame * 0.1
    line1.set_data(x, np.sin(x - t))
    line2.set_data(x, np.cos(x - t))
    return line1, line2

anim = FuncAnimation(fig, animate, init_func=init,
                     frames=100, interval=50, blit=True)
plt.tight_layout()
plt.show()

GIFファイルへの保存

作成したアニメーションをGIFファイルとして保存できます。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig, ax = plt.subplots(figsize=(8, 4))
x = np.linspace(0, 4 * np.pi, 200)
line, = ax.plot([], [], 'b-', linewidth=2)

ax.set_xlim(0, 4 * np.pi)
ax.set_ylim(-1.5, 1.5)
ax.set_title("Sine Wave Animation")
ax.grid(True, alpha=0.3)

def init():
    line.set_data([], [])
    return line,

def animate(frame):
    y = np.sin(x - frame * 0.1)
    line.set_data(x, y)
    return line,

anim = FuncAnimation(fig, animate, init_func=init,
                     frames=60, interval=50, blit=True)

# GIFとして保存(pillowライターを使用)
# anim.save('sine_wave.gif', writer='pillow', fps=20)

# MP4として保存(ffmpegが必要)
# anim.save('sine_wave.mp4', writer='ffmpeg', fps=20)

plt.tight_layout()
plt.show()

anim.save()writer パラメータで保存形式を指定します。GIF保存には pillow、MP4保存には ffmpeg が必要です。

まとめ

本記事では、matplotlibのFuncAnimationを使ったアニメーション作成方法を解説しました。

  • FuncAnimationanimate 関数を繰り返し呼ぶことでアニメーションを実現する
  • init_func で初期化、animate で各フレームの更新を行う
  • blit=True で描画を高速化できる
  • 線グラフだけでなく散布図など様々なプロットに適用可能
  • anim.save() でGIFやMP4として保存できる

ArtistAnimationと比べて、FuncAnimationはメモリ効率が良く、フレーム数が多いアニメーションに適しています。