Google Chart Tools / Image Chartsで時系列折れ線グラフを作ってみる

2011年6月6日月曜日

さきのエントリに続いて脱デ部で使ってるもののお話。

やっぱり、計測する度にどの程度目標達成したかを視覚的に掴みたいところ。というわけで、

Google Chart Tools / Image Charts (aka Chart API)を使って適当な折れ線グラフを出力してみる。基本的にカンマ区切りでデータを渡してやればあとは適当にやってくれるのでとても楽。Chart Wizardでインタラクティブに項目の調整なども出来るし。

ということで
こんな感じのグラフがさっくり作れた。けど、このまま使うと「データがある分だけ等間隔に表示」ということになり、時間軸を正しく反映はしない。とはいえ今回のサービスって「毎日あるタイミングで体重を測定する」ってのを想定してるので、実用上そんなに問題は無いはず。と思ってたところ突っ込みを食らったので少々真面目に考えることに。

やること
・指定した時間レンジ内での変化を平滑化する
・指定した時間レンジよりもデータ量が少なければ空データを補完する

ここで、「過去1週間分を表示するけど未参加時期の分は表示しない」という仕様にする場合は一旦出力用のリストの各要素を'0'で初期化しておけば後が楽そう。
時間レンジの切り方には色々あると思うけれど今回は「最新の計測日時から24時間ずつ逆にたどっていき、その中で得られたデータについては平均値を取る」ということにした。極端に平滑化されたグラフが欲しいわけじゃないので移動平均とかは取らない。
Pythonのコードだとこんな感じ。
def getDailyHistoricalData(self, periodsToBack):
        info = self._fetchWeightLog()
        normalizeFor = 86400
        info.sort(key=lambda x:x['ts'], reverse=True)
        lastBoundary = info[0]['ts'] - normalizeFor
        avgList = list()
        cnt = 0
        sum = 0
        for v in info:
            if lastBoundary < float(v['ts']):
                sum += float(v['wt'])
                cnt += 1
            else:
                avgList.append(('%.2f' % (sum / cnt)) if cnt != 0 else '0')
                lastBoundary -= normalizeFor
                cnt = 0
                sum = 0
                if pos == periodsToBack:
                    break
        # catch last one
        if cnt != 0:
            avgList.append(('%.2f' % (sum / cnt)) if cnt != 0 else '0')
        if len(avgList) < periodsToBack:
            avgList.extend(['0' for i in range(periodsToBack - len(avgList))])
        avgList.reverse()
        return avgList
最新データから逆に古いデータへ辿っていく形で、データが足りなければ最後のほうで追加してる。リスト長が固定なのでインデックス指定でいじってもいいのだけど、appendでペチペチやっていくのが楽だったので最後にreverseかけてる。 これで無事
一日でデータ投入しまくっても平均値のみ1日1回分反映されるようになった。けれど多分これは不具合ある。1日・2日と計測をサボった際、この実装だとその区間データは0になるはずなんだけど、どっちかというと補完処理してほしいだろうなぁ。

で、Google Image Chartsは http://chart.apis.google.com/chart?chs=220x110&cht=lc&chco=3072F3&chm=B,DDDDFF,0,0,0&chd=t:1,2,3,4 というようなURLでimgタグを貼るとグラフ描画してくれるよという程度。あとはChart Wizardのギャラリーとか見ながら頑張るのがいい。使える記法のリファレンスは結構長い。

Python初心者がGoogle App Engineでソーシャルダイエットサービス作るよ

先日お邪魔したGoogle I/O報告会 in 東京でApp Engineなネタを発表したりしましたが、実はApp Engineをちゃんと使ったことは無かったりします。以前にやったことはといえばアカウントを作って試しに静的なファイルをデプロイしてみて終了というなかなか悲しい感じで、きっと当時の私には必要なかったのだと思います。

が、Google I/Oでセッションに触れ、また今後betaを卒業していったり諸々の(全文検索含め)機能強化がされていくことを考えると、いくらPython 2.5という今からするとちょっぴり古い言語がメインであっても(Javaで書く気はあんまり無くて、せっかくApp Engine使うならPythonで書きたい)App Engine使いたい気持ちが高まってきた今日この頃。そんなところに丁度ネタ振りがあったので今回はApp Engineで書いてみようという次第。ちなみにPythonはそれなりに思い通りのものを書ける程度でPythonistaではない。このへんもどんどん晒しつつ勉強していきたいところ。

やり始めてから途中丸一週間停滞という、旬が大事なプロジェクトとしては割と致命的なアクシデントもあった(これは色々言い訳してもしょうがない)んですが、サービスをさっくりとApp Engine上で作ってみました。以下はそのメモです。

----
メモ:
今回、基盤としてはtwitterの情報を利用するのでOAuthベースの認証/認可といくつかの周辺機能で構成することになる。

目的はOAuthの仕組みを作ることでもなければtwitterのAPIを知ることでもないので、既に作成/公開されているライブラリをありがたく使う。
Google App Engineで手軽にOAuthアプリを作成!(Twitterとか!) - AppEngine-OAuth
これを使い、twitter IDでのログインまではさっくりと進んだ。ちなみにApp Engineへのデプロイは結構遅めなので、どんどんコードを書き換えながら進めるには不向き。今回はtwitterの連携アプリ設定で"localhost"を許可リストへ追加し、"localhost:8080"な環境から直接twitterでの認証が使えるようにして開発を効率化した(実装としては、あまりホスト名を見て処理切り分けるのが好きでないのでLOCAL_TESTINGというグローバル変数を用意)。これに伴いOAuth用のライブラリを一部拡張した。コードはgithubあたりに置くつもりなので必要な方はそちらから拾ってもらえばいいのだけど、やってることは認証時のリクエストパラメータに'oauth_callback'を指定してlocalhostへ戻ってこれるようにしただけ。

さて、ログインまでは出来たけれどセッション情報をどのように保持しよう。ちょっと調べてみた感じ、App Engine自体でセッションの仕組みは提供されてなくて、http://gaeutilities.appspot.com/sessionこのへんの実装を使うというのが割と定石っぽい。バックエンドにはDatastoreとMemcachedが使えるのだけど、Memcachedで利用出来る容量は公表されておらずHow much memory of Memcache is available to a Google App Engine account?あたりを読んだ感じでは、アプリでの利用方法やトラフィックに依るということでなかなか恐ろしい。ここで設計を最大限パフォーマンス寄りに倒す必要は現時点で無いので、(Memcachedに保存したほうが当然爆速なんだろうけど)ひとまず安全側に倒してDatastoreでの実装としておく。
Google App Engine Pythonでセッションらしきものを実装するを参考に、Cookieの読み書き部分+Datastoreで管理するようにした。

データ構造としては揮発性のセッションと不揮発性のユーザデータを基盤とする。
揮発性のもの:
・ユーザのログインCookie

不揮発性のもの:
・twitterのOAuthトークン
・ユーザの開始時体重、目標体重
・ユーザの日々の変化情報
・ランキング情報

RDBでのスキーマ設計と異なり、1:1なものはほどほどまとめて1つのデータモデルにする。この例外は容量がやたらデカいものを切り出すとかかな(lazy-loading相当のことをやる方法があるのか分からないため)。1:Nなものは、数量が読めて検索上困らないならJSONなりカンマ区切りなりで1カラムに集約しても良さそう。数が読めなかったり後から最新の一部のみ切りだして利用する必要があるなどの事情があれば大人しく別モデルにしておくべきだろうなぁ。M:Nなものは、一旦別モデルにしておいて集約出来そうなら後から集約するのが良さそう。RDBで言うところのJOINが必要になるケースを避けたほうがパフォーマンス的に有利なはずなのでそのように計らう。
セッションをモデルとして分割するか否かについて少々悩んだ結果、大した容量にならないことだしセッションキーもセッションデータ(これはJSONで)も永続ユーザデータ側モデルの中へ入れておくことにした。小さなサービスで、ユーザからの入力パターンがほとんどないのでデータを階層化したセッションハンドラとかは要らない。
日々の計測データも分量が知れているので同様に。けど、日々の計測データはApp Engine的な作法(コードはなるべくそのままでも多ユーザ・多データにスケールさせられるようにする)としては別モデルに切っておくのが多分正解。

さて、ここで少々ややこしいのが、twitterのOAuthトークン自体の寿命とユーザのログインCookieの寿命が必ずしも一致しないし、ユーザのログアウトがtwitterのOAuthトークン無効化を意味するわけでもないというところ。今回のサービスでは、ユーザに代わってチーム内での日々の順位をツイートする機能を実装するので、ユーザログアウト(これはkeep-logged-inで共有PCなどにおいて他の人が元ユーザに成り代わって操作するのを防ぐためのもの)後でもtwitterのOAuthトークンを維持するのが正しい実装。このへんはサービスがどのような機能/使い勝手を提供するかによって変わるとこ。


スキーマのアップデートとか後からきっと必要になるのだけど、このへんはユーザの新レコードを作成した際にきちんと処理しないと面倒なことになるかも。世代ごとにデフォルト値管理するとか結構めんどい。

まさかテンプレートでincludeが使えないなんて…部分render→結果を食わせて部分renderかぁ、このへんは半分気分の問題なので、ほどよくラップ出来ればあまり気にならないかも。

あと今のところほぼ気にしないで良いレベルだけど、OAuthベースの通信に必要なUrlFetchにはQuotaがかけられてる(回数で1日あたり657,084、容量で1日あたり4GB in/out)。このへんは必要に応じてサブアプリを登録してそいつとの間でメッセージをやり取り(当然これ自体がAPI消費するので集約性重要)することである程度回避が可能な感じ。
----
まとめというか感想というか:
・App Engineはなかなか幸せな環境っぽい
・少なくとも、ある程度の使い方は把握出来たので今後App Engine上でのアプリプロトタイプ開発は加速出来そう。というか、考えたものはどんどん出していけるようにしなきゃまずい
・今度はJavaも使ってみたい。Slim3ちょっと調べてみた感じ結構イケてる。Eclipse含めた環境としてのJavaは書いててそんなに苦痛無いしJava6対応してるし
----
今後勉強したりするnotes:
http://code.google.com/intl/en/appengine/docs/python/tools/webapp/requestclass.html
リクエスト送信されてくる内容はこのへんを参考にする。

・ライブラリ内でApp Engine特有な部分はurlfetchぐらいかな、httplib2と大差ない気がするので今度違いを調べる。というか、google.appengine.extの中身はひとまず全部見ておいたほうが後々よさそうなので見る
・Datastoreで不要になったデータをexpireさせる方法(多分タイムスタンプを用意しておいてcronで範囲指定削除)を用意する。データ容量的にも効率的にも必要
・Datastoreに所謂PKっぽいものを指定する方法、インデックスを作成する(自動作成されない場合)方法
・db.Modelからlazy-loading(一部カラムのデータだけ、fetch時に取得せず具体的コール時に取得する)の仕組みがあるか調べる