matplotlibでグラフのx軸とy軸のメモリの間隔(アスペクト)を揃える

matplotlibを使ってる方はご存知だと思いますが、matplotlibはグラフを綺麗に描写するためにx軸とy軸でそれぞれメモリの間隔をいい感じに調整してくれます。
この縦横の比率をアスペクト比と言うそうです。(Wikipedia: アスペクト比)

ほとんどの場合、自動的に調整してくれるのでただありがたいのですが、描写するものによってはこの比率を揃えたいことがあります。
そうしないと円を書きたかったのに楕円になったりします。
実はこのブログの過去の記事のサンプルコードの中で利用したことがあるのですが、
この間必要になったときに自分でこのブログ内から探せなかったので今回独立した記事にしました。

文章で説明していると分かりにくいので、とりあえず縦横の比率がそろってない例を出します。
シンプルにいつものアヤメのデータから2次元分だけ拝借して、散布図を書きました。


import matplotlib.pyplot as plt
from sklearn.datasets import load_iris

# データ取得。 2次元分だけ利用
X = load_iris().data[:, 2:]
fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1, title="アスペクト未指定")
ax.scatter(X[:, 0], X[:, 1])
plt.show()

見慣れた図ですが x軸における幅1 と y軸における幅1が全然違いますね。

この縦横の比率を揃えるには、 add_subplot で Axes オブジェクトを作るときに aspectで指定するか、
Axes オブジェクトの set_aspect 関数で指定します。
指定できる値は “auto”(これがデフォルト), “equal”(比率を揃える), 数値(指定した比率になる) の3パターンです。

個人的には add_subplot した時点でしていておく方が好きです。ただ、行が長くなるので set_aspect 使う方が pep8を守りやすいとも思ってます。

実際にやってみるとこうなります。


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1, aspect="equal", title="アスペクト equal")
ax.scatter(X[:, 0], X[:, 1])
plt.show()

比率が揃いましたね。
ただ今回の例だと、 “auto”の方が見やすいのでその点ちょっと失敗したと思いました。

set_aspect を使う時は、 anchor という引数を同時に渡すこともできます。
これは aspect の指定によって、グラフが実際より小さくなったときに、元の領域のどの位置に表示するかをしているものです。
‘C’が中心なのはいいとして、 ‘N’とか’SW’とかやけに分かりにくい値で指定しないといけないなと感じていたのですが、どうやらこれは東西南北をEWSNのアルファベットで表したもののようです。

自分は必要になったことがないのですが、 set_anchor と言う関数のドキュメントに説明がありますのでこちらもご参照ください。

WordCloudの文字の色を明示的に指定する

相変わらずWordCloudの話です。(今回くらいで一旦止めます。)

今回は文字の色を個別に指定します。
前回の記事のコードの抜粋が以下ですが、
colormap 引数で どのような色を使うのかはざっくりと指定できます。(実際の色は単語ごとにランダムに決まる)
これを、「この単語は何色」って具体的に指定しようというのが今回の記事です。


# TF-IDFで Word Cloud作成
wc_1 = WordCloud(
        font_path="/Library/Fonts/ipaexg.ttf",
        width=600,
        height=300,
        prefer_horizontal=1,
        background_color='white',
        include_numbers=True,
        colormap='tab20',
    ).generate_from_frequencies(tfidf_dict)

さて、方法ですが、WordCloudのインスタンスを作る時に、color_func って引数で、色を返す関数を渡すことで実装できます。
ドキュメントの該当ページから引用すると、
次のように word とか font_size とかを受け取る関数を用意しておけばいいようです。

color_funccallable, default=None
Callable with parameters word, font_size, position, orientation, font_path, random_state that returns a PIL color for each word. Overwrites “colormap”.

Using custom colors というページに例もあるので、参考にしながらやってみましょう。

さて、どうやって色をつけるかですが、今回は「品詞」で色を塗り分けることにしてみました。ただ、やってみたら名詞のシェアが高すぎてほぼ全体が同じ色になったので、名詞だけは品詞細分類1までみてわけます。

コードですが、前回の記事で準備したデータを使うので、 形態素分析済みで、TF-IDFも計算できているデータはすでにあるものとします。

まず、単語から品詞を返す関数を作ります。
注: 品詞は本当は文脈にも依存するので、形態素解析したときに取得して保存しておくべきものです。ただ、今回そこは本質でないので、一単語渡したらもう一回MeCabにかけて品詞を返す関数を作りました。
もともと1単語を渡す想定ですが、それがさらに複数の単語にわけられちゃったら1個目の品詞を返します。あまりいい関数ではないですね。
あと、処理の効率化のために関数自体はメモ化しておきます。


from functools import lru_cache


@lru_cache(maxsize=None)
def get_pos(word):
    parsed_lines = tagger.parse(word).split("\n")[:-2]
    features = [l.split('\t')[1] for l in parsed_lines]
    pos = [f.split(',')[0] for f in features]
    pos1 = [f.split(',')[1] for f in features]

    # 名詞の場合は、 品詞細分類1まで返す
    if pos[0] == "名詞":
        return f"{pos[0]}-{pos1[0]}"

    # 名詞以外の場合は 品詞のみ返す
    else:
        return pos[0]

(tagger などは前回の記事のコードの中でインスタンス化しているのでこれだけ実行しても動かないので注意してください)

さて、単語を品詞に変換する関数が得られたので、これを使って、単語に対して品詞に応じた色を戻す関数を作ります。


import matplotlib.cm as cm
import matplotlib.colors as mcolors

# 品詞ごとに整数値を返す辞書を作る
pos_color_index_dict = {}
# カラーマップ指定
cmap = cm.get_cmap("tab20")


# これが単語ごとに色を戻す関数
def pos_color_func(word, font_size, position, orientation, random_state=None,
                   **kwargs):

    # 品詞取得
    pos = get_pos(word)

    # 初登場の品詞の場合は辞書に追加
    if pos not in pos_color_index_dict:
        pos_color_index_dict[pos] = len(pos_color_index_dict)

    color_index = pos_color_index_dict[pos]

    # カラーマーップでrgbに変換
    rgb = cmap(color_index)
    return mcolors.rgb2hex(rgb)

**kwargs が吸収してくれるので、実は font_size とか position とか関数中で使わない変数は引数にも準備しなくていいのですが、
いちおうドキュメントの例を参考に近い形で書いてみました。

これを使って、ワードクラウドを作ります。


# TF-IDFで Word Cloud作成
wc = WordCloud(
        font_path="/Library/Fonts/ipaexg.ttf",
        width=600,
        height=300,
        color_func=pos_color_func,
        prefer_horizontal=1,
        background_color='white',
        include_numbers=True,
    ).generate_from_frequencies(tfidf_dict)
wc.to_image()

変わったのは color_func にさっき作った関数を渡しているのと、 colormap の指定がなくなりました。(指定しても上書きされるので無視されます)
そして出力がこちら。

同じ品詞のものが同色に塗られていますね。

どの品詞が何色なのか、凡例?というか色見本も作ってみました。
たまたま対象のテキストで登場した品詞だけ現れます。


fig = plt.figure(figsize=(6, 8), facecolor="w")
ax = fig.add_subplot(111)
ax.barh(
    range(len(pos_color_index_dict)),
    [5] * len(pos_color_index_dict),
    color=[mcolors.rgb2hex(cmap(i)) for i in range(len(pos_color_index_dict))]
)

ax.set_xticks([])
ax.set_yticks(range(len(pos_color_index_dict)))
ax.set_yticklabels(list(pos_color_index_dict.keys()))
plt.show()

単語の頻度データからWord Cloudを作成する方法

今回も Word Cloud の話です。
前回は見た目の設定を変える話でしたが、今回は読み込ませるデータの話になります。

さて、Word Cloudを作るとき、
generate (もしくは generate_from_text) 関数に、 テキストを渡し、その中の出現回数でサイズを決めました。

しかし実際には、出現回数ではなくもっと別の割合でサイズを調整したいことがあります。
例えば、TF-IDFで重みをつけたい場合とか、トピックモデルのトピック別出現確率のようなもともと割合で与えられたデータを使いたい場合などです。

このような時は “単語: 頻度” の辞書(dict)を作成し、
generate_from_frequencies (もしくは fit_words) に渡すと実行できます。
ドキュメントの API Reference には詳しい説明がないので、ギャラリーの Using frequency をみる方がおすすめです。

今回はサンプルとして、ライブドアニュースコーパスから適当に1記事選んで、通常の単語の出現回数(今までと同じ方法)と、
TF-IDFで重み付けした方法(generate_from_frequenciesを使う)でそれぞれ WordCloudを作ってみました。

今まで、単語を名詞動詞形容詞に絞ったり平仮名だけの単語は外したりしていましたが、
今回は差が分かりやすくなるようにするためにSTOPWORDなしで全単語を含めています。(コード中で該当処理をコメントアウトしました。)
「てにをは」系の頻出語が小さくなってSTOPWORD無しでも分かりやすくなる効果が出ているのが感じられると思います。


import re
import MeCab
import pandas as pd
import unicodedata
import matplotlib.pyplot as plt
from wordcloud import WordCloud
from sklearn.feature_extraction.text import TfidfVectorizer

# 分かち書きの中で使うオブジェクト生成
tagger = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
# ひらがなのみの文字列にマッチする正規表現
kana_re = re.compile("^[ぁ-ゖ]+$")


def mecab_tokenizer(text):
    # ユニコード正規化
    text = unicodedata.normalize("NFKC", text)
    # 分かち書き
    parsed_lines = tagger.parse(text).split("\n")[:-2]
    surfaces = [l.split('\t')[0] for l in parsed_lines]
    features = [l.split('\t')[1] for l in parsed_lines]
    # 原型を取得
    bases = [f.split(',')[6] for f in features]
    # 品詞を取得
    # pos = [f.split(',')[0] for f in features]
    # 各単語を原型に変換する
    token_list = [b if b != '*' else s for s, b in zip(surfaces, bases)]
    # 名詞,動詞,形容詞のみに絞り込み
    # target_pos = ["名詞", "動詞", "形容詞"]
    # token_list = [t for t, p in zip(token_list, pos) if (p in target_pos)]
    # ひらがなのみの単語を除く
    # token_list = [t for t in token_list if not kana_re.match(t)]
    # アルファベットを小文字に統一
    token_list = [t.lower() for t in token_list]
    # 半角スペースを挟んで結合する。
    result = " ".join(token_list)
    # 念のためもう一度ユニコード正規化
    result = unicodedata.normalize("NFKC", result)

    return result


# データ読み込み
df = pd.read_csv("./livedoor_news_corpus.csv")
# 形態素解析(全データ)
df["token"] = df.text.apply(mecab_tokenizer)

# サンプルとして用いるテキストを一つ選び、形態素解析する
text = df.iloc[1010].text
tokenized_text = mecab_tokenizer(text)

# tfidfモデルの作成と学習
tfidf_model = TfidfVectorizer(token_pattern='(?u)\\b\\w+\\b', norm=None)
tfidf_model.fit(df.token)

# 対象テキストをtf-idfデータに変換
tfidf_vec = tfidf_model.transform([tokenized_text]).toarray()[0]
# 単語: tf-idfの辞書にする。
tfidf_dict = dict(zip(tfidf_model.get_feature_names(), tfidf_vec))
# 値が正のkeyだけ残す
tfidf_dict = {k: v for k, v in tfidf_dict.items() if v > 0}

# 単語の出現頻度でWord Cloud作成
wc_0 = WordCloud(
        font_path="/Library/Fonts/ipaexg.ttf",
        width=600,
        height=300,
        prefer_horizontal=1,
        background_color='white',
        include_numbers=True,
        colormap='tab20',
        regexp=r"[\w']+",
    ).generate_from_text(tokenized_text)

# TF-IDFで Word Cloud作成
wc_1 = WordCloud(
        font_path="/Library/Fonts/ipaexg.ttf",
        width=600,
        height=300,
        prefer_horizontal=1,
        background_color='white',
        include_numbers=True,
        colormap='tab20',
    ).generate_from_frequencies(tfidf_dict)

# それぞれ可視化
plt.rcParams["font.family"] = "IPAexGothic"
fig = plt.figure(figsize=(12, 12), facecolor="w")
ax = fig.add_subplot(2, 1, 1, title="単語の出現回数で作成")
ax.imshow(wc_0)
ax.axis("off")
ax = fig.add_subplot(2, 1, 2, title="TF-IDFで作成")
ax.imshow(wc_1)
ax.axis("off")
plt.show()

出来上がった図がこちら。

明らかに下のやつの方がいいですね。

wordcloudの見た目を整える設定

最近、真面目にワードクラウドを作る機会がありましたので、久々に以前紹介したwordcloudライブラリを使いました。(並行してTableauでも作りました。)

前回紹介した記事では本当にざっくりとしか紹介していなかったのですが、今回そこそこ見た目を整える必要がありましたので、
細かいオプションの挙動を調べました。その内容をメモしておきます。
参考: Pythonでワードクラウドを作成する

なお、公式のドキュメントはこちらにあるので、英語に抵抗がなければそちらを読む方がおすすめです。サンプルも綺麗ですよ。
ドキュメント: wordcloud

今回は日本語のテキストを使います。
ライブドアニュースのコーパスから長いのを1つ選んで、形態素解析して準備しておきました。
データは一つの文字列形式で、半角スペースで区切ってあります。


# データ型や長さの確認
print(type(tokenized_text))
# 
print(len(tokenized_text))
# 8704
print(len(tokenized_text.split()))
# 2227
# 先頭 70文字
print(tokenized_text[: 70])
# bluetooth bluetooth デジタル 機器 同士 接続 無線 規格 bluetooth 聞く 難しい 思う 今 ケーブル 繋げる

さて、wordcloudの使い方の復習です。
ライブラリをインポートして、 WordCloudのインスタンスを作り、
generate_from_text メソッドか、 そのエイリアスである generate メソッドでワードクラウドが作れます。
デフォルト設定でやってみましょう。


from wordcloud import WordCloud
wc = WordCloud()
wc.generate_from_text(tokenized_text)
wc.to_image()

はい、日本語が見えないですね。
これ以外にも例によって一文字の単語は含まれないとか、いろいろ難点はあります。
これらは、 WordCloudのインスタンスを作るときに、各種設定を渡すことで改善します。一覧はドキュメントにあるので、僕が使うものだけ紹介します。

– font_path: フォントファイル(ttfファイルなど)のファイルパスを指定する。 fontfamilyではないので注意。
– width: 横幅
– height: 高さ
– prefer_horizontal: 横書きで配置することを試みる確率。 (デフォルト0.9) これを1にすると横書きに統一できる。
– background_color: 背景色(デフォルト ‘black’)。とりあえず’white’にすることが多い。
– include_numbers: 数値だけの単語も含むか。デフォルトがFalse
– colormap: 文字色のカラーマップ指定
– regexp: generate_from_text するときの単語区切りに使う正規表現。Noneの場合、r”\w[\w’]+” が使われるので、一文字の単語が消える。

とりあえず、これらをいい感じに設定して出してみましょう。


wc = WordCloud(
        font_path="/Library/Fonts/ipaexg.ttf",  # 日本語フォントファイル 
        width=600,  # 幅
        height=400,  # 高さ
        prefer_horizontal=1,  # 横書きで配置することを試す確率 (デフォルト0.9)
        background_color='white',  # 背景色
        include_numbers=True,  # 数値だけの単語も含む
        colormap='tab20',  # 文字色のカラーマップ指定
        regexp=r"[\w']+",  # 一文字の単語も含む
    ).generate(tokenized_text)

wc.to_image()

圧倒的に良くなりましたね。

このほか、実用的には使う場面が思いつかないのですが、画像データ(要するに2次元の配列)でマスクをかけることができます。
0〜255の整数値(floatだとwarningが出ますが動きます。)が入った配列を渡すと、
「値が255の部分」がマスキングされ文字が入らなくなります。最初0の方がマスクされると思ってたのですが逆でした。

本来は画像データを読み込んでやることを想定されているようですが、配列を作ればなんでもいいので、とりあえず円形のデータを使って試します。


import numpy as np
mask_ary = np.zeros(shape=(400, 400))

for i in range(400):
    for j in range(400):
        if (i-200)**2 + (j-200)**2 > 180**2:
            mask_ary[i, j] = 255

# 整数型に変換
mask_ary = mask_ary.astype(int)

wc = WordCloud(
        font_path="/Library/Fonts/ipaexg.ttf",
        mask=mask_ary,
        contour_width=1,  # マスク領域の枠線の太さ
        contour_color='green',  # マスク両機の枠線の色
        prefer_horizontal=1,  # 横書きで配置することを試す確率 (デフォルト0.9)
        background_color='white',  # 背景色
        include_numbers=True,  # 数値だけの単語も含む
        colormap='tab20',  # 文字色のカラーマップ指定
        regexp=r"[\w']+",  # 一文字の単語も含む
    ).generate(tokenized_text)

wc.to_image()

少しおしゃれになりました。

TensorflowやKerasでJupyterカーネルが落ちるようになってしまった場合の対応

注意: この記事で紹介しているのは根本的解決ではなく、暫定対応です。

前回の記事: tensorflow-textのインストールに苦戦した話 で、やむなくライブラリを1つpipで入れたところ、Tensorflow(keras)を操作しているとJupyterカーネルが死んでしまう事象が再発するようになりました。
実は以前LightGBMを入れた後も同様の事象が発生していたんですよね。
その時は対応方法をメモしていなかったので、この機会に残しておきます。

まず、事象の切り分けです。
今回の事象は jupyter では、結果の出力枠には、Warning など表示せず、メッセージウィンドウで以下のメッセージを表示してお亡くなりになります。

Kernel Restarting
The kernel appears to have died. It will restart automatically.

これだけだと原因は分からないのですが、 コンソールからPythonを起動し、同じコードをコピペして実行していくと、今度は次のエラーが出ます。

OMP: Error #15: Initializing libiomp5.dylib, but found libiomp5.dylib already initialized.
OMP: Hint This means that multiple copies of the OpenMP runtime have been linked into the program. That is dangerous, since it can degrade performance or cause incorrect results. The best thing to do is to ensure that only a single OpenMP runtime is linked into the process, e.g. by avoiding static linking of the OpenMP runtime in any library. As an unsafe, unsupported, undocumented workaround you can set the environment variable KMP_DUPLICATE_LIB_OK=TRUE to allow the program to continue to execute, but that may cause crashes or silently produce incorrect results. For more information, please see http://www.intel.com/software/products/support/.
Abort trap: 6

要するに、 libiomp5.dylib というファイルがダブってるそうです。一個だけリンクされているようにしなさいと言われているのですが、実はまだこの実態ファイルがどこに存在しているのか見つけられておらず、根本的な対応が取れていません。
そこで、次の記述に頼ります。

you can set the environment variable KMP_DUPLICATE_LIB_OK=TRUE

要は問題のライブラリの重複を許して警告を止める設定のようです。
予め環境変数に入れておいても良いでしょうし、Pythonのコード中で行うには次のように設定したら大丈夫です。


import os
os.environ['KMP_DUPLICATE_LIB_OK']='TRUE'

.bash_profile に設定する時は次の記述を入れます。


export KMP_DUPLICATE_LIB_OK=TRUE

こういうのを避けるために 環境構築のconda統一を進めてたようなものなので、とても残念なのですがしばらくこの暫定対応で行かないといけないですね。

もし同じような事象で悩まれている方がいらっしゃいましたら試してみてください。

また、 Jupyter Kernelの突然死は、同じコードをコンソールで実行するとWarningやErrorを見れる場合があるということも覚えておくと便利です。
(実はこのERRORメッセージはJupyterのログに吐き出されているのですそちらから探すこともできます。)

matplotlibの3次元プロットを回転するアニメーションで保存する

matplotlobで3次元のグラフを作る時、jupyter notebookではグリグリと動かしていろんな角度から確認することができます。
それをそのままこのブログに埋め込みたくて方法を探していたのですが良いのが見つからなかったので代用としてgifアニメーションを作ることにしました。
今回の記事では、Z軸を中心にぐるっと一周回転させてみます。

以前、 matplotlib.animation.ArtistAnimation を使ったgifの作り方は紹介したことがあるので、
今回は matplotlib.animation.FuncAnimation
を使う別の方法を紹介します。

参考記事: matplotlibでgif動画生成

(ちなみにFuncAnimation自体は、かなり柔軟に動画を作ることができ、
当然3Dプロットを回す以外の使い方もできます。)

可視化の対象は前回の記事のサッカーボールです。
変数Gには、前回の記事と同じグラフが格納されているものとしてください。
無駄に長いコードなので重複部分は今回のコードに入れていません。

さて、 FuncAnimation の使い方の紹介です。

この関数は、
fig, func, frames の3つの引数を渡して使います。
figはグラフを描写するfigureオブジェクトです。
framesにはリスト等を渡します。整数値を渡すとrange()と同じ動きになり、0からその整数値-1までの値を渡したのと同じになります。
この、framesに渡したリストの値を順番にfuncに渡して関数が実行され、それぞれの実行結果をつなげたものがアニメーションになります。

今回は少し工夫して、init_func という引数も使います。
これは、最初に一回だけ実行する関数を渡します。

1. init_func で 3次元にグラフをplotする
2. func で少しづつ回転する

という流れで、func では回転以外の操作をしないようにして少しだけ効率的にしました。


import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from mpl_toolkits.mplot3d import Axes3D

# G に サッカーボルー型のグラフデータを格納する処理は略

# ノードの座標を固定
pos = nx.spring_layout(G, dim=3)
# 辞書型から配列型に変換
pos_ary = np.array([pos[n] for n in G])

# plot する figureと、 Axesを準備する
fig = plt.figure(figsize=(10, 10), facecolor="w")
ax = fig.add_subplot(111, projection="3d")


# Axes にGraph をプロットする関数を準備
def plot_graph():
    ax.scatter(
        pos_ary[:, 0],
        pos_ary[:, 1],
        pos_ary[:, 2],
        s=200,
    )

    # ノードにラベルを表示する
    for n in G.nodes:
        ax.text(*pos[n], n)

    # エッジの表示
    for e in G.edges:
        node0_pos = pos[e[0]]
        node1_pos = pos[e[1]]
        xx = [node0_pos[0], node1_pos[0]]
        yy = [node0_pos[1], node1_pos[1]]
        zz = [node0_pos[2], node1_pos[2]]
        ax.plot(xx, yy, zz, c="#aaaaaa")


# 引数を受け取って図を回転させる関数を準備
def plt_graph3d(angle):
    ax.view_init(azim=angle*5)


# アニメーションを作成
ani = FuncAnimation(
    fig,
    func=plt_graph3d,
    frames=72,
    init_func=plot_graph,
    interval=300
)

# imagemagickで作成したアニメーションをGIFで書き出す
ani.save("rolling.gif", writer="pillow")

出力結果がこちらのgifです。

もともと対称性の高い図形なので、回転させるありがたみが薄かったかもしれないですね。

図形を回転させるところでは、
view_init
という関数を使いました。
elev と azim という二つの引数をとりますが、回転の向きが違います。
使うのは二つ目の azim の方なので注意が必要です。

globでサブフォルダを含めて再帰的にファイルを探索する

普段は、DBに格納された扱いやすいデータや1ファイルにまとめられたデータばかり扱っていて、
散らばったファイルからデータを拾ってくることは少ない恵まれた環境で仕事しています。
しかし、久々にあるフォルダ配下に散ってるファイルを再帰的に探してまとめて処理する機会があったのでそのメモです。

以前、特定のフォルダの直下のファイルは、 globで手軽に見つけられるという記事を書きました。
参考: globで手軽にファイル名の一覧を取得する

今回は pathlib を紹介しようと思っていたのですが、
よく globのドキュメントを見ると、 バージョン 3.5 から、再帰的なglobが実装されていたんですね。
参考: glob — Unix 形式のパス名のパターン展開

ということでこちらを使ってみます。
recursive に True を指定し、 pathname の中に ** を含めればいいようです。

拡張子付きのファイルパスだけリストアップするには次のように書きます。


import glob
for f in glob.glob("./**/*.*", recursive=True):
    print(f)

"""
./001.txt
./folder01/002.txt
./folder01/003.sql
./folder01/subfolder011/004.txt
./folder01/subfolder011/005.sql
./folder02/006.png
./folder02/subfolder021/007.gif
"""

recursive=False (デフォルト) の場合と一応比較しておきましょう。


for f in glob.glob("./**/*.*", recursive=False):
    print(f)

"""
./folder01/002.txt
./folder01/003.sql
./folder02/006.png
"""

for f in glob.glob("./*/*.*", recursive=False):
    print(f)

"""
./folder01/002.txt
./folder01/003.sql
./folder02/006.png
"""

比較用に ** を * に変えたものも一緒に載せましたが、
recursive=False の場合は、 ** は * と同じ挙動しかしていないことがわかります。

recursive=True にすると、 ** は複数階層のフォルダ(ディレクトリ)も含めて探索してくれています。

特定拡張子のファイルのみ欲しい時は、 pathname の記述で指定しましょう。
ディレクトリだけ指定したい時は / で終えれば可能です。
また、 glob.glob の代わりに、 glob.iglob を使うと、結果をリストではなくイテレーターで返してくれます。


for f in glob.iglob("./**/*.txt", recursive=True):
    print(f)

"""
./001.txt
./folder01/002.txt
./folder01/subfolder011/004.txt
"""


for f in glob.iglob("./**/", recursive=True):
    print(f)

"""
./
./folder01/
./folder01/subfolder012/
./folder01/subfolder011/
./folder02/
./folder02/subfolder021/
"""

望む結果が得られました。

NumPyで行列の固有値と固有ベクトルを求める

最近のNetworkx関係の記事でよく行列の固有ベクトルを求めていますが、
そこで使っているNumPyの関数について紹介します。

最初に行列の固有値と固有ベクトルの定義について復習しておきます。
$\mathbf{A}$を正方行列とします。
この時、スカラー$\lambda$と、零でないベクトル$\mathbf{x}$が、
$$
\mathbf{A}\mathbf{x} = \lambda \mathbf{x}
$$
という関係を満たす時、
$\mathbf{x}$を$\mathbf{A}$の固有ベクトル、$\lambda$を$\mathbf{A}$の固有値と呼びます。

最近のネットワーク分析系の記事でも頻出しているだけでなく、
数学やデータ分析の各所に登場する非常に重要な概念です。

NumPyでは、
numpy.linalg.eig と、 numpy.linalg.eigh として実装されています。

早速、適当な行列に対して使ってみます。


import numpy as np
a = np.array(
        [[-2, -1,  2],
         [1,  4,  3],
         [1,  1,  2]]
    )
print(a)
"""
[[-2 -1  2]
 [ 1  4  3]
 [ 1  1  2]]
 """

# 固有値のリストと、固有ベクトルを列に持つ行列のタプルが戻る
values, vectors = np.linalg.eig(a)
print(values)
# [-2.37646808  4.92356209  1.452906  ]

print(vectors)
"""
[[ 0.97606147  0.04809876  0.4845743 ]
 [-0.05394264 -0.95000852 -0.73987868]
 [-0.21069932 -0.30849687  0.46665542]]
"""

eig一発で、固有値と固有ベクトルをまとめて返してくれるのでとても手軽ですね。
上のサンプルコードのように、それぞれ別の変数で受け取るのがオススメです。

なお、一つの変数で受け取ることもできます。
結果を見ていただければ若干使いにくそうな雰囲気が伝わると思います。


eig_result =  np.linalg.eig(a)
print(eig_result)
"""
(array([-2.37646808,  4.92356209,  1.452906  ]), array([[ 0.97606147,  0.04809876,  0.4845743 ],
       [-0.05394264, -0.95000852, -0.73987868],
       [-0.21069932, -0.30849687,  0.46665542]]))
"""

さて、固有値の方はvalues に入っている値がそれぞれ求めたかった値になりますが、
固有ベクトルの方は少し注意が必要です。 というのもサンプルコードの、コメントに書いている通り、
固有ベクトルは、結果の行列の列ベクトルとして格納されています。

つまり、 vectors[0], vectors[1], vectors[2] は固有ベクトルではありません。
正しい固有ベクトルは、 vectors[:, 0], vectors[:, 1], vectors[:, 2] です。
それぞれ、values[0], values[1], values[2] に対応します。
なお、固有ベクトルを0でないスカラー倍したものはそれもまた同じ固有値の固有ベクトルになりますが、
このeigの戻り値は、単位ベクトル(長さが1)になるように正規化されて戻されます。

一応、固有値と固有ベクトルの定義の両辺をそれぞれ計算して、
これらの値が本当に固有値と固有ベクトルなのか見ておきましょう。


for i in range(3):
    print(values[i] * vectors[:, i])
    print(a @ vectors[:, i])


"""
[-2.31957893  0.12819296  0.5007202 ]
[-2.31957893  0.12819296  0.5007202 ]
[ 0.23681725 -4.67742593 -1.5189035 ]
[ 0.23681725 -4.67742593 -1.5189035 ]
[ 0.70404091 -1.07497418  0.67800646]
[ 0.70404091 -1.07497418  0.67800646]
"""

バッチリですね。

もう一つのeighについての紹介です。
eigは一般の正方行列に対して利用できますが、 eighは、実対称行列と、エルミート行列に対してのみ利用できます。
なお、eighは行列の下三角行列部分だけ使って計算するので、
どちらでもない行列を渡しても普通に動いてしまいます。結果は不正確なので注意が必要です。

ついでに紹介しますが、
numpy.linalg.eigvals と、 numpy.linalg.eigvals というメソッドで、
固有値のみを得ることもできます。
固有ベクトルが不要なら、eig の戻り値の該当部分を捨てれば済むのであまり使ったことはないのですが、
メモリの節約や計算速度等のメリットがあるのかもしれません。

Pandasで欠損のある列の文字列型の数値を数値型に変換する

イケてるタイトルがつけられなくて申し訳ない。

pandas.to_numeric という関数の errors という引数が便利なことを知ったのでそれを紹介します。

データを扱っている時、文字列型の数字を数値型に型変換したいことはよくあります。

単体の変数であれば、 intflaotで変換できます。


int("123") #123
float("123") # 123.0

DataFrameや Series でも、全ての値が問題なく変換できる場合は、 .astypeで変換できます。


data1_str = pd.Series(["1", "2", "3"])
print(data1_str)
"""
0    1
1    2
2    3
dtype: object
"""

data1_int = data1_str.astype(int)
print(data1_int)
"""
0    1
1    2
2    3
dtype: int64
"""

ここで厄介なのが、元の値の中に、欠損値や数値に変換できない値が混ざっている場合です。
.astype(int).astype(float)すると、エラーが発生します。
astypeメソッド自体も、errorという引数をとりますが、
エラーを ignore で抑制した場合、変換は一切行ってくれません。
参考: pandas.Series.astype

僕が期待しているのは、 数値型に変換できる値は数値型に変換して、Noneや変換できない文字列は NaNで埋めてくれることです。
そして、それが、pandas.to_numericを使うと手軽に実現できます。


import pandas as pd

# ダミーデータ生成
df = pd.DataFrame(
    {
        "key": ["key1", "key2", "key3", "key4"],
        "value_str": ["123", "45.67", None, "八十九"],
    }
)

# value_str 列の値を数値に変えられるものは変えた列を作る
df["value_num"] = pd.to_numeric(df["value_str"], errors="coerce")

print(df)
"""
    key value_str  value_num
0  key1       123     123.00
1  key2     45.67      45.67
2  key3      None        NaN
3  key4       八十九        NaN
"""

バッチリできました。

ポイントは、 errors="coerce"の部分です。
errorsは、”ignore”, “raise”, “coerce” の3種類の値を取ります。
“raise” がデフォルトで、これを指定すると普通に例外が発生します。
“ignore” は例外を抑えますが、何の変換もせず、そのままのオブジェクトを返します。
“coerce” は、数値に変換できるものは変換して、そうでないものはNaNにしてくれます。

“raise”だとこのような例外が発生します。


try:
    pd.to_numeric(df["value_str"], errors="raise")
except Exception as e:
    print(e)

# Unable to parse string "八十九" at position 3

文字列を時刻に変える to_datetime や、汎用的な型変換の astype に比べてマイナーな印象があるのですが、
地味に使える場面が多いので、数値への型変換の機会があったら、
to_numeric を試してみてください。

Matplotlibの配色を別の処理でも流用したい

Python(と jupyter notebook)でデータを可視化する場合、色を16進法のRGBで指定できるライブラリは多くあります。
Matplotlibがベースになっているものは、そのカラーマップを指定できることも多いのですし、
「rは赤」、「bは青」など一部の色はアルファベットや色名で指定できるのですが、
もっと多くの色を使いたかったり、値によってグラデーションをつけたい場合で逐一RGBを構築するのは結構な手間です。

そこで、Matplotlibの配色をそのまま流用できないかと思って調べてみました。
結論から言うと、結構簡単に使えそうです。

まず、配色そのもののデータは、
matplotlib.cm と言うモジュールに含まれています。
配色はその名前で指定しますが、名前と実際の色の対応はこちらのリファレンスをみると良いでしょう。
Colormap reference

使いたいカラーマップが決まったら、cm.get_cmap() か、 cmの属性として、使うことができます。
要するに次の2行は同じものです。


cm.get_cmap("Greens")
cm.Greens

さて、どちらもカラーマップのオブジェクトを返してくれますが、
そのカラーマップのオブジェクトにに数値を渡すと、RGBのタプルを返してくれます。


import matplotlib.cm as cm

print(cm.get_cmap("Greens")(0.7, alpha=0.5))
# (0.18246828143021915, 0.5933256439830834, 0.3067589388696655, 0.5)
print(cm.get_cmap("Paired")(3))
# (0.2, 0.6274509803921569, 0.17254901960784313, 1.0)

渡す数値ですが、連続的に色が変化するものには、 0〜1の値を渡します。
色の値が不連続な(要はリファレンスで、Qualitativeのカテゴリにあるもの)は、0〜1の値で渡しても大丈夫ですが、
整数値で0,1,2,3などを指定しても大丈夫です。
これらは、1と1.0や2と2.0など、同じ値でも整数型と浮動小数型で結果が変わるので注意してください。
ちなみに、値はリスト形式で複数同時に渡しても大丈夫です。

さて、最初の話に戻りますが、このRGB値のタプルを他のライブラリ等で使うには、16進法の文字列に変換する必要があります。
255倍して16進法の文字列に変化して、シャープをつけて結合するコードを自分で書いてもいいのですが、
なんと Matplotlibにその関数が用意されていました。

matplotlib.colors.rgb2hex です。
これはなぜか、色のリストは受け取ってくれないので、順番に適用していかないといけないのですが、
RGBのタプルを16進法文字列に手軽に変換してくれます。
(keep_alpha=Trueを指定すると透明度も含めてくれます。デフォルトはFalseです。)

試しにカラーマップから10色取り出してみましょう。


import matplotlib.colors as mcolors
import matplotlib.cm as cm
import numpy as np

cmap = cm.get_cmap("BuGn")
for rgb in cmap(np.arange(0, 1, 0.1)):
    print(mcolors.rgb2hex(rgb))

"""
#f7fcfd
#e9f7fa
#d6f0ee
#b8e4db
#8fd4c2
#65c2a3
#48b27f
#2f9858
#157f3b
#006428
"""

10個の色が取り出せましたね。

最後に何か例を出しておきたいので、networkx で作成したグラフに中心性で色をつけてみました。
(グラフの中心性には複数の種類がありますが、今回は媒介中心性を使いました。)


import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.colors as mcolors


while True:
    # ランダムにグラフを生成する
    G = nx.random_graphs.fast_gnp_random_graph(15, 0.2)
    # 連結なグラフが生成できたらループを抜ける
    if nx.is_connected(G):
        break

# 媒介中心性の計算
centrality = nx.betweenness_centrality(G)
# 辞書形式なので、ノードの順番と揃えてリスト化する。
centrality_list = np.array([centrality[node] for node in G.nodes])
# 媒介中心性を0〜1に正規化する
color_level = centrality_list - min(centrality_list)
color_level/=max(color_level)
# ノードの色の生成
rgb_list = cm.get_cmap("Oranges")(color_level, alpha=0.8)
node_color=[mcolors.rgb2hex(rgb, keep_alpha=True) for rgb in rgb_list]

# グラフの可視化
fig = plt.figure(figsize=(8, 8), facecolor="w")
ax = fig.add_subplot(1,1,1)
nx.draw_networkx(
                G,
                node_color=node_color,
                node_size=500,
                edge_color="#aaaaaa",
                node_shape="s"
            )

出力がこちら。

媒介中心性が高いところが色が濃くなっているのがわかります。