DataFrameの2列の値からdictを作る

DataFrameの2列の値のうち、一方の列の値をKey、もう一方の列のValueとする辞書を作る方法の紹介です。
自分はよくやるのですが、意外に知られてないらしいことと、なぜこれが動くのか自分も十分に理解していなかったのでこの機会に調べました。

例えば次のようなデータフレームがあったとします。

id col1 col2
1 key1 value1
2 key2 value2
3 key3 value3
4 key4 value4
5 key5 value5

そして、このcol1の値をkey, col2の値をvalueとして、
{‘key1’: ‘value1’, ‘key2’: ‘value2’, ‘key3’: ‘value3’, ‘key4’: ‘value4’, ‘key5’: ‘value5’}
のようなdictを作りたいとします。

このような場合、僕は次のコードのようにデータフレームの該当の2列を抽出して、そのvaluesプロパティをdict関数に渡します。


import pandas as pd
# サンプルとなるデータフレームを作る
data = [[i, "key"+str(i), "value"+str(i)] for i in range(1, 6)]
df = pd.DataFrame(data, columns=["id", "col1", "col2"])

result_dict = dict(df[["col1", "col2"]].values)
print(result_dict)
# {'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4', 'key5': 'value5'}

注意ですが .values を忘れると次のように 列名:Seriesの辞書になってしまいます。


print(dict(df[["col1", "col2"]]))
"""
{'col1': 0    key1
1    key2
2    key3
3    key4
4    key5
Name: col1, dtype: object, 'col2': 0    value1
1    value2
2    value3
3    value4
4    value5
Name: col2, dtype: object}
"""

これも参考ですがよく見かけるのは次ような書き方。


result_dict = dict()
for i in range(len(df)):
    result_dict[df.iloc[i]["col1"]] = df.iloc[i]["col2"]

print(result_dict)
# {'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4', 'key5': 'value5'}

さて、話を戻してdict(df[["col1", "col2"]].values)がなぜうまく動くのかです。

改めてドキュメントを読んでみるとdictはiterableを引数に取ることができます。
class dict(iterable, **kwarg)
そして、
iterable のそれぞれの要素自身は、ちょうど 2 個のオブジェクトを持つイテラブルでなければなりません。
とのことです。

実際見てみると、df[[“col1”, “col2”]].valuesはその条件を満たすデータになっています。


print(df[["col1", "col2"]].values)
"""
[['key1' 'value1']
 ['key2' 'value2']
 ['key3' 'value3']
 ['key4' 'value4']
 ['key5' 'value5']]
"""

DataFrame側にdictに渡したら空気を読んでいい感じに変換される機能が実装されている、と勘違いしていたこともあるのですが、
dictの通常の挙動にマッチした動きだったようです。
for分で回すのに比べてかなりスマートに書けるので、最初に紹介した書き方は結構おすすめです。

2019年第3四半期によく読まれた記事

前回:2019年第2四半期によく読まれた記事

3ヶ月ごとの恒例?(まだ3回目ですが)の、この四半期に読まれた記事のランキングです.

7月から9月のベスト5はこちら。

  1. macにgraphvizをインストールする
  2. DataFrameを特定の列の値によって分割する
  3. pythonでARモデルの推定
  4. graphvizで決定木を可視化
  5. pandasでgroupbyした時に複数の集計関数を同時に適用する

前回から引き続きランクインしているものが目立ち、最近書いた記事が無いです。
そもそもここ最近書いていた数式が多めの記事はほぼ読まれていないように見えます。
もともと自分のメモとしてテキスト等を写したような記事も多く、あまり独自の内容がないからでしょうかね。
もっと良い情報を提供できるようにまだまだ精進していく必要があります。

7月から記事の更新を毎日から平日のみに絞り、ネタ切れ感はあるもののだいぶ無理のない運用になってきました。
$TeX$もよりスムーズにかけるようになってきたのも更新が楽になってきたポイントです。

一週間の訪問者も最近は600人を超えるようになってきました。(2Qの1.5倍)
わざわざ訪問してくださった方々の期待に応えられる記事になってるかと言うと、全く自信のないところなのですが、非常にありがたいです。

さっと読み返してみると、時間をかけて調査した内容をまとめた記事はまだ十分な量になっておらず、
ちょっとしたメモ書きのようなのが多くなってしまっていて、まだまだ理想とは遠いと感じています。
この辺りは最近の残業の増加とも関係していると思うので、仕事や生活を総合的に見直して改善していきたいです。

ユークリッド空間内の複数の点の中から、最も近い点を探す短いコード

実務上次の問題を解く必要が発生し、短くて効率のいい書き方を探したのでそのメモです。

n次元ユークリッド空間内に、m個の点が存在し、その座標がm*n行列で与えられる。
新たに1つの点の座標がn次元ベクトルで与えられた時、m個の点のうち最も近い点のインデックスを求める。

結論から言うと次のコードで求まります。
最後の行が今回作成したコードです。(それ以外はサンプルの点を作成しています。)
ここでは、 n=10, m=5です。


import numpy as np
# m個の点の座標を生成
X = (np.random.randn(5, 10) * 20).round(2)
print(X)
"""
[[-25.3   -3.49 -21.3   -1.98  10.99   2.77  -8.37  -9.01  20.05   7.91]
 [ 16.33  -9.    16.55 -10.27   6.56   7.81  16.03 -14.13  10.58 -32.39]
 [ 12.01 -11.79  34.79  -2.94 -12.24   0.71 -35.01 -17.92  20.6   33.73]
 [-11.47  30.95   4.92 -19.94  34.81   2.54 -11.73  21.91  24.16   4.87]
 [ 38.32  22.13  11.5   18.82  -8.29   4.02 -17.15  -5.26 -35.62 -19.43]]
"""
# 新しい点の座標
y = (np.random.randn(10) * 20).round(2)
print(y)
"""
[-23.09 -57.02  28.07  39.45 -22.52  -5.91   9.58  30.66 -11.82  21.5 ]
"""
# 最も近い点のインデックス
print(((X-y)**2).sum(axis=1).argmin())
"""
2
"""

今回は最も近い点を探せればよく、具体的な距離は必要としないので、
ユークリッド距離の定義にある平方根の計算は省略しました。

各点との距離のリストが必要な場合は次のコードで求めることができます。
((X-y)**2).sum(axis=1)**0.5
もしくは線形代数モジュールを使い次のように計算することもできます。
np.linalg.norm(X-y, axis=1)

掲題の問いについても、次のようにかけるのですが、
不要な平方根の計算が入るせいか少しだけ遅かったので、最初の書き方を採用しています。
np.linalg.norm(X-y, axis=1).argmin()

データフレームの背景に棒グラフを表示する

データフレームの書式設定シリーズの最後の記事です。
今回は背景にセルの値の大きさを表す棒グラフを表示します。
これ、地味に便利です。
ドキュメントはこちら

使い方は簡単で、df.style.bar()を呼び出すだけです。
オプションも色々指定子できますので、いくつか設定してやってみます。


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(-100, 100, size=(10, 4)),
        columns=["col0", "col1", "col2", "col3"]
    )

df.style.bar(
    align="mid",
    width=90,
    axis=None,
    color=['#d65f5f', '#5fba7d']
)

出力結果がこちら。

引数をいくつか説明しておきます。
まず align。 以下の3種類の値のどれかを取ります。

left
値が最小のセルの値が左端。デフォルト。
zero
ゼロがセルの中心
mid
最大値と最小値の平均か、もし正の数と負の数を両方含む場合は0が中心。

次に width は0〜100の値を取り、棒グラフの最大長がセルの何%を締めるかを表します。
colerは棒グラフの色です。文字列を一つ渡せば全てその色、配列にして2つ渡せばそれぞれ負の値の時と正の値の時の色です。
axis は最大最小値の基準が列方向(0)、行方向(1)、テーブル全体(None)のどれかを表します。
例では使っていませんが、vmin, vmax で最小値、最大値を指定することもできます。
外れ値があるような時は便利です。

データフレームの書式設定に組込書式を使う

データフレームを表示するときにCSSで書式設定する方法(apply,applymapを使う)を紹介してきましたが、
最初の方で少し書いた通り、これらの方法を使わなくてもよく使う書式はあらかじめ関数が用意されています。

ドキュメント: Builtin styles

非常によく使う、最大値最小値やNullの強調には、それぞれ次の3つの関数が使えます。

  • df.style.highlight_null(null_color=’red’)
  • df.style.highlight_max(subset=None, color=’yellow’, axis=0)
  • df.style.highlight_min(subset=None, color=’yellow’, axis=0)

subsetは対象の列を指定でき、colorは背景色、axisは最大値や最小値を評価する軸方向を指定します。

また、値の大小を色の濃淡(もしくは色相など)で、表現するには、df.style.background_gradientが使えます。
引数は次の通り。だいたいイメージ通り動きます。
df.style.background_gradient(cmap=’PuBu’, low=0, high=0, axis=0, subset=None, text_color_threshold=0.408)

cmapに渡すのはMatplotlibのcolormapです。
具体的な名前と色はこちらを参照して選びます。
Choosing Colormaps in Matplotlib

そして最後に、値によらず、データフレーム全体の書式を一括変更する時は、
set_propertiesを使います。
ドキュメントに例が載っていますが、これだけ少し引数の渡し方が独特なので注意です。
サンプル:


df.style.set_properties(**{'background-color': 'black',
                           'color': 'lawngreen',
                           'border-color': 'white'})

データフレームの書式を列や行ごとに設定する

前回の記事でデータフレームの書式設定をセル単位で設定する方法を紹介しました。
個々のセルの値だけで書式が決められうる場合はそれで十分なのですが、
実際の業務では列ごとの最大値や最小値などを目立たせたい場合がよくあります。
この場合、他のセルも参照しなければ書式を決められません。(厳密に言えば別途変数か何かを定義して実装できますが面倒です。)

このような時は、style.applymapの代わりに、style.applyを使います。
そして、渡す関数は、セルの値ではなく、DataFrameの行や列、つまりSeriesを受け取り、
CSS書式の配列を返す関数です。
引数axisに0(既定)を渡すと列ごと、1を渡すと行ごとに書式を設定できます。

試しに、最大値に色を塗ってみます。


def max_style(values):
    max_value = max(values)
    styles = [
        "background-color: yellow" if value == max_value else ""
        for value in values
    ]
    return styles


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(0, 100, size=(5, 3)),
        columns=["col0", "col1", "col2"]
    )

# 各列の最大値に着色 (axis=0 は省略可能)
print(df.style.apply(max_style).render())
col0 col1 col2
0 9 5 47
1 6 99 17
2 46 11 63
3 27 92 54
4 6 35 15


# 各行の最大値に着色
print(df.style.apply(max_style, axis=1).render())
col0 col1 col2
0 9 5 47
1 6 99 17
2 46 11 63
3 27 92 54
4 6 35 15

キャプチャ貼るのが面倒だったので、
結果はrender()で貼りました。

notebookでデータフレームを表示するときにセルの書式を設定する

pandasのデータフレームの値をjupyter notebookで確認するとき、
エクセルの条件付き書式のようにセルの値によって色を塗ったりするとわかりやすくなることが多くあります。

ネットで少し探せば、すぐにコードが出てくるのでよく理解せずに background_gradient などを使っていましたが、
先日のPyConで、@komo_frさんのセッション、pandasのStyling機能で強化するJupyter実験レポートを聞いて、ちゃんと体系立てて覚えて使おうというモチベーションが湧いてきたので、ドキュメントを読み始めました。

先述の background_gradient とか、 highlight_null とか 便利関数が用意されているのですが、
その前に基本から紹介していこうと思います。

今回は、単純にセルの値によって書式を指定する Styler.applymapです。
ドキュメントはここ

「データフレームの値を引数として受け取り、セルに設定したいCSS文字列を返す関数」をapplymapに渡すことで、
DataFrameの書式を設定します。

CSSっぽいな、というのは前々から感じてたのですが、CSSそのものだったんですね。
(CSSとよく似た独自構文を覚えなきゃ使えないのかと思ってました。)
ドキュメントにもそのまま「スタイル設定は、CSSを使用して行われます。」と書いてあるのでちゃんと読んでおけばよかったです。
The styling is accomplished using CSS.

では早速ですが、適当なデータフレームを作ってみて、値が入ってないセル、一定値より小さいセル、
その他のセルで書式を変えて表示してみました。


def cell_style(value):
    if value != value:
        return "background-color: gray; color: white"
    if value <= 40:
        return "background-color: yellow; font-weight: bold"
    else:
        return ""


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(0, 100, size=(5, 3)),
        columns=["col0", "col1", "col2"]
    )
df.loc[3, "col0"] = None
df.loc[1, "col2"] = None
df.style.applymap(cell_style)

jupyter notebookで実行したときに表示されるのがこちら。

また、.render()を使ってHTML出力もできます。
スタイルが思ったように適用されてないように感じたら、これを使って確認すると良いそうです。


print(df.style.applymap(cell_style).render())

実行して出力されたHTMLを記事中にそのまま貼り付けたのがこちらです。便利ですね。

col0 col1 col2
0 52 35 48
1 81 18 nan
2 37 80 33
3 nan 80 72
4 91 4 63

Type Hintで引数と戻り値の型を注記する

Python 3.5 から実装されている機能で、関数を定義するときに引数や戻り値の型を注記(アノテーション)する
Type Hint という機能があります。
ドキュメント 

次の例のように、引数の後ろには「:」をつけて型を書き、戻り値は行末の「:」の前に「->」を付けて型を書きます。
このように定義しておくと、help関数などでその関数が想定しているデータ型を確認できます。


def add_sample(x: int, y: float) -> float:
    return x + y


help(add_sample)
"""
Help on function add_sample in module __main__:

add_sample(x:int, y:float) -> float
"""

注意としては、あくまでもこれは注記で、本当にその型しか受け付けなくなったり、その方の戻り値を返すことを保証したりしないことです。
サンプルの例で言えば、float同士を受け取っても普通に計算しますし、文字列を渡せば結合します。


print(add_sample(2.5, 3.7))
# 6.2
print(add_sample("Type ", "Hint"))
# Type Hint

あくまでも可読性のための機能ですが、
便利に使える場面は多そうなので今後積極的に使っていこうと思います。


個人的な話になりますが、エンジニア?としてのキャリアの初期にJavaやExcel VBAばかり触っていた影響か、
実は静的型付け言語のほうが好きだったりします。(Pythonは動的型付け)
Python自体はかなり気に入っているので別に良いのですが。

SciPyを使って特定の確率分布にしたがう乱数を生成する

ここまでの数回の記事でいろいろな方法で特定の確率分布に従う乱数を得る方法を紹介してきましたが、
SciPyで生成する方法についてきちんと紹介してないことに気づいたので書いておきます。
numpyについてはこちらで書いてます。

といってもこれまでの実験中で使っている通り、SciPyのstatsモジュールに定義されている各確率分布ごとに、
rvsという関数があるのでそれを使うだけです。
確率分布が連続であっても、離散であっても同じ名前です。

ドキュメント:
(連続の例)正規分布の場合 scipy.stats.norm
(離散の例)二項分布の場合 scipy.stats.binom

最近の記事でも一様分布からのサンプリングで使いまくってるでほぼ説明不要なのですが、
以下の例のように各確率分布に従う乱数を得ることができます。


from scipy.stats import norm
from scipy.stats import binom
print(norm.rvs(loc=2, scale=5, size=5))
# [-1.46417053 -2.76659505  0.80006028  4.83473226  4.05597588]
print(binom.rvs(n=20, p=0.3, size=10))
# [9 7 7 5 9 5 8 4 9 8]

rvs ってなんの略だろう? 特にsは何かということが気になって調べていたのですが、
今の所、明確な答えは見つけられていません。(なんの略語かわからないと覚えにくい。)

sampling かな? と思っていたこともあるのですが、GitHubでソースを見ると _rvs_sampling ってのも登場するので違いそう。
チュートリアルの中に、
random variables (RVs)という記載があるので、random variablesの略である可能性が一番高いかなと思います。

PyConJP 2019に参加しました

9月16日と17日の二日間、大田区産業プラザPiOで開催されたPyConJP 2019に参加してきました。
昨年の2018も参加したのでこれで2回連続の参加です。

今年も非常に面白いセッションがたくさんあり、多くの学びがあった2日間でした。
ありがたいことに、connpassの資料ページや、
公式サイトのタイムテーブルページに発表資料をまとめていただいていて、
時間の被り等で聞けなかった発表の資料もすごい手軽に確認できるようになっています。

ちなみに僕は以下の講演を聞きました。

1日目

基調講演 Why Python is Eating the World
PythonとAutoML
機械学習におけるハイパーパラメータ最適化の理論と実践
Dashとオープンデータでインタラクティブに日本経済を可視化する
Pythonを使ったAPIサーバー開発を始める際に整備したCIとテスト機構
pandasのStyling機能で強化するJupyter実験レポート
LT

2日目

基調講演 Pythonで切り開く新しい農業
Pythonで始めてみよう関数型プログラミング
婚活・恋活領域におけるPythonを使ったマッチング最適化
知ろう!使おう!HDF5ファイル!
Anaconda環境運用TIPS 〜Anacondaの環境構築について知る・質問に答えられるようになる〜
チームメイトのためにdocstringを書こう
LT

2日間通して、今までなんとなくやっていたことの詳細を知れたり、
いつか試したいなと思っていたライブラリをいよいよ触ってみようというモチベーションが上がったり、
全く知らなかった手法を知れたりと本当に参考になる話がたくさんありました。

とりあえず、手軽なところからになると思いますが順次試していって、このブログでも紹介していこうと思います。
また、聞けなかったセッションの資料も順次確認していきます。

イベントを通してですが、昨年と比べて、ちょっとした些細なところにも多くの改善の工夫がされていて、
運営の皆さんのより良いイベントにしていこうという熱意を感じる2日間でした。
1000人を超える人が集まるイベントをスムーズに開催するだけでも相当大変なことだと思いますが、
このような素敵な場を提供していただけて、本当にありがたいなと思います。

来年は8月の開催とのことですが、また是非とも参加したいです。