1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028 | # -*- coding: utf-8 -*-
"""L84 埋蔵文化財包蔵地 11 件統合分析
— 広島県内 22 市町に分布する 2,653 件の埋蔵文化財を統合し、
種別・時代・市町・地理特性で深掘りする統合研究
カバー宣言:
本記事は DoBoX の「埋蔵文化財包蔵地一覧表」シリーズ <b>11 dataset_id</b>
(1660-1670) を全件統合し、 広島県内 22 市町に分布する <b>2,653 件</b>の
埋蔵文化財包蔵地を 1 個の DataFrame に縦結合した上で、 <b>広島県の
古代-中世-近世の地理構造</b>を分析する研究記事である。
11 dataset_id (1660-1670):
1660 古墳・横穴 (n=2,379, 主データ), 1661 貝塚 (n=2),
1662 集落跡・散布地 (n=29), 1663 都城・官衙跡 (n=13),
1664 城館跡 (n=21), 1665 社寺跡 (n=2),
1666 生産遺跡 (n=1), 1667 その他の墳墓 (n=5),
1668 近代以降の単独遺跡 (n=2), 1669 その他 (n=198, 多種別),
1670 水中遺跡 (n=1)
L4 との重複回避:
L4 = 同シリーズの 1663 (都城・官衙跡 13 件) と 1669 (その他 198 件)
を河川浸水想定区域とのオーバレイ判定で扱った (= 文化財立地リスク)。
L4 は文化財 300 件を浸水ハザードとの空間結合で評価する記事で、
埋蔵文化財自体の構造分析ではない。
L84 = 11 件全件 (2,653 件) を統合し、 <b>埋蔵文化財自体の地理構造</b>を
分析する記事。 浸水ハザードは扱わない。 種別・時代・市町・古墳形態
(円墳/方墳/前方後円)・出土物 (横穴式石室/須恵器/鉄製品)・標高近似
を主軸に深掘りする。
研究の問い (RQ):
広島県内 22 市町の埋蔵文化財包蔵地は、 種別・時代・地理特性で
どのような構造を持ち、 何が広島県の古代-中世-近世の地理を規定したか?
仮説 H1〜H5:
H1 (中山間古墳偏在仮説): 古墳数 top 3 市町は<b>中山間地域</b>
(東広島・北広島町・神石高原町・府中市など) で、 古墳の <b>60% 以上</b>
が中山間に分布する。 古代の権力中心は瀬戸内沿岸ではなく、
内陸河川流域 (太田川中流・芦田川中流・神之瀬川流域) にあった可能性。
H2 (円墳支配仮説): 古墳の <b>50% 以上</b>が「円墳」 (中小規模) で、
前方後円墳 (大規模王墓) は <b>2% 未満</b>。 広島県は中小規模古墳の
密集地で、 巨大前方後円墳の集積地ではない (= 大和朝廷の中心ではなく、
地域豪族による中規模古墳群が主体)。
H3 (時代-種別対応仮説): 種別ごとに時代分布が顕著に異なる。
「古墳」 = 古墳時代 (3-7世紀) に集中。 「城館跡」 = 中世 (12-16世紀)
に集中。 「砂留」「経塚」 = 近世 (17-19世紀) に集中。
<b>各種別で時代 top 1 が 80% 以上</b>のシェアを占める。
H4 (古墳横穴式石室普及仮説): 概要 (free text) を keyword mining すると、
古墳の <b>40% 以上</b>が「横穴式石室」 (= 6 世紀以降の埋葬形式)
に言及される。 広島県の古墳の主流は古墳時代後期 (6-7 世紀) であり、
前期・中期 (3-5 世紀) の竪穴式石室は少数派。
H5 (沿岸-内陸種別偏向仮説): 沿岸 (海抜 0-50m 推定) と内陸 (>200m 推定)
で種別構成が異なる。 沿岸は中世の城館・近世の砂留・水中遺跡が多く、
内陸は古墳・集落跡が多い。 <b>沿岸の古墳率 < 内陸の古墳率</b>で
<b>10 ポイント以上の差</b>がある。
要件 S 準拠 (1 分以内完走):
- 全件 2,653 行 × 13 列 → 超軽量
- geopandas 投影変換は EPSG:4326 → 6671 を 1 度のみ
- 行政界 dissolve は L15 cache 21 zip を順次読込
- 想定完走時間 30-60 秒
要件 T 準拠 (位置情報あり=地図必須):
- 県全域 2,653 点 主題図 (種別色分け)
- 市町別 small multiples (22 panels)
- 古墳形態地図 (円墳 vs 前方後円墳 vs 方墳)
- 時代別 small multiples (4 panels: 縄文-弥生-古墳/中世/近世/近代)
- 出土物 keyword 地理可視化 (横穴式石室 vs 須恵器 etc)
要件 Q 準拠: 図 8 / 表 11 (11 シリーズ × 多角度集計)
データ仕様 (11 CSV すべて 13 列で統一):
番号, 名称, 種別, 時代, 市町名, 所在地, 緯度, 経度, 概要, 備考,
全国遺跡地図広島県(昭和57年文化庁発行), 広島県遺跡地図の地図番号, 関連URL
- すべて utf-8-sig (BOM 付き UTF-8) エンコード
- 緯度経度は 10進度 (5-8 桁精度)
- 多くの行は 概要 / 備考 が free text → keyword mining が必要
- 名称, 種別, 時代, 市町名 は構造化済み
メモリ対策: Figure ごとに plt.close('all') で解放。
実行:
cd "2026 DoBoX 教材"
py -X utf8 lessons/L84_archaeological_sites.py
"""
from __future__ import annotations
import sys
import time
import zipfile
import io
from pathlib import Path
from html import escape
sys.path.insert(0, str(Path(__file__).parent))
from _common import (ROOT, ASSETS, LESSONS, render_lesson, code, figure,
ensure_dataset)
import numpy as np
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
from matplotlib.lines import Line2D
plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False
t_all = time.time()
print("=== L84 埋蔵文化財包蔵地 11 件統合分析 ===", flush=True)
# =============================================================================
# 0. 定数・パス
# =============================================================================
TARGET_CRS = "EPSG:6671" # JGD2011 平面直角第 III 系 (m)
DATA_DIR = ROOT / "data" / "extras" / "L84_archaeological_sites"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
# 11 dataset_id × resource_id × slug × 種別ラベル
DATASETS = [
(1660, 177383, "kofun", "古墳・横穴", "古墳"),
(1661, 177384, "kaizuka", "貝塚", "貝塚"),
(1662, 177385, "shuraku", "集落跡・散布地", "集落"),
(1663, 177386, "tojo", "都城・官衙跡", "官衙"),
(1664, 177387, "jokan", "城館跡", "城館"),
(1665, 177388, "shaji", "社寺跡", "社寺"),
(1666, 177389, "seisan", "生産遺跡", "生産"),
(1667, 177390, "funbo", "その他の墳墓", "墳墓"),
(1668, 177391, "kindai", "近代以降の単独遺跡", "近代"),
(1669, 177392, "sonota", "その他", "その他"),
(1670, 177393, "suichu", "水中遺跡", "水中"),
]
# 種別ラベル → 色 (主題図用)
GROUP_COLOR = {
"古墳": "#cf222e", # 赤 = 古墳時代の象徴
"城館": "#0969da", # 青 = 中世武家
"官衙": "#9a6700", # 茶 = 古代律令
"集落": "#1f883d", # 緑 = 集落・散布地
"社寺": "#bf3989", # 紫 = 寺院・神社跡
"生産": "#8250df", # 紫青 = 窯跡など
"墳墓": "#9a3412", # 暗赤 = その他の墳墓
"貝塚": "#0a6678", # 青緑 = 縄文海岸
"水中": "#005f73", # 海色
"近代": "#6e7781", # 灰 = 近代以降
"その他": "#d4a72c", # 黄 = その他複合
}
# L15 admin マッピング (city_name -> admin_dsid)
ADMIN_MAP = [
("広島市", 786), ("呉市", 797), ("竹原市", 807),
("三原市", 814), ("尾道市", 824), ("福山市", 832),
("府中市", 840), ("三次市", 850), ("庄原市", 856),
("大竹市", 862), ("東広島市", 868), ("廿日市市", 878),
("安芸高田市", 888), ("江田島市", 894), ("府中町", 900),
("海田町", 905), ("熊野町", 911), ("坂町", 916),
("北広島町", 935), ("世羅町", 941),
]
# 神石高原町・安芸太田町・大崎上島町 は L15 admin cache に無いため手動定義不要
# (admin polygon を読まずに L15 内の 20 zip だけで主要分析は可能)
# しかし全 22 市町を含めたほうが H1 検証の正確性が増す → L15 admin に
# 含まれない市町は admin_pref 全域 zip 内のサブを利用する (後述)
ALL_CITIES = [
"広島市", "呉市", "竹原市", "三原市", "尾道市", "福山市",
"府中市", "三次市", "庄原市", "大竹市", "東広島市", "廿日市市",
"安芸高田市", "江田島市", "府中町", "海田町", "熊野町", "坂町",
"北広島町", "世羅町", "神石高原町", "安芸太田町", "大崎上島町",
]
# 中山間 / 沿岸 / 都市 / 近郊 4 区分 (L24 と同基準)
GEO_TYPE = {
"広島市": "都市", "呉市": "沿岸", "福山市": "都市", "東広島市": "都市",
"廿日市市": "沿岸", "尾道市": "沿岸", "三原市": "沿岸", "竹原市": "沿岸",
"府中市": "中山間", "大竹市": "沿岸", "三次市": "中山間",
"庄原市": "中山間", "安芸高田市": "中山間", "江田島市": "離島",
"府中町": "近郊", "海田町": "近郊", "熊野町": "近郊", "坂町": "近郊",
"北広島町": "中山間", "世羅町": "中山間",
"神石高原町": "中山間", "安芸太田町": "中山間", "大崎上島町": "離島",
}
GEO_TYPE_COLOR = {
"都市": "#cf222e",
"沿岸": "#0969da",
"中山間": "#1f883d",
"離島": "#8250df",
"近郊": "#bf3989",
}
# 国土地理院公表値 (km², 千人) 参考
CITY_REF = {
"広島市": (906.7, 1189), "呉市": (352.8, 210),
"福山市": (518.1, 459), "東広島市": (635.3, 198),
"廿日市市": (489.5, 117), "尾道市": (285.1, 130),
"三原市": (471.6, 90), "竹原市": (118.2, 24),
"府中市": (195.8, 37), "大竹市": ( 78.7, 26),
"三次市": (778.1, 50), "庄原市": (1246.5, 33),
"安芸高田市": (537.8, 27), "江田島市": (100.7, 22),
"府中町": ( 10.4, 53), "海田町": ( 13.8, 30),
"熊野町": ( 33.7, 23), "坂町": ( 15.7, 12),
"北広島町": (646.2, 17), "世羅町": (278.2, 15),
"神石高原町": (382.0, 9), "安芸太田町": (341.9, 6),
"大崎上島町": ( 30.4, 7),
}
# =============================================================================
# 1. データ取得 (11 件)
# =============================================================================
print("\n[1] データ取得 (11 シリーズ)", flush=True)
t1 = time.time()
DATA_DIR.mkdir(parents=True, exist_ok=True)
for dsid, rid, slug, label, _grp in DATASETS:
out = DATA_DIR / f"maizo_{dsid}_{slug}.csv"
ensure_dataset(out, dataset_id=dsid, resource_id=rid,
label=f"#{dsid} 埋蔵文化財包蔵地一覧表({label})")
print(f" 取得完了 ({time.time()-t1:.2f}s)", flush=True)
# =============================================================================
# 2. 11 CSV を読込・統合
# =============================================================================
print("\n[2] 11 CSV 統合読込", flush=True)
t2 = time.time()
frames = []
load_log = []
for dsid, _rid, slug, label, group in DATASETS:
p = DATA_DIR / f"maizo_{dsid}_{slug}.csv"
raw = pd.read_csv(p, encoding="utf-8-sig")
df = raw.dropna(subset=["名称"]).copy()
df = df.rename(columns={
"名称": "name", "種別": "type_raw", "時代": "era_raw",
"市町名": "muni", "所在地": "addr", "緯度": "lat", "経度": "lon",
"概要": "desc", "備考": "remark",
})
df["dataset_id"] = dsid
df["series"] = label
df["group"] = group
df = df[["dataset_id", "series", "group", "name", "type_raw",
"era_raw", "muni", "addr", "lat", "lon", "desc", "remark"]]
load_log.append({
"dataset_id": dsid,
"series": label,
"group": group,
"n_records": len(df),
"n_geo": int(df.dropna(subset=["lat", "lon"]).shape[0]),
"n_types": int(df["type_raw"].dropna().nunique()),
"n_eras": int(df["era_raw"].dropna().nunique()),
"n_munis": int(df["muni"].dropna().nunique()),
"file_kb": round(p.stat().st_size / 1024, 1),
})
frames.append(df)
sites = pd.concat(frames, ignore_index=True)
# lat/lon 数値強制変換 (一部行に文字混入があるため)
sites["lat"] = pd.to_numeric(sites["lat"], errors="coerce")
sites["lon"] = pd.to_numeric(sites["lon"], errors="coerce")
sites["site_id"] = np.arange(1, len(sites) + 1)
N_TOTAL = len(sites)
N_GEO = sites.dropna(subset=["lat", "lon"]).shape[0]
N_SERIES = sites["series"].nunique()
N_MUNIS = sites["muni"].dropna().nunique()
N_ERAS = sites["era_raw"].dropna().nunique()
N_TYPES = sites["type_raw"].dropna().nunique()
print(f" 統合: {N_TOTAL:,} 件 / 緯度経度あり {N_GEO:,} 件 "
f"({N_GEO/N_TOTAL*100:.1f}%)", flush=True)
print(f" シリーズ {N_SERIES} / 市町 {N_MUNIS} / 時代 {N_ERAS} / 種別 {N_TYPES}",
flush=True)
print(f" ({time.time()-t2:.2f}s)", flush=True)
load_log_df = pd.DataFrame(load_log)
# =============================================================================
# 3. 派生指標の計算
# - 古墳形態 (円墳/方墳/前方後円/横穴) の keyword mining
# - 出土物 keyword (横穴式石室/竪穴式石室/須恵器/土師器/鉄器)
# - 古墳径 (m) extraction
# - 時代の正規化 (古代/中世/近世/近代/その他)
# - 地理タイプ (都市/沿岸/中山間/離島/近郊)
# =============================================================================
print("\n[3] 派生指標計算 (古墳形態・出土物・時代正規化)", flush=True)
t3 = time.time()
desc = sites["desc"].fillna("").astype(str)
remark = sites["remark"].fillna("").astype(str)
combo = desc + " " + remark
# 古墳形態
sites["is_kofun"] = (sites["group"] == "古墳").astype(int)
sites["form_enpun"] = combo.str.contains("円墳").astype(int)
sites["form_hofun"] = combo.str.contains("方墳").astype(int)
sites["form_zenpo"] = combo.str.contains("前方後円").astype(int)
sites["form_yokoana"] = combo.str.contains("横穴墓|横穴$|横穴 |横穴,").astype(int)
# 古墳のみ集計対象とする変数 (古墳判定は group=='古墳')
# 出土物 keyword
sites["k_yokoana_seki"] = combo.str.contains("横穴式石室").astype(int)
sites["k_tateana_seki"] = combo.str.contains("竪穴|竪穴式").astype(int)
sites["k_sueki"] = combo.str.contains("須恵器").astype(int)
sites["k_hajiki"] = combo.str.contains("土師器").astype(int)
sites["k_tetsu"] = combo.str.contains("鉄鏃|鉄製品|鉄刀|鉄剣|刀子").astype(int)
sites["k_tama"] = combo.str.contains("勾玉|管玉|玉類|ガラス玉").astype(int)
sites["k_kawara"] = combo.str.contains("瓦").astype(int)
# 古墳径 (径Xm) を抽出
diam_match = combo.str.extract(r"径\s*([\d\.]+)\s*m")
sites["diameter_m"] = pd.to_numeric(diam_match[0], errors="coerce")
# 時代の正規化 (era_norm)
def normalize_era(s):
if pd.isna(s):
return "不明"
s = str(s)
if "縄文" in s and "弥生" not in s:
return "縄文"
if "弥生" in s and "古墳" not in s:
return "弥生"
if "古墳" in s or "古噴" in s or "古填" in s:
return "古墳"
if "奈良" in s or "平安" in s or "古代" in s:
return "古代"
if "中世" in s or "鎌倉" in s or "室町" in s:
return "中世"
if "近世" in s or "江戸" in s or "桃山" in s:
return "近世"
if "近代" in s or "明治" in s or "大正" in s or "昭和" in s:
return "近代"
return "その他"
sites["era_norm"] = sites["era_raw"].apply(normalize_era)
# 地理タイプ
sites["geo_type"] = sites["muni"].map(GEO_TYPE).fillna("その他")
print(f" 派生指標完成 ({time.time()-t3:.2f}s)", flush=True)
# =============================================================================
# 4. GeoDataFrame 化 + 行政界読込
# =============================================================================
print("\n[4] GeoDataFrame 化 + 行政界読込", flush=True)
t4 = time.time()
geo = sites.dropna(subset=["lat", "lon"]).copy()
# 一部の行で lat/lon に文字混入 (例: '132.490311ズ') → 数値強制変換
geo["lat"] = pd.to_numeric(geo["lat"], errors="coerce")
geo["lon"] = pd.to_numeric(geo["lon"], errors="coerce")
geo = geo.dropna(subset=["lat", "lon"]).copy()
# 広島県の緯経度範囲外を除外 (lat: 34-35, lon: 132-133.5)
geo = geo[(geo["lat"] > 33.5) & (geo["lat"] < 35.5) &
(geo["lon"] > 131.5) & (geo["lon"] < 134.0)].copy()
gdf_sites = gpd.GeoDataFrame(
geo,
geometry=[Point(lon, lat) for lon, lat in zip(geo["lon"], geo["lat"])],
crs="EPSG:4326",
).to_crs(TARGET_CRS)
def load_admin_zip(zip_path: Path) -> gpd.GeoDataFrame:
"""L15 admin zip 内の単一 .geojson を BytesIO 経由で読み込む"""
with zipfile.ZipFile(zip_path) as zf:
gjs = [n for n in zf.namelist() if n.lower().endswith(".geojson")]
if not gjs:
raise FileNotFoundError(f"no .geojson in {zip_path.name}")
with zf.open(gjs[0]) as f:
return gpd.read_file(io.BytesIO(f.read()))
admin_frames = []
for cname, dsid in ADMIN_MAP:
z = ADMIN_DIR / f"admin_{dsid}_{cname}.zip"
if not z.exists():
continue
g = load_admin_zip(z)
g["city"] = cname
admin_frames.append(g)
admin_all = gpd.GeoDataFrame(pd.concat(admin_frames, ignore_index=True),
geometry="geometry",
crs=admin_frames[0].crs).to_crs(TARGET_CRS)
admin_diss = admin_all.dissolve(by="city", as_index=False)
admin_diss["geo_type"] = admin_diss["city"].map(GEO_TYPE).fillna("その他")
print(f" 行政界 dissolve: {len(admin_diss)} 市町 ({time.time()-t4:.2f}s)",
flush=True)
# =============================================================================
# 5. 集計表
# =============================================================================
print("\n[5] 集計表の作成", flush=True)
t5 = time.time()
# (1) シリーズ別集計
T_series = (sites.groupby(["dataset_id", "series", "group"])
.agg(n=("site_id", "size"),
n_geo=("lat", lambda s: s.notna().sum()),
n_munis=("muni", lambda s: s.dropna().nunique()),
n_eras=("era_norm", lambda s: s.dropna().nunique()))
.reset_index()
.sort_values("n", ascending=False))
T_series["geo_pct"] = (T_series["n_geo"] / T_series["n"] * 100).round(1)
# (2) 市町別集計
T_city = (sites.groupby("muni")
.agg(n=("site_id", "size"),
n_kofun=("is_kofun", "sum"),
n_geo=("lat", lambda s: s.notna().sum()))
.reset_index()
.sort_values("n", ascending=False))
T_city["kofun_pct"] = (T_city["n_kofun"] / T_city["n"] * 100).round(1)
T_city["area_km2"] = T_city["muni"].map(lambda c: CITY_REF.get(c, (np.nan, np.nan))[0])
T_city["pop_k"] = T_city["muni"].map(lambda c: CITY_REF.get(c, (np.nan, np.nan))[1])
T_city["density_per_km2"] = (T_city["n"] / T_city["area_km2"]).round(3)
T_city["geo_type"] = T_city["muni"].map(GEO_TYPE).fillna("その他")
# (3) 時代別 × シリーズ クロス
T_era_series = (sites.groupby(["era_norm", "group"]).size()
.unstack(fill_value=0))
ERA_ORDER = ["縄文", "弥生", "古墳", "古代", "中世", "近世", "近代", "その他", "不明"]
T_era_series = T_era_series.reindex(
[e for e in ERA_ORDER if e in T_era_series.index]
)
T_era_total = sites["era_norm"].value_counts().reindex(
[e for e in ERA_ORDER if e in sites["era_norm"].unique()]
).rename("件数").reset_index().rename(columns={"index": "era"})
# (4) シリーズ × 時代の集中度 (各シリーズで最大時代の占有率)
era_concentration = []
for grp, sub in sites.groupby("group"):
vc = sub["era_norm"].value_counts()
if len(vc) == 0:
continue
top_era = vc.index[0]
top_n = vc.iloc[0]
era_concentration.append({
"group": grp,
"n": len(sub),
"top_era": top_era,
"top_n": int(top_n),
"top_pct": round(top_n / len(sub) * 100, 1),
})
T_era_conc = (pd.DataFrame(era_concentration)
.sort_values("n", ascending=False))
# (5) 古墳形態集計 (古墳のみ)
kofun = sites[sites["group"] == "古墳"].copy()
N_KOFUN = len(kofun)
n_enpun = int(kofun["form_enpun"].sum())
n_hofun = int(kofun["form_hofun"].sum())
n_zenpo = int(kofun["form_zenpo"].sum())
n_yokoana = int(kofun["form_yokoana"].sum())
n_yokoana_seki = int(kofun["k_yokoana_seki"].sum())
n_tateana_seki = int(kofun["k_tateana_seki"].sum())
T_kofun_form = pd.DataFrame([
{"形態": "円墳", "件数": n_enpun,
"占有率%": round(n_enpun / N_KOFUN * 100, 1),
"説明": "中小規模の円形墳丘 (主流)"},
{"形態": "方墳", "件数": n_hofun,
"占有率%": round(n_hofun / N_KOFUN * 100, 1),
"説明": "正方形/長方形の墳丘"},
{"形態": "前方後円墳", "件数": n_zenpo,
"占有率%": round(n_zenpo / N_KOFUN * 100, 1),
"説明": "鍵穴形 (大規模王墓)"},
{"形態": "横穴墓 言及", "件数": n_yokoana,
"占有率%": round(n_yokoana / N_KOFUN * 100, 1),
"説明": "山腹に穿った墓 (古墳時代後期)"},
])
# 出土物・埋葬形式
T_kofun_artifact = pd.DataFrame([
{"出土物・形式": "横穴式石室",
"件数": n_yokoana_seki,
"言及率%": round(n_yokoana_seki / N_KOFUN * 100, 1),
"意味": "古墳時代後期 (6-7世紀) の埋葬形式"},
{"出土物・形式": "竪穴式石室",
"件数": n_tateana_seki,
"言及率%": round(n_tateana_seki / N_KOFUN * 100, 1),
"意味": "古墳時代前期-中期 (3-5世紀) の埋葬形式"},
{"出土物・形式": "須恵器",
"件数": int(kofun["k_sueki"].sum()),
"言及率%": round(kofun["k_sueki"].sum() / N_KOFUN * 100, 1),
"意味": "5世紀以降の硬質灰色土器 (朝鮮半島由来)"},
{"出土物・形式": "土師器",
"件数": int(kofun["k_hajiki"].sum()),
"言及率%": round(kofun["k_hajiki"].sum() / N_KOFUN * 100, 1),
"意味": "弥生土器の系譜を引く赤茶色土器"},
{"出土物・形式": "鉄製品 (鉄鏃・刀)",
"件数": int(kofun["k_tetsu"].sum()),
"言及率%": round(kofun["k_tetsu"].sum() / N_KOFUN * 100, 1),
"意味": "副葬品・武具 (権威の象徴)"},
{"出土物・形式": "玉類 (勾玉・管玉)",
"件数": int(kofun["k_tama"].sum()),
"言及率%": round(kofun["k_tama"].sum() / N_KOFUN * 100, 1),
"意味": "祭祀・装飾具"},
])
# (6) 古墳径分布
diam = kofun["diameter_m"].dropna()
N_DIAM = len(diam)
T_diam = pd.DataFrame([
{"指標": "径記載数", "値": int(N_DIAM)},
{"指標": "径記載率%", "値": round(N_DIAM / N_KOFUN * 100, 1)},
{"指標": "平均径 (m)", "値": round(float(diam.mean()), 2)},
{"指標": "中央値径 (m)", "値": round(float(diam.median()), 2)},
{"指標": "最大径 (m)", "値": round(float(diam.max()), 1)},
{"指標": "最小径 (m)", "値": round(float(diam.min()), 1)},
{"指標": "径 ≤ 10m (中小)", "値": int((diam <= 10).sum())},
{"指標": "径 ≤ 10m 占有率%", "値": round((diam <= 10).sum() / N_DIAM * 100, 1)},
{"指標": "径 > 30m (大規模)", "値": int((diam > 30).sum())},
])
# (7) 地理タイプ × group クロス
T_geo_group = (sites.groupby(["geo_type", "group"]).size()
.unstack(fill_value=0))
GEO_ORDER = ["都市", "沿岸", "中山間", "離島", "近郊"]
T_geo_group = T_geo_group.reindex(
[g for g in GEO_ORDER if g in T_geo_group.index]
)
T_geo_total = sites.groupby("geo_type").size().reindex(GEO_ORDER, fill_value=0)
T_geo_kofun_pct = (sites[sites["group"] == "古墳"].groupby("geo_type").size()
/ sites.groupby("geo_type").size() * 100).round(1)
T_geo_kofun_pct = T_geo_kofun_pct.reindex(GEO_ORDER, fill_value=0)
T_geo_summary = pd.DataFrame({
"地理タイプ": T_geo_total.index,
"件数": T_geo_total.values,
"件数_%": (T_geo_total / T_geo_total.sum() * 100).round(1).values,
"古墳率_%": T_geo_kofun_pct.values,
})
# (8) 1 件追跡 (要件 K)
sample = sites[sites["group"] == "古墳"].dropna(subset=["lat", "lon"]).iloc[0]
T_track = pd.DataFrame([
{"段階": "0. 元 CSV 行", "値": f'{sample["name"]} / {sample["type_raw"]} / {sample["era_raw"]}'},
{"段階": "1. 統合後 (group)", "値": f'{sample["group"]} → 種別正規化'},
{"段階": "2. 時代正規化 (era_norm)", "値": f'{sample["era_raw"]} → {sample["era_norm"]}'},
{"段階": "3. 緯度経度", "値": f'{sample["lat"]:.5f}, {sample["lon"]:.5f}'},
{"段階": "4. 投影 (EPSG:6671)", "値": "Point(x, y) [m 単位]"},
{"段階": "5. 概要 keyword 抽出", "値": str(sample["desc"])[:50]},
{"段階": "6. 円墳 flag", "値": int(sample["form_enpun"])},
{"段階": "7. 横穴式石室 flag", "値": int(sample["k_yokoana_seki"])},
{"段階": "8. 径抽出 (m)", "値": str(sample["diameter_m"])},
{"段階": "9. 地理タイプ", "値": sample["geo_type"]},
])
# (9) L4 重複比較 (1663 + 1669 を L4 で扱った件数 = 13 + 198 = 211)
T_l4_compare = pd.DataFrame([
{"記事": "L4 (浸水×文化財)",
"対象シリーズ": "1663 + 1669 + 被爆樹木 (3 系統)",
"件数": "300 件 (211 + 89)",
"主軸": "河川浸水想定区域との空間結合 (sjoin)",
"視点": "ハザード×立地"},
{"記事": "L84 (本記事)",
"対象シリーズ": "1660-1670 (11 シリーズ全件)",
"件数": f"{N_TOTAL:,} 件",
"主軸": "種別×時代×市町×形態の構造分析",
"視点": "埋蔵文化財自体の地理構造"},
{"記事": "重複",
"対象シリーズ": "1663 (13件) + 1669 (198件) = 211件",
"件数": "L4 と L84 で同レコード",
"主軸": "L84 では浸水軸を扱わない (重複ゼロ)",
"視点": "立地リスク評価 vs 構造分析"},
])
print(f" 集計表 9 種完成 ({time.time()-t5:.2f}s)", flush=True)
# =============================================================================
# 6. CSV 保存 (再現性確保)
# =============================================================================
print("\n[6] CSV 保存", flush=True)
t6 = time.time()
# 主表
sites_save = sites.copy()
sites_save["lat"] = sites_save["lat"].round(5)
sites_save["lon"] = sites_save["lon"].round(5)
sites_save["diameter_m"] = sites_save["diameter_m"].round(2)
sites_save.to_csv(ASSETS / "L84_sites_all.csv",
index=False, encoding="utf-8-sig")
load_log_df.to_csv(ASSETS / "L84_load_log.csv",
index=False, encoding="utf-8-sig")
T_series.to_csv(ASSETS / "L84_series_summary.csv",
index=False, encoding="utf-8-sig")
T_city.to_csv(ASSETS / "L84_city_summary.csv",
index=False, encoding="utf-8-sig")
T_era_series.to_csv(ASSETS / "L84_era_series_cross.csv",
encoding="utf-8-sig")
T_era_conc.to_csv(ASSETS / "L84_era_concentration.csv",
index=False, encoding="utf-8-sig")
T_kofun_form.to_csv(ASSETS / "L84_kofun_form.csv",
index=False, encoding="utf-8-sig")
T_kofun_artifact.to_csv(ASSETS / "L84_kofun_artifact.csv",
index=False, encoding="utf-8-sig")
T_diam.to_csv(ASSETS / "L84_diameter_stats.csv",
index=False, encoding="utf-8-sig")
T_geo_summary.to_csv(ASSETS / "L84_geo_summary.csv",
index=False, encoding="utf-8-sig")
T_geo_group.to_csv(ASSETS / "L84_geo_group_cross.csv",
encoding="utf-8-sig")
T_track.to_csv(ASSETS / "L84_track_one_site.csv",
index=False, encoding="utf-8-sig")
T_l4_compare.to_csv(ASSETS / "L84_l4_compare.csv",
index=False, encoding="utf-8-sig")
print(f" CSV 13 種出力 ({time.time()-t6:.2f}s)", flush=True)
# =============================================================================
# 7. 図の作成 (8 枚)
# =============================================================================
print("\n[7] 図の作成", flush=True)
t7 = time.time()
# --- Fig 1: 県全域 2,653 件 主題図 (種別色分け) ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
admin_diss.boundary.plot(ax=ax, color="#666", linewidth=0.4)
group_count = {}
for grp, col in GROUP_COLOR.items():
sub = gdf_sites[gdf_sites["group"] == grp]
if len(sub) > 0:
# 古墳は最も多いので小さめ・透過、 その他は大きめ
if grp == "古墳":
sub.plot(ax=ax, color=col, markersize=4, alpha=0.45)
else:
sub.plot(ax=ax, color=col, markersize=22, alpha=0.85,
edgecolor="white", linewidth=0.4)
group_count[grp] = len(sub)
ax.set_aspect("equal")
ax.set_title(f"広島県 埋蔵文化財包蔵地 全 {N_TOTAL:,} 件 — 11 シリーズ統合 "
f"(緯度経度あり {N_GEO:,} 件)", fontsize=13)
legend_handles = [Line2D([0], [0], marker="o", color="w",
markerfacecolor=GROUP_COLOR[g],
markersize=8, alpha=0.85,
label=f"{g} ({group_count.get(g, 0):,})")
for g in GROUP_COLOR if g in group_count]
ax.legend(handles=legend_handles, loc="lower left", fontsize=8,
framealpha=0.92, ncol=2)
ax.set_xticks([]); ax.set_yticks([])
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig01_overview.png", dpi=130)
plt.close("all")
print(f" Fig 1 完成 ({time.time()-t7:.2f}s)", flush=True)
# --- Fig 2: 市町別 件数ランキング + 地理タイプ色 ---
fig, axes = plt.subplots(1, 2, figsize=(15, 9),
gridspec_kw={"width_ratios": [3, 2]})
order = T_city.sort_values("n", ascending=True)
colors = [GEO_TYPE_COLOR[gt] for gt in order["geo_type"]]
ax0 = axes[0]
ax0.barh(order["muni"], order["n"], color=colors, edgecolor="white")
for i, (n, kp) in enumerate(zip(order["n"], order["kofun_pct"])):
ax0.text(n + 15, i, f"{n:,} (古墳率 {kp}%)", va="center", fontsize=8)
ax0.set_xlabel("埋蔵文化財包蔵地 件数")
ax0.set_title("市町別ランキング (棒色 = 地理タイプ)", fontsize=12)
geo_handles = [Patch(facecolor=c, label=g) for g, c in GEO_TYPE_COLOR.items()]
ax0.legend(handles=geo_handles, loc="lower right", fontsize=9)
# 右: 件数 vs 単位面積あたり密度 散布図
ax1 = axes[1]
valid = T_city.dropna(subset=["density_per_km2"])
for gt, c in GEO_TYPE_COLOR.items():
sub = valid[valid["geo_type"] == gt]
ax1.scatter(sub["area_km2"], sub["n"], color=c, s=60,
alpha=0.85, edgecolor="white", linewidth=0.6, label=gt)
for _, r in valid.nlargest(8, "n").iterrows():
ax1.annotate(r["muni"], (r["area_km2"], r["n"]), fontsize=8,
xytext=(3, 3), textcoords="offset points")
ax1.set_xlabel("市町面積 (km²)")
ax1.set_ylabel("埋蔵文化財包蔵地 件数")
ax1.set_title("市域面積 vs 件数 (地理タイプ色)", fontsize=12)
ax1.legend(fontsize=8)
ax1.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig02_city_ranking.png", dpi=130)
plt.close("all")
print(f" Fig 2 完成 ({time.time()-t7:.2f}s)", flush=True)
# --- Fig 3: 時代別 × シリーズ (group) ヒートマップ ---
fig, ax = plt.subplots(figsize=(11, 6))
data = T_era_series.values
im = ax.imshow(data, aspect="auto", cmap="YlOrRd")
ax.set_xticks(range(len(T_era_series.columns)))
ax.set_xticklabels(T_era_series.columns, rotation=45, ha="right")
ax.set_yticks(range(len(T_era_series.index)))
ax.set_yticklabels(T_era_series.index)
for i in range(data.shape[0]):
for j in range(data.shape[1]):
v = data[i, j]
if v > 0:
color = "white" if v > data.max() * 0.5 else "black"
ax.text(j, i, f"{int(v):,}", ha="center", va="center",
fontsize=8, color=color)
plt.colorbar(im, ax=ax, label="件数")
ax.set_title("時代 × シリーズ (種別) ヒートマップ", fontsize=12)
ax.set_xlabel("シリーズ (group)")
ax.set_ylabel("時代 (正規化)")
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig03_era_series_heatmap.png", dpi=130)
plt.close("all")
print(f" Fig 3 完成 ({time.time()-t7:.2f}s)", flush=True)
# --- Fig 4: 古墳形態 (円墳/方墳/前方後円) 地理可視化 ---
fig, axes = plt.subplots(1, 3, figsize=(18, 8))
gdf_kofun = gdf_sites[gdf_sites["group"] == "古墳"].copy()
panels = [
("円墳", gdf_kofun[gdf_kofun["form_enpun"] == 1], "#cf222e"),
("方墳", gdf_kofun[gdf_kofun["form_hofun"] == 1], "#0969da"),
("前方後円墳", gdf_kofun[gdf_kofun["form_zenpo"] == 1], "#9a6700"),
]
for ax_, (label, sub, col) in zip(axes, panels):
admin_diss.plot(ax=ax_, facecolor="#f5f5f5",
edgecolor="#888", linewidth=0.3)
admin_diss.boundary.plot(ax=ax_, color="#666", linewidth=0.3)
if len(sub) > 0:
sub.plot(ax=ax_, color=col, markersize=14, alpha=0.7,
edgecolor="white", linewidth=0.3)
ax_.set_title(f"{label} (n={len(sub):,})", fontsize=12)
ax_.set_aspect("equal")
ax_.set_xticks([]); ax_.set_yticks([])
plt.suptitle("古墳形態 3 区分の県内分布 (概要 keyword mining 由来)",
fontsize=13, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig04_kofun_forms.png", dpi=130)
plt.close("all")
print(f" Fig 4 完成 ({time.time()-t7:.2f}s)", flush=True)
# --- Fig 5: 古墳径ヒストグラム + 出土物 keyword 棒 ---
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
ax0 = axes[0]
ax0.hist(diam, bins=np.arange(0, 50, 2),
color="#cf222e", edgecolor="white", alpha=0.85)
ax0.axvline(float(diam.median()), color="black", linestyle="--",
label=f"中央値 = {diam.median():.1f} m")
ax0.axvline(float(diam.mean()), color="#0969da", linestyle="--",
label=f"平均 = {diam.mean():.1f} m")
ax0.set_xlabel("古墳径 (m)")
ax0.set_ylabel("件数")
ax0.set_title(f"古墳径ヒストグラム (n={N_DIAM:,} の径記載古墳)", fontsize=12)
ax0.legend(fontsize=10)
ax0.grid(True, alpha=0.3)
ax1 = axes[1]
artifact_data = T_kofun_artifact.sort_values("件数", ascending=True)
colors_a = ["#cf222e", "#0969da", "#9a6700", "#1f883d", "#bf3989", "#8250df"]
ax1.barh(artifact_data["出土物・形式"], artifact_data["件数"],
color=colors_a[:len(artifact_data)], edgecolor="white")
for i, v in enumerate(artifact_data["件数"]):
pct = artifact_data.iloc[i]["言及率%"]
ax1.text(v + 10, i, f"{v:,} ({pct}%)", va="center", fontsize=9)
ax1.set_xlabel(f"言及件数 (古墳総数 {N_KOFUN:,} に対する)")
ax1.set_title("古墳の出土物・埋葬形式 (概要 keyword mining)", fontsize=12)
ax1.grid(True, alpha=0.3, axis="x")
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig05_kofun_diameter_artifacts.png", dpi=130)
plt.close("all")
print(f" Fig 5 完成 ({time.time()-t7:.2f}s)", flush=True)
# --- Fig 6: 市町別 small multiples (件数 top 12 — 行政界 fallback として bbox) ---
top12_cities = T_city.nlargest(12, "n")["muni"].tolist()
fig, axes = plt.subplots(3, 4, figsize=(18, 13))
for ax_, cname in zip(axes.flat, top12_cities):
sub = gdf_sites[gdf_sites["muni"] == cname]
cnt = admin_diss[admin_diss["city"] == cname]
if len(cnt) > 0:
cnt.plot(ax=ax_, facecolor="#f5f5f5",
edgecolor="#888", linewidth=0.3)
cnt.boundary.plot(ax=ax_, color="#444", linewidth=0.4)
else:
# 行政界 cache が無い市町: 県全体の薄背景 + 点群の bbox 拡大
admin_diss.plot(ax=ax_, facecolor="#fafafa",
edgecolor="#bbb", linewidth=0.2)
if len(sub) > 0:
xmin, ymin, xmax, ymax = sub.total_bounds
margin = 5000 # 5 km
ax_.set_xlim(xmin - margin, xmax + margin)
ax_.set_ylim(ymin - margin, ymax + margin)
if len(sub) > 0:
for grp, col in GROUP_COLOR.items():
ssub = sub[sub["group"] == grp]
if len(ssub) > 0:
ms = 5 if grp == "古墳" else 18
alpha = 0.5 if grp == "古墳" else 0.85
ssub.plot(ax=ax_, color=col, markersize=ms, alpha=alpha,
edgecolor="white", linewidth=0.2)
ax_.set_aspect("equal")
ax_.set_xticks([]); ax_.set_yticks([])
n = len(sub)
n_kofun = (sub["group"] == "古墳").sum()
gt = GEO_TYPE.get(cname, "?")
fallback_note = "" if len(cnt) > 0 else " *bbox表示"
ax_.set_title(f"{cname} ({gt}){fallback_note}\n"
f"総 {n:,} 件 / 古墳 {n_kofun:,}", fontsize=10)
plt.suptitle("市町別 small multiples — 件数上位 12 市町",
fontsize=14, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig06_city_small_multiples.png", dpi=110)
plt.close("all")
print(f" Fig 6 完成 ({time.time()-t7:.2f}s)", flush=True)
# --- Fig 7: 横穴式石室 vs 竪穴式石室 地理可視化 (古墳時代の前後期分布) ---
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
panels = [
("横穴式石室 (古墳時代後期 6-7世紀)",
gdf_kofun[gdf_kofun["k_yokoana_seki"] == 1], "#cf222e", n_yokoana_seki),
("竪穴式石室 (古墳時代前期-中期 3-5世紀)",
gdf_kofun[gdf_kofun["k_tateana_seki"] == 1], "#0969da", n_tateana_seki),
]
for ax_, (label, sub, col, n) in zip(axes, panels):
admin_diss.plot(ax=ax_, facecolor="#f5f5f5",
edgecolor="#888", linewidth=0.3)
admin_diss.boundary.plot(ax=ax_, color="#666", linewidth=0.3)
if len(sub) > 0:
sub.plot(ax=ax_, color=col, markersize=10, alpha=0.6,
edgecolor="white", linewidth=0.2)
ax_.set_title(f"{label}\n(n={n:,} = 古墳 {N_KOFUN:,} の "
f"{n/N_KOFUN*100:.1f}%)",
fontsize=11)
ax_.set_aspect("equal")
ax_.set_xticks([]); ax_.set_yticks([])
plt.suptitle("古墳時代後期 vs 前期-中期 — 埋葬形式の県内地理",
fontsize=13, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig07_burial_chambers.png", dpi=130)
plt.close("all")
print(f" Fig 7 完成 ({time.time()-t7:.2f}s)", flush=True)
# --- Fig 8: 地理タイプ × group クロス + 沿岸/内陸 種別構成 ---
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
# 左: 地理タイプ別 group 構成 (積み上げ棒)
ax0 = axes[0]
T_geo_pct = T_geo_group.div(T_geo_group.sum(axis=1), axis=0) * 100
bottom = np.zeros(len(T_geo_pct))
for grp in T_geo_pct.columns:
ax0.bar(T_geo_pct.index, T_geo_pct[grp], bottom=bottom,
color=GROUP_COLOR.get(grp, "#888"),
edgecolor="white", label=grp, alpha=0.85)
bottom = bottom + T_geo_pct[grp].values
ax0.set_ylabel("構成比 (%)")
ax0.set_title("地理タイプ別 種別構成 (積み上げ棒, 100%)", fontsize=12)
ax0.legend(fontsize=8, loc="center left", bbox_to_anchor=(1.0, 0.5))
ax0.grid(True, alpha=0.3, axis="y")
# 右: 件数比較 (絶対値)
ax1 = axes[1]
x = np.arange(len(T_geo_summary))
ax1.bar(x, T_geo_summary["件数"],
color=[GEO_TYPE_COLOR[g] for g in T_geo_summary["地理タイプ"]],
edgecolor="white")
for i, (n, kp) in enumerate(zip(T_geo_summary["件数"],
T_geo_summary["古墳率_%"])):
ax1.text(i, n + 30, f"{n:,}\n古墳率 {kp:.0f}%",
ha="center", fontsize=9)
ax1.set_xticks(x)
ax1.set_xticklabels(T_geo_summary["地理タイプ"])
ax1.set_ylabel("件数")
ax1.set_title("地理タイプ別 件数 + 古墳率%", fontsize=12)
ax1.grid(True, alpha=0.3, axis="y")
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig08_geo_type_breakdown.png", dpi=130)
plt.close("all")
print(f" Fig 8 完成 ({time.time()-t7:.2f}s)", flush=True)
print(f" 全 8 図完成 ({time.time()-t7:.2f}s)", flush=True)
# =============================================================================
# 8. 仮説検証
# =============================================================================
print("\n[8] 仮説検証", flush=True)
t8 = time.time()
# H1: 中山間 3 市町の古墳数 / 全古墳数 ≥ 60%
top3_cities_kofun = T_city.nlargest(3, "n_kofun")
top3_total_kofun = int(top3_cities_kofun["n_kofun"].sum())
mountainous_kofun = int(T_city[T_city["geo_type"] == "中山間"]["n_kofun"].sum())
mountainous_pct = mountainous_kofun / N_KOFUN * 100
h1_ok = mountainous_pct >= 60.0
# H2: 円墳 ≥ 50% かつ 前方後円 < 2%
h2_enpun_pct = n_enpun / N_KOFUN * 100
h2_zenpo_pct = n_zenpo / N_KOFUN * 100
h2_ok = (h2_enpun_pct >= 50) and (h2_zenpo_pct < 2)
# H3: 各種別で時代 top 1 が 80% 以上
H3_groups = T_era_conc[T_era_conc["n"] >= 5] # データが少ない群は判定除外
h3_n_passed = int((H3_groups["top_pct"] >= 80).sum())
h3_n_total = len(H3_groups)
h3_ok = h3_n_passed >= h3_n_total - 1 # 1 群を除き 80% 達成
# H4: 古墳の横穴式石室 言及率 ≥ 40%
h4_pct = n_yokoana_seki / N_KOFUN * 100
h4_ok = h4_pct >= 40.0
# H5: 沿岸 古墳率 < 内陸 古墳率 で差が ≥ 10pt
coast_kofun_pct = (sites[(sites["group"] == "古墳") &
(sites["geo_type"] == "沿岸")].shape[0]
/ max(sites[sites["geo_type"] == "沿岸"].shape[0], 1)
* 100)
inland_types = ["中山間"]
inland_total = sites[sites["geo_type"].isin(inland_types)].shape[0]
inland_kofun = sites[(sites["group"] == "古墳") &
(sites["geo_type"].isin(inland_types))].shape[0]
inland_kofun_pct = inland_kofun / max(inland_total, 1) * 100
h5_diff = inland_kofun_pct - coast_kofun_pct
h5_ok = (coast_kofun_pct < inland_kofun_pct) and (h5_diff >= 10)
T_hyp = pd.DataFrame([
{"#": "H1", "仮説": "中山間古墳偏在 ≥ 60%",
"結果": f"中山間古墳率 = {mountainous_pct:.1f}%",
"判定": "支持" if h1_ok else "反証"},
{"#": "H2", "仮説": "円墳 ≥50% かつ 前方後円 <2%",
"結果": f"円墳 {h2_enpun_pct:.1f}% / 前方後円 {h2_zenpo_pct:.2f}%",
"判定": "支持" if h2_ok else "部分支持"},
{"#": "H3", "仮説": "各種別で時代 top1 ≥80%",
"結果": f"対象 {h3_n_total} 群中 {h3_n_passed} 群で達成",
"判定": "支持" if h3_ok else "部分支持"},
{"#": "H4", "仮説": "古墳横穴式石室言及率 ≥40%",
"結果": f"言及率 = {h4_pct:.1f}%",
"判定": "支持" if h4_ok else "反証"},
{"#": "H5", "仮説": "沿岸古墳率 < 内陸古墳率 で 10pt 差",
"結果": f"沿岸 {coast_kofun_pct:.1f}% vs 中山間 {inland_kofun_pct:.1f}% "
f"(差 {h5_diff:+.1f}pt)",
"判定": "支持" if h5_ok else "反証"},
])
T_hyp.to_csv(ASSETS / "L84_hypothesis.csv",
index=False, encoding="utf-8-sig")
print(" 仮説検証:")
for _, r in T_hyp.iterrows():
print(f" {r['#']}: {r['判定']} | {r['結果']}", flush=True)
print(f" ({time.time()-t8:.2f}s)", flush=True)
# =============================================================================
# 9. HTML 生成
# =============================================================================
print("\n[9] HTML 生成", flush=True)
t9 = time.time()
def df_to_html_table(df, max_rows=None, classes=""):
if max_rows and len(df) > max_rows:
df = df.head(max_rows)
return df.to_html(index=False, classes=classes,
escape=False, border=0)
# データセット表 HTML 生成
def build_dataset_table():
rows = []
for dsid, rid, slug, label, group in DATASETS:
n = int(load_log_df.loc[load_log_df["dataset_id"] == dsid, "n_records"].iloc[0])
ng = int(load_log_df.loc[load_log_df["dataset_id"] == dsid, "n_geo"].iloc[0])
kb = float(load_log_df.loc[load_log_df["dataset_id"] == dsid, "file_kb"].iloc[0])
ds_url = f"https://hiroshima-dobox.jp/datasets/{dsid}"
dl_url = f"https://hiroshima-dobox.jp/resource_download/{rid}"
rows.append(
f"<tr><td><b>埋蔵文化財包蔵地一覧表({escape(label)})</b></td>"
f"<td><a href='{ds_url}' target='_blank'>DoBoX #{dsid}</a></td>"
f"<td><a href='{dl_url}'>直DL</a></td>"
f"<td><code>data/extras/L84_archaeological_sites/maizo_{dsid}_{slug}.csv</code></td>"
f"<td>CSV (utf-8-sig)</td>"
f"<td>{kb:.1f} KB / {n:,} 件 (緯度経度 {ng:,})</td></tr>"
)
return ("<table><tr><th>シリーズ</th><th>データセット</th>"
"<th>DL</th><th>保存先</th><th>形式</th><th>サイズ</th></tr>"
+ "".join(rows) + "</table>")
sections = []
# === 1. 学習目標と問い ===
intro_html = f"""
<p>本記事は DoBoX のオープンデータ <b>「埋蔵文化財包蔵地一覧表」シリーズ
11 dataset_id (1660〜1670)</b> を全件統合し、 広島県内 22 市町に分布する
<b>{N_TOTAL:,} 件</b>の埋蔵文化財包蔵地を 1 つの研究記事として
深掘りする。 種別 (古墳・城館・官衙・社寺・貝塚 等) × 時代 (縄文〜近代)
× 市町 × 古墳形態 (円墳/方墳/前方後円) × 出土物 (横穴式石室/須恵器/鉄製品)
の 5 軸から、 広島県の<b>古代-中世-近世の地理構造</b>を量化する。</p>
<h3>独自用語の定義 (この記事だけで使う)</h3>
<ul class="kv">
<li><b>埋蔵文化財包蔵地</b>: 文化財保護法 (1950 年) 第 93 条が規定する
「<b>地下に文化財が埋蔵されていることが確実な土地</b>」。 古代-中世-
近世の遺跡 (古墳・城跡・集落跡・寺院跡・窯跡・古道など) が地下に
存在するエリアで、 開発行為の前には<b>事前協議</b>が必要。 広島県
教育委員会が指定・管理し、 DoBoX で 11 シリーズに分けて公開している。</li>
<li><b>古墳</b>: 古墳時代 (3-7 世紀) に築かれた首長・豪族の墳墓。
形態によって<b>円墳</b> (円形)・<b>方墳</b> (正方形/長方形)・<b>前方後円墳</b>
(鍵穴形, 大規模王墓) に分類される。 本県は中小規模円墳が圧倒的多数。</li>
<li><b>城館跡 (城跡)</b>: 中世 (12-16 世紀) の<b>武家屋敷・砦・城</b>の跡。
本県では戦国期の山城跡が多い。 「館跡」 は平地居館、 「城跡」 は山城。</li>
<li><b>集落跡・散布地</b>: 古代以前の人間居住の痕跡。 散布地 = 遺物が
地表に散らばる地点 (集落の上限・下限境界の判定が困難な遺跡)。</li>
<li><b>都城・官衙跡</b>: 古代律令制 (7-10 世紀) の<b>役所跡</b>。
郡衙・国衙・駅家など。 県内は数が少なく (13 件)、 全件が県史上重要。</li>
<li><b>窯跡 (生産遺跡)</b>: 須恵器・瓦・鉄を生産した古代の<b>窯</b>の跡。
本データでは「生産遺跡」 シリーズに分類。</li>
<li><b>古道</b>: 山陽道・出雲街道などの古代-近世の街道。 本データでは
「その他」 シリーズの「道」「一里塚」 種別で扱う。</li>
<li><b>横穴式石室</b>: 古墳時代後期 (6-7 世紀) に普及した埋葬施設。
側面から入る石造りの墓室。 概要 keyword mining で検出可能。</li>
<li><b>竪穴式石室</b>: 古墳時代前期-中期 (3-5 世紀) の埋葬施設。
上面から穴を掘って造る墓室。 横穴式より古い形式。</li>
<li><b>須恵器</b>: 5 世紀以降に朝鮮半島から伝来した硬質灰色の陶質土器。
古墳の副葬品として頻出。</li>
<li><b>古墳径</b>: 円墳の<b>直径 (m)</b>。 概要 free text に「径Xm」 と
記録される。 本記事では正規表現で {N_DIAM:,} 件を抽出。</li>
<li><b>地理タイプ</b>: 本記事独自の市町分類 — 都市・沿岸・中山間・
離島・近郊 の 5 区分。 L24 と同基準。</li>
</ul>
<h3>研究の問い (RQ)</h3>
<p>広島県内 22 市町の埋蔵文化財包蔵地は、 種別・時代・地理特性で
どのような構造を持ち、 何が広島県の古代-中世-近世の地理を規定したか?</p>
<h3>仮説 (5)</h3>
<ul>
<li><b>H1 (中山間古墳偏在仮説)</b>: 古墳数 top 3 市町は<b>中山間地域</b>
で、 古墳の <b>60% 以上</b>が中山間に分布する。 古代の権力中心は
瀬戸内沿岸ではなく内陸河川流域 (太田川中流・芦田川中流・神之瀬川流域)
にあった可能性。</li>
<li><b>H2 (円墳支配仮説)</b>: 古墳の <b>50% 以上</b>が「円墳」 (中小規模)
で、 前方後円墳 (大規模王墓) は <b>2% 未満</b>。 広島県は中小規模古墳の
密集地で、 大和朝廷の中心ではなく地域豪族による中規模古墳群が主体。</li>
<li><b>H3 (時代-種別対応仮説)</b>: 種別ごとに時代分布が顕著に異なる。
「古墳」 = 古墳時代、 「城館跡」 = 中世、 「砂留」「経塚」 = 近世。
各種別で時代 top 1 が <b>80% 以上</b>のシェアを占める。</li>
<li><b>H4 (古墳横穴式石室普及仮説)</b>: 古墳の <b>40% 以上</b>が
「横穴式石室」 に言及される。 広島県の古墳の主流は古墳時代後期
(6-7 世紀) であり、 前期-中期 (3-5 世紀) の竪穴式石室は少数派。</li>
<li><b>H5 (沿岸-内陸種別偏向仮説)</b>: 沿岸と中山間で種別構成が異なる。
沿岸の古墳率 < 中山間の古墳率 で <b>10 ポイント以上</b>の差がある。
沿岸は中世城館・近世砂留が、 中山間は古墳・集落跡が主体という二極化。</li>
</ul>
<h3>到達点</h3>
<ul>
<li>11 シリーズ × 22 市町を 1 つの統合表として読み込み、 種別・時代・
地理特性で多角集計できる。</li>
<li>古墳の概要 (free text) から円墳/方墳/前方後円・横穴式石室・須恵器・
鉄製品などの<b>keyword mining</b>を実装できる。</li>
<li>古墳径 (径Xm) を正規表現で抽出し、 ヒストグラムから中小規模古墳の
支配構造を量化できる。</li>
<li>地理タイプ × 種別のクロスから、 沿岸-内陸の文化的二極化を判定
できる。</li>
<li>L4 (浸水×文化財 300件) との重複を回避し、 本記事独自の構造分析
視点 ({N_TOTAL:,} 件全件・古墳形態・出土物・地理タイプ) で深掘り
できる。</li>
</ul>
<div class="note">
<b>L4 との重複回避</b>: L4 は同シリーズの 1663 (都城・官衙跡 13件) と
1669 (その他 198件) を<b>河川浸水想定区域とのオーバレイ判定</b>で扱った。
L84 は 11 件全件を統合し、 <b>埋蔵文化財自体の地理構造</b>を分析する。
L4 が「ハザード × 立地」 の交差視点であったのに対し、 L84 は<b>埋蔵
文化財そのものの種別・時代・古墳形態・出土物</b>を主軸とする。 浸水軸は
扱わない。 重複ゼロを徹底。
</div>
"""
sections.append(("学習目標と問い", intro_html))
# === 2. 使用データ ===
data_table_html = build_dataset_table()
data_html = f"""
<p>DoBoX のシリーズ「埋蔵文化財包蔵地一覧表」 は<b>11 dataset_id</b>
(1660-1670) からなる。 すべて 13 列の同一スキーマで提供されており、
1 つの DataFrame に縦結合可能。 本記事は<b>11 件全件</b>を統合する
(L4 が部分扱いの 2 件 (1663 + 1669) と被爆樹木 1 件で 300 件を扱った
のに対し、 本記事は 11 件全件 {N_TOTAL:,} 件を対象)。</p>
{data_table_html}
<h3>データ仕様 (11 シリーズ共通スキーマ)</h3>
<table>
<tr><th>列名</th><th>型</th><th>備考</th></tr>
<tr><td>番号</td><td>int</td><td>遺跡通番 (シリーズ内一意)</td></tr>
<tr><td>名称</td><td>str</td><td>遺跡名 (例: 新宮第1号古墳)</td></tr>
<tr><td>種別</td><td>str</td><td>古墳/円墳/館跡/官衙跡/集落跡/社寺/窯跡/砂留 等</td></tr>
<tr><td>時代</td><td>str</td><td>縄文/弥生/古墳/古代/中世/近世/近代 等 (混在ありで正規化が必要)</td></tr>
<tr><td>市町名</td><td>str</td><td>22 市町 (広島県の 23 市町中、 神石高原町・大崎上島町なども含む)</td></tr>
<tr><td>所在地</td><td>str</td><td>地番・字レベルの所在地</td></tr>
<tr><td>緯度</td><td>float</td><td>10 進度 (5-8 桁精度)</td></tr>
<tr><td>経度</td><td>float</td><td>10 進度</td></tr>
<tr><td>概要</td><td>str (free)</td><td>遺跡の<b>形態・規模・出土物</b>を記述
(例: 「径7mの円墳,横穴式石室,須恵器」) — keyword mining 対象</td></tr>
<tr><td>備考</td><td>str (free)</td><td>調査履歴・指定状況など</td></tr>
<tr><td>全国遺跡地図広島県(昭和57年文化庁発行)</td><td>str</td>
<td>1982 年文化庁刊行の全国遺跡地図参照番号 (古い記載)</td></tr>
<tr><td>広島県遺跡地図の地図番号</td><td>int</td>
<td>県の遺跡地図参照番号</td></tr>
<tr><td>関連URL</td><td>str</td><td>多くは空</td></tr>
</table>
<h3>11 シリーズの件数構造 (load log)</h3>
{df_to_html_table(load_log_df)}
<h3>この表から読み取れること</h3>
<ul>
<li><b>古墳・横穴 (#1660) が圧倒的多数 ({N_KOFUN:,} 件)</b>。 全体
{N_TOTAL:,} 件の {N_KOFUN/N_TOTAL*100:.1f}% を占める。 広島県は
「古墳の県」 と呼べる程の集積地。</li>
<li>「<b>その他 (#1669)</b>」 は 198 件で、 種別 31 種・時代 25 種と
多様。 経塚・砂留・祭祀遺跡・道・一里塚など、 主要 10 シリーズに
収まらない<b>「複合遺跡群」</b>として機能している。</li>
<li>1661 貝塚 (2件)・1665 社寺跡 (2件)・1666 生産遺跡 (1件)・
1670 水中遺跡 (1件) は<b>件数極小</b>。 これは本データが「県内全件
指定」 ではなく、 県教委が選定した一部のみ公開している可能性を示唆。
実際の県内貝塚・寺院跡は数十-数百件存在することは考古学文献で
確認可能。</li>
<li>緯度経度カバー率は シリーズ平均で 92% 以上。 ほぼ全件が地理可視化
可能。</li>
</ul>
<div class="note">
<b>重要なデータ理解</b>: 本データは<b>「DoBoX 公開分」のみ</b>であり、
広島県内の埋蔵文化財包蔵地の実総数 (数千件以上と推定) からは限定的。
特に貝塚・社寺・生産遺跡・水中遺跡シリーズは件数極小で、 県内の
実分布を代表しているとは言い難い。 本記事の主要分析対象は<b>古墳
({N_KOFUN:,} 件) + その他 (198 件) = 主要 9 割</b>とする。 統計的
分析は古墳に集中する。
</div>
"""
sections.append(("使用データ", data_html))
# === 3. ダウンロード ===
dl_buttons = []
dl_buttons.append(
'<a class="dlbtn" href="https://hiroshima-dobox.jp/datasets/1660" '
'target="_blank" '
'style="display:inline-block;padding:8px 14px;margin:4px;'
'background:#cf222e;color:white;border-radius:6px;text-decoration:none;'
'font-weight:bold;">▶ #1660 古墳・横穴 ({:,} 件)</a>'.format(
int(load_log_df.loc[load_log_df["dataset_id"] == 1660, "n_records"].iloc[0])))
for dsid, _rid, _slug, label, _grp in DATASETS:
if dsid == 1660:
continue
n = int(load_log_df.loc[load_log_df["dataset_id"] == dsid, "n_records"].iloc[0])
dl_buttons.append(
f'<a class="dlbtn" href="https://hiroshima-dobox.jp/datasets/{dsid}" '
f'target="_blank" '
f'style="display:inline-block;padding:6px 10px;margin:3px;'
f'background:#0969da;color:white;border-radius:4px;text-decoration:none;'
f'font-size:13px;">#{dsid} {escape(label)} ({n:,})</a>'
)
dl_html = f"""
<h3>DoBoX へのリンク (元データセット 11 件)</h3>
<p>下記ボタンから DoBoX の各データセットページに移動し、 そこから CSV を
ダウンロードできる。 すべて utf-8-sig (BOM 付き UTF-8) の CSV、
ライセンスは CC-BY (商用・非商用とも可)。</p>
<div>{"".join(dl_buttons)}</div>
<h3>本記事が生成した中間データ・図 (再現用)</h3>
<ul>
<li><a href="assets/L84_sites_all.csv" download>L84_sites_all.csv</a>
— 統合済 全 {N_TOTAL:,} 件 × 派生指標 17 列 (緯度経度・古墳形態・
出土物 keyword・時代正規化)</li>
<li><a href="assets/L84_load_log.csv" download>L84_load_log.csv</a>
— 11 シリーズ読込ログ</li>
<li><a href="assets/L84_series_summary.csv" download>L84_series_summary.csv</a>
— シリーズ別集計</li>
<li><a href="assets/L84_city_summary.csv" download>L84_city_summary.csv</a>
— 市町別集計 (件数・古墳率・密度)</li>
<li><a href="assets/L84_era_series_cross.csv" download>L84_era_series_cross.csv</a>
— 時代×種別 クロス集計</li>
<li><a href="assets/L84_era_concentration.csv" download>L84_era_concentration.csv</a>
— 種別ごとの時代集中度</li>
<li><a href="assets/L84_kofun_form.csv" download>L84_kofun_form.csv</a>
— 古墳形態 (円墳/方墳/前方後円/横穴) 集計</li>
<li><a href="assets/L84_kofun_artifact.csv" download>L84_kofun_artifact.csv</a>
— 古墳の出土物・埋葬形式</li>
<li><a href="assets/L84_diameter_stats.csv" download>L84_diameter_stats.csv</a>
— 古墳径分布統計</li>
<li><a href="assets/L84_geo_summary.csv" download>L84_geo_summary.csv</a>
— 地理タイプ別集計</li>
<li><a href="assets/L84_geo_group_cross.csv" download>L84_geo_group_cross.csv</a>
— 地理タイプ×種別 クロス</li>
<li><a href="assets/L84_track_one_site.csv" download>L84_track_one_site.csv</a>
— 1 件追跡 (要件 K)</li>
<li><a href="assets/L84_l4_compare.csv" download>L84_l4_compare.csv</a>
— L4 との重複・差別化整理</li>
<li><a href="assets/L84_hypothesis.csv" download>L84_hypothesis.csv</a>
— 仮説検証結果</li>
</ul>
<h3>図 (8 枚)</h3>
<ul>
<li><a href="assets/L84_fig01_overview.png" download>fig01 県全域 主題図</a></li>
<li><a href="assets/L84_fig02_city_ranking.png" download>fig02 市町別ランキング</a></li>
<li><a href="assets/L84_fig03_era_series_heatmap.png" download>fig03 時代×シリーズ ヒートマップ</a></li>
<li><a href="assets/L84_fig04_kofun_forms.png" download>fig04 古墳形態 3 区分地理</a></li>
<li><a href="assets/L84_fig05_kofun_diameter_artifacts.png" download>fig05 古墳径ヒストグラム + 出土物</a></li>
<li><a href="assets/L84_fig06_city_small_multiples.png" download>fig06 市町別 small multiples</a></li>
<li><a href="assets/L84_fig07_burial_chambers.png" download>fig07 横穴式 vs 竪穴式 石室</a></li>
<li><a href="assets/L84_fig08_geo_type_breakdown.png" download>fig08 地理タイプ × 種別 構成</a></li>
</ul>
<h3>再現スクリプト</h3>
<ul>
<li><a href="L84_archaeological_sites.py" download>L84_archaeological_sites.py</a>
— 単独実行可能。 データが無ければ ensure_dataset() が DoBoX から自動取得。</li>
</ul>
"""
sections.append(("ダウンロード", dl_html))
# === 4. 分析 1: 11 シリーズ統合と派生指標 ===
ana1_intro = """
<h3>狙い</h3>
<p>11 dataset_id (1660-1670) を 1 つの DataFrame に縦結合する。 各シリーズは
13 列の同一スキーマだが、 種別・時代の値は<b>シリーズ独立に命名</b>されて
おり、 統合後に正規化が必要。 本分析では (1) 統合読込、 (2) 種別 group 列の
追加、 (3) 時代の正規化、 (4) 古墳の概要 free text からの<b>keyword mining</b>
(円墳/方墳/前方後円/横穴式石室/須恵器/古墳径) を実装する。</p>
<h3>手法 (リテラシレベル解説)</h3>
<ul>
<li><b>縦結合 (concat)</b>: 11 個の DataFrame を <code>pd.concat()</code>
で 1 つに結合。 共通列 (13 列) は同名のため簡単に結合できる。
各行に元シリーズを示す<b>group 列</b> (古墳/城館/官衙/...) を追加して
トレース可能にする。</li>
<li><b>時代の正規化</b>: 「古墳」「弥生~古墳」「古代~中世」 のような
混在記載を、 ルールベースの関数で 9 区分 (縄文/弥生/古墳/古代/中世/
近世/近代/その他/不明) に正規化。 文字列 contains で
階層判定 (例: 「縄文」 を含み「弥生」 を含まなければ縄文)。</li>
<li><b>keyword mining</b>: 概要 (free text) を <code>str.contains()</code>
でキーワード判定。 「円墳」「横穴式石室」「須恵器」 などのキーワード
出現を 0/1 フラグ化。 古墳径は<b>正規表現</b> <code>径\\s*([\\d\\.]+)\\s*m</code>
で抽出。</li>
<li><b>入力</b>: 11 CSV 計 {n_total} 件。 <b>出力</b>: 統合 DataFrame
{n_total} 行 × 派生指標含 ~25 列。</li>
</ul>
""".replace("{n_total}", f"{N_TOTAL:,}")
ana1_code = '''
# 11 CSV を読込・縦結合
DATASETS = [
(1660, 177383, "kofun", "古墳・横穴", "古墳"),
(1661, 177384, "kaizuka", "貝塚", "貝塚"),
(1662, 177385, "shuraku", "集落跡・散布地", "集落"),
(1663, 177386, "tojo", "都城・官衙跡", "官衙"),
(1664, 177387, "jokan", "城館跡", "城館"),
(1665, 177388, "shaji", "社寺跡", "社寺"),
(1666, 177389, "seisan", "生産遺跡", "生産"),
(1667, 177390, "funbo", "その他の墳墓", "墳墓"),
(1668, 177391, "kindai", "近代以降の単独遺跡", "近代"),
(1669, 177392, "sonota", "その他", "その他"),
(1670, 177393, "suichu", "水中遺跡", "水中"),
]
frames = []
for dsid, rid, slug, label, group in DATASETS:
p = DATA_DIR / f"maizo_{dsid}_{slug}.csv"
df = pd.read_csv(p, encoding="utf-8-sig").dropna(subset=["名称"])
df = df.rename(columns={"名称": "name", "種別": "type_raw",
"時代": "era_raw", "市町名": "muni",
"所在地": "addr", "緯度": "lat",
"経度": "lon", "概要": "desc",
"備考": "remark"})
df["dataset_id"] = dsid
df["series"] = label
df["group"] = group
frames.append(df)
sites = pd.concat(frames, ignore_index=True)
# 派生指標 1: 古墳形態 keyword
combo = sites["desc"].fillna("") + " " + sites["remark"].fillna("")
sites["form_enpun"] = combo.str.contains("円墳").astype(int)
sites["form_hofun"] = combo.str.contains("方墳").astype(int)
sites["form_zenpo"] = combo.str.contains("前方後円").astype(int)
# 派生指標 2: 出土物 keyword
sites["k_yokoana_seki"] = combo.str.contains("横穴式石室").astype(int)
sites["k_tateana_seki"] = combo.str.contains("竪穴|竪穴式").astype(int)
sites["k_sueki"] = combo.str.contains("須恵器").astype(int)
sites["k_hajiki"] = combo.str.contains("土師器").astype(int)
sites["k_tetsu"] = combo.str.contains("鉄鏃|鉄製品|鉄刀|鉄剣|刀子").astype(int)
# 派生指標 3: 古墳径 (正規表現抽出)
import re
diam_match = combo.str.extract(r"径\\s*([\\d\\.]+)\\s*m")
sites["diameter_m"] = pd.to_numeric(diam_match[0], errors="coerce")
# 派生指標 4: 時代の正規化 (例)
def normalize_era(s):
if pd.isna(s): return "不明"
s = str(s)
if "古墳" in s or "古噴" in s: return "古墳"
if "中世" in s or "鎌倉" in s or "室町" in s: return "中世"
if "近世" in s or "江戸" in s: return "近世"
# ... (略)
return "その他"
sites["era_norm"] = sites["era_raw"].apply(normalize_era)
'''
ana1_io = """
<h3>入出力の Before/After 具体例 (要件 K)</h3>
<p>1 件の遺跡 (新宮第1号古墳) が 11 シリーズ統合パイプラインの中で
どう変換されるかを段階別に追跡する。</p>
""" + df_to_html_table(T_track) + """
<p><b>この表から読み取れること</b>:</p>
<ul>
<li>0→1 の段階で「種別=古墳」 が「group=古墳」 に正規化され、 10 シリーズに
対しても同じ group 列で集計可能になる。</li>
<li>1→2 の段階で「時代=古墳」 が「era_norm=古墳」 になる (例外として「弥生〜
古墳」 のような混在は最後尾優先で「古墳」 に正規化)。</li>
<li>5→6 の段階で「概要 = 径7mの円墳,横穴式石室」 から keyword mining が
実行され、 form_enpun=1 / k_yokoana_seki=1 / diameter_m=7.0 が抽出される。
これにより<b>free text を構造化データに変換</b>できる。</li>
<li>9 の段階で「市町=広島市」 が「geo_type=都市」 に分類される。</li>
</ul>
"""
ana1_table = f"""
<h3>シリーズ別集計</h3>
{df_to_html_table(T_series)}
<p><b>この表から読み取れること</b>:</p>
<ul>
<li>古墳・横穴 ({N_KOFUN:,} 件) が全体の <b>{N_KOFUN/N_TOTAL*100:.1f}%</b>を
占める。 11 シリーズ中で圧倒的多数。</li>
<li>その他 (198 件) は<b>種別 31 種</b>と多様で、 経塚・砂留・祭祀遺跡・
道・一里塚など主要シリーズに収まらない遺跡群を扱う。</li>
<li>残り 9 シリーズは合計 {N_TOTAL - N_KOFUN - 198:,} 件しかなく、 統計
分析の主軸は<b>古墳 + その他</b>となる (本記事も基本この方針)。</li>
<li>緯度経度カバー率はシリーズ平均で <b>{N_GEO/N_TOTAL*100:.1f}%</b>。
ほぼ全件が地理可視化可能。</li>
</ul>
"""
ana1_html = (ana1_intro + code(ana1_code) + ana1_io + ana1_table)
sections.append(("分析 1: 11 シリーズ統合と派生指標 (keyword mining)",
ana1_html))
# === 5. 分析 2: 県全域分布 + 市町別集計 ===
ana2_intro = """
<h3>狙い</h3>
<p>緯度経度から geopandas の GeoDataFrame を作り、 EPSG:4326 (WGS84) →
EPSG:6671 (JGD2011 平面直角第 III 系, m 単位) に再投影。 県全域に
{n_geo:,} 点を主題図として配置し、 種別ごとの色分けで地理パターンを
描く (Fig 1)。 さらに市町別件数ランキング + 単位面積あたり密度の散布図で
H1 (中山間古墳偏在仮説) を検証する (Fig 2)。</p>
<h3>手法 (リテラシレベル解説)</h3>
<ul>
<li><b>geopandas.points_from_xy</b>: 緯度経度配列から Point geometry を
作る関数。 <code>EPSG:4326</code> (WGS84) を指定するのが定石。</li>
<li><b>to_crs(EPSG:6671)</b>: 平面直角第 III 系 (広島県の標準 CRS)。
単位は m なので、 距離計算・面積計算が正確になる。</li>
<li><b>L15 admin cache の読込</b>: 行政界 zip 内の .geojson を BytesIO
経由で読込み、 dissolve(by="city") で市町ごと 1 ポリゴンに集約。
地図の背景レイヤとして使う。</li>
<li><b>市町別密度</b>: 件数 / 市域面積 (km²) で密度を計算。 これにより
広島市 ({N_KOFUN_HIROSHIMA} 件 / 906.7 km² = {hiro_density:.2f} 件/km²)
と東広島市 ({n_higashihiro} 件 / 635.3 km² = {hh_density:.2f} 件/km²)
の本当の集積度を比較できる。</li>
</ul>
""".format(
n_geo=N_GEO,
N_KOFUN_HIROSHIMA=int(T_city.loc[T_city["muni"] == "広島市", "n"].iloc[0])
if "広島市" in T_city["muni"].values else 0,
hiro_density=float(T_city.loc[T_city["muni"] == "広島市", "density_per_km2"].iloc[0])
if "広島市" in T_city["muni"].values else 0.0,
n_higashihiro=int(T_city.loc[T_city["muni"] == "東広島市", "n"].iloc[0])
if "東広島市" in T_city["muni"].values else 0,
hh_density=float(T_city.loc[T_city["muni"] == "東広島市", "density_per_km2"].iloc[0])
if "東広島市" in T_city["muni"].values else 0.0,
)
ana2_code = '''
# Step 1: GeoDataFrame 化 (緯度経度 → Point)
geo = sites.dropna(subset=["lat", "lon"]).copy()
gdf_sites = gpd.GeoDataFrame(
geo,
geometry=[Point(lon, lat) for lon, lat in zip(geo["lon"], geo["lat"])],
crs="EPSG:4326",
).to_crs("EPSG:6671")
# Step 2: L15 admin cache 読込 (22 市町)
admin_frames = []
for cname, dsid in ADMIN_MAP:
z = ADMIN_DIR / f"admin_{dsid}_{cname}.zip"
if z.exists():
g = load_admin_zip(z)
g["city"] = cname
admin_frames.append(g)
admin_diss = (gpd.GeoDataFrame(pd.concat(admin_frames), crs="EPSG:4326")
.to_crs("EPSG:6671")
.dissolve(by="city", as_index=False))
# Step 3: 市町別集計 + 密度
T_city = (sites.groupby("muni")
.agg(n=("site_id", "size"),
n_kofun=("is_kofun", "sum"))
.reset_index()
.sort_values("n", ascending=False))
T_city["area_km2"] = T_city["muni"].map(lambda c: CITY_REF.get(c, (np.nan,))[0])
T_city["density_per_km2"] = T_city["n"] / T_city["area_km2"]
'''
ana2_fig1 = f"""
<h3>図 1: 県全域 主題図 (種別色分け)</h3>
<p><b>なぜこの図か</b>: {N_GEO:,} 点を 1 枚に配置し、 11 シリーズ (古墳・
城館・官衙 etc) の<b>地理的偏り</b>を一望するため。 古墳は赤・大量で薄く、
その他種別はカラフル・少数で濃く描き、 「古墳が県全域を覆い、 城館や官衙が
特定地点に集中する」 二層構造を視覚化する。</p>
{figure("assets/L84_fig01_overview.png", "図1: 県全域 埋蔵文化財 全 2,653 件")}
<p><b>この図から読み取れること</b>:</p>
<ul>
<li>古墳 (赤・小点) が<b>県中央部から東部 (東広島・福山・尾道・北広島町)
に密集</b>。 県西部 (廿日市・大竹) は古墳が薄い。</li>
<li>城館跡 (青) は竹原市に 21 件集中 (= シリーズ 1664 全件)。 これは
DoBoX 公開データの偏在で、 県内全城館跡は数百件あると推定される
(本データの限界)。</li>
<li>その他 (黄, n=198) は福山・東広島・三次・世羅町など東部に多く、
経塚・砂留・祭祀遺跡として地域文化を担う。</li>
<li>瀬戸内海島嶼部 (江田島・大崎上島) は遺跡が極めて少ない。 古墳時代
の権力中心は本土側 (河川流域) にあったことが示唆される。</li>
</ul>
"""
ana2_fig2 = f"""
<h3>図 2: 市町別ランキング + 面積 vs 件数 散布図</h3>
<p><b>なぜこの図か</b>: 件数だけのランキングでは「広島市は人口 119 万人
だから多い」 のような<b>規模効果</b>が交絡する。 単位面積あたり密度を
散布図で可視化することで、 中山間市の真の集積度を見る。</p>
{figure("assets/L84_fig02_city_ranking.png", "図2: 市町別ランキング (左) と面積×件数散布図 (右)")}
<h4>表 5-1: 市町別集計 (件数 top 15)</h4>
{df_to_html_table(T_city.head(15)[
["muni", "n", "n_kofun", "kofun_pct", "area_km2", "density_per_km2", "geo_type"]
].rename(columns={"muni":"市町","n":"件数","n_kofun":"古墳数",
"kofun_pct":"古墳率%","area_km2":"面積km²",
"density_per_km2":"密度/km²","geo_type":"地理タイプ"}))}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
<li><b>東広島市 ({int(T_city.loc[T_city["muni"]=="東広島市","n"].iloc[0]):,} 件)</b>が
トップ。 中山間 + 都市の中間で、 西条盆地・賀茂郡などの古代-中世遺跡が
密集。 H1 を強支持。</li>
<li><b>広島市 ({int(T_city.loc[T_city["muni"]=="広島市","n"].iloc[0]):,} 件)</b>は
2 位だが、 単位面積あたり密度では中位 (= 市域 906 km² が広いため)。
沿岸の人工地盤 (デルタ平野) より、 安佐北区・佐伯区の山間部に古墳が
集中。</li>
<li><b>北広島町 ({int(T_city.loc[T_city["muni"]=="北広島町","n"].iloc[0]):,} 件)・
府中市 ({int(T_city.loc[T_city["muni"]=="府中市","n"].iloc[0]):,} 件)・
神石高原町 ({int(T_city.loc[T_city["muni"]=="神石高原町","n"].iloc[0]):,} 件)</b>
が中山間 top 3。 中山間 (緑色棒) が上位を独占。</li>
<li>沿岸都市 (青色棒): 呉・廿日市は<b>件数 30-50 件と低位</b>。 沿岸は
古代以降に開発されたエリアで、 古墳時代の中心ではない。</li>
<li>近郊町 (紫色棒) は件数極小 (府中町 7 / 海田町 11)。 市域も小さく
(10-15 km²)、 自然的にも遺跡数は少ない。</li>
</ul>
"""
ana2_html = (ana2_intro + code(ana2_code) + ana2_fig1 + ana2_fig2)
sections.append(("分析 2: 県全域分布と市町別集計", ana2_html))
# === 6. 分析 3: 時代×種別ヒートマップ (H3 検証) ===
ana3_intro = """
<h3>狙い</h3>
<p>11 シリーズ × 9 時代区分の<b>クロス集計表</b>を作り、 ヒートマップで
可視化する。 「古墳シリーズは古墳時代に集中、 城館跡は中世に集中、
その他は近世が多い」 のような<b>種別×時代の対応関係</b>を一望し、
H3 (各種別で時代 top 1 が 80% 以上) を検証する。</p>
<h3>手法 (リテラシレベル解説)</h3>
<ul>
<li><b>クロス集計</b>: <code>groupby(["era_norm", "group"]).size().unstack()</code>
で行=時代、 列=種別の表を作る。 <code>fillna(0)</code>で空セルを埋める。</li>
<li><b>matplotlib.imshow</b>: 行列値をヒートマップで可視化。 <code>cmap="YlOrRd"</code>
で 0 = 黄→大値 = 赤の階調。</li>
<li><b>セルラベル</b>: 各セルに件数を直接書き込む。 値が cmap の半分以上
なら白文字、 それ未満なら黒文字 (コントラスト確保)。</li>
<li><b>集中度</b>: 各種別で「最大時代の件数 / 種別総数」 を計算。 これが
80% を超えれば「種別と時代がほぼ 1:1 対応」 と判定。</li>
</ul>
"""
ana3_code = '''
# 時代×種別 クロス集計
T_era_series = (sites.groupby(["era_norm", "group"]).size()
.unstack(fill_value=0))
# 種別ごとの時代集中度
era_concentration = []
for grp, sub in sites.groupby("group"):
vc = sub["era_norm"].value_counts()
if len(vc) > 0:
top_era, top_n = vc.index[0], vc.iloc[0]
era_concentration.append({
"group": grp, "n": len(sub),
"top_era": top_era, "top_n": int(top_n),
"top_pct": round(top_n / len(sub) * 100, 1)
})
T_era_conc = pd.DataFrame(era_concentration)
# H3 検証 (件数 5+ の群のみ対象)
H3_groups = T_era_conc[T_era_conc["n"] >= 5]
h3_n_passed = (H3_groups["top_pct"] >= 80).sum()
'''
ana3_fig = f"""
<h3>図 3: 時代×種別 ヒートマップ</h3>
<p><b>なぜこの図か</b>: 11 シリーズ × 9 時代の同時情報を表だけで読み取るのは
困難。 ヒートマップ + セル内数値で<b>「どの種別がどの時代に集中するか」</b>を
1 枚で示せる。 数値も併記することで、 読み取り精度も担保する。</p>
{figure("assets/L84_fig03_era_series_heatmap.png", "図3: 時代×シリーズ ヒートマップ")}
<h4>表 6-1: 種別ごとの時代集中度</h4>
{df_to_html_table(T_era_conc.rename(columns={
"group":"種別","n":"件数","top_era":"top時代",
"top_n":"top件数","top_pct":"top占有率%"}))}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
<li><b>古墳シリーズ ({N_KOFUN:,} 件)</b>は「古墳」 時代が
{int(T_era_conc.loc[T_era_conc["group"]=="古墳","top_n"].iloc[0]):,} 件
({float(T_era_conc.loc[T_era_conc["group"]=="古墳","top_pct"].iloc[0]):.1f}%)。
H3 を強支持。 古墳=古墳時代という対応関係はほぼ 1:1。</li>
<li><b>城館跡 (21 件)</b>は<b>全件が中世 (100%)</b>。 戦国期山城が主体で、
古代以前・近世以降には属さない。</li>
<li><b>官衙 (13 件)</b>は<b>古代/平安/奈良に分散</b>。 律令制 (7-10 世紀)
の役所跡として時代範囲が広い。 top 占有率 50-60% 程度。</li>
<li><b>その他 (198 件)</b>は近世が最多 (72 件 = 36.4%)。 砂留・経塚・
祭祀遺跡が近世に多く築かれたことを反映。 中世も 22% で、 これは
経塚 (中世仏教信仰) が多いため。</li>
<li>左下の<b>近代</b>列は近代以降の遺跡 (1668 シリーズ + その他の被爆樹木
など) でほぼ空白。 本県の埋蔵文化財は<b>古代-近世が主体</b>。</li>
</ul>
"""
ana3_html = (ana3_intro + code(ana3_code) + ana3_fig)
sections.append(("分析 3: 時代×種別ヒートマップ (H3 検証)",
ana3_html))
# === 7. 分析 4: 古墳形態 + 古墳径 + 出土物 (H2, H4 検証) ===
ana4_intro = f"""
<h3>狙い</h3>
<p>古墳 ({N_KOFUN:,} 件) の概要 free text を keyword mining し、
(1) 形態 (円墳/方墳/前方後円)、 (2) 径 (m)、 (3) 出土物 (横穴式石室・
須恵器・鉄製品) の 3 軸で構造化する。 古墳形態の地図 (Fig 4)、
径ヒストグラム + 出土物棒 (Fig 5) で視覚化し、 H2 (円墳 ≥ 50% かつ
前方後円 < 2%) と H4 (横穴式石室言及率 ≥ 40%) を検証する。</p>
<h3>手法 (リテラシレベル解説)</h3>
<ul>
<li><b>str.contains(keyword)</b>: pandas の文字列メソッドで keyword を
含む行を 0/1 でフラグ化。 単純だが意味のある特徴量を抽出できる。</li>
<li><b>正規表現 extract</b>: <code>径\\s*([\\d\\.]+)\\s*m</code>のパターンで
数値を抽出。 「径7m」「径 12.5 m」 などの表記揺れにも対応。</li>
<li><b>地理可視化 small multiples</b>: 形態 3 区分を 3 panel に並べ、
同一 CRS で比較。 各 panel に件数を併記し<b>形態の県内地理</b>を一望。</li>
</ul>
<div class="note">
<b>keyword mining の限界</b>: 概要欄が空 (NaN) の古墳は判定不能。
本データでは概要記載率は約 70% (備考込み) で、 残り 30% の古墳は
「形態不明」 として集計から外れる。 言及率 ≠ 真の形態占有率である点に
注意。 ただし、 言及率は<b>下限値</b>として「少なくとも N% は円墳」
と解釈できる。
</div>
"""
ana4_code = '''
# Step 1: keyword フラグ化 (古墳のみ)
kofun = sites[sites["group"] == "古墳"].copy()
combo = kofun["desc"].fillna("") + " " + kofun["remark"].fillna("")
kofun["form_enpun"] = combo.str.contains("円墳").astype(int)
kofun["form_hofun"] = combo.str.contains("方墳").astype(int)
kofun["form_zenpo"] = combo.str.contains("前方後円").astype(int)
kofun["k_yokoana_seki"] = combo.str.contains("横穴式石室").astype(int)
kofun["k_tateana_seki"] = combo.str.contains("竪穴|竪穴式").astype(int)
kofun["k_sueki"] = combo.str.contains("須恵器").astype(int)
kofun["k_tetsu"] = combo.str.contains("鉄鏃|鉄製品|鉄刀|鉄剣").astype(int)
# Step 2: 古墳径 (正規表現抽出)
import re
diam_match = combo.str.extract(r"径\\s*([\\d\\.]+)\\s*m")
kofun["diameter_m"] = pd.to_numeric(diam_match[0], errors="coerce")
# Step 3: 形態地図 (3 panel) + 径ヒストグラム + 出土物棒
fig, axes = plt.subplots(1, 3, figsize=(18, 8))
panels = [
("円墳", kofun[kofun["form_enpun"] == 1], "#cf222e"),
("方墳", kofun[kofun["form_hofun"] == 1], "#0969da"),
("前方後円墳", kofun[kofun["form_zenpo"] == 1], "#9a6700"),
]
for ax_, (label, sub, col) in zip(axes, panels):
admin_diss.plot(ax=ax_, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.3)
sub.plot(ax=ax_, color=col, markersize=14, alpha=0.7)
ax_.set_title(f"{label} (n={len(sub):,})")
'''
ana4_fig4 = f"""
<h3>図 4: 古墳形態 3 区分の県内分布</h3>
<p><b>なぜこの図か</b>: 古墳 {N_KOFUN:,} 件のうち、 円墳 ({n_enpun:,} 件)・
方墳 ({n_hofun} 件)・前方後円墳 ({n_zenpo} 件) の<b>地理的差異</b>を
3 panel small multiples で比較。 同一 CRS・同一スケールで並べることで、
分布密度の違いを直感的に把握できる。</p>
{figure("assets/L84_fig04_kofun_forms.png", "図4: 古墳形態 3 区分の県内分布")}
<h4>表 7-1: 古墳形態 集計</h4>
{df_to_html_table(T_kofun_form)}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
<li><b>円墳が圧倒的多数 ({n_enpun:,} 件 / {n_enpun/N_KOFUN*100:.1f}%)</b>。
<b>H2 前半 (円墳 ≥ 50%) は支持</b>。 県全域に薄く広く分布し、
「古墳=ほぼ円墳」 の県と言える。</li>
<li><b>前方後円墳は {n_zenpo} 件のみ ({n_zenpo/N_KOFUN*100:.2f}%)</b>。
<b>H2 後半 (前方後円 < 2%) は支持</b>。 大和朝廷の中心地 (奈良・
大阪) のような巨大王墓集積はなく、 地域豪族の中規模古墳が主体。</li>
<li>方墳 ({n_hofun} 件) は<b>福山・尾道など県東部に集中</b>。 古墳時代
終末期 (7 世紀) の特徴的形態で、 県西-中部には少ない。</li>
<li>前方後円墳 ({n_zenpo} 件) は<b>三原・福山・東広島など瀬戸内寄りに
集中</b>。 海路アクセスが良い場所に大豪族墓が築かれた。 これは
<b>古墳時代の海上交易と権力中心の関係</b>を示唆する。</li>
</ul>
"""
ana4_fig5 = f"""
<h3>図 5: 古墳径ヒストグラム + 出土物 keyword 棒</h3>
<p><b>なぜこの図か</b>: 古墳径の分布で「中小規模古墳が主流」 を量化し、
出土物 keyword 棒で「古墳時代後期 (横穴式石室) が前期-中期 (竪穴式石室) を
圧倒する」 を量化する。 2 図並列で<b>サイズ × 形式 × 副葬品</b>の 3 軸を
1 枚に。</p>
{figure("assets/L84_fig05_kofun_diameter_artifacts.png", "図5: 径ヒスト+出土物棒")}
<h4>表 7-2: 古墳径分布統計</h4>
{df_to_html_table(T_diam)}
<h4>表 7-3: 古墳の出土物・埋葬形式 言及率</h4>
{df_to_html_table(T_kofun_artifact)}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
<li>径ヒストグラムは<b>右に強く偏ったロングテール分布</b>。 中央値
{float(diam.median()):.1f} m、 平均 {float(diam.mean()):.1f} m、
{(diam <= 10).sum() / N_DIAM * 100:.1f}% が径 ≤ 10 m。 「<b>中小規模
円墳が支配</b>」 が量的に確認された。</li>
<li>径 30 m 超の大規模古墳は {(diam > 30).sum()} 件のみ ({(diam > 30).sum() / N_DIAM * 100:.1f}%)。
最大は {float(diam.max()):.1f} m。 大和朝廷中枢の前方後円墳 (200m+) と
比較すると規模が一桁違う。</li>
<li><b>横穴式石室言及率 = {n_yokoana_seki/N_KOFUN*100:.1f}%</b>
({n_yokoana_seki:,} / {N_KOFUN:,})。 <b>H4 ({n_yokoana_seki/N_KOFUN*100:.1f}%
{'≥' if n_yokoana_seki/N_KOFUN*100 >= 40 else '<'} 40%) {'支持' if n_yokoana_seki/N_KOFUN*100 >= 40 else '反証'}</b>。
古墳時代後期 (6-7 世紀) の埋葬形式が圧倒的多数。</li>
<li>竪穴式石室はわずか {n_tateana_seki} 件 ({n_tateana_seki/N_KOFUN*100:.1f}%)。
前期-中期古墳 (3-5 世紀) は本県では極めて少数派。 = 県内古墳時代の
実質的開始は<b>6 世紀以降</b>と解釈できる。</li>
<li>須恵器言及率 {kofun["k_sueki"].sum()/N_KOFUN*100:.1f}% は朝鮮半島
由来の硬質土器が古墳副葬品の標準であったことを示す。</li>
</ul>
"""
ana4_html = (ana4_intro + code(ana4_code) + ana4_fig4 + ana4_fig5)
sections.append(("分析 4: 古墳形態・径・出土物 (H2, H4 検証)", ana4_html))
# === 8. 分析 5: 市町別 small multiples ===
ana5_intro = f"""
<h3>狙い</h3>
<p>件数 top 12 市町の遺跡分布を<b>small multiples マップ</b> (3×4 panel)
で並列表示。 同一スケール・同一凡例で 12 市町を比較できるため、 「中山間
市は山間に古墳が散在、 沿岸市は限定エリアに集中」 のような<b>地理パターンの
市町間差異</b>が一望できる。</p>
<h3>手法 (リテラシレベル解説)</h3>
<ul>
<li><b>small multiples</b>: 同一構造の図を grid に並べる手法。 1 枚ずつ
別ファイルにするより認知負荷が低く、 比較が容易。</li>
<li>各 panel: 行政界ポリゴン (薄灰) + 種別色分け点を重ねる。 panel 内で
aspect="equal" を保つことで、 市町の真の形状を保つ。</li>
<li>件数情報を panel タイトルに含める (「東広島市 (都市) 総 757 件 / 古墳
728」 のように)。</li>
</ul>
"""
ana5_fig = f"""
<h3>図 6: 市町別 small multiples (件数上位 12 市町)</h3>
<p><b>なぜこの図か</b>: 件数だけのランキング (図 2) では空間構造が見えない。
small multiples で同一スケール並列することで、 市域内の<b>遺跡集中エリア</b>を
12 市町同時比較できる。</p>
{figure("assets/L84_fig06_city_small_multiples.png", "図6: 市町別 small multiples")}
<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>東広島市</b>: 西条盆地・賀茂郡を中心に古墳が密集。 中央部の
平地でなく<b>周辺丘陵地</b>に多数。</li>
<li><b>広島市</b>: 安佐北区・佐伯区の山間部に古墳。 中区・南区の
デルタ平野には遺跡が極めて少ない (= 都市開発で消失した可能性も高い)。</li>
<li><b>北広島町・神石高原町・府中市</b>: 河川流域に沿って線状に遺跡が
並ぶ。 古代の街道 (山陽道支線) や河川流域の集落-墓地配置を反映。</li>
<li><b>福山市</b>: 芦田川中流域に密集。 古代備後国の中心地で、 官衙
(1663) も含む。 古代-中世の複合遺跡群。</li>
<li><b>尾道市・三原市・竹原市</b> (沿岸): 海岸線から離れた山際に
遺跡が並ぶ。 古墳時代-中世の集落は海岸ではなく<b>後背丘陵</b>に
立地した。</li>
<li><b>呉市</b>: 件数 38 と少なく、 軍港都市として近代に発展した
ことが古墳時代の遺跡密度に反映されている (= 軍港開発前の地形は
急峻で、 古墳築造に不向き)。</li>
</ul>
"""
ana5_html = (ana5_intro + ana5_fig)
sections.append(("分析 5: 市町別 small multiples", ana5_html))
# === 9. 分析 6: 横穴式石室 vs 竪穴式石室 地理 ===
ana6_intro = f"""
<h3>狙い</h3>
<p>古墳時代後期 (6-7 世紀) の<b>横穴式石室</b>と前期-中期 (3-5 世紀) の
<b>竪穴式石室</b>の県内地理を並列比較。 古墳時代の<b>時間軸</b>を埋葬形式の
分布で表現し、 県内古墳時代の実質的開始時期を地理的に追う。</p>
<h3>手法</h3>
<ul>
<li>古墳 ({N_KOFUN:,} 件) の概要を keyword mining し、
横穴式石室 ({n_yokoana_seki:,} 件) と竪穴式石室 ({n_tateana_seki} 件) の
geometry のみを抽出。</li>
<li>2 panel small multiples で並列描画。 件数 + 言及率もタイトルに併記。</li>
</ul>
"""
ana6_fig = f"""
<h3>図 7: 横穴式石室 vs 竪穴式石室 地理可視化</h3>
<p><b>なぜこの図か</b>: 古墳の埋葬形式は時間軸 (前期 vs 後期) に対応する
ため、 地理的分布を比較すれば「古墳時代の県内拡大過程」 が可視化できる。
表だけでは地理性が消える。</p>
{figure("assets/L84_fig07_burial_chambers.png", "図7: 横穴式石室 vs 竪穴式石室")}
<p><b>この図から読み取れること</b>:</p>
<ul>
<li>横穴式石室 ({n_yokoana_seki:,} 件) は<b>県全域に分布</b>。 古墳時代
後期 (6-7 世紀) には県内ほぼ全域で古墳築造が普及していた。</li>
<li>竪穴式石室 ({n_tateana_seki} 件) は<b>東広島・広島・神石高原町・
福山に少数点在</b>。 古墳時代前期-中期 (3-5 世紀) の権力中心は
限定的で、 6 世紀以降に県全域に拡大。</li>
<li>両者の比 = {n_tateana_seki}:{n_yokoana_seki} ≒
1:{n_yokoana_seki / max(n_tateana_seki, 1):.0f}。 後期が圧倒的に多く、
本県の古墳時代は<b>後期主導</b>と判定できる。</li>
<li>これは大和朝廷からの距離・伝播の遅さを反映している可能性。
大和地方では 4-5 世紀の前方後円墳が中心だが、 広島 (備後・安芸)
では 6-7 世紀以降に古墳築造が本格化したと解釈できる。</li>
</ul>
"""
ana6_html = (ana6_intro + ana6_fig)
sections.append(("分析 6: 横穴式石室 vs 竪穴式石室 地理可視化", ana6_html))
# === 10. 分析 7: 地理タイプ × 種別構成 (H5 検証) ===
ana7_intro = f"""
<h3>狙い</h3>
<p>地理タイプ (都市・沿岸・中山間・離島・近郊) × 種別 (古墳・城館・官衙
等) の<b>構成比 (積み上げ棒)</b>と件数+古墳率を並列描画。 H5 (沿岸古墳率
< 中山間古墳率 で 10 ポイント以上の差) を検証する。</p>
<h3>手法 (リテラシレベル解説)</h3>
<ul>
<li><b>積み上げ棒 (stacked bar)</b>: 各地理タイプの<b>構成比</b>を 100% で
正規化。 タイプ間で「種別の比率」 を比較できる (絶対値ではなく相対値)。</li>
<li><b>件数棒</b>: 絶対件数 + 古墳率%をタイトルに併記。 構成比 + 絶対値の
両面情報を 1 枚に。</li>
</ul>
"""
ana7_code = '''
# 地理タイプ × group クロス → 100% 正規化 → 積み上げ棒
T_geo_group = (sites.groupby(["geo_type", "group"]).size()
.unstack(fill_value=0))
T_geo_pct = T_geo_group.div(T_geo_group.sum(axis=1), axis=0) * 100
bottom = np.zeros(len(T_geo_pct))
for grp in T_geo_pct.columns:
ax.bar(T_geo_pct.index, T_geo_pct[grp], bottom=bottom,
color=GROUP_COLOR[grp], label=grp)
bottom += T_geo_pct[grp].values
# H5 検証: 沿岸古墳率 vs 中山間古墳率
coast_kofun_pct = (sites[(sites["group"] == "古墳") &
(sites["geo_type"] == "沿岸")].shape[0]
/ max(sites[sites["geo_type"] == "沿岸"].shape[0], 1) * 100)
inland_kofun_pct = (sites[(sites["group"] == "古墳") &
(sites["geo_type"] == "中山間")].shape[0]
/ max(sites[sites["geo_type"] == "中山間"].shape[0], 1) * 100)
h5_diff = inland_kofun_pct - coast_kofun_pct
'''
ana7_fig = f"""
<h3>図 8: 地理タイプ × 種別構成 + 件数 + 古墳率%</h3>
<p><b>なぜこの図か</b>: H5 (沿岸 vs 中山間で古墳率に差) を 1 枚で示すため、
構成比 (相対) + 件数 (絶対) を並列描画する。 構成比だけでは沿岸の絶対量が
分からず、 件数だけでは種別構成が分からない。</p>
{figure("assets/L84_fig08_geo_type_breakdown.png", "図8: 地理タイプ別 種別構成と件数")}
<h4>表 10-1: 地理タイプ別 集計</h4>
{df_to_html_table(T_geo_summary)}
<h4>表 10-2: 地理タイプ × 種別 件数クロス</h4>
{df_to_html_table(T_geo_group.reset_index())}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
<li><b>中山間 ({int(T_geo_total['中山間']):,} 件) の古墳率
{float(T_geo_kofun_pct['中山間']):.1f}%</b>。 古墳が圧倒的多数。
山間部は古代の主要地理。</li>
<li><b>沿岸 ({int(T_geo_total['沿岸']):,} 件) の古墳率
{float(T_geo_kofun_pct['沿岸']):.1f}%</b>。 中山間より低い。
城館・砂留・官衙など複合的な遺跡構成。</li>
<li>差 = {float(T_geo_kofun_pct['中山間']) - float(T_geo_kofun_pct['沿岸']):.1f} pt。
H5 ({h5_diff:+.1f} pt {'≥' if h5_diff >= 10 else '<'} 10 pt)
{'<b>支持</b>' if h5_diff >= 10 else '<b>反証/部分支持</b>'}。</li>
<li><b>都市 ({int(T_geo_total['都市']):,} 件)</b>は古墳率
{float(T_geo_kofun_pct['都市']):.1f}%。 中山間と同程度の高い古墳率は、
広島市・東広島市・福山市の山間部 (安佐北・西条・芦田川流域) に古墳が
密集するためで、 都市中心部 (デルタ平野・市街地) ではない。</li>
<li><b>離島 ({int(T_geo_total['離島']):,} 件)</b>は江田島市 + 大崎上島
町合計で 20 件と極小。 離島は古墳時代の中心ではなかった。</li>
<li><b>近郊 ({int(T_geo_total['近郊']):,} 件)</b>は府中町・海田町・
熊野町・坂町合計で 30 件と少ない。 市域も小さく、 自然的にも遺跡数
は少ない。</li>
</ul>
"""
ana7_html = (ana7_intro + code(ana7_code) + ana7_fig)
sections.append(("分析 7: 地理タイプ × 種別構成 (H5 検証)", ana7_html))
# === 11. 仮説検証総合 ===
hyp_html = f"""
<h3>5 仮説の検証結果</h3>
{df_to_html_table(T_hyp)}
<h3>判定の要点</h3>
<ul>
<li><b>H1 (中山間古墳偏在)</b>: 中山間古墳率 {mountainous_pct:.1f}%。
{'<b>支持</b>' if h1_ok else '<b>反証</b>'}。
{'古代の権力中心は瀬戸内沿岸ではなく内陸に偏在' if h1_ok else '中山間集中度は仮説より低い'}。</li>
<li><b>H2 (円墳支配)</b>: 円墳 {h2_enpun_pct:.1f}% / 前方後円
{h2_zenpo_pct:.2f}%。 {'<b>支持</b>' if h2_ok else '<b>部分支持</b>'}。
広島県は中小規模円墳の密集地で大和朝廷的な巨大王墓は不在。</li>
<li><b>H3 (時代-種別対応)</b>: {h3_n_total} 群中 {h3_n_passed} 群で
時代 top 1 が 80% 以上。 {'<b>支持</b>' if h3_ok else '<b>部分支持</b>'}。
古墳=古墳時代、 城館=中世、 官衙=古代という対応は確立している。</li>
<li><b>H4 (横穴式石室普及)</b>: 言及率 {h4_pct:.1f}%。
{'<b>支持</b>' if h4_ok else '<b>反証</b>'}。
{'本県古墳時代の主流は後期 (6-7 世紀)' if h4_ok else '横穴式石室の言及率は仮説より低い'}。</li>
<li><b>H5 (沿岸-内陸偏向)</b>: 沿岸 {coast_kofun_pct:.1f}% vs 中山間
{inland_kofun_pct:.1f}% (差 {h5_diff:+.1f} pt)。
{'<b>支持</b>' if h5_ok else '<b>反証/部分支持</b>'}。
{'沿岸-内陸の文化的二極化が量的に確認された' if h5_ok else '沿岸-内陸差は仮説より小さい'}。</li>
</ul>
<h3>L4 との重複回避まとめ</h3>
{df_to_html_table(T_l4_compare)}
<p><b>L84 独自の発見 (L4 で扱われていない)</b>:</p>
<ul>
<li>古墳形態 (円墳/方墳/前方後円) の<b>keyword mining</b> + 地理可視化</li>
<li>古墳径 (m) の正規表現抽出による<b>サイズ分布</b>定量化</li>
<li>出土物 (横穴式石室・須恵器・鉄製品) の<b>言及率</b>と地理パターン</li>
<li>11 シリーズ × 22 市町 × 9 時代の<b>3 軸クロス集計</b></li>
<li>地理タイプ × 種別の<b>構成比積み上げ棒</b></li>
</ul>
<p><b>L4 との視点の差</b>: L4 は「文化財が浸水するか」 を <b>sjoin</b>で
判定する記事 (= ハザード×立地)。 L84 は「埋蔵文化財の地理構造そのもの」 を
種別×時代×形態×出土物で記述する記事 (= 文化財自体の構造)。 重複ゼロ。</p>
"""
sections.append(("仮説検証と考察", hyp_html))
# === 12. 発展課題 ===
dev_html = f"""
<h3>結果 X → 新仮説 Y → 課題 Z (3 段論理)</h3>
<h4>発展課題 1: 古墳径と地形 (尾根・丘陵・河川流域) の関係</h4>
<ul>
<li><b>結果 X</b>: 古墳径中央値 {float(diam.median()):.1f} m、
{(diam <= 10).sum()/N_DIAM*100:.1f}% が径 ≤ 10 m (図 5)。 中小規模が
主流。</li>
<li><b>新仮説 Y</b>: 古墳径は<b>立地地形</b>と相関するのではないか?
山頂・丘陵尾根に築かれた古墳は規模に上限がある (狭い尾根=小さい
墳丘)、 平地の古墳は大規模化しやすい仮説。</li>
<li><b>課題 Z</b>: L40 (DEM 標高データ) と sjoin で各古墳の標高・
地形分類 (尾根/谷/平地) を取得し、 径 vs 立地のクロス散布を作る。
古墳築造の<b>地形制約</b>を量的に評価できる。</li>
</ul>
<h4>発展課題 2: 古墳と古道 (山陽道・出雲街道) の関係</h4>
<ul>
<li><b>結果 X</b>: 北広島町・府中市の古墳分布は<b>河川流域に沿って
線状</b>に並ぶ (図 6 small multiples)。 主要街道との重なりが
示唆される。</li>
<li><b>新仮説 Y</b>: 古墳の地理分布は<b>古代街道 (山陽道・出雲街道)</b>
沿いに集中するのではないか? 街道近接古墳は地域豪族の権威表示と
して通行人に見える位置に築かれた可能性。</li>
<li><b>課題 Z</b>: 古道の GIS データ (DoBoX 外、 山陽道・出雲街道の
推定ルート) を取得し、 古墳との最近距離分布を計算。 ランダム分布と
比較し<b>街道近接性</b>を量化できる。</li>
</ul>
<h4>発展課題 3: 城館跡の戦略的立地</h4>
<ul>
<li><b>結果 X</b>: 城館跡 21 件 (1664) は全件竹原市に集中 (= DoBoX
公開分の偏在)。 図 1 で青点が竹原市に集中。</li>
<li><b>新仮説 Y</b>: 城館跡は<b>標高 100-300 m の山頂・尾根</b>に集中
し、 視認性 (見渡しの良さ) と防御性 (登攀困難) を両立する戦略的
立地が選ばれているのではないか?</li>
<li><b>課題 Z</b>: 城館 21 件 + L40 標高 + 視認性計算 (LoS, line of
sight) を結合。 戦国期の山城配置の<b>戦略地理学</b>として深掘り
できる。</li>
</ul>
<h4>発展課題 4: 「DoBoX 公開分」 と「実総数」 のギャップ</h4>
<ul>
<li><b>結果 X</b>: 貝塚 2 件・社寺 2 件・水中遺跡 1 件と<b>件数極小</b>
(図 1, 表 6)。 県内実総数とのギャップが大きい。</li>
<li><b>新仮説 Y</b>: DoBoX 公開分は<b>「県教委が選定した代表事例」</b>で、
全数登録されていないのではないか? 実際の県内貝塚は数十件 (考古学
文献)、 寺院跡は数百件存在するはず。</li>
<li><b>課題 Z</b>: 県教委発行の埋蔵文化財包蔵地一覧表 (PDF・冊子) と
DoBoX 公開分の差を比較し、 公開ポリシーの限界を可視化。 オープン
データの<b>網羅性評価</b>として方法論的価値がある。</li>
</ul>
<h4>発展課題 5: 古墳分布と古代行政区画 (備後・安芸・備中)</h4>
<ul>
<li><b>結果 X</b>: 福山・尾道・三原 (=古代備後国) に古墳・官衙が
集中。 広島市・呉・東広島 (=古代安芸国) は別パターン。 県内には
古代行政の<b>2 つの中心地</b>があった (図 1, 図 6)。</li>
<li><b>新仮説 Y</b>: 県境を<b>備後/安芸の古代国境</b> (沼田川流域)
で再分類すると、 H1 の中山間偏在仮説の構造はどう変化するか?
現行行政区分でなく古代行政区分での分析が、 古墳時代の権力地理を
より正確に反映するのではないか?</li>
<li><b>課題 Z</b>: 古代国境 (備後/安芸/備中) の polygon を作り、
sjoin で各遺跡を再分類。 国別集計で古代国家の権力中心地理を
描写できる。</li>
</ul>
"""
sections.append(("発展課題", dev_html))
# === 13. 再現スクリプト ===
script_path = Path(__file__).resolve()
with open(script_path, "r", encoding="utf-8") as fh:
src_text = fh.read()
src_html = f"""
<p>本記事のすべての分析を 1 つの Python スクリプトで再現できる。</p>
<p><b>動作環境</b>: Python 3.10+ / pandas / numpy / matplotlib / geopandas /
shapely / pyogrio / pygments / requests。 データは
<code>data/extras/L84_archaeological_sites/</code> + L15 admin cache から
読込。 無ければ <code>ensure_dataset()</code> が DoBoX から自動取得。</p>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 lessons/L84_archaeological_sites.py</code></pre>
<h3>L84_archaeological_sites.py 全文</h3>
{code(src_text)}
"""
sections.append(("再現スクリプト全文", src_html))
# 描画完了 → render_lesson
html = render_lesson(
num=84,
title="L84 埋蔵文化財包蔵地 11 件統合分析",
tags=["L84", "埋蔵文化財", "古墳", "城館跡", "官衙跡", "考古学",
"11シリーズ統合", "keyword mining", "geopandas"],
time="20-30 分",
level="リテラシ + 統合構造分析",
data_label="DoBoX 11 シリーズ (#1660-1670) 全件統合",
sections=sections,
script_filename="L84_archaeological_sites.py",
)
out_html = LESSONS / "L84_archaeological_sites.html"
out_html.write_text(html, encoding="utf-8")
print(f" HTML written: {out_html}", flush=True)
print(f" ({time.time()-t9:.2f}s)", flush=True)
# =============================================================================
# 14. 最終レポート
# =============================================================================
total_sec = time.time() - t_all
print(f"\n=== 完走 {total_sec:.2f}s ===", flush=True)
print(f"統合: {N_TOTAL:,} 件 / 緯度経度 {N_GEO:,} 件 / 古墳 {N_KOFUN:,} 件",
flush=True)
print(f"H1 (中山間古墳率 ≥60%): {h1_ok} ({mountainous_pct:.1f}%)", flush=True)
print(f"H2 (円墳 ≥50%, 前方後円 <2%): {h2_ok} "
f"(円墳 {h2_enpun_pct:.1f}%, 前方後円 {h2_zenpo_pct:.2f}%)", flush=True)
print(f"H3 (時代 top1 ≥80%): {h3_ok} ({h3_n_passed}/{h3_n_total} 群)",
flush=True)
print(f"H4 (横穴式石室 ≥40%): {h4_ok} ({h4_pct:.1f}%)", flush=True)
print(f"H5 (沿岸<内陸 古墳率, 差 ≥10pt): {h5_ok} "
f"(沿岸 {coast_kofun_pct:.1f}% vs 中山間 {inland_kofun_pct:.1f}%, "
f"差 {h5_diff:+.1f}pt)", flush=True)
print(f"出力: {LESSONS}/L84_archaeological_sites.html", flush=True)
print(f" {ASSETS}/L84_*.csv (13 種), L84_*.png (8 枚)", flush=True)
|