つたはすのブログ

獲得した知識をアウトプットしていきます

【tsfresh】列名から特徴量生成用の辞書を作成する

1. 概要

tsfreshで生成したデータの列名から特徴量生成用の辞書を作成する方法を解説する。

2. 背景

tsfreshはデフォルトの設定だと、800種類近くの特徴量を生成する。
これらに対してFilter MethodやLasso回帰を使って特徴量選択を行い、次回以降は選択した特徴量のみを生成するようにしたかった。

3. tsfreshによる特徴量生成

3-1. データの作成

特徴量作成対象となる時系列データを作成する。今回は正弦波のデータを用意する。

import numpy as np
import pandas as pd

t = np.linspace(0, 1, 100)
sin_data = np.sin(2*np.pi*t)
data_group = ["sin_data"] * len(t)
data_df = pd.DataFrame({"sin": sin_data , "group":data_group })

data_dfが今回の特徴量作成対象となる時系列データである。tsfreshの特徴量生成関数tsfresh.extract_featuresではデータのまとまりを表す列を指定する必要があるため、"group"という列を用意した。このソースコードでは準備した100レコードは1つの時系列データとみなすことになる。

例えば次のようにすれば、2つの時系列データと解釈される。

import numpy as np
import pandas as pd

t = np.linspace(0, 1, 100)
sin_data = np.sin(2*np.pi*t)
# 時系列データを前半と後半に分割
data_group = ["sin_data1"] * len(t)//2 + ["sin_data2"] * len(t)//2
data_df = pd.DataFrame({"sin": sin_data , "group":data_group })

3-2. 特徴量生成

tsfreshを使って特徴量を生成するには次のようなコードを実行すれば良い。

import tsfresh
from tsfresh.feature_extraction import ComprehensiveFCParameters

# 作成対象の特徴量
fcparams_dict = ComprehensiveFCParameters()

# 特徴量作成
ts_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=fcparams_dict
)

tsfresh.extract_featuresのパラメータdefault_fc_parametersは省略することができるが、省略するとComprehensiveFCParameters()が設定されるので上記のソースコードの場合指定しなくても結果は同じである。このComprehensiveFCParameters()は作成対象の特徴量名とパラメータを指定する辞書である。

これは特徴量名をキーとして、パラメータを値とする辞書で内容は以下のようになっている。

{'variance_larger_than_standard_deviation': None, 
'has_duplicate_max': None, 
'has_duplicate_min': None, 
(省略)
'cid_ce': [{'normalize': True}, {'normalize': False}], 
'symmetry_looking': [{'r': 0.0}, {'r': 0.05}, {'r': 0.1}, {'r': 0.15000000000000002}, {'r': 0.2}, {'r': 0.25}, {'r': 0.30000000000000004}, {'r': 0.35000000000000003}, {'r': 0.4}, {'r': 0.45}, {'r': 0.5}, {'r': 0.55}, {'r': 0.6000000000000001}, {'r': 0.65}, {'r': 0.7000000000000001}, {'r': 0.75}, {'r': 0.8}, {'r': 0.8500000000000001}, {'r': 0.9}, {'r': 0.9500000000000001}], 
(省略)
'mean_n_absolute_max': [{'number_of_maxima': 7}]}

ノンパラメトリックな特徴量はNoneが、パラメトリックな特徴量はパラメータのリストが設定されている。

3-3. 生成された特徴量

ここではtsfreshで生成された特徴量の名前を確認する。

特徴量を生成する関数tsfresh.extract_featuresの返り値はpandas.DataFrameであるから、その列名を確認することでどのような特徴量が作成されたか確認することができる。

# 生成した特徴量の列名を取得
ts_cols = list(ts_features.columns)

ts_colsは次のようになっている。

['sin__variance_larger_than_standard_deviation', 
'sin__has_duplicate_max', 
'sin__has_duplicate_min', 
'sin__has_duplicate', 
(省略)
'sin__mean_n_absolute_max__number_of_maxima_7']

今回はこの列名から特徴量生成用の辞書を作成することが目的である。

4. 特徴量列名から辞書へ

4-1. 特徴量列名の構成

tsfresh.extract_featuresで生成された特徴量DataFrameの列名は次のような構成になっている。

ノンパラメトリック特徴量:データ名__特徴量名
パラメトリック特徴量:データ名__特徴量名__パラメータ名1_パラメータ値1__パラメータ名2_パラメータ値2....

今回作成した特徴量の列名を見ると「sin__variance_larger_than_standard_deviation」があるが、最初の「sin」は特徴量作成対象のdata_dfの「sin」列から特徴量を作成したことを表す。後半の「variance_larger_than_standard_deviation」は特徴量名である。これはノンパラメトリックな特徴量の例である。この場合

"variance_larger_than_standard_deviation": None

という辞書の要素に変換できれば良い。

一方パラメトリックな特徴量の例として「sin__mean_n_absolute_max__number_of_maxima_7」では、「mean_n_absolute_max」が特徴量名でパラメータ「number_of_maxima」に7を指定していることを意味する。この場合では

"mean_n_absolute_max": [{"number_of_maxima": 7}]

という辞書の要素に変換できれば良い。

4-2. 辞書の作成1(不要な部分の削除)

最初に列名の最初の部分(今回の場合の「sin__」の部分)は不要なので、取り除く処理をする。

cols_list = ["__".join(x.split("__")[1:]) for x in ts_cols]

ts_colsは生成した特徴量の列からなるリストである。これの要素を「__」で分割して最初の要素を削除(x.split("__")[1:]の部分)して、最初以外の要素を「__」で結合している。("__".join(...)の部分)

「sin__mean_n_absolute_max__number_of_maxima_7」を例に挙げると、

  • sin
  • mean_n_absolute_max
  • number_of_maxima_7

に分割して、最初の「sin」を捨てて

  • mean_n_absolute_max
  • number_of_maxima_7

にする。これらを「__」で結合して「mean_n_absolute_max__number_of_maxima_7」にしている。

4-3. 辞書の作成2(ノンパラメトリック特徴量)

目的の辞書の要素の内でノンパラメトリックな特徴量の部分を作成する。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名

    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
       後述

特徴量の列名からデータ名の部分は削除済みなので、

ノンパラメトリック特徴量:特徴量名
パラメトリック特徴量:特徴量名__パラメータ名1_パラメータ値1__パラメータ名2_パラメータ値2....

という構成になっているので、「__」で分割したリストの最初の要素は特徴量名となっている。(変数nameに格納)

また、リストの長さは次のようになる。

ノンパラメトリック特徴量:1 
パラメトリック特徴量:パラメータの数 + 1 

ノンパラメトリックな特徴量については

特徴量名: None

という辞書の要素に変換できれば良いため

my_params_dict[name] = None

としている。

4-4. 辞書の作成3(パラメトリック特徴量)

ここがこの記事の本題である。考慮するべき要素が多いため内容を分割することにする。

4-4-1. パラメータ名と設定値の分割

パラメトリックな特徴量の列名を「__」で分割すると

特徴量名
パラメータ名1_パラメータ値1
パラメータ名2_パラメータ値2
パラメータ名3_パラメータ値3
...

という要素からなるリストになるので、最初の要素以外を「_」で分割して

{
パラメータ名1: パラメータ値1
パラメータ名2: パラメータ値2
パラメータ名3: パラメータ値3
...
}

という辞書を作成すれば良さそうである。そこで次の処理を考える。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            param, value = param_value.split("_")

最後のparam, value = param_value.split("_")の部分がここでのポイントである。上で説明したとおり、「_」で分割してパラメータ名と設定値に分割して、それぞれparamとvalueに入れようとしているが、これではエラーが発生する。

原因の一つは「f_agg_"mean"」という要素である。これは「f_agg」がパラメータ名で「mean」が設定値である。しかし「_」で分割すると

f
agg
"mean"

の3要素となってしまい、これらを2つの変数param, valueに代入しようとしてエラーとなってしまう。

このようにパラメータの中には名前に「_」が含まれるものが存在しているので、これを考慮する必要がある。
一方で設定値の方には「_」は含まれていない(ComprehensiveFCParameters()の内容を確認)ので、「_」で分割した上で最後の要素を設定値として、それ以前をパラメータ名の一部と判定すれば良い。そこで上の処理を修正して次のようにする。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)

修正点はparam_listの前に「*」をつけて「_」で結合する処理を加えただけである。変数の前に「*」をつけることで最後の要素だけvalueに格納でき、残りはリストとしてparam_listに格納される。リストparam_listを再び「_」で結合することでパラメータ名にすることができる。

4-4-2. 設定値の編集(整数以外)

列名はすべて文字列型であるが、パラメータの中には整数型や不動点小数型で指定しなければならないものが存在しているため、適切な型変換を行う必要がある。
ここで対処するものについては次に列挙する。

  • 不要な「"」を削除
  • タプル型への変換
  • Noneの対応
  • 真偽型への変換
  • 小数型への変換

まず、不要な「"」を削除することについて、例えば「f_agg_"mean"」というものがあり、これまでの処理では設定値が「"mean"」となるが、設定用の辞書では「mean」となっているので、「"」を削除する処理を追加する。
次にタプル型への変換について、例えば「widths_(2, 5, 10, 20)」という要素があり、これは設定値としては(2, 5, 10, 20)というタプルであるが、現在の処理だと(2, 5, 10, 20)という文字列である。そこでこれをタプル型に変換する必要がある。
真偽型や小数型についても、文字列型を適切な型への変換が必要である。
整数型についても同様の変換が必要であるが、これは他よりも複雑な問題があるため分けて説明をする。

設定値の変換については関数として定義することにした。

def get_value(str_value):
    # 不要な「"」を削除
    value = str_value.replace('\"', '')
    value = str_value
    
    # タプル型への変換
    if "(" in value:
        value_list = value.replace("(", "").replace(")", "")
        value_list = value_list.split(",")
        # タプル型設定値はすべて整数型
        value = tuple(map(int, value_list))

    # Noneの対応
    elif value == "None":
        value = None

    # 小数型への変換(小数点を含む場合)
    elif "." in value:
        value = float(value)
        
    # 真偽型への変換
    elif value == "True":
        value = True
    elif value == "False":
        value = False

    return value

この関数を使って辞書作成処理を修正する。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            # 設定値を編集
            value = get_value(value)

修正点は最後のvalue = get_value(value)である。これで設定値のほとんどは適切な型へ変換された。

4-4-3. 設定値の編集(整数)

ここでは設定値が整数である場合の編集をおこなうが、ややこしいことに特徴量によっては文字列型で渡す必要がある。
例えば「matrix_profile」という特徴量のパラメータ「feature」に25という値を設定するときには文字列型で設定する必要がある。(他にもmeanやmedianなどの文字を設定することができるようにするためと思われる。)

対処方法としては、とりあえず整数型にしておいて、tsfreshで用意されている設定用辞書を参考に文字列型だった場合は文字列型に変換する方法を採用する。

まずは設定値編集用の関数を修正する。

def get_value(str_value):
    # 不要な「"」を削除
    value = str_value.replace('\"', '')
    # タプル型への変換
    if "(" in value:
        value_list = value.replace("(", "").replace(")", "")
        value_list = value_list.split(",")
        # タプル型設定値はすべて整数型
        value = tuple(map(int, value_list))
        
    # Noneの対応
    elif value == "None":
        value = None
        
    # 小数型への変換(小数点を含む場合)
    elif "." in value:
        value = float(value)
        
    # 真偽型への変換
    elif value == "True":
        value = True
    elif value == "False":
        value = False

    else:
        # とりあえず整数型に
        try:
            value = int(value)
        finally:
            return value
        
    return value

修正箇所は最後のelseの処理でとりあえず整数型に変換している。

次に辞書作成処理の修正であるが、手本の辞書を参照して、それが文字列型だった場合は型変換をするようにしている。

from tsfresh.feature_extraction import ComprehensiveFCParameters

# 手本の辞書
fcparams_dict = ComprehensiveFCParameters()
# 手本の辞書にないパラメータ
ng_param = ["autolag"]

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            # 手本にないパラメータは処理をしない
            if param in ng_param:
                continue

            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)

手本の辞書は特徴量生成の際に生成対象を指定しなかった場合に使われる辞書である。これまでの処理をして得られたパラメータの中に手本の辞書に存在しないもの(autolag)があったので、そのパラメータについては処理をしないようにしている。
そして最後の処理が今回のポイントで、手本のパラメータの設定値が文字列型の場合は処理中の設定値を文字列型に変換している。

以上で設定値の編集は完了した。

4-4-4. 辞書の作成

これまでの処理でパラメトリックな特徴量について、特徴量の名前・パラメータ・設定値に分解した。ここではこれらを使って特徴量生成用辞書を作成する。
ここでのポイントは特徴量の中には複数パターンのパラメータ設定値が存在しており、リストで格納する必要があるということである。

例えば「cid_ce」という特徴量は「normalize」というパラメータが存在しているが、設定値にはTrueとFalseを選択することができる。このとき、辞書の要素としては次のような形になる。

'cid_ce': [{'normalize': True}, {'normalize': False}]

処理の方針としては、上記の例の場合、{'normalize': True}と{'normalize': False}という設定値パターンの辞書を作り、これらを要素とするリストを作成する。
ソースコードは次の通りである。

from tsfresh.feature_extraction import ComprehensiveFCParameters

# 手本の辞書
fcparams_dict = ComprehensiveFCParameters()
# 手本の辞書にないパラメータ
ng_param = ["autolag"]

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        # 設定値パターン辞書
        param_dict_tmp = {}

        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            if param in ng_param:
                continue
            
            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)
            # 設定値パターン辞書設定
            param_dict_tmp[param] = value
        
        # 設定値パターン辞書を追加
        if name in my_params_dict:
            my_params_dict[name].append(param_dict_tmp)
        else:
            my_params_dict[name] = [param_dict_tmp]

パラメトリック特徴量の処理の最初に設定値パターンの辞書を定義して、これまでの処理をしたあとに設定値パターン辞書を設定する。
その後に特徴量に対応する設定値パターンの辞書を要素とするリストを作成している。1つ以上設定値パターンを作成している場合はappendして、そうでなければ新しくリストを定義している。

4-5. 作成した辞書で特徴量を生成

上記の処理により作成した辞書my_params_dictを使って特徴量を生成してみることにする。
比較のために、デフォルト設定での特徴量生成も実施する。

ts_my_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=my_params_dict
)
ts_ori_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
)

print(len(ts_my_features.columns))
print(len(ts_ori_features.columns))

これの結果はどちらも789となり生成される特徴量の数は同じとなった。

生成された特徴量の列名が同じになるかも確認する。

print((ts_my_features.columns == ts_ori_features.columns).sum())

結果は789となり列数と同じとなった。

5. 選択した特徴量を再生成

ここでは一度デフォルト設定で特徴量を生成してから部分的に列を指定して、同じ特徴量を再生成できることを確認する。

まずはデフォルト設定で789個の特徴量を作成して、その中から200種類の特徴量を選択する。

# デフォルト設定で特徴量を生成
ts_ori_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
)

# ランダムに200個特徴量を選択
sample_features = np.random.choice(list(ts_ori_features.columns), size=200, replace=False)
# 前処理
cols_list = ["__".join(x.split("__")[1:]) for x in sample_features]

次に前処理済の特徴量列から辞書を作成する。(ソースコードは上記のものと同じ)

# 作成する辞書
my_params_dict = {}
ng_param = ["autolag"]


for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        # 設定値パターン辞書
        param_dict_tmp = {}
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            if param in ng_param:
                continue
            
            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)
            # 設定値パターン辞書設定
            param_dict_tmp[param] = value
        
        # 設定値パターン辞書を追加
        if name in my_params_dict:
            my_params_dict[name].append(param_dict_tmp)
        else:
            my_params_dict[name] = [param_dict_tmp]

最後に作成した辞書から特徴量を生成する。

ts_my_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=my_params_dict
)

print(len(ts_my_features.columns))

結果は200となり、生成された特徴量の数は正しい。生成された特徴量の種類も同じか確認しておく。

sample_features = list(sample_features)  # ランダムに選択した特徴量
my_features = list(ts_my_features.columns)  # 再生成した特徴量

# 比較のためにソートしておく
sample_features.sort()
my_features.sort()

 # 同じ列名の数
print((np.array(sample_features) == np.array(my_features)).sum())

結果は200となり、選択したものと同じ列が生成されている。

6. ソースコード全体

import tsfresh
from tsfresh.feature_extraction import ComprehensiveFCParameters

import numpy as np
import pandas as pd

# データ作成
t = np.linspace(0, 1, 100)
sin_data = np.sin(2*np.pi*t)
data_df = pd.DataFrame({"sin": sin_data , "type": ["sin data"]*len(sin_data)})

# 特徴量作成
ts_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=fcparams_dict
)

def get_value(str_value):
    # 不要な「"」を削除
    value = str_value.replace('\"', '')
    # タプル型への変換
    if "(" in value:
        value_list = value.replace("(", "").replace(")", "")
        value_list = value_list.split(",")
        # タプル型設定値はすべて整数型
        value = tuple(map(int, value_list))
        
    # Noneの対応
    elif value == "None":
        value = None
        
    # 小数型への変換(小数点を含む場合)
    elif "." in value:
        value = float(value)
        
    # 真偽型への変換
    elif value == "True":
        value = True
    elif value == "False":
        value = False

    else:
        # とりあえず整数型に
        try:
            value = int(value)
        finally:
            return value
        
    return value

# 辞書作成
# 作成する辞書
my_params_dict = {}
ng_param = ["autolag"]


for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        # 設定値パターン辞書
        param_dict_tmp = {}
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            if param in ng_param:
                continue
            
            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)
            # 設定値パターン辞書設定
            param_dict_tmp[param] = value
        
        # 設定値パターン辞書を追加
        if name in my_params_dict:
            my_params_dict[name].append(param_dict_tmp)
        else:
            my_params_dict[name] = [param_dict_tmp]

# 特徴量再生成
ts_my_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=my_params_dict
)