第1回では、Oracle AI Data Platform(AIDP)に国勢調査のオープンデータを取り込み、東京都62市区町村の人口や世帯などを分析しました。今回は2025年のデータを追加して、「2020年からの5年間で、街がどのように変わったのか」 を見ていきます。
人口が増えた街、減った街。それだけでも違いは見えますが、単独世帯や高齢者の割合も合わせると、どのような変化が見えてくるでしょうか。普段はあまり似ていると思わない街にも、データで見ると共通点があるかもしれません。
そこで今回は、人口増減率・単独世帯割合の変化・高齢者割合の変化を使い、変化の傾向が似ている街を機械学習のK-meansでグループ分けします。さらに、その結果を生成AIに渡して、各グループの特徴を文章で説明してもらいます。
データを揃えて比較し、機械学習で傾向を見つけ、生成AIで説明する。そして、その説明を元の数値に戻って確かめる。今回は、この一連の流れをAIDPのNotebookでつないでいきます。
ということで、2020年と2025年の国勢調査データを使って、東京都の街の変化を機械学習と生成AIで分析してみてみます。
● 今回の内容:似た変化をした街を見つける
この記事では、次の3つを確かめます。
- 街の変化を比べる :人口、単独世帯割合、高齢者割合は、5年間でどのように変化したか。
- 似た変化の街を見つける :複数の指標をまとめて見ると、どの市区町村が似ているか。
- 結果を説明する :グループの特徴を生成AIで文章にし、その説明が計算済みの数値と一致するか。
例えば「人口は減っているが、単独世帯の割合は上がっている」というように、複数の変化を組み合わせて街の特徴を捉えます。ここでの「似ている」は、地理的な近さではなく、分析に使う3指標の変化が近いという意味です。
● 機械学習と生成AIの役割
機械学習には K-means を使います。あらかじめ街の分類を決めるのではなく、指標の変化が似ている市区町村をデータからグループにまとめる方法です。生成AIには、その分類結果と検査済みの指標を渡し、グループ名と説明文を提案してもらいます。
| 担当 | この検証で行うこと |
|---|---|
| データ加工・検査 | 2020年と2025年の比較条件を揃え、変化量を計算する |
| 機械学習(K-means) | 変化の傾向が似た市区町村をグループ分けする |
| 生成AI | 各グループの特徴を、数値を根拠に文章で説明する |
| 人による確認 | 数と割合、平均と個別地域、事実と推測を区別して文章を整える |
人口が変わった原因の特定や将来予測は、今回の分析には含めません。まずは観測された変化を整理し、数値に基づいて説明できるところまでを目指します。
● 作業の流れと、最後にできあがるもの
4本のNotebookを順に使い、データの取込みから説明文の作成まで進めます。
| 順番 | 使用するNotebook | 作業と成果物 |
|---|---|---|
| 1 | nb01 | 2020年・2025年のExcelを取り込み、検査済みの年別データを保存する |
| 2 | nb02 | 両年を比較し、人口増減率や割合の変化を並べた比較テーブルを作る |
| 3 | nb03 | 似た変化の市区町村を分類し、グラフと新宿区に変化が近い街の一覧を作る |
| 4 | nb04 | グループの特徴を生成AIで説明し、数値の検査結果と説明案を保存する |
今回の取込み対象は東京都62市区町村です。比較の途中で町田市の境域変更による差を検知したため、比較・機械学習は町田市を除く61市区町村を対象にしました。その結果、2つのグループに分かれました。以降では、この判断と実行結果を画面・ログとともに確認します。
● 先に結果:61市区町村が2つのグループに分かれた
今回の3指標と選択ルールでは k=2 が選ばれ、17件と44件に分かれました。次の値は市区町村を同じ重みで平均したもので、東京都全体の増減率や人口加重平均ではありません。
| グループ | 市区町村数 | 人口増減率の平均 | 単独世帯割合の差の平均 | 高齢者割合の差の平均 |
|---|---|---|---|---|
| クラスタ0 | 17 | −6.93% | +3.29pp | +2.10pp |
| クラスタ1 | 44 | +1.38% | +2.45pp | 0.00pp |
ppは割合の差を表す「パーセンテージポイント」です。例えば20%から23%への変化は+3ppです。クラスタ番号は優劣を表さず、平均値の傾向が全メンバーに当てはまるわけでもありません。
また、新宿区と3指標の変化が近かった街は、近い順に 文京区・中野区・板橋区・大田区・品川区 でした。
クラスタ0には、島しょ・山間部の町村だけでなく、 渋谷区と八王子市も含まれました。 両市区とも65歳以上人口は減少している一方、年齢既知人口を分母にした高齢者割合は上昇しています。年齢不詳率の大きな上昇に伴う分母の変化が、説明文だけでなく分類結果にも影響した可能性があります。後半では保存済みの所属と人数を確認し、この可能性を次の感度分析でどう確かめるかまで考えます。
● Pythonを知らなくても追える、この記事の読み方
まずはコードを1行ずつ覚えるよりも、 「何を読み、何を変換し、何を検査して、どこへ保存するか」 を追ってみます。各Notebookの実行手順にCell別の説明表を付けています。画面と説明表を照らし合わせ、最後に保存された結果を確認する順で読み進めてください。
・ Notebook、Cell、Computeは何が違うのか
| 用語 | この手順での意味 |
|---|---|
Notebook(.ipynb) |
説明文とプログラムをCell単位で並べた作業ノート。実行するとCellの下に結果が表示されます |
| コードCell | Pythonなどのプログラムを書く欄。 Run でそのCellを実行します |
| Markdown Cell | 見出しや説明文を書く欄。データの加工やAPI送信は行いません |
| Workspace | Notebookを置いて作業する場所。今回はnb04で別Workspaceを使います |
| Compute | Notebookのプログラムを動かす計算環境。今回は既存のCensus_PoC_Clusterを使います |
| Widget | 調査年、入力データのID、出力先などを指定する入力欄。プログラムがここから値を読みます |
| Volume | Notebookからファイルを読み書きする入口。今回はObject Storage上の原本と成果物を参照します |
| run ID | 1回の処理結果を識別する名前。入力に指定するIDと、新しい出力に付けるIDを区別します |
| クラスタ | nb03では「変化が似た市区町村のグループ」。画面の Cluster で選ぶComputeとは別の意味です |
・ Run allで何が起きるのか
Run all はNotebook内のコードCellを上から順に実行します。ただし、すべてのCellでデータ処理が完了するわけではありません。長いコードのCellには、後で使う処理を準備するものもあります。
例えば、def extract_year(...):は「Excelを読み込む処理に名前を付けて用意する」記述です。後のCellにあるextract_year(...)で、その処理を実際に動かします。関数を定義するCellで表やPASSが表示されなくても、それだけで失敗とは判断しません。
説明表のCell番号は、 先頭にWidget用Cellを追加した掲載画面の番号 です。左側の[3]などは実行回数の表示なので、Cellの位置とは一致しない場合があります。自分でCellを追加すると番号もずれるため、見出しやコード中の関数名も照合します。
nb01〜nb03は設定を確認してRun allで進めます。 nb04の初回は公開鍵の登録操作を途中に挟むため、Cell単位で進めます。 詳細はnb04の手順で説明します。
・ コードでよく出てくる書き方
操作手順のPythonコードは、指定されたNotebookのコードCellへ貼り付けます。「実行結果」と付けた枠は実行後に表示される例なので、Cellへ貼り付けません。自分の環境に合わせる箇所は、Widgetの値、パス、run ID、Cell BのOCIDです。それ以外の処理を変更する前に、まず掲載手順のまま実行結果を確認します。
| 書き方 | 読み方・役割 |
|---|---|
import json、from pathlib import Path
|
JSONやファイルの場所を扱う道具を読み込みます |
name = value |
値に名前を付け、後の処理で使います。この名前を「変数」と呼びます |
{"status": "PASS", "rows": 61} |
項目名と値を組にしたデータ。statusは状態、rowsは行数です |
def ... / 関数名(...)
|
処理を定義する / 定義した処理を呼び出す、という違いがあります |
for ... in ... |
年度やファイルなどを1つずつ取り出し、同じ処理を繰り返します |
if ... |
条件を確認し、該当する場合の処理を選びます |
assert ...、raise ValueError(...)
|
前提が満たされているかを確認し、不一致なら処理を止めます |
spark.read.parquet(...) |
保存済みの表をSparkで読み込みます。読み込みだけでは再学習しません |
.select(...).orderBy(...).show(...) |
表の列を選び、並び替え、画面に表示します |
・ 実行中の変数と、保存済みファイルを区別する
Cellで作った変数や関数は、NotebookのPythonセッション内で使います。再起動すると消えるため、途中のCellだけを実行するとNameErrorなどになることがあります。同じComputeを接続した別Notebookでも、変数が引き継がれる前提にはしません。 nb01〜nb04の受け渡しには、Volumeへ保存したファイルを使います。
| 形式 | 保存しているもの | このブログでの用途 |
|---|---|---|
Excel(.xlsx) |
統計表の原本 | nb01の入力 |
| Parquet | 列の型を持つ分析用の表 | 年別データ、比較表、機械学習結果の受け渡し |
JSON(.json) |
項目名と値の組 | 検査結果、設定、生成AIへの入力と応答の記録 |
CSV(.csv) |
表を文字で表したデータ | 比較結果をPCなどで確認 |
PNG(.png) |
グラフ画像 | 結果の確認とブログへの掲載 |
Markdown(.md) |
見出し・文章・表を記述したテキスト | 生成AIが作った説明案 |
nb00_check_and_captureは、環境確認や保存結果の表示に使います。結果をもう一度見たいときは該当する確認Cellを実行すればよく、取込み・学習・生成AI送信を毎回やり直す必要はありません。
■ 最初に確認する環境とファイル
● 第1回から利用する環境と権限
この手順は、第1回でAIDPのWorkspace、Compute、Catalog、External Volumeを作成済みの環境を使います。AIDPを初めて利用する場合は、まずこれらを作成し、NotebookからVolume内のファイルを読み書きできる状態にします。
| 項目 | 本手順の例 | 自分の環境で確認すること |
|---|---|---|
| nb01〜nb03のWorkspace | AIDP_OpenData_PoC |
第1回のNotebookがあるWorkspace |
| nb04のWorkspace | AIDP_OpenData_Workspace |
同じ東京のAIDP内に作成。今回はPrivate networkを有効にせず、大阪への接続を確認 |
| AIDPのリージョン | 東京 ap-tokyo-1
|
AIDP本体を大阪へ移動する手順ではありません |
| 生成AIの接続先 | 大阪 ap-osaka-1
|
nb04からOCI Python SDKで呼び出す |
| Compute | Census_PoC_Cluster |
今回は既存Computeを使用。対象Notebookへ接続でき、稼働中であること |
| Catalog / Schema | blog_catalog.opendata |
第1回で使用したもの |
| 原本用External Volume | census_raw |
Object Storageの tokyo-census/raw/ を参照 |
| 出力用External Volume | census_work |
Object Storageの tokyo-census/work/ を参照 |
| Object Storageバケット | Bucket-AIDP |
実際のバケット名に読み替える |
| 第2回用Workspaceフォルダ | OpenData_AI_2020_2025 |
今回作成するフォルダ名の例 |
バケット名とVolume名は異なる役割の名前です。Notebookには、バケットのURLではなく /Volumes/... のパスを設定します。
必要な権限は、Notebookの編集・実行、Computeの利用、原本Volumeの読込み、出力Volumeへの書込みです。ライブラリの追加やWidgetの作成にも対応する操作権限が必要です。生成AI部分ではモデルの利用権限も確認します。
● 用意するNotebookとライブラリ定義
手元のPCで第2回の実行セット AIDP-OpenData_AI分析_続編_v0.2_実行セット.zip を展開し、次の5ファイルがあることを確認します。nb04は、この後の手順でCell A〜Cを追加・差し替えて使用します。
| ファイル | 用途 |
|---|---|
nb01_ingest_year.ipynb |
調査年ごとの原本取込み |
nb02_compare_years.ipynb |
2020年と2025年の比較 |
nb03_cluster_changes.ipynb |
K-meansによる機械学習 |
nb04_explain_changes.ipynb |
生成AIによる説明と数値照合 |
requirements.txt |
必要なPythonライブラリ |
.py、テスト、ローカル検証結果も同梱されていますが、AIDPへのNotebookインポートには .ipynb を使います。原本ExcelはZIPに含まれていないため、次の手順で用意します。
■ 2025年国勢調査のExcelをダウンロードする
● 利用する統計表を選ぶ
1) e-Statで原数値の表を開く
次のリンクをPCのブラウザで開きます。2025年国勢調査の「集計結果(原数値)」→「人口等基本集計」に属する表を使います。
| 表 | ページ | PCに保存する名前 |
|---|---|---|
| 1-1 人口・世帯数 | 2025年 表1-1 | population_1_1.xlsx |
| 9-1 世帯の家族類型 | 2025年 表9-1 | households_9_1.xlsx |
| 2-7 年齢別人口 | 2025年 表2-7 | age_2_7.xlsx |
各ページで、調査名が令和7年国勢調査、調査年月が2025年10月、表番号が上表と一致することを確認します。人口速報、不詳補完値、参考表を選ばないようにします。
直接リンクから目的の表が表示されない場合は、2025年国勢調査の一覧から「集計結果(原数値)」→「人口等基本集計」へ進み、表番号で探します。
● 原本Excelを保存する
1) EXCELをダウンロードして名前を変更する
各ページの表示・ダウンロード欄で [EXCEL] をクリックします。ダウンロードしたファイルは、PCの census/2025/ など、2025年専用のフォルダへ保存します。
ダウンロード時のファイル名を、上表の名前へ変更します。拡張子は .xlsx のままです。ExcelやNumbersで開いた場合も、元ファイルを編集・再保存せず閉じます。実行セットでは原本のハッシュも照合するため、再保存だけでも別ファイルとして検知されます。
確認すること :2025年フォルダに3つの .xlsx があり、名前の末尾に (1) や二重の .xlsx が付いていないこと。
■ 2020年の原本も年別フォルダに揃える
● 第1回の原本を年別フォルダへ揃える
1) 第1回で使用した2020年の3ファイルを確認する
第1回の原本がPCに残っていれば再利用します。ない場合は次の表から再取得します。
| 表 | ページ | 保存する名前 |
|---|---|---|
| 1-1 | 2020年 表1-1 | population_1_1.xlsx |
| 9-1 | 2020年 表9-1 | households_9_1.xlsx |
| 2-7 | 2020年 表2-7 | age_2_7.xlsx |
こちらはPCの census/2020/ など、2020年専用のフォルダへ保存します。両年のファイル名は同じで、親フォルダで年を区別します。
今回2020年も取り込み直すのは、両年の列を共通化するためです。第1回の curated/tokyo_comparison と、第2回の curated/tokyo_year は出力の形が違います。第1回の変換済みデータのパスを、そのまま第2回の入力へ指定することはしません。
■ Object Storageへ原本をアップロードする
OCIコンソールで、第1回のデータを置いたリージョンとコンパートメントを選択します。Object Storageの [Buckets/バケット] 一覧から、第1回で使ったバケットを開きます。
以下は Bucket-AIDP のルートにある [Objects/オブジェクト] 一覧からアップロードする例です。
● 2025年の原本を配置する
1) 2025年の3ファイルをアップロードする
[Upload/アップロード] をクリックし、次を設定します。
| 設定欄 | 入力内容 |
|---|---|
| Object name prefix/オブジェクト名の接頭辞 | tokyo-census/raw/2025/ |
| Storage tier/ストレージ層 | Standard |
| アップロードするファイル | PCの2025年フォルダにある3つのExcel |
接頭辞の末尾の / まで入力します。アップロード対象の3ファイルを選択し、 [Upload] をクリックします。
既にバケット内の tokyo-census/raw/2025/ フォルダへ移動している場合は、同じ接頭辞を重ねて入力しないようにします。本手順では、バケットのルートから接頭辞を指定する方法に統一します。
2) 2025年の3ファイルのアップロード確認
最終的なオブジェクト名が次の3つであることを確認します。
tokyo-census/raw/2025/population_1_1.xlsx
tokyo-census/raw/2025/households_9_1.xlsx
tokyo-census/raw/2025/age_2_7.xlsx
● 2020年の原本を配置する
1) 2020年の3ファイルもアップロードする
同様に、接頭辞を tokyo-census/raw/2020/ にして、2020年の3ファイルをアップロードします。既に正しい場所にある場合は再アップロード不要です。
第1回のファイルが tokyo-census/raw/ 直下にある場合は、そのファイルを残したまま、今回の年別フォルダへ配置します。
2) 2020年の3ファイルのアップロード確認
最終的なオブジェクト名が次の3つであることを確認します。
tokyo-census/raw/2020/population_1_1.xlsx
tokyo-census/raw/2020/households_9_1.xlsx
tokyo-census/raw/2020/age_2_7.xlsx
■ AIDPのVolumeと配置先を確認する
● Object StorageとVolumeの対応を確認する
1) 原本用Volumeの参照先を確認する
AIDPを開き、対象Workspaceを選択します。 [Master catalog] から blog_catalog → opendata → [Volumes] と進み、census_raw の詳細を開きます。
census_raw が、対象バケットの tokyo-census/raw/ を参照するExternal Volumeであることを確認します。今回追加した 2025/ は、その配下のサブフォルダです。2025年専用のVolumeを追加する必要はありません。
対応は次のようになります。
| Object Storageのオブジェクト名 | Notebookからアクセスするパス |
|---|---|
tokyo-census/raw/2020/population_1_1.xlsx |
/Volumes/blog_catalog/opendata/census_raw/2020/population_1_1.xlsx |
tokyo-census/raw/2025/population_1_1.xlsx |
/Volumes/blog_catalog/opendata/census_raw/2025/population_1_1.xlsx |
/Volumes/.../census_raw/ の後へ、さらに tokyo-census/raw/ を足さないことがポイントです。
出力用の census_work も確認します。本手順では /Volumes/blog_catalog/opendata/census_work に書き込みます。
Volumeがまだない場合は、対象Schemaの [Add to schema] → [Volume] から作成します。種類を [External] にし、バケットと参照するフォルダを選択します。既存Volumeと同じ場所に重複して作成せず、第1回の設定を確認してください。
● 年別フォルダのファイルを確認する
参考:Volumes、Workspaceからのファイルアクセス
■ Computeへライブラリを追加する
● 分析に必要なライブラリを導入する
1) Computeを開き、ライブラリをインストールする
対象Workspaceの [Compute] から Census_PoC_Cluster を開きます。停止中なら起動し、Activeになるまで待ちます。
[Library] → [Install Library] → [Upload file to workspace] と進み、展開した実行セットの requirements.txt を選択して [Install] をクリックします。
指定内容は次のとおりです。ファイル名は requirements.txt のまま使います。
numpy==1.26.4
pandas==2.2.3
openpyxl==3.1.5
matplotlib==3.10.1
scipy==1.13.1
scikit-learn==1.5.2
joblib==1.4.2
threadpoolctl==3.5.0
今回はPython 3.11系の環境で実行しました。第1回と異なるランタイムへ変更している場合は、インストール結果と互換性を確認します。PySparkはAIDPに含まれるものを使います。実際に使用されたバージョンは、後述の確認セルと検査JSONに記録します。
第1回で同じライブラリを登録済みの場合は、既存の指定と今回の指定が競合していないか確認します。別バージョンを重ねて追加するのではなく、登録済みの設定を揃えます。
● 再起動してライブラリを反映する
1) インストール後にComputeを再起動する
インストールの完了を確認して、Computeの [Actions] → [Restart] をクリックします。再びActiveになってから、次へ進みます。Restart前のNotebook変数や一時ビューは引き継がないため、以降は必要なセルを上から実行します。
■ 第2回のNotebookをインポートする
● フォルダを作成してNotebookを取り込む
1) 第2回用フォルダを作る
Workspaceのファイル一覧で、第2回を置く親フォルダを開きます。 [Create] → [Folder] をクリックし、フォルダ名に OpenData_AI_2020_2025 を入力して作成します。
・ Name: OpenData_AI_2020_2025
・ Description: 東京都の2020年・2025年国勢調査データを取り込み、人口・世帯・年齢構成の変化を比較する検証用フォルダ。データ検査、K-meansによる変化パターンの分類、OCI Generative AIによる特徴の説明を行うNotebookを格納する。
2) .ipynbファイルをアップロード
そのフォルダでローカルファイルのアップロード/Notebookインポート画面を開き、対象の .ipynb を選択して [Upload] をクリックします。4本のNotebookを取り込みます。画面の表記は環境により異なるため、ローカルの .ipynb を選ぶ操作を使います。Notebookエディタ内のコード欄へファイル内容を貼る操作ではありません。
3) .ipynbファイルをアップロード確認
一覧に4本が表示され、開くと説明セル・コードセルが分かれていることを確認します。
● NotebookとComputeを接続する
1) NotebookへComputeを接続する
まず nb01_ingest_year を開き、 [Actions] → [Attach an existing cluster] をクリックし、Census_PoC_Cluster を選びます。
Notebookに対象クラスタが接続され、runningの状態になるまで待ちます。nb02、nb03にも、実行前に同じ操作でComputeを接続します。nb04は後述の新しいWorkspace側でComputeを接続します。
2) NotebookへCompute接続確認
■ 実行前にファイルの見え方を確認する
● 確認用Notebookで実行環境を調べる
1) 確認用Notebookを作る
同じWorkspaceフォルダで [Create] → [Notebook] をクリックして nb00_check_and_capture を作成し、同じComputeを接続します。以降の確認用コードはここに貼ります。
確認用Notebookを分けると、nb02のような「出力フォルダを新規作成する処理」を、結果表示のためだけに再実行せずに済みます。
Pythonセルへ次を貼り付け、セルの [Run] 、または [Run] → [Run selected cell(s)] をクリックして実行します。
import platform
from importlib.metadata import version
print("Python:", platform.python_version())
print("Spark:", spark.version)
for package in ["numpy", "pandas", "openpyxl", "matplotlib", "scipy", "scikit-learn"]:
print(package, version(package))
確認すること :不足しているライブラリのエラーがなく、使われるバージョンが表示されること。表示されたバージョンを控えておくと、動作が異なる場合に環境を比較できます。
● 原本の読込みと出力先の書込みを確認する
1) 6つの原本の存在を確認する
別のPythonセルで実行します。自分のCatalog名・Schema名・Volume名が異なる場合は、RAW を変更します。
from pathlib import Path
RAW = Path("/Volumes/blog_catalog/opendata/census_raw")
filenames = ["population_1_1.xlsx", "households_9_1.xlsx", "age_2_7.xlsx"]
missing = []
for year in [2020, 2025]:
for name in filenames:
path = RAW / str(year) / name
exists = path.is_file()
size = path.stat().st_size if exists else 0
print(year, name, "exists=", exists, "bytes=", size)
if not exists or size == 0:
missing.append(str(path))
if missing:
raise FileNotFoundError("配置先を確認してください: " + ", ".join(missing))
print("原本6ファイルの存在確認: PASS")
確認すること :6ファイルとも exists=True で、サイズが0より大きいこと。これは存在確認であり、調査年や表頭の検査は後のnb01で行います。
2) 出力先への書込みを確認する
次のコードは、専用の確認ファイルを1個作り、読込み後にそのファイルだけ削除します。
from pathlib import Path
from uuid import uuid4
WORK = Path("/Volumes/blog_catalog/opendata/census_work")
if not WORK.is_dir():
raise FileNotFoundError("出力用Volumeを確認してください: " + str(WORK))
check_dir = WORK / "_checks"
check_dir.mkdir(exist_ok=True)
probe = check_dir / ("write_probe_" + uuid4().hex + ".txt")
with probe.open("x", encoding="utf-8") as stream:
stream.write("AIDP census write check")
assert probe.read_text(encoding="utf-8") == "AIDP census write check"
probe.unlink()
print("出力用Volumeの読書き確認: PASS")
■ 実行IDを決めておく
● 筆者の実行IDと、読者が設定するID
次の表は筆者の実行記録です。 explain_20261020_03 の _03 は筆者が成功した実行の番号で、読者に必須の値ではありません。 新しく生成する場合は、自分の出力先で未使用のIDを設定します。初回なら未使用の _01 でも構いません。入力に使うIDは前段の実際の出力IDに合わせ、後の確認コードのパスも変更します。
同じ条件の保存応答を読む操作と、新たに生成AIへ送信する操作を区別します。既にある送信記録や失敗ログは削除せず、新たに送信すると決めた場合だけ新しいIDを使います。
| 用途 | 筆者の実行値(読者は自分の実行IDに置換) |
|---|---|
| 2020年のsource_run_id | census2020_20261020_01 |
| 2025年のsource_run_id | census2025_20261020_01 |
| 境域検査で停止するcomparison_run_id | compare_strict_20261020_01 |
| 61市区町村のcomparison_run_id | compare61_20261020_01 |
| 機械学習のai_run_id | change_ml_20261020_01 |
| 生成AIのreport_run_id(成功した実行) | explain_20261020_03 |
● 後段の処理へ引き継ぐID
run IDは出力先フォルダの名前に使います。Notebookの名前とは異なります。後段のNotebookや確認コードには、前段で実際に使用したIDを設定します。
■ nb01で2020年を取り込む
● 2020年の入力パラメータを設定する
1) 処理前にWidgetを表示する
nb01_ingest_year を開き、既存のコードより前にPythonセルを1つ追加します。以下のコードを貼り、 追加したセルだけ を実行します。これは入力欄を作るセルで、データの取込みはまだ行いません。
defaults = {
"census_year": "2020",
"source_run_id": "census2020_20261020_01",
"raw_root": "/Volumes/blog_catalog/opendata/census_raw",
"work_root": "/Volumes/blog_catalog/opendata/census_work",
}
for name, value in defaults.items():
aidputils.widgets.text(name=name, defaultValue=value, label=name)
for name in defaults:
print(name, "=", aidputils.widgets.get(name))
以前の値が残っている場合は、Widgetの入力欄で次の表に合わせます。defaultValue をコードで変更しただけでは、既存Widgetの値が変わらない場合があります。画面の値と get() の出力で確認します。
| Widget | 値 |
|---|---|
census_year |
2020 |
source_run_id |
census2020_20261020_01 |
raw_root |
/Volumes/blog_catalog/opendata/census_raw |
work_root |
/Volumes/blog_catalog/opendata/census_work |
raw_root の末尾へ /2020 は追加しません。Notebookが census_year を使って年別のフォルダを選びます。
● データを取り込み、検査結果を確認する
・ nb01の各Cellは何をしているのか
nb01の入力は、人口・世帯・年齢に関する3つのExcelです。年によって異なる表の位置を読み分け、東京都62市区町村について、後のNotebookが共通に使える1つの表へ揃えます。
| 画面のCell | 種類・役割 | 何を処理するか | ここで確認すること |
|---|---|---|---|
| Cell 1 | Widgetの作成 | 調査年、run ID、原本・出力のパスを入力できるようにします | 表示された値が今回の設定か。まだExcelは取り込みません |
| Cell 2 | 説明文 | nb01の目的を説明します | 年別取込みのNotebookであること |
| Cell 3 | 設定の読取り | Widgetを読み、調査年・ID・パスを検査し、出力先を決めます |
census_year、source_run_id、targetの出力 |
| Cell 4 | 説明文 | 年度別の読込定義について説明します | 次のCellがExcelの読み方の設定であること |
| Cell 5 | 年度別の定義 |
SOURCE_REGISTRYから、年度に合うファイル名、シート、列位置、表頭、原本ハッシュなどの設定を選びます |
2020年用・2025年用の設定が分かれていること。ここでは取込みは動きません |
| Cell 6 | 説明文 | 共通の読込処理を説明します | 年が違っても共通処理を使う構成であること |
| Cell 7 | 関数の定義 |
extract_yearなどを用意します。数値の変換、表頭・コード・件数・合計などの検査方法を定義します |
長いコードは「処理の準備」。Excelの読込み本体は次のCellで呼び出します |
| Cell 8 | 読込み・原本検査の実行 |
extract_year(...)を呼び、Excelから62市区町村を取り出して表を組み合わせ、検査します |
原本検査のPASSと件数62。この段階では年別Parquetの保存はまだです |
| Cell 9 | 説明文 | Sparkで保存する共通スキーマを説明します | スキーマは「列名とデータ型の決まり」のこと |
| Cell 10 | 保存・読戻し検査 | Sparkの表を作り、件数・重複・必須値などを検査。Parquetへ保存後に読み直し、保存前と一致するか確認します | 最後のPASS、年度、62件、出力先。検査結果もvalidation.jsonに保存されます |
・ 表を取り込む前に、何を検査するのか
表頭チェックは「この位置に、想定した見出しがあるか」を確かめる処理です。列がずれたまま別の数値を人口として読まないために行います。原本ハッシュはファイル内容から計算した照合用の値で、想定した原本との一致を確認します。公式ファイルが更新された場合も、まず差分を確認します。
市区町村は名前だけでなくarea_code(地域コード)で対応付けます。全国計・東京都計などの集計行を市区町村として混ぜず、62地域が一度ずつ現れるか、人数・世帯数の合計が東京都計と合うかを検査します。
年齢不詳欄の-は、このNotebookではNULLとして保持します。NULLは「ここで数値として扱わない値」であり、0人とは区別します。この方針は後の比較・機械学習・生成AIの説明にも引き継ぎます。
nb01の完了地点はCell 10です。 Cell 8のPASSは原本からの抽出検査であり、保存後の読戻し検査とは別です。
1) Notebookを上から実行する
入力を確認したら [Run] → [Run all] をクリックし、セルを順番に実行します。原本の表頭、原本ハッシュ、地域コード、件数、東京都計との一致を検査し、Parquetとして保存します。
確認すること :最後の検査結果がPASS、census_year が2020、件数が62であること。途中の表表示だけで成功と判断せず、最後の保存・読戻しまで確認します。
{'census_year': 2020, 'source_run_id': 'census2020_20261020_01', 'target': '/Volumes/blog_catalog/opendata/census_work/runs/census2020_20261020_01/curated/tokyo_year'}
{
"status": "PASS",
"census_year": 2020,
"rows": 62,
"totals": {
"population": 14047594,
"general_households": 7216650,
"households": 7227180,
"single_households": 3625810,
"age_under15": 1566840,
"age_15_64": 8944193,
"age_65plus": 3107822,
"population_previous_rearranged": 13515272
},
"age_unknown_null_codes": [
"13361",
"13362",
"13363",
"13364",
"13382",
"13402"
]
}
{"status": "PASS", "rows": 62, "census_year": 2020, "source_run_id": "census2020_20261020_01", "target": "/Volumes/blog_catalog/opendata/census_work/runs/census2020_20261020_01/curated/tokyo_year"}
+---------+----------+----------+------------------+----------+------------------------------+----------------------------+---------------------+-----------+---------+----------+-----------+---------------+---------------------------------+--------------+-----------------+-----------------+-----------+--------------+------------------------+--------+--------------+---------------------+
|area_code|area_name |population|general_households|households|population_previous_rearranged|population_change_pct_source|population_source_row|age_under15|age_15_64|age_65plus|age_unknown|age_unknown_raw|age_65plus_share_known_source_pct|age_source_row|single_households|single_source_row|census_year|reference_date|population_previous_year|series |boundary_basis|unknown_policy |
+---------+----------+----------+------------------+----------+------------------------------+----------------------------+---------------------+-----------+---------+----------+-----------+---------------+---------------------------------+--------------+-----------------+-----------------+-----------+--------------+------------------------+--------+--------------+---------------------+
|13101 |千代田区 |66680 |36963 |37011 |58406 |14.16635 |1229 |8976 |46517 |10852 |335 |335 |16.35692 |1226 |21076 |26962 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13102 |中央区 |169179 |92487 |92533 |141183 |19.82958 |1230 |23086 |121217 |24683 |193 |193 |14.60654 |1227 |48646 |27002 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13103 |港区 |260486 |146084 |146160 |243283 |7.07119 |1231 |34714 |174886 |42546 |8340 |8340 |16.87356 |1228 |83099 |27042 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13104 |新宿区 |349385 |222461 |222800 |333560 |4.74427 |1232 |29202 |223593 |63185 |33405 |33405 |19.99652 |1229 |150831 |27082 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13105 |文京区 |240069 |133564 |133661 |219724 |9.25934 |1233 |28065 |152818 |42020 |17166 |17166 |18.85125 |1230 |77316 |27122 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13106 |台東区 |211444 |121557 |124345 |198073 |6.75054 |1234 |18648 |142955 |47510 |2331 |2331 |22.71977 |1231 |71778 |27162 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13107 |墨田区 |272085 |145609 |145768 |256274 |6.16957 |1235 |28028 |183630 |60108 |319 |319 |22.11756 |1232 |75960 |27202 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13108 |江東区 |524310 |264111 |264278 |498109 |5.26009 |1236 |66472 |341716 |110002 |6120 |6120 |21.22812 |1233 |123488 |27242 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13109 |品川区 |422488 |237447 |237641 |386855 |9.21094 |1237 |47036 |272685 |79532 |23235 |23235 |19.9202 |1234 |136020 |27282 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13110 |目黒区 |288088 |155610 |155715 |277622 |3.76987 |1238 |31547 |196916 |55811 |3814 |3814 |19.63282 |1235 |82777 |27322 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13111 |大田区 |748081 |399628 |400164 |717082 |4.32294 |1239 |77231 |475656 |161855 |33339 |33339 |22.64523 |1236 |214237 |27362 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13112 |世田谷区 |943664 |491717 |492065 |903346 |4.46318 |1240 |108940 |638629 |190042 |6053 |6053 |20.26875 |1237 |250635 |27402 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13113 |渋谷区 |243883 |149856 |149967 |224533 |8.61789 |1241 |22984 |154258 |41261 |25380 |25380 |18.88349 |1238 |96707 |27442 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13114 |中野区 |344880 |207944 |208093 |328215 |5.07746 |1242 |29123 |231515 |65878 |18364 |18364 |20.17604 |1239 |129649 |27482 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13115 |杉並区 |591108 |336103 |336339 |563997 |4.80694 |1243 |58944 |380639 |117072 |34453 |34453 |21.03134 |1240 |196919 |27522 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13116 |豊島区 |301599 |183687 |183819 |291167 |3.58282 |1244 |26489 |211034 |58539 |5537 |5537 |19.77255 |1241 |117608 |27562 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13117 |北区 |355213 |189579 |189700 |341076 |4.14482 |1245 |36527 |229948 |87381 |1357 |1357 |24.69394 |1242 |99015 |27602 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13118 |荒川区 |217475 |111799 |112009 |212264 |2.45496 |1246 |24470 |141156 |50411 |1438 |1438 |23.33443 |1243 |55368 |27642 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13119 |板橋区 |584483 |314133 |314446 |561916 |4.01608 |1247 |60014 |367976 |132602 |23891 |23891 |23.65392 |1244 |170849 |27682 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13120 |練馬区 |752608 |374485 |374842 |721722 |4.27949 |1248 |85920 |478648 |159298 |28742 |28742 |22.00656 |1245 |178473 |27722 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13121 |足立区 |695043 |345010 |345346 |670122 |3.71888 |1249 |75589 |424335 |170261 |24858 |24858 |25.40507 |1246 |163352 |27762 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13122 |葛飾区 |453093 |215709 |215948 |442913 |2.29842 |1250 |51556 |285968 |111893 |3676 |3676 |24.89737 |1247 |93974 |27802 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13123 |江戸川区 |697932 |332895 |333200 |681298 |2.44152 |1251 |86270 |449387 |145764 |16511 |16511 |21.39118 |1248 |148816 |27842 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13201 |八王子市 |579355 |266549 |267020 |577513 |0.31895 |1252 |62867 |344893 |153504 |18091 |18091 |27.3497 |1249 |110686 |27882 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13202 |立川市 |183581 |89599 |89727 |176295 |4.13285 |1253 |21828 |115971 |45524 |258 |258 |24.83267 |1250 |40896 |27922 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13203 |武蔵野市 |150149 |77966 |78054 |144730 |3.74421 |1254 |17232 |95301 |31725 |5891 |5891 |21.99185 |1251 |39402 |27962 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13204 |三鷹市 |195391 |96116 |96389 |186936 |4.52294 |1255 |23468 |123039 |41623 |7261 |7261 |22.12459 |1252 |45726 |28002 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13205 |青梅市 |133535 |56354 |56523 |137381 |-2.79951 |1256 |13763 |76704 |42492 |576 |576 |31.95872 |1253 |19099 |28042 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13206 |府中市 |262790 |123766 |123931 |260274 |0.96667 |1257 |32986 |165586 |56764 |7454 |7454 |22.2311 |1254 |54568 |28082 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13207 |昭島市 |113949 |52111 |52163 |111539 |2.16068 |1258 |13780 |68947 |30003 |1219 |1219 |26.61492 |1255 |20446 |28122 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13208 |調布市 |242614 |120790 |120945 |229061 |5.91676 |1259 |28754 |153175 |50414 |10271 |10271 |21.69809 |1256 |57424 |28162 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13209 |町田市 |431079 |191703 |192015 |432349 |-0.29374 |1260 |51174 |253130 |116470 |10305 |10305 |27.67994 |1257 |71308 |28202 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13210 |小金井市 |126074 |63140 |63182 |121396 |3.8535 |1261 |14879 |80498 |25591 |5106 |5106 |21.15518 |1258 |30485 |28242 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13211 |小平市 |198739 |91125 |91287 |190005 |4.59672 |1262 |25072 |121648 |45060 |6959 |6959 |23.49567 |1259 |38015 |28282 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13212 |日野市 |190435 |90257 |90404 |186283 |2.22887 |1263 |22894 |115015 |45652 |6874 |6874 |24.87021 |1260 |39135 |28322 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13213 |東村山市 |151815 |68340 |68478 |149956 |1.2397 |1264 |17720 |90285 |40749 |3061 |3061 |27.39355 |1261 |26136 |28362 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13214 |国分寺市 |129242 |63916 |63962 |122742 |5.29566 |1265 |15082 |81431 |27184 |5545 |5545 |21.97628 |1262 |29880 |28402 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13215 |国立市 |77130 |38227 |38275 |73655 |4.71794 |1266 |8448 |48511 |17748 |2423 |2423 |23.75681 |1263 |17924 |28442 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13218 |福生市 |56414 |28117 |28148 |58395 |-3.39241 |1267 |5692 |34800 |15153 |769 |769 |27.23156 |1264 |13054 |28482 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13219 |狛江市 |84772 |42578 |42616 |80249 |5.63621 |1268 |9736 |52699 |19961 |2376 |2376 |24.22569 |1265 |20347 |28522 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13220 |東大和市 |83901 |36291 |36336 |85157 |-1.47492 |1269 |10661 |50007 |22923 |310 |310 |27.42281 |1266 |11781 |28562 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13221 |清瀬市 |76208 |34918 |35003 |74864 |1.79526 |1270 |8710 |44106 |21482 |1910 |1910 |28.9133 |1267 |14500 |28602 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13222 |東久留米市|115271 |51146 |51217 |116632 |-1.16692 |1271 |13735 |67397 |32745 |1394 |1394 |28.75471 |1268 |18148 |28642 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13223 |武蔵村山市|70829 |29956 |29978 |71229 |-0.56157 |1272 |9375 |41663 |18847 |944 |944 |26.96859 |1269 |9892 |28682 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13224 |多摩市 |146951 |68354 |68415 |146631 |0.21823 |1273 |16577 |87607 |42582 |185 |185 |29.01353 |1270 |26359 |28722 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13225 |稲城市 |93151 |39880 |39906 |87636 |6.29308 |1274 |13130 |58186 |20087 |1748 |1748 |21.9763 |1271 |13933 |28762 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13227 |羽村市 |54326 |23789 |23809 |55833 |-2.69912 |1275 |6531 |32849 |14728 |218 |218 |27.21963 |1272 |8499 |28802 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13228 |あきる野市|79292 |31840 |31887 |80954 |-2.05302 |1276 |9235 |43149 |23792 |3116 |3116 |31.23293 |1273 |8856 |28842 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13229 |西東京市 |207388 |96896 |97018 |200012 |3.68778 |1277 |24707 |128947 |48446 |5288 |5288 |23.9713 |1274 |39856 |28882 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13303 |瑞穂町 |31765 |13000 |13017 |33445 |-5.02317 |1280 |3443 |18609 |9517 |196 |196 |30.14666 |1277 |3935 |28922 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13305 |日の出町 |16958 |5973 |6012 |17446 |-2.7972 |1281 |2235 |8095 |6538 |90 |90 |38.75978 |1278 |1440 |28962 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13307 |檜原村 |2003 |830 |835 |2209 |-9.32549 |1282 |131 |806 |1058 |8 |8 |53.03258 |1279 |301 |29002 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13308 |奥多摩町 |4750 |1975 |1984 |5234 |-9.24723 |1283 |337 |1998 |2410 |5 |5 |50.79031 |1280 |690 |29042 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13361 |大島町 |7102 |3657 |3712 |7884 |-9.91882 |1284 |710 |3679 |2713 |NULL |- |38.20051 |1281 |1880 |29082 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13362 |利島村 |327 |193 |193 |337 |-2.96736 |1285 |50 |197 |80 |NULL |- |24.46483 |1282 |121 |29122 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13363 |新島村 |2441 |1160 |1163 |2749 |-11.20407 |1286 |272 |1202 |967 |NULL |- |39.61491 |1283 |464 |29162 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13364 |神津島村 |1855 |804 |808 |1891 |-1.90375 |1287 |274 |994 |587 |NULL |- |31.6442 |1284 |289 |29202 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13381 |三宅村 |2273 |1376 |1377 |2482 |-8.42063 |1288 |225 |1151 |894 |3 |3 |39.38326 |1285 |819 |29242 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13382 |御蔵島村 |323 |187 |187 |335 |-3.58209 |1289 |60 |205 |58 |NULL |- |17.95666 |1286 |120 |29282 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13401 |八丈町 |7042 |3758 |3767 |7613 |-7.50033 |1290 |799 |3415 |2801 |27 |27 |39.92872 |1287 |1846 |29322 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13402 |青ヶ島村 |169 |118 |118 |178 |-5.05618 |1291 |21 |117 |31 |NULL |- |18.3432 |1288 |90 |29362 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
|13421 |小笠原村 |2929 |1457 |1469 |3022 |-3.07743 |1292 |416 |2099 |413 |1 |1 |14.10519 |1289 |867 |29402 |2020 |2020-10-01 |2015 |original|survey_date |preserve_dash_as_null|
+---------+----------+----------+------------------+----------+------------------------------+----------------------------+---------------------+-----------+---------+----------+-----------+---------------+---------------------------------+--------------+-----------------+-----------------+-----------+--------------+------------------------+--------+--------------+---------------------+
■ 同じnb01で2025年を取り込む
● 年度を切り替えて2025年を取り込む
1) 調査年とrun IDを変更する
同じNotebookのWidgetを変更します。
| Widget | 変更後の値 |
|---|---|
census_year |
2025 |
source_run_id |
census2025_20261020_01 |
raw_root |
変更なし |
work_root |
変更なし |
[Run] → [Run all] をクリックし、入力を読み取るセルから処理をやり直します。最後のセルだけを実行すると、2020年の変数を使う恐れがあります。
2025年は表頭の位置が一部変わっているため、Notebookに埋め込まれた2025年用の読込定義が使われます。
処理の順番は2020年と同じです。Cell 3で変更後のWidgetを読み、Cell 5で2025年用の定義を選び、Cell 8で原本を読み、Cell 10で保存します。共通の処理を再利用しながら、年度固有のExcelの違いを定義側で吸収しています。
確認すること :最後の検査結果がPASS、census_year が2025、件数が62、出力先が2025年用run IDになっていること。
{'census_year': 2025, 'source_run_id': 'census2025_20261020_01', 'target': '/Volumes/blog_catalog/opendata/census_work/runs/census2025_20261020_01/curated/tokyo_year'}
{
"status": "PASS",
"census_year": 2025,
"rows": 62,
"totals": {
"population": 14236627,
"general_households": 7577227,
"households": 7588310,
"single_households": 4014608,
"age_under15": 1445383,
"age_15_64": 9064654,
"age_65plus": 3108083,
"population_previous_rearranged": 14047598
},
"age_unknown_null_codes": [
"13362",
"13363",
"13364",
"13382"
]
}
{"status": "PASS", "rows": 62, "census_year": 2025, "source_run_id": "census2025_20261020_01", "target": "/Volumes/blog_catalog/opendata/census_work/runs/census2025_20261020_01/curated/tokyo_year"}
+---------+----------+----------+------------------+----------+------------------------------+----------------------------+---------------------+-----------+---------+----------+-----------+---------------+---------------------------------+--------------+-----------------+-----------------+-----------+--------------+------------------------+--------+--------------+---------------------+
|area_code|area_name |population|general_households|households|population_previous_rearranged|population_change_pct_source|population_source_row|age_under15|age_15_64|age_65plus|age_unknown|age_unknown_raw|age_65plus_share_known_source_pct|age_source_row|single_households|single_source_row|census_year|reference_date|population_previous_year|series |boundary_basis|unknown_policy |
+---------+----------+----------+------------------+----------+------------------------------+----------------------------+---------------------+-----------+---------+----------+-----------+---------------+---------------------------------+--------------+-----------------+-----------------+-----------+--------------+------------------------+--------+--------------+---------------------+
|13101 |千代田区 |66270 |36904 |36969 |66680 |-0.61488 |1229 |8576 |46923 |10758 |13 |13 |16.23677 |1226 |21232 |26962 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13102 |中央区 |181841 |99164 |99259 |169179 |7.48438 |1230 |24880 |130995 |25834 |132 |132 |14.21724 |1227 |52332 |27002 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13103 |港区 |272478 |152581 |152721 |260486 |4.6037 |1231 |33069 |174435 |45483 |19491 |19491 |17.97839 |1228 |86951 |27042 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13104 |新宿区 |361357 |237431 |237866 |349385 |3.42659 |1232 |27368 |236445 |62748 |34796 |34796 |19.21479 |1229 |167061 |27082 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13105 |文京区 |249800 |141709 |141860 |240069 |4.05342 |1233 |28395 |161016 |42717 |17672 |17672 |18.40235 |1230 |84881 |27122 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13106 |台東区 |228046 |141697 |143691 |211444 |7.85172 |1234 |17652 |160540 |46067 |3787 |3787 |20.54187 |1231 |91667 |27162 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13107 |墨田区 |285038 |158272 |158437 |272085 |4.76064 |1235 |26461 |200020 |58431 |126 |126 |20.50844 |1232 |86371 |27202 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13108 |江東区 |553502 |296483 |296712 |524310 |5.5677 |1236 |62336 |357476 |110581 |23109 |23109 |20.84888 |1233 |156727 |27242 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13109 |品川区 |426005 |246145 |246273 |422488 |0.83245 |1237 |45086 |277470 |77457 |25992 |25992 |19.36362 |1234 |147077 |27282 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13110 |目黒区 |286082 |158723 |158877 |288088 |-0.69632 |1238 |30109 |199502 |56284 |187 |187 |19.68695 |1235 |88137 |27322 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13111 |大田区 |758181 |417771 |418208 |748081 |1.35012 |1239 |71225 |494271 |160167 |32518 |32518 |22.07182 |1236 |232766 |27362 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13112 |世田谷区 |954737 |508796 |509227 |943664 |1.1734 |1240 |102941 |652401 |196828 |2567 |2567 |20.67152 |1237 |267949 |27402 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13113 |渋谷区 |238826 |152013 |152186 |243883 |-2.07354 |1241 |18179 |126594 |37664 |56389 |56389 |20.64493 |1238 |102635 |27442 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13114 |中野区 |354853 |222092 |222326 |344880 |2.89173 |1242 |28816 |239351 |64689 |21997 |21997 |19.43453 |1239 |145927 |27482 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13115 |杉並区 |597038 |343860 |344230 |591108 |1.0032 |1243 |56609 |386561 |119880 |33988 |33988 |21.29118 |1240 |205217 |27522 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13116 |豊島区 |305516 |190061 |190181 |301599 |1.29874 |1244 |25659 |220116 |56735 |3006 |3006 |18.75475 |1241 |124321 |27562 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13117 |北区 |367827 |204290 |204480 |355213 |3.55111 |1245 |36475 |243750 |82800 |4802 |4802 |22.80835 |1242 |114163 |27602 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13118 |荒川区 |226291 |122975 |123290 |217475 |4.0538 |1246 |22433 |149132 |47948 |6778 |6778 |21.8429 |1243 |66455 |27642 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13119 |板橋区 |595972 |333654 |334084 |584483 |1.96567 |1247 |55466 |382738 |132366 |25402 |25402 |23.19891 |1244 |191771 |27682 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13120 |練馬区 |760819 |389866 |390252 |752608 |1.09101 |1248 |81574 |485721 |163814 |29710 |29710 |22.40623 |1245 |194264 |27722 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13121 |足立区 |701667 |363727 |364043 |695043 |0.95303 |1249 |68213 |443414 |166829 |23211 |23211 |24.58951 |1246 |184644 |27762 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13122 |葛飾区 |467267 |236614 |236835 |453093 |3.12828 |1250 |48341 |301552 |111869 |5505 |5505 |24.22655 |1247 |114990 |27802 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13123 |江戸川区 |704696 |351887 |352189 |697932 |0.96915 |1251 |76305 |458249 |145557 |24585 |24585 |21.40195 |1248 |168518 |27842 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13201 |八王子市 |574428 |277968 |278494 |579355 |-0.85043 |1252 |46599 |295694 |148747 |83388 |83388 |30.29224 |1249 |125255 |27882 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13202 |立川市 |189304 |95729 |95874 |183581 |3.11743 |1253 |20392 |117267 |45555 |6090 |6090 |24.86437 |1250 |46826 |27922 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13203 |武蔵野市 |150528 |79460 |79541 |150149 |0.25242 |1254 |16357 |94932 |32822 |6417 |6417 |22.7755 |1251 |41063 |27962 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13204 |三鷹市 |194892 |98384 |98558 |195391 |-0.25539 |1255 |22048 |122618 |42477 |7749 |7749 |22.69762 |1252 |48624 |28002 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13205 |青梅市 |129778 |57839 |58207 |133535 |-2.81349 |1256 |11408 |73878 |43186 |1306 |1306 |33.61511 |1253 |22148 |28042 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13206 |府中市 |263084 |128750 |128916 |262790 |0.11188 |1257 |29451 |168016 |58734 |6883 |6883 |22.92497 |1254 |59885 |28082 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13207 |昭島市 |116590 |55485 |55552 |113949 |2.3177 |1258 |13374 |70818 |30604 |1794 |1794 |26.65947 |1255 |23465 |28122 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13208 |調布市 |245306 |125400 |125525 |242614 |1.10958 |1259 |26941 |155440 |51828 |11097 |11097 |22.12895 |1256 |61940 |28162 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13209 |町田市 |420385 |198537 |198913 |431083 |-2.48166 |1260 |38868 |218075 |111254 |52188 |52188 |30.21589 |1257 |84408 |28202 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13210 |小金井市 |128953 |65719 |65856 |126074 |2.28358 |1261 |15165 |81308 |27092 |5388 |5388 |21.9253 |1258 |32501 |28242 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13211 |小平市 |198691 |91516 |91732 |198739 |-0.02415 |1262 |23906 |123072 |47227 |4486 |4486 |24.31812 |1259 |38122 |28282 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13212 |日野市 |190616 |90933 |91128 |190435 |0.09505 |1263 |21615 |119105 |46393 |3503 |3503 |24.79411 |1260 |39264 |28322 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13213 |東村山市 |152134 |71765 |71879 |151815 |0.21012 |1264 |16437 |90961 |41060 |3676 |3676 |27.65765 |1261 |30108 |28362 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13214 |国分寺市 |128033 |61656 |61727 |129242 |-0.93545 |1265 |15790 |83646 |28329 |268 |268 |22.17274 |1262 |26582 |28402 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13215 |国立市 |76546 |38747 |38808 |77130 |-0.75716 |1266 |7926 |48227 |18698 |1695 |1695 |24.98029 |1263 |18525 |28442 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13218 |福生市 |57117 |30070 |30123 |56414 |1.24614 |1267 |5172 |34538 |15592 |1815 |1815 |28.19428 |1264 |15421 |28482 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13219 |狛江市 |84031 |43266 |43306 |84772 |-0.87411 |1268 |9137 |51668 |19890 |3336 |3336 |24.64837 |1265 |21645 |28522 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13220 |東大和市 |83163 |37318 |37366 |83901 |-0.87961 |1269 |9573 |50087 |23361 |142 |142 |28.13866 |1266 |13253 |28562 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13221 |清瀬市 |74768 |35651 |35743 |76208 |-1.88957 |1270 |7791 |42461 |20885 |3631 |3631 |29.35884 |1267 |15897 |28602 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13222 |東久留米市|115239 |53942 |53990 |115271 |-0.02776 |1271 |13092 |66353 |33061 |2733 |2733 |29.38599 |1268 |21516 |28642 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13223 |武蔵村山市|69057 |30653 |30695 |70829 |-2.5018 |1272 |7872 |41447 |18686 |1052 |1052 |27.47739 |1269 |11295 |28682 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13224 |多摩市 |141575 |68329 |68376 |146951 |-3.65836 |1273 |13892 |85080 |42574 |29 |29 |30.07785 |1270 |28020 |28722 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13225 |稲城市 |95120 |42344 |42381 |93151 |2.11377 |1274 |12374 |59728 |21198 |1820 |1820 |22.72026 |1271 |15905 |28762 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13227 |羽村市 |53438 |24480 |24518 |54326 |-1.63458 |1275 |4947 |28923 |14242 |5326 |5326 |29.60176 |1272 |9484 |28802 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13228 |あきる野市|77141 |32364 |32429 |79292 |-2.71276 |1276 |8323 |44576 |24095 |147 |147 |31.29465 |1273 |9990 |28842 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13229 |西東京市 |207409 |100029 |100165 |207388 |0.01013 |1277 |23396 |128430 |50117 |5466 |5466 |24.8174 |1274 |43334 |28882 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13303 |瑞穂町 |31097 |13603 |13634 |31765 |-2.10294 |1280 |3075 |18393 |9576 |53 |53 |30.84654 |1277 |4675 |28922 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13305 |日の出町 |16078 |5898 |5938 |16958 |-5.18929 |1281 |1586 |7371 |6269 |852 |852 |41.17299 |1278 |1674 |28962 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13307 |檜原村 |1764 |750 |753 |2003 |-11.9321 |1282 |122 |669 |963 |10 |10 |54.90308 |1279 |284 |29002 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13308 |奥多摩町 |4206 |1815 |1825 |4750 |-11.45263 |1283 |273 |1584 |2294 |55 |55 |55.26379 |1280 |711 |29042 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13361 |大島町 |6297 |3392 |3407 |7102 |-11.33484 |1284 |577 |3276 |2443 |1 |1 |38.80241 |1281 |1836 |29082 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13362 |利島村 |304 |200 |201 |327 |-7.03364 |1285 |33 |197 |74 |NULL |- |24.34211 |1282 |131 |29122 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13363 |新島村 |2167 |1105 |1106 |2441 |-11.22491 |1286 |220 |1056 |891 |NULL |- |41.11675 |1283 |506 |29162 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13364 |神津島村 |1654 |784 |788 |1855 |-10.83558 |1287 |227 |834 |593 |NULL |- |35.85248 |1284 |347 |29202 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13381 |三宅村 |1986 |1244 |1246 |2273 |-12.62648 |1288 |178 |968 |778 |62 |62 |40.43659 |1285 |777 |29242 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13382 |御蔵島村 |294 |175 |175 |323 |-8.97833 |1289 |44 |185 |65 |NULL |- |22.10884 |1286 |110 |29282 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13401 |八丈町 |6466 |3709 |3723 |7042 |-8.17949 |1290 |675 |3023 |2489 |279 |279 |40.22951 |1287 |2074 |29322 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13402 |青ヶ島村 |168 |124 |124 |169 |-0.59172 |1291 |15 |111 |38 |4 |4 |23.17073 |1288 |93 |29362 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
|13421 |小笠原村 |2711 |1379 |1392 |2929 |-7.44281 |1292 |344 |1967 |397 |3 |3 |14.66027 |1289 |858 |29402 |2025 |2025-10-01 |2020 |original|survey_date |preserve_dash_as_null|
+---------+----------+----------+------------------+----------+------------------------------+----------------------------+---------------------+-----------+---------+----------+-----------+---------------+---------------------------------+--------------+-----------------+-----------------+-----------+--------------+------------------------+--------+--------------+---------------------+
● 2020年と2025年の保存結果を確認する
1) 両年の保存結果を別Notebookから確認する
nb00_check_and_capture に戻り、次を実行します。
import json
from pathlib import Path
WORK = Path("/Volumes/blog_catalog/opendata/census_work")
run_ids = {
2020: "census2020_20261020_01",
2025: "census2025_20261020_01",
}
for year, run_id in run_ids.items():
root = WORK / "runs" / run_id
meta = json.loads((root / "validation.json").read_text(encoding="utf-8"))
print(year, meta["status"], meta["rows"], meta["target"])
assert meta["status"] == "PASS" and meta["census_year"] == year
assert meta["rows"] == 62
df = spark.read.parquet(str(root / "curated" / "tokyo_year"))
df.select("area_code", "area_name", "census_year", "population").orderBy("area_code").show(5, truncate=False)
掲載した取込み結果では、2020年の東京都人口計は14,047,594人、2025年は14,236,627人でした。これは取込み対象62市区町村の検査に使った東京都計です。後段の機械学習は町田市を除く61市区町村が対象になるため、集計対象を区別します。

■ nb02で2020年と2025年を比較する
・ nb02の各Cellは何をしているのか
nb02では、nb01が保存した2020年・2025年の表を読みます。最初に「同じ地域として比較してよいか」を確認し、条件が揃った対象について人口や割合の変化をSparkで計算します。
| 画面のCell | 種類・役割 | 何を処理するか | ここで確認すること |
|---|---|---|---|
| Cell 1 | Widgetの作成 | 両年の入力run ID、今回の比較run ID、境域の扱いを設定します | 入力はnb01で成功したID、出力は未使用のIDか |
| Cell 2 | 説明文 | 年度比較と境域検査の方針を説明します | 最初はstrictで差を調べること |
| Cell 3 | 設定・出力先の準備 | WidgetとIDを確認し、今回の比較用フォルダを新規作成します | 同じIDで繰り返すと、既存フォルダを検知して停止すること |
| Cell 4 | 説明文 | 検査関数を説明します | 次のCellで検査方法を定義すること |
| Cell 5 | 関数の定義 | 年別のデータ検査、境域差の確認、照合用の変化量計算、ハッシュ計算を準備します | この時点では両年のファイルの比較はまだ動きません |
| Cell 6 | 入力・境域検査 | 両年のvalidation.jsonとParquetを読み、年度・件数・内容の一致を確認。2025年表の「2020年人口(組替)」と2020年原数値を突合します |
strictで差を検知すると記録して停止。今回は町田市の4人差を確認します |
| Cell 7 | 説明文 | 割合の分母、差の単位、NULLの扱いを説明します | 割合の差はポイントで表すこと |
| Cell 8 | 変化量の計算・照合 | 地域コードで両年の表を結合し、確定した対象について3指標と年齢不詳率の変化を計算。Python側の照合用計算とも比較します | 計算結果の件数と値。ここに表示される表は次のCellで保存します |
| Cell 9 | 保存・読戻し検査 | 比較表をParquet・CSVに保存し、Parquetの読戻しを検査。定義・対象範囲・検査結果を記録します | 今回はPASS、61件、除外コード13209
|
最初のstrict実行はCell 6で止まることを確認する手順です。 原因を確認して分析対象を決めた後、別の比較run IDでCell 9まで進めます。差を見つけたことと、比較表の作成に成功したことを区別して読みます。
想定した停止は、保存された検査結果が BOUNDARY_MISMATCH で、町田市の4人差を示す場合です。別のエラーは原因を確認してから進めます。
● 年度間の境域の違いを検出する
1) まず厳格な境域検査を行う
nb02_compare_years を開き、Computeを接続します。先頭へ次のPythonセルを追加し、このセルだけを実行してWidgetを表示します。
defaults = {
"work_root": "/Volumes/blog_catalog/opendata/census_work",
"source_run_id_2020": "census2020_20261020_01",
"source_run_id_2025": "census2025_20261020_01",
"comparison_run_id": "compare_strict_20261020_01",
"boundary_policy": "strict",
}
for name, value in defaults.items():
aidputils.widgets.text(name=name, defaultValue=value, label=name)
for name in defaults:
print(name, "=", aidputils.widgets.get(name))
Widgetの値がこの表と一致していることを確認してから [Run all] をクリックします。
| Widget | 値 |
|---|---|
source_run_id_2020 |
census2020_20261020_01 |
source_run_id_2025 |
census2025_20261020_01 |
comparison_run_id |
compare_strict_20261020_01 |
boundary_policy |
strict |
nb02の元の先頭セルは出力フォルダを作成します。Widgetを表示する目的だけで元の処理セルを実行してから、同じIDで再実行すると FileExistsError になることがあります。そのため、先に追加したWidget専用セルで値を揃えます。
{'source_run_id_2020': 'census2020_20261020_01', 'source_run_id_2025': 'census2025_20261020_01', 'comparison_run_id': 'compare_strict_20261020_01', 'boundary_policy': 'strict'}
{
"accepted": false,
"differences": [
{
"area_code": "13209",
"area_name": "町田市",
"population_2020_original": 431079,
"population_2020_rearranged": 431083,
"difference": 4
}
],
"input_municipality_count": 62,
"rows": 62,
"excluded_area_codes": [],
"excluded_reasons": [],
"boundary_policy": "strict",
"caveat": "Population agreement is a check, not proof that every demographic definition is unchanged."
}
opc-request-id: csid69f4ca6e449e90b54fb075ad600d/b58e8d6de5da45c99e779725473dc56a/7CD630A1BBA64EC4A56715B1DD3ABFF6
Command ID b1d4b694-fa1e-4432-8d7b-a68edcae0d0e failed with java.lang.RuntimeException: [Command b1d4b694-fa1e-4432-8d7b-a68edcae0d0e in Context 354e198f-3219-46f4-8154-b4093f960940] failed with error:
---------------------------------------------------------------------------ValueError Traceback (most recent call last)Cell In[25], line 27
25 with (ROOT/"validation.json").open("x") as f:
26 json.dump({"status":"FAIL","reason":"BOUNDARY_MISMATCH",**scope},f,ensure_ascii=False,indent=2)
---> 27 raise ValueError("Boundary mismatch. Inspect boundary_comparison.json; do not change the tolerance. Use a new run ID for a documented scope change.")
ValueError: Boundary mismatch. Inspect boundary_comparison.json; do not change the tolerance. Use a new run ID for a documented scope change.
2) 町田市の差分で停止することを確認する
今回のAIDP実行でも、次の差を検出して停止しました。以下のエラーは、比較条件の違いを検査で見つけた記録です。
| 地域コード | 地域 | 2020年原本 | 2025年表にある2020年人口(組替) | 差 |
|---|---|---|---|---|
| 13209 | 町田市 | 431,079 | 431,083 | +4 |
{
"accepted": false,
"differences": [
{
"area_code": "13209",
"area_name": "町田市",
"population_2020_original": 431079,
"population_2020_rearranged": 431083,
"difference": 4
}
],
"input_municipality_count": 62,
"rows": 62,
"excluded_area_codes": [],
"excluded_reasons": [],
"boundary_policy": "strict",
"caveat": "Population agreement is a check, not proof that every demographic definition is unchanged."
}
opc-request-id: csid69f4ca6e449e90b54fb075ad600d/b58e8d6de5da45c99e779725473dc56a/7CD630A1BBA64EC4A56715B1DD3ABFF6
Command ID b1d4b694-fa1e-4432-8d7b-a68edcae0d0e failed with java.lang.RuntimeException: [Command b1d4b694-fa1e-4432-8d7b-a68edcae0d0e in Context 354e198f-3219-46f4-8154-b4093f960940] failed with error:
---------------------------------------------------------------------------ValueError Traceback (most recent call last)Cell In[25], line 27
25 with (ROOT/"validation.json").open("x") as f:
26 json.dump({"status":"FAIL","reason":"BOUNDARY_MISMATCH",**scope},f,ensure_ascii=False,indent=2)
---> 27 raise ValueError("Boundary mismatch. Inspect boundary_comparison.json; do not change the tolerance. Use a new run ID for a documented scope change.")
ValueError: Boundary mismatch. Inspect boundary_comparison.json; do not change the tolerance. Use a new run ID for a documented scope change.
strict では、この差を保存して停止します。今回の検査で意図した停止かどうかを、画面の例外と保存したJSONで確認します。どのエラーでも想定どおりというわけではありません。
確認用Notebookのnb00_check_and_captureで次を実行します。
import json
from pathlib import Path
root = Path("/Volumes/blog_catalog/opendata/census_work/comparisons/compare_strict_20261020_01")
for filename in ["boundary_comparison.json", "validation.json"]:
print(filename)
print(json.dumps(json.loads((root / filename).read_text(encoding="utf-8")), ensure_ascii=False, indent=2))
validation.json の status=FAIL、reason=BOUNDARY_MISMATCH と、町田市の4人差を確認します。
boundary_comparison.json
{
"accepted": false,
"differences": [
{
"area_code": "13209",
"area_name": "町田市",
"population_2020_original": 431079,
"population_2020_rearranged": 431083,
"difference": 4
}
],
"input_municipality_count": 62,
"rows": 62,
"excluded_area_codes": [],
"excluded_reasons": [],
"boundary_policy": "strict",
"caveat": "Population agreement is a check, not proof that every demographic definition is unchanged."
}
validation.json
{
"status": "FAIL",
"reason": "BOUNDARY_MISMATCH",
"accepted": false,
"differences": [
{
"area_code": "13209",
"area_name": "町田市",
"population_2020_original": 431079,
"population_2020_rearranged": 431083,
"difference": 4
}
],
"input_municipality_count": 62,
"rows": 62,
"excluded_area_codes": [],
"excluded_reasons": [],
"boundary_policy": "strict",
"caveat": "Population agreement is a check, not proof that every demographic definition is unchanged."
}
相模原市は、2020年12月1日に町田市との行政境界を一部変更したと公表しています。人口だけ組替値に置き換えても、2020年の世帯・年齢別人口まで同じ境域には揃いません。今回は町田市を機械学習から除外し、その理由を残します。
・ 4人差でも町田市を除外した理由
4人を2020年の町田市人口431,079人で割ると、約 0.00093%(約0.001%) です。量としては小さく、4人差そのものが機械学習結果を大きく変えると判断したわけではありません。今回は 差の大きさではなく、人口・世帯・年齢を同じ境域で比較できるかを基準にした保守的な選択 として除外しました。
人口だけを組替値へ変更しても、単独世帯割合や年齢別人口の比較条件まで自動的に揃うわけではありません。各項目を同じ境域へ組み替えられることを確認できれば、町田市を含めて再分析する余地があります。
掲載ログの東京都計でも、2020年原数値14,047,594人に対し、2025年表の2020年人口(組替)は14,047,598人で、差はちょうど4人です。市区町村別検査で見つかった町田市の差と整合します。ただし、人口の整合だけで世帯・年齢の比較条件まで検証できたことにはなりません。
● 比較する地域の範囲を確定する
1) 分析対象を61市区町村として再実行する
nb02のWidgetを次のように変更します。
| Widget | 値 |
|---|---|
comparison_run_id |
compare61_20261020_01 |
boundary_policy |
exclude_documented_machida |
| 両年のsource_run_id | 変更なし |
新しいcomparison_run_id で [Run all] をクリックします。差を許容誤差として丸める処理ではなく、記録した根拠に基づき分析対象を変える操作です。町田市以外の差など、未確認の差があれば停止します。
確認すること :最後がPASS、件数が61、除外コードが 13209、除外理由と根拠URLが保存されていること。
{'source_run_id_2020': 'census2020_20261020_01', 'source_run_id_2025': 'census2025_20261020_01', 'comparison_run_id': 'compare61_20261020_01', 'boundary_policy': 'exclude_documented_machida'}
{
"accepted": true,
"differences": [
{
"area_code": "13209",
"area_name": "町田市",
"population_2020_original": 431079,
"population_2020_rearranged": 431083,
"difference": 4
}
],
"input_municipality_count": 62,
"rows": 61,
"excluded_area_codes": [
"13209"
],
"excluded_reasons": [
{
"area_code": "13209",
"area_name": "町田市",
"reason": "2020-12-01境界変更。人口だけを組み替えても世帯・年齢の比較条件は揃わないため除外。",
"effective_date": "2020-12-01",
"source_url": "https://www.city.sagamihara.kanagawa.jp/shisei/1026766/seido/1026777/1004437.html",
"supporting_url": "https://www.gikai-machida.jp/voices/GikaiDoc/attach/Gk/Gk5001_64_somu_gyosei_kyokaihenko.pdf"
}
],
"boundary_policy": "exclude_documented_machida",
"caveat": "Population agreement is a check, not proof that every demographic definition is unchanged."
}
+---------+---------------+--------------------------+-------------------------------+--------------------+---------------+--------------------------+-------------------------------+--------------------+---------+---------------------+----------------------+-----------------------+---------------------+
|area_code|population_2020|single_households_pct_2020|age_65plus_share_known_pct_2020|age_unknown_pct_2020|population_2025|single_households_pct_2025|age_65plus_share_known_pct_2025|age_unknown_pct_2025|area_name|population_growth_pct|single_share_change_pp|elderly_share_change_pp|age_unknown_change_pp|
+---------+---------------+--------------------------+-------------------------------+--------------------+---------------+--------------------------+-------------------------------+--------------------+---------+---------------------+----------------------+-----------------------+---------------------+
|13106 |211444 |59.04884128433574 |22.719773519580322 |1.1024195531677419 |228046 |64.69226589130326 |20.54187345881325 |1.660629872920376 |台東区 |7.851724333629706 |5.643424606967528 |-2.1779000607670724 |0.558210319752634 |
|13102 |169179 |52.597662374171506 |14.606535452641047 |0.11408035276245869 |181841 |52.77318381670768 |14.217237451089378 |0.07259088984332467 |中央区 |7.484380449110106 |0.17552144253617286 |-0.38929800155166916 |-0.041489462919134024|
|13108 |524310 |46.75609876150558 |21.22812095949362 |1.167248383589861 |553502 |52.862052798980045 |20.84887998144772 |4.1750526646696855 |江東区 |5.567698498979601 |6.1059540374744685 |-0.3792409780458996 |3.0078042810798244 |
|13107 |272085 |52.16710505531938 |22.117557015962262 |0.117242773398019 |285038 |54.57124443995147 |20.50843769304206 |0.0442046323648075 |墨田区 |4.760644651487578 |2.4041393846320886 |-1.6091193229202005 |-0.0730381410332115 |
|13103 |260486 |56.884395279428276 |16.873557383420717 |3.2017075773745995 |272478 |56.98678079184171 |17.978394146734814 |7.153238059586461 |港区 |4.603702310296898 |0.1023855124134343 |1.104836763314097 |3.9515304822118615 |
|13118 |217475 |49.52459324323116 |23.334428824692065 |0.6612254282101391 |226291 |54.039438910347634 |21.842897687152927 |2.9952583178296974 |荒川区 |4.053799287274407 |4.514845667116475 |-1.4915311375391376 |2.334032889619558 |
|13105 |240069 |57.88685573957054 |18.851249198081675 |7.150444247278907 |249800 |59.89810103804275 |18.402346980976013 |7.074459567654124 |文京区 |4.053417975665319 |2.011245298472211 |-0.4489022171056618 |-0.07598467962478317 |
|13117 |355213 |52.22888611080341 |24.693943298969074 |0.38202430654283487 |367827 |55.88281364726614 |22.80834653260795 |1.305505033616347 |北区 |3.5511087713569145 |3.6539275364627315 |-1.8855967663611253 |0.9234807270735121 |
|13104 |349385 |67.80109772049933 |19.99651876701057 |9.561085908095654 |361357 |70.3619156723427 |19.2147868239012 |9.629258600220835 |新宿区 |3.426592441003473 |2.560817951843376 |-0.7817319431093708 |0.06817269212518084 |
|13122 |453093 |43.56517345127 |24.897367033289797 |0.8113124678597992 |467267 |48.59813874073385 |24.226549607806618 |1.1781272805483802 |葛飾区 |3.128276093428939 |5.032965289463853 |-0.6708174254831789 |0.36681481268858107 |
|13202 |183581 |45.643366555430305 |24.832672386989085 |0.1405374194497252 |189304 |48.91516677286924 |24.86436626022029 |3.2170477116172926 |立川市 |3.117425005855723 |3.271800217438937 |0.031693873231205316 |3.0765102921675673 |
|13114 |344880 |62.348036009694916 |20.176040377806906 |5.324750637903039 |354853 |65.70565351295859 |19.434530247314154 |6.1989048986481725 |中野区 |2.8917304569705315 |3.3576175032636755 |-0.7415101304927525 |0.8741542607451338 |
|13207 |113949 |39.23547811402583 |26.61492060675951 |1.0697768299853445 |116590 |42.290709200684866 |26.65946548660232 |1.5387254481516426 |昭島市 |2.317703534037152 |3.0552310866590346 |0.04454487984280675 |0.46894861816629807 |
|13210 |126074 |48.28159645232816 |21.155181535612723 |4.050002379554864 |128953 |49.454495655746435 |21.925302472382956 |4.178266500197746 |小金井市 |2.2835794850643243 |1.1728992034182752 |0.7701209367702333 |0.12826412064288206 |
|13225 |93151 |34.93731193580742 |21.9763027471746 |1.8765230647013988 |95120 |37.561401851501984 |22.720257234726688 |1.9133725820016823 |稲城市 |2.113772262240876 |2.6240899156945616 |0.743954487552088 |0.03684951730028341 |
+---------+---------------+--------------------------+-------------------------------+--------------------+---------------+--------------------------+-------------------------------+--------------------+---------+---------------------+----------------------+-----------------------+---------------------+
only showing top 15 rows
{"status": "PASS", "rows": 61, "target": "/Volumes/blog_catalog/opendata/census_work/comparisons/compare61_20261020_01/curated/tokyo_changes"}
● 人口と割合の変化を確認する
・ 増減率と、割合の差を区別する
次の数値例は、計算方法を説明するための架空の値です。
| 指標 | 計算方法 | 例・読み方 |
|---|---|---|
| 人口増減率 | (2025年人口 ÷ 2020年人口 − 1) × 100 |
10,000人→10,500人なら+5%。5年間の増減率であり、年平均ではありません |
| 単独世帯割合 | 単独世帯数 ÷ 一般世帯数 × 100 |
1人暮らしの世帯が、一般世帯の中でどれだけを占めるか |
| 単独世帯割合の差 | 2025年の割合 − 2020年の割合 |
40%→45%なら+5ポイント(pp)。世帯数そのものの増加とは区別します |
| 高齢者割合 | 65歳以上人口 ÷ 年齢既知人口 × 100 |
分母は15歳未満+15〜64歳+65歳以上の人口。年齢不詳を除きます |
| 高齢者割合の差 | 2025年の割合 − 2020年の割合 |
25%→27%なら+2ポイント
|
| 年齢不詳率 | 年齢不詳人口 ÷ 総人口 × 100 |
年齢が分からない人の割合。NULLの場合は率もNULLのまま保持します |
40%→45%の変化を相対的な増加率で表すなら12.5%ですが、この分析で使うのは 差の5ポイント です。列名の_pctは百分率、_ppは割合の差を表します。年齢不詳率の差もポイントで記録し、高齢者割合の変化を解釈する際の補助情報にします。
・ 確認用コードの読み方
下のコードは、validation.jsonでPASSと件数を確認し、保存済み比較表から見たい列を選んで表示します。show(10, ...)は画面に10行だけ表示する指定です。 分析対象が10市区町村に減るわけではありません。 比較表自体は61行あります。
1) 比較テーブルを表示する
確認用Notebookのnb00_check_and_captureで次を実行します。
import json
from pathlib import Path
root = Path("/Volumes/blog_catalog/opendata/census_work/comparisons/compare61_20261020_01")
meta = json.loads((root / "validation.json").read_text(encoding="utf-8"))
print({key: meta[key] for key in ["status", "rows", "input_municipality_count", "excluded_area_codes"]})
assert meta["status"] == "PASS" and meta["rows"] == 61
changes = spark.read.parquet(str(root / "curated" / "tokyo_changes"))
changes.select(
"area_code", "area_name", "population_2020", "population_2025",
"population_growth_pct", "single_share_change_pp", "elderly_share_change_pp",
"age_unknown_pct_2020", "age_unknown_pct_2025", "age_unknown_change_pp"
).orderBy("area_code").show(10, truncate=False)
人口増減率の単位は %、単独世帯割合・高齢者割合の変化は「ポイント」です。高齢者割合の分母は両年とも年齢既知人口です。年齢不詳率がNULLの行では、NULLを0と読み替えません。
{'status': 'PASS', 'rows': 61, 'input_municipality_count': 62, 'excluded_area_codes': ['13209']}
+---------+---------+---------------+---------------+---------------------+----------------------+-----------------------+--------------------+--------------------+---------------------+
|area_code|area_name|population_2020|population_2025|population_growth_pct|single_share_change_pp|elderly_share_change_pp|age_unknown_pct_2020|age_unknown_pct_2025|age_unknown_change_pp|
+---------+---------+---------------+---------------+---------------------+----------------------+-----------------------+--------------------+--------------------+---------------------+
|13101 |千代田区 |66680 |66270 |-0.6148770245950841 |0.5138774035136322 |-0.12014716710469031 |0.5023995200959808 |0.019616719480911423|-0.48278280061506934 |
|13102 |中央区 |169179 |181841 |7.484380449110106 |0.17552144253617286 |-0.38929800155166916 |0.11408035276245869 |0.07259088984332467 |-0.041489462919134024|
|13103 |港区 |260486 |272478 |4.603702310296898 |0.1023855124134343 |1.104836763314097 |3.2017075773745995 |7.153238059586461 |3.9515304822118615 |
|13104 |新宿区 |349385 |361357 |3.426592441003473 |2.560817951843376 |-0.7817319431093708 |9.561085908095654 |9.629258600220835 |0.06817269212518084 |
|13105 |文京区 |240069 |249800 |4.053417975665319 |2.011245298472211 |-0.4489022171056618 |7.150444247278907 |7.074459567654124 |-0.07598467962478317 |
|13106 |台東区 |211444 |228046 |7.851724333629706 |5.643424606967528 |-2.1779000607670724 |1.1024195531677419 |1.660629872920376 |0.558210319752634 |
|13107 |墨田区 |272085 |285038 |4.760644651487578 |2.4041393846320886 |-1.6091193229202005 |0.117242773398019 |0.0442046323648075 |-0.0730381410332115 |
|13108 |江東区 |524310 |553502 |5.567698498979601 |6.1059540374744685 |-0.3792409780458996 |1.167248383589861 |4.1750526646696855 |3.0078042810798244 |
|13109 |品川区 |422488 |426005 |0.8324496790441449 |2.467816188834142 |-0.5565802929935835 |5.499564484671754 |6.101336838769498 |0.6017723540977444 |
|13110 |目黒区 |288088 |286082 |-0.6963150148565744 |2.3336469438334433 |0.0541289646019969 |1.32390103024076 |0.06536587412000755 |-1.2585351561207523 |
+---------+---------+---------------+---------------+---------------------+----------------------+-----------------------+--------------------+--------------------+---------------------+
only showing top 10 rows
■ nb03で機械学習を実行する
● 学習に使う比較データを指定する
1) 機械学習の入力と出力IDを設定する
nb03_cluster_changes を開き、Computeを接続します。先頭へ次のWidget専用セルを追加し、そのセルだけを実行します。
defaults = {
"work_root": "/Volumes/blog_catalog/opendata/census_work",
"comparison_run_id": "compare61_20261020_01",
"ai_run_id": "change_ml_20261020_01",
}
for name, value in defaults.items():
aidputils.widgets.text(name=name, defaultValue=value, label=name)
for name in defaults:
print(name, "=", aidputils.widgets.get(name))
work_root = /Volumes/blog_catalog/opendata/census_work
comparison_run_id = compare61_20261020_01
ai_run_id = change_ml_20261020_01
比較に成功した compare61_20261020_01 を指定します。停止した compare_strict_20261020_01 は指定しません。その他の検索対象・近隣件数のパラメータは、実行セットの既定値を使うと新宿区に近い5件を確認できます。
● K-meansで分類し、グループ数を選ぶ
・ nb03の各Cellは何をしているのか
nb03で機械学習に使うのは、 人口増減率、単独世帯割合の差、高齢者割合の差の3列 です。年齢不詳率は解釈のために保持し、クラスタ分けの入力には混ぜません。
| 画面のCell | 種類・役割 | 何を処理するか | ここで確認すること |
|---|---|---|---|
| Cell 1 | Widgetの作成 | 比較run IDと新しい機械学習run IDを指定します | 比較に成功した61件のIDを使うこと |
| Cell 2 | 説明文 | 教師なし学習の目的、3指標、グループ数の選び方を説明します | 将来予測や地域の優劣を判定する分析ではないこと |
| Cell 3 | 設定の読取り | 入出力先、新宿区の地域コード13104、似た街の表示件数などを確認します |
inputとoutput。既存の学習結果は上書きしません |
| Cell 4 | 説明文 | 学習と検査の共通処理を説明します | 次のCellで処理を定義すること |
| Cell 5 | 関数の定義 | 入力検査、標準化、K-meansの評価、グループ集計、似た街の検索、グラフ作成を準備します | 長いCellですが、ここでは学習はまだ始まりません |
| Cell 6 | 学習の実行 | 比較表と検査記録を照合し、run_analysis(...)を呼びます。標準化して候補を学習・評価し、分類・集計・近い街の一覧を作ります |
対象61件、k_summaryの候補別評価、町田市の除外記録 |
| Cell 7 | 説明文 | 結果の保存と読戻し検査を説明します | 生成AI用の平均値は小数点以下2桁にすること |
| Cell 8 | 保存・読戻し検査 | 分類結果、グループ集計、評価表、生成AI用JSON、グラフを保存。ParquetとJSONを読み戻して一致を確認します | 最後のPASS、61件、selected_k=2、検査項目のPASS |
| Cell 9 | 説明文 | 図の解釈について説明します | 人数、平均、対象範囲などを確認すること |
| Cell 10 | 図の表示 | Cell 8で保存したグラフをNotebookへ表示します | このCellは表示のみ。機械学習をやり直しません |
Sparkは比較表の読込み・結果の保存に使います。今回のK-meansは、少量の61行をPython側へ取り出し、 Compute上でscikit-learnを使って学習 します。生成AIへ質問して分類する処理ではありません。
・ 標準化とK-meansを、今回のデータで考える
1つの市区町村を、3つの変化量を持つ点として扱います。例えば「人口が増え、単独世帯割合も上がり、高齢者割合は下がる」という点と、似た変化の点をまとめます。分類の正解ラベルを事前に渡さずにグループを見つけるため、 教師なし学習 と呼びます。
3列は数値の広がりが異なるため、そのまま距離を計算すると、広がりの大きい列の影響を受けやすくなります。StandardScalerで各列を「その列の平均から、標準偏差の何個分離れているか」に変換してからK-meansへ渡します。元の比較表を書き換える操作ではなく、学習用の値を別に作ります。
K-meansでは、点を近い中心に割り当て、各グループの中心を計算し直すことを繰り返します。ここでのkはグループ数です。学習後は元の単位の指標でグループ平均を集計するので、ブログでは%やポイントを使って説明できます。
・ なぜ2グループを選んだのか
今回のコードでは、k=2〜6を、乱数seed 42・7・21で評価します。seedは初期配置に使う乱数の設定です。同じデータでも初期配置で結果が変わり得るため、複数の設定を比べます。
候補の採用条件は、 すべてのseedで、各グループに2市区町村以上が属すること です。その候補の中から平均silhouetteが最大のkを選び、最終結果にはseed 42のモデルを使います。seed間のARIも記録しますが、ARIの最大値でkを選ぶ実装ではありません。
| 評価表の項目 | 意味 | 今回のk=2の結果の読み方 |
|---|---|---|
eligible |
このコードの候補条件を満たすか |
trueなら最小人数の条件を満たす候補 |
mean_silhouette |
グループ内のまとまりと、他グループからの分かれ方を表す値のseed平均。値は−1〜1で、大きいほど分離が明確な傾向 | 約0.4009。正解率40.09%という意味ではありません |
min_cluster_size_all_seeds |
全seedを通して最も小さいグループの人数 |
17。1市区町村だけのグループはありません |
min_pairwise_ari |
seed同士の分類の一致度を比べた最小値。グループ番号の付け替えは同じ分類として扱います |
1.0。試したseed間で同じグループ分けになりました |
この選び方でk=2となりました。 2つが唯一の正解だと証明したわけではありません。 今回の3指標・標準化・候補範囲・選択ルールで得られた探索結果として読みます。ARIが1.0でも、データや指標を変えた場合まで同じ分類になることを保証するものではありません。
1) 学習して評価表を確認する
[Run all] をクリックします。3指標を標準化し、K-meansのグループ数2〜6と複数の乱数seedを比較します。
確認用 Notebookの nb00_check_and_captureから、保存された評価表と分類結果を読みます。確認のためだけにnb03の全処理を再実行する必要はありません。
import json
from pathlib import Path
ml = Path("/Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/ml")
meta = json.loads((ml / "validation.json").read_text(encoding="utf-8"))
print(json.dumps(meta, ensure_ascii=False, indent=2))
assert meta["status"] == "PASS" and meta["rows"] == 61
spark.read.parquet(str(ml / "k_summary")).orderBy("k").show(truncate=False)
assignments = spark.read.parquet(str(ml / "assignments"))
assignments.groupBy("cluster_id").count().orderBy("cluster_id").show()
assignments.select("area_code", "area_name", "cluster_id").orderBy("area_code").show(15, truncate=False)
この確認コードでは、機械学習の検査JSON、k別の評価表、各クラスタの件数、市区町村別の分類が表示されます。以下のRun all実行結果と照合します。
確認すること :入力の件数が61、分類件数の合計も61、評価値や各グループの件数が保存されていること。今回のAIDP実行では selected_k=2 となり、クラスタ0が17件、クラスタ1が44件でした。クラスタIDの番号自体に大小や順位の意味はありません。
・ nb03_cluster_changes Run all実行結果

{
"comparison_run_id": "compare61_20261020_01",
"ai_run_id": "change_ml_20261020_01",
"input": "/Volumes/blog_catalog/opendata/census_work/comparisons/compare61_20261020_01/curated/tokyo_changes",
"output": "/Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/ml"
}
{
"municipality_count": 61,
"coverage": {
"input_municipality_count": 62,
"excluded_area_codes": [
"13209"
],
"excluded_reasons": [
{
"area_code": "13209",
"area_name": "町田市",
"reason": "2020-12-01境界変更。人口だけを組み替えても世帯・年齢の比較条件は揃わないため除外。",
"effective_date": "2020-12-01",
"source_url": "https://www.city.sagamihara.kanagawa.jp/shisei/1026766/seido/1026777/1004437.html",
"supporting_url": "https://www.gikai-machida.jp/voices/GikaiDoc/attach/Gk/Gk5001_64_somu_gyosei_kyokaihenko.pdf"
}
]
},
"k_summary": [
{
"k": 2,
"eligible": true,
"reason": "eligible",
"mean_silhouette": 0.40090720404207025,
"min_cluster_size_all_seeds": 17,
"min_pairwise_ari": 1.0,
"mean_pairwise_ari": 1.0,
"pairwise_ari": [
{
"seed_a": 42,
"seed_b": 7,
"ari": 1.0
},
{
"seed_a": 42,
"seed_b": 21,
"ari": 1.0
},
{
"seed_a": 7,
"seed_b": 21,
"ari": 1.0
}
]
},
{
"k": 3,
"eligible": true,
"reason": "eligible",
"mean_silhouette": 0.2562270525965309,
"min_cluster_size_all_seeds": 15,
"min_pairwise_ari": 0.8489030431705591,
"mean_pairwise_ari": 0.8992686954470394,
"pairwise_ari": [
{
"seed_a": 42,
"seed_b": 7,
"ari": 0.8489030431705591
},
{
"seed_a": 42,
"seed_b": 21,
"ari": 0.8489030431705591
},
{
"seed_a": 7,
"seed_b": 21,
"ari": 1.0
}
]
},
{
"k": 4,
"eligible": true,
"reason": "eligible",
"mean_silhouette": 0.2769845083257529,
"min_cluster_size_all_seeds": 2,
"min_pairwise_ari": 0.31281868848571087,
"mean_pairwise_ari": 0.5356789908131964,
"pairwise_ari": [
{
"seed_a": 42,
"seed_b": 7,
"ari": 0.31281868848571087
},
{
"seed_a": 42,
"seed_b": 21,
"ari": 0.333645443196005
},
{
"seed_a": 7,
"seed_b": 21,
"ari": 0.9605728407578734
}
]
},
{
"k": 5,
"eligible": true,
"reason": "eligible",
"mean_silhouette": 0.2909682061422665,
"min_cluster_size_all_seeds": 2,
"min_pairwise_ari": 0.5906001294695556,
"mean_pairwise_ari": 0.7270667529797038,
"pairwise_ari": [
{
"seed_a": 42,
"seed_b": 7,
"ari": 0.5906001294695556
},
{
"seed_a": 42,
"seed_b": 21,
"ari": 0.5906001294695556
},
{
"seed_a": 7,
"seed_b": 21,
"ari": 1.0
}
]
},
{
"k": 6,
"eligible": true,
"reason": "eligible",
"mean_silhouette": 0.3079910839400188,
"min_cluster_size_all_seeds": 2,
"min_pairwise_ari": 0.5714043678451123,
"mean_pairwise_ari": 0.6917131507602066,
"pairwise_ari": [
{
"seed_a": 42,
"seed_b": 7,
"ari": 0.5714043678451123
},
{
"seed_a": 42,
"seed_b": 21,
"ari": 0.73073346438619
},
{
"seed_a": 7,
"seed_b": 21,
"ari": 0.7730016200493174
}
]
}
],
"nearest_neighbors": [
{
"target_area_code": "13104",
"target_area_name": "新宿区",
"rank": 1,
"area_code": "13105",
"area_name": "文京区",
"cluster_id": 1,
"standardized_distance": 0.3898271740542,
"population_growth_pct": 4.053417975665319,
"single_share_change_pp": 2.011245298472211,
"elderly_share_change_pp": -0.4489022171056618
},
{
"target_area_code": "13104",
"target_area_name": "新宿区",
"rank": 2,
"area_code": "13114",
"area_name": "中野区",
"cluster_id": 1,
"standardized_distance": 0.42273964626287003,
"population_growth_pct": 2.8917304569705315,
"single_share_change_pp": 3.3576175032636755,
"elderly_share_change_pp": -0.7415101304927525
},
{
"target_area_code": "13104",
"target_area_name": "新宿区",
"rank": 3,
"area_code": "13119",
"area_name": "板橋区",
"cluster_id": 1,
"standardized_distance": 0.46901068828594433,
"population_growth_pct": 1.9656688047385407,
"single_share_change_pp": 3.0885352582055816,
"elderly_share_change_pp": -0.4550166388891803
},
{
"target_area_code": "13104",
"target_area_name": "新宿区",
"rank": 4,
"area_code": "13111",
"area_name": "大田区",
"cluster_id": 1,
"standardized_distance": 0.5140382586605361,
"population_growth_pct": 1.3501211767174892,
"single_share_change_pp": 2.1070633943778674,
"elderly_share_change_pp": -0.5734185192912236
},
{
"target_area_code": "13104",
"target_area_name": "新宿区",
"rank": 5,
"area_code": "13109",
"area_name": "品川区",
"cluster_id": 1,
"standardized_distance": 0.5677399236090989,
"population_growth_pct": 0.8324496790441449,
"single_share_change_pp": 2.467816188834142,
"elderly_share_change_pp": -0.5565802929935835
}
]
}
{
"status": "PASS",
"ai_run_id": "change_ml_20261020_01",
"comparison_run_id": "compare61_20261020_01",
"rows": 61,
"municipality_count": 61,
"selected_k": 2,
"source_sha256": "dc788e9b128990694483bb0aa6f40164be31a6390934bea14ad958372bcf345d",
"profile_payload_sha256": "0ed073c56797d1b021dbeb7b23dd419047d44d7f4e4b96ca9cf5cd8edb85914f",
"comparison_sha256": "004bd87cd433d05e54a4955144e81a8ae04926f51f071379baa0d86df8dfff40",
"output_root": "/Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/ml",
"coverage": {
"input_municipality_count": 62,
"excluded_area_codes": [
"13209"
],
"excluded_reasons": [
{
"area_code": "13209",
"area_name": "町田市",
"reason": "2020-12-01境界変更。人口だけを組み替えても世帯・年齢の比較条件は揃わないため除外。",
"effective_date": "2020-12-01",
"source_url": "https://www.city.sagamihara.kanagawa.jp/shisei/1026766/seido/1026777/1004437.html",
"supporting_url": "https://www.gikai-machida.jp/voices/GikaiDoc/attach/Gk/Gk5001_64_somu_gyosei_kyokaihenko.pdf"
}
]
},
"python_version": "3.11.13",
"versions": {
"numpy": "1.26.4",
"scikit_learn": "1.5.2",
"scipy": "1.13.1",
"spark": "3.5.0-dataflow-3-5-2-12-df.1.0.103"
},
"checks": {
"input": "PASS",
"parquet_read_back": "PASS",
"json_read_back": "PASS"
},
"interpretation_review": "REQUIRED"
}
● 各クラスタに属する市区町村を確認する
nb03が保存した assignments(地域コードとクラスタ番号の対応表) を読み、所属を確認しました。参照した学習結果は change_ml_20261020_01 で、クラスタ0が17件、クラスタ1が44件でした。
・ 確認できたクラスタ0の17市区町村
| 地域コード | 市区町村 | クラスタ |
|---|---|---|
| 13113 | 渋谷区 | 0 |
| 13201 | 八王子市 | 0 |
| 13205 | 青梅市 | 0 |
| 13224 | 多摩市 | 0 |
| 13227 | 羽村市 | 0 |
| 13305 | 日の出町 | 0 |
| 13307 | 檜原村 | 0 |
| 13308 | 奥多摩町 | 0 |
| 13361 | 大島町 | 0 |
| 13362 | 利島村 | 0 |
| 13363 | 新島村 | 0 |
| 13364 | 神津島村 | 0 |
| 13381 | 三宅村 | 0 |
| 13382 | 御蔵島村 | 0 |
| 13401 | 八丈町 | 0 |
| 13402 | 青ヶ島村 | 0 |
| 13421 | 小笠原村 | 0 |
渋谷区・八王子市は、どちらもクラスタ0でした。これはクラスタ平均からの推定ではなく、保存済みの所属表で確認した結果です。
都心の区、多摩地域の市町村、島しょ部の町村が同じグループに含まれています。今回の分類は、人口増減率・単独世帯割合の差・高齢者割合の差という 3指標の変化を標準化して比べた結果 です。同じクラスタに属することは、街の規模や環境、変化の原因まで同じであることを意味しません。
・ 所属と年齢指標を保存データから確かめる
以下の確認セルは、クラスタ0の全件と、渋谷区・八王子市の所属・年齢指標をMarkdown形式で表示します。2地域の人数・割合を並べた解釈は、後述の「実例:高齢者数が減っても、高齢者割合が上がる」で説明します。
1) 保存済みの所属と、2地域の数値を表示する
nb00_check_and_capture のPythonセルで実行します。最初の4つの設定値を自分の保存済みrun IDに合わせます。取込み・再学習・生成AIへの送信・ファイルへの書込みは行いません。
import json
from pathlib import Path
WORK_ROOT = Path("/Volumes/blog_catalog/opendata/census_work")
AI_RUN_ID = "change_ml_20261020_01"
SOURCE_2020 = "census2020_20261020_01"
SOURCE_2025 = "census2025_20261020_01"
ml_root = WORK_ROOT / "ai_runs" / AI_RUN_ID / "ml"
meta = json.loads((ml_root / "validation.json").read_text(encoding="utf-8"))
assert meta["status"] == "PASS", "学習結果がPASSではありません"
assert meta["ai_run_id"] == AI_RUN_ID
members = (spark.read.parquet(str(ml_root / "assignments"))
.select("area_code", "area_name", "cluster_id")
.orderBy("area_code").collect())
assert len(members) == meta["rows"], "保存された所属の件数が検査記録と異なります"
by_code = {str(r["area_code"]).zfill(5): r.asDict() for r in members}
assert len(by_code) == len(members), "地域コードが重複しています"
def print_table(headers, rows):
def safe(value):
return str(value).replace("|", "/").replace("\n", " ")
print("| " + " | ".join(headers) + " |")
print("| " + " | ".join(["---"] * len(headers)) + " |")
for row in rows:
print("| " + " | ".join(safe(v) for v in row) + " |")
counts = {}
for row in members:
cluster_id = int(row["cluster_id"])
counts[cluster_id] = counts.get(cluster_id, 0) + 1
print("ai_run_id:", AI_RUN_ID)
print("クラスタ別件数:", counts)
cluster0 = [r for r in members if int(r["cluster_id"]) == 0]
print("\nクラスタ0の市区町村一覧")
print_table(["地域コード", "市区町村", "クラスタ"],
[[r["area_code"], r["area_name"], r["cluster_id"]] for r in cluster0])
# 年別の保存データから、人数・分母・割合を読み解く。
targets = ["13113", "13201"]
years = {}
for year, source_id in [(2020, SOURCE_2020), (2025, SOURCE_2025)]:
root = WORK_ROOT / "runs" / source_id
check = json.loads((root / "validation.json").read_text(encoding="utf-8"))
assert check["status"] == "PASS"
assert check["census_year"] == year
data = (spark.read.parquet(str(root / "curated" / "tokyo_year"))
.filter("area_code IN ('13113', '13201')")
.select("area_code", "area_name", "population", "age_under15",
"age_15_64", "age_65plus", "age_unknown").collect())
assert len(data) == 2, "2地域の年別データを取得できませんでした"
years[year] = {str(r["area_code"]).zfill(5): r.asDict() for r in data}
assert set(years[year]) == set(targets)
detail = []
for code in targets:
assert code in by_code, f"{code}が分類結果にありません"
a, b = years[2020][code], years[2025][code]
known_a = a["age_under15"] + a["age_15_64"] + a["age_65plus"]
known_b = b["age_under15"] + b["age_15_64"] + b["age_65plus"]
assert known_a > 0 and known_b > 0
assert a["age_unknown"] is not None and b["age_unknown"] is not None
elderly_a, elderly_b = 100*a["age_65plus"]/known_a, 100*b["age_65plus"]/known_b
unknown_a = 100*a["age_unknown"]/a["population"]
unknown_b = 100*b["age_unknown"]/b["population"]
detail.append([
a["area_name"], by_code[code]["cluster_id"],
f'{a["age_65plus"]:,} → {b["age_65plus"]:,}',
f'{known_a:,} → {known_b:,}',
f'{elderly_a:.2f}% → {elderly_b:.2f}%',
f'{elderly_b-elderly_a:+.2f}pp',
f'{unknown_a:.2f}% → {unknown_b:.2f}%',
f'{unknown_b-unknown_a:+.2f}pp',
])
print("\n渋谷区・八王子市の所属と年齢指標(2020年 → 2025年)")
print_table(["市区町村", "クラスタ", "65歳以上人口(人)", "年齢既知人口(人)",
"高齢者割合", "割合の差", "年齢不詳率", "不詳率の差"], detail)
確認セルの出力結果(所属一覧・2地域の年齢指標)
ai_run_id: change_ml_20261020_01
クラスタ別件数: {1: 44, 0: 17}
クラスタ0の市区町村一覧
| 地域コード | 市区町村 | クラスタ |
| --- | --- | --- |
| 13113 | 渋谷区 | 0 |
| 13201 | 八王子市 | 0 |
| 13205 | 青梅市 | 0 |
| 13224 | 多摩市 | 0 |
| 13227 | 羽村市 | 0 |
| 13305 | 日の出町 | 0 |
| 13307 | 檜原村 | 0 |
| 13308 | 奥多摩町 | 0 |
| 13361 | 大島町 | 0 |
| 13362 | 利島村 | 0 |
| 13363 | 新島村 | 0 |
| 13364 | 神津島村 | 0 |
| 13381 | 三宅村 | 0 |
| 13382 | 御蔵島村 | 0 |
| 13401 | 八丈町 | 0 |
| 13402 | 青ヶ島村 | 0 |
| 13421 | 小笠原村 | 0 |
渋谷区・八王子市の所属と年齢指標(2020年 → 2025年)
| 市区町村 | クラスタ | 65歳以上人口(人) | 年齢既知人口(人) | 高齢者割合 | 割合の差 | 年齢不詳率 | 不詳率の差 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 渋谷区 | 0 | 41,261 → 37,664 | 218,503 → 182,437 | 18.88% → 20.64% | +1.76pp | 10.41% → 23.61% | +13.20pp |
| 八王子市 | 0 | 153,504 → 148,747 | 561,264 → 491,040 | 27.35% → 30.29% | +2.94pp | 3.12% → 14.52% | +11.39pp |
筆者の実行に対応する件数はクラスタ0が17件、クラスタ1が44件です。異なる件数が表示された場合は、参照している ai_run_id と学習条件を確認します。別条件で再学習した場合は、クラスタ番号自体も変わる可能性があります。
● グループごとの変化をグラフで見る
1) 保存されたグラフをNotebookに表示する
図を保存しているだけでセルに表示されない場合は、確認用Notebookで次を実行します。
from pathlib import Path
from IPython.display import Image, display
charts = Path("/Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/ml/charts")
for name in ["k_selection.png", "feature_changes.png", "share_movements.png"]:
path = charts / name
if not path.is_file():
raise FileNotFoundError(str(path))
print(name)
display(Image(filename=str(path)))
| 図 | 確認する内容 |
|---|---|
k_selection.png |
グループ数ごとの評価 |
feature_changes.png |
3指標の変化とクラスタの分布 |
share_movements.png |
2020年の割合から2025年への移動 |
凡例や軸の pp はpercentage points、つまり割合の差です。日本語フォントに依存しないよう、図の軸は英語表記になっています。
feature_changes.pngの点は1つの市区町村を表し、色は所属クラスタです。3指標を2列ずつ組み合わせて表示しています。横軸・縦軸とも、正は増加、負は減少です。
share_movements.pngは変化量ではなく、各年の割合そのものを示します。白抜きの点が2020年、塗りつぶした点が2025年で、矢印が5年間の移動です。右へ動けば年齢既知人口中の高齢者割合が上昇し、上へ動けば単独世帯割合が上昇しています。この図の色にも3指標で学習した分類結果を使います。
グラフでは、軸の指標と単位、凡例のクラスタ番号を確認します。細かな文字は保存されたPNGを開くと読みやすくなります。
2) Object Storageに保存されたグラフを取得する
NotebookのVolumeパスと、Object Storageのオブジェクト名は次のように対応します。バケットのObjects一覧では、右側の接頭辞を開いて3つのPNGをダウンロードします。
| アクセス元 | グラフの保存先 |
|---|---|
| Notebook | /Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/ml/charts/ |
| Object Storage | tokyo-census/work/ai_runs/change_ml_20261020_01/ml/charts/ |
ファイル名は k_selection.png、feature_changes.png、share_movements.png です。

● 新宿区に似た変化の街を探す
ここでは、読者がイメージしやすい街を1つ決めて類似検索の使い方を示すため、新宿区を基準にします。東京都の代表と統計的に判定したわけではありません。新宿区を選ぶのは学習後の検索対象の指定であり、新宿区を中心にK-meansを学習する処理ではありません。
1) 新宿区と変化が近い市区町村を表示する
path = "/Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/ml/nearest_neighbors"
spark.read.parquet(path).orderBy("rank").show(truncate=False)
表示される距離は、標準化した3指標の変化量の距離です。地理的な距離ではありません。入力条件や学習に使った指標を変えると、近い候補も変わります。
掲載したnb03の nearest_neighbors の結果を表にすると、次の順です。距離は小さいほど今回の3指標の変化が近いことを示します。
| 順位 | 地域コード | 市区町村 | 標準化後の距離 |
|---|---|---|---|
| 1 | 13105 | 文京区 | 0.3898 |
| 2 | 13114 | 中野区 | 0.4227 |
| 3 | 13119 | 板橋区 | 0.4690 |
| 4 | 13111 | 大田区 | 0.5140 |
| 5 | 13109 | 品川区 | 0.5677 |
この距離は確率や類似度の百分率ではありません。地理的な近さ、住みやすさ、街の魅力を評価した順位でもありません。
■ nb04で生成AIの説明を作る
ここでは、検査済みのクラスタ別指標を生成AIへ渡し、グループ名と説明文を作ります。分類そのものはnb03のK-meansが担当し、生成AIは所属を変更しません。
今回成功した構成は、東京のAIDP内に作成した AIDP_OpenData_Workspace から、API署名鍵を使って大阪のOCI Generative AIを呼び出す形です。元のnb04の送信処理をCell Cで差し替え、既存の送信記録・JSON検査・根拠数値の照合を使います。
・ nb04の各Cellは何をしているのか
nb04は、nb03が保存したグループ別の集計値と検査結果を読みます。LLM(文章を生成する大規模言語モデル)に指示文とデータを送り、返された説明を検査します。 ここでは新しいモデルを学習せず、既存モデルへ推論を依頼します。
以下は、先頭Widget用CellとCell A・Bを追加し、元の最後の実行CellをCell Cへ差し替えた後の、掲載画面の番号です。
| 画面のCell | 種類・役割 | 何を処理するか | この時点で生成AIに送信するか |
|---|---|---|---|
| Cell 1 | Widgetの作成 | 学習済み結果のID、説明作成のID、モデル名を指定します | しません |
| Cell 2 | 説明文 | 生成AIによる説明と検査の範囲を説明します | しません |
| Cell 3 | 入力検査の関数定義 | JSONの読書き、必須項目、学習結果・集計値・単位・対象範囲の検査方法を準備します | しません |
| Cell 4 | 説明文 | 生成AIへの指示と出力検査を説明します | しません |
| Cell 5 | 指示・出力検査の関数定義 |
make_prompt、parse_response、validate_answer、make_reportを定義します |
しません |
| Cell 6 | 説明文 | 送信記録と再実行時の扱いを説明します | しません |
| Cell 7 | 一連の実行処理の定義 |
collect_once_with_journalとrun_notebookを定義。入力検査→指示作成→送信→応答検査→説明案保存の流れを準備します |
定義だけなので、まだ送信しません |
| Cell 8 | 説明文 | 実行前の確認事項を説明します | しません |
| Cell 9=Cell A | API署名鍵の作成 | 秘密鍵・公開鍵のペアを作り、公開鍵とFingerprintを表示します | しません。この後、OCIコンソールへ公開鍵を登録します |
| Cell 10=Cell B | 接続設定 | OCIDと署名鍵を使い、大阪向けのSDKクライアントを用意します | しません。「設定完了」は接続成功の証明ではありません |
| Cell 11=Cell C | 実行 | API呼出しを大阪向けに差し替え、run_notebook(...)を実行。入力検査後に生成AIを呼び、応答を保存・検査します |
必要な場合にここで送信。同一条件の保存応答があれば再利用します |
・ 生成AIの説明を、どう検査するのか
Cell 5のmake_promptは、生成AIへの「依頼文」を作ります。指標・単位・期間・対象範囲を一緒に渡し、決まったJSON形式で回答するよう指示します。生成AIに自由な文章だけを返してもらうと、どの数値を根拠にしたかを機械的に照合しにくいためです。
| 出力の項目 | 生成AIに求める内容 | 検査・確認の方法 |
|---|---|---|
cluster_id / member_count
|
nb03で決まったグループ番号と市区町村数 | 入力と一致するかコードで照合 |
label / summary
|
グループ名と、データから言える特徴 | 長さなどの形式を検査し、意味は人が確認 |
evidence |
根拠となる3指標の値・単位・期間 | nb03の生成AI用集計値とコードで照合 |
caution |
年齢不詳率など、解釈上の注意 | 形式を検査し、注意の内容が妥当か人が確認 |
hypotheses |
要確認の仮説と、その検証に必要な資料 | 事実と分けた形式か検査し、根拠の有無は人が確認 |
parse_responseは回答をJSONとして読み、validate_answerは項目の過不足や根拠値の一致などを検査します。通過した回答からmake_reportが表と文章を組み立て、report_draft.mdを保存します。数値を本文へ自由に書かせず、evidenceに集めて照合する設計です。
構造と数値のPASSだけでは、文章の意味まで正しいとは限りません。 例えば「割合が上がった」を「世帯数が増えた」と説明しても、引用した数値自体は一致する場合があります。そのため、出力にhuman_review=REQUIREDを残して人が確認します。
● 接続時につまずいた箇所と今回の構成
| 段階 | 観測した結果 | 今回の対応 |
|---|---|---|
| 元のカタログ経由の呼出し | 東京側への呼出しで404 | OCI Python SDKで大阪の推論エンドポイントを明示 |
| Resource Principalでの認証 | Notebookで必要なトークン・秘密鍵の環境変数が未設定 | この検証ではIAMユーザーのAPI署名鍵を使用 |
| 元のPrivate Workspaceから大阪への接続 | DNSは解決するがTCP 443がタイムアウト。同じサブネットのComputeでは接続成功 | 同じAIDP内に別Workspaceを作成して再確認 |
| 新しいWorkspaceから大阪への接続 | TCP 443が成功 | このWorkspaceでnb04を実行 |
explain_20261020_02 の再利用 |
送信記録が残り、再送防止で停止 | 記録を残し、未使用の explain_20261020_03 で実行 |
TCPの結果だけでは、元Workspaceの内部経路やタイムアウトの根本原因までは断定できません。以下は今回接続できた構成で再現する手順です。Private networkへの接続が必要な環境では、その要件を維持して接続経路を調査します。
・ 東京で404になった理由をどう考えたか
記録から確認できるのは、東京向けの呼出しで404となり、大阪の推論エンドポイントを明示したSDK呼出しでは成功したことです。まず疑うのは 呼出先リージョンとモデルの提供条件の不一致 です。ただし、SDKや認証方式も変更しているため、今回のログだけで404の原因を1つに断定はしません。
Oracleのモデル別・リージョン別の提供表では、Gemini 2.5 Proの大阪でのオンデマンド利用が案内されています。一方、現在のローカルでモデル処理を行わないリージョンの説明では、東京からの利用はルーティングプロファイルを通す形で案内されています。本記事で成功したのは大阪のエンドポイントへモデルIDを指定する方式で、ルーティングプロファイル方式は検証していません。
したがって「東京では生成AIを使えない」と一般化せず、モデルID・呼出先リージョン・利用方式・権限をセットで確認します。なお、ここでいう「大阪」はOCI APIの接続先です。Geminiの実際の処理場所は、同じ提供表のExternal Callsの条件も確認します。
● Workspace・Notebook・Computeを準備する
1) 生成AI用のWorkspace作成
AIDPのホームで [Workspaces] → [Create Workspace] をクリックし、次を設定します。設定後、 [Create] をクリックします。
| 項目 | 設定値 |
|---|---|
| Workspace name | AIDP_OpenData_Workspace |
| Default catalog | blog_catalog |
| Enable private network | 選択しない |
AIDP本体は東京のままです。同名Workspaceを作成済みの場合は、それを使用します。
2) nb04をアップロードする
作成した AIDP_OpenData_Workspace に OpenData_AI_2020_2025 フォルダを作成し、nb04_explain_changes.ipynb を選択して [Upload] をクリックします。既にアップロード済みの場合は、そのNotebookを開きます。
確認用に nb00_check_and_capture も作成します。元のnb01〜nb03の成果物はVolumeに残っているため、読めれば再取込み・再学習は不要です。
3) 既存のComputeを接続する
Notebookで [Actions] → [Attach an existing cluster] をクリックし、接続可能な既存の Census_PoC_Cluster を選択します。今回使用したのは、前から利用しているComputeです。この手順のために別のComputeを新規作成する必要はありません。
画面上部に Cluster: Census_PoC_Cluster (Active) と表示されることを確認します。確認用Notebookの nb00_check_and_capture にもComputeを接続します。
前述のライブラリを確認し、必要なインストールと再起動はAPI署名鍵を作る前に済ませます。既存Computeで必要なライブラリと接続を確認できていれば、そのまま次の手順へ進めます。
nb04では追加で oci と cryptography を使います。確認用 Notebookで次のセルが動くことを確認します。今回のOCI SDKは 2.185.2 です。oci が見つからない場合はComputeのLibraryから oci==2.185.2 を追加し、インストール完了後に再起動します。cryptography はOCI SDKの依存ライブラリとして導入されます。
import oci
import cryptography
print("OCI SDK:", oci.__version__)
print("cryptography:", cryptography.__version__)
OCI SDK: 2.185.2
cryptography: 50.0.2
● 通信と入力データを確認する
1) 大阪への接続と保存済みの機械学習結果を確認する
新Workspaceの確認用Notebook nb00_check_and_capture で次を実行します。
import socket
for region in ("ap-tokyo-1", "ap-osaka-1"):
host = f"inference.generativeai.{region}.oci.oraclecloud.com"
print(f"--- {region} ---")
try:
print("DNS:", socket.gethostbyname_ex(host)[2])
with socket.create_connection((host, 443), timeout=10):
print("TCP 443: OK")
except OSError as error:
print("接続確認:", type(error).__name__, str(error))
--- ap-tokyo-1 ---
DNS: ["192.29.41.223"]
TCP 443: OK
--- ap-osaka-1 ---
DNS: ["192.29.255.15"]
TCP 443: OK
今回、新WorkspaceのNotebookでは東京・大阪の両方が TCP 443: OK になりました。表示されたIPアドレスは実行時のDNS応答で、固定の設定値ではありません。この確認はTCP接続までで、認証やモデル利用権限は後のAPI実行で確認します。
続けて同じNotebookで、nb03の保存済み検査結果を読みます。
import json
from pathlib import Path
ml_root = Path("/Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/ml")
ml_check = json.loads((ml_root / "validation.json").read_text(encoding="utf-8"))
print({key: ml_check.get(key) for key in ("status", "ai_run_id", "rows", "selected_k")})
assert ml_check["status"] == "PASS"
assert ml_check["rows"] == 61
{'status': 'PASS', 'ai_run_id': 'change_ml_20261020_01', 'rows': 61, 'selected_k': 2}
読込みに失敗する場合は、新WorkspaceからのCatalog・Schema・Volumeの参照権限を確認します。
● モデルの利用権限とNotebookを設定する
1) 利用するモデルとIAM権限を確認する
Notebookのカタログ表示で [default] → [oci_ai_models] を開き、モデルの完全名を確認します。今回のWidgetとSDKの指定は次のとおりです。
| 設定する場所 | 値 |
|---|---|
nb04のWidget model_name
|
default.oci_ai_models.google.gemini-2.5-pro |
Cell CのSDK model_id
|
google.gemini-2.5-pro |
| SDKの接続先リージョン | ap-osaka-1 |
このCell CはGemini 2.5 Pro用です。Widgetだけを別モデルへ変えると停止します。別モデルを使う場合は、提供リージョン・API形式・応答形式も含めてコードを見直します。
API署名鍵を登録するIAMユーザーに、対象コンパートメントでチャットAPIを呼ぶ権限があることを確認します。必要な場合に管理者が設定するポリシーの例は次のとおりです。グループ・コンパートメントは実際の名前に置き換えます。
Allow group <domain-name>/<group-name> to use generative-ai-chat in compartment <compartment-name>
既に同等以上の権限があれば追加は不要です。API署名鍵の登録だけでは、生成AIの利用権限は付与されません。カタログのUSE権限と、SDKで呼ぶIAMユーザーの権限も区別します。
参考:API-Level Permissions for Chat
2) Widgetを設定し、最後の実行セルを差し替える
新Workspaceの nb04_explain_changes に、次のWidget専用セルを先頭へ追加します。まずこのセルだけを実行します。
defaults = {
"work_root": "/Volumes/blog_catalog/opendata/census_work",
"ai_run_id": "change_ml_20261020_01",
"report_run_id": "explain_20261020_03",
"model_name": "default.oci_ai_models.google.gemini-2.5-pro",
}
for name, value in defaults.items():
aidputils.widgets.text(name=name, defaultValue=value, label=name)
for name in defaults:
print(name, "=", aidputils.widgets.get(name))
| Widget | 今回成功した実行の値 |
|---|---|
ai_run_id |
change_ml_20261020_01 |
report_run_id |
explain_20261020_03 |
model_name |
default.oci_ai_models.google.gemini-2.5-pro |
work_root |
/Volumes/blog_catalog/opendata/census_work |
defaultValue を変えても、既存Widgetの現在値が変わらない場合があります。画面の入力欄も修正し、get() の出力を確認します。_03 も使用済みなら、再生成には未使用のIDを選びます。保存結果を見るだけなら後述の確認セルを使います。
次の画像は筆者の初回設定時の記録です。読者は画像の番号に合わせる必要はなく、自分の出力先で未使用のIDを設定します。
元のnb04で、最後に run_notebook(spark, aidputils) を呼んでいる実行セルを探します。その 直前にCell AとCell Bを追加 し、 最後の実行セルの内容をCell Cへ置き換えます 。元の実行セルを残して末尾へCell Cを追加すると、元の呼出しも動いてしまいます。
元のインポート・関数定義セルを先頭から実行し、元の最後の実行セルは実行しません。初回は鍵の登録を挟むため、ここからは 各セルの [Run] をクリックし、セル単位で 進めます。Cell A〜Cはすべて同じnb04・同じPythonセッションで実行します。
● API署名鍵を作成・登録する
1) Cell A:API署名鍵のペアを作る
この手順の「API Key」は、OCIの API署名鍵 です。Cell AはRSA鍵ペアをメモリ内で作り、登録に使う公開鍵とフィンガープリントだけを表示します。
秘密鍵はNotebook側でAPIリクエストに署名するために使い、対になる公開鍵をOCI側へ登録します。これは「どのIAMユーザーからの呼出しか」を確かめる仕組みです。モデルを利用できるかは、そのユーザーに適用されるIAM権限でも確認されます。鍵の登録とモデルの利用権限は別の準備です。
import hashlib
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# 再実行しても、このPythonセッション内の鍵を再利用する。
if "_aidp_genai_signing_material" not in globals():
_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
_public = _key.public_key()
_der = _public.public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
_hex = hashlib.md5(_der, usedforsecurity=False).hexdigest()
_aidp_genai_signing_material = {
"private_pem": _key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
).decode("ascii"),
"public_pem": _public.public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
).decode("ascii"),
"fingerprint": ":".join(_hex[i:i+2] for i in range(0, 32, 2)),
}
del _key, _public, _der, _hex
# 出力するのは公開鍵と、その照合用フィンガープリントだけ。
print("Fingerprint:", _aidp_genai_signing_material["fingerprint"])
print(_aidp_genai_signing_material["public_pem"])
2) OCIコンソールへ公開鍵を登録する
OCIコンソール右上のプロフィールメニューから [My Profile] を開き、対象ユーザーの [API keys] 画面で [Add API key] をクリックします。
追加画面で [Paste a public key] を選択し、Cell Aでコピーした公開鍵を先頭・末尾の行ごと貼り付けて [Add] をクリックします。Cell Aの秘密鍵と対になる公開鍵を使うため、この画面で別の鍵ペアは生成しません。

登録後の Configuration file preview で次を確認します。
| 表示される項目 | 確認・転記先 |
|---|---|
user |
Cell Bの USER_OCID
|
tenancy |
Cell Bの TENANCY_OCID
|
fingerprint |
Cell AのFingerprintと一致すること |
region(表示される場合) |
本手順のSDK接続先はCell Bで ap-osaka-1 を明示 |
key_file |
本手順はメモリ内の秘密鍵を使うため、ファイルパスの設定は不要 |
GenAIを利用するコンパートメントの詳細画面も開き、OCIDをコピーしてCell Bの GENAI_COMPARTMENT_OCID に使います。AIDPインスタンスやWorkspaceのIDは入れません。
Cell AのFingerprintと登録後のFingerprintを照合し、同じ公開鍵が登録されたことを確認します。読者の環境で生成される値は、掲載画像とは異なります。
● 大阪リージョンへ接続して生成AIを実行する
1) Cell B:大阪向けSDKクライアントを作る
次の3つの REPLACE_WITH_... を自分のOCIDへ置き換え、Cell Bを実行します。
import oci
# OCIの登録画面に表示される値に置き換える。
USER_OCID = "REPLACE_WITH_USER_OCID"
TENANCY_OCID = "REPLACE_WITH_TENANCY_OCID"
GENAI_COMPARTMENT_OCID = "REPLACE_WITH_COMPARTMENT_OCID"
for _name, _value, _prefix in (
("USER_OCID", USER_OCID, "ocid1.user."),
("TENANCY_OCID", TENANCY_OCID, "ocid1.tenancy."),
("GENAI_COMPARTMENT_OCID", GENAI_COMPARTMENT_OCID, "ocid1."),
):
if not _value.startswith(_prefix):
raise ValueError(f"{_name} を実際のOCIDに置き換えてください。")
if not GENAI_COMPARTMENT_OCID.startswith(("ocid1.compartment.", "ocid1.tenancy.")):
raise ValueError("GenAIを利用するコンパートメントのOCIDを指定してください。")
if "_aidp_genai_signing_material" not in globals():
raise RuntimeError("同じnb04で、公開鍵を生成するセルから実行してください。")
_osaka_signer = oci.signer.Signer(
tenancy=TENANCY_OCID,
user=USER_OCID,
fingerprint=_aidp_genai_signing_material["fingerprint"],
private_key_file_location=None,
private_key_content=_aidp_genai_signing_material["private_pem"],
)
_osaka_genai_client = oci.generative_ai_inference.GenerativeAiInferenceClient(
config={
"region": "ap-osaka-1",
"user": USER_OCID,
"tenancy": TENANCY_OCID,
"fingerprint": _aidp_genai_signing_material["fingerprint"],
"key_content": _aidp_genai_signing_material["private_pem"],
},
signer=_osaka_signer,
service_endpoint="https://inference.generativeai.ap-osaka-1.oci.oraclecloud.com",
retry_strategy=oci.retry.NoneRetryStrategy(),
timeout=(10, 240),
)
print("OCI SDK:", oci.__version__)
print("設定完了: API署名鍵 / ap-osaka-1(まだAPI送信していません)")
OCI SDK: 2.185.2
設定完了: API署名鍵 / ap-osaka-1(まだAPI送信していません)
今回のOCI Python SDKは 2.185.2 でした。「設定完了」が出た時点ではAPIへ送信していません。登録済み公開鍵と手元の秘密鍵が対になっているか、IAM権限があるかはCell Cの呼出しで確認します。
ComputeやPythonセッションを再起動すると、メモリ内の秘密鍵が失われます。その場合はCell Aで新しい鍵を作り、対応する公開鍵を登録してからCell Bへ進みます。同じセッション内でCell Aを再実行した場合は、既に作った鍵を再利用します。
2) Cell C:送信・保存・検査を実行する
・ 長いCell Cを、4つの役割に分けて読む
| コード内の名前・箇所 | 役割 |
|---|---|
_write_osaka_json |
送受信の記録やエラーをJSONファイルへ保存します。既存ファイルは上書きしません |
_osaka_answer_text |
SDK応答から生成された文章を取り出します。本文が空、応答が途中で打ち切られたなどの状態なら停止します |
_make_osaka_collector |
大阪のエンドポイントとモデルを使う呼出しを用意します。応答原文を保存してから、既存のJSON検査へ渡します |
末尾のtry〜finally
|
送信部分を一時的に差し替えてrun_notebook(...)を実行し、終わったら元の定義へ戻します |
このCellが長いのは、APIを呼ぶだけでなく、 何を送ったか・何が返ったかを残し、途中で失敗した場合にも確認できるようにしているため です。前の関数定義CellとCell Bを実行していない場合は、必要な関数やクライアントが見つからず停止します。
元の最後の実行セルを、次の内容へ置き換えて実行します。このセルで生成AIへの送信処理へ進みます。同一条件の応答が保存済みなら既存処理で再利用し、送信記録だけが残る場合は自動再送せず停止します。筆者固有の _01 を禁止するチェックは設けず、保存記録と設定の照合で判断します。
import json
from datetime import datetime, timezone
from pathlib import Path
import uuid
import oci
def _write_osaka_json(path, value):
with Path(path).open("x", encoding="utf-8") as stream:
json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False)
def _osaka_answer_text(data):
chat = data.get("chat_response") or {}
choices = chat.get("choices") or []
if len(choices) != 1:
raise ValueError("生成AIの応答候補が1件ではありません。保存した応答を確認してください。")
choice = choices[0]
reason = str(choice.get("finish_reason") or "").upper()
if reason in {"LENGTH", "MAX_TOKENS", "CONTENT_FILTER", "SAFETY", "RECITATION"}:
raise ValueError(f"生成AIが正常完了していません: finish_reason={reason}")
content = (choice.get("message") or {}).get("content") or []
text = "".join(
item["text"] for item in content
if str(item.get("type", "")).upper() == "TEXT"
and isinstance(item.get("text"), str)
).strip()
if not text:
raise ValueError("生成AIの本文が空です。保存した応答を確認してください。")
return text
def _make_osaka_collector(base_collector, client, compartment_id):
transport = {
"method": "OCI_PYTHON_SDK_API_SIGNING_KEY",
"region": "ap-osaka-1",
"model_id": "google.gemini-2.5-pro",
"compartment_id": compartment_id,
"max_tokens": 16384,
"temperature": 0,
"num_generations": 1,
}
def collect_osaka(report_root, request, unused_original_invoke):
root = Path(report_root)
if request.get("model_name") != "default.oci_ai_models.google.gemini-2.5-pro":
raise ValueError("この修正セルはGemini 2.5 Pro専用です。model_nameを確認してください。")
if not isinstance(request.get("prompt"), str) or not request["prompt"].strip():
raise ValueError("元のnb04からpromptを受け取れませんでした。送信を中止します。")
request_with_transport = dict(request, inference_transport=transport)
# 前回の東京向けキャッシュや、別設定の応答を再利用させない。
old_request_path = root / "request.json"
if old_request_path.exists():
old_request = json.loads(old_request_path.read_text(encoding="utf-8"))
if old_request.get("inference_transport") != transport:
raise ValueError("同じreport_run_idに別の接続設定があります。新しいIDを使ってください。")
def invoke_osaka():
m = oci.generative_ai_inference.models
details = m.ChatDetails(
compartment_id=compartment_id,
serving_mode=m.OnDemandServingMode(model_id=transport["model_id"]),
chat_request=m.GenericChatRequest(
api_format="GENERIC",
messages=[m.UserMessage(content=[m.TextContent(text=request["prompt"])])],
is_stream=False,
max_tokens=transport["max_tokens"],
temperature=transport["temperature"],
num_generations=transport["num_generations"],
),
)
root.mkdir(parents=True, exist_ok=True)
attempt_id = uuid.uuid4().hex[:12]
try:
response = client.chat(details, retry_strategy=oci.retry.NoneRetryStrategy())
except oci.exceptions.ServiceError as error:
_write_osaka_json(root / f"sdk_service_error_{attempt_id}.json", {
"region": transport["region"],
"status": error.status,
"code": error.code,
"message": error.message,
"opc_request_id": error.headers.get("opc-request-id") if error.headers else None,
})
raise
# 本文の抽出や既存のJSON検査に失敗しても、応答原文は残す。
data = oci.util.to_dict(response.data)
_write_osaka_json(root / f"sdk_response_{attempt_id}.json", {
"received_at_utc": datetime.now(timezone.utc).isoformat(),
"transport": transport,
"opc_request_id": response.headers.get("opc-request-id"),
"data": data,
})
return _osaka_answer_text(data)
return base_collector(root, request_with_transport, invoke_osaka)
return collect_osaka
# 元のnb04の定義セルが実行済みか、送信前に確認する。
for _required in ("run_notebook", "collect_once_with_journal", "_osaka_genai_client"):
if _required not in globals():
raise RuntimeError(f"{_required} が未定義です。nb04の前のセルを実行してください。")
# 特定のrun IDは拒否しない。
# IDの形式検査、設定の照合、送信記録による再送防止は既存処理に任せる。
# 元の送信記録・再送防止・JSON/数値検査を使い、API呼出部分だけ差し替える。
_base_collect = collect_once_with_journal
try:
collect_once_with_journal = _make_osaka_collector(
_base_collect, _osaka_genai_client, GENAI_COMPARTMENT_OCID
)
run_notebook(spark, aidputils)
finally:
collect_once_with_journal = _base_collect
以下は実行結果の抜粋です。記事では fingerprint と profile_payload_sha256 の値を省略しています。照合に使う完全な値は、保存された validation.json を参照します。ここでの fingerprint は実行記録の値であり、API署名鍵のFingerprintとは別です。
Prompt characters: 7181; logical requests: at most 1; report: /Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/genai/explain_20261020_03
{
"status": "PASS",
"validation_scope": "schema_and_evidence_only",
"human_review": "REQUIRED",
"cluster_count": 2,
"municipality_count": 61,
"coverage": {
"excluded_area_codes": [
"13209"
],
"excluded_reasons": [
{
"area_code": "13209",
"area_name": "町田市",
"effective_date": "2020-12-01",
"reason": "2020-12-01境界変更。人口だけを組み替えても世帯・年齢の比較条件は揃わないため除外。",
"source_url": "https://www.city.sagamihara.kanagawa.jp/shisei/1026766/seido/1026777/1004437.html",
"supporting_url": "https://www.gikai-machida.jp/voices/GikaiDoc/attach/Gk/Gk5001_64_somu_gyosei_kyokaihenko.pdf"
}
],
"input_municipality_count": 62
},
"fingerprint": "(省略)",
"profile_payload_sha256": "(省略)",
"ai_run_id": "change_ml_20261020_01",
"report_run_id": "explain_20261020_03"
}
Response: generated. 文章確認前の原稿: /Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/genai/explain_20261020_03/report_draft.md
元のNotebookの検査で、機械学習のPASSとプロファイルのハッシュを確認し、指標・単位・期間・除外理由とともに生成AIへ渡します。送信記録と応答原文を保存し、出力JSONの構造と根拠数値を照合します。
今回はNotebookエディタから対話実行しました。2026年10月21日時点のKnown Issuesには、スケジュールジョブからのGenAIアクセスに制限が記載されています。この手順でWorkflow実行まで確認したわけではありません。
以下は実行時に記録した画面です。再現時は、上のCell A→公開鍵登録→Cell B→Cell Cの順で進めます。
今回のCell Cは status=PASS、cluster_count=2、municipality_count=61 となり、explain_20261020_03 のフォルダに説明案が保存されました。検査範囲は schema_and_evidence_only、文章の確認は human_review=REQUIRED です。
● 生成結果を確認し、記事用の文章に整える
・ どの保存ファイルを見ればよいか
| ファイル | 入っているもの | 確認する目的 |
|---|---|---|
request.json |
送信する指示文、モデル、入力を識別する情報など | どのデータと条件で依頼したか確認 |
sdk_response_*.json |
SDKで受け取った応答原文と接続情報など | JSONの解析前の応答を確認 |
raw_response.json |
後続の検査へ渡す応答と照合情報 | 保存応答の再利用・検査の確認 |
validated_answer.json |
構造・根拠値の検査を通過した回答 | グループ名、要約、根拠値を確認 |
validation.json |
最終的な検査結果と検査範囲 | PASS、対象件数、human_reviewを確認 |
report_draft.md |
読みやすい文章と表に整形した説明案 | 人が内容を確認し、記事用の文章へ整える |
request.jsonがあるだけでは、応答の取得や検査まで成功したとは言えません。以下の確認では、最後の検査結果と説明案まで確認します。
・ 保存済みファイルの確認結果
次の画面とログは、保存済みのnb04の検査結果を読み、主要なファイルの存在を確認した記録です。生成AIへの再送は行っていません。後の確認コードでは、ファイル一覧に加えて説明案の本文も表示します。
{
"status": "PASS",
"validation_scope": "schema_and_evidence_only",
"human_review": "REQUIRED",
"cluster_count": 2,
"municipality_count": 61,
"ai_run_id": "change_ml_20261020_01",
"report_run_id": "explain_20261020_03"
}
request.json exists= True bytes= 10925
validation.json exists= True bytes= 890
report_draft.md exists= True bytes= 4231
その他の保存ファイル: raw_response.json, report_draft.md, request.json, sdk_response_28c568dcaf57.json, validated_answer.json, validation.json
1) 数値の検査結果と説明案を読む
新Workspaceの確認用Notebookで次を実行します。保存済みファイルの表示だけなので、APIは再送しません。自分の実行IDが異なる場合はパスを合わせます。
import json
from pathlib import Path
report_dir = Path("/Volumes/blog_catalog/opendata/census_work/ai_runs/change_ml_20261020_01/genai/explain_20261020_03")
print("保存ファイル:")
for path in sorted(report_dir.iterdir()):
if path.is_file():
print(path.name, path.stat().st_size, "bytes")
validation_path = report_dir / "validation.json"
if validation_path.is_file():
print(json.dumps(json.loads(validation_path.read_text(encoding="utf-8")), ensure_ascii=False, indent=2))
else:
print("検査結果は未作成です。nb04の実行結果と送信・応答の保存状態を確認してください。")
draft_path = report_dir / "report_draft.md"
if draft_path.is_file():
print(draft_path.read_text(encoding="utf-8"))
else:
print("説明案は未作成です。JSONや根拠値の照合で停止していないか確認してください。")
数値照合に通った場合も、文章確認の human_review は REQUIRED です。次を原文・グループの指標と見比べます。
- 増加と減少を取り違えていないか。
- グループの平均を、所属する全市区町村の共通事実として書いていないか。
- 年齢不詳の影響を無視していないか。
- 再開発や転入など、入力にない原因を断定していないか。
- 町田市を除く61市区町村の分析を、62市区町村全体の結果として説明していないか。
AIの文章を編集して公開する場合は、保存された応答原文を残し、記事へ採用した説明を区別します。
以下は生成AIが作った説明案の原文です。表現の修正点は、この出力の後に記載します。
# 変化パターンの説明案(人による確認前)
構造・根拠数値の検査: PASS / 文章の意味と因果関係の確認: REQUIRED
対象: 2020 年 → 2025 年。分類は KMeans、以下の名称と文章は生成 AI の提案です。
集計は市区町村を同じ重みで平均。人口加重平均ではありません。
分析対象: 61 市区町村。入力元: 62 市区町村。
除外地域コード: 13209
対象範囲と除外理由(上流で確定したメタデータ):
```json
{
"excluded_area_codes": [
"13209"
],
"excluded_reasons": [
{
"area_code": "13209",
"area_name": "町田市",
"effective_date": "2020-12-01",
"reason": "2020-12-01境界変更。人口だけを組み替えても世帯・年齢の比較条件は揃わないため除外。",
"source_url": "https://www.city.sagamihara.kanagawa.jp/shisei/1026766/seido/1026777/1004437.html",
"supporting_url": "https://www.gikai-machida.jp/voices/GikaiDoc/attach/Gk/Gk5001_64_somu_gyosei_kyokaihenko.pdf"
}
],
"input_municipality_count": 62
}
```
モデル: `default.oci_ai_models.google.gemini-2.5-pro`
## クラスタ 0: 人口減少・単身世帯と高齢者割合が増加
所属市区町村数: 17
このクラスタの市区町村は、平均して人口が減少する一方、一般世帯に占める単独世帯の割合と、年齢判明人口に占める高齢者の割合はともに増加する傾向が観測されました。
| 指標 | 平均 | 単位 | 期間 |
| --- | ---: | --- | --- |
| population_growth_pct | -6.93 | percent | 2020-2025 |
| single_share_change_pp | 3.29 | percentage_points | 2020-2025 |
| elderly_share_change_pp | 2.10 | percentage_points | 2020-2025 |
留意点案: 年齢不詳率が平均で四パーセンテージポイント以上増加しており、高齢者割合の変化に影響を与えた可能性があります。また、一部の市区町村で年齢データが欠損しています。分析対象外の地域があり、結果は東京都全体を示すものではありません。
年齢不詳率の確認用集計(上流の検査済み数値):
```json
{
"change": {
"available_count": 11,
"max_abs_pp": 13.2,
"mean_pp": 4.34,
"null_count": 6
},
"year_2020": {
"available_count": 11,
"max_pct": 10.41,
"mean_pct": 1.46,
"null_count": 6
},
"year_2025": {
"available_count": 13,
"max_pct": 23.61,
"mean_pct": 5.1,
"null_count": 4
}
}
```
## クラスタ 1: 人口増加と単身世帯増、高齢者割合は横ばい
所属市区町村数: 44
このクラスタの市区町村は、平均して人口が増加し、一般世帯に占める単独世帯の割合も増加する傾向が観測されました。一方、年齢判明人口に占める高齢者の割合の変化は、平均するとほぼ見られませんでした。
| 指標 | 平均 | 単位 | 期間 |
| --- | ---: | --- | --- |
| population_growth_pct | 1.38 | percent | 2020-2025 |
| single_share_change_pp | 2.45 | percentage_points | 2020-2025 |
| elderly_share_change_pp | 0.00 | percentage_points | 2020-2025 |
留意点案: 高齢者割合の変化は平均するとほぼありませんが、個々の市区町村では増減の方向が異なる可能性があります。年齢不詳率の変化は比較的小さいものの、その影響は皆無ではありません。分析対象外の地域があり、結果は東京都全体を示すものではありません。
年齢不詳率の確認用集計(上流の検査済み数値):
```json
{
"change": {
"available_count": 44,
"max_abs_pp": 4.08,
"mean_pp": 0.22,
"null_count": 0
},
"year_2020": {
"available_count": 44,
"max_pct": 9.56,
"mean_pct": 2.7,
"null_count": 0
},
"year_2025": {
"available_count": 44,
"max_pct": 9.63,
"mean_pct": 2.91,
"null_count": 0
}
}
```
## 公開前の確認
- 増加・減少やクラスタ間比較の表現が数値と一致するか。
- 年齢不詳率・分母の違いを踏まえた文章か。
- 観測値だけでは分からない原因や将来予測を断定していないか。
2) 生成AIの文章を確認し、記事用の説明に整える
原文は保存したまま、記事で使う表現を次のように整理しました。
| 原文で確認した表現 | 記事での表現・理由 |
|---|---|
| 「単身世帯増」 | 「単独世帯割合の上昇」。ここで比較しているのは世帯数そのものではなく、一般世帯数に占める割合 |
| 「高齢者割合は横ばい」 | 「クラスタ平均ではおおむね横ばい」。全市区町村で変化がないという意味ではなく、0.00ppは表示桁数で丸めた値 |
| 「一部の市区町村で年齢データが欠損」 | 「年齢不詳率を算出できない市区町村がある」。年齢別人口全体が欠けているという意味に広げない |
記事用に整理した結果は次のとおりです。市区町村を同じ重みで平均した値であり、東京都全体の増減率や人口加重平均ではありません。
| グループ | 市区町村数 | 人口増減率の平均 | 単独世帯割合の変化の平均 | 高齢者割合の変化の平均 |
|---|---|---|---|---|
| クラスタ0:人口減少・単独世帯割合と高齢者割合が上昇する傾向 | 17 | −6.93% | +3.29pp | +2.10pp |
| クラスタ1:人口増加・単独世帯割合が上昇し、高齢者割合は平均でおおむね横ばい | 44 | +1.38% | +2.45pp | 0.00pp |
単独世帯割合の分母は一般世帯数、高齢者割合の分母は年齢既知人口です。クラスタ0の年齢不詳率の変化は、両年の値がある11市区町村で平均+4.34pp、算出できない市区町村が6件でした。この変化が高齢者割合へ影響した可能性があるため、実際の高齢化だけで説明することはできません。
年齢不詳率の年別平均は、2020年が11件、2025年が13件と対象が異なります。年別平均の差を、そのまま同じ市区町村の変化の平均と読み替えません。クラスタ1は44件とも変化を算出でき、平均+0.22ppでした。
今回のAI処理は、K-meansによる変化パターンの分類と、生成AIによる名称・説明文の提案です。PASS は構造と根拠数値の照合結果を示し、再開発・転入などの原因や将来予測の正しさを保証するものではありません。
・ 実例:高齢者数が減っても、高齢者割合が上がる
前述のとおり、渋谷区・八王子市は ともにクラスタ0に属します。 nb01で保存した年別データから分子・分母・年齢不詳率も取得し、クラスタ平均だけでは見えない個別地域の変化を読み解きます。
| 指標(2020年 → 2025年) | 渋谷区 | 八王子市 |
|---|---|---|
| クラスタ(assignmentsで確認) | 0 | 0 |
| 総人口 | 243,883人 → 238,826人 | 579,355人 → 574,428人 |
| 65歳以上人口 | 41,261人 → 37,664人 | 153,504人 → 148,747人 |
| 年齢既知人口(割合の分母) | 218,503人 → 182,437人 | 561,264人 → 491,040人 |
| 高齢者割合 | 18.88% → 20.64% | 27.35% → 30.29% |
| 高齢者割合の差 | +1.76pp | +2.94pp |
| 年齢不詳率 | 10.41% → 23.61% | 3.12% → 14.52% |
| 年齢不詳率の差 | +13.20pp | +11.39pp |
人数・割合・差は、年別の保存データを読み出した確認セルの結果に合わせています。割合は小数点以下2桁で表示し、差は丸める前の値から計算しています。
八王子市の年齢不詳率は、表示された値だけを引くと 14.52 − 3.12 = 11.40 ですが、元の人数から計算した差は約11.39409ppです。最後に小数点以下2桁へ丸めるため、実行結果の +11.39pp を採用します。
高齢者割合は 「65歳以上人口 ÷ 年齢既知人口 × 100」 で計算しています。
渋谷区では65歳以上人口が3,597人減少しました。分子の減少率は約8.72%ですが、分母の年齢既知人口は36,066人、約16.51%減っています。分母の方が大きな率で縮小しているため、37,664 ÷ 182,437 × 100で求める2025年の高齢者割合は20.64%となり、2020年の18.88%より上がります。
八王子市でも65歳以上人口は4,757人、約3.10%減る一方、年齢既知人口は70,224人、約12.51%減っています。その結果、高齢者割合は27.35%から30.29%へ上がりました。どちらも 分子の人数が減っていても、分母がより大きな率で減れば割合は上がる 例です。
「高齢者割合が上がった」と「高齢者数が増えた」は別の事実です。 今回の2地域では、観測された65歳以上人口は減少しています。年齢不詳率も大きく上昇しており、この割合だけから「高齢者が増えた」「高齢化が進んだ」と断定できません。年齢不詳の人がどの年齢層に属するかも、原数値だけでは分かりません。
今回の分類に使ったのは高齢者「数」の増減ではなく、高齢者「割合」の差です。割合の計算や引用値の照合がPASSでも、それを「高齢者が増えた」と言い換えると説明はずれます。人による確認では、割合に加えて分子の人数・分母の人数・年齢不詳率を並べて読む必要があります。年齢不詳率の上昇が確認できても、観測された変化のすべてをその影響だけで説明できるわけではありません。
・ なぜ渋谷区・八王子市が島しょ・山間部と同じグループに入ったのか
クラスタ0の17市区町村には、渋谷区・八王子市のほか、青梅市・多摩市・羽村市、西多摩・島しょの町村が含まれています。街の様子が違う地域が同じグループに入った点は、今回の結果を読み解く手掛かりになります。
人口増減率と高齢者割合の差を、クラスタ平均と並べてみます。人口増減率は上表の総人口から計算し、小数点以下2桁に丸めています。
| 指標 | 渋谷区(クラスタ0) | 八王子市(クラスタ0) | クラスタ0の平均 | クラスタ1の平均 |
|---|---|---|---|---|
| 人口増減率 | −2.07% | −0.85% | −6.93% | +1.38% |
| 高齢者割合の差 | +1.76pp | +2.94pp | +2.10pp | 0.00pp |
人口増減率という1つの指標だけで見ると、両市区の値はクラスタ0よりクラスタ1の平均に近い値です。一方、高齢者割合の差はクラスタ0の平均に近くなっています。 人口の増減だけでは、今回の所属を説明しきれません。
K-meansが比べるのは、人口増減率・単独世帯割合の差・高齢者割合の差を標準化した、3指標を合わせた距離です。上の2指標の平均との比較だけでは、どの指標が所属をどの程度左右したかは確定できません。
ここからは、次の検証につなげる仮説です。 両市区では、年齢不詳率の上昇とともに年齢既知人口が大きく減っています。この分母の変化に伴う高齢者割合の上昇が、入力特徴量を通じてクラスタ所属にも影響した可能性があります。ただし、「年齢不詳が増えたからクラスタ0になった」とまでは確認できていません。単独世帯割合の差を含む3指標と標準化の影響を踏まえ、不詳補完値を使った再分析で確かめる必要があります。
つまり、人による確認は生成AIの文章だけでなく、 分類に渡した指標が何を表しているか にも必要でした。この点を、所感で挙げる感度分析の問いにつなげます。
● 検証後のAPI署名鍵を片付ける
1) 検証に使ったAPI署名鍵を片付ける
追加の推論が不要になったら、OCIコンソールのユーザー詳細 → [API keys] で、今回のフィンガープリントに対応する鍵を確認して削除します。他の用途で使っている鍵は削除しません。鍵を削除しても、保存済みのJSON・説明案・グラフは引き続き確認できます。
■ CSVやPNGをダウンロードして結果を確認する
● 保存された成果物をダウンロードする
1) 出力先を確認する
census_work がObject Storageの tokyo-census/work/ に対応している場合、主な出力の場所は次のとおりです。
| 内容 | バケット内の接頭辞・オブジェクト名 |
|---|---|
| 2025年の検査JSON | tokyo-census/work/runs/census2025_20261020_01/validation.json |
| 境域差 | tokyo-census/work/comparisons/compare_strict_20261020_01/boundary_comparison.json |
| 比較表のCSV | tokyo-census/work/comparisons/compare61_20261020_01/csv/tokyo_changes/ |
| 機械学習のPNG | tokyo-census/work/ai_runs/change_ml_20261020_01/ml/charts/ |
| 生成AIの説明案 | tokyo-census/work/ai_runs/change_ml_20261020_01/genai/explain_20261020_03/report_draft.md |
バケットのObjects一覧で該当接頭辞を開き、対象ファイルのダウンロード操作からPCへ保存します。実際のVolume参照先が異なる場合は、その参照先の接頭辞に読み替えます。
Sparkが保存したCSVは、tokyo_changes.csv という単一ファイル名ではなく、ディレクトリ内の part-...csv です。.csv のデータファイルを取得します。_SUCCESS はデータ本文ではありません。
グラフは feature_changes.png、share_movements.png、k_selection.png を取得します。これらのグラフは、2020年・2025年の国勢調査の原数値から作成したものです。対象は町田市を除く61市区町村で、出典と対象範囲は末尾の「データの出典」に記載しています。
■ 途中で止まったときの確認順序
● 症状別の確認ポイント
| 症状 | 最初に確認すること | 対応 |
|---|---|---|
| 原本が見つからない | Object Storageの接頭辞、Volumeの参照先、年フォルダ、拡張子 | 存在確認の6行を見直す |
| Permission denied | 原本の読込み権限、出力先の書込み権限、選んだWorkspace | 第1回の権限・Volume設定と照合する |
| ModuleNotFoundError | ComputeのLibrary、インストール状態、Restart、接続したクラスタ | Library追加後に再起動し、バージョン確認セルを実行する |
| 年を変えたのに古いデータが出る | Widgetの実値、途中セルだけを実行していないか | 年とrun IDを確認して処理の先頭から実行する |
| FileExistsError/出力先が既に存在する | 同じrun IDを再利用していないか | 処理をやり直す場合は新しいIDを使い、後段の参照IDも合わせる |
| 表頭の不一致 | 調査年、原数値か参考表か、表番号、Excelの編集有無 | 選んだ原本を確認する。列番号を推測で直さない |
| 原本SHA-256の不一致 | 再保存したファイル、公式訂正、ダウンロードした表 | 公式原本・訂正情報を確認して読込定義を見直す |
| BOUNDARY_MISMATCH | 差の地域コードと数値 | 町田市4人差の事例か確認し、記録した分析対象へ切り替える |
| nb03が上流のPASS/ハッシュで停止 | 比較のrun ID、Parquetや検査JSONを変更していないか | 正しい実行を指定し、改変がある場合は前段から再作成する |
| モデルが見つからない/404 | 完全なモデル名、SDKのmodel_id、提供リージョン、利用権限 | Widget名とSDKの短いIDを区別し、大阪のエンドポイント設定を確認する |
| 大阪へのTCP接続がタイムアウト | 実際にNotebookを動かしているWorkspace、DNSとTCPの結果 | nb04の新Workspace側で確認。同じサブネットのComputeで成功してもAIDPでの到達性は別途確認する |
session_token was not provided |
Resource Principal用の実行環境が提供されているか | 本手順ではCell A〜CのAPI署名鍵方式を使う |
NotAuthenticated/401 |
登録した公開鍵のFingerprint、user・tenancy、セッション再起動の有無 | 現在のCell Aの鍵とOCI登録鍵が対になっているか確認する |
| 権限エラー | API署名鍵のIAMユーザーと対象コンパートメント | チャットAPIの利用権限とコンパートメントOCIDを確認する |
| 生成AIのJSONや数値が不一致 | 保存された応答原文、エラーメッセージ | 記事へ採用せず内容を確認。再生成するなら新しいreport_run_idを使う |
| 送信記録だけが残り応答がない | 通信・サービス側の結果、保存ファイル | 自動再送はしない。状況確認後、明示的に再送するなら新しいreport_run_idを使う |
● 結果の再表示と処理の再実行を区別する
結果を表示したいだけなら、nb00_check_and_capture で保存済みファイルを読みます。学習や推論の処理全体を再実行する必要はありません。
■ 実行結果と検証から分かったこと
● 各段階のPASSは何を意味するのか
| 段階 | PASSで確認したこと | 読者が続けて確認すること |
|---|---|---|
| nb01 | 設定した原本・表頭・地域・件数・合計などの検査と、保存後の一致 | 年度とrun IDが目的どおりか |
| nb02 | 確定した対象範囲での比較計算と、保存後の一致 | 今回の比較対象は町田市を除く61市区町村であること |
| nb03 | 入力、分類結果の保存、Parquet・JSONの読戻し | kの評価、グループ人数、分類の解釈。分類の「正解率」を示すPASSではありません |
| nb04 | JSONの構造と、引用した根拠数値・単位・期間などの一致 | 文章の増減方向、数と割合、平均と個別地域、事実と仮説 |
結果を見る際は、 グループ平均は市区町村を同じ重みで平均した値 であることにも注意します。人口の大きい区も小さい村も1市区町村として扱うため、東京都全体の人口増減率や割合とは異なります。また、平均が増加していても、所属する全市区町村で増加したとは限りません。
● 今回の実測値と検査結果
掲載した実行結果と、Cell Cの成功ログを整理します。run ID中の日付は識別子の一部で、実際の実行日とは別です。
| 項目 | 今回の実測値・記録 |
|---|---|
| AIDPでの実行日・リージョン | 主な処理:2026年10月20日〜21日。ライブラリと保存済み結果の追加確認:11月9日。AIDPは東京、生成AIの推論接続先は大阪 |
| Python / Spark / scikit-learn |
3.11.13 / 3.5.0-dataflow-3-5-2-12-df.1.0.103 / 1.5.2(nb03の検査JSON) |
| OCI SDK / cryptography |
2.185.2 / 50.0.2(11月9日の確認セル) |
| 2020年のrun ID・件数・PASS |
census2020_20261020_01 / 62件 / PASS |
| 2025年のrun ID・件数・PASS |
census2025_20261020_01 / 62件 / PASS |
| 境域差の地域・値・対応 | 町田市13209、2020年原本431,079人と組替431,083人の+4人差。理由を記録して分析対象から除外 |
| 機械学習のrun ID・対象件数 |
change_ml_20261020_01 / 61件 / PASS |
| 選択したk・各グループの件数 | k=2 / クラスタ0:17件、クラスタ1:44件 |
| 所属と年齢指標の追加確認 | assignmentsでクラスタ0の17件を確認。渋谷区・八王子市ともクラスタ0で、65歳以上人口は減少、高齢者割合は上昇 |
| 生成AIのモデル名・report run ID |
google.gemini-2.5-pro / explain_20261020_03
|
| 構造・数値の検査結果 | PASS / validation_scope=schema_and_evidence_only / human_review=REQUIRED
|
| 文章の確認結果・修正内容 | 単独世帯の「数」と「割合」、クラスタ平均、年齢不詳率の欠測範囲を修正。応答原文は保持 |
● やってみた所感
● やってみた所感
今回は、AIDPに2020年・2025年の国勢調査データを取り込み、比較条件を揃えた61市区町村について、K-meansによる分類から生成AIによる説明まで実行できました。第1回で取り込んだデータを、年度間の比較、機械学習、生成AIへとつなげるところまで進められました。
実際に試してみると、分析結果だけでなく、その結果をどう読めばよいかにも学びがありました。
例えば、渋谷区と八王子市は、島しょ・山間部の町村を含むグループに入りました。同じグループといっても、街の規模や環境が似ているわけではありません。今回選んだ3つの指標で「変化の仕方」を比べると、こうした組合せになるということです。普段の街のイメージと照らし合わせながら、分類結果を読み解く面白さがありました。
一方で、元の人数まで確認すると、両市区では高齢者割合が上がる一方、65歳以上人口は減っていました。割合の変化を説明するには、分子の人数だけでなく、分母や年齢不詳の状況まで見る必要があります。
数値が合っていても、その数値を説明する文章の意味まで合っているとは限らない。 今回、生成AIの説明を確認していて、最も学びがあったのはこの点です。「単独世帯割合が上がった」と「単独世帯数が増えた」では意味が違いますし、グループ平均の傾向が、所属するすべての街に当てはまるわけでもありません。
また、比較・学習結果をファイルとして保存しておいたことで、生成AIへの接続方法を変更した際も、取込みからやり直さずに続けられました。説明を読んで気になったところから、元のデータへ戻って確認できる。この往復ができることも、分析の流れを組み立てるうえで大切だと感じました。
データの比較条件を揃えること、分析に使う指標を決めること、生成された説明の意味を確かめること。AIDPで機械学習と生成AIをつないでみると、それぞれの段階で確認することが具体的に見えてきました。
次に確かめたいのは、年齢不詳の扱いを変えた場合にも、同じような分類になるのかです。不詳補完値を使うと渋谷区・八王子市の所属や、新宿区に変化が近い街の順位はどう変わるのか。今回の結果を残しながら、比較してみてみたいと思います。
■ データの出典と参照資料
出典:総務省統計局「令和2年国勢調査」「令和7年国勢調査」(e-Stat)の人口等基本集計・集計結果(原数値)を加工して作成。
本記事の比較表・グラフ・クラスタ分類は筆者による加工・分析結果です。入力は東京都62市区町村、比較・機械学習は境域変更の扱いを統一するため町田市を除く61市区町村を対象としています。クラスタ名と説明案には生成AIを利用し、本文の解釈は人による確認を加えています。公式統計の機関が行った分類や見解ではありません。
● 国勢調査と境域変更
● AIDPの操作と制限事項
- AIDP Workspaces
- AIDP Volumes
- AIDP Notebooks
- AIDP Manage Libraries
- AIDP OCI Generative AI
- AIDP Known Issues
● Object Storageと生成AIの認証
● 機械学習の用語と評価指標
以下は今回使用したscikit-learn 1.5系の公式資料です。














































































