くないらぼ

python でおかねをあならいず

python可視化ライブラリのAltairをオススメしてきた

第17回のはんなりpython にて Altair というpythonの可視化ライブラリの良さを発表しました!

hannaripython.hatenadiary.com

会場提供はマネーフォワードさんでした。 オフィスとは思えないおしゃんな感じで圧倒されました!
畳での発表は初でしたが、いつもより心理的な距離感は近くて良いなと思いました。


Altairの特徴と発表資料


Altair では、X座標、Y座標だけでなく、サイズや色も表現軸として扱うことができます。(日本語むずい)

さらにそれらの変更・交換も簡単なので、様々な切り口での可視化する際の可視化の試行錯誤がやりやすいです。

Exploratory Data Analysis ( EDA ) こと、 探索的データ分析 には良いのでは?と思っています。


資料はこちら



スライド内でのコードやリンク

Altair のインストール

pip install altair
pip install vega
pip install vega_datasets  #  公式サンプルを動かすのに必要

Altair を使う時のテンプレコード

import altair as alt

alt.renderers.enable('notebook') # jupyter notebook のみ必要。  jupyter labなら不要

alt.data_transformers.enable('default', max_rows=None) # dfのサイズ制限解除
#alt.data_transformers.enable('json') # ipynb が巨大化しないが、 json大量発生
alt.themes.enable('opaque') # for dark background color
from vega_datasets import data # サンプル用のデータ (一部ネット回線が必要)

import pandas as pd
import numpy as np

# 今回のスライド用
cars = data.cars()

# 代表として例5のソースコードだけ載せておきます
alt.Chart(data=cars).mark_point().encode(
    alt.X("Acceleration"),
    alt.Y("Horsepower"),
    alt.Color("Origin"),
    alt.Opacity("Miles_per_Gallon"),
)



その他スライド中のソースコードはこちらからならコピー可能です。(ややこしくてすみません)

docs.google.com


Altair関係のリンク

公式ドキュメント https://altair-viz.github.io/

公式サンプル集 https://altair-viz.github.io/gallery/index.html


インタラクティブでイケてるおすすめサンプル乱れ打ち

https://altair-viz.github.io/gallery/multiline_highlight.html

https://altair-viz.github.io/gallery/scatter_linked_brush.html

https://altair-viz.github.io/gallery/selection_histogram.html

https://altair-viz.github.io/gallery/select_detail.html

https://altair-viz.github.io/gallery/multiple_interactions.html

https://altair-viz.github.io/gallery/interactive_layered_crossfilter.html

https://altair-viz.github.io/gallery/us_population_pyramid_over_time.html


Altair入門 〜 インタラクティブなものを作るまでのチュートリアル動画

 作者のイケメン jake 氏 によるハンズオン。 非常にわかりやすい。
 なぜか休憩時間もずっと録画されているため3時間と長い。ゆるい。

www.youtube.com

 

かんそう

10分で終わらせるために高速でスライドをめくりました・・・w
資料を作る前は10分ならなんとかできるかな・・・と思っていましたが、 実際に作っていると伝えたいことが多くなってきて15分でも足りないなー、という印象でした。
とはいえ、発表時間10分というのは資料作りのハードルの低さとしてはとても良かったです。


来月のはんなりpythonはハンズオン回です。
この前やった pandas のハンズオンを私がまたやります。
今度はもうちょい範囲を絞ってゆっくりやれたらいいなと思います。

参考までに前回のハンズオンの記事はこちら〜

kunai-lab.hatenablog.jp

はんなりpythonで泥臭いpandasのハンズオンをやりました。対象データはなんと花粉ですよ奥さん。

くない(id:jdgthjdg) です。 2019/03/15(金) に、運営として参加している "はんなりpython" という京都のpython勉強会でpandasのハンズオンを発表しました。 開催してすぐに、同じ運営の (id:mazarimono) さんがブログを書いてくれました。

hannaripython.hatenadiary.com

本ハンズオンの会場提供のスプーキーズさん ありがとうございました!



ハンズオンでは、pandas のやってみた系ではあまり触れられない、 魔の部分 についてをメインに猛スピードでやっていきました笑

今話題の 花粉 に関するデータをいじっています。最後には、盆地は気温だけでなく、花粉も辛いのかもしれない という仮説が得られました笑

f:id:jdgthjdg:20190321133018p:plain
花粉とpandas

エラーのセルを残して試行錯誤の過程を残した

今回はちょっと工夫して、jupyter notebook のセルに試行錯誤の過程を残していきました。
セルでエラーが起きたら、そのセルはそのまま残して、下のセルでまた試すという感じです。 (実行していくと分かります。)
そのように書いていったら、ほんの数行の処理にも、何十ものセルが必要になり、5つ以上のノートブックに分割することになりました笑
逆に言うと、この何十もの試行錯誤のセルは初心者には見えないまま、最後の正解のセルだけが巷で紹介されているとも言えます。
そういう、行間を紹介するという意味では良い試みになったかなと思います。


資料について

こちらが資料です。dockerで環境ごと動かしてもいいですし、.ipynbファイルをローカル環境のjupyter系で開いて動作させても大丈夫です。 コードの一部で、ファイルを開くための相対パスの調整が必要かもしれません。

github.com

ハンズオンのオープニングで使った雑なスライドはこちらです。
こちらは内容は無いので、githubのレポジトリの方を見てください笑


資料のレベル(pandas的な意味で)について

内容としては、初心者のみを対象にしているわけではありません。
むしろ中級者以上の方でも知らない方が多そうな、便利な処理方法や、groupbyネタも織り交ぜ、自分のpandasエッセンスを詰め込んでいます。


公式ドキュメントのSEOが低い問題

発表では "公式documentを読もう!" とお伝えしたのですが、そのための公式のメソッドのリンクを貼ろうと検索すると、 googleの検索結果の下の方に出てきてしまいました。上位には個人のブログ記事が出てきます。
個人のブログもとても良いのですが、知らなかった使い方を知るには 公式ドキュメントが一番です。

実際に、今回のハンズオンで使った pandas.read_excel についても、 sheet_name という引数には、シート名だけでなく、正の数のindex値が使えることを公式docを見て初めて知りました。(sheet_nameっていう名前がよくないのは置いておいて)
おそらく個人のブログでは便利オプション全てについて触れられることは少ないでしょう。


ハンズオン中の質疑応答をscrapboxで非同期にできた

scrapbox に質問を書いてもらいました。

それらを発表側の区切りのいいタイミングで、ハンズオンを中断して回答することができました。
一方通行感が減ったので、個人的にはかなり良かったです。

そのときの scrapboxのリンクはこちら。
scrapbox.io


感想

多少なりともpandasを触ったことのある経験者が多いということで、このようなハードモードハンズオンをしてしまいましたが、経験者でもこれをあの時間で全部理解するのは大変だと思いますw

次にやるならもっとゆっくりに、何回かに分けてやりたいですね笑

あと、groupbyは説明できなかったので、これは専用に時間をとってでもやる価値があると思います。


Special thanks

今回のhands-onでは運営の id:masayuki14 さんによる、github レポジトリ と そのDocker環境に随分助けられました。 jupyter 環境の構築も、初めての方だと大変だと思います。 Windows 機ではうまくいかなかった方が多くいたので、そこが課題ですね。。。なにか良い方法あればいいですね。




はんなりpythonについて

はんなりpython では、運営を募集しています。

もはやpython関係なくても問題無いくらいゆるいですw
全体的に心理的安全性が高い勉強会だと思います。これは関西人ならではなのかも。

また、gitter というチャットで運営はわちゃわちゃ話をしていますので、適当に絡んだり希望するpythonネタなどを教えていただければと思います。
gitter.im


vim でマークダウン + PlantUMLのリアルタイムプレビュー環境を構築する

vim でマークダウン + PlantUMLのリアルタイムプレビュー環境を構築する

最近、README などの markdown や設計のための PlantUML をちょいちょい書いています。 書くのは全然良いのですが、vimで出力を確認するためのキーを叩くのが面倒だなあと思っていました。

解決策としては、 ブラウザ上のリアルタイムプレビュー系、ネイティブの専用ソフトがあるかと思いますが、前者はオフライン環境下で困りそうですし、後者はちょっと大げさかなと思ってしまいます。どちらにしても普段使うvimとの連携ができないのが辛いです。

じゃあvimでやれば?という話ですが、以前にもvim上でのリアルタイムプレビューを試行錯誤していました。 PlantUML も使えるとのことで確か previm を試したのですが、期待したように動かず諦めていました。

そんな中、 markdown-preview.nvim という vim でリアルタイムのプレビューを出しながら マークダウン+PlantUML を書くことができる便利なプラグインを導入できたのでその設定を書いてみます。

動作例 (gif)

とりあえず動作例を見てみましょう。

前回の記事で書いた gif動画 が大活躍です。


リアルタイムプレビューの開始, 同期スクロール, 終了

f:id:jdgthjdg:20190203170130g:plain


PlantUML のリアルタイムプレビュー

f:id:jdgthjdg:20190203170139g:plain


使ってみての感想

  • 良い点
    • ブラウザも勝手にオープンしてくれて楽
    • 一文字書く毎に瞬時に反映されるので待つストレスが無い(PlantUMLも結構速い)
    • スクロールが同期されるので長文でもブラウザ側のスクロールが不要
    • ブラウザを落としても、また同じタブを開けば同期は継続される
    • vim上の編集ファイルを閉じたらブラウザ上のタブも閉じてくれる


  • ちょっと困る点
    • 空行が必要なはずの時もうまくいってしまう。
    • PlantUML の出力結果が微妙に違う時がある。


以下導入手順

install "open-browser.vim"

Open-Browserというvimプラグインを導入します。
https://github.com/tyru/open-browser.vim

これは :OpenBrowser url で指定ブラウザを起動できるようにするものです。 設定は以下で、今回は google chrome を指定しました。 なお、vimプラグイン管理は NeoBundle を使っています。

NeoBundle "tyru/open-browser.vim"
let g:openbrowser_browser_commands = [ {'name': 'google-chrome-stable',  'args': ['{browser}', '{uri}']} ]


install "markdown-preview.nvim"

https://github.com/iamcco/markdown-preview.nvim

本記事の本丸です。BTCアドレスなんかも載せていて、なかなかやりおる作者さんです。

NeoBundle 'iamcco/markdown-preview.nvim'

注意として、インストール後にも自分でビルドする必要があります。
プラグインマネージャにもよりますが、以下のスクリプトを叩いて終わりでした。
なお、プラグインの設定はデフォルトのままです。

~/.vim/bundle/markdown-preview.nvim/app/install.sh


gif動画作成のためのメモ ( imagemagick と peek を添えて)

gifの動画(gif画像? gifアニメーション?)って画像のように気軽に見れて、動画のように情報が多く、見たくなるのでとても良いですよね。

ちょっと自分もやってみようとした時のメモです。 ubuntu 16.04 です。

ただ、皮肉にもこの記事に gif動画 はありませんw

imagemagick, peek のインストール

https://github.com/phw/peek

https://github.com/ImageMagick/ImageMagick

ubuntu 16.04

sudo add-apt-repository ppa:peek-developers/stable
sudo apt update
sudo apt install peek 
sudo apt install imagemagick

gif動画を作成 (peek)

GUIでシンプルに録画できます。

peek

f:id:jdgthjdg:20190129233116p:plain
peek動作画面

作成したgifの編集 (imagemagick)


目的
  • 見えてはいけないものが写ってしまった際の再録画の手間を省く(補間の一覧にチラっ・・・とか)
  • 冗長な部分のカット

公式のsample https://imagemagick.org/Usage/anim_opt/

gif 動画を画像にバラして出力する (imagemagick)

https://www.imagemagick.org/discourse-server/viewtopic.php?t=22597

mkdir pngs
convert original.gif -coalesce pngs/aaaaa_%05d.png


複数の画像から gif動画を生成する

# gif は -layers オプションで容量を節約した方が良い。この有無で15MBから500KBになった・・・
convert -delay 7 -loop 0 -layers optimize simgle_*.png editted_simple.gif


  • -delay は小さいほどコマ送りのスピード(再生速度)が速い
    • peek で録画したものなら7ぐらいで良い気がした


  • *.png の ところは、以下のような指定もできる


imagemagick 番外編

f:id:jdgthjdg:20190129230351g:plain
gen_by_imagemagick

アイキャッチ画像は以下のコマンドで生成しました・・・笑

$ convert -pointsize 72 label:gif_kunai gif_kunai.gif

pythonでよく使うオプションのメモ pandas / matplotlib / altair 編

evernoteにオレオレスニペットをまとめているんですが、量が果てしなく多くて、検索もちょっと手間なので、 レギュラーメンバーだけでもまとめておこうと思いました〜

matplotlib

決まり文句
import matplotlib.pylab as plt
%matplotlib inline
# or 
%matplotlib notebook 

# 日本語表示 豆腐対策
import matplotlib
from matplotlib.font_manager import FontProperties
font_path = '/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttf'
font_prop = FontProperties(fname=font_path)
matplotlib.rcParams['font.family'] = font_prop.get_name()
plot のせってい

さんこうURL https://matplotlib.org/users/customizing.html

# figsize 指定が面倒なのでまともな値に設定(初期値はいくらなんでも小さすぎるw)
plt.rcParams["figure.figsize"] = [18, 5]

plt.rcParams['xtick.labelsize'] = 12 # 横軸のフォントサイズ
plt.rcParams['ytick.labelsize'] = 12 # 縦軸のフォントサイズ

# matplotlib 2.x 以降でスタイルが好かないので1.x系に近づける設定たち
plt.rcParams['legend.frameon'] = False
plt.rcParams['axes.xmargin'] = 0.0 # 2.0 から余計なマージンが付いたのでキャンセル
plt.rcParams['axes.ymargin'] = 0.0 # 2.0 から余計なマージンが付いたのでキャンセル

# 1e2 1e-2 などの表記 ( scientific notation ) を抑制する: Falseなら抑制
plt.rcParams['axes.formatter.useoffset'] = False 

pandas

# 長い場合でも省略( A B ... Y Z  ってやつ) をさせない(下の折り返しとは無関係)
pd.set_option('max_columns',20) # この値を超えたら省略される
pd.set_option('max_rows',100)

# 横に長いdf の時に、折り返し(word wrap)を無しにする。 
pd.set_option('expand_frame_repr', False) # 折り返しをオフにできる

# float の表示精度変更 + おふざけ
pd.options.display.float_format ='${:,.3f} : いぇい'.format
pd.DataFrame([0.123456]) # --> $0.123 : いぇい

altair

import altair as alt
alt.renderers.enable('notebook')
alt.data_transformers.enable('default', max_rows=None)

alt.data_transformers.enable('json')
alt.themes.enable('opaque') # set color explicitly: good for non-white background color

# お試し用データセット
from vega_datasets import data

楽したい時のpythonコード

案外見ない気がする?、ゆるふわな書き方です。気づいたら随時追加するかも。

len() するとき・外すときに、括弧のためにカーソルを動かすのがめんどい → __len__() を直接呼ぶ

my_list = [1,2,3]

# ふつう
len( my_list )

# たいだ
my_list.__len__()


pandas とか numpy とかのスライス( [start:end] ) で使う変数をまとめてすっきりさせたい → slice を使う

同じスライスを複数回使う時は便利

my_np_arrray = np.array( list( range(30) ) )
start_day = 5
end_day = 19

# ふつう
my_np_arrray[ start_day : end_day ]

# sliceでまとめる
day_slicer = slice( start_day, end_day, None) # いらないところは None
my_np_arrray[ day_slicer ]

pandas.DataFrame のforループをゆるふわ△改良して300倍高速化する

主張:高速化は最後のおたのしみにしましょう。

無駄にいじいじして高速化させて満足し、結局その後はほとんど使わなかったなあ、、、が私の日常です。

えらい人も言っていますが、高速化なんてホント最後でいいんです・・・。 今まで何十回後悔したことか。。。(これからもまたやりますが。)

pythonであれば numba,cython,swig など、コンパイルしちゃう系の力(パワー)を借りることで、 全く同じアルゴリズムでもざっくり100倍単位で簡単に高速化できます。

しかし、このやり方ではpythonインタープリターなゆるふわ△言語の良さを(該当コード部分において)捨ててしまいます。結局C/C++に魂を売っているだけです。
私は魂を売ることそれ自体が好きなので良いですが、この行為はpythonの持つ別の面での高速性、つまり "生産性の高さ" を犠牲にしています。
コードの実行スピードが速くなっても生産性が下がれば、制作時間+コード実行時間は悪化していることがよくあります。私はほぼ確実に悪化できています。

こんなデメリットもあるため、高速化は生産性をほとんど犠牲にしないで済むような段階、つまりできるだけ最後の段階でやるとお得になるわけです。

この点において、numpyなどは実に良い粒度で魂を売ることに成功しているため、使用者は生産性をほとんど損なわずに高速化の恩恵を得ることができます。 このバランスはとても素晴らしく、魂を売る者ならこの塩梅を目指すと良いでしょう。

(とはいえ、千倍以上高速化されて、数時間の処理が数秒になったりすると高速化それ自体に喜んでしまい、高速化沼に引きずり込まれます^^)

本題 : pandas.DataFrame のforループをゆるふわ△改良して300倍高速化する

ここから本題ですが、上記のようにできるだけ魂を売らずに(== pythonの柔軟さを損なわずに)高速化するという、ゆるふわ△高速化をしてみます。
よく、pythonのfor文は遅いからダメだ、とかいう人がいますが、個人的にはその意見には同意しかねます。

for文は悪者ではなく、for文内に遅いコードを書くことが悪いのです。

forループだけを回した時間計測結果を下の方に書きましたが、pythonの他の処理に比べて遅いなんてことはないと思います。


さて、以下のような DataFrameがあったとします。

import pandas as pd
df = pd.DataFrame({"a":list(range(500000))})
df.shape # (500000,1)

変数 df は pandas.DataFrame で、 50万行です。
ここでは、非常にややこしい条件分岐があるため、for文を使うことを選んだケースを想定しています。
そして何かを計算するため、変数dfのa列を1行ずつ順番に取り出したくなったとします。 (バックテストとかね。)

なお、ここでの時間計測ですが、%%time とか書くのが面倒なので、 Jupyter の nbextensions の ExecuteTime ってやつで自動で計測させています。 若干オーバーヘッドがある気がして、数ms遅いかもしれません。
キャッシュに関しては無視しています。キャッシュが効いたからこそ速かった場合もあるかもしれませんが、一旦はそれも実力(?)として比較しています。


なお、使用したバージョンはこれでした。
python 3.6.3
pandas 0.22.0
計測はそんなに速くないノートでやっているので時間は遅めです。


(a) ナイーブというか公式的(?)な実装 -> 27秒
for idx,row in df.iterrows(): 
    row.a # 27秒

(a) row はpandas.Series型で、結構いかつめのオブジェクトです。
これがdf.iterrows() によって計50万回も生成されるので、遅いメモリ確保がたくさん発生してそうです。。。


(b)ちょっと意識高くしてみた -> 10秒
for idx in range(df.shape[0]): 
    df.a.iloc[idx] # 10秒

(b) Seriesの生成は避けることができましたが、 iloc の内部で起きているindexの探索やら例外処理が重そうです。(ソース見てないので想像)
また、50万回もあると、 df.a の裏で暗躍している、 __getattr__ もほんのちょっとだけ負荷になってるかもしれません。


(b') (b)をベースにして、 __getattr__ が殆ど作用しないようにした。 -> 6.4秒
df_a = df.a
for idx in range(df.shape[0]):
    df_a.iloc[idx] # 6.4秒

(b') あら!! 思ったよりも __getattr__ の影響が大きかったです。自分でも驚きました。
・・・というか、DataFrameの __getattr__ のオーバーライドがいかついんでしょうか。。。普通こんなに重くないでしょ・・・


ということで、ちょっと脱線してノーマルの __getattr__ を計測。 -> 75ms
class A:
    def __init__(self):
        self.a = 0

InsA = A()
for _ in range(500000):
    InsA.a # 75ms

やっぱりこんなもんでした! つまり pandas の DataFrameの __getattr__ 内の処理が重いだけでした。これは気をつけなきゃ・・・


(b'') さらに、 .iloc(これは普通の関数呼び出し)も一個にまとめてみた。 -> 6.2秒
df_a_iloc = df.a.iloc
for idx in range(df.shape[0]):
    df_a_iloc[idx] # 6.2秒

(b'') これは(b')とほとんど変わらず。 __getattr__ の負荷に比べると、関数呼び出しのコストは無視できるレベルですね。


(b''') (b)のdf.aの代わりに df["a"] を使う版 -> 8.9秒

ちょっと(b)との比較まで話が戻るのですが、 df.a のアクセスと、 df["a"] のアクセスはどっちが速いのでしょうか。

for idx in range(df.shape[0]): 
    df["a"].iloc[idx] # 8.9秒

(b''') __getitem__でのアクセス(df["a"]) のが __getattr__ (df.a (b)にて10秒だった )よりもちょっと速いんですね。へー。


(c) pandasを捨て、numpyに逃げた -> 2.5秒
for idx in range(df.shape[0]):
    df.a.values[idx] # 2.5秒

(c) pandasをうたっていたくせにやっぱり numpyに逃げる好例ですね。
valuesでnumpy.arrayにしてからのindexingならほとんど生アクセスに近いので速いです。


(c') (c)をベースにして例の __getattr__地獄を回避した -> 0.45秒(448ms)
df_a = df.a
for idx in range(df.shape[0]):
    df_a.values[idx] # 0.45秒 (448ms)


(c'') (c')からさらに .values 分の __getattr__ も削減 -> 0.09秒(90ms) ←さいしゅうけっか
a = df.a.values
for idx in range(df.shape[0]):
    a[idx] # 0.09秒( 90ms)


(参考) 50万回の空ループ -> 0.03秒(35ms)

ちなみに空ループが以下のように35msでしたので、これがゆるふわ△高速化の限界値でしょう。
( values[ ] の __getitem__ や take も試したのですが、前者は 120ms, 後者は 350ms と遅かったです。なんでだろう。どんな実装になっているのでしょうか。)

for _ in range(500000):
    pass # 0.03秒( 35ms)



まとめ

  • numbaやcythonを新たに使わずに、 27000ms(27秒)→90ms と、 300倍に高速化できました。
  • for文は悪くないよ、for文の中で重い処理を書くことが悪いんだよ。


最後に注意
pandasでもそうですが、できるだけfor文を使わない書き方で済ませるのが良いです。(基本的には apply とか groupby を使いましょう)