この記事でわかること

  • 集計表の自動化にグラフを足すときに、新しく決めること
  • Claude Codeに調査・仕様・コード・検証の順で頼む指示文
  • 棒グラフと折れ線グラフを作るPythonのコードと、範囲の決め方
  • 作ったグラフが正しい範囲を参照しているかを、コードで確かめる方法

この記事はExcelの定型レポートを自動化する手順の続きです。CSVの読み込みや表の書式は、そちらで説明しました。

表の自動化にグラフを足すと何が変わるか

グラフを足すと、決めることが3つ増えます。

決めること 例 間違えると
範囲 店舗の行だけ(合計を除く) 合計の棒が1本だけ飛び抜ける
種類 比較は棒、推移は折れ線 伝えたいことが読み取れない
置く場所 表の右の空いたセル 表に重なって数字が隠れる

openpyxlのグラフはセル範囲への参照でできています。範囲を間違えると、表が正しくてもグラフは誤ります。たとえば、範囲に合計の行が入るとグラフが誤ります。逆に、表の値が誤っていればグラフも誤るので、表の数値を確かめてからグラフを足します。

例にする業務:店舗別売上の集計表に棒グラフと折れ線グラフを足す

例として、架空の売上データ(CSV)から、次の2つを毎月作る業務を自動化します。

  • 「店舗別」シート:店舗ごとの売上と合計の行、店舗を比べる縦棒グラフ
  • 「日別」シート:日付ごとの店舗別の売上、日別の推移は折れ線グラフにします

元データの列は、日付・店舗・商品区分・数量・売上金額の5つです。店舗は架空のA店・B店・C店で、月によって日数も店舗の数も変わる前提にします。

筆者も以前、既存のExcelを参考に、帳票やグラフを自動で作れないかに取り組んだことがあります。そのときも一度で完成させたのではなく、出力を確かめて直すことを重ねて形にしました。

Claude Codeと進める4つのステップ

調べる・決める・作る・確かめるの4つに分け、段階ごとに結果を読んでから次へ進みます。

STEP 1 今のグラフを調べてもらう

手で作っている今のレポートに、どんなグラフがあるかを調べてもらいます。この段階では何も作らせません。

monthly_report.xlsx のグラフを調べてください。ファイルの作成や変更はまだしないでください。
知りたいこと: グラフの数と種類、それぞれが参照しているセル範囲、系列の名前、置いてある位置。
読み取りに使う方法を先に示し、私が了解してから実行してください。

STEP 2 グラフの仕様を決める

調べた結果から、グラフごとの仕様を表にしてもらい、人が範囲を確定させます。

調べた結果をもとに、グラフの仕様を表にまとめてください。コードはまだ不要です。
グラフごとに、種類、値の範囲、項目の範囲、合計の行を含めないこと、タイトル、置くセルを書いてください。
範囲の最後の行は、データの行数から計算して決める前提にしてください。

STEP 3 コードを書いてもらう

仕様に合意してから、コードを書いてもらいます。

合意した仕様で、sales.csv から集計表と2つのグラフを chart_report.xlsx に作るPythonのコードを書いてください。
ライブラリは csv と openpyxl に限ります。同じ名前のファイルがあれば保存せずに止まるようにしてください。
実行の前に、作るファイルの名前と内容を報告し、私の返事を待ってください。

STEP 4 グラフの範囲を確かめてもらう

グラフが表示されたことと、正しい範囲を指していることは別です。確かめるコードも書いてもらいます。

chart_report.xlsx のグラフを確かめるコードを書いてください。ファイルは書き換えないでください。
xlsx の中のグラフの定義を読み、グラフの数、種類、参照範囲を、sales.csv の店舗の数と日数から計算した範囲と比べてください。
一致しなかったものは、期待した範囲と実際の範囲を並べて報告してください。

コードの中身:参照範囲・グラフの種類・置く場所

以下は、記事の作成時に架空のデータで実行して確かめたコードです。集計・表・グラフの3つを続けて make_chart_report.py として保存します。

CSVを店舗ごと・日付ごとに集計します。

from collections import defaultdict
import csv

SRC = "sales.csv"
by_shop = defaultdict(int)
by_day = defaultdict(lambda: defaultdict(int))
with open(SRC, newline="", encoding="utf-8") as src:
    for rec in csv.DictReader(src):
        yen = int(rec["売上金額"])
        by_shop[rec["店舗"]] += yen
        by_day[rec["日付"]][rec["店舗"]] += yen
shops = sorted(by_shop)
days = sorted(by_day)

集計した値を2つのシートに書き込みます。店舗別のシートの最後には合計の行を置きます。

import openpyxl

wb = openpyxl.Workbook()
ws = wb.worksheets[0]
ws.title = "店舗別"
ws.append(["店舗", "売上金額"])
for shop in shops:
    ws.append([shop, by_shop[shop]])
ws.append(["合計", sum(by_shop.values())])

wd = wb.create_sheet("日別")
wd.append(["日付"] + shops)
for day in days:
    wd.append([day] + [by_day[day][shop] for shop in shops])

続けて、グラフを作って置き、保存します。

from pathlib import Path
from openpyxl.chart import BarChart, LineChart, Reference

last = 1 + len(shops)  # 見出しの行 + 店舗の数。合計の行は含めない
bar = BarChart()
bar.type = "col"
bar.title = "店舗別の売上"
bar.add_data(Reference(ws, min_col=2, min_row=1, max_row=last), titles_from_data=True)
bar.set_categories(Reference(ws, min_col=1, min_row=2, max_row=last))
ws.add_chart(bar, "D2")

line = LineChart()
line.title = "日別の売上の推移"
line.add_data(Reference(wd, min_col=2, max_col=1 + len(shops), min_row=1, max_row=1 + len(days)),
              titles_from_data=True)
line.set_categories(Reference(wd, min_col=1, min_row=2, max_row=1 + len(days)))
wd.add_chart(line, "F2")

out = Path("chart_report.xlsx")
if out.exists():
    raise SystemExit(f"{out} は既にあります。上書きしません")
wb.save(out)
  • 範囲:値の範囲は見出しの行を含めて指定し、titles_from_data=True で系列の名前にします。項目名(店舗名・日付)の範囲は set_categories で項目名の範囲を指定します。最後の行は last で、合計の行の1つ上です。
  • 種類:type を col にすると縦棒のグラフになります。日別の推移は、各店舗を同じ日付に対して描く折れ線にしました。
  • 置く場所:add_chart の2つめの引数で、グラフの左上のセルを決めます。表と重ならない、右の空いたセルを選びます。

保存の前に同じ名前のファイルがないかを確かめています。openpyxlの保存は、同じ名前のファイルがあると警告なしで上書きします。

作ったグラフを検証する

範囲が正しいかは、コードで確かめられます。xlsxファイルの中身はzip形式で、グラフごとに定義のファイル(XML)が入っています。その中の参照範囲を読み、元のデータから計算した範囲と比べます。

import csv
import zipfile
import xml.etree.ElementTree as ET

C = "{http://schemas.openxmlformats.org/drawingml/2006/chart}"
with open("sales.csv", newline="", encoding="utf-8") as src:
    recs = list(csv.DictReader(src))
n_shops = len({rec["店舗"] for rec in recs})
n_days = len({rec["日付"] for rec in recs})

found = {}
with zipfile.ZipFile("chart_report.xlsx") as z:
    for name in z.namelist():
        if name.startswith("xl/charts/chart") and name.endswith(".xml"):
            root = ET.fromstring(z.read(name))
            kind = "棒" if root.find(f".//{C}barChart") is not None else "折れ線"
            found[kind] = [f.text for f in root.iter(f"{C}f")]

last = 1 + n_shops
assert sorted(found) == ["折れ線", "棒"]
assert f"'店舗別'!$B$2:$B${last}" in found["棒"]
assert f"'店舗別'!$A$2:$A${last}" in found["棒"]
assert f"'日別'!$A$2:$A${1 + n_days}" in found["折れ線"]
assert len(found["折れ線"]) == n_shops * 3
print("OK", found["棒"])

次の順に実行します。

python make_chart_report.py
python check_chart_report.py

サンプル(7日分・3店舗)では、棒グラフの範囲が B2:B4 になり、合計の行(5行目)は入りませんでした。わざと範囲を1行広げると、合計の行まで入れた版は、検証で止まりました。

定義のファイルのタグには名前空間が付いているため、文字列の検索ではなく、名前空間を指定してXMLとして読みます。

行数が毎月変わる場合の範囲の決め方

範囲を「B2:B4」のように手で書くと、行が増えた月は最後の行が漏れ、減った月は空の行や合計の行が入ります。範囲の最後の行はデータの行数から計算します。

上のコードでは、last = 1 + len(shops) と 1 + len(days) が、その計算です。記事の検証では、30日分・5店舗のデータに変えても、同じコードで棒グラフの範囲が B2:B6 に、折れ線の範囲が31行目までに自動で変わりました。

つまずきやすい点と判断のポイント

  • 合計の行を範囲に入れない:表の最後の行に合わせると、合計が1つの項目として描かれます。最後の行は計算で決め、検証で確かめます。
  • 見出しの行の扱い:値の範囲に見出しの行を含めずに titles_from_data=True にすると、1行目のデータが系列の名前になります。
  • 元のファイルを上書きしない:openpyxlの公式ドキュメントには、既存のファイルを開いて同じ名前で保存すると図形が失われると書かれています。元のファイルは読むだけにして、新しい名前で保存します。記事の検証では、openpyxlで作ったグラフは、開いて別の名前で保存しても残りました。Excelで作ったグラフの書式が保たれるかは確かめていません。
  • 見た目は人が確かめる:グラフの数・種類・範囲はコードで確かめられますが、色・大きさ・ラベルの重なりは確かめられません。見た目はExcelで開いて確かめます。

検証した環境と、確かめていないこと

この記事のコードは、2026年10月に作業PCのPython 3.12とopenpyxl 3.1.5で、架空のデータを使って確かめました。

一方で、Excelで開いたときの見た目(色・項目のラベル・重なり)と、Excelで作ったグラフを開いて保存したときの書式は確かめていません。この2つは、ご自身の環境で確かめてから運用してください。

Claude Codeは、ファイルを読んで編集し、コマンドも実行できるAIのコーディングツールです。始め方はClaude Codeの始め方と使い方、AIでExcel作業を自動化する全体像はExcelをAIで自動化する方法、Pythonの学び方はPythonを学ぶ方法にまとめています。

まとめ

  • グラフを足すと、参照する範囲・グラフの種類・置く場所の3つを新しく決める
  • 指示は調査 → 仕様 → コード → 検証に分け、段階ごとに結果を読む
  • 範囲の最後の行はデータの行数から計算し、合計の行を含めない
  • グラフの範囲は、xlsxの中の定義を読んでコードで確かめ、見た目はExcelで開いて確かめる

まずは毎月手で作り直しているグラフを1つ選び、STEP 1から始めてみてください。