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
コメント