Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

鉄道系各移動手段を分類するモデルを作ってみる

Last updated at Posted at 2024-10-28

1.前置き

最近、データ分析について学びだしたので、練習としてタイトルのテーマに取り組んでみたいと思います。

全国各所には私鉄や地下鉄、路面電車、モノレールなど様々な鉄道系の移動手段があるわけですが、そのなかで、それぞれの移動手段が採用されている要因として、都市の規模や移動距離の長さがあるんじゃないか?と思っていたわけです。
最近では宇都宮ライトレールの成功例も記事で目にしますが、ここにも何か宇都宮という都市の特徴と路面電車との親和性が隠れているのではないかと考えました。
これを習いたてのデータ分析でどこまで調査できるか試してみたいという思いがモチベーションとなっています。

それでは早速始めていきます。
本当に初心者なので、温かい目でご覧ください、、、。

2.ゴール設定

分析のテーマは上記の通り、日本全国の鉄道系移動手段の路線タイプ分類モデルの作成です。
加えてタイプ分類に影響する要素の分析も実施したいと思います。
行は各駅ごととし、説明変数には「その駅の存在する市区町村の人口」「その駅が含まれる路線の営業キロ」「その駅の運行本数」などを用います。

3.環境について

・Python 3.11.1
・pip 22.3.1

4.データ収集

今回扱うデータは冒頭に説明したゴールを達成するために「全国 鉄道 オープンデータ」あたりでgoogle検索してヒットした下記のデータをもとに進めています。

ファイル名 データ種別 データ説明 出典
station20240426free.csv 駅データ 日本全国の駅名のデータ(2024年4月26日更新版)。路線データとも紐づけられる。本分析ではこれが頭になる。 駅データ.jp(https://ekidata.jp/ )
line20240426.csv 路線データ 路線ごとのタイプを6つ(0:その他、1:新幹線、2:一般、3:地下鉄、4:市電・路面電車、5:モノレール・新交通)に分類しており、これが今回の目的変数となる。(2024年4月26日更新版) 駅データ.jp(https://ekidata.jp/ )
unkohonsu2024_eki_sjis.csv 事業者別・駅別発着本数データ2024 駅別に発着本数を集計したデータ。各駅について当該駅を通る各路線の発着本数データとその合計データがある。(2024年9月24日更新版) 全国鉄道運行本数データ公開ページ(https://gtfs-gis.jp/railway_honsu/ )
SSDSE-A-2024.csv SSDSE-市区町村(SSDSE-A) 市区町村別、多分野データ(1741市区町村×多分野125項目)。 独立行政法人 統計センター SSDSE-市区町村(https://www.nstac.go.jp/use/literacy/SSDSE/

5.分析・前処理

データの内容確認

各種データの内容を確認していきます。

import pandas as pd

df1 = pd.read_csv('station20240426free.csv', encoding='utf-8')
print(df1.head())
#   station_cd  station_g_cd station_name  station_name_k  station_name_r  \
#0     1110101       1110101           函館             NaN             NaN   
#1     1110102       1110102          五稜郭             NaN             NaN   
#2     1110103       1110103           桔梗             NaN             NaN   
#3     1110104       1110104          大中山             NaN             NaN   
#4     1110105       1110105           七飯             NaN             NaN  

#   line_cd  pref_cd      post           address         lon        lat  \
#0    11101        1  040-0063    北海道函館市若松町12-13  140.726413  41.773709   
#1    11101        1  041-0813           函館市亀田本町  140.733539  41.803557   
#2    11101        1  041-0801  北海道函館市桔梗3丁目41-36  140.722952  41.846457   
#3    11101        1  041-1121       亀田郡七飯町大字大中山  140.713580  41.864641   
#4    11101        1  041-1111         亀田郡七飯町字本町  140.688556  41.886971   

#     open_ymd   close_ymd  e_status   e_sort  
#0  1902-12-10  0000-00-00         0  1110101  
#1  0000-00-00  0000-00-00         0  1110102  
#2  1902-12-10  0000-00-00         0  1110103  
#3  0000-00-00  0000-00-00         0  1110104  
#4  0000-00-00  0000-00-00         0  1110105  

df2 = pd.read_csv('line20240426.csv', encoding='utf-8')
print(df2.head())
#   line_cd  company_cd line_name   line_name_k line_name_h line_color_c  \
#0     1001           3     中央新幹線   チュウオウシンカンセン       中央新幹線          NaN   
#1     1002           3    東海道新幹線  トウカイドウシンカンセン      東海道新幹線       0000FF   
#2     1003           4     山陽新幹線    サンヨウシンカンセン       山陽新幹線       0000FF   
#3     1004           2     東北新幹線    トウホクシンカンセン       東北新幹線       008000   
#4     1005           2     上越新幹線   ジョウエツシンカンセン       上越新幹線       008000   

#  line_color_t  line_type         lon        lat  zoom  e_status  e_sort  
#0           無し          1  137.493896  35.411438     8         1    1001  
#1          ブルー          1  137.721489  35.144122     7         0    1002  
#2          ブルー          1  133.147896  34.419338     7         0    1003  
#3         グリーン          1  140.763192  38.274267     7         0    1004  
#4         グリーン          1  139.121488  36.798565     8         0    1005 

df3 = pd.read_csv('unkohonsu2024_eki_sjis.csv', encoding='shift-jis')
print(df3.head())
#   ID  事業者コード   事業者名  事業者駅番号     駅名  路線数  両方向発着計  両方向着計  両方向発計  方向1発着計  ...  \
#2   3     101  JR北海道       3     桔梗    1      90     45     45      48  ...   
#3   4     101  JR北海道       4    大中山    1      90     45     45      48  ...   
#4   5     101  JR北海道       5     七飯    2      89     44     45      47  ...   
#5   6     101  JR北海道       6  新函館北斗    1      74     36     38      33  ...   
#6   7     101  JR北海道       7     仁山    1      42     21     21      16  ...   

#   路線11路線名  路線11駅順  路線11方向1  路線11方向1着数  路線11方向1発数  路線11方向2 路線11方向2着数  \
#2      NaN     NaN      NaN        NaN        NaN      NaN       NaN   
#3      NaN     NaN      NaN        NaN        NaN      NaN       NaN   
#4      NaN     NaN      NaN        NaN        NaN      NaN       NaN   
#5      NaN     NaN      NaN        NaN        NaN      NaN       NaN   
#6      NaN     NaN      NaN        NaN        NaN      NaN       NaN   

#   路線11方向2発数         緯度          経度  
#2        NaN  41.846350  140.722985  
#3        NaN  41.864780  140.713500  
#4        NaN  41.887010  140.688560  
#5        NaN  41.904843  140.648815  
#6        NaN  41.930035  140.635200  

#都道府県名のデータセット
df_pref = pd.read_csv('pref.csv', encoding='utf-8')
print(df_pref.head())
#   pref_cd pref_name
#0        1       北海道
#1        2       青森県
#2        3       岩手県
#3        4       宮城県
#4        5       秋田県

df4 = pd.read_csv(r'data_population\SSDSE-A-2024.csv', encoding='shift-jis', skiprows=2)
print(df4.head())
#    地域コード 都道府県 市区町村      総人口  総人口(男)   総人口(女)    日本人人口  日本人人口(男)  日本人人口(女)  \
#0  R01100  北海道  札幌市  1973395  918682  1054713  1933094    897727   1035367   
#1  R01202  北海道  函館市   251084  113965   137119   248208    112718    135490   
#2  R01203  北海道  小樽市   111299   50136    61163   109971     49441     60530   
#3  R01204  北海道  旭川市   329306  152108   177198   325287    150318    174969   
#4  R01205  北海道  室蘭市    82383   40390    41993    81658     39960     41698 

#   15歳未満人口  ...   小売店数  飲食店数  大型小売店数  一般病院数  一般診療所数  歯科診療所数   医師数  歯科医師数  \
#0   215366  ...  10370  7354     348    177    1413    1206  6978   2142   
#1    23560  ...   2163  1256      41     26     206     122   822    182   
#2     9169  ...   1097   616      17     11      79      78   338    105   
#3    34691  ...   2409  1503      67     33     227     170  1364    246   
#4     7769  ...    621   447      13      6      52      39   249     56  

#   薬剤師数  保育所等数(基本票)  
#0  5758         368  
#1   683          54  
#2   334          23  
#3   876          72  
#4   193          12  


データのマージ

各種データをマージして分析を行うためのデータセットを用意します。

#データのマージ
df1 = df1.drop_duplicates(subset='station_name', keep=False)
df_all = pd.merge(df1, df2, left_on='line_cd', right_on='line_cd', how='left')
df3 = df3.drop_duplicates(subset='駅名', keep=False)

df_all = pd.merge(df_all, df3, left_on='station_name', right_on='駅名', how='left')
df_all = pd.merge(df_all, df_pref, on='pref_cd', how='left')

さらに、市区町村名をキー列として市区町村別他分野データをマージしていきます。
住所文字列から正規表現によって市区町村の抽出を行う方法については下記の記事を参考にさせていただきました。

import re

# 市区町村の情報を抽出する関数
def extract_municipality(address):
    munici = re.sub(r'...??[都道府県]', '', address) if re.match(r'...??[都道府県]', address) else address
    gun = re.sub(r'.+?郡', '', munici) if re.match(r'.+?郡', munici) else munici
    match = re.search(r'\
        (?:旭川|伊達|石狩|盛岡|奥州|田村|南相馬|那須塩原|東村山|武蔵村山|羽村|十日町|上越|富山|野々市|大町|蒲郡|\
        四日市|姫路|大和郡山|廿日市|下松|岩国|田川|大村)市|.+?郡.+?[町村]|.+?[市区町村]', gun)
    return match.group(0) if match else None

# df4, df_allのそれぞれでキー列となる 'pref-municipality' 列を作成
df4['pref-municipality'] = df4['都道府県'] + df4['市区町村']
df_all['municipality'] = df_all['address'].apply(extract_municipality)
df_all['pref-municipality'] = df_all['pref_name'] + df_all['municipality']

#'pref-municipality'をキー列としてdf4とdf_allをマージ
df_all = pd.merge(df_all, df4, on='pref-municipality', how='left')

これでひとまず使いたいデータを一つのデータセットにマージする作業が完了しました。
(いくらかはうまく市区町村の取得ができずにちゃんとマージがなされていない行がありましたが、全体の2%程度でしたのでご勘弁を、、、)

用意したデータセットの大まかな傾向をつかむために、データセットの中から路線タイプに関係がありそうな項目として駅の両方向発着数、駅が存在する市区町村の総人口・可住地面積・従業者数(民営)・経常収支比率(市町村財政)、そして路線タイプを選択します。

df_focus = df_all[['両方向発着計','総人口','可住地面積','従業者数(民営)','経常収支比率(市町村財政)', 'line_type']]

上記の着目する項目のそれぞれの関係性を可視化させるため、試しにseabornのペアプロット図を作成してみました。(文字が小さくてすみません、、!)

import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib

#グラフの表示
sns.pairplot(df_focus, hue='line_type', plot_kws={'alpha':0.5}, size=5, palette='Set1')
plt.show()

9c7f3dc2-49a5-4f54-b5ec-566e321f6b78.png
まるっきりきれいに分離されているわけではないですが、
・地下鉄(3)は発着本数が多く、一般鉄道(2)は本数が少ない側に集中。路面電車(4)やモノレール(5)はその中間。
・地下鉄(3)従業者数が多い地域に採用されやすい。
といった特徴がありそうです。

欠損処理、データ不均衡の調整

上記の傾向予想を踏まえつつ、説明変数としては両方向発着計と各種市区町村データのうち数値データである項目を適用します。

#市区町村データのうち数値データ列のみ抽出
df4_int = df4.select_dtypes(include=['int64','float64'])

#説明変数:両方向発着計+各種市区町村データ(数値)、目的変数:路線種別 としてデータセット用意
df_focus2 = df_all[df4_int.columns.tolist()+['両方向発着計','line_type']]

欠損値の処理を行います。本データにおける欠損は、もともとな入力がなかったり、マージする際にうまくはまらずに欠損値となったものが主であり、欠損のメカニズムとしてはMCARであると考えますので、シンプルにリストワイズ削除にて対応します。

#欠損行を削除
df_focus2 = df_focus2.dropna()

モデル構築のため、先んじてテストデータと訓練データに分割したのちに、データ不均衡を確認します。

#説明変数、目的変数の選択
y2 = df_focus2['line_type']
X2 = df_focus2[df_focus2.columns[df_focus2.columns != 'line_type']]

#テストデータと訓練データに分割
from sklearn.model_selection import train_test_split

X_train2, X_test2, y_train2, y_test2 = train_test_split(X2, y2, test_size = 0.3, random_state = 71, stratify=y2)
y_train2.value_counts()

#2    4145
#4     247
#3     222
#5     112
#0       3
#Name: line_type, dtype: int64

路線タイプ0(その他)が非常に小さい値となっています。その他のデータは除外してモデル構築でもよいかと思いましたが、いったんそのまま残しておきます。
また、路線タイプ2(一般鉄道)とそれ以外3(地下鉄)、4(市電・路面電車)、5(モノレール・新交通)でもデータの偏りがあるため、対策を行います。

今回はSMOTE-ENNにて調整を行いました。

#SMOTE-ENN法を使った不均衡データの調整
import numpy as np
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import EditedNearestNeighbours
from imblearn.combine import SMOTEENN

np.random.seed(0)

sm_enn = SMOTEENN(smote=SMOTE(k_neighbors=2), enn=EditedNearestNeighbours(n_neighbors=3))
X_train_resampled2, y_train_resampled2 = sm_enn.fit_resample(X_train2, y_train2)
y_train_resampled2.value_counts()

#0    4145
#2    3784
#4    3737
#3    3386
#5    3058
#Name: line_type, dtype: int64

6.モデル構築・評価

いよいよモデルを構築します。
今回はランダムフォレストによる手法を選択しました。

#ランダムフォレストによる学習と分類予測
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier

# モデルの構築
model = RandomForestClassifier()

# モデルの学習
model.fit(X_train_resampled2, y_train_resampled2)

# テストデータの予測
y_pred2 = model.predict(X_test2)

# 正解率の算出
print(accuracy_score(y_test2, y_pred2))
#0.9383629191321499

正解率は0.94。思いのほか高く出ました。
さらにここからパラメータの変更や改良など試してみたいところですが、いったんこのまま考察に進めます。

7.分析結果考察

ランダムフォレストのfeature importanceを使って、各特徴量の重要度から今回の分析の考察をしていきます。

import matplotlib.pyplot as plt

labels = X_train_resampled2.columns
importances = model.feature_importances_

plt.figure(figsize = (10,25))
plt.barh(y = range(len(importances)), width = importances)
plt.yticks(ticks = range(len(labels)), labels = labels)
plt.show()

057deb2a-b8c2-49fe-bce1-7d3872333169.png

モデル構築前のペアプロット図でも触れたように発着本数の路線タイプへの影響度は特に大きそうです。
それ以外で影響度が大きそうな項目としては、以下が特徴的です。
・高等学校数
・実質公債費比率(市町村財政)
・従業者数(民営)(情報通信業)
・従業者数(民営)(漁業)
・出生数
上記の影響度の大きいものについて再びペアプロット図で可視化してみます。

#グラフの表示
matplotlib.rcParams['font.size'] = 18
sns.pairplot(df_focus2_forplot, hue='line_type', plot_kws={'alpha':1}, size=5, palette='Set1', )
plt.show()

db9bdd59-28f8-4f5e-ba3d-1184d2c0c5d0.png
出生数や高等学校数の数が多い地域のほうが地下鉄の採用が多く、少ない地域では一般鉄道が走ることが多い傾向が見られます。若い層の通学経路として地下鉄や路面電車が使われることが多いものと予想します。

対照的で興味深いのは「従業者数(民営)(情報通信業)」と「従業者数(民営)(漁業)」で情報通信業などは都市部に多く、通勤にも地下鉄などの利用客が多い一方で、漁業従事者は普段の生活や通勤には鉄道は使わず、長距離移動時などでJRのような一般鉄道を利用する場面が多いことが反映されていると考えます。

また、ペアプロット図を見ると、地域差の比較的少ない項目(従業者数(民営)(運輸業・郵便業)、従業者数(民営)(電気・ガス・熱供給・水道業)など)は路線タイプへの影響度も小さいことがわかります。
高等学校数の影響度に比べて小学校数、中学校数の影響度が小さいのも、高校に比べて義務教育である小学校・中学校のほうが数の地域差が小さいことが影響していると予想します。

8.まとめ

ここまで読んでいただきありがとうございました。

今回は分析までとしましたが、これをもとに開発予定地へ新たな交通機関を設ける場合にどの路線タイプが適しているかを予想するところまでできると面白いなと思っています。
その点では、本モデルに路線ごとの走行距離や、路線の経営状況のデータなども含めると、新しく鉄道系交通手段を敷設する際の推奨のタイプを予測してくれるようになるのではと思います。

今後も楽しみながら勉強していきたいと思います!

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?