GEMINI LABEN
API — temperature・top_p・top_k の非推奨は、エラーではなく無言の no-op です。リクエストは 200 で通り、値だけが受け取られたうえで無視されますAUDIT — 実行時に検知する手段がないため、コードベースを静的に検索して指定を洗い出すしかありません。監査の方法が開発者フォーラムで議論になっていますCHECK — 同じプロンプトを temperature 0 と 1 で複数回投げ、出力の分散が変わらないことを実測すれば、効いていないことを自分の目で確認できますMODELS — Gemini 3.7 Flash は DeepSWE v1.1 で 65.3%、FrontierCode 1.1 Main で 43.6% を記録しています。導入価格は2026年12月31日までですSEARCH — File Search が gemini-embedding-2 によるマルチモーダル検索に対応しました。画像を書き起こさずに、そのまま埋め込んで検索できますDEPRECATION — gemini-robotics-er-1.6-preview が8月31日に停止します。残り4日で、移行先は7月30日から公開プレビューの ER 2 系ですAPI — temperature・top_p・top_k の非推奨は、エラーではなく無言の no-op です。リクエストは 200 で通り、値だけが受け取られたうえで無視されますAUDIT — 実行時に検知する手段がないため、コードベースを静的に検索して指定を洗い出すしかありません。監査の方法が開発者フォーラムで議論になっていますCHECK — 同じプロンプトを temperature 0 と 1 で複数回投げ、出力の分散が変わらないことを実測すれば、効いていないことを自分の目で確認できますMODELS — Gemini 3.7 Flash は DeepSWE v1.1 で 65.3%、FrontierCode 1.1 Main で 43.6% を記録しています。導入価格は2026年12月31日までですSEARCH — File Search が gemini-embedding-2 によるマルチモーダル検索に対応しました。画像を書き起こさずに、そのまま埋め込んで検索できますDEPRECATION — gemini-robotics-er-1.6-preview が8月31日に停止します。残り4日で、移行先は7月30日から公開プレビューの ER 2 系です
記事一覧/API / SDK
API / SDK/2026-08-27中級

Gemini に渡す前に表が壊れる場所は結合セルと2行ヘッダ

スプレッドシートの表をそのまま Gemini に渡すと、結合セルと2行ヘッダで行の所属が失われます。抽出時点で何が消えるかを実測し、平坦化と集計行の検出までを Python で組みます。

Gemini API221スプレッドシート4データ前処理Python44構造化出力16

月次の売上シートを Gemini に渡して、カテゴリごとの傾向を書き出してもらったことがあります。返ってきた文章は流暢でしたが、カテゴリの割り振りが実際の表とずれていました。

最初はプロンプトを疑いました。指示を細かくしても、モデルを変えても、ずれ方が変わるだけで直りません。おかしいと思って、渡していた文字列そのものを目で追ったところ、原因はモデルより手前にありました。表は、Gemini に届く前の抽出の時点で、すでに壊れていました。

個人開発でスプレッドシートを扱っていると、この形の表は嫌になるほど出てきます。人が読む前提で作られた表だからです。ここでは、その表が抽出時に何を失うかを実際に数えて、失わない形へ直すところまでを Python で組みます。

抽出した時点で、行の半分が所属を失っています

題材として、手元の売上集計に近い形の表を用意しました。カテゴリ列は同じ値が続く範囲を縦に結合してあり、月ごとの見出しが1行目、その下に販売数と売上が並ぶ2段構成です。表の途中に区切りの空行があり、最後に合計行が付いています。よくある形だと思います。

これを openpyxl でそのまま読み出し、CSV に落とすと、こうなりました。

カテゴリ,商品,2026年7月,,2026年8月,
,,販売数,売上,販売数,売上
壁紙,四季の壁紙,1200,84000,1310,91700
,浮世絵壁紙,940,65800,1005,70350
,夜景壁紙,610,42700,588,41160
癒し,焚き火の音,430,30100,402,28140
,波の音,380,26600,411,28770
,,,,,
ツール,単位変換,220,15400,198,13860
,方位磁針,160,11200,175,12250
合計,,3940,275800,4089,286230

同じスクリプトの中で数えた結果が次のとおりです。

指標素の抽出
データ行8 行
カテゴリが空になった行4 行(データ行の 50%)
同名の列見出し販売数 ×2 / 売上 ×2
ヘッダが占める行数2 行

結合セルの値は、範囲の左上のセルにしか入っていません。残りのセルは空です。画面では「壁紙」が3行分に見えていても、読み出せば3行のうち2行は空になります。浮世絵壁紙も夜景壁紙も、どのカテゴリのものか分からない行として渡っていました。

見出しも同じです。1行目の「2026年7月」は C 列にしかなく、D 列は空。2行目には販売数と売上が2組ずつ並びます。この2行を別々の行として渡すと、販売数という列名が2つある表になります。どちらが7月かは、モデルの側では判断できません。

Docs の表でも似たことが起きますが、崩れ方も直し方も違います。そちらは表を渡したはずが、行と列の対応が消えていたで扱っています。

結合セルは、読み出した直後に展開しておきます

直し方はそれほど難しくありません。結合範囲の情報はファイルに残っているので、読み出した直後に、範囲内の全セルへ左上の値を配ります。

from openpyxl import load_workbook
 
def expand_merges(ws):
    """結合範囲の全セルに左上の値を配り、素の二次元配列にして返す"""
    grid = [[c.value for c in row] for row in ws.iter_rows()]
    for rng in ws.merged_cells.ranges:
        top = grid[rng.min_row - 1][rng.min_col - 1]
        for r in range(rng.min_row - 1, rng.max_row):
            for c in range(rng.min_col - 1, rng.max_col):
                grid[r][c] = top
    return grid
 
ws = load_workbook("sales.xlsx").active
grid = expand_merges(ws)

ws.merged_cells.ranges は縦の結合も横の結合も同じ形で返してきます。カテゴリ列の縦結合と、月見出しの横結合を、同じループで処理できるのはここが理由です。

この段階で grid を眺めると、カテゴリが空だった4行に値が入っています。ただし見出しはまだ2行のままです。

2行ヘッダは、列ごとに縦へ連結して1行に畳みます

見出しを1行にするときに悩むのが、上下で同じ語が繰り返される場合です。B 列は「商品」が縦に結合されているため、展開後は上下とも「商品」になります。これを機械的に連結すると「商品 / 商品」という列名ができてしまいます。

重複を落としてから連結し、それでも同名になった列には連番を振る、という順序にしました。

def flatten_header(grid, depth):
    """先頭 depth 行を1行のヘッダへ畳み、残りを本体として返す"""
    head = grid[:depth]
    names, seen = [], {}
    for col in range(len(grid[0])):
        parts = [str(head[r][col]).strip() for r in range(depth)
                 if head[r][col] not in (None, "")]
        uniq = []
        for p in parts:
            if p not in uniq:
                uniq.append(p)
        name = " / ".join(uniq) or f"col{col + 1}"
        seen[name] = seen.get(name, 0) + 1
        if seen[name] > 1:
            name = f"{name}#{seen[name]}"
        names.append(name)
    return names, grid[depth:]
 
def drop_blank(rows):
    return [r for r in rows if any(v not in (None, "") for v in r)]

depth を引数にしているのは、3段の見出しを持つ表が実際にあるからです。私は自動判定を一度試して、やめました。データの1行目が文字列だと見出しと区別がつかず、静かに1行食われる事故のほうが痛かったためです。表ごとに 2 なり 3 なりを明示して渡すほうが、後から読んでも意味が通ります。

ここまでを通した結果が次の CSV です。

カテゴリ,商品,2026年7月 / 販売数,2026年7月 / 売上,2026年8月 / 販売数,2026年8月 / 売上
壁紙,四季の壁紙,1200,84000,1310,91700
壁紙,浮世絵壁紙,940,65800,1005,70350
壁紙,夜景壁紙,610,42700,588,41160
癒し,焚き火の音,430,30100,402,28140
癒し,波の音,380,26600,411,28770
ツール,単位変換,220,15400,198,13860
ツール,方位磁針,160,11200,175,12250
合計,,3940,275800,4089,286230

カテゴリの空欄は 4 行から 0 行へ、同名の列見出しは 2 組から 0 組になりました。空行も落ちています。

合計行を残したまま渡すと、数字がそのまま倍になります

平坦化が済んで安心したところに、まだ問題が残っていました。最終行の合計です。

整形後の表で、7月の販売数の列を素直に足すとこうなります。

足し方7月の販売数の合計
全行をそのまま足す7,880
合計行を除いて足す3,940

ちょうど倍です。合計行が、他の行と同じ見た目のデータ行として並んでいるためです。人が見れば「合計」という文字で分かりますが、その語は日本語版のシートでしか使われません。英語のシートなら Total、私の手元には小計だけ入った表もあります。文字列で判定する方法は、いずれ取りこぼします。

数値の関係で見るほうが確実でした。ある行のある列の値が、同じ列の他の全行の和と一致するなら、その行は集計行の可能性が高いという判定です。

def looks_like_total(rows, col):
    """列 col について、他の全行の和と一致する行の index を返す"""
    hits = []
    column = [r[col] for r in rows]
    for i, r in enumerate(rows):
        others = sum(x for j, x in enumerate(column) if j != i)
        if r[col] == others:
            hits.append(i)
    return hits
 
hits = looks_like_total(body, 2)   # 2 = 2026年7月 / 販売数
clean = [r for i, r in enumerate(body) if i not in hits]

この表に当てると hits は最終行だけを拾い、除外後の合計は 3,940 になりました。判定に使う列は、数値が入っていて欠損の少ない列を1つ選べば十分です。複数の列で一致を要求すると、行数の少ない表で取りこぼしが増えました。

なお、この判定は「全体の1行だけが他の和と一致する」場面を想定しています。カテゴリごとの小計が各ブロックの末尾に入る表では、小計が拾えず、総合計が小計の分まで含んだ和と一致しなくなります。そういう表は、集計列を持たない生データの形でエクスポートし直すほうが早いと考えています。

整形しても、文字数は減りません

ここが、やる前の予想と逆だった点です。無駄な空欄が埋まって空行が消えるのだから、渡す文字列は短くなるだろうと思っていました。実際に数えると、こうでした。

形式文字数
素の抽出(CSV)281
平坦化後(CSV)302

21 文字、率にして 7.5% 増えています。カテゴリを埋め直した分と、列名に月を足した分が、空行と空欄を削った分を上回りました。

つまりこの前処理は、入力を軽くするための工夫ではありません。増えた 21 文字は、8 行のうち 4 行に所属を返し、2 組の同名列を区別するために払っています。この交換は、私自身の用途では毎回引き合いました。逆に言えば、行数が数万に及ぶ表でこれをそのまま貼るのは、量の面で無理があります。その規模なら、要約させる前に集計を済ませてから渡すほうが素直です。

整形済みの表を渡す

前処理が済んだら、渡し方は素直で構いません。整形後の CSV を本文に入れ、構造化出力で受け取る形にしています。

from google import genai
from google.genai import types
from pydantic import BaseModel
 
class RowInsight(BaseModel):
    category: str
    product: str
    delta_units: int
    note: str
 
client = genai.Client(api_key="YOUR_API_KEY")
 
config = types.GenerateContentConfig(
    system_instruction=(
        "入力は1行目がヘッダの CSV です。"
        "列名の / の左は対象月を表します。合計行は除外済みです。"
    ),
    response_mime_type="application/json",
    response_schema=list[RowInsight],
)
 
response = client.models.generate_content(
    model="gemini-3.7-flash",
    contents=f"次の表から、前月比の増減が大きい行を抜き出してください。\n\n{csv_text}",
    config=config,
)
rows = response.parsed

system_instruction に「合計行は除外済みです」と書いているのは、モデルへの説明であると同時に、自分への注記でもあります。この一文と実際の前処理がずれたときに気づけるように、同じ言葉で残しています。

サンプリング系のパラメータ(temperature など)は 2026 年 8 月に非推奨となったため、この構成には入れていません。手元で config.model_dump(exclude_none=True) を確認すると、送出されるキーは response_mime_typeresponse_schemasystem_instruction の3つだけになります。実際に何が送られるかを目で見ておくと、後から設定を足したときの差分が追いやすくなります。

前処理と API 呼び出しを一本のパイプラインにまとめる話は、Google Sheets API × Gemini API でつくるデータ処理パイプラインのほうが詳しいです。Sheets 側の機能をどこまで使い、どこから自前のコードに残すかという線引きについては、Sheets canvas へ移せるのは入口までで、実行境界は Apps Script に残りますで判断の材料を整理しています。

次にやること

お使いのシートを1枚選び、expand_merges を通す前と後で、キーになる列の空欄が何行あるかだけ数えてみてください。0 のままなら前処理は要りません。1 行でも減るなら、その行数がそのまま、これまでモデルに判断させていた曖昧さの量です。

シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API / SDK2026-07-09
Google Sheets API × Gemini API でつくるデータ処理パイプライン — Apps Script 不要の Python 自動化
サービスアカウントを使った Google Sheets API の認証から、Gemini API でのデータ分析・分類・要約まで、Python だけで完結するスプレッドシート AI 自動化パイプラインを解説します。
API / SDK2026-05-04
Gemini の Function Calling で構造化出力を実装する — 複数ツール連携の設計パターン
Gemini API の Function Calling で信頼性の高い構造化出力を実装する設計パターンを紹介します。JSON モードとの違い、ツール定義を書くときのコツ、複数ツールの組み合わせ方、エラーと不確実性への対処を実プロジェクトの活用例とともに解説します。
API / SDK2026-08-23
Gemini TTS を streamGenerateContent で受け取り、先頭のチャンクから鳴らす
Gemini の TTS をストリーミングで受け取ると、音声は「ファイル」ではなく PCM の断片で届きます。チャンクごとに WAV ヘッダを付けた場合に何が起きるかを手元で測り、連結とヘッダの正しい順序を実装まで示します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →