はじめに
はじめまして!三井住友カード データ開発本部に所属している、新卒データサイエンティストの初田です。
現在の業務では、加盟店売上予測モデルの構築や、マーケティング施策のためのモデリング支援等 に携わっています。
今回、2025年11月から2026年の1月にかけて開催された、SMBCグループのプライベートデータ分析コンペである「第6回D-1グランプリ」に参加し、ビズデータチャレンジ(初中級者向け)で323人中9位(メジャー部門7位)、テクニカルチャレンジ(中上級者向け)で180人中7位という結果を収めることが出来ました!
本記事では参加報告の前編として、D-1グランプリの概要と、初中級者向けの「ビズデータチャレンジ」の参加報告をお伝えできればと思います。
来週公開予定の後編では、中上級者向けの「テクニカルチャレンジ」の参加報告をお伝えする予定です。
SMBCグループ内で開催されているデータ分析コンペの内容や、コンペ参加から得られた学びなどを知っていただき、三井住友カードでのデータサイエンス業務やD-1グランプリに興味を持っていただければ幸いです!
SMBCグループ D-1グランプリとは
D-1グランプリの概要
D-1グランプリは、SMBCグループ内で2020年より開催されているデータ分析コンペで、隠れたデータ活用人材の発掘やスキルアップを目的とした、SMBCグループの従業員全員が参加できるコンペです。
今回のコンペで第6回を迎え、今大会は過去最大となる576名が参加し、主要グループ全8社で昨年より参加者数が増加するなど、グループ全体でデータ利活用への関心が高まっていると感じました。
今年は課題設定の難易度に応じて2種類のコンペが設けられていました。
全員が両方のコンペに参加することが出来ますが、初中級者向けのビズデータチャレンジでは、①プログラミング未経験者向けの「ルーキー部門」と、②経験者向けの「メジャー部門」というように、参加者のデータ分析・プログラミングのスキルレベルに応じて順位が設定される工夫がなされていました。
各コンペ課題の概要
ビズデータチャレンジ(初中級者向け): 二値分類課題。2025年11月から12月中旬まで開催。ルーキー部門とメジャー部門があり、それぞれの上位3名はモデルの精度だけでなく、コンペ終了後に提出するレポートの質も加味して順位が決定される。323名 が参加。
テクニカルチャレンジ(中上級者向け): 時系列予測課題。2025年11月下旬から2026年1月初旬まで開催。モデル精度のみで順位が決定され、スキル別の部門設定はない。180名が参加。
D-1グランプリはコンペ終了後に 表彰式と懇親会があり、上位入賞者による参加者同士の交流の場が設けられていました。
懇親会では過去2回以上入賞の殿堂入りの方々の参加や、テクニカルチャレンジ総合1位の方による解法紹介もあり、美味しいお食事を囲みながら様々なお話を伺うことが出来ました。
初中級者向け「ビズデータチャレンジ」
ビズデータチャレンジ概要
ビズデータチャレンジの課題は、架空の企業データセットを用いて、各企業が従業員向けのDX教育商材を購入するかどうかを予測する二値分類タスクでした。
コンペのために設計された企業データが提供され、従業員数や業界区分、主要な業務内容、組織構造、財務指標、従業員アンケートの情報など、さまざまな切り口で企業を捉えられる内容になっています。
これらのデータを用いて予測モデルを構築し、「どのような企業がDX教育を積極的に取り入れる傾向にあるのか」 をデータから導き出すことが、このコンペのテーマとなります。

提供データ
提供されたデータは以下の3種類です。
- 企業の基本情報:従業員数、業界、組織図といった、企業の規模や性質を示す基本データ
- 財務三表データ:B/S(貸借対照表)、P/L(損益計算書)、C/F(キャッシュフロー計算書)からなる財務情報
- 企業へのアンケート結果:企業文化やDXの現状、意欲に関する複数のアンケート結果。特に、自由記述形式のテキストデータが含まれており、定性的な意見も分析の対象となる。
このタスクは、数値、カテゴリ、そしてテキスト データといった、形式の異なるデータを統合的に扱う必要がありました。これらの異種混合データから、いかにして企業のDX教育投資への意思決定に繋がる本質的な特徴を抽出し、有効な特徴量を設計するかが、モデルの精度を向上させる鍵となりました。
評価指標
本コンペの評価指標はF1スコアだったため、Precision(適合率)とRecall(再現率)の両方が考慮された評価となっていました。
解法紹介
前処理
データの欠損値補完、特徴量生成を含む前処理を実施しました。
欠損値補完
初期の特徴量の中で工場数と従業員数等に欠損値が含まれていたため、これらをそれぞれの平均値で補完しました。
一部のアンケート回答データにも欠損値が存在しましたが、他の質問で特定の回答をした場合にのみ回答できるものだったため、欠損値埋めはしていません。
特徴量開発
(1) DX関連部署フラグ
組織図の中にDX推進室等のDX関連部署がある場合に1、それ以外を0としました。
これは組織図中に"DX"という単語が入っているか否かで判定しました。DX教材の購入をする企業はそれなりの意欲があると考え、すでにDX関連部署を設置している企業を抽出する意図です。
(2) 財務系特徴量
財務三表データを活用し、さまざまな指標を作成(以下は一部例)。特に「ソフトウェア投資比率」、「キャッシュフロー・マージン」、「有利子負債度依存度」、「ROE(自己資本利益率)、「ROA(総資産利益率)」がよく効いていました。
df["DX投資指数"] = df["無形固定資産変動(ソフトウェア関連)"] / df["投資CF"] df["一人当たり教育余力"] = df["営業利益"] / df["従業員数"] df["財務安定性スコア"] = df["自己資本"] / df["総資産"] df["ソフトウェア投資比率"] = df["無形固定資産変動(ソフトウェア関連)"] / df["売上"] df["対営業CF投資比率"] = df["投資CF"] / df["営業CF"] df["減価償却比率"] = df["減価償却費"] / df["売上"] df["一人当たり売上高"] = df["売上"] / df["従業員数"] df["売上高営業比率"] = df["営業利益"] / df["売上"] df["ROA(総資産利益率)"] = df["営業利益"] / df["総資産"] df["ROE(自己資本利益率)"] = df["当期純利益"] / df["自己資本"] df["キャッシュフロー・マージン"] = df["営業CF"] / df["売上"] df["有利子負債度依存度"] = (df["短期借入金"] + df["長期借入金"]) / df["総資産"] df["流動性プロキシ"] = df["流動資産"] / df["総資産"]
(3) アンケート系特徴量
アンケートデータの数値を用いて新たな指標を作成。一例ではありますが以下のような特徴量を作成していました。 アンケート項目から企業のDX教育への意欲やDX化への焦りを捉えようとしましたが、モデル精度向上につながる特徴量は生み出せませんでした。
df["情報感度"] = df["アンケート9"] + df["アンケート11"] df["戦略と現場ギャップ"] = df["アンケート1"] - df["アンケート3"] df["焦燥感スコア"] = df["アンケート1"] - df["アンケート2"]
※ 各アンケートの内容
- アンケート1:DX推進の戦略的方向性はどの程度明確か?(1:非常に低い、…、5:非常に高い)
- アンケート2:DX化の満足度は?(1:非常に低い、…、5:非常に高い)
- アンケート3:最新デジタル技術の導入状況は?(1:非常に低い、…、5:非常に高い)
- アンケート9:新技術イベントやセミナーへの参加率は?(1:非常に低い、…、5:非常に高い)
- アンケート11:DXに関する情報収集をどの程度行っているか(1:非常に低い、…、5:非常に高い)
(4) テキスト埋め込み
自由記述の「今後のDX展望」という各社1,000字程度のテキストデータから、DX教材購入可否に関わる部分を抽出し、BERTを用いてベクトル化。その後次元削減を行いました。
「今後のDX展望」は、その企業のDX教育商材購入の本気度が最も現れていた箇所だったため、コンペ参加当初から、このデータを活用することで、DX教材の購入有無を高精度で当てられるだろうと考え、特に力を入れた部分です。
テキストデータのベクトル化には複数のBERTモデルを試しましたが、最終的には sentence-BERT [1]、次元削減には UMAP [2] を用いました。
df = pd.read_csv("train.csv", encoding='utf8') df2 = pd.read_csv("test.csv", encoding='utf8') # キーワード設定 dx_keywords = { "従業員", "研修", "DX理解", "デジタルスキル", "リテラシー", "DX教育", "カリキュラム", "商材", "しかしながら", "ましてや" } def build_pattern(keywords): parts = [re.escape(kw) for kw in keywords] return re.compile("|".join(parts), flags=re.IGNORECASE) DX_PATTERN = build_pattern(dx_keywords) def extract_dx_words(series: pd.Series) -> pd.Series: """ DX関連の文のみを抽出したSeriesを返す関数 """ def _process_single_text(text): # 欠損値(NaN)や空文字の場合は空文字を返す if pd.isna(text) or text == "": return "" sentences = re.split('[。.\n]', str(text)) matched_sentences = [] for s in sentences: # パターンにマッチする場合のみリストに追加 if DX_PATTERN.search(s): matched_sentences.append(s + "。") return "".join(matched_sentences) return series.apply(_process_single_text) # 実行例 df["今後のDX展望"] = extract_dx_words(df["今後のDX展望"]) df2["今後のDX展望"] = extract_dx_words(df2["今後のDX展望"]) def get_bert_embeddings(text_list): bert_model.eval() embeddings = [] # バッチ処理 batch_size = 32 for i in tqdm(range(0, len(text_list), batch_size)): batch_texts = text_list[i : i + batch_size] # トークン化 inputs = tokenizer( batch_texts, return_tensors="pt", padding=True, truncation=True, max_length=256 ).to(device) with torch.no_grad(): outputs = bert_model(**inputs) token_embeddings = outputs.last_hidden_state attention_mask = inputs['attention_mask'].unsqueeze(-1).expand(token_embeddings.size()).float() sum_embeddings = torch.sum(token_embeddings * attention_mask, 1) sum_mask = torch.clamp(attention_mask.sum(1), min=1e-9) mean_embeddings = (sum_embeddings / sum_mask).cpu().numpy() embeddings.append(mean_embeddings) return np.concatenate(embeddings, axis=0) # 実行 df['今後のDX展望'] = df['今後のDX展望'].fillna("なし") df2['今後のDX展望'] = df2['今後のDX展望'].fillna("なし") all_texts = df['今後のDX展望'].tolist() + df2['今後のDX展望'].tolist() text_vectors = get_bert_embeddings(all_texts) # UMAPの設定 n_components = 50 n_neighbors = 15 min_dist = 0.1 print(f"UMAPによる次元圧縮を開始... (metric='cosine', dim={n_components})") umap_reducer = umap.UMAP( n_components=n_components, n_neighbors=n_neighbors, min_dist=min_dist, metric='cosine', random_state=42, n_jobs=-1 ) umap_vectors = umap_reducer.fit_transform(text_vectors) train_umap_vectors = umap_vectors[:len(df)] test_umap_vectors = umap_vectors[len(df):] cols = [f'bert_umap_{i}' for i in range(50)] df_train_umap = pd.DataFrame(train_umap_vectors, columns=cols, index=df.index) df = pd.concat([df, df_train_umap], axis=1) df_test_umap = pd.DataFrame(test_umap_vectors, columns=cols, index=df2.index) df2 = pd.concat([df2, df_test_umap], axis=1)
全体の流れとしては、①日本語に特化したモデル準備(sonoisa/sentence-bert-base-ja-mean-tokens-v2[3])→ ②データ読み込みとキーワードフィルタリング → ③sentence-BERT 埋め込みの生成 → ④UMAPによる次元圧縮 → ⑤特徴量としてDataFrameに結合です。
①では、日本語に特化したsentence-BERTを選定しています。
②では、「今後のDX展望」カラムの文章内からDX商材の購入に関係のある部分のみ抽出しています。
各企業、当該カラムの文章量は全体で1,000字程度であり、DX商材購入にはクリティカルに効いてこない文章も含まれていたため、DX商材について語っている文章によく含まれていたワードを見つけ出し、そのワードが含まれている文章のみ抽出する加工をベクトル化の前に行っています。
社員のDX教育に関連する文脈だけを残してsentence-BERTに渡すことで、より意味のある埋め込みを得る意図があります。
③では、テキストをトークナイズし、出力されたトークンにMean Poolingを施しています。
④768次元のベクトルを50次元に圧縮しています。
sentence-BERTを適用したそのままの状態では、700次元以上作成されることから、高次元データの局所的な近傍構造を維持しつつ非線形に次元削減を行えるUMAPを用いて次元削減しました。
次元削減手法にはPCA、t-SNEなどもありますが、手元の結果ではUMAPが最もモデル精度向上に寄与していました。
モデル構築
モデルは勾配ブースティング木のLightGBM 、Catboostとニューラルネットワークを使用しました。この3モデルをCatboostでスタッキングし、最終的な推論値を出力しました。
本コンペでは、スタッキングを用いることで、モデル単体や複数モデルのアンサンブルと比較して、PublicスコアとPrivateスコアどちらも改善していました。
後処理
モデルスコアの分類閾値を単純に0.5と設定することもできますが、今回はF1スコアを最大化するよう、K-fold CVを用いて、閾値を0から1まで0.01単位で動かしつつ、各閾値でのF1スコアを計算することで、最もF1スコアが高くなる閾値の探索を行いました。
以下は、スタッキング1段目のCatboostの予測値で閾値を最適化したときのグラフです。閾値を0.5に設定するとF1スコアが約0.72、0.24では0.8以上のスコアが出ることが分かります。本コンペはF1スコア1/100単位で競っていたため、閾値走査の重要性が高いことが分かります。

結果
上記の努力が功を奏して、最終的なPrivateデータでの評価結果として、323人中9位(メジャー部門7位、F1スコア:0.7597)に入賞することができました!
(Publicでも7位で、F1スコアは0.8244でした)
おわりに
本記事(前編)では、SMBCグループのプライベートデータ分析コンペである「D-1グランプリ」の概要と、初中級者向けの「ビズデータチャレンジ」についてご紹介しました!
私はコンペに参加したとたんハマってしまい、平日は業後に毎日モデルを回し、土日も時間を割いてモデルの精度向上に努めて、あっという間の2か月でした。
次回記事(後編)では、中上級者向けの「テクニカルチャレンジ」についてご紹介します!