国交省オープンデータの交通量APIのコード例


# はじめに

国土交通省・JARTICが収集・提供しているオープンデータ「交通量データ」の「交通量API」を分析する簡単なコード例です．せっかくオープンデータとして公開していただいているので，本コードもオープンに公開します．

参考URL：[国交省xROAD](https://www.xroad.mlit.go.jp/), [JARTIC交通量データの提供ページ](https://www.jartic-open-traffic.org/)

本記事は，[GitHubにあるJupyter notebook](https://github.com/toruseo/xROAD-API-example)をマークダウンに変換したものです．直接実行したい方はそちらにアクセスしてください．


# Jupyter notebookのコマンド


```python
%matplotlib inline
```


```python
from matplotlib.pyplot import *
from numpy import *
from numpy.linalg import *

from tqdm.notebook import tqdm as tqdm
import pandas as pd

rcParams['font.family'] = "MS Gothic"
```

# APIでデータ取得


```python
import requests
import json
from datetime import datetime

#道路種別を指定．この例は一般道
road_type = "3"

#日時を指定
time_code_from = "20250512"+"0000"
time_code_to   = "20250512"+"2355"

#緯度経度を指定．この例は東京
min_x = 139.45
min_y = 35.55
max_x = 139.93
max_y = 35.82

api = f"https://api.jartic-open-traffic.org/geoserver?service=WFS&version=2.0.0&request=GetFeature&typeNames=t_travospublic_measure_5m&srsName=EPSG:4326&outputFormat=application/json&exceptions=application/json&cql_filter=道路種別={road_type} AND 時間コード>={time_code_from} AND 時間コード<={time_code_to} AND BBOX(ジオメトリ,{min_x},{min_y},{max_x},{max_y},'EPSG:4326')"
response = requests.get(api)

print(response.text[:200])

data = json.loads(response.text)


```

    {"type": "FeatureCollection", "features": [{"type": "Feature", "id": "t_travospublic_measure_5m.24158256.202505121150", "geometry": {"type": "MultiPoint", "coordinates": [[139.7049058, 35.58550262]]},
    

# 内容確認


```python
time_code_from_obj = datetime.strptime(time_code_from, "%Y%m%d%H%M")
time_code_to_obj = datetime.strptime(time_code_to, "%Y%m%d%H%M")
timestep_counts = ((time_code_to_obj - time_code_from_obj).total_seconds()) // (60*5)

print(f'total: {len(data["features"])}, time counts (aprx): {timestep_counts}, locations (aprx):, {len(data["features"])/timestep_counts}')
```

    total: 5742, time counts (aprx): 287.0, locations (aprx):, 20.006968641114984
    


```python
data.keys()
```




    dict_keys(['type', 'features', 'totalFeatures', 'numberMatched', 'numberReturned', 'timeStamp', 'crs'])




```python
data["features"][0]
```




    {'type': 'Feature',
     'id': 't_travospublic_measure_5m.24158256.202505121150',
     'geometry': {'type': 'MultiPoint',
      'coordinates': [[139.7049058, 35.58550262]]},
     'geometry_name': 'ジオメトリ',
     'properties': {'地方整備局等番号': 83,
      '開発建設部／都道府県コード': '',
      '常時観測点コード': 3110010,
      '収集時間フラグ（5分間／1時間）': '1',
      '観測年月日': 20250512,
      '時間帯': 1150,
      '上り・小型交通量': 91,
      '上り・大型交通量': 7,
      '上り・車種判別不能交通量': 0,
      '上り・停電': '0',
      '上り・ループ異常': '0',
      '上り・超音波異常': '0',
      '上り・欠測': '0',
      '下り・小型交通量': 73,
      '下り・大型交通量': 10,
      '下り・車種判別不能交通量': 3,
      '下り・停電': '0',
      '下り・ループ異常': '0',
      '下り・超音波異常': '0',
      '下り・欠測': '0',
      '道路種別': '3',
      '時間コード': 202505121150}}



# GeoPandasに変換


```python
import geopandas as gpd
from shapely.geometry import Point

# Initialize an empty list to collect rows
rows = []

for feature in data["features"]:
    prop = feature["properties"]
    coords = feature["geometry"]["coordinates"][0]
    
    datetime_obj = datetime.strptime(str(prop['観測年月日'])+str(prop['時間帯']), "%Y%m%d%H%M")

    # Calculate totals
    traffic_up = prop['上り・小型交通量'] + prop['上り・大型交通量'] + prop['上り・車種判別不能交通量']
    traffic_down = prop['下り・小型交通量'] + prop['下り・大型交通量'] + prop['下り・車種判別不能交通量']
    
    rows.append({
        "lon": coords[0],
        "lat": coords[1],
        "datetime": datetime_obj,
        "traffic_up": traffic_up,
        "traffic_down": traffic_down,
        "traffic_up_small": prop['上り・小型交通量'],
        "traffic_up_large": prop['上り・大型交通量'],
        "traffic_up_unidentified": prop['上り・車種判別不能交通量'],
        "traffic_down_small": prop['下り・小型交通量'],
        "traffic_down_large": prop['下り・大型交通量'],
        "traffic_down_unidentified": prop['下り・車種判別不能交通量'],
        "geometry": Point(coords[0], coords[1])
    })

# Create GeoDataFrame in one operation
df = gpd.GeoDataFrame(rows, crs="EPSG:4326")

print(f"Created GeoDataFrame with {len(df)} rows")
df
```

    Created GeoDataFrame with 5742 rows
    




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>lon</th>
      <th>lat</th>
      <th>datetime</th>
      <th>traffic_up</th>
      <th>traffic_down</th>
      <th>traffic_up_small</th>
      <th>traffic_up_large</th>
      <th>traffic_up_unidentified</th>
      <th>traffic_down_small</th>
      <th>traffic_down_large</th>
      <th>traffic_down_unidentified</th>
      <th>geometry</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>139.704906</td>
      <td>35.585503</td>
      <td>2025-05-12 11:50:00</td>
      <td>98</td>
      <td>86</td>
      <td>91</td>
      <td>7</td>
      <td>0</td>
      <td>73</td>
      <td>10</td>
      <td>3</td>
      <td>POINT (139.70491 35.5855)</td>
    </tr>
    <tr>
      <th>1</th>
      <td>139.878654</td>
      <td>35.702749</td>
      <td>2025-05-12 11:50:00</td>
      <td>88</td>
      <td>94</td>
      <td>71</td>
      <td>11</td>
      <td>6</td>
      <td>74</td>
      <td>15</td>
      <td>5</td>
      <td>POINT (139.87865 35.70275)</td>
    </tr>
    <tr>
      <th>2</th>
      <td>139.727326</td>
      <td>35.564747</td>
      <td>2025-05-12 11:50:00</td>
      <td>85</td>
      <td>77</td>
      <td>58</td>
      <td>22</td>
      <td>5</td>
      <td>54</td>
      <td>20</td>
      <td>3</td>
      <td>POINT (139.72733 35.56475)</td>
    </tr>
    <tr>
      <th>3</th>
      <td>139.685433</td>
      <td>35.792401</td>
      <td>2025-05-12 11:50:00</td>
      <td>51</td>
      <td>79</td>
      <td>33</td>
      <td>16</td>
      <td>2</td>
      <td>63</td>
      <td>16</td>
      <td>0</td>
      <td>POINT (139.68543 35.7924)</td>
    </tr>
    <tr>
      <th>4</th>
      <td>139.648182</td>
      <td>35.803057</td>
      <td>2025-05-12 11:50:00</td>
      <td>126</td>
      <td>143</td>
      <td>83</td>
      <td>43</td>
      <td>0</td>
      <td>91</td>
      <td>52</td>
      <td>0</td>
      <td>POINT (139.64818 35.80306)</td>
    </tr>
    <tr>
      <th>...</th>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
    </tr>
    <tr>
      <th>5737</th>
      <td>139.613062</td>
      <td>35.607644</td>
      <td>2025-05-12 11:10:00</td>
      <td>155</td>
      <td>136</td>
      <td>118</td>
      <td>32</td>
      <td>5</td>
      <td>103</td>
      <td>28</td>
      <td>5</td>
      <td>POINT (139.61306 35.60764)</td>
    </tr>
    <tr>
      <th>5738</th>
      <td>139.696999</td>
      <td>35.748881</td>
      <td>2025-05-12 11:10:00</td>
      <td>92</td>
      <td>78</td>
      <td>75</td>
      <td>13</td>
      <td>4</td>
      <td>50</td>
      <td>15</td>
      <td>13</td>
      <td>POINT (139.697 35.74888)</td>
    </tr>
    <tr>
      <th>5739</th>
      <td>139.683590</td>
      <td>35.757753</td>
      <td>2025-05-12 11:10:00</td>
      <td>118</td>
      <td>112</td>
      <td>95</td>
      <td>20</td>
      <td>3</td>
      <td>86</td>
      <td>22</td>
      <td>4</td>
      <td>POINT (139.68359 35.75775)</td>
    </tr>
    <tr>
      <th>5740</th>
      <td>139.900546</td>
      <td>35.648160</td>
      <td>2025-05-12 11:10:00</td>
      <td>117</td>
      <td>131</td>
      <td>53</td>
      <td>54</td>
      <td>10</td>
      <td>47</td>
      <td>73</td>
      <td>11</td>
      <td>POINT (139.90055 35.64816)</td>
    </tr>
    <tr>
      <th>5741</th>
      <td>139.823846</td>
      <td>35.646721</td>
      <td>2025-05-12 11:10:00</td>
      <td>44</td>
      <td>40</td>
      <td>20</td>
      <td>23</td>
      <td>1</td>
      <td>13</td>
      <td>23</td>
      <td>4</td>
      <td>POINT (139.82385 35.64672)</td>
    </tr>
  </tbody>
</table>
<p>5742 rows × 12 columns</p>
</div>




```python
df.to_csv('traffic_data.csv', index=False)
print(f"Data saved to traffic_data.csv with {len(df)} rows and {len(df.columns)} columns")
```

    Data saved to traffic_data.csv with 5742 rows and 12 columns
    

# 適当に可視化



```python
# ただの位置確認

figure()
subplot(111, aspect="equal")
plot(df["lon"], df["lat"], "o")
```


![n01_traffic_volume_api_14_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3849254/57478718-512f-42c2-bb86-c964cf1720af.png)

    



```python
# OSMと合わせて平均交通量

import contextily as ctx
import geopandas as gpd
from shapely.geometry import box
from pyproj import Transformer

# 緯度経度のbboxを指定
min_lon, min_lat, max_lon, max_lat = min_x, min_y, max_x, max_y 

# bboxからジオメトリを作成
bbox_geom = box(min_lon, min_lat, max_lon, max_lat)

# GeoDataFrameに変換（WGS84座標系）
gdf = gpd.GeoDataFrame(geometry=[bbox_geom], crs="EPSG:4326")

# Web Mercator座標系に変換（OSMタイルと合わせるため）
gdf = gdf.to_crs(epsg=3857)

# 交通データ
key = "traffic_up"
df_mean = df.groupby(["lon", "lat"], as_index=False)[key].mean()
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
df_mean["x"], df_mean["y"] = transformer.transform(df_mean["lon"].values, df_mean["lat"].values)

# プロット
fig, ax = subplots(figsize=(16, 10))
gdf.plot(ax=ax, alpha=0.5, edgecolor='red', facecolor='none')

traffic_scat = ax.scatter(df_mean["x"], df_mean["y"], s=200, c=df_mean[key], cmap="jet")

# カラーバーを追加
cbar = fig.colorbar(traffic_scat, ax=ax)
cbar.set_label('平均5分間交通量')

# contextily でOSMタイルを追加
ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik)

# 軸ラベルなどを設定
ax.set_axis_off()
tight_layout()
show()
```


![n01_traffic_volume_api_15_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3849254/71ec40ff-0b1a-4580-8ff4-47a3d62d407b.png)

    



```python
# 時系列プロット

figure()
plot(df["datetime"], df["traffic_up"], "b.", alpha=0.1)
xlim([time_code_from_obj, time_code_to_obj])
ylabel("5分間交通量（上り）")
xlabel("日時")
figure()
plot(df["datetime"], df["traffic_down"], "r.", alpha=0.1)
xlim([time_code_from_obj, time_code_to_obj])
ylabel("5分間交通量（下り）")
xlabel("日時")
show()
```

![n01_traffic_volume_api_16_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3849254/e814ba57-9fbc-42e9-8302-2429b8c1b50a.png)

![n01_traffic_volume_api_16_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3849254/c3cbaabc-4207-45ec-b7c5-162b115f7a6d.png)


↑上りと下りで明らかにパターンが違うのがおもしろい


```python
#下の3次元プロットをインタラクティブに動かす場合
#%matplotlib qt
```


```python
# 3次元時空間プロット

from mpl_toolkits.mplot3d import Axes3D

import matplotlib.pyplot as plt

# Create a 3D figure
fig = plt.figure(figsize=(14, 10))
ax = fig.add_subplot(111, projection='3d')

# Convert datetime to hours from midnight for better visualization
hours = [(dt - time_code_from_obj).total_seconds()/3600 for dt in df['datetime']]

# Create the scatter plot
scatter = ax.scatter(df['lon'], df['lat'], hours, 
                    c=df['traffic_up'], 
                    cmap='jet', 
                    s=30)

# Add labels and title
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.set_zlabel('Hours (from start time)')
ax.set_title('Traffic Volume (Up Direction) by Location and Time')

ax.set_zlim([0, (time_code_to_obj-time_code_from_obj).total_seconds()/3600])

# Add a color bar
cbar = plt.colorbar(scatter)
cbar.set_label('Traffic Volume (Up Direction)')

# Adjust the view angle for better visualization
ax.view_init(elev=35, azim=225)

plt.tight_layout()
plt.show()
```

![n01_traffic_volume_api_19_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3849254/ffec4948-7aa3-4292-8259-7e11526c9f31.png)


# データ出典・ライセンス

データの出典は以下です：交通量 API（国土交通省）機能による交通量(参考値)および国土交通省 API 機能による交通量(参考値)を加工して作成．交通量 API 機能を使用していますが、内容は国土交通省によって保証されたものではありません。

本コードのライセンスはパブリックドメイン（CC0）です．