はじめに
こんにちは、事業会社で働いているデータサイエンティストです。
営業活動は、多くの会社にとって売上を生み出す最後の砦です。開発側がどれほど優れたプロダクトを作っても、それを実際に顧客へ届け、契約を獲得しなければ売上にはなりません。もちろん、プロダクトの魅力だけで自然と顧客が集まる会社もありますが、そのような会社は決して多数派ではありません。世の中の多くの会社では、営業チームがお客様と向き合い、商談を重ね、最終的に売上を持ち帰っています。そのため、営業とデータサイエンティストが協業する場面も少なくありません。ミッションのスケールは当時とは大きく変わりましたが、私たちのチームも、もともとは営業データ分析を目的として立ち上がった組織でした。
営業データ分析をしていると、「どの顧客に商談すれば最も売上につながりそうか」というテーマは、ほぼ必ずと言っていいほど登場します。そして、このテーマは営業との協業に限った話ではありませんが、分析を進めていると、高い確率で次のような提案を受けます。
受注企業だけで学習すれば精度が上がるじゃん!
一見すると、とても合理的なアイデアに聞こえます。
確かに、日本中の法人をすべて学習データに含めると、水道やガス、インターネット回線のようなインフラサービスでもない限り、大半の法人には売上ゼロが紐づいているはずです。ドメイン知識のない業界を例に挙げるのは恐縮ですが、例えば業務用ベッドを販売する会社であれば、主な顧客はホテルや社員寮、宿泊施設などになるでしょう。飲食店やソフトウェア開発会社が大量にベッドを購入することは、あまり考えられません。
一方で、ホテルや宿泊施設は日本中の法人の中ではごく一部です。そのため、一法人一レコードとして全国の法人を学習データにすると、ほとんどの企業は売上ゼロというデータになります。
だからこそ、「ゼロ売上の企業は最初から削除して、実際に受注した企業だけで学習したほうが精度が上がるのでは?」という発想は、ごく自然に生まれます。
実は、この考え方には、データサイエンスだけでなく、ビジネスそのものを誤った方向へ導きかねない、大きな落とし穴があります。これは計量経済学では、選択バイアス(selection bias)の一種であり、特に「従属変数に基づくサンプル選択(selection on the dependent variable)」と呼ばれる問題です。ただ、教科書で紹介される例は政治学や経済学を題材にしたものが多く、ビジネスの現場でこの問題がどのように現れるのかは、少しイメージしづらいかもしれません。
そこで本記事では、できるだけ営業やB2Bビジネスで実際に起こり得る例を使いながら、「受注企業だけで学習する」と何が起こるのかを直感的に説明します。最後には簡単なシミュレーションも交えながら、従属変数に基づくサンプル選択が、なぜモデルの性能評価や意思決定を誤らせるのかを可視化していきます。
シミュレーション実験
ここでは、受注数を予測するモデルを考えましょう。受注数は、営業行動に依存し、ポワソン分布に従うという前提でシミュレーションを実施します:
$$
\lambda_{i} = -8 + 2 \cdot 営業行動_{i}
$$
$$
受注数_{i} \sim Poisson(exp(\lambda_{i}))
$$
まず、R でデータを生成しましょう。x と y はそれぞれ営業行動と受注数を指しています。営業行動は何かしらの標準化を取ったので負の値にもなりうるとします:
set.seed(1)
train_df <- 50000 |>
rnorm() |>
tibble::tibble(
x = _
) |>
dplyr::mutate(
lambda = -8 + 2 * x,
y = lambda |>
exp() |>
purrr::map_int(
\(x){
rpois(1, x)
}
)
)
受注数の分布を見てみましょう:
> train_df |> dplyr::count(y) |> dplyr::mutate(ratio = n/sum(n))
# A tibble: 3 × 3
y n ratio
<int> <int> <dbl>
1 0 49873 0.997
2 1 125 0.0025
3 2 2 0.00004
いかがでしょうか。この受注数データは、99.7% がゼロという、ほとんどゼロだけで構成されたデータになっています。皆さんの会社の売上データや受注データも、これとよく似た分布になっているのではないでしょうか。
そうすると、「99.7% のゼロ受注データはノイズなのだから分析対象から除外し、本当に受注できた 127 社だけで学習したほうが、営業にとって役立つモデルが作れるのではないか」と考える人が出てきても不思議ではありません。実際、営業担当者や営業企画、あるいはデータサイエンスプロジェクトのPMから、このような提案を受けた経験のある方も少なくないでしょう。
ここまで読んで、「あの案件でまさに同じことを言われた」と思い当たる方も、多いのではないでしょうか。
なのでここでは、ゼロを含むモデルと、あえてゼロを除外するモデルを両方作成して、まずは係数を確認します。m_full と m_subset はそれぞれ、ゼロを含むモデルとゼロを除外するモデルを意味します:
m_full <- train_df |>
glm(y ~ x, family = poisson("log"), data = _)
m_subset <- train_df |>
dplyr::filter(y > 0) |>
glm(y ~ x, family = poisson("log"), data = _)
係数表はこちらです:
> stargazer::stargazer(m_full, m_subset, type = "text")
==============================================
Dependent variable:
----------------------------
y
(1) (2)
----------------------------------------------
x 2.013*** 0.018
(0.095) (0.090)
Constant -7.975*** -0.020
(0.206) (0.196)
----------------------------------------------
Observations 50,000 127
Log Likelihood -651.183 -128.350
Akaike Inf. Crit. 1,306.365 260.699
==============================================
Note: *p<0.1; **p<0.05; ***p<0.01
まず、このシミュレーションでは、説明変数 x の真の係数を 2 と設定していたことを思い出してください。その前提で結果を見ると、ゼロ受注を含めて学習したモデルは係数を 2.013 とほぼ正しく推定できている一方で、ゼロ受注を除外したモデルの係数は 0.018 と真の値から大きく乖離し、統計的にも有意ではなくなっています。
ただし、これはあくまで統計学的な観点からの評価です。ビジネスの現場では、係数が真の値に近いかよりも、実際によく当たるモデルかのほうが重要です。いくら美しい推定結果が得られたとしても、予測性能が低ければ実務では役に立ちません。そこで次は、両者の予測性能を比較してみましょうと思いますが、その前に、線形モデルが悪いという考えに答えるため、lightGBM でもゼロを含めたモデルとゼロを外したモデルを学習します:
> m_full_lightgbm <- lightgbm::lightgbm(
+ data = train_df |>
+ dplyr::select(x) |>
+ as.matrix(),
+ label = train_df$y,
+ objective = "poisson"
+ )
[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.000439 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 255
[LightGBM] [Info] Number of data points in the train set: 50000, number of used features: 1
[LightGBM] [Info] Start training from score -5.959966
>
> m_subset_lightgbm <- lightgbm::lightgbm(
+ data = train_df |>
+ dplyr::filter(y > 0) |>
+ dplyr::select(x) |>
+ as.matrix(),
+ label = train_df$y[which(train_df$y > 0)],
+ objective = "poisson"
+ )
[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.000006 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 44
[LightGBM] [Info] Number of data points in the train set: 127, number of used features: 1
[LightGBM] [Info] Start training from score 0.015625
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
# 以下省略
では、検証データを作成し:
set.seed(1)
test_df <- 10000 |>
rnorm() |>
tibble::tibble(
x = _
) |>
dplyr::mutate(
lambda = -8 + 2 * x,
y = lambda |>
exp() |>
purrr::map_int(
\(x){
rpois(1, x)
}
)
)
精度確認用のデータをデータフレイムに格納します:
| 変数名 | 内容 |
|---|---|
predict_full |
ゼロ受注を含むポワソン回帰モデル (m_full) の予測値 |
predict_subset |
ゼロ受注を除外して学習したポワソン回帰モデル (m_subset) の予測値 |
predict_full_lightgbm |
ゼロ受注を含む LightGBM モデル (m_full_lightgbm) の予測値 |
predict_subset_lightgbm |
ゼロ受注を除外して学習した LightGBM モデル (m_subset_lightgbm) の予測値 |
test_with_pred_df <- test_df |>
dplyr::bind_cols(
predict_full = predict(m_full, test_df, type = "response"),
predict_subset = predict(m_subset, test_df, type = "response"),
predict_full_lightgbm = predict(m_full_lightgbm, test_df |> dplyr::select(x) |> as.matrix(), type = "response"),
predict_subset_lightgbm = predict(m_subset_lightgbm, test_df |> dplyr::select(x) |> as.matrix(), type = "response")
)
全体精度の指標はこちらです:
> test_with_pred_df |>
+ dplyr::mutate(
+ ape_full = abs((y - predict_full)/(y + 1)),
+ ape_subset = abs((y - predict_subset)/(y + 1)),
+ ape_full_lightgbm = abs((y - predict_full_lightgbm)/(y + 1)),
+ ape_subset_lightgbm = abs((y - predict_subset_lightgbm)/(y + 1)),
+ se_full = (y - predict_full)^2,
+ se_subset = (y - predict_subset)^2,
+ se_full_lightgbm = (y - predict_full_lightgbm)^2,
+ se_subset_lightgbm = (y - predict_subset_lightgbm)^2
+ ) |>
+ dplyr::summarise(
+ mape_full = mean(ape_full),
+ mape_subset = mean(ape_subset),
+ mape_full_lightgbm = mean(ape_full_lightgbm),
+ mape_subset_lightgbm = mean(ape_subset_lightgbm),
+ rmse_full = sqrt(mean(se_full)),
+ rmse_subset = sqrt(mean(se_subset)),
+ rmse_full_lightgbm = sqrt(mean(se_full_lightgbm)),
+ rmse_subset_lightgbm = sqrt(mean(se_subset_lightgbm))
+ ) |>
+ dplyr::glimpse()
Rows: 1
Columns: 8
$ mape_full <dbl> 0.003304389
$ mape_subset <dbl> 0.9782482
$ mape_full_lightgbm <dbl> 0.003199498
$ mape_subset_lightgbm <dbl> 0.9985675
$ rmse_full <dbl> 0.04331894
$ rmse_subset <dbl> 0.9793288
$ rmse_full_lightgbm <dbl> 0.04252596
$ rmse_subset_lightgbm <dbl> 0.9995106
>
> test_with_pred_df |>
+ dplyr::filter(y > 0) |>
+ dplyr::mutate(
+ ape_full = abs((y - predict_full)/(y + 1)),
+ ape_subset = abs((y - predict_subset)/(y + 1)),
+ ape_full_lightgbm = abs((y - predict_full_lightgbm)/(y + 1)),
+ ape_subset_lightgbm = abs((y - predict_subset_lightgbm)/(y + 1)),
+ se_full = (y - predict_full)^2,
+ se_subset = (y - predict_subset)^2,
+ se_full_lightgbm = (y - predict_full_lightgbm)^2,
+ se_subset_lightgbm = (y - predict_subset_lightgbm)^2
+ ) |>
+ dplyr::summarise(
+ mape_full = mean(ape_full),
+ mape_subset = mean(ape_subset),
+ mape_full_lightgbm = mean(ape_full_lightgbm),
+ mape_subset_lightgbm = mean(ape_subset_lightgbm),
+ rmse_full = sqrt(mean(se_full)),
+ rmse_subset = sqrt(mean(se_subset)),
+ rmse_full_lightgbm = sqrt(mean(se_full_lightgbm)),
+ rmse_subset_lightgbm = sqrt(mean(se_subset_lightgbm))
+ ) |>
+ dplyr::glimpse()
Rows: 1
Columns: 8
$ mape_full <dbl> 0.4717241
$ mape_subset <dbl> 0.009590529
$ mape_full_lightgbm <dbl> 0.4726553
$ mape_subset_lightgbm <dbl> 0.006453236
$ rmse_full <dbl> 0.9469512
$ rmse_subset <dbl> 0.02258345
$ rmse_full_lightgbm <dbl> 0.9477485
$ rmse_subset_lightgbm <dbl> 0.02079734
まずは予測性能を比較してみましょう。
今回使用する評価指標は、MAPE(Mean Absolute Percentage Error)と RMSE(Root Mean Squared Error)の 2 つです。
| 指標 | 内容 | 小さいほど良い |
|---|---|---|
| MAPE | 予測値が実測値から何 % ずれているかを表す指標です。今回は受注数がゼロの場合にゼロ除算を避けるため、(y + 1) で割っています。 |
○ |
| RMSE | 予測誤差の二乗平均平方根です。大きな予測ミスほど強くペナルティを与えるため、極端な外れ値にも敏感な指標です。 | ○ |
まずは、実際の運用に最も近い状況、つまり受注企業と非受注企業が混在したテストデータ全体で予測性能を比較します。
| モデル | MAPE | RMSE |
|---|---|---|
| ポワソン(全データ学習) | 0.0033 | 0.0433 |
| ポワソン(受注企業のみ学習) | 0.9782 | 0.9793 |
| LightGBM(全データ学習) | 0.0032 | 0.0425 |
| LightGBM(受注企業のみ学習) | 0.9986 | 0.9995 |
結果は非常に極端になりました。
ゼロ受注を含めて学習したモデルは、ポワソン回帰・LightGBM のどちらもほぼ完全に真のデータ生成過程を再現できています。一方で、受注企業だけで学習したモデルは、MAPE・RMSE の両方で大幅に性能が悪化しました。
これは決してアルゴリズムの性能差ではありません。学習した母集団そのものが違うためです。また、R は本番環境に向いていない、Python に切り替えればすべて解決する、といった種類の問題でもありません。この問題は、Python を使おうが、C++ を使おうが、アセンブリ言語で実装しようが消えません。極端な話、南北アメリカ大陸ほどの大きさの GPU クラスターを用意し、数兆層の深層学習モデルを学習させたとしても、学習データの作り方そのものが間違っていれば、間違ったパターンをより高い精度で学習するだけです。モデルは実戦で同じ理由で爆死します。
では、「営業は受注企業だけに興味があるのだから、その企業だけで評価すべきでは?」という反論はどうでしょうか。
そこで今度は、実際に受注した企業(y > 0)だけを対象に、もう一度同じ評価を行います。
| モデル | MAPE | RMSE |
|---|---|---|
| ポワソン(全データ学習) | 0.4717 | 0.9470 |
| ポワソン(受注企業のみ学習) | 0.0096 | 0.0226 |
| LightGBM(全データ学習) | 0.4727 | 0.9477 |
| LightGBM(受注企業のみ学習) | 0.0065 | 0.0208 |
今度は営業側の主張どおりの結果になりました。
受注企業だけを見るのであれば、受注企業だけで学習したモデルのほうが予測性能は明らかに高くなっています。営業担当者が日々目にするのはまさにこの世界なので、「ほら、やっぱり受注企業だけで学習したほうがいいじゃないか」と言いたくなる気持ちもよくわかります。
しかし、ここで本記事の一番重要なポイントがあります。
営業担当者は、明日商談する会社が「受注企業」なのか「非受注企業」なのかを、商談する前から知っているでしょうか?
もちろん、知りません。
もし事前にそれがわかるのであれば、予測モデルなど最初から必要ありません。「受注する会社だけ営業してください」で終わりです。
つまり、受注企業だけを対象にした評価は、一見するとモデルの性能を測っているようでいて、実際には「未来を知っている」という現実には存在しない条件の上で評価していることになります。
モデルを運用する現場では、営業担当者の前には受注企業と非受注企業が混ざった状態で並んでいます。その状況で意思決定する以上、本当に評価すべきなのは、テストデータ全体に対する性能です。
そして、その条件では、ゼロ受注企業を学習データから削除したモデルは、ポワソン回帰でも LightGBM でも、一貫して性能が悪化していました。
さらに面白いことに、LightGBM を使っても状況はほとんど変わりませんでした。
「線形モデルだから失敗したのでは?」という反論は成り立ちません。より柔軟で複雑な機械学習モデルを使っても、間違った母集団から学習すれば、やはり間違ったモデルになります。
つまり今回の問題は、アルゴリズムの性能ではなく、学習データそのものにあったのです。
では、今回たまたまこんな結果になっただけなのかを検証するため、5000 回のシミュレーションを回します。LightGBM が魔法のように問題を解決しれくれないことがわかったので、ポワソンモデルのみにします。
set.seed(1)
future::plan(future::multisession(workers = 10))
simulation_df <- 1:5000 |>
furrr::future_map(
\(i){
train_df <- 50000 |>
rnorm() |>
tibble::tibble(
x = _
) |>
dplyr::mutate(
lambda = -8 + 2 * x,
y = lambda |>
exp() |>
purrr::map_int(
\(x){
rpois(1, x)
}
)
)
test_df <- 10000 |>
rnorm() |>
tibble::tibble(
x = _
) |>
dplyr::mutate(
lambda = -8 + 2 * x,
y = lambda |>
exp() |>
purrr::map_int(
\(x){
rpois(1, x)
}
)
)
m_full <- train_df |>
glm(y ~ x, family = poisson("log"), data = _)
m_subset <- train_df |>
dplyr::filter(y > 0) |>
glm(y ~ x, family = poisson("log"), data = _)
test_with_pred_df <- test_df |>
dplyr::bind_cols(
predict_full = predict(m_full, test_df, type = "response"),
predict_subset = predict(m_subset, test_df, type = "response")
)
test_with_pred_df |>
dplyr::mutate(
ape_full = abs((y - predict_full)/(y + 1)),
ape_subset = abs((y - predict_subset)/(y + 1)),
se_full = (y - predict_full)^2,
se_subset = (y - predict_subset)^2
) |>
dplyr::summarise(
full_mape_full = mean(ape_full),
full_mape_subset = mean(ape_subset),
full_rmse_full = sqrt(mean(se_full)),
full_rmse_subset = sqrt(mean(se_subset))
) |>
dplyr::bind_cols(
test_with_pred_df |>
dplyr::filter(y > 0) |>
dplyr::mutate(
ape_full = abs((y - predict_full)/(y + 1)),
ape_subset = abs((y - predict_subset)/(y + 1)),
se_full = (y - predict_full)^2,
se_subset = (y - predict_subset)^2
) |>
dplyr::summarise(
subset_mape_full = mean(ape_full),
subset_mape_subset = mean(ape_subset),
subset_rmse_full = sqrt(mean(se_full)),
subset_rmse_subset = sqrt(mean(se_subset))
),
beta_full = coef(m_full)[2],
beta_subset = coef(m_subset)[2],
sample_id = i
) |>
dplyr::select(sample_id, dplyr::everything())
},
.progress = TRUE,
.options = furrr::furrr_options(seed = TRUE)
) |>
dplyr::bind_rows()
では、係数と評価指標を確認しましょう:
simulation_df |>
tidyr::pivot_longer(!sample_id, names_to = "metric", values_to = "value") |>
dplyr::mutate(
学習対象 = dplyr::case_when(
metric %in% c("full_mape_full", "full_mape_subset") ~ "全データ学習 MAPE",
metric %in% c("full_rmse_full", "full_rmse_subset") ~ "全データ学習 RMSE",
metric %in% c("subset_mape_full", "subset_mape_subset") ~ "受注企業のみ学習 MAPE",
metric %in% c("subset_rmse_full", "subset_rmse_subset") ~ "受注企業のみ学習 RMSE",
metric %in% c("beta_full", "beta_subset") ~ "係数",
TRUE ~ NA
),
評価対象 = dplyr::case_when(
stringr::str_ends(metric, "full") ~ "全データ",
stringr::str_ends(metric, "subset") ~ "受注企業",
TRUE ~ NA
)
) |>
ggplot2::ggplot() +
ggplot2::geom_density(ggplot2::aes(x = value, fill = 評価対象)) +
ggplot2::labs(
x = "値", y = "分布"
) +
ggplot2::facet_wrap(~ 学習対象, scales = "free") +
ggplot2::theme_gray(base_family = "HiraKakuPro-W3")
なかなかインパクトのある図になりました。結論だけ先に言うと、「受注企業だけで学習したモデルは、受注企業だけを見ると非常に優秀に見えるが、日本中の企業を対象にすると壊滅的に性能が悪い」ということが、5000 回すべてのシミュレーションでほぼ一貫して確認できます。
まず左上の係数を見てください。赤色が全データで学習したモデル、青色が受注企業だけで学習したモデルです。全データで学習したモデルは、真の係数である 2 の周辺に非常に集中しています。一方で、受注企業だけで学習したモデルはほぼ 0 の近辺に張り付いており、サンプルを受注企業だけに制限した瞬間、本来存在していた x の効果をほとんど学習できなくなっています。
次に、「全データ学習 MAPE」や「全データ学習 RMSE」と書かれたパネルを確認してください。評価対象を(受注企業だけに閉じず)日本中の企業全体に広げた環境において、全データで学習したモデル(赤色の分布)の方が、受注企業だけで学習したモデル(青色の分布)よりも、MAPE・RMSEともに圧倒的にゼロ(左側)に近く、優れた予測性能を示していることがわかります。一方で、「受注企業のみ学習 MAPE」や「受注企業のみ学習 RMSE」のパネルから分かるように、評価対象を受注企業だけに絞ると、受注企業のみで学習したモデルの方が精度が高いことがわかります。
少し繰返しになりますが、ここで起きていることは極めて単純です。
学習データを受注企業だけに限定し、その後の評価も受注企業だけで行えば、「受注企業だけで学習したモデルは素晴らしい」という結論になります。しかし、そのモデルを実際の営業現場で使う瞬間、営業担当者が見るリストには受注企業だけが並んでいるわけではありません。ほとんどはまだ一度も契約したことのない企業です。
つまり、この評価はモデルが実際に戦う世界を評価していません。
これはまさに従属変数に基づくサンプル選択の典型例です。モデルが間違っているのではありません。評価対象そのものが、モデルにとって都合の良い世界だけを切り取ってしまっています。
営業現場に置き換えると、「過去に受注した会社だけを分析したら、この営業戦略は最高だった」というのと全く同じ話です。しかし、本当に知りたいのは、「まだ契約していない数百万社の中から、次にどの会社へ営業すべきか」です。
評価対象を都合よく切り取った瞬間、モデルの精度は簡単に良く見せることができます。しかし、そのモデルが現実世界でも本当に役立つかどうかは、また別の話なのです。
余談ですが、ゼロが多すぎて学習が終わらなくなることもあるでしょう。そういう時は、安易にゼロを消さずに、ゼロの観測値を統計的・機械学習的に安心して除去・削減するための手法を利用すること強くお勧めします:
ゼロの観測値を統計的・機械学習的に安心して除去・削減する手法は、また別の記事で説明します。
結果の解釈
では、なぜこのようなことが起きるのでしょうか。ここからは、ビジネスの実例で考えてみます。
計量経済学では、「従属変数に基づくサンプル選択によって、誤差項と説明変数の独立性が破られるため、推定量にバイアスが生じる」と説明されることが一般的です。もちろん、それは理論的には正しい説明です。ただ、ビジネスの現場で営業や企画担当者にその説明をしても、おそらく誰もピンと来ません。
そこで本記事では、あえて数式を忘れて、「業務用ベッドを販売する会社」という架空の例で考えてみます。
私は新卒から人材業界のディップ株式会社で働いているため、業務用ベッド業界については完全に素人です。それでも、おそらく業務用ベッド会社の営業先はホテル、旅館、社員寮、学生寮、病院などが中心でしょう。営業組織にも「ホテル営業部」や「宿泊施設営業部」のような部署は存在するかもしれません。
一方で、「図書館営業部」「水族館営業部」「美術館営業部」が存在する会社は、さすがにほとんどないのではないでしょうか。なぜなら、普通に考えれば図書館も水族館も美術館も、業務用ベッドを大量に購入することはほとんどないからです。
もちろん、世の中には例外があります。ホテルを併設した図書館や、図書館をコンセプトにしたホテル、宿泊施設を備えた美術館などを探そうと思えば見つかるでしょう。
しかし、それらはあくまで例外です。日本中の図書館や美術館を代表しているとは、誰も考えないでしょう。
ところが、受注した企業だけに絞って分析した瞬間、話は変わります。
もし営業データに登録されている図書館が、たまたまホテル併設の図書館や宿泊施設を備えた特殊な図書館ばかりだったらどうでしょうか。
モデルの立場からすると、「図書館」という業種は業務用ベッドを大量に購入してくれる、とても魅力的な顧客として映ります。しかも、このような特殊な図書館は決して一桁しか存在しないような極端なレアケースとは限りません。受注企業だけで学習したデータの中では、それなりの件数を占めてしまうため、モデルはそれをノイズではなく「意味のあるパターン」として学習してしまう可能性があります。
一般化線形モデルで業種固定効果を入れる場合でいうと、「図書館固定効果」が統計的に有意にならないと事前に保証することはできません。モデルは「図書館はベッドを買わない」という当たり前の事実を学ぶ機会そのものを失っています。その結果、ごく一部のホテル併設図書館や宿泊施設付き図書館だけを見て、「図書館とはベッドを大量に購入する業種である」と勘違いしたまま学習を終えてしまうのです。
つまり、モデルが間違っているのではありません。モデルは与えられたデータから最ももっともらしい規則を学習しているだけです。問題なのは、そのデータから「ベッドを買わない普通の図書館」が丸ごと消えてしまっていることなのです。
これが、受注企業だけで学習することの恐ろしさです。
ここで、「それなら CRM の業種分類をもっと細かくすればいい」「ホテル併設図書館を『ホテル』として管理すればいい」「法人マスターをもっと充実させて特徴量を増やせばいい」という意見もあるでしょう。
もちろん、それ自体は間違っていません。実際、第二業種や事業内容などの情報を追加すれば、この問題の一部は緩和できます。
ただ、その方向へ進み続けると、だんだん本業が変わってきますw。
業務用ベッド会社であるにもかかわらず、「全国の法人をどう分類するか」「どの会社が実はホテル事業も運営しているのか」「関連会社まで含めると何業なのか」といった情報をひたすら収集・整備することが仕事になっていきます。
もちろん、それを専門に行う会社は存在します。法人マスターを販売している会社は、まさにそのために膨大なコストをかけてデータを収集・整備しています。
もし「もっと特徴量を増やそう」「もっと深く法人情報を調べよう」という方向だけで問題を解決しようとすると、気が付けばあなたは業務用ベッド会社ではなく、法人マスター販売会社のような仕事をしていることになります。
データを増やすこと自体は重要です。しかし、「そもそも受注企業だけで学習している」という根本的なサンプリングの問題を放置したまま特徴量を増やし続けても、本質的な解決にはなりません。
おわりに
「受注企業だけで学習すれば精度が上がるじゃん!」
営業や営業企画、あるいはデータサイエンスプロジェクトの PM と仕事をしていると、一度は耳にしたことがある言葉ではないでしょうか。実際、受注企業だけを見れば、そのモデルの予測精度は向上することが多いです。本記事のシミュレーションでも、その結果はきれいに再現されました。
しかし、そのモデルは「受注した企業を予測するモデル」ではなく、「日本中の企業の中から、これから受注しそうな企業を探すモデル」です。評価対象が変われば、最適な学習データも変わります。
今回の業務用ベッドの例では、「図書館はベッドを大量に買う」という、一見もっともらしい結論が導かれました。しかし、それはホテル併設図書館だけを見て学習した結果であり、普通の図書館という圧倒的多数が学習データから消えていたために起きた現象でした。モデルは何も悪くありません。与えられたデータから、最も合理的な規則を学習しただけなのです。
もちろん、「第二業種」を追加したり、CRM の情報を充実させたりすれば、この問題を回避できるケースもあります。しかし、それは営業予測モデルの改善というより、法人マスターの整備という別のプロジェクトです。データを増やすことにはコストがあり、「営業戦略を改善したい」のか、「日本中の企業属性を完璧に管理したい」のかは、分けて考える必要があります。
ビジネスの現場では、「データを減らした方がノイズが減る」という発想は非常に魅力的です。実際、その場で評価すると精度が上がることも少なくありません。しかし、モデルは学習データの外側を知りません。消したデータは、ノイズだったのかもしれないし、将来意思決定を行う対象そのものだったのかもしれません。
営業データ分析に限らず、マーケティング、レコメンド、需要予測、与信、医療など、現実のデータは「ゼロだらけ」であることがほとんどです。そのゼロを見た瞬間に「邪魔だから消そう」と思ったら、一度だけ立ち止まって考えてみてください。
そうしてもゼロが多すぎて学習が終わらなくなることもあるでしょう。そういう時は、安易にゼロを消すのではなく、前述したゼロの観測値を統計的・機械学習的に安心して除去・削減するための手法を利用すること強くお勧めします。
そのゼロは、本当にノイズでしょうか。それとも、あなたがこれから予測したい世界そのものなのでしょうか。
最後に、私たちと一緒に働きたい方はぜひ下記のリンクもご確認ください:
