3
7

More than 3 years have passed since last update.

Googleロケーション履歴を使って整形(pandas)と地図上に表示(folium)させることを考える[初心者のPython3]

Last updated at Posted at 2019-08-24

はじめに

Googleでロケーション履歴を見ても、まとめて通った経路を表示させたりはできない。
簡単に自分の通った道を表示させたい。
QGISとかをわざわざ起動させるのも面倒なので
”folium”を使ってhtml形式のmapにしてしまうことを考える。
そうすればこんな感じに表示できる
2.png

出来るだけ使うパッケージやモジュールを減らして、初心者(つまり私)でも中身が分かりやすいように心がけていますので
ぜひ、一度書いてみてください。

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ファイルにしたら出力までできそうです。

それがこちらです。

seikei.py
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 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:

みたいな条件を付けることが考えられます。

seikei.py
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の数字を増やすことにした。

seikei.py
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)まで

また、二次利用などなどは自由で良いですが、ご連絡いただければ私が喜びます。
(無いとは思いますが商業利用はダメです。)


3
7
1

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
3
7