周期データを直感的に可視化！Matplotlibで位相を簡単に二次軸に追加する方法

## はじめに  

周期的なデータを可視化する際、「時間」と「位相」を同時に表示したい場面があります。例えば、天文学で惑星や衛星の軌道周期を調べるときや、信号処理で振動データを解析するときです。しかし、Matplotlibの標準機能では、「観測開始からの経過日数」などを主軸に表示しつつ、位相を二次軸（`secondary axis`）で表現する方法が直感的ではありません。そのため、少し工夫が必要です。

本記事のコードは、Google Colab上で動作確認が可能です。[こちら](https://colab.research.google.com/drive/1CxRTRvkSuAFCV01OX2fAqx-4NzBRGEld?usp=sharing)からコードを直接ご覧いただけます。


## 解決したい課題  

### 位相は周期的なため直接表示が難しい  
位相は **0～1 の範囲で周期的に変化** します。この特性が原因で、単純に二次軸に表示しようとすると破綻する場合があります。特に、累積的な時間データを扱う際、主軸と二次軸を正確に対応付ける工夫が必要です。

### シンプルで再利用可能な方法を実現したい  
この記事では、**汎用性が高く、比較的簡単に実装できる方法**に焦点を当てます。特に、`secondary_axis` の変換関数を再利用可能な形で設計するため、グローバル変数を避けたラッパーを活用します。これにより、複数のグラフや異なるデータセットを安全かつ効率的に扱うことが可能になります。

> **補足**: `secondary axis` の設計やラッパーの使い方について詳しく知りたい方は、以下の記事も参考にしてください。  
> [Matplotlibでdatetimeの日時と経過時間を2軸で表示する方法](https://qiita.com/yusuke_s_yusuke/items/9563f2484fdac29031d8)


## 実装例：位相の表示  

以下のコード例では、時刻データを位相データに変換し、二次軸として表示します。基準時刻と周期を変換関数で管理することで、柔軟で安全な実装を実現しています。


### 実装コード  

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter

def create_phase_functions(t0, period):
    """
    時刻と位相を相互変換する関数を生成します。

    Args:
        t0 (float): 基準時刻
        period (float): 周期

    Returns:
        tuple: 
            - time_to_phase (function): 時刻から位相に変換する関数
            - phase_to_time (function): 位相から時刻に変換する関数
    """
    def time_to_phase(t):
        return (t - t0) / period
    def phase_to_time(phase):
        return phase * period + t0
    return time_to_phase, phase_to_time

def custom_phase_formatter(decimal_places=2):
    """
    位相ラベルのフォーマットをカスタマイズします。

    Args:
        decimal_places (int): 小数点以下の桁数

    Returns:
        function: Matplotlibのフォーマッター関数
    """
    def formatter(x, pos):
        fractional_phase = x % 1
        return f"{fractional_phase:.{decimal_places}f}"
    return formatter

# 基準時刻と周期
t0, period = 0, 162.15
time_to_phase, phase_to_time = create_phase_functions(t0, period)

# データ生成
times = np.linspace(1000, 1300, 1000)
phases = time_to_phase(times)

# グラフ作成
fig, ax = plt.subplots()

# 位相を0～1に収めたデータをsin波としてプロット
ax.plot(times, np.sin(2 * np.pi * phases), label="Example Data")
ax.set_xlabel("Time (days)")
ax.set_ylabel("Data Value")
ax.set_title("Time vs Phase")

# 二次軸を追加し、位相を表示
formatter = FuncFormatter(custom_phase_formatter(decimal_places=2))
secax = ax.secondary_xaxis('top', functions=(time_to_phase, phase_to_time))
secax.xaxis.set_major_formatter(formatter)
secax.set_xlabel("Phase")

ax.grid(True)
ax.legend()
plt.show()
```

**実行結果** 

- 主軸（下側）には「時刻（days）」が表示されます。  
- 二次軸（上側）には「位相（0～1）」が表示されます。この際、位相の小数点以下の桁数は **Pythonのfリテラル（フォーマット文字列）** を使用して指定しており、浮動小数点数の表記揺れを防ぎつつ、任意の桁数で表示できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/d2603590-059f-c752-12ae-c80b9293afd1.png)




## 応用例：Astropyを活用した実装  

天文学のデータ解析では、[Astropy](https://www.astropy.org/) が広く使用されています。ここでは、先ほどのコードにAstropyを取り入れ、**天文学で標準的な形式（MJD、JDなど）を柔軟に扱える実装**を紹介します。この方法は、特に天文学のプロジェクトで時刻管理を効率化したい場合に有用です。

<details>

<summary>Astropyを活用した実装例を見る</summary>

この実装では、先の関数をベースに、主軸データを日付形式（ISOなど）ではなく、Matplotlibで扱いやすい **数値形式（MJDやJD）** に限定しています。Astropyのデータフォーマットを`secondary_axis`の内部で直接使用せず、変換関数を外部で管理することで、予期せぬバグを防ぐ設計を採用しています。

```python:Astropyを活用した実装例
from astropy.time import Time

# データ生成の前に MJD へ変換
start_time = Time("2025-04-01", format="iso").mjd
end_time = Time("2025-12-31", format="iso").mjd

# 基準時刻と周期
t0 = Time("2000-01-01", format="iso").mjd  # 基準時刻を MJD で指定
period = 162.15  # 周期 (日単位)

# 関数生成
time_to_phase, phase_to_time = create_phase_functions(t0, period)

# サンプルデータ生成
times = np.linspace(start_time, end_time, 1000)  # MJD 形式の時刻データ
phases = time_to_phase(times)  # 位相を計算

# グラフ作成
fig, ax = plt.subplots()

# 位相を0～1に収めたデータをsin波としてプロット
ax.plot(times, np.sin(2 * np.pi * phases), label="Example Data")
ax.set_xlabel("Time (MJD)")
ax.set_ylabel("Data Value")
ax.set_title("Time vs Phase")

# 二次軸を追加し、位相を表示
formatter = FuncFormatter(custom_phase_formatter(decimal_places=2))
secax = ax.secondary_xaxis('top', functions=(time_to_phase, phase_to_time))
secax.xaxis.set_major_formatter(formatter)
secax.set_xlabel("Phase")

ax.grid(True)
ax.legend()
plt.show()
```

**実行結果**   

MJD形式の使用例 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/ad767f1e-851d-4709-34ef-63356a00360a.png)

- 主軸（下側）には「時刻（MJD）」が表示されます。  
- 二次軸（上側）には「位相（0～1）」が表示されます。  

このように、Astropyと本コードを活用することで、標準的な時刻形式（MJDやJD）を用いた天文学データの解析や可視化を、より簡単かつ効率的に行うことが可能になります。


</details>

## まとめ  

Matplotlibを使って位相を二次軸に表示する方法を解説しました。このアプローチでは、基準時刻と周期を変換関数を用いて整理し、天文学をはじめとする周期的データの視覚化に幅広く応用できます。また、Astropyを活用することで、標準的な時刻形式（MJDやJD）を用いた効率的なデータ解析を実現しています。

シンプルな`numpy`と`matplotlib`による基本実装と、Astropyを活用した応用例を提示することで、プロジェクトのニーズに応じた柔軟な選択肢を提供しています。

この記事が、皆さんのデータ解析や研究において一助となれば幸いです。