2026年3月のKaggleplaygroundにて開催された『Predict Customer Churn』に参加した。
その際にやったこと、できなかったこと(次回への課題)、学んだことなどを共有します。
――――――――――――――――
【目次】
【1】.やったこと
(1).初期EDA
①.前提条件の確認
②.各データのざっくりとした確認
③.目的変数の分布について
④.各列のカテゴリの確認
⑥.各説明変数と目的変数の関係の可視化
(2).ベースライン作成
①.アルゴリズムの選定
②.特徴量の選定
③.データ前処理
④.最終的なベースラインの概要
(3).スコア改善(フェーズ1)
①.ノイズデータの除去
②.Seedアンサンブル
(4).スコア改善のためのEDA
①.FP(False Positive)とFN(False Negative)に着目したEDA
②.FPとTP(True Predict)との差に着目したEDA
③.長期ユーザーの中での比較に着目したEDA
(5).スコア改善のための試行
①.tenureが20以上でかつTech supportがNo
②.tenureが20以上でかつOnline SecurityがNo
③.tenureが20以上でかつInternetServiceがFiber optic
④.tenureが20以上でかつPaymentMethodがaymentMethod
⑤.tenureが20以上でかつContractがOne year
(6).ChatGPTの支援を受けたスコア改善ための試行
①. 特徴量 TotalCharges / (tenure + 1) を追加
②.特徴量 train_x["MonthlyCharges"] * train_x["tenure"] を追加
③.特徴量 test_x["TotalCharges"] を追加
④.特徴量 TotalCharges / (MonthlyCharges + 1) を追加
(7).Seedアンサンブル
(8).最終結果
【2】.今回できなかったこと。(次回以降の課題)
・先月のコンペの復習に時間をかけすぎて、今月のコンペの参加が遅れてしまった。
・わからないことを何でもかんでも理解しようとしすぎて、今進めるべきこととのバ
・一番最初に工程を調べるべきだった。
【3】.学び・気づき(コーディング部分)
・『df[col].value_counts().head()』(その列でよく出てくる値トップ5を表示する)
・forについて掴んだコツ
・pd.get_dummies()『カテゴリ変数をダミー変数に変換する』
【4】.学び・気づき(分析部分)
・多重共線性【統計学】
・データから得た洞察と、提案の間に多くの仮説・検証がある。
・どんな特徴を拾えてないかを見る
【5】.今回使ったコード
――――――――――――――――
【1】.やったこと
(1).初期EDA
①.前提条件の確認
(a).評価基準
AUC
(b).予測する対象
Churn(サービスの退会者)の確率
②.各データのざっくりとした確認
(a).データ数
train:594194行、21列
test:254655行、20列
(b).列
データ型Object多い。変換が必要になる場面が多そうである。
(c).欠損値
nullなし。
③.目的変数の分布について
No 0.774792
Yes 0.225208
④.各列のカテゴリの確認
・ID
これは識別ID
・gender
性別についてのデータ。2値。
・SeniorCitizenPartner
高齢者かどうか。0と1の2値。
・Dependents
扶養家族の有無。0と1の2値。
・Partner、Dependents、PhoneService、PaperlessBilling
NoとYesの2値
・MultipleLines
NoとYesのほかにNo phone serviceの3値
・InternetService
DSLとFiber opticとNoの3値
・OnlineSecurity、
YesとNo OnlineSecurityとNo internet serviceの3値
・OnlineBackup
NoとYes OnlineBackupとNo internet serviceの3値
・DeviceProtection
YesとNo DeviceProtectionとNo internet serviceの3値
・TechSupport
YesとNo TechSupportとNo internet serviceの3値
・StreamingTV
NoとYes StreamingTVとNo internet serviceの3値
・StreamingMovies
NoとStreamingMovieとNo internet serviceの3値
・tenure
サービスの使用年数。
数値。
・Contract
Month-to-month、Two year、One yearの3値
恐らく、契約更新の間隔
・PaymentMethod
Electronic check、Credit card (automatic)Mailed check、
Bank transfer (automatic)の4値。
支払方法。
・MonthlyCharges
1ヶ月の支払額。数値
・TotalCharges
支払総額。数値。
・Churn
退会の有無。今回の目的変数。2値。
⑤.単変量についての確認
各データの分布についてのざっくりとした把握。
⑥.各説明変数と目的変数の関係の可視化
とりあえず単純そうな特徴量がないか調べた。
※しかし、最終的にはとりあえず全部特徴量として学習させた方がいいと後に
考え直したので、この時点での可視化はざっくりとした把握以上の意味はな
かった。
(2).ベースライン作成
** ①.アルゴリズムの選定**
LightGBMを使用。
まずは全ての改善の元となるヒントが欲しいので、前処理がほとんど不要でか
つ人間に拾いきれない特徴も広いうるLightGBM(以下LGBM)による予測を行い、そ
の後、拾いきれていない要素を探す、という方向性で今回は行く。
②.特徴量の選定
人間の目では無関係に見える特徴量もLGBMの場合は意味がある場合もあるので、
いったんすべての特徴量を学習に使うことを目指す。
③.データ前処理
3値あるカテゴリ変数のデータがあるので、これらについてはダミー変数化する
ことで対応。
④.最終的なベースラインの概要
(a).アルゴリズム
LightGBM。
(b).特徴量
全て(カテゴリ変数の物はダミー変数に変換)
(c).バリデーション設計
学習段階では交差検証(5フォールド。OOF)
最終検証はホールドアウト検証(全体の10%が検証用データ)
(d).結果
OOF AUC=0.91611
LBスコア AUC=0.91493
(3).スコア改善(フェーズ1)
この時点で、締め切りまで残り3日程度だったため、ChatGPTと戦略を相談しな
がら、特徴量改善を進めた。
まずは、少ない時間で確実に試行できる①~②を行った。
①.ノイズデータの除去
(a).仮説
とりあえずデータを全て学習に使っているが、その中にもしかしたら学習を
妨げているデータがあるかもしれない。
(b).試行
全ての列を1つずつ削除してみて、AUCの増減を見る。
(c).結果
Partner、gender、PhoneServiceは削除したほうがAUCが上がった。
OOF AUC=0.91616 ベースラインの0.91611より上昇
LBスコア 変化なし
LBの変化はないが、Fold間の標準偏差がベースラインより小さくなってるの
で予測のブレが少なくなっていると判断し、この試行は採用とした。
②.Seedアンサンブル
(a).仮説
現在はStratifiedKFoldにおいてあるSeed値一つでデータを切り分けているが
、これはもしかすると偏った形に切り分けられているかもしれない。
(b).試行
3通りのSeed値で切り分けて、それぞれ学習し、その結果を平均する。
(Seed値は0,42,2026)
(c).結果
AUC = 0.91644に改善した。
各
LBスコアも0.91376と、ベースラインよりも0.00005向上した。
基本的に、アンサンブルはブレが少なくなっているはずなので、その上で
スコアが改善しているのは、予測精度が上がっているとみなし、採用した。
(4).スコア改善のためのEDA
時間が多少余っていたので、何とか一つでも自分なりの特徴量を追加する形で
のスコア改善を目指すことにした。
見るデータを決め、仮説を建てるところまでは自分でやったが、データを見る
ためのPythonコードの実装などはChatGPTにやってもらった。
(コンペ終了後に復習した)
①.FP(False Positive)とFN(False Negative)に着目したEDA
(a).仮説
予測を間違えたデータの中で、明らかな予測ミスのデータに着目し、そのミ
スがなぜ起きたかを推察し、アルゴリズムが拾えてない意味を持つ特徴量を作
成できれば、AUCが改善するのではないかと考えた。
(b).試行
下記2つの変数を作成し、可視化した。
・FP:Chrunの確率が80%以上と予測して外したもの。偽陽性。
・FN:Chrunの確率が20%以下と予測して外したもの。偽陰性。問題有。
(c).結果
FNが約30,000行、FPが約7,000行と、FNが圧倒的に多いのでまずFNを集中的
に見ていくことにした。
また、データの傾向自体はわかっても、他の正解データとの比較をしないと
「アルゴリズムが見逃しているのは何か」がわからないことに気が付いた。
②.FPとTP(True Predict)との差に着目したEDA
(a).仮説
FNの中にあって、正しい予測の中にない特徴こそが、Churnしたかどうかを
予測するために重要な要素ではないか。
(予測で拾えてない特徴だが、その特徴を持ったデータがChurnしたという
ことなので)
(b).試行
FNとTPを並べて可視化した。
(c).結果
TPはFNと比べ、TotalCharges、tenureが低く、MonthlyChargesが高いことが
分かった。
また、tenureとTotalChargesが高い分布の部分がある、というFNのみでしか
捉えられてない特徴も見つかった。
このことから、現状の学習においては、短期間にお試しでサービスを一通り
使った人間のChurnは予測できているが、長期で多くのサービスを使っていた
人間のChurnは予測できていないのではないか、という仮説が建った。
特に、ヒストグラムを見る限り、tenureが20以上、TotalCharges2000以上の
Churnがあまり拾えてないように見えた。
③.長期ユーザーの中での比較に着目したEDA
(a).仮説
tenure20以上かつ、トータル2000ドル以上のデータの、Churnした人としてな
い人の傾向を比較すれば、Churnする人の特徴を拾えるのではないか。
(だいたいそのあたりのChurnしている人が現在拾えてないため)
(b).試行
tenure20以上かつ、トータル2000ドル以上でChurnした人
tenure20以上かつ、トータル2000ドル以上でChurnしてない人
この2者のヒストグラムを突き合わせて比べた。
(c).結果
Churn1の人の特徴は以下の5つが高い傾向にあることが分かった。
・InternetServiceがFiber optic
・Online SecurityがNo
・Tech supportがNo
・ContractがOne year
・PaymentMethodがElectronic checkが多い
まずはこれらの特徴を反映した特徴量を作ることにした。
(5).スコア改善のための試行
(4)で見出した仮説を元に、下記①~⑤の特徴量を作った。
①.tenureが20以上でかつTech supportがNo
②.tenureが20以上でかつOnline SecurityがNo
③.tenureが20以上でかつInternetServiceがFiber optic
④.tenureが20以上でかつPaymentMethodがaymentMethod
⑤.tenureが20以上でかつContractがOne year
(a).仮説
これらの特徴量は、恐らくアルゴリズムが拾えなかったChurnした人の特徴
のため、これらを拾わせることで精度が上がるのではないか。
(b).試行
それぞれの特徴量を1つずつ追加
(c).結果
①~⑤全て、AUCが微減
大きく減少したわけではないので、仮説が完全に間違っているわけではなさ
そうだが、表現が弱い可能性がある。
(6).ChatGPTの支援を受けたスコア改善ための試行
ここからは、コンペの締め切りまで3時間を切っていたため、ChatGPTによる支援
を強めた。
(5).③.(c)より、私の仮説は「長期ユーザーの中で、リテラシーの高い人間が
Churnしやすいのではないか」である。
この仮説をChatGPTに伝えた上で、スコア改善に繋がりそうな特徴量のアイデア
を求め、いくつかの提案の中から、私なりに検討できたものを採用し、試行した。
①. 特徴量 TotalCharges / (tenure + 1) を追加
(a).仮説
長期ユーザーの中で離脱するユーザーの「利用期間に対する支払いの密度
」に差があるのではないか、という提案があった。
この密度の中に、リテラシーの高いユーザーの情報も表現されている可能
性があると考え、この提案を採用した。
(b).試行
TotalCharges / (tenure + 1)という特徴量を追加し、アルゴリズム学習を
行った。
(c).結果
AUCが改善 0.91624に(0.00008の改善)
Fold間の標準偏差も小さいことから、確かな改善ではないかと思う。
特徴量の中に、的中と言えるほどではないものの、私の仮説で想定したユー
ザーの情報が恐らく含まれているのではないかと思う。
②.特徴量 train_x["MonthlyCharges"] * train_x["tenure"] を追加
(a).仮説
強い利用×長期間という特徴量は、総合的な利用強度という意味を持ち、
LGBMの分岐をよりよくしてくれるのではないか、とChatGPTに提案された。
交互作用の強調なので、もしかするとLGBMの分岐を簡易にして、精度の改善
に繋がりうると思い、この提案を採用した。
(b).試行
特徴量 train_x["MonthlyCharges"] * train_x["tenure"] を追加して
学習を行った。
(c).結果
AUC微減
ほとんどTotalChargesとの差がなく、ノイズにしかならなかったようである。
③.特徴量 test_x["TotalCharges"] を追加
- (test_x["MonthlyCharges"] * test_x["tenure"]) の追加
(a).仮説
理論値(期待される総額)と実績(実際の総額)のズレを表現するものとして
提案された。
このズレによって離脱する人がいるかもしれないという提案だったが、そ
の中には、リテラシーの高い人も当然含まれる(ズレに気が付くから)と思い
、この提案を採用した。
(b).試行
特徴量 test_x["TotalCharges"]
- (test_x["MonthlyCharges"] * test_x["tenure"]) の追加
(c).結果
AUC微減
②と同様、TotalChargesとあまり変わらず、ノイズにしかならなかったよう
である。
④.特徴量 TotalCharges / (MonthlyCharges + 1) を追加
(a).仮説
「契約上の期間」と「実際の利用期間」のズレを見れる特徴量として提案
された。
これは、割引などの消失と共に離脱した人を捉えうるという提案だった。
リテラシーの高い人ならば、割引などを十全に利用するだろうし、その割引
と主に離脱するというのはあり得ると考え、この提案を採用した。
(b).試行
特徴量 TotalCharges / (MonthlyCharges + 1) を追加
(c).結果
AUC微減
恐らくだが、tenureとあまり数値が変わらず、ノイズが増えただけになって
しまったようだ。
(7).Seedアンサンブル
この時点で、締め切りまで30分を切り、これ以上の試行は難しいと判断。
あとは、(3)の時のような特徴量の改善を期待して、Seedアンサンブルを実施し
た。(Seed値は(3)と同じ)
結果、AUCは変わらず。
しかし、アンサンブルによって、偏った結果がならされた上で、AUCが同じなら
ば、精度としては向上していると考え、採用した。
(8).最終結果
(a).提出したもの
・Partner、gender、PhoneServiceは特徴量から削除
・特徴量 TotalCharges / tenure を追加
・Seedアンサンブル(0,4,2026)の実施
(b).結果
PrivateScore 0.91521
順位 1599位(4143人中) 上位38%
【2】.今回できなかったこと。(次回以降の課題)
・2月のコンペの復習に時間をかけすぎて、3月のコンペの参加が遅れてしまった。
もう少し復習の時間短くし、復習事項のリストをあらかじめ作り、規定の時間で
やりきれなった分は、スキマ時間にキャッチアップをする形を取るべきだった。
4月のコンペでは、最初の10日を復習期間とし、残りの20日でコンペに臨めるよ
うにする。
・わからないことを何でもかんでも理解しようとしすぎて、今進めるべきこととのバ
ランスを崩してしまった。
きちんと理解した上でやれるのは理想的だが、理論的なことを突き詰めるよりも
、試行を増やし、そこから得られる生の知見の方が、今は経験値が多い。
わからないことの対象によって、もう少し対応を細かく変えるべきだった。
EDA、特徴エンジニアリングなど、工程に関する手法など、工程を進める上で必要
な知識ならきちんとキャッチアップ。
コーディングなど実装部分は、何をやってるのか、どこをいじるのかだけまずは
把握し、理論的なところは後でキャッチアップ。(完全なブラックボックス化だけ避
ける)
これくらいの方針で4月は臨む。
・一番最初に工程を調べるべきだった。
書籍を参考に工程を組んではいるものの、EDAの見所、アルゴリズムの読みやすい
特徴量など、より具体的な手法がおそらくあったのに、我流で推し進めてしまった。
我流でやったことにより、どこが具体的にわからないか、どんな手法が欲しいかが
浮き彫りになった面もあるので、無駄だったとまではいかないが、来季以降は同じこ
とをしてはいけない。
【3】.学び・気づき(コーディング部分)
・『df[col].value_counts().head()』(その列でよく出てくる値トップ5を表示する)
(a).コードの解説
『df[col].value_counts()』(その列にどの値が難解出てくるか教える)
『df.head()』(出現回数が多い順に上から5個表示する)
これらの合わせ技で成立している。
(b).使用シーン
EDAで、どのデータが良く出てくるかを調べる時、等。
(c).より高度なこと
for col in df.columns:
print(col)
print(df[col].value_counts().head())
print("-"*30)
※1行目と3行目dfを任意の変数に変える。
colは変えなくて良い。そこにforの効果が及ぼされる。
・forについて掴んだコツ
forは、要するに、for以下のインデントしてある行におけるfor 以下の変数に、
in以下のリストや戻り値などを1つずつ順番に最後まで入れてプログラムを実行し、
それをリストや戻り値の値に全部について行いますよ。
ということ
厳密には繰り返しなのだが、意味としては全部やる、で良い。
全部やりたい時に使うのに、説明が「繰り返し」だから、なかなか使用場面と
定義がリンクしなかった、というのがこれまでよくわからなかった原因。
要は、forは全部に同じ処理をするための道具といえる。
そして、順番に取り出すものなら何でも回せる。(というように現時点では思え
る)
・pd.get_dummies()『カテゴリ変数をダミー変数に変換する』
(a).概要
カテゴリ変数をダミー変数に変換する。
例)Colorという列に入力されているRed、Blue、Greenというデータがあるとする。
Color_Red、Color_Blue、Color_Greenといった列を新たに設け、各行を1と0で
表現することで、該当するデータがどのカテゴリなのかを数値で表現する形に変
換する。
(b).具体例
pd.get_dummies(all_data, drop_first=False)
(c).具体例の解説
all_data→変換する対象のDataFrame(複数列を含む)
drop_first=False→Falseだと全部のカテゴリを残す。Trueだと1つだけ削除する。
(d).備考
・なぜdrop_firstがあるのか
多重共線性(ダミー変数トラップ)を避けるため。
※多重共線性については別のメモがあるのでそちらを参照。
決定木の場合はFalseで問題ない。(「列同士の線形関係」を気にしないため)
線形回帰、ロジスティック回帰では問題になる。
・get_dummiesの“怖いところ”
列爆発
カテゴリが多いと → 数百列
train/testズレ
concatしないと事故る
意味の破壊
順序があるカテゴリでもバラバラにする。
(順序関係(low < medium < high)や距離感の情報が失われる)
※この変換は何を失っているか、を考えると注意しやすい。
・どのエンコーディングを使うかは、モデルとデータの性質によって選択する必要
がある
【4】.学び・気づき(分析部分)
・多重共線性【統計学】
(a).概要
説明変数同士が強く相関してしまっている状態。
(ある説明変数が、他の説明変数からほぼ表現できてしまう状態)
(b).具体例、
身長(cm)
体重(kg)
BMI(体重 ÷ 身長²)
というデータがあったとして、BMIは身長と体重から作られている。
つまり、「ほぼ同じ情報を別の形で持っている」。
こういう状態が多重共線性である。
(c).多重共線性で起こる問題。
・回帰係数が不安定になる。
違いがあまりないため、データのたびに役割の比重が変わってしまい、どちら
がより影響力を持っているのかがわからなくなってしまう。
影響力がデータごとに変わってしまうため、解釈が揺れてしまう。(=解釈が
不安定)
(同じ予測を出せる係数の組み合わせが複数存在するため)
予測悪化しなくとも、係数の解釈が不安定になる。
※あくまでも線形モデルの問題の話。決定木などは別。
・データから得た洞察と、提案の間に多くの仮説・検証がある。
何かそれっぽい洞察を得た時に、だから〇〇だ!、となりがち。
ただ、そういう単純な関係であることはかなり少ない。
実際は、何故そうなったか? という仮説をいくつか建て、その上でそれが本当
ならこうなっているのではないか? という検証が必要。
要するに、グラデーションがあり、それらを細かく考えていく必要がある。
・どんな特徴を拾えてないかを見る、
特徴量を改善するときの着眼点で、「どんな特徴を拾えずに予測を外しているか」
という着眼点がある。
【5】.今回使ったコード
#前処理用
import numpy as np
import numpy.random as random
import scipy as sp
import pandas as pd
from pandas import Series, DataFrame
#可視化
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
%matplotlib inline
#モデリング
from sklearn.model_selection import train_test_split, KFold, StratifiedKFold
from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix
import lightgbm as lgb
import warnings
warnings.filterwarnings('ignore')
#小数点第三位まで表示
%precision 3
――――――――
#データの読み込み
train = pd.read_csv('../input/competitions/playground-series-s6e3/train.csv')
test = pd.read_csv('../input/competitions/playground-series-s6e3/test.csv')
sample_submission = pd.read_csv('../input/competitions/playground-series-s6e3/sample_submission.csv')
――――――――
#Churnをint64へ変換
train['Churn'] = train['Churn'].map({
"No": 0,
"Yes": 1
})
――――――――
#=========================
#ベースライン構築
#=========================
#目的変数
target = "Churn"
#idを保存(提出用)
test_id = test["id"].copy()
#削除したい元の特徴量
drop_cols = ["Partner", "gender", "PhoneService"]
#説明変数と目的変数に分ける
train_x = train.drop(columns=["id", target] + drop_cols).copy()
test_x = test.drop(columns=["id"] + drop_cols).copy()
y = train[target].copy()
#=====================================================
#特徴量追加(本命)
#TotalCharges / tenure の比率
#tenure=0 対策として +1
#=====================================================
train_x["charge_per_tenure"] = train_x["TotalCharges"] / (train_x["tenure"] + 1)
test_x["charge_per_tenure"] = test_x["TotalCharges"] / (test_x["tenure"] + 1)
#train/testを結合してダミー変数化
all_data = pd.concat([train_x, test_x], axis=0)
all_data = pd.get_dummies(all_data, drop_first=False)
#元に戻す
X_train = all_data.iloc[:len(train_x)].copy()
X_test = all_data.iloc[len(train_x):].copy()
print("X_train shape:", X_train.shape)
print("X_test shape :", X_test.shape)
print(X_train[["tenure", "TotalCharges", "charge_per_tenure"]].head())
――――――――
#=========================
#seed ensemble + CV + LightGBM
#=========================
#使うseed
seeds = [0, 42, 2026]
#seed平均用の箱
oof_pred_ensemble = np.zeros(len(X_train))
test_pred_ensemble = np.zeros(len(X_test))
#各seedの結果を記録
seed_cv_scores = []
for seed in seeds:
print("=" * 50)
print(f"seed = {seed}")
# このseed用のOOF/test予測の箱
oof_pred = np.zeros(len(X_train))
test_pred = np.zeros(len(X_test))
# CV
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=seed)
# LightGBMパラメータ
params = dict(
objective="binary",
learning_rate=0.05,
n_estimators=10000,
num_leaves=31,
subsample=0.8,
colsample_bytree=0.8,
random_state=seed,
n_jobs=-1
)
fold_scores = []
for fold, (tr_idx, va_idx) in enumerate(skf.split(X_train, y), 1):
X_tr = X_train.iloc[tr_idx]
X_va = X_train.iloc[va_idx]
y_tr = y.iloc[tr_idx]
y_va = y.iloc[va_idx]
model = lgb.LGBMClassifier(**params)
model.fit(
X_tr, y_tr,
eval_set=[(X_va, y_va)],
eval_metric="auc",
callbacks=[
lgb.early_stopping(100, verbose=False),
lgb.log_evaluation(0)
]
)
# valid予測
va_pred = model.predict_proba(X_va, num_iteration=model.best_iteration_)[:, 1]
oof_pred[va_idx] = va_pred
# test予測
test_pred += model.predict_proba(X_test, num_iteration=model.best_iteration_)[:, 1] / skf.n_splits
# fold AUC
fold_auc = roc_auc_score(y_va, va_pred)
fold_scores.append(fold_auc)
print(f" Fold {fold}: AUC = {fold_auc:.5f}")
# seedごとのCV
cv_auc = roc_auc_score(y, oof_pred)
seed_cv_scores.append(cv_auc)
print("-" * 40)
print(f"seed {seed} CV OOF AUC = {cv_auc:.5f}")
print(f"seed {seed} Fold mean AUC = {np.mean(fold_scores):.5f}")
print(f"seed {seed} Fold std AUC = {np.std(fold_scores):.5f}")
# seed平均用に加算
oof_pred_ensemble += oof_pred / len(seeds)
test_pred_ensemble += test_pred / len(seeds)
seedアンサンブル後のCV
ensemble_cv_auc = roc_auc_score(y, oof_pred_ensemble)
print("=" * 50)
print("Seed Ensemble Result")
print(f"Seeds = {seeds}")
print(f"Each seed CV AUC = {[round(score, 5) for score in seed_cv_scores]}")
print(f"Ensemble CV OOF AUC = {ensemble_cv_auc:.5f}")
――――――――
#=========================
#submission作成
#=========================
submission = pd.DataFrame({
"id": test_id,
"Churn": test_pred
})
submission.to_csv("submission_Attempt15.csv", index=False)
submission.head()
――――――――