サブスクリプション型ビジネスの成功は、顧客の継続率(リテンションレート)に大きく依存します。私は以前、月間解約率が15%に達して事業危機に瀕したSaaS企業で、この課題に真正面から向き合いました。本稿では、HolySheep AIを活用したAI駆動型の顧客流失予測モデル構築手順を、コード例と実際のエラーハンドリングを交えて詳細に解説します。

問題背景と本稿の構成

サブスクリプション型ビジネスにおける顧客流失(チャーン)は、以下の式で表される生涯価値(LTV)に直結します:

LTV = ARPU × 利益率 / 月次解約率

月次解約率が5%から7%に上昇するだけで、LTVは約30%低下します。私はこの計算を見た瞬間に、予測モデルの必要性を経営陣に提言しました。本ガイドでは以下の構成で説明します:

環境構築と前提条件

まず、必要なPythonライブラリをインストールします。HolySheep AIのAPIはOpenAI互換のエンドポイントを提供しているため、既存のLangChainワークフローと高い互換性があります。

# requirements.txt
openai>=1.12.0
pandas>=2.1.0
numpy>=1.26.0
scikit-learn>=1.4.0
python-dateutil>=2.8.2
httpx>=0.27.0

インストール

pip install -r requirements.txt

HolySheep AI API接続の実装

HolySheep AIでは、レートが¥1=$1(公式¥7.3=$1比85%節約)で、WebMoney/銀行振込/暗号資産払いに加えてWeChat Pay/Alipayにも対応しています。<50msの低レイテンシも魅力的で、本番環境でのリアルタイムスコアリングに適しています。

# holy_sheep_client.py
import os
from openai import OpenAI
import httpx
from typing import List, Dict, Any
from dataclasses import dataclass
from datetime import datetime

@dataclass
class ChurnPredictionResult:
    customer_id: str
    churn_risk_score: float  # 0.0 - 1.0
    risk_level: str  # "low", "medium", "high", "critical"
    factors: List[Dict[str, Any]]
    recommended_actions: List[str]

class HolySheepChurnPredictor:
    """
    HolySheep AIを活用した顧客流失予測クライアント
    2026年Output価格参考:
    - GPT-4.1: $8/MTok
    - Claude Sonnet 4.5: $15/MTok  
    - DeepSeek V3.2: $0.42/MTok
    """
    
    BASE_URL = "https://api.holysheep.ai/v1"
    
    def __init__(self, api_key: str = None):
        self.api_key = api_key or os.environ.get("HOLYSHEEP_API_KEY")
        if not self.api_key:
            raise ValueError("API key must be provided or set as HOLYSHEEP_API_KEY")
        
        self.client = OpenAI(
            api_key=self.api_key,
            base_url=self.BASE_URL,
            http_client=httpx.Client(timeout=30.0)
        )
    
    def analyze_churn_risk(
        self, 
        customer_data: Dict[str, Any],
        model: str = "gpt-4.1"
    ) -> ChurnPredictionResult:
        """
        顧客データから流失リスクを分析
        
        Args:
            customer_data: 顧客の詳細データ辞書
            model: 使用するモデル(デフォルト: gpt-4.1)
        
        Returns:
            ChurnPredictionResult: 予測結果
        """
        prompt = self._build_analysis_prompt(customer_data)
        
        try:
            response = self.client.chat.completions.create(
                model=model,
                messages=[
                    {
                        "role": "system", 
                        "content": "あなたは顧客維持のエキスパートです。提供された顧客データから流失リスクを評価し、スコア(0-1)、リスクレベル、要因分析、推奨アクションを出力してください。"
                    },
                    {"role": "user", "content": prompt}
                ],
                temperature=0.3,
                max_tokens=2000
            )
            
            result_text = response.choices[0].message.content
            return self._parse_prediction_result(result_text, customer_data.get("customer_id"))
            
        except Exception as e:
            raise ChurnAnalysisError(f"分析中にエラーが発生: {str(e)}") from e
    
    def batch_predict(
        self,
        customers: List[Dict[str, Any]],
        model: str = "gpt-4.1"
    ) -> List[ChurnPredictionResult]:
        """複数顧客の一括予測"""
        results = []
        for customer in customers:
            try:
                result = self.analyze_churn_risk(customer, model)
                results.append(result)
            except ChurnAnalysisError as e:
                print(f"顧客 {customer.get('customer_id')} の予測をスキップ: {e}")
                continue
        return results
    
    def _build_analysis_prompt(self, data: Dict[str, Any]) -> str:
        return f"""
以下の顧客データに基づいて流失リスクを分析してください:

【顧客ID】{data.get('customer_id')}
【契約開始日】{data.get('subscription_start')}
【直近ログイン日】{data.get('last_login_date')}
【月間アクティブ日数】{data.get('active_days_last_30d')}
【サポートチケット数】{data.get('support_tickets_30d')}
【平均セッション時間(分)】{data.get('avg_session_minutes')}
【プランレベル】{data.get('plan_tier')}
【月度請求額】¥{data.get('monthly_charge')}
【機能利用率】{data.get('feature_adoption_rate')}%
【NPSスコア】{data.get('nps_score')}

JSON形式で以下を出力してください:
{{
  "churn_risk_score": 0.0-1.0,
  "risk_level": "low|medium|high|critical",
  "risk_factors": ["要因1", "要因2"],
  "recommended_actions": ["アクション1", "アクション2"]
}}
"""

    def _parse_prediction_result(
        self, 
        response_text: str, 
        customer_id: str
    ) -> ChurnPredictionResult:
        import json
        import re
        
        json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
        if json_match:
            data = json.loads(json_match.group())
        else:
            data = {"churn_risk_score": 0.5, "risk_level": "medium", 
                    "risk_factors": ["解析失敗"], "recommended_actions": ["手動確認"]}
        
        return ChurnPredictionResult(
            customer_id=customer_id,
            churn_risk_score=data.get("churn_risk_score", 0.5),
            risk_level=data.get("risk_level", "medium"),
            factors=[{"factor": f, "weight": 0.5} for f in data.get("risk_factors", [])],
            recommended_actions=data.get("recommended_actions", [])
        )


class ChurnAnalysisError(Exception):
    """カスタム例外クラス"""
    pass

特徴量エンジニアリングとデータパイプライン

予測精度を最大化するには、適切な特徴量設計が重要です。私は過去のプロジェクトで、以下の特徴量設計でAUC-ROC 0.87を達成しました。

# feature_engineering.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import List, Dict, Tuple

class ChurnFeatureEngineering:
    """
    顧客流失予測用の特徴量エンジニアリング
    私が実際に使った特徴量カテゴリと設計思想を記載
    """
    
    def __init__(self):
        self.feature_names = []
    
    def create_features(
        self, 
        df: pd.DataFrame, 
        reference_date: datetime = None
    ) -> pd.DataFrame:
        """
        生データから予測用特徴量を生成
        
        Args:
            df: 生顧客データ DataFrame
            reference_date: 特徴量計算の基準日(デフォルト: 今日)
        """
        df = df.copy()
        ref_date = reference_date or datetime.now()
        
        # 1. エンゲージメント特徴量
        df = self._add_engagement_features(df, ref_date)
        
        # 2. 契約関連特徴量
        df = self._add_subscription_features(df, ref_date)
        
        # 3. 行動パターン特徴量
        df = self._add_behavioral_features(df)
        
        # 4. サポート関連特徴量
        df = self._add_support_features(df)
        
        # 5. 財務関連特徴量
        df = self._add_financial_features(df)
        
        return df
    
    def _add_engagement_features(self, df: pd.DataFrame, ref_date: datetime) -> pd.DataFrame:
        """エンゲージメントスコア特徴量"""
        
        # アクティビティ低下率
        df['activity_trend_7d'] = (
            df['active_days_last_7d'] / 
            df['active_days_last_30d'].replace(0, 1) * 4
        ).clip(0, 2)
        
        # セッション頻度の変化
        df['session_frequency_change'] = (
            df['sessions_last_7d'] / df['sessions_last_30d'].replace(0, 1) * 4
        ).clip(0, 2)
        
        # コア機能利用率
        df['core_feature_ratio'] = (
            df['core_features_used'] / 
            df['total_core_features'].replace(0, 1)
        )
        
        # エンゲージメント総合スコア
        df['engagement_score'] = (
            df['activity_trend_7d'] * 0.3 +
            df['session_frequency_change'] * 0.3 +
            df['core_feature_ratio'] * 0.4
        )
        
        return df
    
    def _add_subscription_features(
        self, 
        df: pd.DataFrame, 
        ref_date: datetime
    ) -> pd.DataFrame:
        """契約期間と運命に関連する特徴量"""
        
        # 契約からの経過月数
        df['subscription_months'] = (
            ref_date - pd.to_datetime(df['subscription_start'])
        ).dt.days / 30
        
        # 運命の危ない期間フラグ(契約後6-8ヶ月)
        df['is_critical_period'] = (
            (df['subscription_months'] >= 6) & 
            (df['subscription_months'] <= 8)
        ).astype(int)
        
        # アップグレード/ダウングレードの履歴
        df['plan_change_count'] = df.get('plan_changes', 0)
        df['downgraded'] = (df['plan_change_count'] < 0).astype(int)
        
        # 年間契約优惠利用率
        df['annual_discount_used'] = df.get('annual_contract_discount', 0)
        
        return df
    
    def _add_behavioral_features(self, df: pd.DataFrame) -> pd.DataFrame:
        """行動パターン特徴量"""
        
        # 機能探索深度
        df['feature_exploration_depth'] = (
            df['unique_features_used'] / 
            df['total_available_features'].replace(0, 1)
        )
        
        # コラボ利用度
        df['collaboration_score'] = (
            df['shared_items'] / df['total_items'].replace(0, 1)
        )
        
        # チームサイズとの比率
        df['user_per_seat_ratio'] = (
            df['active_users'] / df['total_seats'].replace(0, 1)
        )
        
        return df
    
    def _add_support_features(self, df: pd.DataFrame) -> pd.DataFrame:
        """サポートチケット特徴量"""
        
        # チケット頻度
        df['ticket_rate_30d'] = (
            df['support_tickets_30d'] / 30
        )
        
        # 解決待ちチケット
        df['unresolved_tickets'] = df.get('open_tickets', 0)
        
        # 苦情レベルスコア
        df['frustration_score'] = (
            df['support_tickets_30d'] * 0.5 +
            df['unresolved_tickets'] * 2.0 +
            df.get('escalations', 0) * 3.0
        )
        
        return df
    
    def _add_financial_features(self, df: pd.DataFrame) -> pd.DataFrame:
        """財務・請求関連特徴量"""
        
        # 請求失敗履歴
        df['payment_failure_count'] = df.get('failed_payments_90d', 0)
        df['has_payment_issue'] = (df['payment_failure_count'] > 0).astype(int)
        
        # 課金額に対する利用率
        df['value_per_yen'] = (
            df['active_days_last_30d'] / 
            df['monthly_charge'].replace(0, 1)
        )
        
        # 請求増加に対する反応
        df['price_sensitivity'] = (
            (df['monthly_charge'] > df['avg_charge_last_6m']) &
            (df['engagement_score'] < 0.5)
        ).astype(int)
        
        return df
    
    def prepare_training_data(
        self, 
        df: pd.DataFrame, 
        target_col: str = 'churned_next_30d'
    ) -> Tuple[pd.DataFrame, pd.Series]:
        """
        機械学習モデル用の訓練データ準備
        
        Returns:
            X: 特徴量DataFrame
            y: ターゲットSeries
        """
        exclude_cols = [
            'customer_id', 'subscription_start', 'last_login_date',
            'email', 'company_name', target_col
        ]
        
        feature_cols = [c for c in df.columns if c not in exclude_cols]
        self.feature_names = feature_cols
        
        X = df[feature_cols].fillna(0)
        y = df[target_col].astype(int)
        
        return X, y

早期警戒システムのデプロイ

私が以前担当したプロジェクトでは、この予測モデルを週間バッチ処理で実行し、リスクスコアが0.7以上の顧客に対して自動的にCustomer SuccessチームにSlack通知を送るシステムを構築しました。

# early_warning_system.py
import asyncio
from datetime import datetime, timedelta
from typing import List, Optional
import logging

from holy_sheep_client import HolySheepChurnPredictor, ChurnAnalysisError
from feature_engineering import ChurnFeatureEngineering

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class ChurnEarlyWarningSystem:
    """
    顧客流失早期警戒システム
    リスクレベルに応じた介入アクションを自動実行
    """
    
    RISK_THRESHOLDS = {
        'critical': 0.85,  # 即座の対応が必要
        'high': 0.70,       # 1週間以内の介入
        'medium': 0.50,    # 監視強化
        'low': 0.0         # 通常運用
    }
    
    def __init__(self, api_key: str):
        self.predictor = HolySheepChurnPredictor(api_key=api_key)
        self.feature_engine = ChurnFeatureEngineering()
        self._action_queue: List[dict] = []
    
    async def run_weekly_scan(
        self, 
        customer_df,
        notification_handler: Optional[callable] = None
    ) -> dict:
        """
        週次スキャン実行
        
        Args:
            customer_df: 顧客データDataFrame
            notification_handler: 通知コールバック関数
        
        Returns:
            スキャン結果サマリー
        """
        logger.info(f"週次スキャン開始: {datetime.now()}")
        
        # 特徴量生成
        feature_df = self.feature_engine.create_features(customer_df)
        
        # HolySheep AIで一括予測
        customers = self._df_to_dict_list(feature_df)
        predictions = await self._async_batch_predict(customers)
        
        # リスク分類
        risk_groups = self._categorize_by_risk(predictions)
        
        # アクショナブル介入タスク生成
        action_tasks = self._generate_action_tasks(risk_groups)
        
        # 通知送信
        if notification_handler:
            await notification_handler(action_tasks)
        
        # 結果サマリー
        summary = {
            'scan_time': datetime.now().isoformat(),
            'total_customers': len(predictions),
            'risk_distribution': {
                level: len(customers) 
                for level, customers in risk_groups.items()
            },
            'action_required': len(action_tasks)
        }
        
        logger.info(f"スキャン完了: {summary}")
        return summary
    
    async def _async_batch_predict(
        self, 
        customers: List[dict]
    ) -> List[dict]:
        """
        非同期一括予測(httpx.AsyncClient使用)
        """
        results = []
        semaphore = asyncio.Semaphore(10)  # 同時実行数制限
        
        async def predict_one(customer: dict):
            async with semaphore:
                try:
                    result = await asyncio.to_thread(
                        self.predictor.analyze_churn_risk,
                        customer
                    )
                    return {
                        'customer_id': result.customer_id,
                        'risk_score': result.churn_risk_score,
                        'risk_level': result.risk_level,
                        'factors': result.factors,
                        'actions': result.recommended_actions
                    }
                except ChurnAnalysisError as e:
                    logger.error(f"予測失敗 - {customer.get('customer_id')}: {e}")
                    return None
        
        tasks = [predict_one(c) for c in customers]
        results = await asyncio.gather(*tasks)
        
        return [r for r in results if r is not None]
    
    def _categorize_by_risk(
        self, 
        predictions: List[dict]
    ) -> dict:
        """予測結果のリスク分類"""
        groups = {level: [] for level in self.RISK_THRESHOLDS.keys()}
        
        for pred in predictions:
            score = pred['risk_score']
            if score >= self.RISK_THRESHOLDS['critical']:
                groups['critical'].append(pred)
            elif score >= self.RISK_THRESHOLDS['high']:
                groups['high'].append(pred)
            elif score >= self.RISK_THRESHOLDS['medium']:
                groups['medium'].append(pred)
            else:
                groups['low'].append(pred)
        
        return groups
    
    def _generate_action_tasks(
        self, 
        risk_groups: dict
    ) -> List[dict]:
        """介入タスク自動生成"""
        tasks = []
        
        #  критическийリスク:CEO/CPOへのエスカレーション
        for pred in risk_groups['critical']:
            tasks.append({
                'priority': 'URGENT',
                'customer_id': pred['customer_id'],
                'action': 'executive_outreach',
                'message': f"[危篤] 顧客 {pred['customer_id']} "
                          f"リスクスコア: {pred['risk_score']:.2f}",
                'factors': pred['factors'],
                'due_days': 1
            })
        
        # 高リスク:CSマネージャー介入
        for pred in risk_groups['high']:
            tasks.append({
                'priority': 'HIGH',
                'customer_id': pred['customer_id'],
                'action': 'personal_check_in',
                'message': f"[高リスク] 顧客 {pred['customer_id']} "
                          f"リスクスコア: {pred['risk_score']:.2f}",
                'recommended_actions': pred['actions'],
                'due_days': 7
            })
        
        return tasks
    
    def _df_to_dict_list(self, df) -> List[dict]:
        return df.to_dict('records')


使用例

async def slack_notification(tasks: List[dict]): """Slack通知ハンドラ例""" import os # 本番ではWebhook URLを設定 webhook_url = os.environ.get('SLACK_WEBHOOK_URL') if not tasks: logger.info("介入タスクなし") return for task in tasks: priority_emoji = { 'URGENT': '🚨', 'HIGH': '⚠️', 'MEDIUM': '📋', 'LOW': 'ℹ️' }.get(task['priority'], '📌') message = f"{priority_emoji} *{task['priority']}*\n" message += f"顧客ID: {task['customer_id']}\n" message += f"アクション: {task['action']}\n" message += f"期限: {task['due_days']}日以内" logger.info(f"通知送信: {message}") # await send_to_slack(webhook_url, message)

メイン実行

if __name__ == "__main__": import pandas as pd # デモデータ demo_data = pd.DataFrame({ 'customer_id': ['C001', 'C002', 'C003'], 'subscription_start': [ datetime.now() - timedelta(days=200), datetime.now() - timedelta(days=90), datetime.now() - timedelta(days=400) ], 'active_days_last_30d': [5, 20,