Claude Media
PLATEAUのCityGMLから建物属性を取り出す — Claude Codeで集計する手順

PLATEAUのCityGMLから建物属性を取り出す — Claude Codeで集計する手順

PLATEAUの3D都市モデルから建物の用途・階数・建築年を取り出す手順。標準製品仕様書の属性名を根拠に、Claude Codeで集計スクリプトを書かせて検算するまで。

PLATEAUの建築物モデルは、CityGMLというXML形式の .gml ファイルで配布されています。建物1棟ごとに用途・階数・建築年といった属性が入っており、スクリプトで集計すれば「この区域の住宅は築何年が多いか」といった問いに答えられます。

Claude Codeに任せるなら、先に決めておくことは3つです。どの属性名を読むか、どこまで入っているデータなのか、結果をどう検算するか。この3つを仕様書とCLAUDE.mdで固定すると、生成されたスクリプトの当たり外れを自分で判定できます。

PLATEAUのデータはどこで手に入るか

PLATEAUの3D都市モデルは、国土交通省のオープンデータポータルサイトから入手します。データのダウンロード先は「G空間情報センター」です。ポータルは2021年度に全国56都市をオープンデータ化したところを起点とし、2025年度末には約300都市まで整備範囲を広げる計画を掲げています。

ポータルには「整備都市の属性リスト」というExcelも置かれています。各都市に含まれる地物のLOD(Level of Detail)と、属性情報の一覧です。集計に使う属性が対象都市に入っているかは、このリストか手元のファイルで先に確かめます。属性は仕様上すべて必須というわけではなく、都市によって入り方が違うためです。

ダウンロードしたフォルダの構成は、標準製品仕様書(第5.1版)が定めています。3D都市モデルは udx フォルダの直下に地物型ごとのサブフォルダで格納され、建築物は bldg です。ファイル名は [メッシュコード]_[地物型]_[CRS]_[オプション] の形で、拡張子は .gml です。つまり建物の属性を読むだけなら、udx/bldg/ 以下の .gml を対象にすれば足ります。

座標参照系は、日本測地系2011の経緯度と東京湾平均海面を基準とする標高を組み合わせた複合座標参照系で、略称は「6697」に統一されています。今回は面積や距離を計算しないので座標には触れませんが、後で地図に重ねるときに必要になる値です。

建物属性はCityGMLのどこに入っているか

標準製品仕様書の建築物(bldg:Building)の定義から、集計に使う属性を拾います。

取り出したいもの属性名型と多重度仕様書での説明
用途属性名bldg:usage型と多重度コード型[0..*]仕様書での説明主な使い道。複数あるときは主たる用途を最初に記載
建築年属性名bldg:yearOfConstruction型と多重度年[0..1]仕様書での説明建築された年
地上階数属性名bldg:storeysAboveGround型と多重度非負整数[0..1]仕様書での説明地上階の階数。地下街では使わない
地下階数属性名bldg:storeysBelowGround型と多重度非負整数[0..1]仕様書での説明地下階の階数
高さ属性名bldg:measuredHeight型と多重度長さ[0..1]仕様書での説明計測した地上最低点から最高点までの高さ。単位はm
構造種別属性名uro:buildingStructureType型と多重度コード型[0..1]仕様書での説明uro:buildingDetailAttribute の中に入る
市区町村コード属性名uro:city型と多重度コード型[1..1]仕様書での説明uro:buildingIDAttribute の中に入る。必須

読み方で気をつける点が3つあります。

1つ目は多重度です。[0..1] は「無いことがある」という意味なので、建築年や階数が空の建物は普通に存在します。集計では「不明」の行を作り、欠損を黙って捨てないことが大切です。

2つ目は用途のコードです。bldg:usage の中身は「住宅」という文字列ではなく、コードリスト(Building_usage.xml)の数値です。仕様書の表では、たとえば次のように対応します。

コード用途
401用途業務施設
402用途商業施設
403用途宿泊施設
404用途商業系複合施設
411用途住宅
412用途共同住宅
413用途店舗等併用住宅
414用途店舗等併用の共同住宅
415用途作業所併用住宅
421用途官公庁施設
422用途文教厚生施設
431用途運輸倉庫施設
441用途工場
451用途農林漁業用施設
452用途供給処理施設
453用途防衛施設
454用途その他
461用途不明

用途の区分は、都市計画の基礎調査で使う実施要領の区分に従うと仕様書にあります。店舗等併用住宅などは1/3以上が住宅のものとする、といった判定基準も書かれているので、「住宅」だけを数えるのか併用住宅まで含めるのかは、集計の前に決めておく必要があります。

3つ目は階層です。uro: で始まる拡張属性は、建物要素の直下ではなく uro:buildingDetailAttribute の中の uro:BuildingDetailAttribute に入れ子で入ります。仕様書は、建築物部分(bldg:BuildingPart)に uro:buildingDetailAttribute が書かれている場合は bldg:Building には現れない、とも定めています。大きな建物が部品に分割されているデータでは、属性が親ではなく部品側にあることがある、という意味です。

Claude Codeに渡す前の準備

CLAUDE.mdに読み方の決まりを書く

仕様書の内容を毎回会話で説明するより、プロジェクトのCLAUDE.mdに固定したほうが再現性が上がります。公式ドキュメントも、CLAUDE.mdは強制される設定ではなくコンテキストとして扱われるため、検証できる具体的な書き方が効くと説明しています。サイズは1ファイル200行未満が目安です。

# PLATEAU建物属性の集計
 
- 入力は data/bldg/*.gml(CityGML 2.0、i-UR 3.2)。ファイルは大きいので
  Readで全体を開かず、grep -c や head -c で構造だけ確認する
- 集計対象は bldg:Building のみ。bldg:BuildingPart は別集計にする
- 読む属性: bldg:usage / bldg:yearOfConstruction /
  bldg:storeysAboveGround / bldg:measuredHeight /
  uro:buildingStructureType(uro: は buildingDetailAttribute の内側)
- 属性が無い建物は「不明」として数える。0 や空欄に丸めない
- bldg:usage はコード値。コード表は docs/Building_usage.csv を使う
- スクリプトは scripts/ に置き、標準ライブラリのみで書く
- 完成条件: 集計の総数が grep -c '<bldg:Building ' の件数と一致すること

この断片は、仕様書の属性名と公式のCLAUDE.mdの書き方を組み合わせた一例です。コード表は仕様書の表をCSVに起こして docs/ に置いておくと、Claudeがコードの意味を推測せずに済みます。CLAUDE.md全般の書き方はClaude CodeのCLAUDE.mdを実用に引き上げる10のパターンにまとめています。

データ読み取りの権限を決める

集計スクリプトは、Claudeが書いてBashで実行する形になります。ここで押さえておきたいのは、Read のdenyルールが及ぶ範囲です。公式ドキュメントによると、ReadとEditのdenyルールは組み込みのファイルツールと、Bashの中の cat や head のような認識済みコマンドに効きます。一方で、Pythonスクリプトが自分でファイルを開く場合は対象外です。

つまり「生データはClaudeに読ませたくない」という運用を権限ルールだけで完全に実現することはできません。PLATEAUはCC BY 4.0で公開されたオープンデータなので隠す必要はありませんが、自治体の非公開データを混ぜるなら、サンドボックスなどOS側の制限が別途必要になります。権限ルールの設定画面はClaude Codeの/permissionsコマンドで権限ルールとauto mode拒否を管理するで扱っています。

集計スクリプトを書かせて動かす

最初のプロンプト

スクリプトをいきなり書かせず、まず構造の確認から入ります。

data/bldg/ の gml を1つ選び、bldg:Building の件数と、
usage / yearOfConstruction / storeysAboveGround / measuredHeight が
それぞれ何件に入っているかを grep で数えて。ファイル全体は開かないこと。

欠損の多い属性が見つかったら、その属性は集計の軸に向きません。この確認を飛ばすと、スクリプトは動くのに結果の大半が「不明」という状況になりかねません。

スクリプトの一例

構造がわかったら、次のような形のスクリプトを書かせます。以下は仕様書の属性名と入れ子の構造に沿った一例で、Claudeが出す実際のコードとは細部が違ってかまいません。名前空間のURIをコードに埋め込まず、要素のローカル名で照合しているのは、データの版による差を吸収するためです。

import sys
from collections import Counter
from pathlib import Path
from xml.etree import ElementTree as ET
 
USAGE = {"401": "業務施設", "402": "商業施設", "403": "宿泊施設",
         "404": "商業系複合施設", "411": "住宅", "412": "共同住宅",
         "413": "店舗等併用住宅", "414": "店舗等併用共同住宅",
         "415": "作業所併用住宅", "421": "官公庁施設",
         "422": "文教厚生施設", "431": "運輸倉庫施設", "441": "工場",
         "451": "農林漁業用施設", "452": "供給処理施設",
         "453": "防衛施設", "454": "その他", "461": "不明"}
 
 
def local(tag):
    return tag.rsplit("}", 1)[-1]
 
 
def first(elem, name):
    """部品(BuildingPart)の内側は見ずに、最初の該当要素を返す。"""
    for child in elem:
        n = local(child.tag)
        if n == "consistsOfBuildingPart":
            continue
        if n == name:
            return child.text.strip() if child.text else None
        found = first(child, name)
        if found:
            return found
    return None
 
 
def summarize(files):
    by_usage, by_decade, total = Counter(), Counter(), 0
    for f in files:
        for _, el in ET.iterparse(f, events=("end",)):
            if local(el.tag) != "Building":
                continue
            total += 1
            code = first(el, "usage")
            by_usage[USAGE.get(code, "不明" if code is None else code)] += 1
            year = first(el, "yearOfConstruction")
            by_decade[f"{int(year) // 10 * 10}年代" if year else "不明"] += 1
            el.clear()
    return total, by_usage, by_decade
 
 
if __name__ == "__main__":
    files = sorted(Path(sys.argv[1]).glob("*.gml"))
    total, by_usage, by_decade = summarize(files)
    print("建物数", total)
    for k, v in by_usage.most_common():
        print("用途", k, v)
    for k, v in sorted(by_decade.items()):
        print("建築年", k, v)

iterparse で1棟ずつ処理して clear() するのは、ファイルをメモリに全展開しないためです。一方で、first が bldg:usage を複数持つ建物の最初の値だけを返す点には注意が要ります。仕様書は複数の用途を書くとき主たる用途を最初に記載するとしているので、「最初の値=主用途」という扱いは仕様に沿っています。

実行と検算

python3 scripts/summarize_bldg.py data/bldg
grep -c '<bldg:Building ' data/bldg/*.gml

後者の grep -c は、bldg:Building の開始タグを数える検算用です。bldg:BuildingPart は Building の直後が空白ではないため、この書き方なら部品を数えずに済みます。スクリプトの「建物数」と grep -c の合計が一致すれば、少なくとも取りこぼしや二重計上はありません。一致しないときは、Claudeに差分の原因を調べさせます。

検算をClaudeに任せるときのコツは、コマンドの実行結果を会話に戻させ、数値を並べて比較させることです。「合っているはず」という推測で終わらせない運用にすると、集計の信頼性が上がります。

階数と高さを突き合わせて異常値を見つける

用途と建築年が出せたら、次は階数と高さの突き合わせです。bldg:storeysAboveGround と bldg:measuredHeight は別々の経路で付与された値なので、両者が極端に食い違う建物は、入力ミスか形状の取り違えの候補になります。たとえば高さ数メートルで20階とされている建物、あるいは10階建てなのに高さが5メートルに満たない建物です。

Claudeには「1階あたりの高さ(高さ÷地上階数)が極端な建物を、gml:id付きで上位20件だけ出して」と頼みます。gml:id を添えさせるのは、該当の建物を元のファイルで直接確認できるようにするためです。なお bldg:measuredHeight は地上の最低点から最高点までの高さで、屋根や塔屋を含みます。1階あたりの値はあくまで目安で、閾値を決め打ちして自動で除外するより、候補を目で確かめる使い方が向いています。

建物が部品に分割されているデータの扱い

仕様書は、主たる建築物を複数の bldg:BuildingPart に分けて記述できると定めています。このとき、部品の識別には uro:partID を使い、bldg:BuildingPart では必須です。部品に分かれた建物を1棟として数えたいのか、部品単位で数えたいのかで、集計の定義が変わります。

先ほどのスクリプトは bldg:Building だけを数え、部品の中は見に行きません。部品側に属性が書かれている建物は「不明」に入ってしまうので、部品の数を別に数えさせ、「部品を持つ建物が何棟あるか」を最初の構造確認に加えると安全です。棟数を扱う分析では、この定義を結果の注記に残しておくと、後から数字を読み返したときに混乱しません。

結果を読むときの落とし穴

  • 欠損を含めた割合で語る: 建築年が入っている建物だけで割合を出すのと、全体を分母にするのとでは数字が大きく変わります。「不明」を表に残しておけば、読み手がどちらの数字なのかを判断できます
  • 用途コードの粒度: 「住宅」(411)と「共同住宅」(412)は別コードです。居住用をまとめて見たいなら、どのコードを含めるかを先に決めます
  • 都市ごとの属性の差: 属性リストのExcelで、対象都市にその属性が入っているかを確認できます。複数都市を同じスクリプトに流すと、都市ごとの欠損率の差が結果に混ざります
  • 整備年度による差: 3D都市モデルは年度ごとに整備されており、仕様書にも版があります。古い年度のデータでは属性名や入れ子の構造が異なる場合があるため、ローカル名で照合したうえで、取れなかった件数を必ず出力させます
  • テストの併走: 集計が仕様どおりかを守るテストは、手で作った小さなCityGML断片に対して書かせると回しやすくなります。pytestでの規約の置き方はClaude CodeにpytestのCLAUDE.md規約を教えるの手順がそのまま使えます

次に広げるなら

建物単位の属性がCSVに落ちれば、あとは通常のデータ分析です。用途別の棟数、築年代別の分布、階数別の高さの散らばりなどは、同じスクリプトに集計軸を足すだけで出せます。地図に重ねたい場合は、bldg:lod0RoofEdge などの地物形状と座標参照系(6697)の扱いが次の論点になります。

仕様書には、ここで触れていない属性が多く定義されています。構造種別(uro:buildingStructureType)、延べ床面積(uro:totalFloorArea)、敷地面積(uro:siteArea)などで、いずれも都市計画の基礎調査に基づく拡張属性です。入っているかどうかは都市次第なので、最初に件数を数えてから集計の軸に採用するという手順は、どの属性でも共通です。

この記事を共有:XはてブLinkedIn