はじめに
データ解析において、日時データと経過時間を同時に表示することは、トレンドや変動の理解を深めるのに有用です。例えば、天文学のライトカーブや株価データの解析など、様々な分野に応用できます。matplotlibでの時刻表示では、datetime
形式を使うことが多いですが、経過時間を同時に表示したいケースがあると思います。
そこで本記事では、任意の時刻からの経過時間を様々なスケール(秒、年など)で表示する方法を紹介します。
Secondary Axisとは
matplotlibでは異なる単位を表示する方法としてSecondary Axisがあります。この方法は単位変換前後の関数を用意し、目盛りの位置関係を対応させることで実装します。
一般的な使い方は
も合わせてご覧ください。datatime
ではハマりどころがありそこについて本記事で説明します。
方法
datetime
を用いる場合の変換関数は、数値に直接変換するだけでは2軸描画でうまく表示できないことが多く、公式のSecondary Axisの例では、では、matplotlibのmdates.date2num
を使って実装しています。本記事もこのモジュールを使用します。
以下のコードでは、基準時刻base_date
からの経過時間をunit
の単位で表示します。
create_date_time_funcs()
で変換関数を取得して実装します。このように関数内関数を使用する目的は、コードの補足で説明します。
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import datetime
import numpy as np
def create_date_time_funcs(base_date, unit='seconds'):
base_date_num = mdates.date2num(base_date) # 基準日付を内部形式に変換
def date_to_time(date):
"""Convert matplotlib date to elapsed time."""
elapsed_days = date - base_date_num
if unit == 'seconds':
elapsed_time = elapsed_days * 24 * 60 * 60
elif unit == 'minutes':
elapsed_time = elapsed_days * 24 * 60
elif unit == 'hours':
elapsed_time = elapsed_days * 24
elif unit == 'days':
elapsed_time = elapsed_days
elif unit == 'years':
elapsed_time = elapsed_days / 365.25 # 1年を約365.25日と定義
else:
raise ValueError("Invalid unit. Choose from 'seconds', 'minutes', 'hours', 'days', 'years'.")
return elapsed_time
def time_to_date(elapsed_time):
"""Convert elapsed time to matplotlib date."""
if unit == 'seconds':
elapsed_days = elapsed_time / (24 * 60 * 60)
elif unit == 'minutes':
elapsed_days = elapsed_time / (24 * 60)
elif unit == 'hours':
elapsed_days = elapsed_time / 24
elif unit == 'days':
elapsed_days = elapsed_time
elif unit == 'years':
elapsed_days = elapsed_time * 365.25 # 1年を約365.25日と定義
else:
raise ValueError("Invalid unit. Choose from 'seconds', 'minutes', 'hours', 'days', 'years'.")
date = elapsed_days + base_date_num
return date
return date_to_time, time_to_date
# 基準日付を定義
base_date = datetime.datetime(2024, 5, 5, 5, 5, 5)
# 単位を指定して変換関数を作成
unit = 'days' # ここで単位を変更できる('seconds', 'minutes', 'hours', 'days', 'years')
date_to_time, time_to_date = create_date_time_funcs(base_date, unit)
# ダミーデータの作成(ランダムウォークにインフレーションバイアスを追加)
np.random.seed(7)
num_points = 10000
dates = [base_date + datetime.timedelta(days=i) for i in range(num_points)]
daily_bias = 0.00025
daily_std_dev = 0.01
values = np.cumsum(np.random.randn(num_points) * daily_std_dev + daily_bias)
# グラフに描画
fig, ax1 = plt.subplots()
ax1.plot(dates, values)
ax1.set_xlabel('Date')
ax1.set_ylabel('Value')
# 副次的なX軸を追加
secax_x = ax1.secondary_xaxis('top', functions=(date_to_time, time_to_date))
# 副次的なX軸のラベルを設定
secax_x.set_xlabel(f'Elapsed Time ({unit})')
# 主軸のフォーマットを設定
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
fig.autofmt_xdate()
plt.show()
横軸下に日時が、上に経過時間(unit = 'days'
)が表示されているのがわかります。unit
引数を変えた際の結果を以下に示します。
経過時間の単位`unit`を変えた時の結果
unit = 'seconds'
|
unit = 'minutes'
|
unit = 'hours'
|
unit = 'years'
|
コードの補足
Secondary Axisの変換関数に引数を受け取るための工夫
secondary_xaxis()
のfunctions
引数は、基本関数のみを指定します。このため、base_date
とunit
のように引数を受け取りたい場合は工夫が必要です。
変数を受け取るための方法の一つに、クロージャとして受け取る方法があります。これは関数内関数create_date_time_funcs()
を用意して実装します。
この他の方法として、create_date_time_funcs()
を使わずに、base_date
とunit
をグローバル変数として渡すことも可能です。ただし、グローバル変数を使用すると処理が煩雑になり、デバッグが難しくなる点に注意してください。
また、lambda
式を使う方法もあります。例えば、日付から経過時間の軸を作成するには、以下のように書きます。
secax_x = ax1.secondary_xaxis(
'top',
functions=(
lambda date: date - mdates.date2num(base_date),
lambda elapsed_time: elapsed_time + mdates.date2num(base_date)
)
)
secax_x = ax1.secondary_xaxis(
'top',
functions=(
lambda date: (date - mdates.date2num(base_date)) * 24 * 60 * 60,
lambda elapsed_time: elapsed_time / (24 * 60 * 60) + mdates.date2num(base_date)
)
)
この方法は、$n$倍の目盛りを表示するようなシンプルな変換には向いていますが、今回のように処理が複雑な場合は可読性が下がる点に注意してください。
様々な経過時間軸を同時に描画する方法
複数の経過時間の単位を描画するには、以下のコードのようにsecondary_xaxis()
の位置を数値で指定します。1.0
がTop
と指定した場合と同じですが、軸がはみ出ることがあります。適宜、plt.subplots_adjust()
で配置を調整してください。配置調整のパラメータはこちらの記事で図解されており参考になります。
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import datetime
import numpy as np
def create_date_time_funcs(base_date, unit='seconds'):
base_date_num = mdates.date2num(base_date) # 基準日付を内部形式に変換
def date_to_time(date):
"""Convert matplotlib date to elapsed time."""
elapsed_days = date - base_date_num
if unit == 'seconds':
elapsed_time = elapsed_days * 24 * 60 * 60
elif unit == 'minutes':
elapsed_time = elapsed_days * 24 * 60
elif unit == 'hours':
elapsed_time = elapsed_days * 24
elif unit == 'days':
elapsed_time = elapsed_days
elif unit == 'years':
elapsed_time = elapsed_days / 365.25 # 1年を約365.25日と定義
else:
raise ValueError("Invalid unit. Choose from 'seconds', 'minutes', 'hours', 'days', 'years'.")
return elapsed_time
def time_to_date(elapsed_time):
"""Convert elapsed time to matplotlib date."""
if unit == 'seconds':
elapsed_days = elapsed_time / (24 * 60 * 60)
elif unit == 'minutes':
elapsed_days = elapsed_time / (24 * 60)
elif unit == 'hours':
elapsed_days = elapsed_time / 24
elif unit == 'days':
elapsed_days = elapsed_time
elif unit == 'years':
elapsed_days = elapsed_time * 365.25 # 1年を約365.25日と定義
else:
raise ValueError("Invalid unit. Choose from 'seconds', 'minutes', 'hours', 'days', 'years'.")
date = elapsed_days + base_date_num
return date
return date_to_time, time_to_date
# 基準日付を定義
base_date = datetime.datetime(2024, 5, 5, 5, 5, 5)
# 単位を指定して変換関数を作成
units = ['days', 'seconds', 'minutes', 'hours', 'years']
funcs = [create_date_time_funcs(base_date, unit) for unit in units]
# ダミーデータの作成(ランダムウォークにインフレーションバイアスを追加)
np.random.seed(7)
num_points = 10000
dates = [base_date + datetime.timedelta(days=i) for i in range(num_points)]
daily_bias = 0.00025
daily_std_dev = 0.01
values = np.cumsum(np.random.randn(num_points) * daily_std_dev + daily_bias)
# グラフに描画
fig, ax1 = plt.subplots()
ax1.plot(dates, values)
ax1.set_xlabel('Date')
ax1.set_ylabel('Value')
# 複数の副次的なX軸を追加
secax_x1 = ax1.secondary_xaxis('top', functions=funcs[0])
secax_x1.set_xlabel(f'Elapsed Time ({units[0]})')
secax_x2 = ax1.secondary_xaxis(1.5, functions=funcs[1])
secax_x2.set_xlabel(f'({units[1]})')
secax_x3 = ax1.secondary_xaxis(2.0, functions=funcs[2])
secax_x3.set_xlabel(f'({units[2]})')
secax_x3.spines['top']
secax_x4 = ax1.secondary_xaxis(2.5, functions=funcs[3])
secax_x4.set_xlabel(f'({units[3]})')
secax_x5 = ax1.secondary_xaxis(3.0, functions=funcs[4])
secax_x5.set_xlabel(f'({units[4]})')
# 主軸のフォーマットを設定
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
fig.autofmt_xdate()
# 余白を調整
plt.subplots_adjust(top=0.44)
plt.show()
出力結果(years
の軸が少しはみ出ていますが、そういうことが起きやすいですので、注意しましょう。)
まとめ
本記事では、観測データの日付から経過時間を計算し、matplotlibを使用してグラフに表示する方法について解説しました。2軸を用いることで、日時データと経過時間を同時に表示し、データのトレンドや変動を直感的に把握することができます。この記事が、読者のデータ解析に役立つことを願っています。