たねやつの木

Photographs, Keyboards and Programming

【ぴよログでRAG: 第4回】前処理でデータを整形し、AIが理解しやすい形にする

こんにちは、たねやつです。

前回は、Pythonと正規表現を駆使して、ぴよログのテキストデータをプログラムで扱える「構造化データ(JSON形式)」に変換しました。これでデータ活用の第一歩を踏み出せました。

しかし、現在のデータはまだ「生の」状態です。例えば、ミルクの量が 240ml という文字列のままだったり、日付と時刻が別の項目になっていたりと、このままではAIがデータの意味を正確に理解し、計算などに使うことが困難です。

そこで今回は、データ分析の必須ライブラリである Pandas を使って、この生のデータをさらに洗練された形式に整える「データ前処理」を行います。AIにとって分かりやすく、栄養価の高い"食事"を用意してあげるようなイメージですね!

前の記事

この記事でできること

  • 前回作成したJSONデータを、Pythonのデータ分析ライブラリ PandasDataFrame という形式で読み込む方法がわかる。
  • 日付と時刻の文字列を結合し、時系列分析に不可欠な datetime 型に変換する方法を習得できる。
  • 「240ml」や「5分」のような文字列から、数値だけを抽出して計算可能な形式にクレンジングする方法がわかる。
  • データをより扱いやすくするために、データ型を適切に変換する作業の重要性が理解できる。

事前に必要なもの

  • Pythonの実行環境
  • Pandasライブラリ: 本記事の主役です。インストールされていない場合は、ターミナル(コマンドプロンプト)で以下のコマンドを実行してください。
python -m venv venv
.\venv\Scripts\activate
pip install pandas
  • 前回の成果物: parse_piyolog.py と、それによって生成されたJSONデータ。今回はこのJSONデータを入力として使います。

Pandas DataFrameにデータを読み込む

まずは、前回作成したパーサー (parse_piyolog.py) の末尾を少し改造して、解析結果をJSONファイルとして出力するようにしましょう。そして、そのJSONファイルをPandasで読み込みます。

1. parse_piyolog.py を修正してJSONファイルを出力

if __name__ == '__main__': ブロックを以下のように変更します。標準出力に表示していた部分を、ファイルに書き出す処理に置き換えるだけです。

# parse_piyolog.py の if __name__ == '__main__': ブロックを修正

if __name__ == '__main__':
    # スクリプトが直接実行された場合に以下の処理を行う
    logs_directory = 'logs'
    structured_data = parse_all_logs(logs_directory)

    # 日付と時刻でソートする
    structured_data.sort(key=lambda x: (x['date'], x['time']))

    # 結果をJSONファイルとして保存
    with open('piyolog_structured.json', 'w', encoding='utf-8') as f:
        json.dump(structured_data, f, indent=2, ensure_ascii=False)

    print(f"'{logs_directory}' ディレクトリのログを解析し、piyolog_structured.json に保存しました。")

このスクリプトを再度実行すると、piyolog_structured.json というファイルがプロジェクトフォルダ内に生成されます。

2. 新しいスクリプトでJSONをDataFrameに読み込む

ここからが本番です。preprocess_data.py という新しいファイルを作成し、Pandasを使ってデータを読み込みます。

# preprocess_data.py
import pandas as pd

# JSONファイルを読み込んでDataFrameを作成
df = pd.read_json('piyolog_structured.json')

# 最初の5行を表示して、正しく読み込めたか確認
print(df.head())

これを実行すると、スプレッドシートのような表形式でデータが表示されるはずです。この表が DataFrame で、Pandasでデータを扱う際の基本単位となります。

         date   time event        detail  memo
0  2022/05/24  22:15   おしっこ          None  None
1  2022/05/24  22:15    うんち          None  None
2  2022/05/24  22:30    ミルク           40ml  None
3  2022/05/24  22:40     寝る          None  None
4  2022/06/01  00:55    起きる     (1時間55分)  None

データ前処理の実装

ここから、このDataFrameを本格的に加工していきます。AIが理解しやすいように、イベントごとに情報を整理し、新しいカラムを追加していくのが目標です。

1. 日付と時刻を datetime 型に変換

まずは基本のキ。datetime カラムは現在ただの文字列(object型)です。これらを結合して、Pandasが時刻として認識できる datetime 型に変換します。これにより、後々「2時間後のイベント」のような時間計算が簡単にできるようになります。

# preprocess_data.py (続き)

# dateとtimeを結合して新しい'datetime'カラムを作成
# errors='coerce'は、変換できない値があった場合にエラーとせず、NaT(Not a Time)として扱うオプション
df['datetime'] = pd.to_datetime(df['date'] + ' ' + df['time'], errors='coerce')

# 元のdateとtimeカラムは不要なので削除
df = df.drop(columns=['date', 'time'])

# datetimeをインデックスに設定すると、時系列データとして扱いやすくなる
df = df.set_index('datetime')

# info()でデータ型が変換されたか、head()でデータの中身を確認
print("--- datetime変換後 ---")
print(df.info())
print(df.head())

df.info() の出力を見ると、インデックスが DatetimeIndex という型に変わっていることが確認できます。これで時系列データを扱う準備が整いました。

2. detail カラムをイベントごとに解析

次に、本丸である detail カラムのクレンジングです。このカラムには (1時間55分)左14分 ◀ 右7分 のように、イベントの種類によって全く異なる形式の情報が入っています。

そこで、イベントの種類 (event カラムの値) に応じて、それぞれ専用の解析処理を適用し、意味のある新しいカラム(sleep_duration_minmilk_ml など)を作成していきます。

ヘルパー関数を定義する

まずは、特定の文字列から数値を抽出するための小さな関数をいくつか定義します。正規表現が再び活躍します。

# preprocess_data.py (続き)
import re

def get_sleep_duration(detail):
    """ 'xx時間yy分' から合計分を計算する """
    if pd.isna(detail): return None
    h_match = re.search(r'(\d+)時間', str(detail))
    m_match = re.search(r'(\d+)分', str(detail))
    hours = int(h_match.group(1)) if h_match else 0
    minutes = int(m_match.group(1)) if m_match else 0
    return hours * 60 + minutes

def get_milk_volume(detail):
    """ 'xxxml' から数値(xxx)を抽出する """
    if pd.isna(detail): return None
    match = re.search(r'(\d+)ml', str(detail))
    return int(match.group(1)) if match else None

def get_breast_milk_time(detail, side):
    """ '左x分 / 右y分' などから左右それぞれの時間を抽出する """
    if pd.isna(detail): return 0 # データがない場合は0分とする
    
    pattern = r'左(\d+)分' if side == 'left' else r'右(\d+)分'
    match = re.search(pattern, str(detail))
    return int(match.group(1)) if match else 0

Pandasの apply を使ってデータを一括処理

次に、これらのヘルパー関数を使って、新しいカラムにデータを格納していきます。

# preprocess_data.py (続き)

# 「起きる」イベントから睡眠時間(分)を抽出
is_wakeup = df['event'] == '起きる'
df.loc[is_wakeup, 'sleep_duration_min'] = df.loc[is_wakeup, 'detail'].apply(get_sleep_duration)

# 「ミルク」イベントから量(ml)を抽出
is_milk = df['event'] == 'ミルク'
df.loc[is_milk, 'milk_ml'] = df.loc[is_milk, 'detail'].apply(get_milk_volume)

# 「母乳」イベントから左右の時間(分)を抽出
is_breast_milk = df['event'] == '母乳'
df.loc[is_breast_milk, 'breast_milk_left_min'] = df.loc[is_breast_milk, 'detail'].apply(get_breast_milk_time, side='left')
df.loc[is_breast_milk, 'breast_milk_right_min'] = df.loc[is_breast_milk, 'detail'].apply(get_breast_milk_time, side='right')

# 処理が終わった元のカラムは削除
df = df.drop(columns=['detail', 'memo'])

print("\n--- detail解析後 ---")
# '母乳'と'起きる'イベントの行だけをフィルタして表示してみる
print(df[is_breast_milk | is_wakeup].head())
  • df['event'] == '起きる' のような条件で、処理対象の行を絞り込みます(Boolean Indexing)。
  • .loc[行の条件, 列名] で対象のデータを選択し、.apply(関数) を使って一括で変換処理を適用しています。
  • これにより、イベントごとに最適化されたデータクレンジングが実現できました。

コード全体像

preprocess_data.py の最終的なコードは以下のようになります。

# preprocess_data.py
import pandas as pd
import re

# --- ヘルパー関数の定義 ---
def get_sleep_duration(detail):
    """ 'xx時間yy分' から合計分を計算する """
    if pd.isna(detail): return None
    h_match = re.search(r'(\d+)時間', str(detail))
    m_match = re.search(r'(\d+)分', str(detail))
    hours = int(h_match.group(1)) if h_match else 0
    minutes = int(m_match.group(1)) if m_match else 0
    return hours * 60 + minutes

def get_milk_volume(detail):
    """ 'xxxml' から数値(xxx)を抽出する """
    if pd.isna(detail): return None
    match = re.search(r'(\d+)ml', str(detail))
    return int(match.group(1)) if match else None

def get_breast_milk_time(detail, side):
    """ '左x分 / 右y分' などから左右それぞれの時間を抽出する """
    if pd.isna(detail): return 0
    pattern = r'左(\d+)分' if side == 'left' else r'右(\d+)分'
    match = re.search(pattern, str(detail))
    return int(match.group(1)) if match else 0

# --- メイン処理 ---
# 1. データの読み込み
df = pd.read_json('piyolog_structured.json')

# 2. datetimeへの変換とインデックス設定
# dateとtimeを文字列として結合し、datetime型に変換
df['datetime'] = pd.to_datetime(df['date'].astype(str) + ' ' + df['time'].astype(str), errors='coerce')
df = df.drop(columns=['date', 'time'])
df = df.set_index('datetime')

# 3. イベントごとのデータ抽出
# 「起きる」イベントから睡眠時間(分)を抽出
is_wakeup = df['event'] == '起きる'
df.loc[is_wakeup, 'sleep_duration_min'] = df.loc[is_wakeup, 'detail'].apply(get_sleep_duration)

# 「ミルク」イベントから量(ml)を抽出
is_milk = df['event'] == 'ミルク'
df.loc[is_milk, 'milk_ml'] = df.loc[is_milk, 'detail'].apply(get_milk_volume)

# 「母乳」イベントから左右の時間(分)を抽出
is_breast_milk = df['event'] == '母乳'
df.loc[is_breast_milk, 'breast_milk_left_min'] = df.loc[is_breast_milk, 'detail'].apply(get_breast_milk_time, side='left')
df.loc[is_breast_milk, 'breast_milk_right_min'] = df.loc[is_breast_milk, 'detail'].apply(get_breast_milk_time, side='right')

# 4. 不要なカラムの削除と結果の保存
df = df.drop(columns=['detail', 'memo'])

# 念のためインデックス(datetime)でソートして時系列順を保証します
df = df.sort_index()

# 処理結果をCSVファイルとして保存
df.to_csv('piyolog_preprocessed.csv')

print("--- 最終的なDataFrame ---")
print(df.head())
print("\n--- データ型 ---")
print(df.info())
print("\npiyolog_preprocessed.csv に保存しました。")

最後に

今回は、データ分析の土台作りとして非常に重要な「データ前処理」のプロセスを、Pandasを使って実践しました。生のデータをそのまま使うのではなく、このように一手間加えてあげることで、後続のAIモデルのパフォーマンスは劇的に向上します。

  • 文字列を datetime 型に変換して、時間の概念をコンピュータに理解させる。
  • イベントごとに文字列から数値や単位を分離して、計算可能なデータと意味のあるカテゴリに分ける。

これらの作業は、RAGシステムに限らず、あらゆるデータサイエンスのタスクで基本となる考え方です。

さて、これでようやくAIに入力するための「きれいなデータ」が準備できました。次回は、このデータをRAGシステムで使えるように、意味のある塊(チャンク)に分割する「チャンキング」というステップに進みます。

次の記事