Bedrockを使って売上予測をしてみよう！

## はじめに
生成AIを使えばなんか色々出来るというイメージは皆さん持ってると思います。作り込めば大抵のことはいい感じに出来てしまえるので、選択肢が多くて逆に分からなくなるといったケースはままあります。

今回は、生成AIを活用した売上予測アプリケーションのサンプルを作ってみました。このアプリケーションは、過去の売上データを分析し、プロモーションの影響や天候の変化を考慮しながら、次週の売上を予測します。単なる数値の羅列ではなく、AIによる分析結果も提供することで、分析結果を元にした判断の助けになるようにしてみました。

## サンプルコード
### ファイル構成
```
- templates
    ┗index.html
- sample.py
- sales_data.csv
- requirements.txt
```

### index.html

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>売上予測</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            margin: 0;
            padding: 20px;
            background-color: #f4f4f4;
        }
        h1, h2, h3 {
            color: #333;
        }
        table {
            border-collapse: collapse;
            width: 100%;
            margin-bottom: 20px;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        th {
            background-color: #f2f2f2;
        }
        #trend-graph {
            max-width: 100%;
            height: auto;
        }
        .analysis {
            background-color: white;
            padding: 15px;
            border-radius: 5px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        .promotion-checkbox {
            margin-right: 10px;
        }
        #predict-button {
            margin-top: 10px;
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }
        #predict-button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <h1>売上予測</h1>
    <div id="promotion-selection">
        <h3>プロモーション日を選択:</h3>
        <!-- プロモーションのチェックボックスがここに動的に追加されます -->
    </div>
    <button id="predict-button">次の7日間の売上を予測</button>
    <div id="result"></div>
    <div id="analysis" class="analysis"></div>
    <img id="trend-graph" src="/static/sales_trend.png" alt="売上トレンド" style="display:none;">

    <script>
        $(document).ready(function() {
            const today = moment();
            for (let i = 1; i <= 7; i++) {
                const date = today.clone().add(i, 'days');
                const checkboxHtml = `<label class="promotion-checkbox"><input type="checkbox" class="promo-day" value="${date.format('YYYY-MM-DD')}"> ${date.format('YYYY-MM-DD (ddd)')}</label>`;
                $('#promotion-selection').append(checkboxHtml);
            }

            $('#predict-button').click(function() {
                const promotionDays = $('.promo-day:checked').map(function() {
                    return $(this).val();
                }).get();

                $.ajax({
                    url: '/',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify({ promotion_days: promotionDays }),
                    success: function(response) {
                        try {
                            const prediction = JSON.parse(response.prediction);
                            let tableHtml = '<h2>次の7日間の予測売上と天候:</h2><table><tr><th>日付</th><th>予測売上</th><th>予測天候</th><th>プロモーション</th></tr>';
                            prediction.forEach(function(item) {
                                tableHtml += `<tr><td>${item.date}</td><td>${item.sales.toLocaleString()}円</td><td>${item.weather}</td><td>${item.promotion ? 'あり' : 'なし'}</td></tr>`;
                            });
                            tableHtml += '</table>';
                            $('#result').html(tableHtml);
                            $('#analysis').html(response.analysis.replace(/\n/g, '<br>'));
                            $('#trend-graph').show();
                        } catch (error) {
                            console.error('Error parsing prediction:', error);
                            $('#result').html('<p>予測データの解析中にエラーが発生しました。</p>');
                        }
                    },
                    error: function(jqXHR, textStatus, errorThrown) {
                        console.error('AJAX request failed:', textStatus, errorThrown);
                        $('#result').html('<p>サーバーとの通信中にエラーが発生しました。</p>');
                    }
                });
            });
        });
    </script>
</body>
</html>
```

### sales_data.csv
```csvs
date,sales,promotion,weather,day_of_week
2024-05-01,36000,0,mild,Wednesday
2024-05-02,37500,1,mild,Thursday
2024-05-03,39500,1,mild,Friday
2024-05-04,41000,1,mild,Saturday
2024-05-05,35000,0,mild,Sunday
2024-05-06,36500,0,mild,Monday
2024-05-07,35500,0,rainy,Tuesday
2024-05-08,36000,0,mild,Wednesday
2024-05-09,37500,1,mild,Thursday
2024-05-10,39500,1,mild,Friday
2024-05-11,41000,1,mild,Saturday
2024-05-12,35000,0,mild,Sunday
2024-05-13,36500,0,mild,Monday
2024-05-14,35500,0,mild,Tuesday
2024-05-15,36000,0,mild,Wednesday
2024-05-16,37500,1,mild,Thursday
2024-05-17,39500,1,mild,Friday
2024-05-18,41000,1,mild,Saturday
2024-05-19,35000,0,rainy,Sunday
2024-05-20,36500,0,mild,Monday
2024-05-21,35500,0,mild,Tuesday
2024-05-22,36000,0,mild,Wednesday
2024-05-23,37500,1,mild,Thursday
2024-05-24,39500,1,mild,Friday
2024-05-25,41000,1,hot,Saturday
2024-05-26,35000,0,hot,Sunday
2024-05-27,36500,0,hot,Monday
2024-05-28,35500,0,hot,Tuesday
2024-05-29,36000,0,hot,Wednesday
2024-05-30,37500,1,hot,Thursday
2024-05-31,39500,1,hot,Friday
2024-06-01,41500,1,hot,Saturday
2024-06-02,35500,0,hot,Sunday
2024-06-03,37000,0,hot,Monday
2024-06-04,36000,0,rainy,Tuesday
2024-06-05,36500,0,hot,Wednesday
2024-06-06,38000,1,hot,Thursday
2024-06-07,40000,1,hot,Friday
2024-06-08,42000,1,hot,Saturday
2024-06-09,36000,0,hot,Sunday
2024-06-10,37500,0,hot,Monday
2024-06-11,36500,0,hot,Tuesday
2024-06-12,37000,0,hot,Wednesday
2024-06-13,38500,1,hot,Thursday
2024-06-14,40500,1,hot,Friday
2024-06-15,42500,1,hot,Saturday
2024-06-16,36500,0,rainy,Sunday
2024-06-17,38000,0,hot,Monday
2024-06-18,37000,0,hot,Tuesday
2024-06-19,37500,0,hot,Wednesday
2024-06-20,39000,1,hot,Thursday
2024-06-21,41000,1,hot,Friday
2024-06-22,43000,1,hot,Saturday
2024-06-23,37000,0,hot,Sunday
2024-06-24,38500,0,hot,Monday
2024-06-25,37500,0,hot,Tuesday
2024-06-26,38000,0,hot,Wednesday
2024-06-27,39500,1,hot,Thursday
2024-06-28,41500,1,hot,Friday
2024-06-29,43500,1,hot,Saturday
2024-06-30,37500,0,hot,Sunday
2024-07-01,39000,0,hot,Monday
2024-07-02,38000,0,rainy,Tuesday
2024-07-03,38500,0,hot,Wednesday
2024-07-04,40000,1,hot,Thursday
2024-07-05,42000,1,hot,Friday
2024-07-06,44000,1,hot,Saturday
2024-07-07,38000,0,hot,Sunday
2024-07-08,39500,0,hot,Monday
2024-07-09,38500,0,hot,Tuesday
2024-07-10,39000,0,hot,Wednesday
2024-07-11,40500,1,hot,Thursday
2024-07-12,42500,1,hot,Friday
2024-07-13,44500,1,hot,Saturday
2024-07-14,38500,0,hot,Sunday
2024-07-15,40000,0,hot,Monday
2024-07-16,39000,0,rainy,Tuesday
2024-07-17,39500,0,hot,Wednesday
2024-07-18,41000,1,hot,Thursday
2024-07-19,43000,1,hot,Friday
2024-07-20,45000,1,hot,Saturday
2024-07-21,39000,0,hot,Sunday
2024-07-22,40500,0,hot,Monday
2024-07-23,39500,0,hot,Tuesday
2024-07-24,40000,0,hot,Wednesday
2024-07-25,41500,1,hot,Thursday
2024-07-26,43500,1,hot,Friday
2024-07-27,45500,1,hot,Saturday
2024-07-28,39500,0,rainy,Sunday
2024-07-29,41000,0,hot,Monday
2024-07-30,40000,0,hot,Tuesday
2024-07-31,40500,0,hot,Wednesday

```

### sample.py

profile_nameに自身のAWSアカウントのプロフィール名を入力してください。

```python
import os
from flask import Flask, render_template, request, jsonify
import boto3
from botocore.exceptions import ClientError
import json
import pandas as pd
from datetime import datetime, timedelta
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta

app = Flask(__name__)

session = boto3.Session(profile_name='プロフィール名を入力')

bedrock = session.client('bedrock-runtime')

# グラフ生成関数
def generate_sales_trend_graph():
    df = pd.read_csv('sales_data.csv')
    df['date'] = pd.to_datetime(df['date'])
    plt.figure(figsize=(12, 6))
    sns.lineplot(x='date', y='sales', data=df)
    plt.title('Sales Trend')
    plt.savefig('static/sales_trend.png')
    plt.close()

# アプリケーション起動時にグラフを生成
generate_sales_trend_graph()

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        promotion_days = request.json.get('promotion_days', [])  # フロントエンドから受け取ったプロモーション日
        prediction, analysis = get_sales_prediction(promotion_days)
        return jsonify({"prediction": prediction, "analysis": analysis})
    return render_template('index.html')

def get_sales_prediction(promotion_days):
    df = pd.read_csv('sales_data.csv')
    df['date'] = pd.to_datetime(df['date'])
    df = df.sort_values('date')

    last_date = df['date'].max()

    analysis = analyze_data(df)

    last_30_days = df.tail(30)

    sales_data = last_30_days.to_json(orient='records')

    next_7_days = [(last_date + timedelta(days=i+1)).strftime('%Y-%m-%d') for i in range(7)]

    promotion_info = ", ".join([f"{date}: プロモーションあり" if date in promotion_days else f"{date}: プロモーションなし" for date in next_7_days])

    user_prompt = f"""以下の過去30日間の売上データと、次の7日間のプロモーション情報に基づいて、次の7日間（{', '.join(next_7_days)}）の売上と天候を予測してください：

    売上データ：
    {sales_data}
    
    プロモーション情報：
    {promotion_info}
    
    予測の際には、以下の点を考慮してください：
    1. 売上のトレンドと季節性
    2. プロモーションの影響（プロモーションがある日は売上が増加する傾向がある）
    3. 天候の季節的な変化（冬季は寒い日が多く、夏季は暑い日が多いなど）
    4. 曜日による売上の変動
    
    予測結果は、次の7日間の日次売上と天候を表すJSON配列として提供してください。
    必ず以下の形式で回答してください：
    
    [
      {{"date": "YYYY-MM-DD", "sales": 予測売上額, "weather": "予測天候", "promotion": プロモーションの有無（true/false）}},
      {{"date": "YYYY-MM-DD", "sales": 予測売上額, "weather": "予測天候", "promotion": プロモーションの有無（true/false）}},
      ...
    ]
    
    天候は "cold", "mild", "hot", "rainy" のいずれかを選択し、季節に応じた適切な予測を行ってください。
    特に、現在の季節（{last_date.strftime('%B')}）を考慮に入れてください。
    
    その後、あなたの予測に影響を与えた要因について簡単な分析を提供してください。
    特に、季節性が天候と売上にどのように影響しているか、そしてプロモーションが売上にどのような影響を与えているかを説明してください。"""
    
    try:
        response = bedrock.invoke_model(
            modelId="anthropic.claude-3-haiku-20240307-v1:0",
            body=json.dumps({
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 1000,
                "messages": [
                    {
                        "role": "user",
                        "content": user_prompt
                    }
                ],
                "temperature": 0.5
            })
        )
        
        response_body = json.loads(response['body'].read())
        completion = response_body['content'][0]['text']

        print("生の応答:", completion) 

        prediction_start = completion.find('[')
        prediction_end = completion.rfind(']') + 1
        prediction_str = completion[prediction_start:prediction_end]

        print("抽出された予測文字列:", prediction_str) 

        try:
            prediction = json.loads(prediction_str)
        except json.JSONDecodeError as e:
            print(f"JSONデコードエラー: {e}")
            prediction = []

        ai_analysis = completion[prediction_end:].strip()

        try:
            with open('sales_data.csv', 'a+', newline='') as f:
                for pred in prediction:
                    f.write(f"{pred['date']},{pred['sales']},{1 if pred['promotion'] else 0},{pred['weather']},{datetime.strptime(pred['date'], '%Y-%m-%d').strftime('%A')}\n")
            print("予測結果をCSVファイルに追加しました。")
        except IOError as e:
            print(f"CSVファイルへの書き込み中にエラーが発生しました: {e}")
        except Exception as e:
            print(f"予期せぬエラーが発生しました: {e}")

        return json.dumps(prediction), analysis + "\n\nAI分析:\n" + ai_analysis

    except ClientError as e:
        print(f"エラーが発生しました: {e}")
        return "[]", f"予測エラー: {str(e)}\n\n分析エラー"

def analyze_data(df):
    analysis = "データ分析:\n"

    # 全体的なトレンド
    overall_trend = df['sales'].diff().mean()
    analysis += f"全体的なトレンド: {'上昇' if overall_trend > 0 else '下降'}\n"

    # 曜日ごとの平均売上
    day_of_week_avg = df.groupby('day_of_week')['sales'].mean().sort_values(ascending=False)
    analysis += f"最も売上が高い曜日: {day_of_week_avg.index[0]}\n"
    analysis += f"最も売上が低い曜日: {day_of_week_avg.index[-1]}\n"

    # プロモーションの効果
    promo_effect = df.groupby('promotion')['sales'].mean()
    analysis += f"プロモーション時の平均売上: {promo_effect[1]:.2f}\n"
    analysis += f"プロモーションなしの平均売上: {promo_effect[0]:.2f}\n"

    # 天候の影響
    weather_effect = df.groupby('weather')['sales'].mean().sort_values(ascending=False)
    analysis += f"売上が最も高い天候: {weather_effect.index[0]}\n"
    analysis += f"売上が最も低い天候: {weather_effect.index[-1]}\n"

    return analysis

if __name__ == '__main__':
    app.run(debug=True)
```

### requirements.txt
```txt
flask==3.0.3
pandas==2.2.2
boto3==1.34.151
matplotlib==3.9.1
seaborn==0.13.2
```

## 環境準備
### 仮想環境準備
```sh
python -m venv myenv
source myenv/bin/activate
```

### ライブラリインストール
```sh
pip install -r requirements.txt
```

## 実行
```
python3 sample.py
```

ブラウザから[127.0.0.1:5000](http://127.0.0.1:5000/)にアクセス

![スクリーンショット 2024-07-31 17.58.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/82d82271-e609-897a-327f-53feaacef603.png)

このアプリケーションを実行すると、``salse_data.csv``に格納されている30日間の売上データを用いて次の7日間の売上や天候、プロモーションの影響を予測します。※プロモーションを実施する日にチェックを入れることで、プロモーション効果を含めた予測を出力してくれます。

例えば金、土、日の3日間にプロモーションを行う場合以下のようにチェックを入れてから「次の7日間の売上を予測」ボタンをクリックします。

![スクリーンショット 2024-07-31 18.04.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/b3167633-f3ea-8905-df4f-2c5aaf680eb4.png)

### 出力結果
2024/8/1から2024/8/7の予測売上と天候、プロモーションの有無が表で出力されました。
![スクリーンショット 2024-07-31 18.07.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/95d36506-71c6-aee6-d334-a3c426bb5adc.png)

また、出力として以下の分析結果とグラフをそれぞれ出力してくれます。
- 全体的なトレンド
- 最も売上が高い曜日
- 最も売上が低い曜日
- プロモーション時の平均売上
- プロモーションなしの平均売上
- 売上が最も高い天候
- 売上が最も低い天候
- AI分析

![スクリーンショット 2024-07-31 18.08.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/1ba473fd-9349-74da-282e-ce7faff55379.png)

## さいごに
今回のサンプルでは売上や天候、プロモーションの有無など変数となる要素が少なく実際の予測として使用するにはシンプルですが、ここから自身の商品やサービスに向けたカスタマイズ(季節性や特殊イベント、競合、プロモーション活動の詳細、顧客層、ソーシャルメディアなど)を施すことで、より精度の高い、実用的な予測アプリケーションへと発展させることができるようになるかと思います。