背景
私は毎朝SAP Communityのブログを読んでいます。前日以降に投稿された全てのタイトルを見て、興味のある記事を読んで、内容が良ければ「いいね」しています。
「AIの時代にもっと効率的な方法ないの?」と思われるでしょう。考えられる方法の一つは、「生成AIにブログと自分の好みを伝えて、おすすめリストを作ってもらう」ことです。
しかし、今回はあえてSAP-RPTを使って「分類」(Classification)の問題として扱ってみることにしました。理由はSAP-RPTを使ったことがなく、使ってみたかったからです。
検証の概要
SAP Community APIを使い、7月1ヶ月分のブログを抽出し、そこに実際「いいね」したブログを突き合わせたものをコンテキスト(参考データ)として作成しました。
RPTにコンテキストと予測対象である直近のブログを渡し、「いいね」するかしないかを予測させました。「いいね」は白黒ではなく、確率 (0~1) として返ってきます。
コンテキストの作成、評価指標の提案、計算はClaudeが実施し、文章は作者が書いています。
評価方法
Recall@10(その日実際「いいね」した全体の件数のうち、上位10件までに何件を拾えたかの割合)で評価を行いました。
※詳細な説明は「評価」のセクションを参照
結果
8/1〜8/14のブログについて、RPTによる予測と実際の結果を突き合わせた結果、Recall@10の平均は0.796でした。これは、「上位10件に目を通せば、好みのブログの約8割を拾える」ことを表します。
普段の「古い順に読む」(0.312)のと比べ、Recall@10は平均約2.6倍となりました。
「いいね」予想の手順
1. コンテキストの作成
1.1. 7月1ヶ月分のブログを取得
SAP CommunityのAPI (https://community.sap.com/api/2.0/search) を使います。
7月1ヶ月分のブログを取得ためのクエリは以下です。
https://community.sap.com/api/2.0/search?q=...の形式で渡すと、結果が帰ってきます。
q=SELECT id, subject, teaser, author.login, post_time, board.id, view_href
FROM messages
WHERE conversation.style = 'blog'
AND depth = 0
AND post_time > 2026-07-01T00:00:00.000Z AND post_time < 2026-08-01T00:00:00.000Z ORDER BY post_time DESC LIMIT 1000
1.2. 自分が「いいね」したブログを取得
クエリに以下を指定します。kudos取得時はブログの投稿日時を指定することができないため、過去1000件分を取得しています。
q=SELECT id, message.id, message.subject, message.post_time, time
FROM kudos
WHERE user.id = '<自分のユーザID(数字)>' ORDER BY time DESC LIMIT 1000
1.3. コンテキストの作成
1.1.と1.2.の結果をidでひもづけて、コンテキストを作成します。コンテキストに使用した列はsubject、author.login、board.idです。
[
{
"message_id": "m14453712",
"subject": "Good to Know # 4. The SAP Feature That Saved Us from Recreating an Entire Sales Order",
"board_id": "erp-blog-members",
"author_login": "Prashanth-Gaddam",
"liked": 0
},
{
"message_id": "m14436107",
"subject": "Your Signing Certificates Are About to Expire. Here Is What to Do.",
"board_id": "technology-blog-sap",
"author_login": "MichaelShea",
"liked": 1
},
...
]
RPTのAPIにはindex_columnという指定があり、「レスポンスのどの行がリクエストのどの行か」を突き合わせるためのキー列を指定できます。
SAP Communityのブログは14312523のような数値形式のidを持っているので、これを使用しようと考えました。ところが、検証の中で大きなid(例:20000001)をindex_columnに渡すと、レスポンスで丸められて20000000のようになる事象が確認されました。
Claudeによると、「RPTは数値のindex_columnを内部でfloat32にキャストしており、float32の有効精度は24ビットしかないことから、2^24 = 16,777,216 を超える整数は正確に表現できない」ためだと考えられました。8月時点でidはまだその閾値に届いていませんが、今年中には到達するとみられます。そこで、APIに渡す際にidの先頭に"m"のプリフィックスをつけてm14312523のようにし、文字列型と認識させることにしました。
後から振り返ると、別にここはブログのidにする必要はなく、連番などで良かったと思います。
1.4. 予測対象のブログを取得
8/1〜8/14までの2週間分のブログを予測対象として取得します。1.1.と同じAPIを使い、クエリは以下のように指定します。予測対象についても、1.2.のクエリで「正解」(いいねしたか、しなかったか)を取得済みです。
q=SELECT id, subject, teaser, author.login, post_time, board.id, view_href
FROM messages
WHERE conversation.style = 'blog'
AND depth = 0
AND post_time > 2026-08-01T00:00:00.000Z AND post_time < 2026-08-15T00:00:00.000Z ORDER BY post_time DESC LIMIT 1000
2. SAP-RPTのAPIを使って予測する
SAP-RPTはhttps://rpt.cloud.sap/api/predictというプレイグラウンドのエンドポイントを持っています。
rowsにコンテキストと予測したい行を渡します。予測したい項目(liked)には"[PREDICT]"という値を設定します。
参考:https://rpt.cloud.sap/docs/api/overview
検証に利用したのはRPT-1.5です。
{
"rows": [
{
"message_id": "m14453712",
"subject": "Good to Know # 4. The SAP Feature That Saved Us from Recreating an Entire Sales Order",
"board_id": "erp-blog-members",
"author_login": "Prashanth-Gaddam",
"liked": 0
},
{
"message_id": "m14436107",
"subject": "Your Signing Certificates Are About to Expire. Here Is What to Do.",
"board_id": "technology-blog-sap",
"author_login": "MichaelShea",
"liked": 1
},
/* … context行 計882件 … */
{
"message_id": "m14464273",
"subject": "Useful document collection for MDS_PPO2",
"board_id": "erp-blog-sap",
"author_login": "SherryWang01",
"liked": "[PREDICT]"
},
/* … query行 計334件 … */
],
"index_column": "message_id",
"scenario": "manual-run",
"sampler_options": { "seed": 42 },
"explanations": { "top_column_scores": 5, "top_relevant_context_rows": 3 }
}
レスポンスは以下の形式です。likedの予測結果はpredictionに設定されており、0.08500514924526215のような確率の値として入ってきます。
また、explanationsには予測対象の行ごとに、予測に影響した列とコンテキスト行が設定されます。
{
"prediction": {
"explanations": {
"top_column_scores": [
{ "author_login": 0.217, "board_id": 0.327, "subject": 0.456 },
{ "author_login": 0.223, "board_id": 0.338, "subject": 0.439 }
/* … 残り 332 件(query行1件につき1個) … */
],
"top_relevant_context_rows": [
[382, 128, 721],
[382, 128, 574]
/* … 残り 332 件 … */
]
},
"id": "5e01d463-573d-4fa8-9f19-fe5a46324294",
"metadata": {
"num_columns": 5,
"num_predictions": 334,
"num_query_rows": 334,
"num_rows": 1216
},
"predictions": [
{
"liked": [
{ "confidence": null, "confidence_interval": [0, 1], "prediction": 0.08500514924526215 }
],
"message_id": "m14464273"
},
{
"liked": [
{ "confidence": null, "confidence_interval": [0, 1], "prediction": 0.07571086287498474 }
],
"message_id": "m14464272"
}
/* … 残り 332 件(query行と同じ順) … */
],
"status": { "code": 0, "message": "ok" }
},
"delay": 297.38564997911453,
"samplingMetadata": {
"sampledFrom": 882,
"sampledTo": 882,
"strategy": "random",
"downsampled": false
}
}
評価
- 予測対象:2026/8/1〜8/14 のブログ334件(うち「いいね」55件)
- モデルに渡した参考データ(コンテキスト):7月の記事882件(うち「いいね」156件)
「いいね」判定の成否
RPTからのレスポンスは0〜1の確率として返ってくるので、便宜的に0.5以上を「いいねする」と予測したものとして、以下の表にまとめます。
| 実際に「いいね」した | 実際はしていない | |
|---|---|---|
| モデルが「いいね」すると判定 | 15 | 13 |
| モデルが「いいね」しないと判定 | 40 | 266 |
適合率(Precision): 53.6%
再現率(Recall) 27.3%
適合率とは、モデルが「いいね」すると判定したブログのうち、実際に「いいね」したブログの割合です。モデルによるおすすめの信頼度の指標になります。
モデルが判定し、実際にいいねもした (15) / モデルが判定した全部 (15 + 13) = 53.6%
再現率とは、「いいね」したブログ全体のうち、モデルが「いいね」すると判定したブログの割合です。「取りこぼしの少なさ」の指標になります。
モデルが判定し、実際にいいねもした (15) / 実際にいいねした全部 (15 + 40) = 27.3%
上記の結果から言えることは、以下です。
- しきい値を0.5とした場合、モデルがおすすめした記事の約半数は実際に好みであった
- しかし、実際にいいねするブログの27%しか拾えていないため、取りこぼしが多い
しきい値を0.3まで下げると「いいね」と判定されるブログは80件(全334件の24%)に増える一方、再現率は72.7%まで上がります。適合率は50.0%とほとんど落ちません。つまり、モデルがお勧めする記事だけ目を通したとしても、興味ある記事の7割は拾えるということになります。
ランキングとしての評価
しきい値を設けて「いいね」かそうでないか判定する方式だと、どうしても「対象外になったが実は好み」のブログが落ちてしまいます。
そこで、ランキング形式でpredictionの値が上位のものから順に並べてみました。こうすることで、どこまで読むかは自分で決めることができます。
順位の良さを測る指標として、Recall@K(上位K件までの再現率)を使用します。この指標は、その日実際「いいね」した全体の件数のうち、上位K件までに何件を拾えたかを表します。
以下のランキングを例に計算してみます。全部で3件が当たりで、当たりが1位・2位・5位に出現します。
| 順位 | 結果 | 上位K件の当たり | Recall@K |
|---|---|---|---|
| 1 | ⭕️ | 1 | 1/3 = 0.333 |
| 2 | ⭕️ | 2 | 2/3 = 0.667 |
| 3 | — | 2 | 2/3 = 0.667 |
| 4 | — | 2 | 2/3 = 0.667 |
| 5 | ⭕️ | 3 | 3/3 = 1.000 |
Kを増やすと(当然)Recall@Kは上がります。下位の記事まで読めば「ハズレ」が含まれる割合が上がる一方、取りこぼしは減ります。Recall@Kは「何件読むか = どこで折り合いをつけるか」の判断の指標になります。
2週間分の結果
1日1回、前日投稿されたブログをランク付けする運用を想定して、日ごとのRecall@10を計算しました。K=10としたのは、10件くらいのブログだったら目を通すのが苦にならないからです。
結果は以下のようになりました。土日はブログの投稿が少なくいいねも少ないので無視すると、平均 Recall@10は0.796となります。日によって変動がありますが、毎日30件程度のブログの中からモデルが推奨する上位10件を読めば、好みのブログ全体の6〜8割を拾えるということになります。
| 日付 | ブログ件数 | いいね件数 | Recall@10 |
|---|---|---|---|
| 08-01(土) | 3 | 0 | — |
| 08-02(日) | 4 | 0 | — |
| 08-03(月) | 31 | 5 | 0.60 |
| 08-04(火) | 29 | 5 | 0.80 |
| 08-05(水) | 44 | 7 | 0.57 |
| 08-06(木) | 36 | 5 | 0.80 |
| 08-07(金) | 19 | 3 | 1.00 |
| 08-08(土) | 4 | 0 | — |
| 08-09(日) | 4 | 1 | — |
| 08-10(月) | 37 | 11 | 0.73 |
| 08-11(火) | 33 | 5 | 1.00 |
| 08-12(水) | 27 | 7 | 0.71 |
| 08-13(木) | 36 | 4 | 0.75 |
| 08-14(金) | 25 | 2 | 1.00 |
| 08-15(土) | 2 | 0 | — |
サンプルとして、8/3(月)の上位10件と当たり外れを以下の表に示します。
| 順位 | prediction | 結果 | タイトル |
|---|---|---|---|
| 1 | 0.621 | — | August Developer Challenge Week 1 – Joule |
| 2 | 0.408 | ⭕️ | CPIOps: Quality Checks and Git Integration for All Your iFlows |
| 3 | 0.377 | ⭕️ | Beyond Pretty Printer: Exploring ABAP Cleaner in VS Code and Eclipse |
| 4 | 0.348 | — | Enhanced Landscape and Cost Estimation in Migration Assessment |
| 5 | 0.320 | ⭕️ | FAQ 2026 on SAP Fiori elements, SAP Fiori tools, Fiori MCP, building apps, and the future of SAP UX |
| 6 | 0.270 | — | Sending HTML Notifications in SAP SRM Without SO10 — Using CL_BBP_DOC_SEND_BCS and OData |
| 7 | 0.270 | — | ABAP's agentic AI capabilities, commercialization and activation |
| 8 | 0.245 | — | From Blank Canvas to Branded Launchpad: Configuring SAP Build Work Zone, Advanced Edition |
| 9 | 0.223 | — | S/4HANA TM: Beware of Own Business System setting |
| 10 | 0.223 | — | SAP and Shopify Integration – Part 1: Building a Scalable B2C Commerce Foundation |
predictionの値が低く、取りこぼした2件の順位は以下でした。27位は、普段この分野のブログは読まないので仕方ないなと思います。
| 順位 | prediction | 「いいねしたか | タイトル |
|---|---|---|---|
| 14 | 0.085 | ⭕️ | How We Brought Transparency to Our AI Activities: A Practical Approach to Tracking and Coordinating |
| 27 | 0.029 | ⭕️ | SAP PP vs. SAP PP/DS: A Complete Planning Capability Comparison |
普段の読み方との比較
普段はAll SAP Community Blogsを遡って一番古いまだ読んでいないブログから順に見ています。この読み方とRPTを使った場合とでRecall@10を比較した結果が以下です。10日間すべてでRPTが上回り、平均で約2.6倍でした。
| 日付 | ブログ件数 | いいね件数 | 古い順 | RPT |
|---|---|---|---|---|
| 08-03(月) | 31 | 5 | 0.40 | 0.60 |
| 08-04(火) | 29 | 5 | 0.40 | 0.80 |
| 08-05(水) | 44 | 7 | 0.29 | 0.57 |
| 08-06(木) | 36 | 5 | 0.00 | 0.80 |
| 08-07(金) | 19 | 3 | 0.67 | 1.00 |
| 08-10(月) | 37 | 11 | 0.55 | 0.73 |
| 08-11(火) | 33 | 5 | 0.00 | 1.00 |
| 08-12(水) | 27 | 7 | 0.57 | 0.71 |
| 08-13(木) | 36 | 4 | 0.25 | 0.75 |
| 08-14(金) | 25 | 2 | 0.00 | 1.00 |
| 平均 | 0.312 | 0.796 |
予測に影響を与えた要素
APIレスポンスのexplanationsには、予測対象の行ごとに、予測に影響した列とコンテキスト行が設定されています。ここから8/3(月)分の予測に影響を与えた要素を見てみます。
"explanations": {
"top_column_scores": [
{ "author_login": 0.217, "board_id": 0.327, "subject": 0.456 },
{ "author_login": 0.223, "board_id": 0.338, "subject": 0.439 }
/* … 残り 332 件(query行1件につき1個) … */
],
"top_relevant_context_rows": [
[382, 128, 721],
[382, 128, 574]
/* … 残り 332 件 … */
]
},
top_column_scores(予測に影響したカラム)
上位10件について、各カラムのスコアを表示しています。8/3(月)の全31件の平均は以下のようになり、subject > board_id > author_login という順です。
| 順位 | 確率 | 結果 | subject | board_id | author_login | タイトル |
|---|---|---|---|---|---|---|
| 1 | 0.621 | — | 0.433 | 0.328 | 0.239 | August Developer Challenge Week 1 – Joule |
| 2 | 0.408 | ⭕️ | 0.467 | 0.328 | 0.205 | CPIOps: Quality Checks and Git Integration for All Your iFlows |
| 3 | 0.377 | ⭕️ | 0.402 | 0.383 | 0.215 | Beyond Pretty Printer: Exploring ABAP Cleaner in VS Code and Eclipse |
| 4 | 0.348 | — | 0.373 | 0.389 | 0.238 | Enhanced Landscape and Cost Estimation in Migration Assessment |
| 5 | 0.320 | ⭕️ | 0.444 | 0.310 | 0.245 | FAQ 2026 on SAP Fiori elements, SAP Fiori tools, Fiori MCP... |
| 6 | 0.270 | — | 0.398 | 0.360 | 0.242 | Sending HTML Notifications in SAP SRM Without SO10 |
| 7 | 0.270 | — | 0.405 | 0.290 | 0.305 | ABAP's agentic AI capabilities, commercialization and activation |
| 8 | 0.245 | — | 0.415 | 0.331 | 0.254 | From Blank Canvas to Branded Launchpad: SAP Build Work Zone |
| 9 | 0.223 | — | 0.377 | 0.307 | 0.315 | S/4HANA TM: Beware of Own Business System setting |
| 10 | 0.223 | — | 0.397 | 0.340 | 0.263 | SAP and Shopify Integration – Part 1 |
top_relevant_context_rows(予測の参考にしたコンテキスト行)
上位3件について、RPTが予測の参考にしたコンテキスト行を表示しています。全てに共通しているのは、board_idが参考記事と同一であるということです。しかし不思議なことに、board_idは予測に影響したカラムとしては2番手で、subjectが一番手です。
1位は予測対象のブログのタイトルと参考にしたブログのタイトルに関連がみられます("August Developer Challenge"、"Joule")。3位についても、参考1のブログには"ABAP"というキーワードが入っています。しかし、2位のブログには予測対象のブログと参考ブログのタイトルに全く関連がありません。
1位(ハズレ)prediction 0.621
August Developer Challenge Week 1 – Joule
subject 0.433 / board_id 0.328 / author_login 0.239
| いいね | タイトル | ボード | 投稿者 | |
|---|---|---|---|---|
| 予測対象 | — | August Developer Challenge Week 1 – Joule | aiblog-board | Rekha_DR |
| 参考1 | ⭕️ | August Developer Challenge – New Joule Studio | aiblog-board | Dan_Wroblewski |
| 参考2 | ⭕️ | SAP-RPT-1.5 now live on generative AI hub | aiblog-board | sherene_tan |
| 参考3 | ⭕️ | Bringing Domain Intelligence into SAP Joule | aiblog-board | cassiobinkowski |
2位(当たり)prediction 0.408
CPIOps: Quality Checks and Git Integration for All Your iFlows
subject 0.467 / board_id 0.328 / author_login 0.205
| いいね | タイトル | ボード | 投稿者 | |
|---|---|---|---|---|
| 予測対象 | ⭕️ | CPIOps: Quality Checks and Git Integration for All Your iFlows | technology-blog-members | Pavan_SJ |
| 参考1 | ⭕️ | Federating, Replicating, and Caching Remote Data in CAP — Part 2 | technology-blog-members | mike_zaschka |
| 参考2 | ⭕️ | Federating, Replicating, and Caching Remote Data in CAP — Part 1 | technology-blog-members | mike_zaschka |
| 参考3 | ⭕️ | RAP Augmentation in Managed Scenario – Enriching Transactional Requests Before BO Processing | technology-blog-members | Abhi_0118 |
3位(当たり)prediction 0.377
Beyond Pretty Printer: Exploring ABAP Cleaner in VS Code and Eclipse
subject 0.402 / board_id 0.383 / author_login 0.215
| いいね | タイトル | ボード | 投稿者 | |
|---|---|---|---|---|
| 予測対象 | ⭕️ | Beyond Pretty Printer: Exploring ABAP Cleaner in VS Code and Eclipse | abapblog-board | VikashChandraSingh |
| 参考1 | ⭕️ | How to Scan Custom ABAP for Security Issues in Eclipse (Step-by-Step) | abapblog-board | vahagn |
| 参考2 | ⭕️ | How to rename an OData V4 service? | abapblog-board | Andre_Fischer |
| 参考3 | ⭕️ | The paging catastrophe | abapblog-board | josealba |
ボード名だけで予測しているわけではない
1〜3位のブログは、予測対象のブログと参考ブログのボード名が完全に一致していました。これだけを見ると「ボード名だけで予測しているのでは?」という疑問が出てきますが、board_idのみを使って予測した場合の平均Recall@10は0.525に下がることから、他の項目も予測に効いていることが裏付けられました。
| 送った列 | Recall@10 |
|---|---|
| subject + board_id + author_login | 0.796 |
| subject + author_login | 0.700 |
| board_id | 0.525 |
おまけ:n8nワークフローも作った
8月のDeveloper Challengeで初めて触ったn8nを使い、日次でブログを抽出しランクづけしてメールを送るワークフローを作成しました。実際のところ、Claudeで定義(ファイル)を作成してアップロードしただけです。

