task-frequency-design — curumi-ops

タスク生成頻度の設計方針

背景

現在のタスク自動生成システムは、全商材に対して一律に毎日タスクを生成している。しかし広告運用の現場では、データ量が少ない商材に対して毎日アラートを出しても統計的に意味のある判断ができない。予算の大小ではなく、データボリューム(クリック数・インプレッション数)の統計的有意性に基づいてタスク生成頻度を制御すべきである。


現行システム

実行スケジュール

Vercel Cron で毎日 22:00 UTC(翌日 JST 7:00)に実行される。

// apps/ops/vercel.json
{
  "crons": [
    { "path": "/api/cron/sync-ads", "schedule": "0 20 * * *" },
    { "path": "/api/cron/generate-tasks", "schedule": "0 22 * * *" }
  ]
}

広告データ同期(20:00 UTC)の完了後にタスク生成(22:00 UTC)が実行される順序。

タスク生成フロー

  1. generate-daily-tasks.ts がアクティブなルールを持つ全商材を取得
  2. 商材ごとに evaluateRulesForProduct() を呼び出し
  3. 直近 14 日間のトレンドデータから指標を計算(直近 7 日 vs 前 7 日)
  4. taskRules テーブルの各ルールに対して条件を評価
  5. 条件を満たした場合、dailyTasks テーブルにタスクを生成

ルール評価エンジン (evaluate-rules.ts)

評価対象の指標:

指標 算出方法 ルール例
cpa 直近 7 日の cost / conversions gt 目標 CPA 超過
ctr_change 直近 7 日 vs 前 7 日の CTR 変化率 change_percent 20%以上変動
clicks 直近 7 日の合計クリック数 lt クリック激減
frequency Meta 広告のフリクエンシー加重平均 gt フリクエンシー過多
quality_score Google Ads キーワード品質スコア平均 lt 品質低下
cpm_change 直近 7 日 vs 前 7 日の CPM 変化率 change_percent CPM 高騰

ルール演算子: gt(超過), lt(未満), change_percent(変化率), periodic_days(定期実行)

重複防止: 同日・同商材・同メトリクスのタスクはスキップ。

商材テーブルの関連フィールド (products)

targetCpa: integer('target_cpa'),                                    // 目標 CPA(円)
targetBudget: integer('target_budget'),                              // 目標予算(円)
targetConversions: integer('target_conversions'),                    // 目標 CV 数
alertThresholdPercent: integer('alert_threshold_percent').default(20), // アラート閾値(%)

現在の問題

現行システムは データ母数を考慮せず 閾値超過のみで判定している。日次クリック数が 5 件の商材でも、CTR が 20% 変動したらアラートを出してしまう。これは統計的に無意味なノイズである。


核心的な洞察: データボリュームと統計的有意性

判断基準は「予算額」ではなく「データ量」

月額予算 100 万円の商材でも、CPC が高ければ日次クリック数は少ない。逆に月額 30 万円でも CPC が低ければクリック数は多い。タスク生成頻度を決めるべきは予算額ではなく、実際に蓄積されるデータの量である。

クリック数に基づく頻度設計

日次クリック数に応じて、タスク生成頻度を 3 段階に分類する。

日次クリック数 頻度 根拠
100 clicks/日 超 毎日 CTR・CPC の日次変動が統計的に有意。異常検知が即座に意味を持つ
20-100 clicks/日 2-3 日ごと 1 日のデータだけでは偶然変動と区別できない。2-3 日分を蓄積すれば傾向が見える
20 clicks/日 未満 週次 日次・数日単位では標本数が不足。週単位でないとトレンド判定が不安定

CV(コンバージョン)の統計的有意性

CPA の変動を検知する場合、CV 数の母数が特に重要になる。

期間 CV 数 CPA 判断の信頼度 推奨アクション
50 CV 以上 高い。日次の CPA 変動にも意味がある 日次 CPA アラート有効
30-50 CV 中程度。3-7 日単位なら傾向が見える 閾値を緩めて(例: 20% → 40% 変動)判定
10-30 CV 低い。週単位で初めて傾向が分かる 週次レポートでのみ CPA を評価
10 CV 未満 非常に低い。偶然変動が支配的 CPA タスクを生成しない。クリック・インプレッションベースの指標に限定

データ量が少ない場合にスキップすべきタスク種別

スキップ対象

タスクカテゴリ スキップ条件 理由
tcpa_adjustment 週次 CV < 20 CPA の信頼区間が広すぎて tCPA 調整の根拠にならない
targeting (CTR ベース) 日次 clicks < 20 CTR の変動が標本誤差に埋もれる
budget (CPA ベース) 月次 CV < 30 CPA が安定しないため予算配分の判断ができない
adset_split 週次 CV < 10(広告セット単位) 分割テストの有意差が出ない
frequency 週次 reach < 1,000 フリクエンシーの加重平均が不安定
cpm_change 週次 impressions < 1,000 CPM の変化率が標本誤差に埋もれる

スキップしない対象

以下は低データ量でも生成を維持する。

タスクカテゴリ 理由
creative(定期更新) periodic_days ルールはデータ量に依存しない
budget(消化率ベース) 予算消化率は CV 数に依存せず、cost と budget の比較で判断可能
knowledge_suggestion ナレッジ提案はデータ量に関係なく価値がある
Quality Score 低下アラート Google Ads の品質スコアは独自の指標で、クリック数の多寡に依存しない

将来の実装計画

Phase 1: ルール定義に minDataVolume 条件を追加

RuleDefinition インタフェースにオプショナルな minDataVolume を追加する。

// apps/ops/src/lib/db/schema.ts - RuleDefinition の拡張
export interface RuleDefinition {
  metric: string;
  operator: RuleOperator;
  threshold: number;
  category: TaskCategory;
  taskTemplate: string;
  description: string;
  // 新規追加
  minDataVolume?: {
    metric: 'clicks' | 'conversions' | 'impressions';
    period: 'daily' | 'weekly';  // daily = recent7 の日平均, weekly = recent7 の合計
    minValue: number;
  };
}

taskRules テーブルへのカラム追加:

// taskRules テーブルに追加
minDataMetric: text('min_data_metric'),   // 'clicks' | 'conversions' | 'impressions'
minDataPeriod: text('min_data_period'),   // 'daily' | 'weekly'
minDataValue: integer('min_data_value'),  // 最小値

Phase 2: evaluateRulesForProduct() にデータ量チェックを追加

ルール評価の冒頭で、現在の metricValues からデータ量を確認し、minDataVolume 条件を満たさないルールをスキップする。

// evaluate-rules.ts のルールループ内に追加するロジック
for (const rule of rules) {
  // ... 既存の重複チェック ...

  // データ量チェック(新規追加)
  if (rule.minDataMetric && rule.minDataValue) {
    const volumeKey = rule.minDataMetric as keyof MetricValues;
    const currentVolume = metricValues[volumeKey];

    // period が 'daily' の場合は 7 日平均、'weekly' の場合はそのまま
    const effectiveVolume = rule.minDataPeriod === 'daily'
      ? (currentVolume ?? 0) / 7
      : (currentVolume ?? 0);

    if (effectiveVolume < rule.minDataValue) {
      // データ量不足のためスキップ(ログ出力で可視化)
      continue;
    }
  }

  // ... 既存の条件評価 ...
}

Phase 3: テンプレートにデフォルト値を設定

taskRuleTemplates のシステムテンプレートに、推奨される minDataVolume をプリセットする。

// Google Ads 獲得型テンプレートの例
const googleAdsAcquisitionRules: RuleDefinition[] = [
  {
    metric: 'cpa',
    operator: 'gt',
    threshold: 120,
    category: 'tcpa_adjustment',
    taskTemplate: 'CPA が目標の {threshold}% を超過しています',
    description: 'CPA 高騰アラート',
    minDataVolume: {
      metric: 'conversions',
      period: 'weekly',
      minValue: 20,
    },
  },
  {
    metric: 'ctr_change',
    operator: 'change_percent',
    threshold: 20,
    category: 'targeting',
    taskTemplate: 'CTR が {threshold}% 以上変動しています',
    description: 'CTR 変動アラート',
    minDataVolume: {
      metric: 'clicks',
      period: 'daily',
      minValue: 50,
    },
  },
];

実装上の注意点

  1. 既存ルールとの互換性: minDataVolume はオプショナルフィールドとし、未設定の場合は従来通り無条件で評価する
  2. period の計算: dailyrecent7 の日平均、weeklyrecent7 の合計値を使う。computeMetrics() 関数が既に 7 日分のデータを集計しているため、追加の DB クエリは不要
  3. ログ出力: スキップされたルールはログに記録し、「なぜタスクが生成されなかったか」を運用者が確認できるようにする
  4. 段階的な導入: まず CPA 系ルールにのみ minDataVolume を適用し、効果を確認してから他の指標に展開する

参照ファイル

ファイル 役割
apps/ops/src/lib/tasks/evaluate-rules.ts ルール評価エンジン。MetricValues 計算、条件評価、タスク生成
apps/ops/src/lib/tasks/generate-daily-tasks.ts 日次バッチ。アクティブルールを持つ商材を列挙して評価
apps/ops/src/lib/db/schema.ts products テーブル(targetCpa, targetBudget, alertThresholdPercent)、taskRules テーブル(metric, operator, threshold, ruleType)
apps/ops/vercel.json Vercel Cron 定義(sync-ads: 20:00 UTC、generate-tasks: 22:00 UTC)
apps/ops/src/app/api/cron/generate-tasks/route.ts Cron エンドポイント(認証付き GET)