体重などのヘルスケアデータを時系列分析（STL分解）できるツールを作った

# 要約
- 体重、消費カロリー、心拍数を時系列分析（STL分解）できるツールを作りました
- Streamlitで動きます
- OpenAI apiを使ってデータの説明・解釈を自動生成します


# スクリーンショット
![STL_graph_s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80212/d4db589c-7d5f-4b42-8885-55ecc79e4c96.png)

![STL_analysis_s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80212/f349e79a-a2d2-4e58-8cbf-4dd768a72744.png)

# デモ版
 Streamlit community cloudでデモ版を公開しています。
[Health_STL_analyzer](https://healthstlanalyzer-f9afa2q5qkbflrugizkeqv.streamlit.app/)
 

# 経緯
最近、ダイエットをしていて、体重や摂取カロリーをあすけんやiPhoneの「ヘルスケア」アプリを使って管理していました。体重の変化がグラフになって現れるのは楽しいです。

が、あすけんやヘルスケアアプリのデフォルトだと、体重の変化が一時的な変化なのか、あるいは長期的な傾向として増加・減少しているのかがちょっと読みにくいんですよね。iPhoneのほうはある程度のトレンドを分析してくれますが、自分で自由に分析するのはちょっと難しい。

なので、ヘルスケアデータをSTL分解（トレンド、季節性）できるツールを作ってみました。

# コード等
コードはGithubで公開しています。
[health_STL_analyzer](https://github.com/epsilonminder/health_STL_analyzer/tree/main)

## 主な機能
- CSVファイルからのデータ読み込み
- STL分解による時系列分析
- インタラクティブなグラフ表示
- グラフのPNGダウンロード機能
- AIを活用したデータ分析（apiキーが必要です）
- チャットボットによる質問応答機能（apiキーが必要です）

## 必要条件
- Python 3.7以上
- OpenAI APIキー（AI分析機能を使用する場合）

## 扱えるデータ
扱えるデータはCSVファイルです。CSVファイルには以下のカラムが必要です。
  - `datetime`: 日時データ（タイムゾーン情報を含む）
  - `type`: データの種類
  - `value`: 測定値

**データ例:**

|datetime | type | value |
|:--------|:-----|:------|
|2020-10-27 21:35:26 +0900 | 消費カロリー | 410.753 |
|2020-10-27 23:42:37 +0900 | 基礎代謝 | 1287.901 |
|2020-10-28 21:11:40 +0900 | 消費カロリー | 432.91|

このツールを使うために、iPhoneの「ヘルスケア」データからCSVファイルを作成するためのコードも用意しました。よかったら使ってみてください。
[apple_healthcare_converter](https://github.com/epsilonminder/apple_healthcare_converter)

## 使っているライブラリなど
（主にCursorとChatGPTに教えてもらいながらやったのであんまり理解してないんですが……）

STL分解にはStatmodelsのstatmodels.tsa.seasonalのSTLクラスを使ってSTL分解を行っています。

また、データ解釈の部分では、OpenAI apiのGPT4.1-nanoを使ってデータ解釈をさせています。GPT4.1-nanoには基本統計量だけでなく、観測期間、トレンド変化、変化点、季節性の特徴なども渡しています。

```python:変化点の検出
  # 時系列の特徴を抽出
    trend = res.trend
    trend_diff = trend.diff()  # トレンドの変化率
    
    # 変化点の検出（トレンドの傾きが変化する点）
    change_points = []
    trend_diff_values = trend_diff.values[1:]  # 最初のNaNを除外
    trend_diff_sign = np.sign(trend_diff_values)
    sign_changes = np.where(np.diff(trend_diff_sign) != 0)[0]
    
    # インデックスを1つずらして変化点を取得（diffによるずれを補正）
    for idx in sign_changes:
        actual_idx = idx + 1  # diffによるずれを補正
        if actual_idx < len(trend_diff):
            change_points.append({
                "date": trend_diff.index[actual_idx].strftime("%Y-%m-%d"),
                "value": float(trend.iloc[actual_idx]),
                "direction": "上昇→下降" if trend_diff.iloc[actual_idx] < 0 else "下降→上昇"
            })
    
    # 期間ごとの統計
    total_days = (daily_data.index[-1] - daily_data.index[0]).days
    start_date = daily_data.index[0].strftime("%Y-%m-%d")
    end_date = daily_data.index[-1].strftime("%Y-%m-%d")
    
    # トレンドの全体的な変化
    total_change = float(trend.iloc[-1] - trend.iloc[0])
    avg_daily_change = float(total_change / total_days) if total_days > 0 else 0
    
    # 最大の変化率を記録した期間
    max_increase_idx = trend_diff.idxmax()
    max_decrease_idx = trend_diff.idxmin()
    
    # 季節性の強さ（振幅）の時間変化
    seasonal_amplitude = pd.Series(np.abs(res.seasonal)).resample('W').mean()
    max_seasonal_week = seasonal_amplitude.idxmax().strftime("%Y-%m-%d")
    min_seasonal_week = seasonal_amplitude.idxmin().strftime("%Y-%m-%d")
```
この結果をOpenAI APIに渡して自然言語でレポートを作成しています。
その他、詳細は実際のコードをみていただくのが良いと思います。

# Comment
今回のコードは主にCursorを使って作成しました。
~~なので、中で何やってるのかあんまりよく理解してないです~~

ヴァイブコーディングは直感的にサクッと作れるのが利点なんですが、仕事の合間にちょっとずつ作っていたら２週間ほどかかりました。
それでも、あまりプログラミングに詳しくないものが簡単にこんなツールを作ることができるのは素晴らしいですね。