Article

Feature Engineering自動化で開発を加速

高田晃太郎
Feature Engineering自動化で開発を加速

Anaconda State of Data Science(2022)によると、データサイエンティストは作業時間の約38%をデータ準備に費やしていると報告されています[1]。モデル選択やハイパーパラメータ探索が高度化しても、現場のボトルネックは依然として特徴量エンジニアリングとデータ配管です[2]。Web開発の文脈では、ABテストやリリースサイクルが短いほど、毎週のようにラベルの定義やスキーマが揺れます。多くの現場で観察される傾向として、人手による特徴量起票と手作業の前処理が、リードタイムと運用コストの主要因になりがちです。ここで重要なのは、理想的な特徴量を一気に作り切ることではありません。自動化により“作る・選ぶ・配る”を高速反復できる状態をつくることです[3]。この記事では、CTOやエンジニアリーダーが即日着手できる設計と実装、評価のフレーム、そしてROIの考え方までを一気通貫で解説します。

なぜ今、特徴量エンジニアリングを自動化するのか

特徴量はモデルの上流に位置し、品質が下流すべてに波及します。学習器の選択や最適化よりも、適切な表現を素早く見出し、再現可能に供給できることが、プロダクトKPIの改善速度を左右します。自動化の価値は三つの軸で語れます。第一にリードタイム短縮です。データスキーマの追加やイベントの粒度変更に対して、定義を修正すれば再生成が自動で走る状態にすることで、企画から検証までの往復が縮まります。第二に品質の一貫性です。学習時と推論時の特徴量ロジック差異は、典型的な精度劣化の原因です。Feature Store(特徴量定義を一元管理する仕組み)とコード生成の統一を前提にすれば、学習・推論で同一ロジックを再利用できます[4][7]。第三に運用コストの最適化です。人手による試行錯誤を、探索と選択のアルゴリズムに肩代わりさせるほど、反復あたりのクラウドコストと人件費を抑制できます[3]。なお、自動化の導入初期は一時的に複雑度が上がりますが、モジュール境界を明確にし、測定しながら進めれば、数スプリントで投資回収の見通しが立つことは珍しくありません。

自動化の前提条件と落とし穴を先に潰す

設計に先立ち、データの時間的一貫性とID設計を妥協しないことが肝要です。リーク(学習時に本来見えない未来情報が紛れ込むこと)を防ぐためにウィンドウ境界と遅延の扱いを仕様化し、派生特徴量の生成ルールにバージョニングを持たせます[5]。欠損と外れ値は単なる前処理ではなく、ドメインの意図と収集仕様の表現としてモデルに渡るべき対象です。さらに、スキーマ変更やバケット移動を検知するメタデータ監視を用意しておくと、夜間バッチの崩壊を未然に防げます。

アーキテクチャ設計:作る・選ぶ・配るを分離する

自動化のコアは、生成、選択、提供の三機能を疎結合に保つことです。生成はドメインテーブルを受け取り、ウィンドウ、集計、エンコーディング、時系列特徴抽出などを規則で拡張します。選択はタスク依存で、特徴量の寄与を多面的に評価し、冗長とリークを抑えます。提供はオンライン・オフラインの双方向に同一定義を配給し、推論のレイテンシ制約を満たします。

スキーマと時間を中心に据えた生成パイプライン

生成では、Deep Feature Synthesis(関係データを再帰的集約して特徴を作る手法)、ターゲットエンコーディング、テキストや時系列の統計量化などを、関数合成で表現します[5]。重要なのは、時間窓と遅延の宣言的定義です。ラベル時刻tに対して、観測可能な最大時刻t−δのみを入力に許し、ウィンドウ幅wを仕様に固定します。仕様からコードを生成するアプローチを採ると、レビューと再現性が飛躍的に向上します。

選択は単一指標に依存しない

選択段階では、交差検証での安定重要度、SHAP(特徴量貢献度の一貫的な説明手法)の一貫性[10]、Permutation Importance(特徴量の入れ替えによる頑健な重要度評価)[11]、相関閾値やVIF(分散拡大要因)での多重共線性制御[12]を組み合わせます。オーバーフィッティングを防ぐため、選択自体を学習フローに内包し、データ分割の外側に位置させないことが原則です。特徴量のドロップや変換は、学習パイプラインの一部としてシリアライズし、実運用での再現を保証します。

Feature Storeで学習と推論を同一化する

Feature Storeは、定義、計算、提供、監査のハブです[7]。オフラインは学習用データセットに結合し、オンラインはキャッシュとオンデマンド計算を選択してレイテンシを満たします。スキーマの互換性検査とバックフィルのパスを整備すれば、プロダクションでのロールフォワードが安全になります[4]。

実装例:Pythonで組む自動化の最小構成

ここからは、最小構成で再現可能な実装を段階的に示します。すべてのコードはインポートを含む完全形で提示し、例外処理とログ出力を加えています。実運用ではリポジトリとCI上での検証フローに組み込み、同一のコードを学習と推論で使用してください(ライブラリは環境に合わせてインストールしてください)。

例1:前処理と基本的な特徴量生成をPipelineで一体化

import logging
from typing import List, Tuple
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold, cross_validate
from sklearn.preprocessing import StandardScaler, FunctionTransformer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.metrics import f1_score, roc_auc_score, make_scorer
from sklearn.linear_model import LogisticRegression

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("fe_baseline")

try:
    X_num, y = make_classification(n_samples=5000, n_features=10, n_informative=6, random_state=42)
    df = pd.DataFrame(X_num, columns=[f"num_{i}" for i in range(10)])
    rng = np.random.default_rng(42)
    df["cat_a"] = rng.choice(["A", "B", "C"], size=len(df))
    df["cat_b"] = rng.choice(["X", "Y"], size=len(df))

    num_cols = [c for c in df.columns if c.startswith("num_")]
    cat_cols = ["cat_a", "cat_b"]

    def add_poly(X: pd.DataFrame) -> pd.DataFrame:
        Z = X.copy()
        for c in num_cols:
            Z[c+"_sq"] = Z[c] ** 2
            Z[c+"_log"] = np.log1p(np.abs(Z[c]))
        return Z

    num_pipe = Pipeline([
        ("poly", FunctionTransformer(add_poly, validate=False)),
        ("scaler", StandardScaler()),
    ])

    # 簡易カテゴリ変換(ターゲットエンコーディングはリークに注意。ここではone-hot)
    cat_pipe = Pipeline([
        ("ohe", FunctionTransformer(lambda X: pd.get_dummies(X.astype(str)), validate=False))
    ])

    pre = ColumnTransformer([
        ("num", num_pipe, num_cols),
        ("cat", cat_pipe, cat_cols)
    ], remainder="drop", verbose_feature_names_out=False)

    clf = Pipeline([
        ("pre", pre),
        ("model", LogisticRegression(max_iter=1000))
    ])

    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    scoring = {
        "f1": make_scorer(f1_score),
        "auc": make_scorer(roc_auc_score, needs_threshold=True)
    }

    res = cross_validate(clf, df, y, cv=cv, scoring=scoring, n_jobs=1, return_estimator=False)
    logger.info({k: float(np.mean(v)) for k, v in res.items() if k.startswith("test_")})
except Exception as e:
    logger.exception("Baseline pipeline failed: %s", e)

この最初の例では、数値の二次項と対数項を自動付与し、カテゴリは安全側でワンホット化しています。リークを避けるため、ターゲットエンコーディングを使う場合は必ず交差検証の折り目の内側で学習・適用してください。

例2:Featuretoolsでリレーショナルデータから自動集約

import logging
import pandas as pd
import numpy as np
import featuretools as ft

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("fe_dfs")

try:
    customers = pd.DataFrame({
        "customer_id": [1, 2, 3],
        "joined": pd.to_datetime(["2023-01-01", "2023-02-15", "2023-03-10"]),
        "segment": ["A", "B", "A"]
    })
    orders = pd.DataFrame({
        "order_id": [10, 11, 12, 13, 14],
        "customer_id": [1, 2, 1, 3, 1],
        "amount": [120.5, 80.0, 45.0, 200.0, 75.5],
        "order_time": pd.to_datetime([
            "2023-05-01 10:00", "2023-05-01 11:00", "2023-05-02 09:00", "2023-05-03 12:00", "2023-05-04 14:00"
        ])
    })

    es = ft.EntitySet("retail")
    es = es.add_dataframe(dataframe_name="customers", dataframe=customers, index="customer_id", time_index="joined")
    es = es.add_dataframe(dataframe_name="orders", dataframe=orders, index="order_id", time_index="order_time")
    es = es.add_relationship("customers", "customer_id", "orders", "customer_id")

    cutoff = pd.DataFrame({"customer_id": [1, 2, 3], "time": pd.to_datetime(["2023-05-05"]*3)})

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name="customers",
        agg_primitives=["sum", "mean", "std", "count", "max"],
        trans_primitives=["day", "month"],
        cutoff_time=cutoff,
        max_depth=2
    )
    logger.info("generated_features=%d", feature_matrix.shape[1])
except Exception as e:
    logger.exception("DFS generation failed: %s", e)

Deep Feature Synthesisを使うと、関係モデルから自然に集約特徴を生み出せます[5]。特に注文履歴やイベントログのような時間付きのリレーショナルデータで効果を発揮します。cutoff_timeの指定でリークを防げる点が実務上の要となります[5]。

例3:tsfreshで時系列から統計的特徴を一括抽出

import logging
import numpy as np
import pandas as pd
from tsfresh import extract_features
from tsfresh.utilities.dataframe_functions import make_forecasting_frame

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("fe_tsfresh")

try:
    rng = np.random.default_rng(0)
    ts = np.cumsum(rng.normal(size=200)) + 0.1 * np.arange(200)
    df, y = make_forecasting_frame(pd.Series(ts), kind="sensor", max_timeshift=10, rolling_direction=1)
    X = extract_features(df, column_id="id", column_sort="time")
    logger.info("ts_features=%d", X.shape[1])
except Exception as e:
    logger.exception("tsfresh extraction failed: %s", e)

時系列は人手で特徴を設計すると偏りが生じがちです。tsfreshは統計検定に裏づけられた関数群で幅広く特徴を抽出でき、以降の選択フェーズと組み合わせることで過学習のリスクを抑えつつ表現力を高められます[6]。

例4:LightGBMとSelectFromModelで頑健な特徴選択

import logging
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
from sklearn.feature_selection import SelectFromModel
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from lightgbm import LGBMClassifier

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("fe_select")

try:
    X, y = make_classification(n_samples=4000, n_features=60, n_informative=15, random_state=0)
    X = pd.DataFrame(X, columns=[f"f{i}" for i in range(X.shape[1])])

    selector = SelectFromModel(
        estimator=LGBMClassifier(
            n_estimators=300, learning_rate=0.05, max_depth=-1, subsample=0.8, colsample_bytree=0.8, random_state=0
        ),
        threshold="median"
    )

    pipe = Pipeline([
        ("scaler", StandardScaler(with_mean=False)),
        ("select", selector),
        ("model", LGBMClassifier(n_estimators=400, learning_rate=0.05, random_state=0))
    ])

    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
    aucs, kept_counts = [], []
    for tr, va in cv.split(X, y):
        pipe.fit(X.iloc[tr], y[tr])
        pred = pipe.predict_proba(X.iloc[va])[:, 1]
        aucs.append(roc_auc_score(y[va], pred))
        kept_counts.append(int(pipe.named_steps["select"].get_support().sum()))
    logger.info("mean_auc=%.4f, kept_features_median=%d", np.mean(aucs), int(np.median(kept_counts)))
except Exception as e:
    logger.exception("feature selection failed: %s", e)

ツリーベースのモデルは非線形性と相互作用を部分的に内包するため、過剰な派生特徴を抑えても精度を確保しやすい利点があります[8]。SelectFromModelは重要度の閾値を柔軟に調整でき、計算資源と精度のバランスを取りやすい選択です。

例5:Feastで学習・推論の特徴量定義を一元管理

from datetime import timedelta
import pandas as pd
from feast import FeatureStore, Entity, Field, FeatureView, FileSource
from feast.types import Float32, Int64

customer = Entity(name="customer_id", join_keys=["customer_id"]) 

source = FileSource(
    path="data/customer_features.parquet",
    timestamp_field="event_timestamp",
)

fv = FeatureView(
    name="customer_agg",
    entities=[customer],
    ttl=timedelta(days=7),
    schema=[
        Field(name="total_amount_30d", dtype=Float32),
        Field(name="orders_30d", dtype=Int64),
    ],
    source=source,
    online=True,
)

store = FeatureStore(repo_path=".")
# 初期化とマテリアライズはCLIでも可能: feast apply / feast materialize-incremental

Feature Storeの導入により、特徴量の定義と提供経路がコード化されます。学習データの作成とオンライン推論で同一の定義を参照できるため、トレーニング・サービングスキュー(学習時と本番提供時の不一致)の根を断てます[7][4]。TTLの設計はレイテンシと鮮度のトレードオフで決めます。

例6:Prefectで生成→選択→学習を信頼可能にオーケストレーション

import logging
from time import perf_counter
import pandas as pd
from prefect import flow, task

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("fe_flow")

@task(retries=2, retry_delay_seconds=30)
def generate_features() -> pd.DataFrame:
    # 実際はFeaturetools/SQLで生成し、メタデータを付与
    df = pd.DataFrame({"f1": [1,2,3], "f2": [0.1, 0.5, 0.0], "label": [0,1,0]})
    return df

@task
def select_features(df: pd.DataFrame) -> pd.DataFrame:
    return df[["f1", "f2", "label"]]

@task
def train_model(df: pd.DataFrame) -> float:
    # ダミー学習。実運用では上記のPipelineを再利用
    import numpy as np
    from sklearn.linear_model import LogisticRegression
    X, y = df[["f1","f2"]], df["label"]
    m = LogisticRegression().fit(X, y)
    return float(m.score(X, y))

@flow(name="feature_engineering_pipeline")
def run():
    t0 = perf_counter()
    df = generate_features()
    df_sel = select_features(df)
    score = train_model(df_sel)
    logger.info("score=%.4f elapsed=%.2fs", score, perf_counter()-t0)

if __name__ == "__main__":
    run()

オーケストレーション層は、再実行、遅延、失敗通知、成果物のメタデータ化を担い、失敗しないのではなく失敗してもすぐ立ち上がる状態をつくります[9]。学習器の精度と同じくらい、再現性と可観測性に投資してください。

評価とベンチマーク:速度と精度を同時に測る

自動化の価値は、性能指標の向上と、サイクル時間の短縮で測れます。AUCやF1といった予測性能だけでなく、特徴量生成時間、学習時間、推論レイテンシ、特徴量点数、オンラインキャッシュヒット率、そして一反復あたりのクラウドコストを並行して追跡すると、真の改善が見えてきます。以下は、ベースラインと自動化パイプラインを同一分割で比較するための測定ハーネスです。

比較用の測定ハーネス(再現可能)

import logging
from time import perf_counter
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("fe_benchmark")

X, y = make_classification(n_samples=8000, n_features=50, n_informative=12, random_state=42)
X = pd.DataFrame(X, columns=[f"f{i}" for i in range(X.shape[1])])

baseline = Pipeline([
    ("scaler", StandardScaler(with_mean=False)),
    ("lr", LogisticRegression(max_iter=1000))
])

def automated_pipeline():
    from sklearn.feature_selection import SelectFromModel
    from lightgbm import LGBMClassifier
    return Pipeline([
        ("scaler", StandardScaler(with_mean=False)),
        ("select", SelectFromModel(LGBMClassifier(n_estimators=200, subsample=0.8, colsample_bytree=0.8, random_state=42), threshold="median")),
        ("lr", LogisticRegression(max_iter=1000))
    ])

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for name, pipe in [("baseline", baseline), ("auto", automated_pipeline())]:
    aucs, t_train = [], []
    for tr, va in cv.split(X, y):
        t0 = perf_counter()
        pipe.fit(X.iloc[tr], y[tr])
        t_train.append(perf_counter() - t0)
        proba = pipe.predict_proba(X.iloc[va])[:, 1]
        aucs.append(roc_auc_score(y[va], proba))
    logger.info("%s mean_auc=%.4f train_time_s=%.2f±%.2f", name, np.mean(aucs), float(np.mean(t_train)), float(np.std(t_train)))

このハーネスは、学習時間と予測性能を同一の分割で比較し、誤差の幅も把握できます。結果の傾向として、派生特徴の自動生成と選択を組み合わせたパイプラインは、ベースラインに比べて特徴量点数を削減しつつ精度を維持または向上させることが多く、学習時間はモデルとデータによって増減します。重要なのは、**「精度を落とさずに反復時間を短縮できる閾値」**をプロダクトごとに見つけることです。

ガバナンス、SLA、そしてROIの考え方

自動化は、技術的資産であると同時に運用の約束事でもあります。定義はリポジトリでバージョン管理し、学習・推論の両パスで同一コミットを参照できるようにします。特徴量に付随するメタデータ、例えばソース、計算式、ウィンドウ、遅延、作成者、最終更新日、期待範囲などはデータカタログに登録し、変更時には下流影響を自動通知します。SLAの設定では、再計算の締切とオンラインのレイテンシ上限、デプロイ時のウォームアップ時間をプロダクト要件と整合させます。ROIは、反復時間短縮と精度向上の両輪で見積もると実態に近づきます。例えば、週あたりの実験回数を2回から6回に増やせば、同一期間に検証できる仮説数は三倍に増えます。さらに、スパイク時のオンコール削減や、人手のレビュー時間の短縮など、運用負荷の軽減は見逃されがちな利益です。コスト側では、派生特徴の爆発がストレージと計算資源を圧迫します。生成と選択を別ジョブに分離し、キャッシュの再利用率を監視しながら閾値を調整すると、TCOの下振れを防げます[4]。

導入順序の目安と現実的な落としどころ

まずは既存の学習コードにパイプライン化と交差検証内部での選択を組み込み、リークの芽を摘むところから始めます。次に、二つめのデータソースが加わった時点でFeature Storeを採用し、定義と提供を一元化します。時系列やセッションデータが主要因であれば、tsfreshや自作の窓関数群で抽出の標準ライブラリを整えます。自動生成は最初から万能にしようとせず、初月は集約とエンコーディングの限定セットで十分です。二ヶ月目に相互作用や時系列の差分統計を追加し、三ヶ月目にオンライン提供の最適化へ進むと、移行のリスクと学習コストをバランスできます。

まとめ:速く試し、同じ定義で届けるチームへ

特徴量エンジニアリングの自動化は、派手な魔法ではありません。仕様を定義し、生成と選択と提供を分離し、失敗してもすぐ再開できる配管を敷くという、地味だが強固な土木工事です。それでも、この工事が終われば、あなたのチームは毎週同じリソースでより多くの仮説を検証し、同じ定義を本番に安全に届けられるようになります。明日から取り組むなら、学習コードのパイプライン化と交差検証内での特徴選択から始め、次にFeature Storeで定義を固定し、三つめとして生成の宣言化に着手してください。プロダクトの速度は、精度だけでなく、反復の速さで決まります。あなたのチームは、次にどの仮説をどれだけ早く検証しますか。

参考文献

  1. Anaconda. The State of Data Science 2022. https://www.anaconda.com/resources/state-of-data-science-2022
  2. 日経xTECH. 「データの前処理とは何か」講座. https://xtech.nikkei.com/atcl/learning/lecture/19/00110/00001/
  3. DataRobot. 自動特徴量エンジニアリング(Automated Feature Engineering). https://www.datarobot.com/jp/blog/automatedfeatureengineering/
  4. Featurespace. Feature stores for real-time machine learning. https://www.featurespace.com/newsroom/feature-stores-for-realtime-machine-learning/
  5. Featuretools Documentation. Automated Feature Engineering (Deep Feature Synthesis) on relational and temporal data. https://docs.featuretools.com/en/latest/getting_started/afe.html
  6. tsfresh Documentation. Feature filtering and statistical tests. https://tsfresh.readthedocs.io/en/stable/text/feature_filtering.html
  7. Towards Data Science. Announcing Feast 0.10 — an open source feature store for machine learning. https://towardsdatascience.com/announcing-feast-0-10-feast-1d635762896b
  8. Ke, G. et al. LightGBM: A Highly Efficient Gradient Boosting Decision Tree. NeurIPS 2017. https://proceedings.neurips.cc/paper_files/paper/2017/file/6449f44a102fde848669bdd9eb6b76fa-Paper.pdf
  9. Prefect Documentation. Orchestration, resiliency, and retries. https://docs.prefect.io/
  10. Lundberg, S.M., Lee, S.-I. A Unified Approach to Interpreting Model Predictions (SHAP). NeurIPS 2017. https://arxiv.org/abs/1705.07874
  11. scikit-learn. Permutation feature importance. https://scikit-learn.org/stable/modules/permutation_importance.html
  12. statsmodels. Variance Inflation Factor (VIF) documentation. https://www.statsmodels.org/stable/generated/statsmodels.stats.outliers_influence.variance_inflation_factor.html