データロガー ~UnityによるHCI研究 その1~

この記事では,Unityで研究する際に,測定したデータをcsvデータに出力するデータロガーのおすすめ実装を紹介します.ポイントは,①正確であること,②変更が容易であること,の2点です.

基本構造

Dataというクラスに値を格納し,DataLoggerというクラスを作って複数のDataをまとめて出力します.DataとDataLoggerは実験に合わせて適宜継承します(ここではそれぞれDataExample,DataLoggerExampleとします).

サンプルコード

Data.cs

using System.Collections.Generic;

public abstract class Data
{
    /// <summary>
    /// 参加者ID
    /// </summary>
    public int Id { private set; get; }
    /// <summary>
    /// 実験条件
    /// enumで管理するとなお良い
    /// </summary>
    public int Condition { private set; get; }
    /// <summary>
    /// 値を全て取得する
    /// </summary>
    public abstract List<object> Values { get; }
    /// <summary>
    /// CSVファイルの1行目
    /// </summary>
    public abstract string Header { get; }

    /// <summary>
    /// コンストラクタ
    /// 適宜overrideしてValuesを追加する
    /// </summary>
    /// <param name="id"></param>
    /// <param name="condition"></param>
    public Data(int id, int condition)
    {
        Id = id;
        Condition = condition;
    }
    /// <summary>
    /// string型へのキャスト
    /// CSVの2行目以降を構成するように各値をコンマで繋ぐ
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        var result = $"{Id},{Condition}";
        for (int i=0; i<Values.Count; i++)
        {
            result += $",{Values[i].ToString()}";
        }
        return result;
    }
}

Conditionはint型で定義していますが,enum等で定義しても良いでしょう(enumとは整数にそれぞれラベルを付けてわかりやすくしたもの).

DataExample.cs

public class DataExample : Data
{
    // ここを実験で取りたいデータに書き換える
    public float Hoge { private get; set; }
    public float Fuga { private get; set; }
    public float Piyo { private set; get; }

    // 上記のデータをValuesとHeaderに反映させる
    public override List<object> Values => new List<object>()
    {
        Hoge,
        Fuga,
        Piyo,
    };
    public override string Header => "ID,Condition,Hoge,Fuga,Piyo";

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="id"></param>
    /// <param name="condition"></param>
    /// <param name="hoge"></param>
    /// <param name="fuga"></param>
    /// <param name="piyo"></param>
    public DataExample(int id, int condition, float hoge, float fuga, float piyo) : base(id, condition)
    {
        Hoge = hoge;
        Fuga = fuga;
        Piyo = piyo;
    }
}

サンプルコード中にfloat型のhoge,fuga,piyoが出てきていますが,ここは取りたいデータに合わせて型と変数名を改変してください.csvファイルにするので型はint型,float型,bool型,string型のうちのどれかにすると良いでしょう(Vector3型は3つのfloat型にするなど,適宜分割する).

DataLogger.cs

using System.IO;
using System.Text;
using UnityEngine;

public abstract class DataLogger<T> : MonoBehaviour where T : Data
{
    /// <summary>
    /// データをまとめたリスト
    /// </summary>
    protected List<T> DataList = new List<T>();
    /// <summary>
    /// ファイル名
    /// 重複が発生しないよう日付時刻で決める等する
    /// </summary>
    protected string FileName
    {
        get
        {
            var time = DateTime.Now;
            return $"{time.Year}_{time.Month}_{time.Day}_{time.Hour}_{time.Minute}_{time.Second}.csv";
        }
    }
    [Header("Assets以下のディレクトリを指定する")]
    public string DataPath = "ExperimentData";

    public void Add(T data)
    {
        DataList.Add(data);
        Debug.Log($"DataLogger<{typeof(T)}>.Add: Added a new data.");
    }
    /// <summary>
    /// CSVファイルを生成してデータを出力する
    /// </summary>
    /// <param name="hasHeader"></param>
    public void Export(string fileName, string directory, bool hasHeader=false)
    {
        // FileNameの名前でCSVファイルを生成する
        var file = new StreamWriter($"{Application.dataPath}/{directory}/{fileName}", false, Encoding.GetEncoding("Shift_JIS"));

        // 1行目:
        if (hasHeader)
        {
            file.WriteLine(DataList[0].Header);
        }

        // 2行目以降:
        foreach (Data data in DataList)
        {
            file.WriteLine(data.ToString());
        }

        // StreamWriterを破棄する
        file.Close();
        Debug.Log($"DataLogger<{typeof(T)}>.Export: Exported {fileName} to {directory}.");
    }
    /// <summary>
    /// CSVファイルを生成してデータを出力する
    /// </summary>
    /// <param name="hasHeader"></param>
    public void Export(string directory, bool hasHeader = false) => Export(FileName, directory, hasHeader);
    /// <summary>
    /// CSVファイルを生成してデータを出力する
    /// </summary>
    /// <param name="hasHeader"></param>
    public void Export(bool hasHeader = false) => Export(DataPath, hasHeader);
}

謎の<T>の表記がありますが,これはジェネリクスといい,ここではDataクラスやその継承クラスが入ります.以下に書くような具体的なDataLoggerクラスでは扱いたいDataクラスを指定して使います.
そういうわけでDataListはDataクラスのリストになります.今回はデータの追加だけをして削除はしたくないので,DataListをprivateに設定し,Add関数だけを露出させています.

DataLoggerExample.cs

public class DataLoggerExample : DataLogger<DataExample>
{
    // DataExample型のデータを集めると指定すること以外は特に変更しない
}

データ追加

データを追加する場合は以下のようにします.

float hogeData = 0.4f;
float fugaData = 0.8f;
float piyoData = 1.4f;
DataLogger.Add(new DataLogger.Data(0, 1, hogeData, fugaData, piyoData);

データの追加は,FixedUpdate毎に行うか,ボタン等の何らかのアクションの実行時に行うのが普通だと思います.これは取りたいデータの種類に依存するでしょう.

データ出力

データをcsvファイルに出力する場合は以下のようにします.

DataLogger.Export();

サンプルコードでは引数を与えなかった場合,Assets/ExperimentData/に日付と時刻で構成されたファイルが生成され,その中にDataListの中身がそれぞれ記述されます.1行目にヘッダを付けるかどうか指定することもできます.ディレクトリやファイルの名称はお好みで調整してください.

工夫①(正確さの担保)

実験用プログラムは動作が正確であることが重要です.データロガーは解析するデータを直接生成するプログラムですから,なおさら重要です.
サンプルコードでは,記録する情報をDataクラスにまとめたり,DataListで各Data構造体のインスタンスを保持するようにしたりして,コード量の削減に努めました.こうすると,コードが明瞭になってヒューマンエラーが減ります.

工夫②(変更容易性の担保)

ある実験用プログラムを他の実験でも使いたいという場合は多いでしょう.サンプルコードでは,hoge,fuga,piyoにあたる部分の記述を変更するだけで,取りたいデータの種類を変更することができるようにしました.
また,複数種類のデータを出力できるよう,Dataクラスを継承して使うことを前提に設計しました.


この記事が気に入ったらサポートをしてみませんか?