海岸線データの取得

海岸線データの取得

import numpy as np
import geopandas as gpd
import rasterio
from rasterio import features
from shapely.geometry import Point
from skimage.graph import route_through_array  # 1対1のとき
import osmnx as ox
import networkx as nx
import folium

# --------------------
# 0) 入力
# --------------------
epicenter_wgs = (35.5, 142.0)  # 例: 緯度, 経度(仮)
gebco_tif = "data/gebco_bathymetry.tif"   # 海底地形(m, 海は正の深さ or 負の標高のことも)
dem_tif   = "data/dem_coast.tif"          # 陸域標高(m)
coastline = gpd.read_file("data/coastline.gpkg")  # 海岸線
shelters  = gpd.read_file("data/shelters.geojson")# 避難所点データ(WGS84想定)

# 便利関数:WGS84→ラスタ座標系へ
def to_crs_point(lat, lon, crs_epsg):
    g = gpd.GeoSeries([Point(lon, lat)], crs=4326).to_crs(crs_epsg)
    p = g.iloc[0]
    return p.y, p.x  # (Y, X) in projected CRS

# --------------------
# 1) 海上到達時間ラスタ(簡易)
# --------------------
with rasterio.open(gebco_tif) as src:
    bathy = src.read(1).astype("float32")
    prof  = src.profile
    # 海域の水深(m)に統一(負の標高なら反転)
    depth = np.where(bathy < 0, -bathy, bathy)  # 例:負値を深さに
    depth = np.where(depth<=0, np.nan, depth)

    g = 9.81
    v = np.sqrt(g * depth)  # m/s
    cost = 1.0 / v          # s/m(セル通過時間はこれ×セルサイズ)

    # 震源セル
    epic_y, epic_x = to_crs_point(epicenter_wgs[0], epicenter_wgs[1], src.crs.to_epsg())
    row, col = src.index(epic_x, epic_y)

    # coast マスク(海→陸境界の近傍)を後で使うためにベクタ→ラスタ化しておくのも可

# ここではデモとして「震源→任意の海岸点」1対1のrouteで travel time を出す例。
# 実際は multi-source 最短時間(fast marching/多点Dijkstra)が望ましい。
# skimage.graphの route_through_array はコスト配列にセルサイズを掛けて合計。
# セルサイズ(m):
pix_m = (prof["transform"][0])  # x解像度(m/px)。投影ラスタ前提
# 到達時間ラスタの厳密生成はここでは割愛(本格的には多源最短経路/コスト距離を実装)

# --------------------
# 2) 浸水域(閾値で簡易)
# --------------------
with rasterio.open(dem_tif) as demsrc:
    dem = demsrc.read(1).astype("float32")
    # 仮の沿岸波高 H(m)を設定(現実は断層モデルや公表想定を用いる)
    H0 = 1.0      # 沖合初期波高 1 m(例)
    h0 = 3000.0   # 沖合水深 3000 m(例)
    h_coast = 10.0# 沿岸水深(例)
    H_coast = H0*((h0/h_coast)**0.25)

    tide = 0.5    # 満潮加算(例)
    safety_margin = 0.5
    flood_threshold = H_coast + tide + safety_margin  # m

    # 海から連結する低地 = 浸水候補
    flood_mask = (dem <= flood_threshold).astype(np.uint8)

# flood_mask をポリゴン化(例)
shapes = features.shapes(flood_mask, transform=demsrc.transform)
flood_polys = [sh for sh, val in shapes if val == 1]
flood = gpd.GeoDataFrame(geometry=gpd.GeoSeries.from_iterable(flood_polys), crs=demsrc.crs)

# 海から連結していない内陸窪地は除外(簡易には海ポリゴンとの接触でフィルタ)
# coastlineを同一CRSへ
coastline_proj = coastline.to_crs(flood.crs)
flood = flood[flood.intersects(coastline_proj.unary_union)].buffer(0)

# --------------------
# 3) 安全エリア&避難所(浸水域の外)
# --------------------
shelters_proj = shelters.to_crs(flood.crs)
safe_shelters = shelters_proj[~shelters_proj.intersects(flood.unary_union)]

# --------------------
# 4) 避難ルート(浸水域の道路を除外)
# --------------------
place = "Sumida-ku, Tokyo, Japan"
G = ox.graph_from_place(place, network_type="walk")  # 徒歩網の方が避難向き

nodes, edges = ox.graph_to_gdfs(G)
edges_proj = edges.to_crs(flood.crs)

# 浸水と交差するエッジを除外
bad = edges_proj.intersects(flood.unary_union)
edges_keep = edges_proj.loc[~bad]

# サブグラフ再構築
G2 = ox.utils_graph.graph_from_gdfs(nodes.to_crs(edges_keep.crs), edges_keep)

# 出発地(例:現在地)と最寄り安全避難所
current_wgs = (35.71, 139.81)  # 例
cur_pt = gpd.GeoSeries([Point(current_wgs[1], current_wgs[0])], crs=4326).to_crs(edges_keep.crs).iloc[0]
dest_pt = safe_shelters.geometry.iloc[0] if len(safe_shelters) else shelters_proj.geometry.iloc[0]

orig = ox.distance.nearest_nodes(G2, X=cur_pt.x, Y=cur_pt.y)
dest = ox.distance.nearest_nodes(G2, X=dest_pt.x, Y=dest_pt.y)

route = nx.shortest_path(G2, orig, dest, weight="length")

# --------------------
# 5) Foliumで可視化
# --------------------
center = (current_wgs[0], current_wgs[1])
m = folium.Map(location=center, zoom_start=13)

# 浸水域
flood_wgs = flood.to_crs(4326)
folium.GeoJson(flood_wgs.__geo_interface__, name="Flood (simple)").add_to(m)

# ルート
route_latlon = [(G2.nodes[n]["y"], G2.nodes[n]["x"]) for n in route]
folium.PolyLine(route_latlon, color="red", weight=5, opacity=0.9, tooltip="Evacuation Route").add_to(m)

# 出発・目的地
folium.Marker(location=[cur_pt.y, cur_pt.x], popup="Start", icon=folium.Icon(color="green")).add_to(m)
folium.Marker(location=[dest_pt.to_crs(4326).y, dest_pt.to_crs(4326).x], popup="Shelter", icon=folium.Icon(color="blue")).add_to(m)

# 避難所レイヤ
folium.GeoJson(safe_shelters.to_crs(4326).__geo_interface__, name="Safe shelters",
               tooltip=folium.GeoJsonTooltip(fields=[:4])
               ).add_to(m)

folium.LayerControl().add_to(m)

# Jupyterなら m だけで表示
m  # もしくは m.save("evac.html")

を実行しても

DataSourceError: data/coastline.gpkg: No such file or directory 

となるため
データの取得先を調べる

data/coastline.gpkg は自分で用意する必要があります。用途(全球/日本のみ、精度、更新頻度)に応じて、下のどれかをダウンロード→GeoPackage に変換すれば OK です。
入手先(おすすめ順)
1. Natural Earth – Coastline(手軽・軽量)
1:10m/50m/110m の海岸線 Shapefile。全球で数MB、試作には十分です。Natural Earth Data+2Natural Earth Data+2
2. OSM Coastlines / Land polygons(高頻度更新・実用)
OpenStreetMap から抽出された最新の海岸線。WGS84 / Web Mercator の Shapefile がそのまま落とせます。OpenStreetMap Data+1
3. GSHHG(高解像度・全球)
解像度別(c/l/i/h/f)で海岸線を配布。Shapefile もあり。厳密な可視化に向く一方、古い部分もあるので用途に注意。ngdc.noaa.gov+2ngdc.noaa.gov+2
4. 国土数値情報「海岸線データ」(日本限定・行政用途向け属性つき)
都道府県ごとに海岸線(線)を提供。JPGIS/GML と Shapefile が選べます。nlftp.mlit.go.jp+2geospatial.jp+2

使うときの注意
* CRS を統一:あなたの dem_tif / gebco_tif / 道路網(OSMnx)と同じ座標系に coastline を揃えてください。
例:coastline = coastline.to_crs(flood.crs)(すでにコードでやっている通り)
* 粒度の選択:
* 試作:Natural Earth(軽い)
* 実運用・最新形状:OSM Coastlines
* 日本限定・行政属性重視:国土数値情報
* ライン or ポリゴン:浸水域との連結判定には、**海岸線(ライン)**だけでなく 陸/水ポリゴンがあると便利です(OSM の Land/Water polygons 参照)。

種類が多いためGPTで候補を割り出す

使用する用途のコンセプト また震源地から津波が到達するまでの時間と津波の高さを計算し GIS情報を使い避難するエリアを算出しルート案内できるようにしたい 家族の避難経路・地域防災マップをGraphDBで整理 避難所や重要連絡先も紐付け 使い方例: 「地震が来たら最寄りの避難所はどこ?」 「非常食の賞味期限が近いものは?」 など この用途で使うならどれを推奨する?

✅ あなたの用途の要件
* 精度:震源→沿岸までの到達時間・波高推定の入力に使うので、誤差が大きすぎると危険。
* 範囲:主に日本周辺で十分。全球は必須ではない。
* 更新頻度:将来的に OSM/防災機関の最新データと連携したい。
* GraphDB連携:避難所・道路・避難経路・浸水域をノード/リレーションで管理。
* 利用イメージ:
* 「地震が来たら最寄りの避難所はどこ?」 → 避難所ノード+道路グラフ
* 「非常食の賞味期限が近いものは?」 → 在庫管理ノード
* 「津波到達までに何分ある?」 → 震源ノード→海岸ノードのコスト距離

✅ データセットごとの特徴
1. 国土数値情報「海岸線データ」
* 日本限定。行政データとして信頼性が高い。
* GML/Shapefile 提供。
* 避難計画(住民向け防災マップ)に最も適する。
➡ 推奨(基準データにする)
2. OSM Coastlines / Land polygons
* 更新頻度が高い(週次〜月次)。
* 最新の人工改変(埋立地・護岸)も比較的反映。
* 避難経路や実際の生活圏との整合性が良い。
➡ 補助データ(現況把握用)
3. Natural Earth
* 全球対応、軽量。
* 精度は粗い(1:10mでも数百 m〜km の誤差)。
* 可視化や概念実証には便利。
➡ プロトタイプ用のみ
4. GSHHG
* 全球対応、高解像度あり。
* ファイルが大きく、取り回しに手間。
* 学術研究向き。
➡ 専門シミュレーション以外では過剰

✅ 推奨構成(防災マップ+GraphDB向け)
1. 海岸線:
* 基本 → 国土数値情報(海岸線)
* 最新の現況補正 → OSM Coastlines
2. 標高/地形:
* 陸域 DEM → 国土地理院 DEM / ALOS / SRTM
* 海域 DEM → GEBCO
3. 避難所・道路:
* 避難所 → 自治体オープンデータ(CSV/GeoJSON)
* 道路 → OSMnx で抽出
4. GraphDBに格納:
* ノード:震源・海岸・浸水域・避難所・家庭・在庫
* リレーション:到達時間・避難経路・所要時間・備蓄品管理

✅ まとめ
ご提示の用途(家族避難+地域防災+GraphDB管理)なら:
* **国土数値情報(海岸線)**をメイン
* OSM Coastlinesで現況更新
* DEM(陸域+海域)+道路+避難所を組み合わせ
* GraphDBでノード/リレーションを整理して「質問に答えられる防災知識ベース」を構築

OSMxを使い最短経路の算出

OSMxを使い最短経路の算出

東京スカイツリーから
江戸東京博物館への最短経路を算出する

まずはスカイツリーと江戸東京博物館の座標をセットする

skytree = (35.7100,139.8180)
museum = (35.6963,139.7967)

次に一番近い node つまり交差点を取得

始点はスカイツリーなので

start = ox.get_nearest_node(G,skytree)

としたがエラー

--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[15], line 1 ----> 1 start = ox.get_nearest_node(G,skytree) AttributeError: module 'osmnx' has no attribute 'get_nearest_node'

これもバージョンの違いが原因

OSMnx 2.x では廃止され、ox.distance サブモジュールに移動
引数は (X=経度, Y=緯度) の順番で渡す必要があります
普通の Point(lon, lat) と同じ並び

start = ox.distance.nearest_nodes(G, X=skytree[1], Y=skytree[0])

とすればOK

同様に終点も

end = ox.distance.nearest_nodes(G,museum[1],museum[0])

次に最短経路

shortest_path = ox.shortest_path(G,start,end)

Foliumで表示できないので調べた

plot_route_folium
https://github.com/gboeing/osmnx/issues/1252
によれば

release (currently 1.9.4), then run your code. The latest v1 release will remain compatible with the v1 API, but any deprecated functionality you're using will issue a FutureWarning with simple guidance to make it v2 compatible.
Yes, the folium module was deprecated with a warning for removal back in 2023, and finally removed in v2.0.0. The good news: interactive web mapping with folium remains easy to do with OSMnx in ~2 lines of code! See this example in the Examples Gallery for a demonstration.

となっていて
foliumモジュールは2023年に削除の警告とともに非推奨となり、最終的にv2.0.0で削除されました。

とのこと

https://github.com/gboeing/osmnx-examples/blob/main/notebooks/11-interactive-web-mapping.ipynb
のサンプルが参考になりそう

OSMnx 2.0.6 には Folium 専用関数は無い → 自分で PolyLine を追加すればOK

再度GPTでコード生成

route = nx.shortest_path(G, orig, dest, weight="length")

というように
weight=”length”)
があるので調べた

✅ weight の意味
networkx.shortest_path の weight 引数は、**「どのエッジ属性を最短経路計算に使うか」**を指定するものです。
OSMnx で作成した G(道路グラフ)には、各エッジ(道路の区間)に様々な属性があります:
* “length” : 区間の長さ(メートル単位) ← デフォルトでよく使う
* “travel_time” : 区間の推定走行時間(秒単位)※ ox.add_edge_speeds / ox.add_edge_travel_times を実行した場合のみ
* その他、highway(道路種別)や lanes(車線数)など

✅ 例
1. 最短距離ルート(距離を最小化)

route = nx.shortest_path(G, orig, dest, weight="length")

2. 最短時間ルート(所要時間を最小化)
まず速度と所要時間をエッジに付与してから:

import osmnx as ox

G = ox.add_edge_speeds(G)        # 各道路に推定速度 (km/h) を追加
G = ox.add_edge_travel_times(G)  # 各道路に travel_time (秒) を追加

route = nx.shortest_path(G, orig, dest, weight="travel_time")

✅ まとめ
* weight=”length” → 「最短距離ルート」
* weight=”travel_time” → 「最短時間ルート」
* 他のエッジ属性を指定すれば、それに基づいた経路探索ができる

シェープファイルへの書き出し

シェープファイルへの書き出し

今回は作成した world を書き出す

これは GeodataFlameになっていれば簡単にできる

保存するときにはto_file を使う

シェープファイルで保存するなら

world.to_file(driver='ESRI Shapefile',filename='test_shape_world.shp')

というように
driverへ ESRI Shapefileを指定すればいい

でも

/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 328239523 of field POP_EST of feature 4 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 270625568 of field POP_EST of feature 8 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 144373535 of field POP_EST of feature 18 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 127575529 of field POP_EST of feature 27 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 211049527 of field POP_EST of feature 29 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 200963599 of field POP_EST of feature 56 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 1366417754 of field POP_EST of feature 98 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 163046161 of field POP_EST of feature 99 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 216565318 of field POP_EST of feature 102 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 1397715000 of field POP_EST of feature 139 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 108116615 of field POP_EST of feature 147 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 126264931 of field POP_EST of feature 155 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 100388073 of field POP_EST of feature 163 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pyogrio/raw.py:723: RuntimeWarning: Value 112078730 of field POP_EST of feature 165 not successfully written. Possibly due to too larger number with respect to field width
  ogr_write(

となる

GPTによれば
併せて知っておくと良い Shapefile の制約
* フィールド名は最大 10 文字(長い列名は切られる)
* 文字列の最大長、数値の幅と小数点以下桁を持つ
* 64bit 整数を正式には持てない
* 複数のファイル(.shp/.shx/.dbf …)が必要

警告は Shapefile のフィールド幅不足。
ベストは GeoPackage/GeoJSON へ保存。
Shapefile が必須なら layer_options で幅指定を。

これは Shapefile(.shp)側の制約で数値フィールドの桁数が足りないために出る警告です。
Shapefile は属性を dBase(DBF) で持ちますが、各数値フィールドには「幅(桁数)と小数桁」を設定する必要があり、デフォルト推定だと POP_EST のような 10桁超え(例: 1,306,370,000) が収まりきらずに “not successfully written / too large for field width” になります。

Shapefile は古く、制限が多いです。GeoPackageやGeoJSONなら桁数制限に悩まされません。

このためGeoJSONでj保存する

world.to_file(driver='GeoJSON',filename='test_shape_world.geojson')

driverにGeoJSON ファイル拡張子を .geojsonにすればOK

GeoDataFlame の集計

GeoDataFlame の集計

これは dissolve を使う

これを使い地理データの集計を行う

# world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowers'))
# cities = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))
import geopandas as gpd
import requests, zipfile, io

# 世界地図(国境)
url_world = "https://naturalearth.s3.amazonaws.com/110m_cultural/ne_110m_admin_0_countries.zip"
r = requests.get(url_world)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall("naturalearth_data")
world = gpd.read_file("naturalearth_data/ne_110m_admin_0_countries.shp")

# 都市データ
url_cities = "https://naturalearth.s3.amazonaws.com/110m_cultural/ne_110m_populated_places.zip"
r = requests.get(url_cities)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall("naturalearth_data")
cities = gpd.read_file("naturalearth_data/ne_110m_populated_places.shp")

print(world.head())
print(cities.head())

で世界地理情報の取得

国名はADMINになっているので
日本の情報を取得するのなら

world[world['ADMIN']=='Japan']

とすれば

	featurecla	scalerank	LABELRANK	SOVEREIGNT	SOV_A3	ADM0_DIF	LEVEL	TYPE	TLC	ADMIN	...	FCLASS_TR	FCLASS_ID	FCLASS_PL	FCLASS_GR	FCLASS_IT	FCLASS_NL	FCLASS_SE	FCLASS_BD	FCLASS_UA	geometry
155	Admin-0 country	1	2	Japan	JPN	0	2	Sovereign country	1	Japan	...	None	None	None	None	None	None	None	None	None	MULTIPOLYGON (((141.8846 39.18086, 140.95949 3...
world[world['ADMIN']=='Japan'].plot()

で日本地図の表示

この世界データは国ごとの情報になっている

これを国ではなく大陸ごとに変えたい
この時に位置情報などを取り扱うには dissolve を使う

その前に人口が出てないので調べた

「人口情報が出てこない」のは、表示されていないだけの可能性が高い
Natural Earth の属性は列数が多く、print(df.head()) だと途中で省略される

# 国ごとの推定人口
print(world[['ADMIN','ISO_A3','POP_EST']].head())

# 都市の人口(大きい順に上位10件)
print(cities[['NAME','ADM0NAME','POP_MAX','POP_MIN']].sort_values('POP_MAX', ascending=False).head(10))

とすると

                         ADMIN ISO_A3      POP_EST
0                         Fiji    FJI     889953.0
1  United Republic of Tanzania    TZA   58005463.0
2               Western Sahara    ESH     603253.0
3                       Canada    CAN   37589262.0
4     United States of America    USA  328239523.0
             NAME                  ADM0NAME   POP_MAX   POP_MIN
233         Tokyo                     Japan  35676000   8336599
218      New York  United States of America  19040000   8008278
224   Mexico City                    Mexico  19028000  10811002
234        Mumbai                     India  18978000  12691836
239     São Paulo                    Brazil  18845000  10021295
232      Shanghai                     China  14987000  14608512
237       Kolkata                     India  14787000   4631392
171         Dhaka                Bangladesh  12797394   7000940
210  Buenos Aires                 Argentina  12795000  10929146
216   Los Angeles  United States of America  12500000   3694820

というように表示された

なので

japan = world[world['ADMIN'] == 'Japan']
print(japan[['ADMIN','ISO_A3','POP_EST']])

とすれば

     ADMIN ISO_A3      POP_EST
155  Japan    JPN  126264931.0

というように表示された

Natural Earth の Admin-0 Countries データには continent というカラムがあり

continent の意味
* 各国・地域が属している 大陸名 を示す属性です。
* 値の例:
* “Asia”(日本、中国、インドなど)
* “Europe”(フランス、ドイツなど)
* “Africa”
* “North America”
* “South America”
* “Oceania”
* “Seven seas (open ocean)”(公海などの海洋エリア)
* “Antarctica”

ということで大陸名を調べようと

japan = world[world['ADMIN'] == 'Japan']
print(japan[['ADMIN','continent']])

としたが

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[53], line 2
      1 japan = world[world['ADMIN'] == 'Japan']
----> 2 print(japan[['ADMIN','continent']])

File ~/.pyenv/versions/3.11.0/lib/python3.11/site-packages/geopandas/geodataframe.py:1896, in GeoDataFrame.__getitem__(self, key)
   1890 def __getitem__(self, key):
   1891     """
   1892     If the result is a column containing only 'geometry', return a
   1893     GeoSeries. If it's a DataFrame with any columns of GeometryDtype,
   1894     return a GeoDataFrame.
   1895     """
-> 1896     result = super().__getitem__(key)
   1897     # Custom logic to avoid waiting for pandas GH51895
   1898     # result is not geometry dtype for multi-indexes
   1899     if (
   1900         pd.api.types.is_scalar(key)
   1901         and key == ""
   (...)   1904         and not is_geometry_type(result)
   1905     ):

File ~/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pandas/core/frame.py:4108, in DataFrame.__getitem__(self, key)
   4106     if is_iterator(key):
   4107         key = list(key)
-> 4108     indexer = self.columns._get_indexer_strict(key, "columns")[1]
   4110 # take() does not accept boolean indexers
   4111 if getattr(indexer, "dtype", None) == bool:

File ~/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pandas/core/indexes/base.py:6200, in Index._get_indexer_strict(self, key, axis_name)
   6197 else:
   6198     keyarr, indexer, new_indexer = self._reindex_non_unique(keyarr)
-> 6200 self._raise_if_missing(keyarr, indexer, axis_name)
   6202 keyarr = self.take(indexer)
   6203 if isinstance(key, Index):
   6204     # GH 42790 - Preserve name from an Index

File ~/.pyenv/versions/3.11.0/lib/python3.11/site-packages/pandas/core/indexes/base.py:6252, in Index._raise_if_missing(self, key, indexer, axis_name)
   6249     raise KeyError(f"None of [{key}] are in the [{axis_name}]")
   6251 not_found = list(ensure_index(key)[missing_mask.nonzero()[0]].unique())
-> 6252 raise KeyError(f"{not_found} not in index")

KeyError: "['continent'] not in index"

となる
どうやら変更されている

print(world.columns.tolist())

の結果は

['featurecla', 'scalerank', 'LABELRANK', 'SOVEREIGNT', 'SOV_A3', 'ADM0_DIF', 'LEVEL', 'TYPE', 'TLC', 'ADMIN', 'ADM0_A3', 'GEOU_DIF', 'GEOUNIT', 'GU_A3', 'SU_DIF', 'SUBUNIT', 'SU_A3', 'BRK_DIFF', 'NAME', 'NAME_LONG', 'BRK_A3', 'BRK_NAME', 'BRK_GROUP', 'ABBREV', 'POSTAL', 'FORMAL_EN', 'FORMAL_FR', 'NAME_CIAWF', 'NOTE_ADM0', 'NOTE_BRK', 'NAME_SORT', 'NAME_ALT', 'MAPCOLOR7', 'MAPCOLOR8', 'MAPCOLOR9', 'MAPCOLOR13', 'POP_EST', 'POP_RANK', 'POP_YEAR', 'GDP_MD', 'GDP_YEAR', 'ECONOMY', 'INCOME_GRP', 'FIPS_10', 'ISO_A2', 'ISO_A2_EH', 'ISO_A3', 'ISO_A3_EH', 'ISO_N3', 'ISO_N3_EH', 'UN_A3', 'WB_A2', 'WB_A3', 'WOE_ID', 'WOE_ID_EH', 'WOE_NOTE', 'ADM0_ISO', 'ADM0_DIFF', 'ADM0_TLC', 'ADM0_A3_US', 'ADM0_A3_FR', 'ADM0_A3_RU', 'ADM0_A3_ES', 'ADM0_A3_CN', 'ADM0_A3_TW', 'ADM0_A3_IN', 'ADM0_A3_NP', 'ADM0_A3_PK', 'ADM0_A3_DE', 'ADM0_A3_GB', 'ADM0_A3_BR', 'ADM0_A3_IL', 'ADM0_A3_PS', 'ADM0_A3_SA', 'ADM0_A3_EG', 'ADM0_A3_MA', 'ADM0_A3_PT', 'ADM0_A3_AR', 'ADM0_A3_JP', 'ADM0_A3_KO', 'ADM0_A3_VN', 'ADM0_A3_TR', 'ADM0_A3_ID', 'ADM0_A3_PL', 'ADM0_A3_GR', 'ADM0_A3_IT', 'ADM0_A3_NL', 'ADM0_A3_SE', 'ADM0_A3_BD', 'ADM0_A3_UA', 'ADM0_A3_UN', 'ADM0_A3_WB', 'CONTINENT', 'REGION_UN', 'SUBREGION', 'REGION_WB', 'NAME_LEN', 'LONG_LEN', 'ABBREV_LEN', 'TINY', 'HOMEPART', 'MIN_ZOOM', 'MIN_LABEL', 'MAX_LABEL', 'LABEL_X', 'LABEL_Y', 'NE_ID', 'WIKIDATAID', 'NAME_AR', 'NAME_BN', 'NAME_DE', 'NAME_EN', 'NAME_ES', 'NAME_FA', 'NAME_FR', 'NAME_EL', 'NAME_HE', 'NAME_HI', 'NAME_HU', 'NAME_ID', 'NAME_IT', 'NAME_JA', 'NAME_KO', 'NAME_NL', 'NAME_PL', 'NAME_PT', 'NAME_RU', 'NAME_SV', 'NAME_TR', 'NAME_UK', 'NAME_UR', 'NAME_VI', 'NAME_ZH', 'NAME_ZHT', 'FCLASS_ISO', 'TLC_DIFF', 'FCLASS_TLC', 'FCLASS_US', 'FCLASS_FR', 'FCLASS_RU', 'FCLASS_ES', 'FCLASS_CN', 'FCLASS_TW', 'FCLASS_IN', 'FCLASS_NP', 'FCLASS_PK', 'FCLASS_DE', 'FCLASS_GB', 'FCLASS_BR', 'FCLASS_IL', 'FCLASS_PS', 'FCLASS_SA', 'FCLASS_EG', 'FCLASS_MA', 'FCLASS_PT', 'FCLASS_AR', 'FCLASS_JP', 'FCLASS_KO', 'FCLASS_VN', 'FCLASS_TR', 'FCLASS_ID', 'FCLASS_PL', 'FCLASS_GR', 'FCLASS_IT', 'FCLASS_NL', 'FCLASS_SE', 'FCLASS_BD', 'FCLASS_UA', 'geometry']
The history saving thread hit an unexpected error (OperationalError('unable to open database file')).History will not be written to the database.

カラム一覧を見ると、このデータでは
* 大陸は CONTINENT(全部大文字)
* 地域は REGION_UN、SUBREGION

これを元に

# 日本だけ絞り込み
japan = world[world["ADMIN"] == "Japan"]

# 大陸名を見る(大文字の CONTINENT)
print(japan[["ADMIN", "CONTINENT"]])

# 代替として地域区分を見る
print(japan[["ADMIN", "REGION_UN", "SUBREGION"]])

とすれば

  ADMIN CONTINENT
155  Japan      Asia
     ADMIN REGION_UN     SUBREGION
155  Japan      Asia  Eastern Asia

となる

これで世界の大陸ごとに分けることができそうなので
大陸列に対して dissolve で集計する

集計方法は
aggfuncで指定する
今回は sum

continents = world.dissolve(by='CONTINENT',aggfunc='sum')
continents

とすると

geometry	featurecla	scalerank	LABELRANK	SOVEREIGNT	SOV_A3	ADM0_DIF	LEVEL	TYPE	TLC	ADMIN	ADM0_A3	GEOU_DIF	GEOUNIT	GU_A3	SU_DIF	SUBUNIT	SU_A3	BRK_DIFF	NAME	NAME_LONG	BRK_A3	BRK_NAME	BRK_GROUP	ABBREV	POSTAL	FORMAL_EN	FORMAL_FR	NAME_CIAWF	NOTE_ADM0	NOTE_BRK	NAME_SORT	NAME_ALT	MAPCOLOR7	MAPCOLOR8	MAPCOLOR9	MAPCOLOR13	POP_EST	POP_RANK	POP_YEAR	GDP_MD	GDP_YEAR	ECONOMY	INCOME_GRP	FIPS_10	ISO_A2	ISO_A2_EH	ISO_A3	ISO_A3_EH	ISO_N3	ISO_N3_EH	UN_A3	WB_A2	WB_A3	WOE_ID	WOE_ID_EH	WOE_NOTE	ADM0_ISO	ADM0_DIFF	ADM0_TLC	ADM0_A3_US	ADM0_A3_FR	ADM0_A3_RU	ADM0_A3_ES	ADM0_A3_CN	ADM0_A3_TW	ADM0_A3_IN	ADM0_A3_NP	ADM0_A3_PK	ADM0_A3_DE	ADM0_A3_GB	ADM0_A3_BR	ADM0_A3_IL	ADM0_A3_PS	ADM0_A3_SA	ADM0_A3_EG	ADM0_A3_MA	ADM0_A3_PT	ADM0_A3_AR	ADM0_A3_JP	ADM0_A3_KO	ADM0_A3_VN	ADM0_A3_TR	ADM0_A3_ID	ADM0_A3_PL	ADM0_A3_GR	ADM0_A3_IT	ADM0_A3_NL	ADM0_A3_SE	ADM0_A3_BD	ADM0_A3_UA	ADM0_A3_UN	ADM0_A3_WB	REGION_UN	SUBREGION	REGION_WB	NAME_LEN	LONG_LEN	ABBREV_LEN	TINY	HOMEPART	MIN_ZOOM	MIN_LABEL	MAX_LABEL	LABEL_X	LABEL_Y	NE_ID	WIKIDATAID	NAME_AR	NAME_BN	NAME_DE	NAME_EN	NAME_ES	NAME_FA	NAME_FR	NAME_EL	NAME_HE	NAME_HI	NAME_HU	NAME_ID	NAME_IT	NAME_JA	NAME_KO	NAME_NL	NAME_PL	NAME_PT	NAME_RU	NAME_SV	NAME_TR	NAME_UK	NAME_UR	NAME_VI	NAME_ZH	NAME_ZHT	FCLASS_ISO	TLC_DIFF	FCLASS_TLC	FCLASS_US	FCLASS_FR	FCLASS_RU	FCLASS_ES	FCLASS_CN	FCLASS_TW	FCLASS_IN	FCLASS_NP	FCLASS_PK	FCLASS_DE	FCLASS_GB	FCLASS_BR	FCLASS_IL	FCLASS_PS	FCLASS_SA	FCLASS_EG	FCLASS_MA	FCLASS_PT	FCLASS_AR	FCLASS_JP	FCLASS_KO	FCLASS_VN	FCLASS_TR	FCLASS_ID	FCLASS_PL	FCLASS_GR	FCLASS_IT	FCLASS_NL	FCLASS_SE	FCLASS_BD	FCLASS_UA	
CONTINENT																																																																																																																																																																								
Africa	MULTIPOLYGON (((-11.43878 6.78592, -11.70819 6.8601, -12.4281 7.26294, -12.94905 7.79865, -13.12403 8.16395, -13.24655 8.90305, -13.68515 9.49474, -14.07404 9.88617, -14.33008 10.01572, -14.5797 10.21447, -14.69323 10.6563, -14.83955 10.87657, -15.13031 11.04041, -15.66418 11.45847, -16.08521 11.52459, -16.31479 11.80651, -16.30895 11.9587, -16.61384 12.17091, -16.67745 12.38485, -16.84152 13.15139, -16.71373 13.59496, -17.12611 14.37352, -17.62504 14.72954, -17.18517 14.91948, -16.70071 15.62153, -16.4631 16.13504, -16.54971 16.67389, -16.27055 17.16696, -16.14635 18.10848, -16.25688 19.09672, -16.37765 19.59382, -16.27784 20.09252, -16.53632 20.56787, -17.06342 20.99975, -17.02043 21.42231, -16.97325 21.88574, -16.58914 22.15823, -16.26192 22.67934, -16.32641 23.01777, -15.98261 23.72336, -15.426 24.35913, -15.08933 24.52026, -14.82465 25.10353, -14.80093 25.63626, -14.43994 26.25442, -13.7738 26.61889, -13.13994 27.64015, -13.12161 27.65415, -12.61884 28.03819, -11.68892 28.14864, -10.90096 28.83214, -10.39959 29.09859, -9.56481 29.93357, -9.81472 31.17774, -9.43479 32.0381, -9.30069 32.56468, -8.65748 33.24025, -7.65418 33.69706, -6.91254 34.11048, -6.24434 35.14587, -5.92999 35.75999, -5.19386 35.75518, -4.59101 35.33071, -3.64006 35.39986, -2.60431 35.17909, -2.16991 35.1684, -1.2086 35.71485, -0.12745 35.88866, 0.50388 36.30127, 1.46692 36.60565, 3.1617 36.7839, 4.81576 36.86504, 5.32012 36.71652, 6.26182 37.11066, 7.33038 37.11838, 7.73708 36.88571, 8.42096 36.94643, 9.50999 37.34999, 10.21 37.23, 10.18065 36.72404, 11.02887 37.0921, 11.10003 36.9, 10.6 36.41, 10.59329 35.94744, 10.93952 35.69898, 10.80785 34.83351, 10.14959 34.33077, 10.33966 33.78574, 10.85684 33.76874, 11.1085 33.29334, 11.48879 33.137, 12.66331 32.79278, 13.08326 32.87882, 13.91868 32.71196, 15.24563 32.26508, 15.71394 31.37626, 16.61162 31.18218, 18.02109 30.76357, 19.08641 30.26639, 19.57404 30.52582, 20.05335 30.98576, 19.82033 31.75179, 20.13397 32.2382, 20.85452 32.7068, 21.54298 32.8432, 22.89576 32.63858, 23.2368 32.19149, 23.60913 32.18726, 23.9275 32.01667, 24.92114 31.89936, 25.16482 31.56915, 26.49533 31.58568, 27.45762 31.32126, 28.45048 31.02577, 28.91353 30.87005, 29.68342 31.18686, 30.09503 31.4734, 30.97693 31.55586, 31.68796 31.4296, 31.96041 30.9336, 32.19247 31.26034, 32.99392 31.02407, 33.7734 30.96746, 34.26543 31.21936, 34.26544 31.21936, 34.82324 29.76108, 34.9226 29.50133, 34.64174 29.09942, 34.42655 28.34399, 34.15451 27.8233, 33.92136 27.6487, 33.58811 27.97136, 33.13676 28.41765, 32.42323 29.85108, 32.32046 29.76043, 32.73482 28.70523, 33.34876 27.69989, 34.10455 26.14227, 34.47387 25.59856, 34.79507 25.03375, 35.69241 23.92671, 35.49372 23.75237, 35.52598 23.10244, 36.69069 22.20485, 36.86623 22, 37.18872 21.01885, 36.96941 20.83744, 37.1147 19.80796, 37.48179 18.61409, 37.86276 18.36786, 38.41009 17.99831, 38.99062 16.84063, 39.26611 15.92272, 39.81429 15.43565, 41.17927 14.49108, 41.73495 13.92104, 42.27683 13.34399, 42.58958 13.00042, 43.08123 12.69964, 43.31785 12.39015, 43.28638 11.97493, 42.71587 11.73564, 43.1453 11.46204, 43.47066 11.27771, 43.66667 10.86417, 44.1178 10.44554, 44.61426 10.44221, 45.55694 10.69803, 46.6454 10.81655, 47.52566 11.12723, 48.0216 11.19306, 48.37878 11.37548, 48.94821 11.41062, 48.9482 11.41062, 49.26776 11.43033, 49.72862 11.5789, 50.25878 11.67957, 50.73202 12.0219, 51.1112 12.02464, 51.13387 11.74815, 51.04153 11.16651, 51.04531 10.6409, 50.83418 10.27972, 50.55239 9.19874, 50.07092 8.08173, 49.4527 6.80466, 48.59455 5.33911, 47.74079 4.2194, 46.56476 2.85529, 45.56399 2.04576, 44.06815 1.05283, 43.13597 0.2922, 42.04157 -0.91916, 41.81095 -1.44647, 41.58513 -1.68325, 40.88477 -2.08255, 40.63785 -2.49979, 40.26304 -2.57309, 40.12119 -3.27768, 39.80006 -3.68116, 39.60489 -4.34653, 39.20222 -4.67677, 38.74054 -5.90895, 38.79977 -6.47566, 39.44 -6.84, 39.47 -7.1, 39.19469 -7.7039, 39.25203 -8.00781, 39.18652 -8.48551, 39.53574 -9.11237, 39.9496 -10.0984, 40.31659 -10.3171, 40.31659 -10.3171, 40.47839 -10.76544, 40.43725 -11.76171, 40.56081 -12.63918, 40.59962 -14.20198, 40.77548 -14.69176, 40.47725 -15.40629, 40.08926 -16.10077, 39.45256 -16.72089, 38.53835 -17.10102, 37.41113 -17.58637, 36.28128 -18.65969, 35.8965 -18.84226, 35.1984 -19.55281, 34.78638 -19.78401, 34.70189 -20.49704, 35.17613 -21.25436, 35.37343 -21.84084, 35.38585 -22.14, 35.56255 -22.09, 35.53393 -23.07079, 35.37177 -23.53536, 35.60747 -23.70656, 35.45875 -24.12261, 35.04073 -24.47835, 34.21582 -24.81631, 33.01321 -25.35757, 32.57463 -25.72732, 32.66036 -26.14858, 32.91596 -26.21587, 32.83012 -26.74219, 32.58026 -27.47016, 32.46213 -28.30101, 32.20339 -28.7524, 31.521 -29.25739, 31.32556 -29.40198, 30.90176 -29.90996, 30.62281 -30.42378, 30.05572 -31.14027, 28.92555 -32.17204, 28.21976 -32.77195, 27.46461 -33.22696, 26.41945 -33.61495, 25.90966 -33.66704, 25.78063 -33.94465, 25.17286 -33.79685, 24.67785 -33.98718, 23.59404 -33.79447, 22.98819 -33.91643, 22.57416 -33.86408, 21.5428 -34.25884, 20.68905 -34.41718, 20.07126 -34.79514, 19.61641 -34.81917, 19.19328 -34.4626, 18.85531 -34.44431, 18.42464 -33.99787, 18.37741 -34.13652, 18.2445 -33.86775, 18.25008 -33.28143, 17.92519 -32.61129, 18.24791 -32.42913, 18.22176 -31.66163, 17.56692 -30.72572, 17.06442 -29.87864, 17.06292 -29.87595, 16.34498 -28.57671, 15.60182 -27.82125, 15.21047 -27.09096, 14.98971 -26.11737, 14.74321 -25.39292, 14.40814 -23.85301, 14.38572 -22.65665, 14.25771 -22.11121, 13.86864 -21.69904, 13.3525 -20.87283, 12.82685 -19.67317, 12.60856 -19.04535, 11.79492 -18.06913, 11.7342 -17.30189, 11.6401 -16.67314, 11.77854 -15.79382, 12.12358 -14.87832, 12.17562 -14.44914, 12.5001 -13.5477, 12.73848 -13.13791, 13.31291 -12.48363, 13.63372 -12.03864, 13.73873 -11.29786, 13.68638 -10.73108, 13.38733 -10.37358, 13.12099 -9.7669, 12.87537 -9.16693, 12.92906 -8.95909, 13.23643 -8.56263, 12.93304 -7.59654, 12.7283 -6.92712, 12.22735 -6.29445, 12.32243 -6.10009, 12.18234 -5.78993, 11.91496 -5.03799, 11.09377 -3.97883, 10.06614 -2.96948, 9.40525 -2.14431, 8.798 -1.1113, 8.83009 -0.77907, 9.04842 -0.45935, 9.29135 0.26867, 9.49289 1.01012, 9.30561 1.16091, 9.64916 2.28387, 9.7952 3.0734, 9.40437 3.73453, 8.94812 3.90413, 8.74492 4.35222, 8.48882 4.49562, 8.50029 4.77198, 7.46211 4.41211, 7.0826 4.46469, 6.69807 4.24059, 5.89817 4.26245, 5.3628 4.88797, 5.03357 5.6118, 4.32561 6.27065, 3.57418 6.2583, 2.6917 6.25882, 1.86524 6.14216, 1.06012 5.92884, -0.50764 5.34347, -1.06362 5.00055, -1.96471 4.71046, -2.85613 4.99448, -3.31108 4.9843, -4.00882 5.17981, -4.64992 5.16826, -5.8345 4.9937, -6.52877 4.70509, -7.51894 4.33829, -7.71216 4.36457, -7.97411 4.35576, -9.00479 4.83242, -9.91342 5.59356, -10.76538 6.14071, -11.43878 6.78592)), ((50.20027 -16.00026, 49.86061 -15.41425, 49.67261 -15.7102, 49.86334 -16.45104, 49.77456 -16.87504, 49.49861 -17.10604, 49.43562 -17.95306, 49.04179 -19.11878, 48.54854 -20.49689, 47.93075 -22.3915, 47.54772 -23.78196, 47.09576 -24.94163, 46.28248 -25.17846, 45.40951 -25.60143, 44.83357 -25.3461, 44.03972 -24.98835, 43.76377 -24.46068, 43.69778 -23.57412, 43.34565 -22.7769, 43.25419 -22.05741, 43.4333 -21.33648, 43.89368 -21.16331, 43.89637 -20.83046, 44.37433 -20.07237, 44.4644 -19.43545, 44.23242 -18.96199, 44.04298 -18.33139, 43.96308 -17.40994, 44.31247 -16.8505, 44.44652 -16.21622, 44.94494 -16.17937, 45.50273 -15.97437, 45.87299 -15.79345, 46.31224 -15.78002, 46.88218 -15.21018, 47.70513 -14.5943, 48.00521 -14.09123, 47.86905 -13.66387, 48.29383 -13.78407, 48.84506 -13.08917, 48.86351 -12.48787, 49.19465 -12.04056, 49.54352 -12.46983, 49.80898 -12.89528, 50.05651 -13.55576, 50.21743 -14.75879, 50.47654 -15.22651, 50.37711 -15.70607, 50.20027 -16.00026)))			

というようになってしまうので方法を変える

continents = world.dissolve(
    by='CONTINENT',
    aggfunc={
        'POP_EST': 'sum',
        'GDP_MD': 'sum'
    }
).reset_index()

continents[['CONTINENT','POP_EST','GDP_MD']]

としたが

CONTINENT	POP_EST	GDP_MD	
0	Africa	1.306370e+09	2455514
1	Antarctica	4.490000e+03	898
2	Asia	4.550277e+09	32725478
3	Europe	7.454125e+08	21587850
4	North America	5.837560e+08	25075988
5	Oceania	4.120487e+07	1647113
6	Seven seas (open ocean)	1.400000e+02	16
7	South America	4.270667e+08	3852015

となって geometry が無くなっている

計後に列を選び直したときに geometry 列を外してしまい、ただの DataFrame になったからです。geometry が無いと GeoDataFrame ではなくなるので地理情報が消えたように見える

これだと使えないので別の方法にする

GPTで調べたら
pandas の表示設定で抑える(geometryはそのまま)
Shapelyオブジェクトは文字列化されると長い座標列が出ますが、
pd.set_option を調整すると途中で省略される

import pandas as pd
pd.set_option("display.max_colwidth", 80)  # 長さ制限
continents.head()

とすれば
これなら geometry は Shapely のまま保持され、
計算や可視化にもそのまま使える

CONTINENT	geometry	POP_EST	GDP_MD	
0	Africa	MULTIPOLYGON (((-11.43878 6.78592, -11.70819 6.8601, -12.4281 7.26294, -12.9...	1.306370e+09	2455514
1	Antarctica	MULTIPOLYGON (((-61.13898 -79.98137, -60.61012 -79.62868, -59.57209 -80.0401...	4.490000e+03	898
2	Asia	MULTIPOLYGON (((48.67923 14.0032, 48.23895 13.94809, 47.93891 14.00723, 47.3...	4.550277e+09	32725478
3	Europe	MULTIPOLYGON (((-53.55484 2.3349, -53.77852 2.3767, -54.08806 2.10556, -54.5...	7.454125e+08	21587850
4	North America	MULTIPOLYGON (((-155.22217 19.23972, -155.54211 19.08348, -155.68817 18.9161...	5.837560e+08	25075988

座標系の変換

座標系の変換

緯度経度は度になっているが
面積、距離などを求める時には変換が必要

https://sorabatake.jp/20670/
も参考にする

Name: WGS 84

– Lat[north]: Geodetic latitude (degree)
– Lon[east]: Geodetic longitude (degree)

がよく使われる
これはGPSで使われる

WGS 84
これを
EPSGにすると4326
https://epsg.io/4326
単位は度

これは世界

日本近辺に特化したものが
EPSG:6668
https://epsg.io/6668
単位は度

日本付近に絞るならこちらを使う方が良さそう

EPSG:6691
https://epsg.io/6691
これは
単位がメートルになっている
こちらも日本周辺がメイン

Area of use: Japan – between 138°E and 144°E, onshore and offshore.
となっているので
それ以外のエリアの精度が正しくないことがあるので注意

とりあえず品川の情報を表示

shinagawa
	N03_001	N03_002	N03_003	N03_004	N03_007	geometry
87	東京都	None	品川区	None	13109	POLYGON ((139.75501 35.61771, 139.75507 35.617...
88	東京都	None	品川区	None	13109	POLYGON ((139.77231 35.62188, 139.77302 35.621...
89	東京都	None	品川区	None	13109	POLYGON ((139.75953 35.625, 139.75966 35.6216,...
90	東京都	None	品川区	None	13109	POLYGON ((139.71943 35.64167, 139.71953 35.641...

座標情報を見るなら crs で表示できる

shinagawa.crs

結果は

<Geographic 2D CRS: EPSG:6668>
Name: JGD2011
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: Japan - onshore and offshore.
- bounds: (122.38, 17.09, 157.65, 46.05)
Datum: Japanese Geodetic Datum 2011
- Ellipsoid: GRS 1980
- Prime Meridian: Greenwich

この中で注目するのは

EPSG:6668

- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)

から
単位は度であることがわかる

同様に路線図の crs も確認する

gdf.crs

結果は

<Geographic 2D CRS: EPSG:4326>
Name: WGS 84
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: World.
- bounds: (-180.0, -90.0, 180.0, 90.0)
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich

こちらも

EPSG:4326

- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)

から単位は度であることがわかる

この2つはEPSGコードが異なるため
このままだと一緒に使えないので変換をして合わせる

今回は路線図のEPSGコードを6668に変換するので
to_crs を使う

gdf.to_crs(epsg=6668,inplace=True)
gdf.crs

これで

<Geographic 2D CRS: EPSG:6668>
Name: JGD2011
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: Japan - onshore and offshore.
- bounds: (122.38, 17.09, 157.65, 46.05)
Datum: Japanese Geodetic Datum 2011
- Ellipsoid: GRS 1980
- Prime Meridian: Greenwich

となる

山手線のプロット

山手線のプロット

路線図から山手線のみプロットする

gdf = gpd.read_file('./N02-19_GML/N02-19_RailroadSection.geojson')
gdf

	鉄道区分	事業者種別	路線名	運営会社	geometry
0	23	5	沖縄都市モノレール線	沖縄都市モノレール	LINESTRING (127.67948 26.21454, 127.6797 26.21...
1	12	5	いわて銀河鉄道線	アイジーアールいわて銀河鉄道	LINESTRING (141.29139 40.3374, 141.29176 40.33...
2	12	5	いわて銀河鉄道線	アイジーアールいわて銀河鉄道	LINESTRING (141.27554 40.23936, 141.27567 40.2...
3	12	5	いわて銀河鉄道線	アイジーアールいわて銀河鉄道	LINESTRING (141.28659 40.26092, 141.28538 40.2...
4	12	5	いわて銀河鉄道線	アイジーアールいわて銀河鉄道	LINESTRING (141.29082 40.28615, 141.29089 40.2...
...	...	...	...	...	...
22011	12	4	相鉄新横浜線	相模鉄道	LINESTRING (139.58522 35.48055, 139.58384 35.4...
22012	12	4	相鉄新横浜線	相模鉄道	LINESTRING (139.56671 35.47802, 139.56433 35.4...
22013	11	2	おおさか東線	西日本旅客鉄道	LINESTRING (135.56227 34.68802, 135.564 34.68798)
22014	11	2	東海道線	東日本旅客鉄道	LINESTRING (139.66375 35.57335, 139.66382 35.5...
22015	11	2	東海道線	東日本旅客鉄道	LINESTRING (139.58705 35.4819, 139.58646 35.48...

となっているので
路線名で調べれるかをテスト

gdf['路線名'].unique()

結果は

array(['沖縄都市モノレール線', 'いわて銀河鉄道線', 'いすみ線', '三国芦原線', '勝山永平寺線', '湯前線',
       'しなの鉄道線', '広島短距離交通瀬野線', '七尾線', 'わたらせ渓谷線', '阿佐東線', '阿武隈急行線',
       '愛知環状鉄道線', '東部丘陵線', '鞍馬山鋼索鉄道', '伊勢線', '伊豆急行線', '十国鋼索線', '駿豆線',
       '大雄山線', '横河原線', '花園線', '郡中線', '高浜線', '城南線', '城北線', '大手町線', '本町線',
       '井原線', '大社線', '北松江線', '鞍馬線', '叡山本線', '鉄道線', 'みなとみらい21線', '1号線',
       '3号線', '金沢シーサイドライン', '清輝橋線', '東山本線', '会津線', '岳南線', '甘木線', '常総線',
       '竜ヶ崎線', '紀州鉄道線', '井の頭線', '京王線', '競馬場線', '高尾線', '相模原線', '動物園線',
       '宇治線', '鴨東線', '京阪本線', '京津線', '交野線', '鋼索線', '石山坂本線', '押上線', '金町線',
       '千原線', '千葉線', '東成田線', '本線', '烏丸線', '東西線', '久里浜線', '空港線', '逗子線',
       '大師線', '北野線', '嵐山本線', '錦川清流線', '橿原線', '吉野線', '京都線', '御所線', '山田線',
       '志摩線', '信貴線', '生駒鋼索線', '生駒線', '西信貴鋼索線', '大阪線', '長野線', '鳥羽線', '天理線',
       '田原本線', '湯の山線', '道明寺線', '奈良線', '内部線', '南大阪線', '難波線', '阪神なんば線',
       '八王子線', '名古屋線', '鈴鹿線', '多賀線', '八日市線', '吉都線', '久大線', '宮崎空港線',
       '九州新幹線', '後藤寺線', '香椎線', '佐世保線', '三角線', '山陽線', '指宿枕崎線', '鹿児島線',
       '篠栗線', '大村線', '筑肥線', '筑豊線', '長崎線', '唐津線', '日田彦山線', '日南線', '日豊線',
       '肥薩線', '豊肥線', '幹線', '健軍線', '上熊本線', '水前寺線', '田崎線', '菊池線', '藤崎線',
       'ケーブルカー', '広島新交通1号線', '宇品線', '横川線', '皆実線', '宮島線', '江波線', '白島線',
       '弘南線', '大鰐線', '江ノ島電鉄線', '琴平線', '志度線', '長尾線', '高尾鋼索線', '嵯峨野観光線',
       '伊丹線', '甲陽線', '今津線', '神戸高速線', '神戸線', '千里線', '宝塚線', '箕面線', '嵐山線',
       '阪堺線', '上町線', '武庫川線', '埼玉高速鉄道線', '伊奈線', '1条線', '山鼻西線', '山鼻線',
       '東豊線', '南北線', '近鉄連絡線', '三岐線', '北勢線', 'リアス線', '釜石線', 'フラワー長井線',
       'ユーカリが丘線', '網干線', '八栗ケーブル', '高徳線', '土讃線', '徳島線', '内子線', '本四備讃線',
       '牟岐線', '鳴門線', '予讃線', '予土線', '第一期線', '第二期線', '谷山線', '唐湊線', '大洗鹿島線',
       '芝山鉄道線', '若桜線', '常磐新線', '秋田内陸線', '江ノ島線', '小田原線', '多摩線', '小湊鐵道線',
       '西九州線', '上高地線', '江の島線', '上信線', '別所線', '上毛線', '信楽線', '新京成線', '真岡線',
       '海岸線', '山手線', '西神延伸線', '西神線', '摩耶ケーブル線', '六甲アイランド線', '粟生線',
       '公園都市線', '三田線', '有馬線', '水間線', '水島本線', '貝塚線', '太宰府線', '天神大牟田線',
       'JR東西線', '因美線', '宇部線', '宇野線', '越美北線', '加古川線', '可部線', '関西空港線',
       '関西線', '岩徳線', '紀勢線', '吉備線', '境線', '芸備線', '湖西線', '呉線', '高山線', '阪和線',
       '桜井線', '桜島線', '山陰線', '山口線', '山陽新幹線', '小浜線', '小野田線', '城端線', '赤穂線',
       '草津線', '大阪環状線', '大糸線', '津山線', '東海道線', '播但線', '伯備線', '博多南線', '美祢線',
       '姫新線', '氷見線', '舞鶴線', '福塩線', '福知山線', '片町線', '日本海ひすいライン',
       '妙高はねうまライン', 'あいの風とやま鉄道線', 'IRいしかわ鉄道線', '北陸線', '木次線', '和歌山線',
       '狭山線', '国分寺線', '新宿線', '西武園線', '西武秩父線', '西武有楽町線', '多摩湖線', '多摩川線',
       '池袋線', '拝島線', '豊島線', '青い森鉄道線', '静岡清水線', '2号線', 'いずみ野線', '流山線',
       '多摩都市モノレール線', '井川線', '大井川本線', '国際文化公園都市モノレール線(彩都線)', '大阪モノレール線',
       '1号線(御堂筋線)', '2号線(谷町線)', '3号線(四つ橋線)', '4号線(中央線)', '5号線(千日前線)',
       '6号線(堺筋線)', '7号線(長堀鶴見緑地線)', '南港ポートタウン線', '泉北高速鉄道線', '大山鋼索線', '樽見線',
       '天橋立鋼索鉄道', '智頭線', '筑波山鋼索鉄道線', '筑豊電気鉄道線', '秩父本線', '銚子電気鉄道線',
       '蛍茶屋支線', '桜町支線', '赤迫支線', '大浦支線', '越美南線', '津軽鉄道線', '天竜浜名湖線', '阿佐線',
       '宿毛線', '中村線', '伊野線', '後免線', '桟橋線', '駅前線', '島原鉄道線', '御殿場線', '参宮線',
       '身延線', '太多線', '中央線', '東海道新幹線', '飯田線', '武豊線', '名松線', '東京モノレール羽田線',
       'こどもの国線', '世田谷線', '大井町線', '池上線', '田園都市線', '東横線', '東急多摩川線', '目黒線',
       '2号線日比谷線', '3号線銀座線', '4号線丸ノ内線', '4号線丸ノ内線分岐線', '5号線東西線', '7号線南北線',
       '8号線有楽町線', '13号線副都心線', '9号線千代田線', '11号線半蔵門線', '1号線浅草線', '6号線三田線',
       '10号線新宿線', '12号線大江戸線', '荒川線', '上野懸垂線', 'りんかい線', '伊東線', '烏山線',
       '羽越線', '越後線', '奥羽線', '横須賀線', '横浜線', '花輪線', '外房線', '気仙沼線', '久留里線',
       '京葉線', '五日市線', '五能線', '吾妻線', '高崎線', '根岸線', '左沢線', '鹿島線', '篠ノ井線',
       '小海線', '上越新幹線', '上越線', '常磐線', '信越線', '北しなの線', '水郡線', '水戸線', '成田線',
       '青梅線', '石巻線', '赤羽線(埼京線)', '仙山線', '仙石線', '川越線', '相模線', '総武線',
       '大船渡線', '大湊線', '只見線', '男鹿線', '津軽線', '鶴見線', '田沢湖線', '東金線', '東北新幹線',
       '東北線', '東北線(埼京線)', '内房線', '南武線', '日光線', '白新線', '八戸線', '八高線', '飯山線',
       '磐越西線', '磐越東線', '武蔵野線', '米坂線', '北上線', '北陸新幹線', '弥彦線', '陸羽西線',
       '陸羽東線', '両毛線', '伊勢崎線', '宇都宮線', '越生線', '鬼怒川線', '亀戸線', '桐生線', '佐野線',
       '小泉線', '東上本線', '野田線', '東葉高速線', '高森線', '加太線', '高師浜線', '高野線', '多奈川線',
       '南海本線', '和歌山港線', '日生線', '妙見線', '大森線', '湯の川線', '宝来・谷地頭線',
       '皿倉山ケーブルカー', '比叡山鉄道線', '肥薩おれんじ鉄道線', '安野屋線', '呉羽線', '支線', '上滝線',
       '不二越線', '立山線', '河口湖線', '大月線', 'ディズニーリゾートライン', '福武線', '1号線(空港線)',
       '2号線(箱崎線)', '3号線(七隈線)', '飯坂線', '伊田線', '糸田線', '田川線', '渥美線', '東田本線',
       'ほくほく線', '海峡線', '釧網線', '道南いさりび鉄道線', '根室線', '札沼線', '室蘭線', '宗谷線',
       '石勝線', '石北線', '千歳線', '日高線', '函館線', '富良野線', '留萌線', '宮津線', '宮福線',
       '小倉線', '北条線', '北神線', '北総線', '石川線', '浅野川線', '高岡軌道線', '新湊港線',
       'ガイドウェイバス志段味線', '1号線東山線', '2号線名港線', '2号線名城線', '3号線鶴舞線', '4号線名城線',
       '6号線桜通線', '上飯田線', '羽島線', '河和線', '各務原線', '蒲郡線', '犬山線', '広見線', '三河線',
       '小牧線', '常滑線', '瀬戸線', '西尾線', '知多新線', '築港線', '竹鼻線', '津島線', '尾西線',
       '豊川線', '豊田線', '名古屋本線', '西名古屋港線', '明知線', '会津鬼怒川線', '鳥海山ろく線',
       '無軌条電車線', '六甲ケーブル線', '貴志川線', '東京臨海新交通臨海線', 'けいはんな線', 'ポートアイランド線',
       '富山港線', '仙台空港線', '8号線(今里筋線)', '中之島線', '富山都心線', '門司港レトロ観光線', '4号線',
       '日暮里・舎人線', 'おおさか東線', '湊線', '成田空港線', '養老線', '別府ラクテンチケーブル線', '伊賀線',
       '青函トンネル竜飛斜坑線', '富山駅南北接続線', '都心線', '北海道新幹線', '相鉄新横浜線'], dtype=object)

となる

この中から
山手線
が入っているかを true falseで調べる

'山手線' in gdf['路線名'].unique()

結果は

True

なので山手線に絞って表示する

gdf_yamanote = gdf[gdf['路線名']=='山手線']
gdf_yamanote

結果は

	鉄道区分	事業者種別	路線名	運営会社	geometry
6631	12	3	山手線	神戸市	LINESTRING (135.14601 34.65849, 135.1453 34.65...
6632	12	3	山手線	神戸市	LINESTRING (135.19571 34.70498, 135.19561 34.7...
6633	12	3	山手線	神戸市	LINESTRING (135.15161 34.66859, 135.15099 34.6...
6634	12	3	山手線	神戸市	LINESTRING (135.19273 34.69404, 135.1921 34.69...
6635	12	3	山手線	神戸市	LINESTRING (135.18364 34.69074, 135.18351 34.6...
...	...	...	...	...	...
14500	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71054 35.64593, 139.71079 35.6...
14501	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71334 35.64093, 139.71343 35.6...
14502	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71438 35.63865, 139.71442 35.6...
14503	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71438 35.63865, 139.71447 35.6...
14504	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71499 35.63644, 139.71502 35.6...

これだと神戸のものも含まれるため
運営会社も絞り込みする

gdf_yamanote = gdf[(gdf['路線名']=='山手線') & (gdf['運営会社']=='東日本旅客鉄道')]
gdf_yamanote

とすれば

鉄道区分	事業者種別	路線名	運営会社	geometry	
14455	11	2	山手線	東日本旅客鉄道	LINESTRING (139.74144 35.73419, 139.74435 35.7...
14456	11	2	山手線	東日本旅客鉄道	LINESTRING (139.7585 35.74006, 139.759 35.7396...
14457	11	2	山手線	東日本旅客鉄道	LINESTRING (139.74914 35.73738, 139.74937 35.7...
14458	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70915 35.72618, 139.70802 35.7...
14459	11	2	山手線	東日本旅客鉄道	LINESTRING (139.7105 35.72903, 139.70915 35.72...
14460	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71167 35.73152, 139.7105 35.72...
14461	11	2	山手線	東日本旅客鉄道	LINESTRING (139.72695 35.73203, 139.72422 35.7...
14462	11	2	山手線	東日本旅客鉄道	LINESTRING (139.76067 35.73826, 139.76248 35.7...
14463	11	2	山手線	東日本旅客鉄道	LINESTRING (139.74144 35.73419, 139.73926 35.7...
14464	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70423 35.7141, 139.70418 35.71...
14465	11	2	山手線	東日本旅客鉄道	LINESTRING (139.74697 35.73634, 139.74909 35.7...
14466	11	2	山手線	東日本旅客鉄道	LINESTRING (139.7066 35.72145, 139.70596 35.71...
14467	11	2	山手線	東日本旅客鉄道	LINESTRING (139.72893 35.73154, 139.72695 35.7...
14468	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70596 35.71933, 139.70582 35.7...
14469	11	2	山手線	東日本旅客鉄道	LINESTRING (139.73926 35.73325, 139.73863 35.7...
14470	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70024 35.68926, 139.70034 35.6...
14471	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70176 35.68735, 139.70222 35.6...
14472	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70019 35.70011, 139.70006 35.6...
14473	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70129 35.65907, 139.70124 35.6...
14474	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70125 35.6879, 139.70148 35.68...
14475	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70295 35.6722, 139.70345 35.67...
14476	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70004 35.69167, 139.70024 35.6...
14477	11	2	山手線	東日本旅客鉄道	LINESTRING (139.7006 35.68995, 139.70004 35.69...
14478	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70125 35.6879, 139.70091 35.68...
14479	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70203 35.6845, 139.70239 35.68...
14480	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70239 35.68309, 139.70255 35.6...
14481	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70032 35.70175, 139.70019 35.7...
14482	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70251 35.67042, 139.70264 35.6...
14483	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70068 35.69107, 139.70116 35.6...
14484	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70342 35.71171, 139.70298 35.7...
14485	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70222 35.68555, 139.70272 35.6...
14486	11	2	山手線	東日本旅客鉄道	LINESTRING (139.72888 35.61912, 139.72761 35.6...
14487	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71054 35.64593, 139.70971 35.6...
14488	11	2	山手線	東日本旅客鉄道	LINESTRING (139.72761 35.62077, 139.72628 35.6...
14489	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71334 35.64093, 139.71404 35.6...
14490	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70971 35.64743, 139.70956 35.6...
14491	11	2	山手線	東日本旅客鉄道	LINESTRING (139.70221 35.65723, 139.70201 35.6...
14492	11	2	山手線	東日本旅客鉄道	LINESTRING (139.72436 35.62526, 139.72422 35.6...
14493	11	2	山手線	東日本旅客鉄道	LINESTRING (139.73881 35.62549, 139.73883 35.6...
14494	11	2	山手線	東日本旅客鉄道	LINESTRING (139.73762 35.62049, 139.73764 35.6...
14495	11	2	山手線	東日本旅客鉄道	LINESTRING (139.7328 35.61677, 139.73342 35.61...
14496	11	2	山手線	東日本旅客鉄道	LINESTRING (139.72888 35.61912, 139.7295 35.61...
14497	11	2	山手線	東日本旅客鉄道	LINESTRING (139.73863 35.627, 139.73844 35.62927)
14498	11	2	山手線	東日本旅客鉄道	LINESTRING (139.72262 35.62727, 139.72231 35.6...
14499	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71636 35.63136, 139.71585 35.6...
14500	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71054 35.64593, 139.71079 35.6...
14501	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71334 35.64093, 139.71343 35.6...
14502	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71438 35.63865, 139.71442 35.6...
14503	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71438 35.63865, 139.71447 35.6...
14504	11	2	山手線	東日本旅客鉄道	LINESTRING (139.71499 35.63644, 139.71502 35.6...

となr

gdf_yamanote.plot()

で表示すると

となる
環状でないのは、運営会社が別のところになっているかららしい

x軸の数値を表示を綺麗にするなら

fig = plt.figure(figsize=(9,6))
ax = fig.add_subplot(1,1,1)
gdf_yamanote.plot(ax=ax)

とすれば

というようにx軸の表示が潰れない

これは応用で東京都の県境とか市区町村の境界データを持ってきて
一緒に表示することもできる

次はCSVファイルの読み込み

外付けSSDへのバックアップ

外付けSSDへのバックアップ

mv: fastcopy: read() failed: /Users/snowpool/Desktop/スクリーンショット 2025-05-11 6.26.05.png: Need authenticator

となるため
ファイルを調べたら iCloud に保存されている
これが原因だった

最初に外付けSSDが認識しなかったため

df -h /Volumes/KINGSTON 

としたが認識していない

このため

diskutil list

で調べた

 diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *251.0 GB   disk0
   1:             Apple_APFS_ISC Container disk1         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         245.1 GB   disk0s2
   3:        Apple_APFS_Recovery Container disk2         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +245.1 GB   disk3
                                 Physical Store disk0s2
   1:                APFS Volume Macintosh HD            16.1 GB    disk3s1
   2:              APFS Snapshot com.apple.os.update-... 16.1 GB    disk3s1s1
   3:                APFS Volume Preboot                 13.9 GB    disk3s2
   4:                APFS Volume Recovery                2.1 GB     disk3s3
   5:                APFS Volume Data                    208.1 GB   disk3s5
   6:                APFS Volume VM                      2.1 GB     disk3s6

/dev/disk4 (disk image):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        +3.4 GB     disk4
   1:                        EFI NO NAME                 209.7 MB   disk4s1
   2:                  Apple_HFS QGIS-LTR.app            3.1 GB     disk4s2

/dev/disk5 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *1.0 TB     disk5
   1:               Windows_NTFS KINGSTON                1.0 TB     disk5s1

どうやらマウントできているらしい

/dev/disk5 (external, physical):
   #:                       TYPE NAME     SIZE       IDENTIFIER
   0:     FDisk_partition_scheme         *1.0 TB     disk5
   1:               Windows_NTFS KINGSTON 1.0 TB     disk5s1

がその部分

このあとは認識したので

mkdir /Volumes/KINGSTON/M1Mac_bak/download_bak
mv ~/Desktop/* /Volumes/KINGSTON/M1Mac_bak/desctok_bak

でバックアップフォルダを作成

あとは

 mv ~/Downloads/* /Volumes/KINGSTON/M1Mac_bak/download_bak

でバックアップしておく

購入頻度と消費間隔の予測

購入頻度と消費間隔の予測

まず過去の購入データを全てcsvにする
これをSqlite にインポート
その後に
SQLite の購入データを Neo4j に同期させる

ここまでできたら情報が整うので
購入頻度
消費感覚
価格の推移
最安値店舗の把握と最高値店舗の把握
ができるので在庫がどれくらいの期間で消費されているかを予測できる

店舗での購入物を管理できるので
家計の把握とコントロールが可能

家で作成しているレシピを登録することで
1品ごとのコストが把握できる

これなら惣菜を買った方が得という意見の反論の証拠にもなる

あと購入物をカテゴライズし
生鮮食品なら3日
調味料1日
というようにすると良いかもしれないがバターなどはカテゴリが面倒

購入管理なら
日用品ならまとめ買いで頻繁に購入する必要がないので使えそう
あとは調味料関連
とりあえず年間の購入の履歴から購入頻度を割り出す
その後にカテゴリ分けと期間の設定を割り出した方が良さそう

あとはレシピの登録

mkdir recipes
ここにレシピを登録して neo4jに一括登録する

Google Drive の画像を linebot へ送信する機能のモジュール化

Google Drive の画像を linebot へ送信する機能のモジュール化

import requests
import random
import time
from googleapiclient.discovery import build
from google.oauth2 import service_account

# ① Google Drive API の設定
SCOPES = ["https://www.googleapis.com/auth/drive"]
SERVICE_ACCOUNT_FILE = "service_account.json"

creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
drive_service = build("drive", "v3", credentials=creds)

# ② 送信先の LINE API 設定
LINE_PUSH_URL = "https://api.line.me/v2/bot/message/push"
LINE_CHANNEL_ACCESS_TOKEN = ""

USER_ID = ""


# ③ Google Drive の「temp」フォルダID
FOLDER_ID = ""

def get_drive_images():
    """
    Google Drive の temp フォルダ内の画像リストを取得し、画像の `id` をリストで返す
    """
    query = f"'{FOLDER_ID}' in parents and mimeType contains 'image/' and trashed=false"
    results = drive_service.files().list(q=query, fields="files(id, name)").execute()
    files = results.get("files", [])

    if not files:
        print("No images found in the Drive folder.")
        return None

    return [file["id"] for file in files]

def get_drive_image_url(file_id):
    """
    Google Drive のファイルIDを公開URLに変換する
    """
    try:
        # ファイルのアクセス権限を「公開」に設定
        drive_service.permissions().create(
            fileId=file_id,
            body={"role": "reader", "type": "anyone"},
        ).execute()

        # 画像のダウンロードURLを取得
        return f"https://drive.google.com/thumbnail?id={file_id}&sz=w1000"
    except Exception as e:
        print(f"Error making image public: {e}")
        return None

def send_image(image_url):
    """
    LINE API を使い、取得した画像URLを LINE ユーザーに送信する
    """
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {LINE_CHANNEL_ACCESS_TOKEN}"
    }

    payload = {
        "to": USER_ID,
        "messages": [
            {
                "type": "image",
                "originalContentUrl": image_url,
                "previewImageUrl": image_url
            }
        ]
    }

    response = requests.post(LINE_PUSH_URL, headers=headers, json=payload)
    print(response.status_code, response.text)

def main():
    """
    画像リストを取得し、ランダムな画像を選んでLINEに送信
    """
    image_ids = get_drive_images()
    if not image_ids:
        print("No images found, skipping LINE push.")
        return

    # 画像をランダムに選択
    random_image_id = random.choice(image_ids)
    image_url = get_drive_image_url(random_image_id)

    if image_url:
        # LINE に画像を送信
        send_image(image_url)
    else:
        print("Failed to get a valid image URL.")

# スケジュール実行(30分ごと)
if __name__ == "__main__":
    while True:
        main()
        time.sleep(1800)  # 30分ごとに実行

をモジュールにして文字送信以外に画像も送信可能にする

 vim line_image_sender.py

内容は

import requests
import random
from googleapiclient.discovery import build
from google.oauth2 import service_account

# 設定
SCOPES = ["https://www.googleapis.com/auth/drive"]
SERVICE_ACCOUNT_FILE = "service_account.json"

LINE_PUSH_URL = "https://api.line.me/v2/bot/message/push"
LINE_CHANNEL_ACCESS_TOKEN = "YOUR_LINE_CHANNEL_ACCESS_TOKEN"
USER_ID = "YOUR_LINE_USER_ID"
FOLDER_ID = "YOUR_GOOGLE_DRIVE_FOLDER_ID"

# 認証とサービスの初期化
creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
drive_service = build("drive", "v3", credentials=creds)

def get_drive_images(folder_id=FOLDER_ID):
    query = f"'{folder_id}' in parents and mimeType contains 'image/' and trashed=false"
    results = drive_service.files().list(q=query, fields="files(id, name)").execute()
    files = results.get("files", [])
    return [file["id"] for file in files] if files else []

def get_drive_image_url(file_id):
    try:
        drive_service.permissions().create(
            fileId=file_id,
            body={"role": "reader", "type": "anyone"},
        ).execute()
        return f"https://drive.google.com/thumbnail?id={file_id}&sz=w1000"
    except Exception as e:
        print(f"Error making image public: {e}")
        return None

def send_image(image_url, user_id=USER_ID, token=LINE_CHANNEL_ACCESS_TOKEN):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}"
    }
    payload = {
        "to": user_id,
        "messages": [
            {
                "type": "image",
                "originalContentUrl": image_url,
                "previewImageUrl": image_url
            }
        ]
    }
    response = requests.post(LINE_PUSH_URL, headers=headers, json=payload)
    print(response.status_code, response.text)

def send_random_image():
    image_ids = get_drive_images()
    if not image_ids:
        print("No images found.")
        return
    random_image_id = random.choice(image_ids)
    image_url = get_drive_image_url(random_image_id)
    if image_url:
        send_image(image_url)
    else:
        print("Failed to get valid image URL.")

# スクリプトとして直接実行された場合の処理(30分おきに実行)
if __name__ == "__main__":
    import time
    while True:
        send_random_image()
        time.sleep(1800)

これを config.jsonから読み込むようにする

cp ../gas_bot/config.json .

でコピー

{
  "token": "",
  "ollama_model": "elyza:jp8b",
  "line_bot_channel_access_token": "",
  "channel_secret": "",
  "line_bot_user_id": "",
  "google_drive_folder_id": ""
}

というように
Google drive のIDを追加する

import requests
import random
import json
from googleapiclient.discovery import build
from google.oauth2 import service_account

# 設定ファイルの読み込み
with open("config.json", "r") as f:
    config = json.load(f)

LINE_PUSH_URL = "https://api.line.me/v2/bot/message/push"
LINE_CHANNEL_ACCESS_TOKEN = config["line_bot_channel_access_token"]
USER_ID = config["line_bot_user_id"]
FOLDER_ID = config["google_drive_folder_id"]

# Google Drive API の設定
SCOPES = ["https://www.googleapis.com/auth/drive"]
SERVICE_ACCOUNT_FILE = "service_account.json"

creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
drive_service = build("drive", "v3", credentials=creds)

def get_drive_images(folder_id=FOLDER_ID):
    query = f"'{folder_id}' in parents and mimeType contains 'image/' and trashed=false"
    results = drive_service.files().list(q=query, fields="files(id, name)").execute()
    files = results.get("files", [])
    return [file["id"] for file in files] if files else []

def get_drive_image_url(file_id):
    try:
        drive_service.permissions().create(
            fileId=file_id,
            body={"role": "reader", "type": "anyone"},
        ).execute()
        return f"https://drive.google.com/thumbnail?id={file_id}&sz=w1000"
    except Exception as e:
        print(f"Error making image public: {e}")
        return None

def send_image(image_url, user_id=USER_ID, token=LINE_CHANNEL_ACCESS_TOKEN):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}"
    }
    payload = {
        "to": user_id,
        "messages": [
            {
                "type": "image",
                "originalContentUrl": image_url,
                "previewImageUrl": image_url
            }
        ]
    }
    response = requests.post(LINE_PUSH_URL, headers=headers, json=payload)
    print(response.status_code, response.text)

def send_random_image():
    image_ids = get_drive_images()
    if not image_ids:
        print("No images found.")
        return
    random_image_id = random.choice(image_ids)
    image_url = get_drive_image_url(random_image_id)
    if image_url:
        send_image(image_url)
    else:
        print("Failed to get valid image URL.")

# スクリプトとして実行されたとき、30分おきに画像送信
if __name__ == "__main__":
    import time
    while True:
        send_random_image()
        time.sleep(1800)

というようにコード変更
これで設定ファイルから読み込むようになる

あとは linebot の画像の管理

これだとどんどん画像が増えていく

LINEbot 特定のメッセージの取得

LINEbot 特定のメッセージの取得

from google.cloud import firestore
import datetime

# Firestore クライアントの初期化
db = firestore.Client.from_service_account_json("serviceAccountKey.json")

def get_user_messages(user_id):
    # Firestore のコレクション名が "messages" であると仮定
    messages_ref = db.collection("messages")
    
    # user_id に一致するドキュメントをクエリで取得
    query = messages_ref.where("user_id", "==", user_id).where("timestamp", ">=", datetime.datetime.now() - datetime.timedelta(days=1))
    
    # メッセージを取得してリストに格納
    user_messages = []
    for doc in query.stream():
        user_messages.append(doc.to_dict())

    return user_messages

# 特定のユーザーIDを指定してメッセージを取得
user_id = ""
messages = get_user_messages(user_id)

for message in messages:
    print(f"メッセージ: {message['text']}, タイムスタンプ: {message['timestamp']}")

を実行したら

/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/cloud/firestore_v1/base_collection.py:304: UserWarning: Detected filter using positional arguments. Prefer using the 'filter' keyword argument instead.
  return query.where(field_path, op_string, value)
/Users/snowpool/aw10s/linebot/get_message.py:12: UserWarning: Detected filter using positional arguments. Prefer using the 'filter' keyword argument instead.
  query = messages_ref.where("user_id", "==", user_id).where("timestamp", ">=", datetime.datetime.now() - datetime.timedelta(days=1))
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1731082506.541246 3718460 config.cc:230] gRPC experiments enabled: call_status_override_on_cancellation, event_engine_dns, event_engine_listener, http2_stats_fix, monitoring_experiment, pick_first_new, trace_record_callops, work_serializer_clears_time_cache
Traceback (most recent call last):
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/api_core/grpc_helpers.py", line 162, in error_remapped_callable
    return _StreamingResponseIterator(
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/api_core/grpc_helpers.py", line 88, in __init__
    self._stored_first_result = next(self._wrapped)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/grpc/_channel.py", line 543, in __next__
    return self._next()
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/grpc/_channel.py", line 969, in _next
    raise self
grpc._channel._MultiThreadedRendezvous: <_MultiThreadedRendezvous of RPC that terminated with:
	status = StatusCode.FAILED_PRECONDITION
	details = "The query requires an index. You can create it here: https://console.firebase.google.com/v1/r/project/voicelinebot/firestore/indexes?create_composite=

	debug_error_string = "UNKNOWN:Error received from peer ipv4:142.251.42.170:443 {grpc_message:"The query requires an index. You can create it here: https://console.firebase.google.com/v1/r/project/voicelinebot/firestore/indexes?create_composite=, grpc_status:9, created_time:"2024-11-09T01:15:06.942376+09:00"}"
>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/get_message.py", line 23, in <module>
    messages = get_user_messages(user_id)
  File "/Users/snowpool/aw10s/linebot/get_message.py", line 16, in get_user_messages
    for doc in query.stream():
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/cloud/firestore_v1/stream_generator.py", line 58, in __next__
    return self._generator.__next__()
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/cloud/firestore_v1/query.py", line 400, in _make_stream
    response_iterator, expected_prefix = self._get_stream_iterator(
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/cloud/firestore_v1/query.py", line 250, in _get_stream_iterator
    response_iterator = self._client._firestore_api.run_query(
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/cloud/firestore_v1/services/firestore/client.py", line 1558, in run_query
    response = rpc(
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/api_core/gapic_v1/method.py", line 113, in __call__
    return wrapped_func(*args, **kwargs)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/api_core/retry.py", line 349, in retry_wrapped_func
    return retry_target(
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/api_core/retry.py", line 191, in retry_target
    return target()
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/api_core/timeout.py", line 120, in func_with_timeout
    return func(*args, **kwargs)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/api_core/grpc_helpers.py", line 166, in error_remapped_callable
    raise exceptions.from_grpc_error(exc) from exc
google.api_core.exceptions.FailedPrecondition: 400 The query requires an index. You can create it here: 

となった

このエラーログをGPTへ貼り付け
解説と対処を日本語で表示

結果は

このエラーは、Firestoreで複合インデックスが必要なクエリを実行しようとしたために発生しています。
where(“user_id”, “==”, user_id).where(“timestamp”, “>=”, datetime.datetime.now() – datetime.timedelta(days=1))という複数条件のクエリを実行した際、
Firestoreは自動で複合インデックスを作成しません。そのため、手動でインデックスを作成する必要があります。

とのこと

Firestoreインデックス作成ページにアクセス

インデックス作成画面で、user_idとtimestampフィールドを基にしたインデックスを作成する

インデックスを追加をクリック

コレクションIDには「messages」と入力
(クエリで指定したコレクション名と一致させる必要があるため)

フィールドのパスに
user_id を入力し
Ascending のままでOK
意味は照準

同様に
time_stampも設定する

クエリのスコープで
コレクション
特定のコレクション パス内のクエリの場合

コレクション グループ
同じコレクション ID を持つすべてのコレクションを対象とするクエリ用

とあるのでGPTで調べる

「コレクション」スコープと「コレクション グループ」スコープのどちらが適しているかは、
アプリケーションの構造や必要なデータ取得の範囲によって異なります。

### 1. **コレクション スコープ**
– **対象**:特定のコレクション内のドキュメントを対象とします。
– **メリット**:クエリ範囲が狭いため、効率的で検索速度が速くなります。
– **適用例**:ユーザーごとに分けた特定のコレクションからメッセージを取得する場合などに適しています。
– **おすすめの利用場面**:
特定のコレクションのみからデータを取得する場合や、データ構造が単一階層で構成されている場合。

### 2. **コレクション グループ スコープ**
– **対象**:同じコレクションIDを持つすべてのサブコレクションに対してクエリを行います。
– **メリット**:複数の階層やドキュメントにまたがって一括検索ができるため、
   データの階層が深くても一度のクエリで対象をカバーできます。
– **適用例**:例えば、複数のユーザーごとに階層を作ってメッセージサブコレクションがある場合、
   すべてのユーザーの「messages」サブコレクションからまとめてメッセージを検索する場合に便利です。
– **おすすめの利用場面**:
データが階層構造で保存されている場合や、複数のコレクション階層にまたがって同じデータ構造を持つ場合。

### 選択のポイント
– **単一のコレクション内で特定ユーザーや特定の条件のデータを取得したい場合**は「コレクション」スコープが最適です。
– **すべてのユーザーのデータに対して同時に検索をかけたい場合**や、
  データが階層化されている場合は「コレクション グループ」スコープが便利です。

今回は特定のユーザーのメッセージにアクセスするので、**コレクション スコープ**で十分だと思われます。

とのこと

なので
コレクションを選択

これでインデックスを作成する

再度実行すると

The above exception was the direct cause of the following exception:

WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1731083489.912237 3734254 config.cc:230] gRPC experiments enabled: call_status_override_on_cancellation, event_engine_dns, event_engine_listener, http2_stats_fix, monitoring_experiment, pick_first_new, trace_record_callops, work_serializer_clears_time_cache

Firestoreの警告メッセージで指摘されているように、
whereメソッドの代わりに filter キーワードを使用して条件を指定すると警告が解消されます
とのこと

query = messages_ref \
    .where("user_id", "==", user_id) \
    .where("timestamp", ">=", datetime.datetime.now() - datetime.timedelta(days=1))
の部分を
query = messages_ref.filter("user_id", "==", user_id).filter("timestamp", ">=", datetime.datetime.now() - datetime.timedelta(days=1))

に変更

しかし

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/get_message.py", line 25, in <module>
    messages = get_user_messages(user_id)
  File "/Users/snowpool/aw10s/linebot/get_message.py", line 13, in get_user_messages
    query = messages_ref.filter("user_id", "==", user_id).filter("timestamp", ">=", datetime.datetime.now() - datetime.timedelta(days=1))
AttributeError: 'CollectionReference' object has no attribute 'filter'

となる

AttributeError: ‘CollectionReference’ object has no attribute ‘filter’ というエラーは、
FirestoreのPython SDKには filter メソッドが存在しないために発生

where メソッドを使用して、複数の条件をチェーンで繋げる

しかし結果は変わらない

一度
main2.pyを実行し

音声メッセージを linebot へ送信

本来ならこれを取得できるはずだがDBに保存されていない

このため保存するコードを追加することにする

npm install firebase-admin

ってなっているけど
そもそもインストールがあってるか怪しいので調べる

npm install -g firebase-tools   

でインストールしてるので多分OK

npm install firebase-admin

を実行したら

added 158 packages in 20s 13 packages are looking for funding run npm fund for details npm notice npm notice New minor version of npm available! 10.7.0 -> 10.9.0 npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.9.0 npm notice To update run: npm install -g npm@10.9.0 npm notice

となったのでOK

次にindex.js を書き換える

const functions = require("firebase-functions");
const express = require("express");
const line = require("@line/bot-sdk");

const config = {
  channelAccessToken: functions.config().line.channel_access_token,
  channelSecret: functions.config().line.channel_secret,
};

const app = express();

app.post("/webhook", line.middleware(config), (req, res) => {
  Promise.all(req.body.events.map(handleEvent))
    .then((result) => res.json(result))
    .catch((err) => {
      console.error(err);
      res.status(500).end();
    });
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== "message" || event.message.type !== "text") {
    return Promise.resolve(null);
  }

  return client.replyMessage(event.replyToken, {
    type: "text",
    text: event.message.text,
  });
}

exports.webhook = functions.https.onRequest(app);

const functions = require("firebase-functions");
const express = require("express");
const line = require("@line/bot-sdk");
const admin = require("firebase-admin");

// Firebase Admin SDKの初期化
admin.initializeApp();
const db = admin.firestore();

const config = {
  channelAccessToken: functions.config().line.channel_access_token,
  channelSecret: functions.config().line.channel_secret,
};

const app = express();
const client = new line.Client(config);

// 特定のユーザーID
const targetUserId = "";  // 実際のLINEユーザーIDに置き換えてください

// Firestoreにメッセージを保存する関数
async function saveMessageToFirestore(userId, text) {
  const currentTimestamp = admin.firestore.FieldValue.serverTimestamp();

  // Firestoreにメッセージを保存
  await db.collection("messages").add({
    user_id: userId,        // LINEのユーザーID
    text: text,              // メッセージ本文
    timestamp: currentTimestamp,
    read: false,             // 既読状態
  });
}

// LINEメッセージのイベント処理
async function handleEvent(event) {
  if (event.type !== "message" || event.message.type !== "text") {
    return Promise.resolve(null);
  }

  const userId = event.source.userId;
  const messageText = event.message.text;

  // 特定のユーザーからのメッセージのみFirestoreに保存
  if (userId === targetUserId) {
    await saveMessageToFirestore(userId, messageText);
    console.log("メッセージがFirestoreに保存されました:", messageText);
  }

  // メッセージ返信
  return client.replyMessage(event.replyToken, {
    type: "text",
    text: messageText,
  });
}

app.post("/webhook", line.middleware(config), (req, res) => {
  Promise.all(req.body.events.map(handleEvent))
    .then((result) => res.json(result))
    .catch((err) => {
      console.error(err);
      res.status(500).end();
    });
});

// Firebase Functionsのエントリーポイント
exports.webhook = functions.https.onRequest(app);

へ変更

これで

firebase deploy --only functions --gen 1

とすると

error: unknown option '--gen'

   ╭────────────────────────────────────────────────────────────────────╮
   │                                                                    │
   │                 Update available 13.18.0 → 13.24.2                 │
   │           To update to the latest version using npm, run           │
   │                   npm install -g firebase-tools                    │
   │   For other CLI management options, visit the CLI documentation    │
   │         (https://firebase.google.com/docs/cli#update-cli)          │
   │                                                                    │
   │                                                                    │
   │                                                                    │
   ╰────────────────────────────────────────────────────────────────────╯

となる

npm install --save firebase-functions@latest

を実行

added 70 packages, and audited 229 packages in 3s

25 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

再度デプロイするので

firebase deploy --only functions     

を実行

✔  Deploy complete!

実行はできたけど

python main2.py                

を実行し

[2024-11-09 02:38:39.425] [ctranslate2] [thread 3780473] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
stand by ready OK
recording...
finished
アロエベラはれ
stand by ready OK
recording...
finished
10秒間音声が入力されなかったため、ループを終了します。

入力された音声テキスト一覧:
アロエベラはれ

この結果を取得しようとしたけど
DBに格納されていない

index.jsのコード変更だけでは保存機能が動作しない?
ということでGPTで調べる

GPTによれば

はい、`index.js`のコード変更だけでは、Firestoreに保存機能が動作しない可能性があります。
`index.js`では主にLINEbotからのメッセージをFirestoreに保存する役割を果たしますが、
**`main2.py`の実行中に音声から認識されたテキストをFirestoreに保存するには、
`main2.py`内にFirestoreへの保存処理を追加する必要があります**。

以下が、`index.js`と`main2.py`の役割の違いです:

1. **`index.js`の役割**
– `index.js`は、LINEbotのWebhookとして機能し、LINEのユーザーからメッセージを受信した際に、そのメッセージをFirestoreに保存します。
– したがって、`index.js`はLINEからの入力メッセージに対して保存処理を行うものです。

2. **`main2.py`の役割**
– `main2.py`は、音声認識を行い、認識されたテキストをLINE NotifyやLINE Botに送信する役割を持っています。
– Firestoreに認識されたテキストを保存したい場合、`main2.py`内にFirestoreへの保存処理(`save_to_firestore`関数)を追加する必要があります。

### `main2.py`にFirestore保存機能を追加する理由

– `main2.py`が音声認識で生成したテキストは、LINEのメッセージとは異なるソースからのデータです。そのため、このテキストデータをFirestoreに保存するためには、`main2.py`内でFirestore保存処理を直接行う必要があります。

– `index.js`の変更は、LINEメッセージのみをFirestoreに保存する機能を提供するものなので、音声から生成されたテキストには影響を与えません。

したがって、**音声認識されたテキストをFirestoreに保存するには、`main2.py`内にFirestore保存機能を追加することが必須**です。

ということ
なのでwebhookの設定だけじゃだめ

次にmain2.pyを改造してmain3.pyとして
これに firestore保存機能をつける