モニタリングシステムのコストレポートを確認していたときのことです。LINEの予測コストが140,000円と表示されていました。でも待てよ、LINEの月額基本料金は15,000円のはずです。なぜ9倍以上の予測値になっているのでしょうか。
この違和感から始まった調査で、予測ロジックの設計に根本的な問題があることがわかりました。そして、その問題を解決しようとする過程で、さらに別の問題が見つかりました。
問題の発見:140,000円という違和感
コストモニタリングシステムが出力したレポートには、こんなデータが含まれていました。
{
"service": "LINE",
"current_cost": 15000.0,
"forecast": 140000.0,
"period_start": "2026-02-01",
"period_end": "2026-02-03",
"message_count": 220,
"monthly_fee": 15000.0,
"message_cost": 0.0
}
3日間で220通のメッセージを送信し、現在のコストは15,000円。ここまではいいのですが、月末予測が140,000円になっています。
LINEのスタンダードプランは月額15,000円で、30,000通までは無料です。220通なら確実に無料枠内に収まるはずなので、月末の着地も15,000円前後になるはずです。なぜこんな予測値になっているのでしょうか。
第一の調査:現状のロジックを読み解く
monitoring/cost_monitor.py の実装を確認してみました。コスト予測は、標準的な日割り計算を使っていました。
forecast = (current_cost / days_passed) * days_in_month
この式に実際の値を当てはめると:
forecast = (15000 / 3) * 28
forecast = 5000 * 28
forecast = 140000
謎が解けました。このロジックは「毎日5,000円かかっている」という前提で予測を立てていたのです。しかし、15,000円は月額固定費であり、月初に1回だけ課金されるものです。日割りで計算してはいけない性質のコストでした。
第二の調査:もっと直接的な方法はないか?
この問題を解決するにあたって、まずLINE Messaging APIのドキュメントを確認しました。金額を直接取得できるエンドポイントがあれば、予測計算そのものが不要になるかもしれません。
調査の結果、以下のことがわかりました:
- 金額を直接返すAPIは存在しない
- メッセージ配信数を取得できるエンドポイントはある
/v2/bot/message/quota/consumption(月間合計)/v2/bot/insight/message/delivery(日別)
現在の実装は日別APIを複数回呼んでいましたが、月間合計のエンドポイントを使えばAPI呼び出し回数を削減できることがわかりました。ただし、これは効率化にはなっても、予測ロジックの問題を解決するものではありません。
メッセージ数から料金を計算するしかない、という制約の中で設計を考える必要がありました。
[📦 商品リンク: moshimo-card-TUBHo]
第一の問題:段階的料金体系の複雑さ
LINEの料金ページを確認したところ、予想以上に複雑な料金体系であることがわかりました。
最初は「4段階くらいだろう」と思っていたのですが、実際には15段階の従量課金制でした。
| 追加メッセージ配信数 | 単価 |
|---|---|
| 〜50,000通 | 3.0円 |
| 50,001〜100,000通 | 2.8円 |
| 100,001〜200,000通 | 2.6円 |
| 200,001〜300,000通 | 2.4円 |
| ... | ... |
| 7,000,001〜10,000,000通 | 1.1円 |
しかも、現在の実装は固定単価3.0円で計算していました。大量配信のケースでどれくらいの誤差が出るか計算してみます。
150万通配信の場合:
現在の実装(固定単価3.0円):
1,500,000通 × 3.0円 = 4,500,000円
正しい計算(15段階の料金体系):
月額基本料: 15,000円
無料分: 30,000通 → 0円
課金対象: 1,470,000通
・50,000通 × 3.0円 = 150,000円
・50,000通 × 2.8円 = 140,000円
・100,000通 × 2.6円 = 260,000円
... (中略) ...
・470,000通 × 1.4円 = 658,000円
合計: 2,733,000円
差額:約177万円(40%の過大評価)
この段階的料金体系への対応が必要だと気づき、課題管理ツールにIssueを作成しました。
第二の問題:予測ロジックの根本的誤り
段階的料金への対応を考えていたとき、さらに根本的な問題に気づきました。そもそも予測の仕方自体が間違っているのです。
最初のアプローチ:固定費と変動費を分離
当初、こんな実装を考えました:
# 固定費を除外して変動費だけ予測
monthly_fee = 15000
variable_cost = current_cost - monthly_fee
forecast_variable = (variable_cost / days_passed) * days_in_month
forecast = monthly_fee + forecast_variable
このアプローチなら、月額固定費を何度もカウントすることは避けられます。でも、これで本当に正しいのでしょうか。
正直ここで詰まりました。何かしっくりこない感覚がありました。
転換点:「いや、そうじゃなくて」
そこで方針を変えました。コストを予測するのではなく、メッセージ数を予測してから料金を計算する。
graph LR
A[現在のメッセージ数] --> B[メッセージ数を日割り予測]
B --> C[予測メッセージ数]
C --> D[段階的料金計算]
D --> E[予測コスト]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#e8f5e9
このアプローチが自然な理由は、LINEのビジネスモデルにあります:
- 課金の基準はメッセージ配信数
- 料金はメッセージ数の関数
- つまり
cost = f(message_count)という関係
だとすれば:
# 誤ったアプローチ
forecast_cost = f(current_cost, days) # コストからコストを予測
# 正しいアプローチ
forecast_message_count = g(current_message_count, days) # メッセージ数を予測
forecast_cost = f(forecast_message_count) # 予測メッセージ数から料金を計算
予測すべきはコストではなくメッセージ数だったのです。
正しい予測ロジックの設計
新しいロジックの実装イメージはこうなります:
def _get_cost_data(self, config, context):
# ① 現在のメッセージ数を取得
message_count = self._fetch_monthly_message_count(...)
# ② 現在のコストを計算(実績)
current_cost_result = self._calculate_line_cost(message_count, config)
current_cost = current_cost_result["total_cost"]
# ③ 予測を計算
if context["is_current_month"] and context["days_passed"] > 0:
# メッセージ数を予測
forecast_message_count = (
message_count / context["days_passed"]
) * context["days_in_month"]
# 予測メッセージ数から料金を計算
forecast_cost_result = self._calculate_line_cost(
int(forecast_message_count),
config
)
forecast = forecast_cost_result["total_cost"]
else:
forecast = current_cost
return CostData(...)
_calculate_line_cost() メソッドは、メッセージ数から段階的料金を計算します:
def _calculate_line_cost(self, message_count, config):
monthly_fee = float(config.get("monthly_fee", 15000))
free_messages = int(config.get("free_messages", 30000))
# 無料枠を超えた分を計算
billable_messages = max(0, message_count - free_messages)
# 段階的料金テーブル(15段階)
tiers = [
(50000, 3.0),
(100000, 2.8),
(200000, 2.6),
# ... 15段階すべて
]
# 段階的に料金を計算
message_cost = 0
remaining = billable_messages
prev_threshold = 0
for threshold, unit_price in tiers:
if remaining <= 0:
break
tier_messages = min(remaining, threshold - prev_threshold)
tier_cost = tier_messages * unit_price
message_cost += tier_cost
remaining -= tier_messages
prev_threshold = threshold
total_cost = monthly_fee + message_cost
return {
"total_cost": total_cost,
"monthly_fee": monthly_fee,
"message_cost": message_cost,
"billable_messages": billable_messages
}
計算例の比較
ケース1:無料枠内(実際のデータ)
3日で220通 → 月末予測:約2,053通
| 方式 | 予測コスト | 正確性 |
|---|---|---|
| 誤ったロジック(コスト日割り) | 140,000円 | ❌ 9倍以上の誤差 |
| 正しいロジック(メッセージ数予測) | 15,000円 | ✅ 無料枠内、月額のみ |
ケース2:有料段階に入る場合
3日で10,000通 → 月末予測:約93,333通
月額基本料: 15,000円
課金対象: 93,333 - 30,000 = 63,333通
・最初の50,000通: 50,000 × 3.0円 = 150,000円
・次の13,333通: 13,333 × 2.8円 = 37,332円
合計: 202,332円
このように、メッセージ数を予測してから料金を計算することで、段階的料金体系も正しく反映されます。
[📦 商品リンク: moshimo-card-EdE1y]
2つの問題の関係性
振り返ってみると、今回発見した2つの問題は別々のように見えて、根は同じでした。
- 段階的料金体系の問題:15段階の複雑な料金を固定単価で計算していた
- 予測ロジックの問題:コストを日割り予測していた
どちらもLINEの料金モデルを正しく理解していなかったことが原因です。
graph TB
A[LINEの料金モデル理解不足] --> B[固定単価3.0円で実装]
A --> C[コスト日割り予測]
B --> D[大量配信時に100万円超の誤差]
C --> E[月額固定費を日割りして9倍の誤差]
style A fill:#ffebee
style D fill:#fff3e0
style E fill:#fff3e0
問題を解決しようとして調査を進めるうち、別の問題が見つかる。このプロセス自体は、よくあることだと思います。重要なのは、データの違和感に気づき、立ち止まって考えることでした。
なぜこの問題が起きたか
日割り計算によるコスト予測は、一般的には正しいアプローチです。実際、同じシステム内のAWSコスト予測では直近4日間の平均を使った日割り計算が機能しています。
問題は、予測対象を間違えていたことです。
LINEの料金モデルは「固定費 + 従量課金」の構造を持っています:
cost = monthly_fee + f(message_count)
この構造では、コストを直接予測すると固定費の扱いが不自然になります。一方、メッセージ数を予測すれば、料金計算の関数 f() にそのまま渡すだけです。
**意外とここが肝です。**ビジネスモデルの構造を理解していれば、自然と正しい予測対象が見えてきます。
また、API調査で「金額を直接取得できない」という制約がわかったことも、設計の方向性を決める重要な情報でした。制約があるからこそ、その中で最適解を見つける必要があります。
まとめ:予測ロジック設計の原則
今回の経験から学んだことをまとめます:
ビジネスモデルを理解してから実装する
- 課金の基準は何か
- 固定費と変動費の構造
- 料金計算のロジック
予測対象を正しく選ぶ
- 何を予測すれば自然か
cost = f(x)のxは何か- 直接予測すべきか、間接的に計算すべきか
制約の中で最適解を見つける
- APIで取得できる情報は何か
- その情報から何が計算できるか
- トレードオフをどう判断するか
実際の数値で検証する
- 違和感を見逃さない
- 極端なケースで計算してみる
- 「おかしい」と感じたら立ち止まる
コスト予測という一見シンプルな機能でも、ビジネスモデルへの理解が浅いと根本的な間違いを犯してしまいます。今回の140,000円という予測値は、そのことを教えてくれた良い事例でした。
[📦 商品リンク: moshimo-book-monitoring-intro]