【後編・全コード公開】ChatGPTでGoogleスライド月次レポート自動化システムを構築する方法

前回の記事では、GA4やGSCから出力したCSVを元に、GPTとGASを使ってGoogleスライドの月次レポートを“ほぼ自動”で生成する方法をご紹介しました。

ただ、やってみてすぐに限界も見えてきました:

  • 表をスライドに埋め込むとレイアウトが崩れる(10行でもページに収まらない)
  • グラフや表の画像は自分で作ってDriveに上げる必要がある
  • GPTに渡すCSVも毎回手作業で加工が必要

──ということで、「前編の自動化」をさらに自動化するべく、後編では Python(Colab)+API を使った自動処理を導入してみました。

2. Pythonで自動化する理由

今回採用したのは、Google Colab 上で動作する Python スクリプトです。

Colabを使うメリット:

  • 環境構築が不要(ブラウザだけでOK)
  • Google Driveとの親和性◎(自動保存も簡単)
  • 日本語のグラフや表にも対応可能japanize_matplotlib

3. API連携の基本設定

自動処理をするために、まずは Google Analytics Data API(GA4) および Search Console API のデータを Python から取得できるように設定します。

やることリスト:

  • Google Cloud Platform(GCP)でプロジェクトを作成
  • Google Analytics Data API および Search Console API を有効化
  • サービスアカウント作成+JSONキー取得 → 取得したJSONファイルをGoogle Driveにアップして、JSONファイルのパスとファイル名をスクリプトに記述
  • GA4のProperty IDGSCのURL をスクリプトに記述

※一度設定すれば、以降はJSONキーを使って何度でも自動取得できます。

4. 自動生成するもの(Colabスクリプト)

今回のColabスクリプトでは、以下の4種類のPNG画像とCSVファイルをまとめて出力できます:

GA4(日別セッション推移グラフ)

  • 2025年と2024年のセッション数を比較する年比較折れ線グラフ
  • 横軸は日付、縦軸はセッション数

GA4(ランディングページ別の表)

  • 上位20ページ((not set) の行は自動で除外)を表形式でPNG出力
  • 表はそのままスライドに貼り付け可能なレイアウト

GSC(日別 クリック数+表示回数グラフ)

  • クリック数:2025年・2024年の年比較折れ線グラフ
  • 表示回数:色分けされた折れ線で重ねて表示

GSC(クエリ×ランディングページ表)

  • 上位20行の組み合わせをPNG画像化

すべての画像とCSVファイルは Google Drive の指定フォルダに自動アップされ、共有設定も「リンクを知っている全員が閲覧可能」に自動化されます。

5. GASでスライドに貼り付け

生成されたPNG画像(+変数データ)は、GPTに渡してGASスクリプトを出力させます。

このGASスクリプトは:

  • Googleスライドのテンプレートを複製
  • テンプレートファイル内の {{summary}} などの変数を置換
  • 指定ページに画像を縦横比そのままで挿入
  • 必要に応じて生成した画像の上下余白や縦横比を微調整

画像貼り付けで工夫した点:

  • 最初の一回だけは位置調整が必要(画像の高さや余白を見ながら)
  • 一度決めた位置・サイズは再利用可能
  • 以降は毎月「generateFullReport()」の実行(1クリック!)のみでOK

6. 実際の成果物はこちら

今回は、以下のようなスライドが完成しました:

▶︎ 自動生成されたレポート(Googleスライド)

  • 表紙
  • サマリ+所感(GPTが自動生成)
  • GA4グラフ+表(Drive画像)
  • GSCグラフ+表(Drive画像)
  • 今後の打ち手(GPT提案ベース)

これで、数字系のパーツはすべて自動化できたことになります。
また分析コメントも、GPTがそこそこいい感じの叩き台を書いてくれるので、あとはリライトすればレポート完成です。

7. これからは “書く” ことに集中できる

  • GA4/GSCのAPIでデータを自動取得
  • Pythonでグラフ・表・分析用CSVを一括出力
  • GPTに渡すと、スライドが自動生成される
  • 分析コメントの叩き台まで書いてくれる

つまり:

もうレポートは「分析コメントをリライトするだけ」でいい

という状態が現実のものになってきました。

まだ多少の手動作業(初期設定や画像URL貼り付け)は残るものの、
「毎月同じことを繰り返していた作業」からは完全に脱却できそうです。

8. 補足:画像出力時の注意点

Pythonから自動生成される画像には、画像の周囲に不要な白い余白ができてしまうことがあります。
そこで、Pythonの “Pillow” ライブラリの “crop()” メソッドという機能を使って、白背景との差分を検出し、画像の余白を自動でトリミングする処理も組み込みました。
ImageChops.difference() + getbbox()crop() の流れ)。

※今回のサンプルでは、表画像に対して余白削除の処理をしています。

調整ポイント:

  • グラフの縦横比はColab側で一度固定する(例:figsize=(8, 4.5) など)
  • japanize_matplotlib を使っても、環境によっては日本語が崩れることがある(→フォント明示推奨)
  • PNGに変換する際は、余白の自動除去(bbox_inches=”tight”)を指定しておくと、貼り付け時のトリミングが不要になる

拡張:今後の改善アイディア(実現性やコストについては未検証ですが…)

  • GAS側からPythonスクリプトを直接実行する仕組み(WebhookやApps ScriptのUrlFetchApp等を活用)
  • GPTへのCSV渡しもGASで完結させる(Google Drive APIを活用してファイル読み取り→OpenAI API送信)
  • Looker Studioからのキャプチャ自動化(API or Puppeteerを併用)

9. まとめ:もう手動には戻れない

この一連の仕組みを導入してから、レポート作成にかかる時間は3分の1以下になりました。

  • データ取得:Python
  • 可視化:matplotlib
  • 出力反映:GAS+GPT

という三段構成によって、「人間は意味を整えるだけ」という状態に近づきつつあります。

レポートの “作業” に時間をかける時代は、終わりに近づいている。

今後は「どう作るか」ではなく「どんな視点で見るか」「どのように改善していくか」に、より多くの時間とエネルギーを使い、勝負していく時代になりそうです。

10. 参考リンク集

付録:今回使用したスクリプト(Python/GAS)

Pythonスクリプトの使い方

Pythonスクリプトは、Google Colabでの使用を前提として作成したものです。他の環境での動作は保証できませんのでご了承ください。

また、下記のブロックで「# ←差し替え必須」のコメントがついている行は、各自の環境にあわせて書き換えてください。

# ========== 設定 ==========
OUTPUT_DIR = "/content/drive/MyDrive/Report"
KEY_PATH = "/content/drive/MyDrive/content/your-service-account.json"  # ←差し替え必須
PROPERTY_ID = "your-GA4-property-ID"  # ←差し替え必須
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
SITE_URL = "https://your-site.com/"  # ←差し替え必須

また、測定期間および比較期間は下記のブロックで設定してください。

START_DATE = "2025-03-01"
END_DATE = "2025-03-31"
START_DATE_YOY = "2024-03-01"
END_DATE_YOY = "2024-03-31"

Python:GA4・GSCグラフ/表画像・CSV自動生成スクリプト(Colab用)

# Colab用:GA4/GSCの前年同月データを含むグラフ・CSV・表画像出力スクリプト(ファイル名にYYMM付き)

!pip install --upgrade google-analytics-data google-auth google-api-python-client matplotlib pandas seaborn japanize-matplotlib
!pip install pillow
from PIL import Image, ImageChops

import os
from google.colab import drive

# ========== Driveマウント(初回のみ必要。再実行時は自動スキップ) ==========
if not os.path.exists("/content/drive/MyDrive"):
    drive.mount("/content/drive")
    assert os.path.exists("/content/drive/MyDrive"), "Driveマウントが失敗しています"
else:
    print("Driveは既にマウント済みです")

# ========== 設定 ==========
OUTPUT_DIR = "/content/drive/MyDrive/Report"
KEY_PATH = "/content/drive/MyDrive/content/your-service-account.json"  # ←差し替え必須
PROPERTY_ID = "your-GA4-property-ID"  # ←差し替え必須
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
SITE_URL = "https://your-site.com/"  # ←差し替え必須

import datetime
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
from urllib.parse import urlparse
from google.oauth2 import service_account
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import DateRange, Metric, Dimension, RunReportRequest
from googleapiclient.discovery import build

START_DATE = "2025-03-01"
END_DATE = "2025-03-31"
START_DATE_YOY = "2024-03-01"
END_DATE_YOY = "2024-03-31"

yyyymm = datetime.datetime.strptime(START_DATE, "%Y-%m-%d").strftime("%y%m")
os.makedirs(OUTPUT_DIR, exist_ok=True)

plt.rcParams['font.family'] = 'IPAexGothic'

# ========== 認証 ==========
credentials = service_account.Credentials.from_service_account_file(KEY_PATH)
client = BetaAnalyticsDataClient(credentials=credentials)
webmasters_service = build("searchconsole", "v1", credentials=credentials.with_scopes(SCOPES))

# ========== 画像の余白削除 ==========
def trim_white_margin(img_path):
    img = Image.open(img_path).convert("RGB")
    bg = Image.new("RGB", img.size, (255, 255, 255))
    diff = ImageChops.difference(img, bg)
    bbox = diff.getbbox()
    if bbox:
        img.crop(bbox).save(img_path)  # 上書き保存

# ========== 表用補助関数 ==========
def strip_domain(url):
    if "?fbclid=" in url:
        return url.split("?fbclid=")[0] + "?fbclid="
    parsed = urlparse(url)
    return parsed.path if parsed.scheme else url

# ========== GA4 日次セッション取得・グラフ描画 ==========
def fetch_ga4_sessions(start_date, end_date):
    request = RunReportRequest(
        property=f"properties/{PROPERTY_ID}",
        dimensions=[Dimension(name="date")],
        metrics=[Metric(name="sessions")],
        date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
    )
    response = client.run_report(request)
    return pd.DataFrame([
        {"date": row.dimension_values[0].value, "sessions": int(row.metric_values[0].value)}
        for row in response.rows
    ])

ga4_df = fetch_ga4_sessions(START_DATE, END_DATE).sort_values('date')
ga4_yoy_df = fetch_ga4_sessions(START_DATE_YOY, END_DATE_YOY).sort_values('date')

ga4_df['day'] = pd.to_datetime(ga4_df['date']).dt.strftime('%m-%d')
ga4_yoy_df['day'] = pd.to_datetime(ga4_yoy_df['date']).dt.strftime('%m-%d')
day_order = sorted(set(ga4_df['day']) | set(ga4_yoy_df['day']))
ga4_df = ga4_df.set_index('day').reindex(day_order).fillna(0)
ga4_yoy_df = ga4_yoy_df.set_index('day').reindex(day_order).fillna(0)

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(day_order, ga4_df['sessions'], label="2025", color="blue", linewidth=2)
ax.plot(day_order, ga4_yoy_df['sessions'], label="2024", color="slategray", linestyle="dashed", linewidth=1.5)
ax.set_title("GA4 日次セッション(前年同月比較)")
ax.set_xticks(day_order[::2])
ax.set_xticklabels(day_order[::2], rotation=45)
ax.legend()
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/ga4_sessions_yoy_{yyyymm}.png")
plt.close()

ga4_merged_df = pd.DataFrame({
    'date': [f"2025-{d}" for d in day_order],
    'sessions_2025': ga4_df['sessions'].values,
    'sessions_2024': ga4_yoy_df['sessions'].values
})
ga4_merged_df.to_csv(f"{OUTPUT_DIR}/ga4_daily_sessions_{yyyymm}.csv", index=False)

# ========== GSC 日次クリック・表示取得・グラフ描画 ==========
def fetch_gsc_metric(start_date, end_date):
    response = webmasters_service.searchanalytics().query(
        siteUrl=SITE_URL,
        body={
            "startDate": start_date,
            "endDate": end_date,
            "dimensions": ["date"],
            "rowLimit": 1000
        }
    ).execute()
    return pd.DataFrame([
        {"date": row["keys"][0], "clicks": row["clicks"], "impressions": row["impressions"]}
        for row in response.get("rows", [])
    ])

gsc_df = fetch_gsc_metric(START_DATE, END_DATE)
gsc_yoy_df = fetch_gsc_metric(START_DATE_YOY, END_DATE_YOY)
gsc_df['day'] = pd.to_datetime(gsc_df['date']).dt.strftime('%m-%d')
gsc_yoy_df['day'] = pd.to_datetime(gsc_yoy_df['date']).dt.strftime('%m-%d')
gsc_df = gsc_df.groupby('day')[['clicks', 'impressions']].sum().reset_index()
gsc_yoy_df = gsc_yoy_df.groupby('day')[['clicks', 'impressions']].sum().reset_index()
gsc_df = gsc_df.set_index('day').reindex(day_order).fillna(0)
gsc_yoy_df = gsc_yoy_df.set_index('day').reindex(day_order).fillna(0)

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(day_order, gsc_df['clicks'], label="Clicks 2025", color='blue', linewidth=2)
ax.plot(day_order, gsc_yoy_df['clicks'], label="Clicks 2024", color='slategray', linestyle='dashed', linewidth=1.5)
ax.plot(day_order, gsc_df['impressions'], label="Impressions 2025", color='darkorange', linewidth=2)
ax.plot(day_order, gsc_yoy_df['impressions'], label="Impressions 2024", color='saddlebrown', linestyle='dashed', linewidth=1.5)
ax.set_title("GSC 日次クリック・表示回数(前年同月比較)")
ax.set_xticks(day_order[::2])
ax.set_xticklabels(day_order[::2], rotation=45)
ax.legend()
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/gsc_clicks_impressions_yoy_{yyyymm}.png")
plt.close()

gsc_merged_df = pd.DataFrame({
    'date': [f"2025-{d}" for d in day_order],
    'clicks_2025': gsc_df['clicks'].values,
    'impressions_2025': gsc_df['impressions'].values,
    'clicks_2024': gsc_yoy_df['clicks'].values,
    'impressions_2024': gsc_yoy_df['impressions'].values
})
gsc_merged_df.to_csv(f"{OUTPUT_DIR}/gsc_daily_clicks_{yyyymm}.csv", index=False)

# ========== 表画像:GA4 LPテーブル ==========
def fetch_ga4_lp_table(start_date, end_date):
    request = RunReportRequest(
        property=f"properties/{PROPERTY_ID}",
        dimensions=[Dimension(name="landingPagePlusQueryString")],
        metrics=[
            Metric(name="sessions"),
            Metric(name="activeUsers"),
            Metric(name="newUsers"),
            Metric(name="engagementRate")
        ],
        date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
        limit=20
    )
    response = client.run_report(request)
    base_rows = [
        [
            i + 1,
            strip_domain(row.dimension_values[0].value),
            int(row.metric_values[0].value),
            int(row.metric_values[1].value),
            int(row.metric_values[2].value),
            f"{float(row.metric_values[3].value) * 100:.1f}%"
        ] for i, row in enumerate(response.rows)
    ]
    df = pd.DataFrame(base_rows, columns=["#", "LP", "セッション数", "アクティブユーザー", "新規ユーザー", "エンゲージメント率"])
    return df

ga4_table = fetch_ga4_lp_table(START_DATE, END_DATE)
fig, ax = plt.subplots(figsize=(10, 0.5 + len(ga4_table) * 0.35))
ax.axis('off')
table = ax.table(cellText=ga4_table.values,
                 colLabels=ga4_table.columns,
                 cellLoc='center',
                 loc='center',
                 colWidths=[0.03, 0.45, 0.09, 0.12, 0.09, 0.12])
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 1.15)
for col in range(len(ga4_table.columns)):
    table[0, col].set_facecolor('#DDDDDD')
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/ga4_lp_table_{yyyymm}.png")
plt.close()
trim_white_margin(f"{OUTPUT_DIR}/ga4_lp_table_{yyyymm}.png")
ga4_table.to_csv(f"{OUTPUT_DIR}/GA4-seo_lp_table_{yyyymm}.csv", index=False)

# ========== 表画像:GSC クエリテーブル ==========
def fetch_gsc_query_table(start_date, end_date):
    response = webmasters_service.searchanalytics().query(
        siteUrl=SITE_URL,
        body={
            "startDate": start_date,
            "endDate": end_date,
            "dimensions": ["page", "query"],
            "rowLimit": 20
        }
    ).execute()
    rows = [
        [
            i + 1,
            strip_domain(row["keys"][0]),
            row["keys"][1],
            row["clicks"],
            row["impressions"],
            f"{float(row['position']):.1f}",
            f"{(row['clicks'] / row['impressions'] * 100):.2f}%" if row['impressions'] > 0 else "-"
        ] for i, row in enumerate(response.get("rows", []))
    ]
    df = pd.DataFrame(rows, columns=["#", "LP", "検索クエリ", "クリック数", "表示回数", "平均掲載順位", "CTR"])
    return df

gsc_table = fetch_gsc_query_table(START_DATE, END_DATE)
fig, ax = plt.subplots(figsize=(10, 0.5 + len(gsc_table) * 0.35))
ax.axis('off')
table = ax.table(cellText=gsc_table.values,
                 colLabels=gsc_table.columns,
                 cellLoc='center',
                 loc='center',
                 colWidths=[0.03, 0.21, 0.26, 0.1, 0.1, 0.1, 0.1])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.15)
for col in range(len(gsc_table.columns)):
    table[0, col].set_facecolor('#DDDDDD')
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.png")
plt.close()
trim_white_margin(f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.png")
gsc_table.to_csv(f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.csv", index=False)

print("前年同月の比較を含むグラフ+CSV+表画像出力(YYMM付きファイル名)が完了しました。")

GPTが生成したGASスクリプト(画像・コメント入りスライド自動生成)

GoogleスライドのテンプレートとGASの使い方については、前編の記事をご覧ください。

let latestPresentationId = ""; // グローバル変数でIDを保持

// 実行関数(全部まとめて呼び出し)
function generateFullReport() {
  createMonthlyReport();
  insertImageToSummarySlide();
  insertGSCImageToSlide();
  insertGA4LandingPageTable();
  insertGSCLandingQueryTable();
  Logger.log("月次レポート:完全自動生成 完了!");
}

// 1. テンプレートから複製して変数を差し込み
function createMonthlyReport() {
  const templateId = "1n_TmPVJEGqcNdY2SBnj9CTFfF3ZCFOO_vMF9G70Fwxg";
  const newTitle = "サンプル株式会社様|2025年3月 月間レポート 自動生成テスト";

  const data = {
    "{{yearmonth}}": "2025年3月",
    "{{period}}": "2025/03/01〜2025/03/31",
    "{{summary}}": "今月はGA4・GSCの双方で安定的な指標改善が見られ、既存コンテンツの定着と新規記事の滑り出しが順調に進んでいます。
特に検索からの表示機会が拡大している点は、コンテンツ群がインデックスとして機能し始めた証左と捉えることができます。
ただし、CTRの低さやLPごとのエンゲージメント差異が見られるため、今後は質的な最適化と導線設計が求められるフェーズへと移行しつつあります。",
    "{{highlight}}": "GA4の2025年3月の月間セッション数は122件で、前年同月比では53件の増加となりました。
1日あたりの平均セッションは約4件と緩やかな成長を維持しており、中旬以降に10件前後の日が増加した点が注目されます。
特にランディングページ別では、`/` や `/gas-ga4-api-spread_sheet/` が継続して流入上位に位置しており、過去に公開した導入系コンテンツが安定的な流入源となっている状況です。",
    "{{insight}}": "ページ別データを見ると、エンゲージメント率が高いページが多数見られます。
`/clarity_ai_analytics/` や `/gas-ga4-api-spread_sheet/` などは直帰せずに一定の関与を示すユーザーが多く、記事内容が検索意図に合致していると考えられます。
また、ランディングとしての流入は少ないものの、`/python-ga4-api/` など新規コンテンツが既に検索経由で成果を出し始めている兆しも確認されました。",
    "{{gsc}}": "2025年3月のGSCデータでは、検索表示回数が1,338件、クリック数が29件、CTRは2.17%、平均掲載順位は45.4位となりました。
表示機会は増えている一方で、CTRは低水準にとどまっており、検索結果に表示はされるがクリックに至らないケースが散見されます。
ただし、「ga4 python」や「パワーディレクター 重い」など特定の意図が明確なクエリに対してはCTRが5%以上と高く、ニッチな需要には的確に応答できている兆しが見えます。",
    "{{action}}": "表示はされているもののクリックに至らないページに対しては、検索意図により近づけるタイトル設計・構成改善が必要です。
また、CTRが高かったキーワードに類似するテーマを基にしたトピック展開や内部リンク強化により、SEO全体の底上げを図ることができます。
新規流入が見込まれる記事は早期にタグ設定やGTM連携も進め、行動データの取得環境を整備することが次の施策判断に繋がります。"
  };

  const newPresentation = DriveApp.getFileById(templateId).makeCopy(newTitle);
  latestPresentationId = newPresentation.getId();
  const presentation = SlidesApp.openById(latestPresentationId);
  const slides = presentation.getSlides();

  slides.forEach(slide => {
    for (const key in data) {
      slide.replaceAllText(key, data[key]);
    }
  });

  Logger.log("スライド生成完了: https://docs.google.com/presentation/d/" + latestPresentationId + "/edit");
  return latestPresentationId;
}

// 共通関数:画像を比率維持して挿入
function insertImagePreserveAspect(slide, imageUrl, width, top, left) {
  const image = slide.insertImage(imageUrl);
  const aspectRatio = image.getHeight() / image.getWidth();
  image.setWidth(width);
  image.setHeight(width * aspectRatio);
  image.setTop(top);
  image.setLeft(left);
}

// GA4セッション推移グラフ(スライド4)
function insertImageToSummarySlide() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=191Al8RmTtxJhuRQ5ZDx-0Khd27WU_12R";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[3];
  insertImagePreserveAspect(slide, imageUrl, 600, 100, 50);
}

// GSCクリック・表示グラフ(スライド8)
function insertGSCImageToSlide() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=197kfZLt9YGqtDODGP2sYRyLIPTccJy37";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[7];
  insertImagePreserveAspect(slide, imageUrl, 600, 100, 50);
}

// GA4 LP表(スライド5)
function insertGA4LandingPageTable() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=18d0fNSP9JoChja7PbRBrLEet3ssc3h5s";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[4];
  insertImagePreserveAspect(slide, imageUrl, 550, 80, 75);
}

// GSC クエリ表(スライド9)
function insertGSCLandingQueryTable() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=18nW_BzeHSw7sRgYyvlhyHnhI4LMuD_x2";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[8];
  insertImagePreserveAspect(slide, imageUrl, 550, 80, 75);
}