MENU

GAS×データ集計入門|ピボットテーブル風の自動集計を実装する方法

GAS×データ集計入門|ピボットテーブル風の自動集計を実装する方法

毎週同じ集計作業を手動で繰り返している。ピボットテーブルを毎回作り直すのも面倒だし、集計結果を別シートに転記する手間もかかる。

GASを使えば、こうした集計処理を丸ごと自動化できる。合計・平均・カウントといった基本操作はもちろん、カテゴリ別のグループ化やピボットテーブル風のクロス集計まで、コードを書くだけで片付く。


目次

この記事でわかること

  • GASでデータ集計するメリット
  • 基本の集計処理(合計・平均・カウント・最大・最小)
  • カテゴリ別集計(グループ化)の実装方法
  • クロス集計(ピボットテーブル風)の作り方
  • 日別・週別・月別の時系列集計
  • 集計結果を別シートに自動出力する方法

GASでデータ集計するメリット

なぜGASで集計するのか

メリット 詳細
完全自動化 トリガー設定で定期的に自動実行
柔軟なカスタマイズ 複雑な集計ロジックも自由に実装
リアルタイム更新 データ変更時に即座に再集計可能
複数シート連携 集計元と出力先を分けて管理
レポート自動生成 メール送信やSlack通知と組み合わせ可

ピボットテーブル vs GAS集計

項目 ピボットテーブル GAS集計
設定の手軽さ ◎ GUIで簡単 △ コード必要
自動更新 △ 手動更新必要 ◎ トリガーで自動
複雑な条件 △ 限界あり ◎ 自由自在
別シート出力 △ 手動コピー ◎ 自動出力
他システム連携 × 不可 ◎ API連携可

GUIで手軽に使えるピボットテーブルに対し、GAS集計は初期コストがかかるものの、一度書けば手を動かさずに済む。定期的に同じ集計を回す業務にはGASが向いている。


サンプルデータの準備

以下のような売上データを例に進める。

A列(日付) B列(カテゴリ) C列(商品名) D列(金額) E列(数量)
2026-02-01 食品 りんご 150 3
2026-02-01 飲料 お茶 120 5
2026-02-02 食品 バナナ 100 4
2026-02-02 日用品 洗剤 300 1

基本の集計処理

合計・平均・カウント・最大・最小

まずは基本的な集計関数をGASで実装する。


// ===================================================
// 基本集計関数
// ===================================================

/**
 * 売上データの基本集計を実行
 */
function basicAggregation() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('売上データ');
  const data = sheet.getDataRange().getValues();

  // ヘッダーを除いたデータ部分を取得
  const rows = data.slice(1);

  // 金額列(D列 = index 3)を配列で取得
  const amounts = rows.map(row => row[3]).filter(v => typeof v === 'number');

  // 各種集計
  const result = {
    count: amounts.length,
    sum: sumArray(amounts),
    average: averageArray(amounts),
    max: Math.max(...amounts),
    min: Math.min(...amounts)
  };

  console.log('=== 基本集計結果 ===');
  console.log('件数: ' + result.count);
  console.log('合計: ¥' + result.sum.toLocaleString());
  console.log('平均: ¥' + Math.round(result.average).toLocaleString());
  console.log('最大: ¥' + result.max.toLocaleString());
  console.log('最小: ¥' + result.min.toLocaleString());

  return result;
}

/**
 * 配列の合計を計算
 */
function sumArray(arr) {
  return arr.reduce((sum, val) => sum + val, 0);
}

/**
 * 配列の平均を計算
 */
function averageArray(arr) {
  if (arr.length === 0) return 0;
  return sumArray(arr) / arr.length;
}

集計関数パターン一覧

集計 コード 説明
合計 arr.reduce((s,v) => s+v, 0) 全要素を足し合わせ
平均 sum / arr.length 合計 ÷ 件数
件数 arr.length 配列の長さ
最大 Math.max(...arr) 最大値を取得
最小 Math.min(...arr) 最小値を取得
中央値 sorted[Math.floor(n/2)] ソート後の中央
ユニーク数 new Set(arr).size 重複除外した件数

カテゴリ別集計(グループ化)

Mapを使った効率的なグループ化

JavaScriptのMapオブジェクトを使うと、カテゴリ別の集計を少ないコードで実装できる。


// ===================================================
// カテゴリ別集計(グループ化)
// ===================================================

/**
 * カテゴリ別の売上集計
 */
function aggregateByCategory() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('売上データ');
  const data = sheet.getDataRange().getValues();
  const rows = data.slice(1);  // ヘッダー除外

  // Mapでカテゴリ別に集計
  const categoryMap = new Map();

  rows.forEach(row => {
    const category = row[1];  // B列: カテゴリ
    const amount = row[3];    // D列: 金額
    const quantity = row[4];  // E列: 数量

    if (!categoryMap.has(category)) {
      categoryMap.set(category, {
        count: 0,
        totalAmount: 0,
        totalQuantity: 0
      });
    }

    const cat = categoryMap.get(category);
    cat.count++;
    cat.totalAmount += amount;
    cat.totalQuantity += quantity;
  });

  // 結果を配列に変換
  const results = [];
  categoryMap.forEach((value, key) => {
    results.push({
      category: key,
      count: value.count,
      totalAmount: value.totalAmount,
      totalQuantity: value.totalQuantity,
      avgAmount: Math.round(value.totalAmount / value.count)
    });
  });

  // 金額順でソート(降順)
  results.sort((a, b) => b.totalAmount - a.totalAmount);

  console.log('=== カテゴリ別集計 ===');
  results.forEach(r => {
    console.log(`${r.category}: ${r.count}件, ¥${r.totalAmount.toLocaleString()}`);
  });

  return results;
}

/**
 * 集計結果を別シートに出力
 */
function outputCategoryResult() {
  const results = aggregateByCategory();

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let outputSheet = ss.getSheetByName('カテゴリ別集計');

  // シートがなければ作成
  if (!outputSheet) {
    outputSheet = ss.insertSheet('カテゴリ別集計');
  }

  // クリアして新規出力
  outputSheet.clear();

  // ヘッダー
  const headers = ['カテゴリ', '件数', '売上合計', '数量合計', '平均単価'];
  outputSheet.getRange(1, 1, 1, headers.length).setValues([headers]);

  // データ出力
  const outputData = results.map(r => [
    r.category,
    r.count,
    r.totalAmount,
    r.totalQuantity,
    r.avgAmount
  ]);

  if (outputData.length > 0) {
    outputSheet.getRange(2, 1, outputData.length, headers.length)
      .setValues(outputData);
  }

  // 書式設定
  outputSheet.getRange(2, 3, outputData.length, 1)
    .setNumberFormat('¥#,##0');
  outputSheet.getRange(2, 5, outputData.length, 1)
    .setNumberFormat('¥#,##0');

  console.log('カテゴリ別集計を出力しました');
}

クロス集計(ピボット風)

行×列のクロス集計テーブルを作成

ピボットテーブルのように、行と列の2軸で集計するクロス集計を実装する。


// ===================================================
// クロス集計(ピボットテーブル風)
// ===================================================

/**
 * カテゴリ × 月のクロス集計
 */
function crossTabulation() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('売上データ');
  const data = sheet.getDataRange().getValues();
  const rows = data.slice(1);

  // クロス集計用のネストしたMap
  // Map<カテゴリ, Map<月, 金額合計>>
  const crossMap = new Map();
  const months = new Set();  // 存在する月を収集

  rows.forEach(row => {
    const date = new Date(row[0]);
    const category = row[1];
    const amount = row[3];

    // 月を「YYYY-MM」形式で取得
    const month = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy-MM');
    months.add(month);

    // カテゴリのMapを取得または作成
    if (!crossMap.has(category)) {
      crossMap.set(category, new Map());
    }

    const categoryMonths = crossMap.get(category);
    const current = categoryMonths.get(month) || 0;
    categoryMonths.set(month, current + amount);
  });

  // 月をソートして配列化
  const sortedMonths = Array.from(months).sort();

  // 結果を2次元配列に変換
  const results = [];

  // ヘッダー行
  results.push(['カテゴリ', ...sortedMonths, '合計']);

  // データ行
  crossMap.forEach((monthMap, category) => {
    const row = [category];
    let rowTotal = 0;

    sortedMonths.forEach(month => {
      const value = monthMap.get(month) || 0;
      row.push(value);
      rowTotal += value;
    });

    row.push(rowTotal);
    results.push(row);
  });

  // 合計行を追加
  const totalRow = ['合計'];
  let grandTotal = 0;

  sortedMonths.forEach(month => {
    let monthTotal = 0;
    crossMap.forEach(monthMap => {
      monthTotal += monthMap.get(month) || 0;
    });
    totalRow.push(monthTotal);
    grandTotal += monthTotal;
  });
  totalRow.push(grandTotal);
  results.push(totalRow);

  return results;
}

/**
 * クロス集計結果を別シートに出力
 */
function outputCrossTabulation() {
  const results = crossTabulation();

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let outputSheet = ss.getSheetByName('クロス集計');

  if (!outputSheet) {
    outputSheet = ss.insertSheet('クロス集計');
  }

  outputSheet.clear();

  // データ出力
  outputSheet.getRange(1, 1, results.length, results[0].length)
    .setValues(results);

  // 書式設定(数値セルに通貨形式)
  const dataRange = outputSheet.getRange(2, 2,
    results.length - 1, results[0].length - 1);
  dataRange.setNumberFormat('¥#,##0');

  // ヘッダーと合計行の装飾
  outputSheet.getRange(1, 1, 1, results[0].length)
    .setBackground('#4285f4')
    .setFontColor('#ffffff')
    .setFontWeight('bold');

  outputSheet.getRange(results.length, 1, 1, results[0].length)
    .setBackground('#e8f0fe')
    .setFontWeight('bold');

  console.log('クロス集計を出力しました');
}

日別・週別・月別の時系列集計

日付単位での集計パターン


// ===================================================
// 時系列集計(日別・週別・月別)
// ===================================================

/**
 * 日別売上集計
 */
function aggregateByDay() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('売上データ');
  const data = sheet.getDataRange().getValues();
  const rows = data.slice(1);

  const dailyMap = new Map();

  rows.forEach(row => {
    const date = new Date(row[0]);
    const dateKey = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy-MM-dd');
    const amount = row[3];

    const current = dailyMap.get(dateKey) || { count: 0, total: 0 };
    current.count++;
    current.total += amount;
    dailyMap.set(dateKey, current);
  });

  // 日付順にソート
  const sortedDays = Array.from(dailyMap.keys()).sort();

  const results = sortedDays.map(day => ({
    date: day,
    count: dailyMap.get(day).count,
    total: dailyMap.get(day).total
  }));

  return results;
}

/**
 * 週別売上集計
 */
function aggregateByWeek() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('売上データ');
  const data = sheet.getDataRange().getValues();
  const rows = data.slice(1);

  const weeklyMap = new Map();

  rows.forEach(row => {
    const date = new Date(row[0]);
    const weekKey = getWeekKey(date);  // 週の開始日を取得
    const amount = row[3];

    const current = weeklyMap.get(weekKey) || { count: 0, total: 0 };
    current.count++;
    current.total += amount;
    weeklyMap.set(weekKey, current);
  });

  const sortedWeeks = Array.from(weeklyMap.keys()).sort();

  return sortedWeeks.map(week => ({
    week: week,
    count: weeklyMap.get(week).count,
    total: weeklyMap.get(week).total
  }));
}

/**
 * 月別売上集計
 */
function aggregateByMonth() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('売上データ');
  const data = sheet.getDataRange().getValues();
  const rows = data.slice(1);

  const monthlyMap = new Map();

  rows.forEach(row => {
    const date = new Date(row[0]);
    const monthKey = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy-MM');
    const amount = row[3];

    const current = monthlyMap.get(monthKey) || { count: 0, total: 0 };
    current.count++;
    current.total += amount;
    monthlyMap.set(monthKey, current);
  });

  const sortedMonths = Array.from(monthlyMap.keys()).sort();

  return sortedMonths.map(month => ({
    month: month,
    count: monthlyMap.get(month).count,
    total: monthlyMap.get(month).total
  }));
}

/**
 * 週の開始日(月曜日)を取得
 */
function getWeekKey(date) {
  const d = new Date(date);
  const day = d.getDay();
  const diff = d.getDate() - day + (day === 0 ? -6 : 1);  // 月曜日を週の開始
  d.setDate(diff);
  return Utilities.formatDate(d, 'Asia/Tokyo', 'yyyy-MM-dd');
}

集計結果を別シートに自動出力

複数の集計を一括出力


// ===================================================
// 集計レポートを一括出力
// ===================================================

/**
 * 全集計をまとめて実行・出力
 */
function generateAllReports() {
  console.log('集計レポート生成を開始します...');

  // 1. 基本集計
  const basic = basicAggregation();
  outputBasicSummary(basic);

  // 2. カテゴリ別集計
  outputCategoryResult();

  // 3. クロス集計
  outputCrossTabulation();

  // 4. 月別推移
  outputMonthlyTrend();

  // 更新日時を記録
  const summarySheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('サマリー');
  if (summarySheet) {
    summarySheet.getRange('A1').setValue('最終更新: ' + new Date());
  }

  console.log('全集計レポートの生成が完了しました');
}

/**
 * 基本サマリーを出力
 */
function outputBasicSummary(basic) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('サマリー');

  if (!sheet) {
    sheet = ss.insertSheet('サマリー');
  }

  sheet.clear();

  const summaryData = [
    ['項目', '値'],
    ['総件数', basic.count],
    ['売上合計', basic.sum],
    ['平均単価', Math.round(basic.average)],
    ['最高額', basic.max],
    ['最低額', basic.min]
  ];

  sheet.getRange(1, 1, summaryData.length, 2).setValues(summaryData);
  sheet.getRange(2, 2, 5, 1).setNumberFormat('¥#,##0');

  // ヘッダー装飾
  sheet.getRange(1, 1, 1, 2)
    .setBackground('#4285f4')
    .setFontColor('#ffffff')
    .setFontWeight('bold');
}

/**
 * 月別推移を出力
 */
function outputMonthlyTrend() {
  const results = aggregateByMonth();

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('月別推移');

  if (!sheet) {
    sheet = ss.insertSheet('月別推移');
  }

  sheet.clear();

  // ヘッダー
  sheet.getRange(1, 1, 1, 4).setValues([['月', '件数', '売上合計', '前月比']]);

  // データ
  const data = results.map((r, i) => {
    const prevTotal = i > 0 ? results[i - 1].total : r.total;
    const ratio = prevTotal > 0 ? Math.round((r.total / prevTotal) * 100) : 100;
    return [r.month, r.count, r.total, ratio + '%'];
  });

  if (data.length > 0) {
    sheet.getRange(2, 1, data.length, 4).setValues(data);
    sheet.getRange(2, 3, data.length, 1).setNumberFormat('¥#,##0');
  }

  // ヘッダー装飾
  sheet.getRange(1, 1, 1, 4)
    .setBackground('#4285f4')
    .setFontColor('#ffffff')
    .setFontWeight('bold');
}

トリガーで自動更新

集計を定期実行するには、トリガーを設定する。


/**
 * 毎日朝9時に集計を自動実行するトリガーを作成
 */
function createDailyAggregationTrigger() {
  // 既存のトリガーを削除
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'generateAllReports') {
      ScriptApp.deleteTrigger(trigger);
    }
  });

  // 新しいトリガーを作成
  ScriptApp.newTrigger('generateAllReports')
    .timeBased()
    .atHour(9)
    .everyDays(1)
    .create();

  console.log('日次集計トリガーを作成しました');
}

トラブルシューティング

よくあるエラーと解決法

エラー 原因 解決法
TypeError: Cannot read property 空セル・データなし filter()で空値を除外
NaNが出力される 文字列が混入 typeof v === 'number'でチェック
日付がおかしい タイムゾーン問題 Utilities.formatDateを使用
処理が遅い ループ内でシートアクセス 一括取得→メモリで処理→一括出力

パフォーマンス改善のコツ


// ❌ 遅いパターン(ループ内でシートアクセス)
for (let i = 0; i < 1000; i++) {
  const value = sheet.getRange(i, 1).getValue();  // 毎回API呼び出し
}

// ✅ 速いパターン(一括取得)
const data = sheet.getDataRange().getValues();  // 1回のAPI呼び出し
data.forEach(row => {
  // メモリ上で処理
});

GASのシートアクセスはAPI呼び出しと同じコストがかかる。ループ内でgetRange().getValue()を繰り返すと、1000行あれば1000回のAPIコールが走る。getDataRange().getValues()で一括取得し、メモリ上で処理してからsetValues()で一括書き込みするのが鉄則だ。


まとめ

ポイントまとめ

集計パターン 実装方法
基本集計 reduce(), Math.max/min
カテゴリ別 Mapでグループ化
クロス集計 ネストしたMapで2軸管理
時系列 日付をキーにして集計
自動出力 別シートにsetValues()

次のステップ

  • トリガー設定で毎日自動集計
  • ChatGPT連携で集計結果を自然言語レポートに変換
  • Slack通知で集計結果をチームに共有

関連記事

  • GASトリガー完全ガイド
  • GAS×ChatGPT連携入門
  • GASエラー解決ガイド

作成日: 2026-02-02

公開予定日: 2026-03-17

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

この記事を書いた人

コメント

コメントする

目次