2019年のまとめ

2019年最後の更新です。
今年から始めたブログでしたが無事に1年間更新を続けることができました。
ということでGoogleアナリティクスのデータ等も見ながら、振り返って見ます。
(記事執筆時点のデータなので後日残り1日文のデータを入れて更新するかも)

まず基本的なデータ。

– 記事数 305 記事 (この記事含む)
– 訪問ユーザー数 23,803人
– ページビュー 36,569回

密かに目標にしていた300記事は無事に達成し、アクセスもそこそこ集まるようになりました。
どこまで役に立っているのかわかりませんが少なくとも更新する意味はあるブログになってきたのかなと思います。

実際、更新してきてどうだったか、という観点の話はつい先日300記事達成の投稿で書いたので省略します。

次は今年よく読まれた記事の紹介です。
トップテンは次のようになりました。

  1. macにgraphvizをインストールする
  2. pythonでARモデルの推定
  3. DataFrameを特定の列の値によって分割する
  4. graphvizで決定木を可視化
  5. pandasでgroupbyした時に複数の集計関数を同時に適用する
  6. pythonで編集距離(レーベンシュタイン距離)を求める
  7. scikit-learnでテキストをBoWやtfidfに変換する時に一文字の単語も学習対象に含める
  8. Prestoで1ヶ月後の時刻を求める時に気をつけること
  9. pythonで累積和
  10. pythonでARMAモデルの推定

ベスト5は3Qの時とほとんど変化していませんが、6位から10位は少し意外なのも入っていますね。累積和など。
データサイエンティスト色をもっと強めたいなとは思うのですが、とりあえず技術ブログっぽいものにはなったと思います。

元々、各所に散らばってしまっていた自分のメモや検証結果などを一箇所にまとめたいという思いで始め、
訪問者のことよりも自分にとっての使いやすさ重視の記事が多いブログですが、
多くのかたに訪問していただきとてもありがたく思います。

さて、来年の更新ですが、今のペースでやっていくのは少し難しいと思っているので、更新頻度は見直したいと思っています。
というのも、最近は自分の学習時間においてブログへのアウトプットの比重が高まりすぎ、
腰を据えたインプットがおろそかになっているという課題も感じているからです。

年末年始に来年の目標と計画を整理し、このブログの運用はちょうどいい塩梅を探しながら続けていきたいと思います。
とりあえずお正月期間は更新をお休みし、来年の更新は6日以降から再開予定です。

今年一年ありがとうございました。良いお年を。

Googleアナリティクスでアラート設定

このブログでもアクセスの分析のためにGoogleアナリティクスを使っています。
順調に訪問者数が増えているのですが、何百人/日といった節目の達成はやはり嬉しいので、
数ヶ月前から、達成したら通知が来るように設定しています。

そのために使っているのがカスタムアラート機能です。

ドキュメント: カスタム アラートの作成、管理

左ペインのカスタムの中にあるカスタムアラートから飛べます。(飛び先は管理の中なのでそちらからも遷移できます)

アラート名を設定し、
期間を 日/週/月から選択、
このアラートが発生したときにメールで通知する。 にチェックを入れてメアドを設定、
あとは適用するトラフィックの対象と、指標、閾値となる数値を入れて保存したら完成です。

このブログだと、全てのトラフィックで、ユーザー数などを見ているだけですが、
ページを絞った直帰率や新規セッション割合など、かなり多様な設定が可能です。

個人ブログであればアラートが上がるとモチベーションが上がるようなものをいくつか設定しておくと良いですし、
仕事で分析しているサイトであれば異常時の通知などのに使えると思います。

関数内で発生した例外を呼び出し元にも伝える

昨日に続いて例外処理の話です。
ある関数内に、エラーが発生しうる処理がある時、その関数内でtry:〜except:〜処理を書いて
綺麗に例外を処理するけど、その例外を関数の呼び出し元にも伝えて例外を伝播させたいことがあります。

このような時も、 raise 文を使うことができます。

自分は最近まで raise 文は新規に例外を発生させいさせる機能しかないと思ってました。
こういう風に。


raise ValueError

"""
ValueError                                Traceback (most recent call last)
 in ()
----> 1 raise ValueError

ValueError: 
"""

しかし、raiseを単体で使用すると、そのスコープで有効になっている例外を再送出できます。

参考: 7.8. raise 文

試してみる前に、非常に単純な例なのですが次のようなケースを考えてみます。
関数 inv は 引数の逆数を返す関数で、0が渡されたら例外になるはずのものです。
そして、0が渡されたら内部で例外処理をしています。
そして、print_inv は inv を使って、与えられた数の逆数を表示します。


def inv(x):
    try:
        return 1/x
    except Exception as e:
        print(e)


def print_inv(x):
    try:
        print(inv(x))
    except Exception as e:
        print(e)
    else:
        print("print_invで例外は発生しませんでした")


print_inv(0)
"""
division by zero
None
print_invで例外は発生しませんでした
"""

ご覧の通り、 inv 内で例外処理しているので、呼び出し元では例外の発生を検知できていません。

ここで、 raiseを使って例外の再送出を入れてみます。


def inv(x):
    try:
        return 1/x
    except Exception as e:
        print(e)
        # 例外の再送出
        raise


def print_inv(x):
    try:
        print(inv(x))
    except Exception as e:
        print(e)
    else:
        print("print_invで例外は発生しませんでした")


print_inv(0)
"""
division by zero
division by zero
"""

division by zero が 2回表示されました。 inv と print_inv でそれぞれキャッチされた例外です。

Pythonにおける例外処理

jupyterでインタラクティブにPythonを使っているとあまり必要ないのですが、
本番コードを書くときなどは流石に例外処理を真面目に実装する必要があることがあります。
そこまで高頻度にあることではなく、すぐ忘れてしまうので、書き方をまとめておこうと思います。

参考になるドキュメントは次の2箇所です。
8. エラーと例外
組み込み例外

基本的に次のような書き方になります。
必須なのは、 try と except で、 exceptは複数書くこともできます。
except する例外には as e のように別名をつけることができ、
別名をつけておけば処理中で利用できます。
else と finally はオプションなので不要ならば省略可能です。


try:
    # ここに例外が発生しうるコードを書く

except [キャッチしたい例外クラス]:
    # 例外が発生した時に実行するコード

else:
    # 例外が発生なかった時に実行するコード

finally:
    # 必ず実行するコード

とりあえず定番の 0で割る演算で試してみましょう。


import numpy as np


def inv(data):
    try:
        inverse_data = 1/data
    except ZeroDivisionError as e:
        print(e)

    except TypeError as e:
        print(e)

    else:
        print("正常終了")
        return inverse_data
        print("このメッセージは表示されない")

    finally:
        print("finallyに書いた文は必ず実行されます")


print(inv(5))
"""
正常終了
finallyに書いた文は必ず実行されます
0.2
"""

print(inv(0))
"""
division by zero
finallyに書いた文は必ず実行されます
None
"""

print(inv("a"))
"""
unsupported operand type(s) for /: 'int' and 'str'
finallyに書いた文は必ず実行されます
None
"""

例外が発生した、0と”a” については想定通りに動きました。

実は例外が発生しなかったinv(5)が僕にとっては少し驚きでした。
else: のブロックの中で、 return して関数を抜けているので、
それより後ろの finally: のブロックは流石に実行されないと思っていたのですが、
print関数がバッチリ実行されています。

改めてよく読んでみれば、ドキュメント中にもしっかりそう書いてありました。
この辺りはきちんと理解して使う必要がありそうです。

– もし try 文が break 文、 continue 文または return 文のいずれかに達すると、その:keyword:break 文、 continue 文または return 文の実行の直前に finally 節が実行されます。
– もし finally 節が return 文を含む場合、 try 節の return 文より先に、そしてその代わりに、 finally 節の return 文が実行されます。

今回はブログ記事用に書いたコードだったので、
ZeroDivisionError と TypeError を 分けて書きましたが、
Exception のようなキャッチできる範囲の広い例外を指定しておけばまとめて受け取ってくれます。
(本当はあまり良くないと思うのですが、便利なので大抵そうしています。)


def inv2(data):
    try:
        inverse_data = 1/data
        return inverse_data
    except Exception as e:
        print(e)
        return None


print(inv2(5))
"""
0.2
"""

print(inv2(0))
"""
division by zero
None
"""

print(inv2("a"))
"""
unsupported operand type(s) for /: 'int' and 'str'
None
"""

また、例外処理の中で例外の種類を区別する必要が全くない場合、
except Exception as e:
の代わりに、
except:
とだけ書いておけば、より簡単に全ての例外をキャッチしてくれます。

DataFrameをマージする時にkeyの一意性を確認する

昨日の indicator の記事を書くためにドキュメントを読んでいて見つけた、 validate という引数の紹介です。

データフレーム通しを結合する時に、結合に使うキーのユニーク性が重要になることがあります。
事前に確認するようにコードを書いておけば済む話ではあるのですが、
pandasのmerge関数では、キーが一意でなかった時にエラーを上げてくれる引数があるようです。

ドキュメント: pandas.merge

引数 validate には、 one_to_one / one_to_many / many_to_one / many_to_many の4種類の文字列か、None(デフォルト)を
渡すことができます。
そして、 one_to_one なら 1:1, one_to_many なら 1:m 、という風にkeyが対応してなければエラーを上げてくれます。
many_to_many と None はノーチェックです。
なので、きちんとエラーをキャッチするように次のようにコードを書けます。
(データの準備等は省略)


try:
    df_merge = pd.merge(
            df_0,
            df_1,
            on="key",
            how="outer",
            validate="one_to_many",
        )
except pd.errors.MergeError as e:
    print(e)

# Merge keys are not unique in left dataset; not a one-to-many merge

発生するエラーは pd.errors.MergeError です。
データを何種類か用意して、validateの引数を変えながら動かすと色々動きがわかると思います。

pandasのデータフレームを結合する時に元データが左右どちらのデータソースにあったか見分ける方法

どこで見かけたか忘れてしまった(TwitterかQiitaかその辺りのはず)のですが、
pandasのデータフレームのマージをする時に便利な引数を知ったので紹介します。

DataFrame同士を列の値で結合する時、pd.mergeを使います。

how=”inner”で利用する場合は何も問題ないのですが、
left/right/outerで使う場合、結果の中に、ちゃんと左右のデータフレームにレコードが存在してうまく結合できた行と、
一方にしか存在せず、結合はしなかった行が混在します。

left_on/right_on を使って結合した場合はそこの欠損を見ればまだ見分けられるのですが、
同名列をonで結合すると見分けがつかず、少し不便です。

このような時、 indicator=True を指定しておくと、 結果に _merge という列が追加され、
各レコードが左右のデータフレームのどちらに起因しているか出力してくれます。

やってみたのがこちらです。


import pandas as pd
df_0 = pd.DataFrame(
            {
                "id": range(5),
                "key": [1, 5, 12, 7, 8],
                "value0": ["a", "b", "c", "d", "e"],
            }
        )
df_1 = pd.DataFrame(
            {
                "key": range(10),
                "value1": range(0, 100, 10),
            }
        )

df_merge = pd.merge(
        df_0,
        df_1,
        on='key',
        how="outer",
        indicator=True,
    )
print(df_merge)
"""
     id  key value0  value1      _merge
0   0.0    1      a    10.0        both
1   1.0    5      b    50.0        both
2   2.0   12      c     NaN   left_only
3   3.0    7      d    70.0        both
4   4.0    8      e    80.0        both
5   NaN    0    NaN     0.0  right_only
6   NaN    2    NaN    20.0  right_only
7   NaN    3    NaN    30.0  right_only
8   NaN    4    NaN    40.0  right_only
9   NaN    6    NaN    60.0  right_only
10  NaN    9    NaN    90.0  right_only
"""

both / left_only / right_only
で、 key の由来が確認できます。

ブログ記事数300記事達成

このブログを開設してからもうすぐ1年になります。
そして、日々せっせと書いてきた記事数がいよいよこの記事で300記事になりました。

もうしばらくしたら年間の振り返り記事も書くので時期的に微妙なのですが、
記事数のキリが良いので100記事の時の記事も見つつ、ちょっと振り返りをやってみます。
参考: 記事数が100を超えていました

日々の訪問者数がかなり伸びてきた

100記事の頃は日々20〜30人のかたがきてくださることを喜んでいたのですが、
今では平日は1日300人以上の訪問があるようになりました。(休日はもう少し減ります。)

以前はライブラリやモジュールのインストール記事や、エラーメッセージを貼り付けたような記事でアクセスの多くを稼いでいたのですが、
今では、時系列データ分析関係の記事や、pandasの使い方、scikit-learnやkerasの記事などにもある程度の量のアクセスが集まるようになりました。
(と言っても、graphvizの記事が一番人気であることは変わりませんが。)
Qiitaからの流入が出始めたのも最近のことです。

自分用のリファレンスとして便利になってきた

流石に300も記事を書くと、内容を全部覚えているわけでは無いのでこのブログで調べ物をする機会も度々発生するようになりました。
元々は、一度は気になって調べて自分が使ってるどれかの端末化サーバのどこかにメモが残ってるものが多いのですが、それを探すのはかなり手間です。
そのため、このブログに書いたような気がする内容はまずここで検索するようになりました。
当たり前ですが記事中のコードは自分が好きなスタイルで書いてるのでコピペでも使いやすく快適です。
(訪問者の方にとって使いやすいか、という点でまだ課題がある気がします。)

記事を書くためにネタを探すことも増えてきた

流石に最近は、明日のブログ記事を何にしようかと悩むことも増えてきました。
ただ、このブログを書く中で、各ライブラリの公式ドキュメントなどを読む習慣もついているので、
そのような時は主要なライブラリのドキュントを読み漁ってネタを探しています。
それはそれで新しい発見もあり、勉強になるので元々が必要に迫られた情報ばかりだった頃とは違った意味で勉強になると感じています。
ただ、ネタ探しに時間がかかる分、肝心の記事が内容が浅くなりがちでそれは反省しないといけません。

今後の方針について

残り少ないですが今年いっぱいは今のペースで続ける予定です。
ただ、来年の方針は考え直そうと思ってます。

1日1記事のペースでかけるネタが枯渇している一方で、時間かけてしっかり書きたい内容は色々あります。
具体的なペースとか、来年の記事数の目標とかはブログ以外の目標等も含めてしっかり練って、
年末年始の間にでも決めたいと思ってます。

時系列データ分析の話題で言えば、ベクトル自己回帰や状態空間モデルの話をまだかけていないですし。
ディープラーニングの話題でも、普段よく使っているLSTMなどの話をかけていません。
自然言語処理もまだまだで、早めに書きたいと思ってたword2vecの話題が未登場です。

1日1記事のペースを維持しようと、自分がしっかり時間かけてしっかり書きたい記事がかけずに
さっと出せる小ネタを探す傾向が最近強いので、きちんと振り返ってもっと有益な形でブログ更新を続けたいです。

Kerasのモデルやレイヤーの識別子に付く番号をリセットする

Kerasでモデルを構築する時、モデルのオブジェクトやレイヤーにname引数で名前をつけないと、
区別できるように自動的に識別子(名前)をつけてくれています。

適当な例ですが、summary()で表示すると確認できる、
次のコードのsequential_13/lstm_12/dense_14 のようなやつです。


print(model.summary()) 
"""
Model: "sequential_13"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm_12 (LSTM)               (None, 10)                640       
_________________________________________________________________
dense_14 (Dense)             (None, 3)                 33        
=================================================================
Total params: 673
Trainable params: 673
Non-trainable params: 0
_________________________________________________________________
None
"""

jupyter notebookなどで試行錯誤しているとどんどん数字が大きくなっていくのですが、
ちょっと見た目が良く無いので、数値をリセットしたくなることがあります。
(そもそもnameで名前つけてあげればいいのですが。)

そのような場合、 バックエンド関数である、 clear_session() をつかうと、識別子をリセットできます。
ドキュメント: バックエンド

由来をよく知らないのですが、 backend は K という別名でインポートする慣習があるようです。


from tensorflow.keras import backend as K
K.clear_session()

# もう一度モデル構築
model = Sequential()
model.add(LSTM(10, input_shape=(40,5), activation="tanh"))
model.add(Dense(3, activation="softmax"))
print(model.summary()) 
"""
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm (LSTM)                  (None, 10)                640       
_________________________________________________________________
dense (Dense)                (None, 3)                 33        
=================================================================
Total params: 673
Trainable params: 673
Non-trainable params: 0
_________________________________________________________________
None
"""

matplotlibで2本の線で挟まれた領域を塗りつぶす

単に何かの領域を塗りつぶしたり、時系列データの予測モデルの信頼区間の可視化などで使われたり、
関数のグラフとx軸の間を塗りつぶしたりするあいつです。

matplotlibでは、fill_between というメソッドが用意されており、これを使って実現できます。
ドキュメント: matplotlib.axes.Axes.fill_between

通常の plot は xとyの値をリストか何かで渡しますが、fill_betweenでは、y1とy2という風にyの値を2ペア渡します。
(なお、y2を省略すると、y1とx軸の間を塗りつぶしてくれます。)

また、 y1 と y2 の間を全て塗りつぶすのではなく、 where で、塗りつぶす領域を指定することもできます。
where に渡すのは x と同じ長さの True or False のリストです。
TrueとTrueの間が塗りつぶされます。
False, True, False のような孤立したTrueの分は塗りつぶされないので注意が必要です。

この他、 interpolate という引数が用意されています。
これは where が使われていて、かつ二つの曲線が閉じている場合に、はみ出さないように綺麗に塗ってくれるオプションです。
とりあえずTrue指定しておいて良いと思います。
この後サンプルコードを紹介しますが、最後の一つのグラフはあえて interpolate を指定せずに少しガタついてるグラフにしました。


import numpy as np
import matplotlib.pyplot as plt

# データ作成
x = np.linspace(0, 2*np.pi, 101)
y1 = np.sin(x)
y2 = np.sin(2*x)

fig = plt.figure(figsize=(12, 8), facecolor="w")
ax = fig.add_subplot(2, 2, 1, title="2線の間を全て塗りつぶす")
ax.plot(x, y1, label="sin(x)")
ax.plot(x, y2, label="sin(2x)")
ax.fill_between(
    x,
    y1,
    y2,
    alpha=0.3,
    interpolate=True,
)
ax.legend()

ax = fig.add_subplot(2, 2, 2, title="Whereで塗りつぶす領域を絞り込む")
ax.plot(x, y1, label="sin(x)")
ax.plot(x, y2, label="sin(2x)")
ax.fill_between(
    x,
    y1,
    y2,
    where=(y1 >= y2),
    alpha=0.3,
    interpolate=True,
    label="sin(x)>=sin(2x)"
)
ax.fill_between(
    x,
    y1,
    y2,
    where=(y1 < y2),
    alpha=0.3,
    interpolate=True,
    label="sin(x)<sin(2x)"
)
ax.legend()

ax = fig.add_subplot(2, 2, 3, title="y2を省略するとx軸との間を塗りつぶす")
ax.plot(x, y1, label="sin(x)")
ax.fill_between(
    x,
    y1,
    alpha=0.3,
    interpolate=True,
)
ax.legend()

ax = fig.add_subplot(2, 2, 4, title="interpolate=Trueを指定しないと隙間が発生しうる")
ax.plot(x, y1, label="sin(x)")
ax.plot(x, y2, label="sin(2x)")
ax.fill_between(
    x[::10],
    y1[::10],
    y2[::10],
    alpha=0.3,
)
ax.legend()

plt.show()

結果。

matplotlibのxkcdスタイルのパラメーターを変えてみる

実用性は皆無なのですが、他にやっている人を見かけなかったのでやってみました。
前回の記事で紹介した matplotlibのxkcdスタイルの続きです。
ドキュメントを読めば明らかなのですが、 plt.xkcd()には3種類の引数を渡すことができます。

ドキュメント: matplotlib.pyplot.xkcd

3つの引数と意味はそのまま引用します。

scale : float, optional
The amplitude of the wiggle perpendicular to the source line.

length : float, optional
The length of the wiggle along the line.

randomness : float, optional
The scale factor by which the length is shrunken or expanded.

初期値は (scale=1, length=100, randomness=2) です。

色々試したところ、 scaleと randomness は 増やすと徐々にグラフが崩れていき、
length は減らすと崩れていくようです。

初期値と、それぞれ値を変更した3パターンをグラフ出力してみました。
(randomness はこれだけ変えても変化がわかりにくかったので、scaleも変更しています。)


import matplotlib.pyplot as plt
import numpy as np


# グラフを描く処理は共通化
def graph_plot(ax):
    X0 = np.linspace(0, 2*np.pi, 200)
    Y_sin = np.sin(X0)+2
    Y_cos = np.cos(X0)+2
    X1 = np.arange(7)
    Y1 = (X1 ** 2)/36
    ax.plot(X0, Y_sin, label="$y=\\sin(x)$")
    ax.plot(X0, Y_cos, label="$y=\\cos(x)$")
    ax.bar(X1, Y1, alpha=0.3, color="g")
    ax.legend()


fig = plt.figure(figsize=(12, 10), facecolor="w")
# 間隔調整
fig.subplots_adjust(hspace=0.3, wspace=0.3)
# xkcd オプションの影響を局所化するため with で使う。
with plt.xkcd(scale=1, length=100, randomness=2):
    ax = fig.add_subplot(2, 2, 1, title="default")
    graph_plot(ax)

with plt.xkcd(scale=2, length=100, randomness=2):
    ax = fig.add_subplot(2, 2, 2, title="scale=2")
    graph_plot(ax)

with plt.xkcd(scale=1, length=50, randomness=2):
    ax = fig.add_subplot(2, 2, 3, title="length = 50")
    graph_plot(ax)

with plt.xkcd(scale=2, length=100, randomness=6):
    ax = fig.add_subplot(2, 2, 4, title="scale=2, randomness=6")
    graph_plot(ax)
plt.show()

出力されるのがこちらです。

結構雰囲気変わりますね。
とはいえ、あまりやりすぎるとくどくなるので、初期設定だけで困ることもなさそうです。
(そもそもこのスタイルが必要になる場面も基本的に無いのですが。)

全くの余談ですが、matplotlibのドキュメントページのULRにxkcdをつけるとドキュメントのスタイルが変わります。
(よくみると内容も変わってっています。)

お暇な時に見比べてみてください。
https://matplotlib.org/
https://matplotlib.org/xkcd/