目的変数と利用時点|機械学習の問題設定と評価分割
このページはプロモーション(広告)を含みます。
機械学習の問題設定と評価分割(初級・読む30分+演習35分)
この授業のゴール
- 予測の目的を「誰について・いつの時点で・どの期間に起きる・何を」予測し、結果をどう使うかの形で書ける
- 予測時点・特徴量の観測窓・ラベル窓を時間軸上に区別し、境界(含む/含まない)を明示できる
- イベントの記録から、予測時点ごとの学習用の表(スナップショット)をpandasで作れる
- 予測時点にまだ記録されていない・届いていない情報を特徴量から除外できる
仕事でどう使うか
「解約を予測してほしい」という依頼だけでは、モデルは作れません。誰を対象に、いつの時点で、どの期間の解約を予測し、その結果で誰が何をするのかが決まっていないからです。ここを曖昧にしたまま作ると、検証では高い精度が出たのに本番では役に立たない、という典型的な失敗になります。
| 路線 | この授業の内容が使われる成果物・意思決定 |
|---|---|
| DS | 予測問題の定義書(対象・予測時点・予測期間・目的変数・使う施策)。依頼者と合意する最初の文書 |
| ML | 学習データ生成のSQL・pandasコード。予測時点ごとにスナップショットを作り、本番の推論と同じ条件で特徴量を計算する |
| DX | 業務フローとの整合確認。「毎月1日に一覧が届き、15日までに電話する」という運用と、予測期間が合っているか |
| RS | 研究デザインの「アウトカムの定義」「追跡期間」「ベースライン時点」。医学研究のコホート設計と同じ考え方 |
この授業では、前の授業 教師あり・なし・強化学習 の依頼①(毎月1日に来月解約しそうな会員の一覧を作る)を、そのまま作れる形まで定義します。
前提
次の言葉と操作を使います。
- 教師あり学習・目的変数・特徴量 → 教師あり・なし・強化学習
- fit・predict(学習と予測)→ 「fit・transform・predict」(有料版)
- pandasの 比較による行の抽出(
df[df['col'] >= x])、groupby と size / sum、map と fillna → 「列選択と条件抽出」(有料版)(条件抽出)、「groupbyと集計」(有料版)(groupby)、「型と欠損」(有料版)(欠損と fillna) - 日付型:
pd.to_datetimeで文字列を日付に変換し、pd.Timedelta(days=30)で30日の長さを表す。日付どうしは<で前後を比べられる - NaT(Not a Time):日付の欠損。「まだ解約していない」を表すのに使う。0日とは違う
区間の書き方として、$[a, b)$ は「$a$ 以上 $b$ 未満」($a$ を含み、$b$ を含まない)を表します。
身近な状況:「4月1日の一覧」に何を載せるか
会員サポートチームの運用は次のとおりです。
- 毎月1日の朝、「今月中に解約しそうな会員」の一覧を受け取る。
- 1日〜15日に、上位の会員へ電話で引き留めの案内をする。
データには、会員ごとの契約開始日・解約手続きの完了日(まだなら空欄)と、問い合わせの履歴があります。
| 会員 | 契約開始 | 解約完了 | 問い合わせ日 |
|---|---|---|---|
| a1 | 2024-06-01 | 2025-04-12 | 3/20、3/29、4/08 |
| a2 | 2024-09-15 | (なし) | 1/10 |
| a3 | 2025-02-01 | 2025-03-28 | 3/25 |
| a4 | 2025-04-05 | (なし) | なし |
2025年4月1日の一覧を作るとき、次の疑問に答える必要があります。
- a3 は3/28に解約済みです。一覧に載せますか? → 電話する相手ではないので載せません。学習データでも対象外です。
- a4 は4/5に契約しました。4/1の時点ではまだ会員ではないので対象外です。
- a1 の4/08の問い合わせは、4/1の朝にはまだ起きていません。予測に使えるのは3/20と3/29の2回だけです。
- 「解約しそう」とは、いつまでの解約を指しますか? → 4月中(4/1〜4/30)の解約手続き完了、と決めます。a1 は4/12なので「解約した(1)」、a2 は「継続(0)」です。
この4つの決定が、この授業で学ぶ対象母集団・予測時点・観測窓・ラベル窓です。
直感的な説明:天気予報と同じ「発表時刻」がある
天気予報は「今朝6時の発表で、明日の降水確率は70%」と言います。ここには2つの時刻があります。
- 発表時刻(今朝6時):この時刻までに観測した雲や気圧だけを使える。
- 予報の対象期間(明日):当たったかどうかは、明日が終わってから決まる。
明日の実際の雨量を使って予報すれば100%当たりますが、それは予報ではありません。機械学習の予測も同じで、予測時点より後の情報は使えないという当たり前の制約を、データを作るときに機械的に守る必要があります。
時間軸で描くと次のようになります(4/1の一覧の場合)。
| 区間 | 期間 | 何に使うか |
|---|---|---|
| 観測窓 | 3/2 〜 3/31($[t_0-30\text{日}, t_0)$) | 特徴量を計算する(問い合わせ回数など) |
| 予測時点 $t_0$ | 4/1 0:00 | 一覧を作る瞬間。ここで線を引く |
| ラベル窓 | 4/1 〜 4/30($[t_0, t_0+30\text{日})$) | 目的変数を決める(解約したか) |
線の左側は入力、右側は答えです。学習データを作るときは過去の時点について答えがわかっていますが、その答えを入力側に混ぜないように線を引きます。
正確な定義:予測問題を5つの要素で書く
予測問題は、次の5つを決めると一意に定まります。
| 要素 | 意味 | 今回の決定 |
|---|---|---|
| 対象母集団 | 予測する相手の条件 | $t_0$ の時点で契約中の会員(契約開始日 $< t_0$ かつ、未解約または解約完了日 $\ge t_0$) |
| 予測時点 $t_0$ | 特徴量を締め切り、予測を出す時刻 | 毎月1日 0:00 |
| 観測窓(特徴量の期間) | 特徴量の計算に使う過去の期間 | $[t_0-30\text{日}, t_0)$ |
| ラベル窓(予測期間)$h$ | 目的変数を判定する未来の期間 | $[t_0, t_0+30\text{日})$ |
| 目的変数 $y$ と使い道 | 何が起きたら1か、結果を誰がどう使うか | ラベル窓の中で解約手続きが完了したら1。サポートが15日までに電話する |
目的変数を式で書きます。会員 $i$ の解約完了日を $T_i$(未解約なら値なし)、予測時点を $t_0$、予測期間の長さを $h$(日)とすると、
$$y_i = \begin{cases} 1 & (t_0 \le T_i < t_0 + h) \\ 0 & (\text{それ以外。未解約を含む}) \end{cases}$$
数値を代入します。$t_0=$ 4/1、$h=30$ 日のとき、$t_0+h=$ 5/1 です。
- $T_i=$ 4/18 → 4/1 ≤ 4/18 < 5/1 なので $y_i=1$
- $T_i=$ 5/3 → 5/1 以上なので $y_i=0$(5月の一覧では対象になる)
- $T_i=$ 4/1(当日)→ 4/1 ≤ 4/1 が成り立つので $y_i=1$。0時の時点では契約中なので対象に含める
- $T_i=$ 3/20 → $t_0$ より前に解約済みなので、$y$ を決める前に対象母集団から外す
利用時点で使える特徴量とは、「$t_0$ より前に起きた」だけでなく「$t_0$ の時点でデータとして手元に届いている」ものです。たとえば月額請求の確定データが毎月5日に届くなら、4/1の予測に3月分の請求は使えません。データ項目ごとに「いつ発生し、いつ記録・到着するか」を一覧(データ辞書)に書いておきます。
学習データは、過去の複数の予測時点(2024年5/1、6/1、…)について同じ規則で作った表を縦に積みます。1つの $t_0$ について作った表をスナップショットと呼びます。同じ会員が複数のスナップショットに出てくるので、評価の分け方には注意が必要です(時点・個体・群の分離)。
段階付き例題:4/1のスナップショットを作る
身近な状況の4人の記録から、4/1のスナップショットを作ります。手順は「(1) 対象を決める → (2) 観測窓で特徴量を作る → (3) ラベル窓で目的変数を作る」の順です。
使う道具:customers(1行=会員1人の DataFrame)、calls(1行=問い合わせ1件の DataFrame)。groupby('customer_id').size() は会員ごとの行数(問い合わせ回数)を Series で返し、map でスナップショットの各会員に対応づけます。問い合わせが1件もない会員は map の結果が NaN になるので、fillna(0) で0回にします(この特徴量では「記録なし=0回」と定義できるため)。
import pandas as pd
customers = pd.DataFrame({
'customer_id': ['a1', 'a2', 'a3', 'a4'],
'signup_date': pd.to_datetime(['2024-06-01', '2024-09-15', '2025-02-01', '2025-04-05']),
'cancel_date': pd.to_datetime(['2025-04-12', None, '2025-03-28', None]), # NaT=まだ解約していない
})
calls = pd.DataFrame({
'customer_id': ['a1', 'a1', 'a1', 'a2', 'a3'],
'call_date': pd.to_datetime(['2025-03-20', '2025-03-29', '2025-04-08', '2025-01-10', '2025-03-25']),
})
t0 = pd.Timestamp('2025-04-01') # 予測時点
h = pd.Timedelta(days=30) # 予測期間(ラベル窓)の長さ
w = pd.Timedelta(days=30) # 特徴量の観測窓の長さ
# (1) 対象母集団:t0 の時点で契約中の人
active = (customers['signup_date'] < t0) & (
customers['cancel_date'].isna() | (customers['cancel_date'] >= t0))
snap = customers.loc[active, ['customer_id']].copy()
print('(1) 対象:', snap['customer_id'].tolist())
# (2) 特徴量:t0 より前の30日間 [t0-w, t0) の問い合わせ回数
past = calls[(calls['call_date'] >= t0 - w) & (calls['call_date'] < t0)]
cnt = past.groupby('customer_id').size()
snap['calls_30d'] = snap['customer_id'].map(cnt).fillna(0).astype(int) # 問い合わせなし=0回
# (3) 目的変数:[t0, t0+h) に解約手続きが完了したら 1
cd = customers.set_index('customer_id')['cancel_date']
c = snap['customer_id'].map(cd)
snap['y'] = ((c >= t0) & (c < t0 + h)).astype(int)
print(snap.to_string(index=False))
出力:
(1) 対象: ['a1', 'a2']
customer_id calls_30d y
a1 2 1
a2 0 0
結果の意味:
| 会員 | 判定 | 理由 |
|---|---|---|
| a1 | 対象、calls_30d=2、y=1 | 3/20と3/29が観測窓内。4/08は $t_0$ より後なので数えない。解約4/12はラベル窓内 |
| a2 | 対象、calls_30d=0、y=0 | 1/10は観測窓(3/2〜)より前。未解約なので0 |
| a3 | 対象外 | 3/28に解約済み |
| a4 | 対象外 | 4/5契約で、4/1にはまだ会員でない |
本番では、毎月1日に同じコードを t0 だけ変えて実行し、y 以外の列でモデルが予測します。学習用と本番用で同じ関数を使うことが、利用時点のずれを防ぐ一番の方法です。
操作して理解する:予測時点と窓を動かす
例題のコードで、次の値を書き換えて実行し、結果を予想と比べてください。
t0 = pd.Timestamp('2025-04-01')を'2025-03-01'にすると? → a3 が対象に入り(3/28解約なので y=1)、a1 は y=0 になります(4/12はラベル窓 [3/1, 3/31) の外)。a4 はまだ対象外です。t0を'2025-05-01'にすると? → a1・a3 は解約済みで外れ、a2 と a4 だけが対象になります。h = pd.Timedelta(days=30)をdays=7にすると? → a1 の解約4/12はラベル窓 [4/1, 4/8) の外になり、y=0 になります。
3は業務上の重要な問いです。予測期間を短くすると「当てやすさ」と「間に合うか」が変わります。7日以内の解約を予測しても、電話が15日までの運用では間に合いません。逆に90日にすると、施策の前に気持ちが変わる人も多く含まれます。予測期間は、施策を実行するのに必要な時間(リードタイム)と、施策が効く期間から決めます。
次の表で、各情報が4/1の予測に使えるかを判断してみましょう。
| 情報 | 発生 | 手元に届く | 4/1に使えるか |
|---|---|---|---|
| 3月の視聴時間 | 3月中 | 毎日翌朝に集計 | 使える |
| 3月分の請求確定額 | 3月末締め | 4/5に確定 | 使えない(4/1にはまだない) |
| 解約手続きの理由 | 解約時 | 解約時 | 使えない(解約した人にしか値がない未来情報) |
| 契約プラン | 契約時 | 契約時(変更は即時) | 使える。ただし4/1時点のプランを使う |
発展:打ち切り・複数の時点・目的変数の選び方
- 打ち切り:最新の予測時点(たとえば先週)では、ラベル窓がまだ終わっていないので $y$ が確定していません。未確定の行を $y=0$ として学習に入れると、解約が少なく見えます。ラベル窓が終わったスナップショットだけを学習に使います。「いつ解約するか」まで扱うなら、打ち切りを正しく扱う生存時間分析を使います。
- スナップショットの間隔:毎月1日に予測するなら、学習用も毎月1日で作るのが基本です。毎日作ると行数は30倍になりますが、ほぼ同じ行が増えるだけで情報はあまり増えず、同じ会員の重複が強くなります。
- 目的変数は代理指標になりやすい:本当に知りたいのは「電話すれば残ってくれる人」(施策で結果が変わる人)ですが、解約予測が当てるのは「放っておくと解約する人」です。必ず解約する人に電話しても無駄になる可能性があります。施策の効果まで予測したい場合は、A/Bテストのデータを使うアップリフトモデルなどを検討します(上級)。
- 定義の変更の影響:「解約」を「解約申請」とするか「解約手続き完了」とするかで、ラベル窓の境界にいる人の $y$ が変わります。定義を変えたら、過去の評価結果とは比べられないことを報告に書きます。
よくある失敗
失敗1:観測窓に上限を付けず、予測時点より後の問い合わせを数える
「直近30日の問い合わせ回数」を、下限だけで抽出してしまう誤りです。
import pandas as pd
calls = pd.DataFrame({
'customer_id': ['a1', 'a1', 'a1', 'a2', 'a3'],
'call_date': pd.to_datetime(['2025-03-20', '2025-03-29', '2025-04-08', '2025-01-10', '2025-03-25']),
})
t0 = pd.Timestamp('2025-04-01')
wrong = calls[calls['call_date'] >= t0 - pd.Timedelta(days=30)].groupby('customer_id').size()
right = calls[(calls['call_date'] >= t0 - pd.Timedelta(days=30)) & (calls['call_date'] < t0)].groupby('customer_id').size()
print('誤り(上限なし):', wrong.to_dict())
print('正しい(t0未満):', right.to_dict())
出力:
誤り(上限なし): {'a1': 3, 'a3': 1}
正しい(t0未満): {'a1': 2, 'a3': 1}
a1 の3回目(4/08)は予測時点より後で、解約の相談だった可能性が高い問い合わせです。こうした「解約直前の行動」を特徴量に混ぜると、検証での精度は上がりますが、本番の4/1にはその問い合わせはまだ存在しないので、精度が大きく落ちます。
直し方:観測窓は必ず $[t_0-w, t_0)$ の両端を書く(>= t0 - w と < t0)。過去のデータで学習データを作るときほど、未来の行がテーブルに入っているので注意します。
失敗2:予測時点に解約済み・未契約の人を対象に含める
例題の4人全員を対象にして $y$ を計算すると、次のようになります。
| 会員 | 誤った扱い | 問題 |
|---|---|---|
| a3(3/28解約済み) | y=0(4月中に解約していない) | 「解約しない人」として学習される。実際はもう会員でない |
| a4(4/5契約) | y=0、特徴量はすべて0 | 4/1には存在しない人の行が混ざる |
対象4人のうち y=1 は a1 の1人で解約率25%、正しい対象2人なら50%です。分母が変わるので解約率もモデルの評価も変わります。特に、解約済みの人は利用時間0・問い合わせ0のような極端な値を持つことが多く、モデルが「利用0なら解約しない」という奇妙な規則を覚えることがあります。
直し方:対象母集団の条件(契約開始日 $< t_0$、かつ未解約または解約完了日 $\ge t_0$)を最初に適用し、対象人数を毎回記録する。
失敗3:予測時点にはまだ届いていないデータを使う
学習データを作るとき、データベースには3月分の請求確定額がすでに入っています。そこで4/1のスナップショットに「3月の請求額」を特徴量として入れたとします。
| 時点 | 学習時(過去のデータで作成) | 本番の4/1 |
|---|---|---|
| 3月の請求確定額 | 値がある(後から入った) | まだない(4/5確定) |
学習では役立つ特徴量でも、本番では毎回欠損になります。欠損を0で埋めると「請求0円の人」として予測され、結果がでたらめになります。発生は $t_0$ より前でも、記録・到着が $t_0$ より後なら使えません。
直し方:データ辞書に各列の「発生時点」「記録・到着時点」を書き、特徴量は到着時点 $< t_0$ のものに限る。どうしても使いたいなら「2月分の請求確定額」(4/1には届いている)に置き換える。データベースに更新日時の列があれば、それで $t_0$ 時点の状態を復元する。
まとめ
- 予測問題は「対象母集団・予測時点・観測窓・ラベル窓・目的変数と使い道」の5つで定義する。
- 予測時点より前に起き、かつ予測時点に手元に届いている情報だけが特徴量に使える。
- 観測窓は $[t_0-w, t_0)$、ラベル窓は $[t_0, t_0+h)$ のように両端と含む・含まないを明示する。
- 予測時点に契約中でない人は、目的変数を決める前に対象から外す。
- 予測期間は施策のリードタイムに合わせる。学習用と本番用で同じスナップショット関数を使う。
用語
- 予測時点(prediction time / cutoff)
特徴量を締め切り、予測を出す時刻。これより後の情報は入力に使えない。
- 観測窓(observation window)
特徴量の計算に使う、予測時点より前の期間。
- ラベル窓(予測期間)(label window / prediction horizon)
目的変数を判定する、予測時点以降の期間。
- 対象母集団(target population)
予測時点で予測の対象となる個体の集合。予測して施策を打つ相手。
- スナップショット(snapshot)
1つの予測時点について、特徴量と目的変数を計算した表。複数時点を積み上げて学習データにする。
- 打ち切り(censoring)
観測期間が終わっていないなどの理由で、結果がまだ確定していない状態。
よくある質問
なぜここまで細かく定義する必要があるのですか?
定義が1日ずれるだけで、境界にいる会員の y や特徴量が変わり、モデルの評価も変わるからです。また、学習データを作る人と本番の推論を作る人が別だと、定義書がない限り同じ計算になりません。5つの要素(対象・予測時点・観測窓・ラベル窓・目的変数と使い道)を書いた1枚の定義書が、チームの共通言語になります。
目的変数と特徴量の違いは、時間で決まるのですか?
予測問題では、予測時点より後に決まるものが目的変数、前に手元にあるものが特徴量です。同じ「問い合わせ回数」でも、3月分は特徴量、4月分は(使うなら)目的変数側の情報です。列の名前ではなく、予測時点との前後関係で役割が決まります。
観測窓は30日でなければいけませんか?
決まりはありません。直近30日・90日・全期間など、複数の長さで特徴量を作って比べるのが一般的です。ただし、新しく契約した会員は長い窓の期間に存在しないので、「契約前の期間を0と数えるのか、欠損とするのか」を決める必要があります。
予測時点の時刻(0時か朝9時か)まで決める必要がありますか?
日次でデータが届くなら日付単位で十分なことが多いですが、当日の行動を特徴量に入れるなら時刻が必要です。たとえば朝9時に予測するのに当日23時までの行動を使うと未来情報になります。運用で予測を実行する時刻と、データが確定する時刻を両方確認してください。
実務で、どの列が「利用時点で使える」かをどう確認しますか?
データ基盤の担当者に、各テーブルの更新タイミング(毎日何時、月次の確定日など)と、過去の値を上書きするかを確認します。上書きされる列(例:現在の住所)は、過去の時点の値ではなく今の値が入っているので、過去のスナップショットに使うと未来情報になります。変更履歴のテーブルがあればそちらを使います。
条件が変わって、週1回の予測にしたい場合は何を変えますか?
予測時点の間隔、ラベル窓の長さ(7日など)、施策のリードタイムとの整合を見直します。学習データも週ごとのスナップショットで作り直します。月次の定義で作ったモデルの評価値は、週次の問題の評価値とは比べられません。
確認問題
問1 毎月1日に「今月中に解約する会員」を予測する。予測時点を4/1としたとき、4/1より前に解約が完了している会員の扱いとして正しいものはどれか。
- ① y=0として学習データに含める
- ② y=1として学習データに含める
- ③ 対象母集団から外す
- ④ 特徴量を0にして含める
解答と解説
正解:③
予測時点で契約中でない人は、予測して施策を打つ相手ではないので、目的変数を決める前に対象から外します。
問2 予測時点 $t_0$=4/1、ラベル窓 $[t_0, t_0+30\text{日})$ のとき、解約完了日が5/1の会員の y はどれか。
- ① 1(4月の予測で解約と数える)
- ② 0(ラベル窓の右端は含まない)
- ③ 対象外
- ④ 欠損
解答と解説
正解:②
4/1+30日=5/1 で、右端は含まないので5/1の解約は y=0 です。5/1の予測では、当日の解約として y=1 になります。
問3 学習データを作ったら「4/1〜4/30の問い合わせ回数」が最も効く特徴量になった。予測時点は4/1である。正しい判断はどれか。
- ① 予測に役立つので採用する
- ② 回数を対数変換すれば使ってよい
- ③ 予測時点より後の情報なので除外し、[3/2, 4/1) の回数に作り直す
- ④ 検証データでも効いていれば問題ない
解答と解説
正解:③
4/1の朝には、その後の問い合わせはまだ起きていません。本番では使えない未来情報です。
問4 3月分の請求確定額は4/5にデータベースへ届く。4/1の予測にこの列を使う問題点として最も適切なものはどれか。
- ① 発生は3月なので問題はない
- ② 学習時には値があるが、本番の4/1には毎回欠損になり、学習時と予測時の条件がずれる
- ③ 金額の単位が円なので標準化が必要になる
- ④ 請求額は目的変数なので特徴量にできない
解答と解説
正解:②
「発生」が $t_0$ より前でも、「記録・到着」が後なら予測時点では使えません。
問5 サポートは予測一覧を受け取ってから電話まで最大2週間かかる。予測期間(ラベル窓の長さ)を「翌日まで」にすると何が問題か。
- ① 解約が多すぎて予測できなくなる
- ② 電話が間に合う前に解約が起きるので、当たっても施策に使えない
- ③ 特徴量が増えすぎる
- ④ 対象母集団が空になる
解答と解説
正解:②
予測期間は施策のリードタイムと効く期間に合わせて決めます。当たっても行動が間に合わなければ価値がありません。
問6 毎月1日のスナップショットを1年分積み上げた学習データの説明として正しいものはどれか。
- ① 同じ会員が複数の行に現れることがあり、評価データの分け方に注意が必要
- ② 1人1行になるので、行を無作為に分ければよい
- ③ スナップショットごとに目的変数の定義を変える必要がある
- ④ 最新月のラベル窓が終わっていなくても、y=0として使ってよい
解答と解説
正解:①
継続している会員は毎月の表に現れます。行単位で無作為に分けると、同じ人が訓練と評価の両方に入ります。
次に学ぶこと
次は ベースライン で、定義した問題に対して「何もしない予測」「単純な規則」などのベースラインを作り、モデルの価値を測る基準にします。
データの抽出方法に不安があれば PY モジュールの pandas 授業へ、fit / predict の流れは 「fit・transform・predict」(有料版) へ戻ってください。同じ会員が複数のスナップショットに出るときの分割は 時点・個体・群の分離、未来情報の見つけ方は 漏洩の検出 で扱います。
キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。
アプリを無料で開く有料版と無料版の違い