はじめに
Googleでロケーション履歴を見ても、まとめて通った経路を表示させたりはできない。
簡単に自分の通った道を表示させたい。
QGISとかをわざわざ起動させるのも面倒なので
”folium”を使ってhtml形式のmapにしてしまうことを考える。
そうすればこんな感じに表示できる
出来るだけ使うパッケージやモジュールを減らして、初心者(つまり私)でも中身が分かりやすいように心がけていますので
ぜひ、一度書いてみてください。
Version
- python3 == 3.7.2
- folium == 0.10.0
- pandas == 0.24.2
(インストール方法は他のサイトさんにまかせます。)
1.Googleロケーション履歴をダウンロードする
Googleアカウントの”自分のデータをダウンロード”「https://takeout.google.com/」にアクセス。
「ロケーション履歴」(JSON形式)にチェックを入れて
指示に従ってアーカイブを作成し、ダウンロードする。
(以下ではダウンロードして作業ディレクトリに”locate.json”として置いた)
JSON形式とは
Java Scriptの構文をベースにした形式。主にデータの受け渡しに(JavaScript以外の言語でも)使われる。データの受け渡しに特化しているため人にはかなり見にくい(というより人が見るものではない)。
2.コードを考える
2-1.CSVファイルにする
JSON形式のデータをインポート
PythonでJSON形式を扱うならやっぱりPandas。
import pandas as pd
でpandasをインポートしてきます。
JSONをpandasのデータフレームで扱うのは
df = pd.read_json('locate.json')
でOK。
dfの中身は
Index | locations |
---|---|
0 | {'timestampMs': '1410000000000', 'latitudeE7': 340000000, 'longitudeE7': 135000000, 'accuracy': 10} |
1 | {'timestampMs': '1410000010000', 'latitudeE7': 340010000, 'longitudeE7': 135001000, 'accuracy': 10} |
・・・ | ・・・・・・ |
という風になっている。(移動手段などがさらに付属したデータもある。)
- 'timestampMs'はシステム時刻(ミリ秒)
- 'latitudeE7'は小数点以下7桁を含む緯度
- 'longitudeE7'は小数点以下7桁を含む経度
- 'accuracy'は精度
をそれぞれ示している。
データの整形
時刻のデータを年月日+時間に
緯度経度は小数点を含んだ形式に
それぞれ書き換える。
まず、時刻のデータのフォーマットを変える。
pandasにもto_datetimeというモジュールがあるが、今回はpythonの標準モジュールdatetimeを用いる。
from datetime import datetime
例えば"Index"1番目の"locations"内の'timestampMs'を整形して変数timeとするなら
time = datetime.fromtimestamp(float(df.iat[1,0]['timestampMs'])/1000)
となる。ミリ秒のデータを秒に直すために1000で割っている。
同様に緯度を考える。
"Index"1番目の"locations"内の'latitudeE7'を整形して変数latとするなら
lat = float(df.iat[1,0]['latitudeE7'])/10000000
となる。経度も全く同様にできる。
これらをループで回して、新たなデータフレームd2を作ればきれいに整形ができそうだと思います(よね)。
d2をそのままcsvファイルにしたら出力までできそうです。
それがこちらです。
def seikei():
t =[]
l = []
l2 = []
df = pd.read_json('locate.json')
i = 0
while True:
try:
time = datetime.fromtimestamp(float(df.iat[i, 0]['timestampMs'])/1000)
lon = float(df.iat[i, 0]['longitudeE7'])/10000000
lat = float(df.iat[i, 0]['latitudeE7'])/10000000
t.append(time)
l.append(lon)
l2.append(lat)
except IndexError:
break
i+=1
d2 = pd.DataFrame({'date':t, 'lat':l2, 'lon':l})
d2['date'] = d2['date'].dt.strftime('%Y-%m-%d %H:%M:%S')
d2.to_csv('hist.csv')
if __name__=='__main__'
seikei()
i=0からiを1ずつ増やしてすべての'Index'に対して処理をしています。
while True
で条件文が真にして無限ループを作り、
try ~ except IndexError
でループを抜け出す(breakする)ルールを作っています。
また、処理が終わったものはappend
でリストでまとめています。
最後にリストをデータフレームに直し、csvファイルにする前にstr(文字列)に直しています。
(今回はデータの抜けなどがない前提で作っています。)
2-2.htmlファイルにする
foliumを使う
ここでは簡単に今回使う部分だけを説明する。
もっと詳しく知りたい方は公式サイトをチェック
(また追って投稿もします。)
folium公式Githubのページ |
---|
まずはインポート
import folium
最も簡単に指定した場所を中心?としたMapを保存するためには
m = folium.Map(location=[35.0, 135.0], zoom_start=5)
m.save('map1.html')
を実行します。
作られたファイル(ここではmap1.html)をクリックして開くとブラウザで地図が開きます。
下の例はただの図です。
線を引くのにはfolliumのモジュールPolyLineを使います。
使い方は次のようにlocationの引数を渡します。
m = folium.Map(location=[35.0, 135.0], zoom_start=5)
loc = [[35.0,135.0],[35.2,135.2]]
folium.PolyLine(locations=loc).add_to(m)
m.save('map1.html')
例えばlocに先ほどのデータフレームd2の'lat','lon'を渡せば大体の線は引けそうです。
上のloc =
の部分をloc = d2[['lat','lon']].values.tolist()
に変えて先ほどのseikei.py
の関数seikei()
の最後にコピペして一度実行してみてください。
場所によってよさげな場所と下の図のように変なところに伸びた線とができてしまうと思います。
大枠はここまででできていると思います。
ここからはいくつか条件を加えることでエラーのような線をなくしていきたいと思います。
2-3.仕上げ
まずは元のデータに付いていた精度を表す'accuracy'によって地図にプロットするかしないかを決めます。
例えば
accuracy = float(df.iat[i,0]['accuracy'])
if accuracy < 30:
みたいな条件を付けることが考えられます。
def seikei():
t =[]
l = []
l2 = []
df = pd.read_json('locate.json')
i = 0
while True:
try:
accuracy = float(df.iat[i,0]['accuracy'])
if accuracy < 30:
time = datetime.fromtimestamp(float(df.iat[i, 0]['timestampMs'])/1000)
lon = float(df.iat[i, 0]['longitudeE7'])/10000000
lat = float(df.iat[i, 0]['latitudeE7'])/10000000
t.append(time)
l.append(lon)
l2.append(lat)
except IndexError:
break
i+=1
d2 = pd.DataFrame({'date':t, 'lat':l2, 'lon':l})
d2['date'] = d2['date'].dt.strftime('%Y-%m-%d %H:%M:%S')
d2.to_csv('hist.csv')
m = folium.Map(location=[35.0, 135.0], zoom_start=5)
loc = d2[['lat','lon']].values.tolist()
folium.PolyLine(locations=loc).add_to(m)
m.save('map1.html')
if __name__=='__main__'
seikei()
また、GPSを切っていた期間やデータ同士が遠く離れている点を線で結ぶと長い直線となってしまうので
ある半径内(※実際には簡単のため度単位で処理をしているので注意が必要)にデータがないときは線で結ばないことにします。
線で結んでいくデータセットごとに異なった番号を振り識別をするようにします。
counterという変数で識別を行い、データフレームに組み込みます。
今回は一つ手前の緯度経度と今の緯度経度それぞれの差の二乗をとって、閾値以上(今回は0.005)ならcounterの数字を増やすことにした。
def seikei():
t =[]
l = []
l2 = []
df = pd.read_json('locate.json')
i = 0
count=[]
counter = 0
while True:
try:
accuracy = float(df.iat[i,0]['accuracy'])
if accuracy < 30:
time = datetime.fromtimestamp(float(df.iat[i, 0]['timestampMs'])/1000)
lon = float(df.iat[i, 0]['longitudeE7'])/10000000
lat = float(df.iat[i, 0]['latitudeE7'])/10000000
if i>0 and abs((l[-1]-lon)**2+(l2[-1]-lat)**2)>0.005:
counter+=1
t.append(time)
l.append(lon)
l2.append(lat)
except IndexError:
break
i+=1
d2 = pd.DataFrame({'date':t, 'lat':l2, 'lon':l,'numbering':count})
# d2['date'] = d2['date'].dt.strftime('%Y-%m-%d %H:%M:%S')
# d2.to_csv('hist.csv')
df_dict = {}
for name, group in d2.groupby('numbering'):
df_dict[name] = group
m = folium.Map(location=[35.0, 135.0], zoom_start=5)
for d in df_dict:
loc = df_dict[d][['lat','lon']].values.tolist()
folium.PolyLine(locations=loc).add_to(m)
m.save('map1.html')
if __name__=='__main__'
seikei()
このコード内の
for name, group in d2.groupby('numbering'):
df_dict[name] = group
については
分析ノート データサイエンティストのメモ書き
をかなりの参考(引用)とさせていただいた。
今までのコードだとかなりの時間がかかっていたので非常に助けになったコードです。
程よい分量のコードなので実際に動かしてみてくださいね。
コード更新2021/4/22
それでは、
質問やエラー報告などなどは
Twitter(@ohcelot)まで
また、二次利用などなどは自由で良いですが、ご連絡いただければ私が喜びます。
(無いとは思いますが商業利用はダメです。)