MENU

GAS×ChatGPTでスプレッドシートのデータ分析を自動化|レポート生成も

GAS×ChatGPTでスプレッドシートのデータ分析を自動化|レポート生成も

毎週月曜の朝、スプレッドシートを開いて売上データを集計し、前週と比較して、レポートにまとめる。この作業に30分以上かけているなら、GAS×ChatGPTで自動化する価値がある。

スプレッドシートのデータをGASで集計し、ChatGPTに分析させれば、傾向の把握からレポート生成、異常値検知まで自動で回る。データ分析の専門知識は不要で、AIが傾向を読み取って日本語で解説してくれる。コピペで使えるコード付きで、順を追って構築していこう。

目次

この記事で作れるもの

機能1: 売上データの自動分析

スプレッドシートの売上データをChatGPTが分析し、傾向やインサイトを抽出する。

出力例:


【分析結果】
・今月の売上は前月比12%増加
・火曜日の売上が最も高い傾向
・商品Aの売上が急上昇(前月比35%増)
・東京エリアの成長率が顕著

【推奨アクション】
・火曜日に広告予算を増額
・商品Aの在庫確保を優先

機能2: 週次・月次レポートの自動生成

定期的にデータを集計し、読みやすいレポートを自動生成する。メールやSlackでの配信にも対応可能。

機能3: 異常値の自動検知

売上の急落、在庫切れリスク、コスト超過を自動検知し、アラートを送信する。

必要な準備

1. Googleアカウント

Googleスプレッドシートを使用する。

2. OpenAI APIキー

GAS×ChatGPT連携の基礎で取得方法を確認してほしい。

3. 分析対象のデータ

ここでは「売上データ」を例に進めるが、どんなデータにも応用できる。

STEP 1: サンプルデータを準備

1-1: スプレッドシートを作成

新しいGoogleスプレッドシートを作成し、「売上分析」と名前を付ける。

1-2: サンプルデータの構造

以下のような売上データを想定している。

A列 B列 C列 D列 E列
日付 商品名 カテゴリ 売上金額 エリア
2026/01/01 商品A 家電 15000 東京
2026/01/01 商品B 雑貨 3500 大阪

1-3: シート構成

シート名 用途
売上データ 生データを格納
分析結果 ChatGPTの分析結果を記録
設定 パラメータ設定用

STEP 2: 基本コードを設定

2-1: スクリプトエディタを開く

スプレッドシートのメニューから「拡張機能」→「Apps Script」を選択する。

2-2: メインコードを入力


/**
 * スプレッドシートデータ分析システム
 * GAS × ChatGPT API
 */

// ========================================
// 設定
// ========================================
const CONFIG = {
  DATA_SHEET: '売上データ',
  RESULT_SHEET: '分析結果',
  MODEL: 'gpt-4o-mini',
  MAX_TOKENS: 2000,
  TEMPERATURE: 0.5
};

// ========================================
// メイン: データ分析を実行
// ========================================
function runDataAnalysis() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const dataSheet = ss.getSheetByName(CONFIG.DATA_SHEET);
    const resultSheet = ss.getSheetByName(CONFIG.RESULT_SHEET);

    // データを取得
    const data = getSheetData(dataSheet);

    // 基本統計を計算
    const stats = calculateBasicStats(data);

    // ChatGPTで分析
    const analysis = analyzeWithChatGPT(data, stats);

    // 結果を記録
    recordAnalysisResult(resultSheet, analysis, stats);

    // メールで通知(任意)
    sendAnalysisReport(analysis, stats);

    console.log('分析完了');

  } catch (error) {
    console.error('分析エラー:', error);
    notifyError(error);
  }
}

// ========================================
// シートからデータを取得
// ========================================
function getSheetData(sheet) {
  const lastRow = sheet.getLastRow();
  const lastCol = sheet.getLastColumn();

  if (lastRow < 2) {
    throw new Error('データがありません');
  }

  // ヘッダーとデータを取得
  const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
  const values = sheet.getRange(2, 1, lastRow - 1, lastCol).getValues();

  // オブジェクト配列に変換
  return values.map(row => {
    const obj = {};
    headers.forEach((header, i) => {
      obj[header] = row[i];
    });
    return obj;
  });
}

// ========================================
// 基本統計を計算
// ========================================
function calculateBasicStats(data) {
  // 売上金額の列名を想定
  const salesColumn = '売上金額';

  const sales = data
    .map(row => Number(row[salesColumn]) || 0)
    .filter(v => v > 0);

  const total = sales.reduce((a, b) => a + b, 0);
  const count = sales.length;
  const average = count > 0 ? total / count : 0;
  const max = Math.max(...sales);
  const min = Math.min(...sales);

  // カテゴリ別集計
  const categoryTotals = {};
  data.forEach(row => {
    const cat = row['カテゴリ'] || '不明';
    const amount = Number(row[salesColumn]) || 0;
    categoryTotals[cat] = (categoryTotals[cat] || 0) + amount;
  });

  // エリア別集計
  const areaTotals = {};
  data.forEach(row => {
    const area = row['エリア'] || '不明';
    const amount = Number(row[salesColumn]) || 0;
    areaTotals[area] = (areaTotals[area] || 0) + amount;
  });

  // 日別集計
  const dailyTotals = {};
  data.forEach(row => {
    const date = row['日付'];
    const dateStr = date instanceof Date
      ? Utilities.formatDate(date, 'JST', 'yyyy/MM/dd')
      : String(date);
    const amount = Number(row[salesColumn]) || 0;
    dailyTotals[dateStr] = (dailyTotals[dateStr] || 0) + amount;
  });

  return {
    total,
    count,
    average: Math.round(average),
    max,
    min,
    categoryTotals,
    areaTotals,
    dailyTotals,
    period: {
      start: Object.keys(dailyTotals).sort()[0],
      end: Object.keys(dailyTotals).sort().pop()
    }
  };
}

// ========================================
// ChatGPTで詳細分析
// ========================================
function analyzeWithChatGPT(data, stats) {
  const apiKey = PropertiesService.getScriptProperties()
    .getProperty('OPENAI_API_KEY');

  if (!apiKey) {
    throw new Error('APIキーが設定されていません');
  }

  // 統計情報をテキスト化
  const statsText = formatStatsForPrompt(stats);

  // サンプルデータ(最新20件)
  const sampleData = data.slice(-20).map(row =>
    `${row['日付']} | ${row['商品名']} | ${row['カテゴリ']} | ¥${row['売上金額']} | ${row['エリア']}`
  ).join('\n');

  const prompt = `以下の売上データを分析し、ビジネスに役立つインサイトを提供してください。

【集計期間】
${stats.period.start} 〜 ${stats.period.end}

【基本統計】
${statsText}

【直近のデータサンプル(20件)】
${sampleData}

以下の形式でJSON出力してください。JSONのみ出力し、他の文章は含めないでください。

{
  "summary": "全体の傾向を3文で要約",
  "insights": [
    "インサイト1(データから読み取れる重要な発見)",
    "インサイト2",
    "インサイト3"
  ],
  "trends": {
    "positive": ["良い傾向1", "良い傾向2"],
    "negative": ["懸念点1", "懸念点2"]
  },
  "recommendations": [
    "推奨アクション1",
    "推奨アクション2",
    "推奨アクション3"
  ],
  "anomalies": ["異常値や注意すべき点があれば記載"]
}`;

  const response = callChatGPT(apiKey, prompt);

  try {
    return JSON.parse(response);
  } catch (e) {
    // JSONパース失敗時
    return {
      summary: response.substring(0, 200),
      insights: ['分析結果の解析に失敗しました'],
      trends: { positive: [], negative: [] },
      recommendations: ['手動で確認してください'],
      anomalies: []
    };
  }
}

// ========================================
// 統計情報をプロンプト用にフォーマット
// ========================================
function formatStatsForPrompt(stats) {
  let text = '';

  text += `・総売上: ¥${stats.total.toLocaleString()}\n`;
  text += `・取引件数: ${stats.count}件\n`;
  text += `・平均単価: ¥${stats.average.toLocaleString()}\n`;
  text += `・最高額: ¥${stats.max.toLocaleString()}\n`;
  text += `・最低額: ¥${stats.min.toLocaleString()}\n\n`;

  text += '【カテゴリ別売上】\n';
  Object.entries(stats.categoryTotals)
    .sort((a, b) => b[1] - a[1])
    .forEach(([cat, amount]) => {
      text += `・${cat}: ¥${amount.toLocaleString()}\n`;
    });

  text += '\n【エリア別売上】\n';
  Object.entries(stats.areaTotals)
    .sort((a, b) => b[1] - a[1])
    .forEach(([area, amount]) => {
      text += `・${area}: ¥${amount.toLocaleString()}\n`;
    });

  return text;
}

// ========================================
// ChatGPT API呼び出し
// ========================================
function callChatGPT(apiKey, prompt) {
  const url = 'https://api.openai.com/v1/chat/completions';

  const payload = {
    model: CONFIG.MODEL,
    messages: [
      {
        role: 'system',
        content: 'あなたはビジネスデータアナリストです。売上データを分析し、実用的なインサイトと推奨アクションを提供してください。'
      },
      {
        role: 'user',
        content: prompt
      }
    ],
    max_tokens: CONFIG.MAX_TOKENS,
    temperature: CONFIG.TEMPERATURE
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': 'Bearer ' + apiKey
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  const responseCode = response.getResponseCode();

  if (responseCode !== 200) {
    throw new Error(`API Error: ${responseCode}`);
  }

  const json = JSON.parse(response.getContentText());
  return json.choices[0].message.content.trim();
}

// ========================================
// 分析結果を記録
// ========================================
function recordAnalysisResult(sheet, analysis, stats) {
  const timestamp = new Date();

  // 既存データをクリア(ヘッダー以外)
  if (sheet.getLastRow() > 1) {
    sheet.getRange(2, 1, sheet.getLastRow() - 1, 10).clear();
  }

  // 結果を書き込み
  const row = 2;

  sheet.getRange(row, 1).setValue(timestamp);
  sheet.getRange(row, 2).setValue(stats.period.start + ' 〜 ' + stats.period.end);
  sheet.getRange(row, 3).setValue(stats.total);
  sheet.getRange(row, 4).setValue(stats.count);
  sheet.getRange(row, 5).setValue(analysis.summary);
  sheet.getRange(row, 6).setValue(analysis.insights.join('\n'));
  sheet.getRange(row, 7).setValue(
    '【良い傾向】\n' + analysis.trends.positive.join('\n') +
    '\n\n【懸念点】\n' + analysis.trends.negative.join('\n')
  );
  sheet.getRange(row, 8).setValue(analysis.recommendations.join('\n'));
  sheet.getRange(row, 9).setValue(analysis.anomalies.join('\n') || 'なし');
}

// ========================================
// メールでレポート送信
// ========================================
function sendAnalysisReport(analysis, stats) {
  const email = Session.getActiveUser().getEmail();
  const subject = `【売上分析レポート】${stats.period.start}〜${stats.period.end}`;

  const body = `売上分析レポート
================

【集計期間】${stats.period.start} 〜 ${stats.period.end}

【基本統計】
・総売上: ¥${stats.total.toLocaleString()}
・取引件数: ${stats.count}件
・平均単価: ¥${stats.average.toLocaleString()}

【サマリー】
${analysis.summary}

【主なインサイト】
${analysis.insights.map((i, idx) => `${idx + 1}. ${i}`).join('\n')}

【傾向】
◎ 良い傾向
${analysis.trends.positive.map(t => `・${t}`).join('\n')}

△ 懸念点
${analysis.trends.negative.map(t => `・${t}`).join('\n')}

【推奨アクション】
${analysis.recommendations.map((r, idx) => `${idx + 1}. ${r}`).join('\n')}

【異常値・注意点】
${analysis.anomalies.length > 0 ? analysis.anomalies.join('\n') : 'なし'}

---
このレポートはGAS×ChatGPTで自動生成されました。`;

  GmailApp.sendEmail(email, subject, body);
  console.log('レポートメール送信完了');
}

// ========================================
// エラー通知
// ========================================
function notifyError(error) {
  const email = Session.getActiveUser().getEmail();
  GmailApp.sendEmail(
    email,
    '【エラー】売上分析システム',
    `エラーが発生しました:\n\n${error.toString()}`
  );
}

// ========================================
// 異常値検知(単独実行可能)
// ========================================
function detectAnomalies() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dataSheet = ss.getSheetByName(CONFIG.DATA_SHEET);
  const data = getSheetData(dataSheet);
  const stats = calculateBasicStats(data);

  // 標準偏差を計算
  const sales = data.map(row => Number(row['売上金額']) || 0);
  const mean = stats.average;
  const variance = sales.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / sales.length;
  const stdDev = Math.sqrt(variance);

  // 異常値(平均から2標準偏差以上離れた値)を検出
  const threshold = mean + 2 * stdDev;
  const anomalies = data.filter(row => {
    const amount = Number(row['売上金額']) || 0;
    return amount > threshold || amount < mean - 2 * stdDev;
  });

  if (anomalies.length > 0) {
    const message = `異常値を${anomalies.length}件検出しました:\n\n` +
      anomalies.slice(0, 10).map(row =>
        `${row['日付']} | ${row['商品名']} | ¥${row['売上金額']}`
      ).join('\n');

    GmailApp.sendEmail(
      Session.getActiveUser().getEmail(),
      '【アラート】売上データに異常値を検出',
      message
    );

    console.log(`異常値検出: ${anomalies.length}件`);
  } else {
    console.log('異常値なし');
  }
}

// ========================================
// 前週比較分析
// ========================================
function compareWithLastWeek() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dataSheet = ss.getSheetByName(CONFIG.DATA_SHEET);
  const data = getSheetData(dataSheet);

  const today = new Date();
  const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
  const twoWeeksAgo = new Date(today.getTime() - 14 * 24 * 60 * 60 * 1000);

  // 今週と先週のデータを分離
  const thisWeek = data.filter(row => {
    const date = new Date(row['日付']);
    return date >= oneWeekAgo && date <= today;
  });

  const lastWeek = data.filter(row => {
    const date = new Date(row['日付']);
    return date >= twoWeeksAgo && date < oneWeekAgo;
  });

  const thisWeekTotal = thisWeek.reduce((sum, row) =>
    sum + (Number(row['売上金額']) || 0), 0);
  const lastWeekTotal = lastWeek.reduce((sum, row) =>
    sum + (Number(row['売上金額']) || 0), 0);

  const changeRate = lastWeekTotal > 0
    ? ((thisWeekTotal - lastWeekTotal) / lastWeekTotal * 100).toFixed(1)
    : 'N/A';

  const comparison = {
    thisWeek: thisWeekTotal,
    lastWeek: lastWeekTotal,
    changeRate: changeRate,
    trend: thisWeekTotal > lastWeekTotal ? '増加' : '減少'
  };

  console.log('週次比較:', comparison);
  return comparison;
}

// ========================================
// テスト用: サンプルデータ生成
// ========================================
function generateSampleData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(CONFIG.DATA_SHEET);

  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.DATA_SHEET);
  }

  // ヘッダー
  sheet.getRange(1, 1, 1, 5).setValues([
    ['日付', '商品名', 'カテゴリ', '売上金額', 'エリア']
  ]);

  // サンプルデータ生成
  const products = ['商品A', '商品B', '商品C', '商品D', '商品E'];
  const categories = ['家電', '雑貨', '食品', '衣類'];
  const areas = ['東京', '大阪', '名古屋', '福岡', '札幌'];

  const data = [];
  const startDate = new Date('2026-01-01');

  for (let i = 0; i < 100; i++) {
    const date = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000);
    const product = products[Math.floor(Math.random() * products.length)];
    const category = categories[Math.floor(Math.random() * categories.length)];
    const amount = Math.floor(Math.random() * 50000) + 1000;
    const area = areas[Math.floor(Math.random() * areas.length)];

    data.push([date, product, category, amount, area]);
  }

  sheet.getRange(2, 1, data.length, 5).setValues(data);
  console.log('サンプルデータを生成しました');
}

// ========================================
// 分析結果シートの初期化
// ========================================
function initResultSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(CONFIG.RESULT_SHEET);

  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.RESULT_SHEET);
  }

  // ヘッダー
  sheet.getRange(1, 1, 1, 9).setValues([[
    '分析日時', '対象期間', '総売上', '件数',
    'サマリー', 'インサイト', '傾向', '推奨アクション', '異常値'
  ]]);

  // 列幅調整
  sheet.setColumnWidth(5, 300);
  sheet.setColumnWidth(6, 300);
  sheet.setColumnWidth(7, 300);
  sheet.setColumnWidth(8, 300);

  console.log('分析結果シートを初期化しました');
}

2-3: APIキーを設定

  • 「プロジェクトの設定」→「スクリプトプロパティ」
  • OPENAI_API_KEY にAPIキーを設定

STEP 3: 初期設定とテスト

3-1: シートを初期化

  • 関数選択で initResultSheet を実行(分析結果シート作成)
  • 関数選択で generateSampleData を実行(テストデータ生成)

3-2: 分析を実行

  • 関数選択で runDataAnalysis を実行
  • 「分析結果」シートに結果が記録される
  • メールでレポートが届く

STEP 4: 定期実行の設定

4-1: 週次レポートの自動化

毎週月曜日の朝9時に分析を実行する設定。

  • 「トリガー」→「トリガーを追加」
  • 以下の設定で保存
項目 設定値
実行する関数 runDataAnalysis
イベントのソース 時間主導型
時間ベースのトリガータイプ 週ベースのタイマー
曜日 毎週月曜日
時刻 午前9時〜10時

4-2: 異常値検知の日次実行

毎日夕方に異常値をチェックする設定。

項目 設定値
実行する関数 detectAnomalies
イベントのソース 時間主導型
時間ベースのトリガータイプ 日付ベースのタイマー
時刻 午後6時〜7時

応用カスタマイズ

カスタマイズ1: 分析項目の追加

顧客データや在庫データなど、別のデータにも対応できる。


// 顧客分析用のプロンプト例
const customerPrompt = `以下の顧客データを分析し、以下を出力してください:
- 顧客セグメント(VIP/一般/休眠)
- 購買頻度の傾向
- 離脱リスクの高い顧客`;

カスタマイズ2: Slack連携


function sendToSlack(analysis) {
  const webhookUrl = PropertiesService.getScriptProperties()
    .getProperty('SLACK_WEBHOOK_URL');

  const payload = {
    blocks: [
      {
        type: 'header',
        text: { type: 'plain_text', text: '📊 売上分析レポート' }
      },
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `*サマリー*\n${analysis.summary}` }
      },
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `*推奨アクション*\n${analysis.recommendations.join('\n')}` }
      }
    ]
  };

  UrlFetchApp.fetch(webhookUrl, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload)
  });
}

カスタマイズ3: グラフ自動生成


function createChart(sheet, stats) {
  const chartBuilder = sheet.newChart()
    .setChartType(Charts.ChartType.PIE)
    .addRange(sheet.getRange('A2:B10'))
    .setPosition(5, 7, 0, 0)
    .setOption('title', 'カテゴリ別売上');

  sheet.insertChart(chartBuilder.build());
}

トラブルシューティング

Q: データが多すぎてAPIに送れない

対策: サンプルデータを絞るか、統計情報のみを送信するようプロンプトを調整する。


// 直近100件に絞る
const sampleData = data.slice(-100);

Q: 分析結果が的外れ

対策: プロンプトを具体的に書き直す。業種や分析目的を明記すると精度が上がる。


const prompt = `あなたはEC事業のデータアナリストです。
以下は月間売上データです。特に以下の観点で分析してください:
1. 売れ筋商品の傾向
2. 顧客単価の変化
3. 季節性の影響`;

Q: JSONパースエラー

対策: プロンプトに「JSONのみ出力」を強調する。フォールバック処理も実装済みなので、仮にパースに失敗しても処理は止まらない。

まとめ

GAS×ChatGPTでスプレッドシートのデータ分析を自動化する方法を見てきた。

実現できること:

  • 売上データの自動分析(傾向・インサイト抽出)
  • 週次・月次レポートの自動生成
  • 異常値の自動検知とアラート
  • 前週比較分析

応用範囲

このシステムは売上データに限らず、以下にも応用が効く。

  • 顧客アンケートの分析
  • 在庫データの最適化提案
  • 採用データの傾向分析
  • SNSエンゲージメントの分析

関連記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次