MeCabの設定ファイルや辞書、ツールの配置場所をコマンドで取得する

発端としては、MeCabをより理解するために、自分でユーザー辞書を作ってみようとしました。そのためドキュメントの単語の追加方法のページを参照しました。
参考: MeCab: 単語の追加方法

そこには以下のコマンドが載っていました。

% /usr/local/libexec/mecab/mecab-dict-index -d/usr/local/lib/mecab/dic/ipadic \
-u foo.dic -f euc-jp -t euc-jp foo.csv

早速これを試そうと思ったのですが、自分の環境に /usr/local/libexec/mecab/ ディレクトリがありませんでした。

色々探し回った結果、偶然、
/usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index
というパスにこのコマンドがあるのを見つけたのですが、本来はどういう手順でこれを見つけられるのかな?と思って調べた結果が今回の記事です。

結果としてわかったのは、 mecab-config というコマンドで、設定ファイルや辞書ファイル、そして今探してた辞書のコンパイルコマンドなどのコマンドの配置場所がわかるということです。

mecab-config のドキュメントを探したのですが、公式サイトではこれに関連する記述は見つかりませんでした。

そのため、とりあえず –help をつけてみたのですが、使えるオプションが羅列されただけでした。なので、それらしいものを試します。

$ mecab-config --help
Usage: mecab-config [OPTIONS]

Options:
  [--cxx]
  [--prefix[=DIR]]
  [--exec-prefix[=DIR]]
  [--libs]
  [--cflags]
  [--dicdir]
  [--libexecdir]
  [--sysconfdir]
  [--libs-only-L]
  [--libs-only-l]
  [--inc-dir]
  [--help]
  [--version]

実際にこれらのオプションをつけて動かした所、とくに dir と付いてるオプションをつけると必要なディレクトリの場所がわかることが確認できました。
例えば以下の通りです。

# 設定ファイル(つまり mecabrc) のある場所
$ mecab-config --sysconfdir
/usr/local/etc

# 辞書が配置されている場所
$ mecab-config --dicdir
/usr/local/lib/mecab/dic

# 辞書コンパイル等のコマンドの配置場所
$ mecab-config --libexecdir
/usr/local/Cellar/mecab/0.996/libexec/mecab

最後の、–libexecdir 引数をつけて実行した結果の場所に目当ての mecab-dict-index などのファイルがあります。

$ ls /usr/local/Cellar/mecab/0.996/libexec/mecab
mecab-cost-train  mecab-dict-index  mecab-test-gen
mecab-dict-gen    mecab-system-eval

この他にも辞書の配置場所がコマンドで取得できるというのは非常に便利ですね。実はローカル(Mac)とサーバー(AWS EC2)など、辞書の配置場所が違うケースがあります。デフォルトの辞書を使う場合は問題無いのですが、NEologdなどを使う場合は、辞書の配置場所によってコマンドを変える必要がありました。それが次のように統一したコマンドで呼び出せるようになります。

$ mecab -d `mecab-config --dicdir`/mecab-ipadic-neologd
# 次の書き方でも可
$ mecab -d $(mecab-config --dicdir)/mecab-ipadic-neologd

これは非常に便利だ、と思ってPythonでも同様の書き方をしようとすると、こちらはエラーになります。次のコードは動きません。

import MeCab

# 次のコードは動かない
MeCab.Tagger("-d $(mecab-config --dicdir)/mecab-ipadic-neologd")
# 以下のコードも動かない
MeCab.Tagger("-d `mecab-config --dicdir`/mecab-ipadic-neologd")

どうしてもPythonコード中で mecab-config –dicdir を使って自動的に辞書のパスを取得したい場合は、 os か subprocess などのモジュールを使ってコマンドを走らせ、その結果を取得して Taggerの引数の文字列に埋め込む必要があるようです。例えば次のようにするとできます。

import subprocess
import MeCab


dicdir = subprocess.getoutput("mecab-config --dicdir")
tagger = MeCab.Tagger(f"-d {dicdir}/mecab-ipadic-neologd")

これでローカルで開発して、辞書の配置場所が違うようなサーバーで動かすプログラムを作る場合も、共通のコードで動くようになります。

Pythonの文字列を分割するメソッドたちの紹介

このブログでMeCabを使った形態素解析をするとき、MeCabが出力する文字列を行単位に分割するときはいつもstr.split(“\n”)のような書き方をしていました。しかし、最近Pythonのstrオブジェクトが、splitlines() という行単位で区切る専用のメソッドを持っているのに気づいたのでその紹介です。といっても、splitlines()で改行コードで区切れるぜ、だけだと記事が一瞬で終わってしまうので、ついでにPythonのstr型が持っている3つの分割メソッド[split()/ rsplit()/ splitlines()]をそれぞれ紹介しようと思います。

ドキュメントはこちらのページにあります。
参考: 組み込み型 — Python 3.9.4 ドキュメント

まずはいつも使っている str.split() です。
呼び出しは、 str.split(sep=None, maxsplit=-1) のように定義されています。sepが区切り文字で、maxsplit が最大何箇所で分割るかの指定です。分割された結果は最大でmaxsplit+1個の要素になるので注意してください。 デフォルトの -1 の場合は、区切れる場所全部で区切ります。

# 文字列を, (カンマ) で区切る
print("a,b,c,d,e,f,g".split(","))
# ['a', 'b', 'c', 'd', 'e', 'f', 'g']

# スペースで区切る場合
print(" a b c  d e f g ".split(" "))  # cとdの間にはスペース2個入れた
# ['', 'a', 'b', 'c', '', 'd', 'e', 'f', 'g', '']

# maxsplit = 3 を指定。 3箇所で区切られ、長さ4の配列になる
print("a,b,c,d,e,f,g".split(",", 3))
# ['a', 'b', 'c', 'd,e,f,g']

# maxsplit が十分大きい時は指定しないのと同じ
print("a,b,c,d,e,f,g".split(",", 100))
# ['a', 'b', 'c', 'd', 'e', 'f', 'g']

一つ目のセパレーター引数(sep)ですが、これを省略することができます。省略すると、半角スペースやタブ、改行などの空白文字で区切られます。そして、さらに特殊な挙動として、連続する空白文字をひとつのセパレーターとして見なすようになります。また、文字列の先頭や末尾に空白があっても、結果の最初や最後に空文字列は含まれません。それぞれ見ていきましょう。

text = """
最初に改行がある複数行のテキストです。
この行には  連続した半角スペースがあります。

タブも\t入れてみました。
この下に空白行があります。


"""
print(text.split())
# ['最初に改行がある複数行のテキストです。', 'この行には', '連続した半角スペースがあります。', 'タブも', '入れてみました。', 'この下に空白行があります。']

split(” “)だと、スペースが連続してたらり、先頭/末尾にスペースがあると結果の配列に空白文字列が含まれていましたが、それがないのがわかりますね。また、複数の種類の文字(半角スペースやタブや改行)で、全部区切ってくれるのも便利です。

続いて、str.rsplit() を見ていきましょう。 使い方は str.rsplit(sep=None, maxsplit=-1) であり、splitと同じです。 挙動もほぼ同じなのですが、splitとの違いは右から順番に分割していくことです。maxsplit を指定しないと、分割できるところは全部分割するので、splitとrsplitの結果は同じになります。それでは、maxsplit に少し小さめの数値を設定して違いを見てみましょう。

text = "a,b,c,d,e,f,g"
print(text.split(",", 2))
# ['a', 'b', 'c,d,e,f,g']

print(text.rsplit(",", 2))
# ['a,b,c,d,e', 'f', 'g']

雰囲気伝わったでしょうか。

URLを / で分解して、右からn番目の要素を取るなどの使い方ができそうですね。
うちのブログだと、右から2番目がカテゴリを表す文字列です。

# このブログの記事URLの例
url ="https://analytics-note.xyz/machine-learning/scikit-learn-n-gram/"

# 右から4分割した結果。
print(url.rsplit("/", 3))
# ['https://analytics-note.xyz', 'machine-learning', 'scikit-learn-n-gram', '']

# インデックス0 にhttpsからドメイン名まで入る
print(url.rsplit("/", 3)[0])
# https://analytics-note.xyz

# インデックス1がカテゴリ名
print(url.rsplit("/", 3)[1])
# machine-learning

そして、最後が str.splitlines()です。 使い方は str.splitlines([keepends]) であり、セパレーターも分割回数も指定できません。keepends は 分割結果に末尾の改行コードを残すかどうかで、Trueと判定される何かを入れておくと改行コードが残ります。あまり必要ないので、何も入れなくていいでしょう。

import MeCab
tagger = MeCab.Tagger()
result = tagger.parse("すもももももももものうち")


# MeCabの結果を表示しておく
print(result)
"""
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
"""

# splitlines() で分割した配列
print(result.splitlines())
"""
[
'すもも\t名詞,一般,*,*,*,*,すもも,スモモ,スモモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'の\t助詞,連体化,*,*,*,*,の,ノ,ノ',
'うち\t名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ',
'EOS'
]
"""

splitlines() でサクッと行ごとに分離できていますね。いつも使っているsplit(“\n”)との違いも見ておきましょう。

print(result.split("\n"))
"""
[
'すもも\t名詞,一般,*,*,*,*,すもも,スモモ,スモモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'の\t助詞,連体化,*,*,*,*,の,ノ,ノ',
'うち\t名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ',
'EOS',
''
]
"""

一見同じ結果に見えますが、最後に”という空白文字列の要素が入ってます。不要な’EOS’部分を切り捨てるときに切り落とす長さがかわるのでこれは注意して使いましょう。

こうしてみると、 splitlines でも split でもどちらを使っても良さそうですが、splitlinesには明確なメリットがあります。ドキュメントを見ていただけると一覧表が載っているのですが、splitlinesはかなり多くの環境の様々な改行コードや垂直タブ、改ページなど多くの特殊文字で区切ってくれます。split(“\n”)だと、改行コードが違う別の環境ではちょっと動作が不安なので、splitlinesを使った方が汎用性の高いコードになると期待できます。

少し気になる挙動なのですが、「{改行コード}テキスト{改行コード}」という文字列を分解すると、先頭の{改行コード}では区切って1要素目に空白文字列を返してくるのに、末尾の{改行コード}は、その後ろに何もなければ{改行コード}を消して終わります。

print("""
先頭と末尾に改行コード
""".splitlines()
)
# ['', '先頭と末尾に改行コード']

なぜこのような挙動になるのか調べたのですが、結果的に、keepends を指定して動きを見ると理解できました。

"""
aaa
bbb
""".splitlines(True)
# ['\n', 'aaa\n', 'bbb\n']

上記のコードの通り、splitlinesは「改行コードの後ろ」で区切ってるんですね。そして、keependsがFalse(もしくは未指定)の場合は、この区切られた結果から末尾の改行コードを消しているようです。

"""
aaa
bbb
""".splitlines()
# ['', 'aaa', 'bbb']

split() (sep指定なし)は、連続する空白文字をまとめて一つのセパレーターとして扱っていましたが、splitlinesにはそのような機能はなく、普通に改行の数だけ区切って空白文字列だけの要素を返してきます。

print("""この下に連続した改行



この上に連続した改行""".splitlines()
)
# ['この下に連続した改行', '', '', '', 'この上に連続した改行']

今回の記事で紹介したメソッドたちは大変シンプルな機能なのですが、不注意に使うと思ってた結果と要素数やインデックス等がずれたりするので、慣れるまではよく確認しながら使いましょう。

Pythonのリストをn個に分割する

めったに使わないのですが、前回の記事がリストをn個ずつに分割するだったので今回はリストをn個のリストに分割する方法を紹介します。
ちなみに、目的が機械学習のクロスバリデーションであれば、scikit-learnに専用のメソッドがあるのでそちらを使いましょう。
今回の記事はそれ以外の用途で、何かしらの事情があってリストをn分割する必要が発生した時に使います。

さて、まず簡単に思いつくのは前回の記事同様にリストのスライスを使う方法です。
元のデータのサイズをnで割って区切り位置を決め、その位置で区切ります。
コードにすると次のようになりますね。
例として、サイズが23のデータを5分割しています。
途中、スライスする位置をintで整数に丸めているのは、単にスライスの表記が整数しか受け付けないからです。

# サンプルのデータ生成
data = list(range(23))
print(data)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
print(len(data))
# 23

# 5分割する
n = 5
size = len(data)

for i in range(n):
    start = int((size*i/n))
    end = int((size*(i+1)/n))
    print(data[start:end])
"""
[0, 1, 2, 3]
[4, 5, 6, 7, 8]
[9, 10, 11, 12]
[13, 14, 15, 16, 17]
[18, 19, 20, 21, 22]
"""

特に何も変哲のないコードですし、無事にリストが5分割されました。

ただ、一点気持悪いというか少なくとも僕の好みには合わない点があります。
それが分割結果の各リストのサイズです。数えてみると、4個、5個、4個、5個、5個、となっています。23が5で割り切れないので、数が不揃いになるのは仕方ないのですが、個人的には、4/4/5/5/5 か、 5/5/5/4/4 のどちらかで切りたいです。

しかし、これを実装するのはそこそこ手間がかかります。元のデータ長を分けたいグループ数で整数除算し、商とを余を求めて分割後の各グループに属する要素数を求め、その要素数から区切り位置を決め、その位置で切る手順をコードに起こす必要があるからです。
やってみたのが次のコードです。(確認用のprint文や説明のコメントのせいで余計に面倒なコードに見えてしまっていますね。)

import numpy as np


# data は上のコード例と同じものを使う。
data = list(range(23))
n = 5
size = len(data)

# データの件数を分けたいグループ数で割って商と余りを求める
quotient, remainder = divmod(size, n)
print("商:", quotient)
# 商: 4
print("余り:", remainder)
# 余り: 3

# [0] に続けて各グループの要素数を指定するリストを作る
section_sizes = ([0] + remainder * [quotient+1] + (n-remainder) * [quotient])
print(section_sizes)
# [0, 5, 5, 5, 4, 4]

# 累積和をとって、スライスする点のリストにする
slice_points = list(np.cumsum(section_sizes))
print(slice_points)
# [0, 5, 10, 15, 19, 23]

# 作成したスライス位置を使ってリストを切る
for i in range(n):
    start = slice_points[i]
    end = slice_points[i+1]
    print(data[start:end])

"""
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]
[10, 11, 12, 13, 14]
[15, 16, 17, 18]
[19, 20, 21, 22]
"""

はい、これで、5個、5個、5個、4個、4個、に区切れましたね。

途中累積和を取るためにnumpyをインポートしてcumsumまで使っています。
ただ、どうせnumpyを使うことになるのであれば、実はnumpyに専用のメソッドが用意されているので断然そちらがお勧めです。

参考: numpy.array_split

numpyのarray用に実装されたメソッドだと思いますが、ただのlistに対しても動作してくれます。これを使うと、たったこれだけのコードになります。

data = list(range(23))
n = 5
print(np.array_split(data, n))
"""
[array([0, 1, 2, 3, 4]),
 array([5, 6, 7, 8, 9]),
 array([10, 11, 12, 13, 14]),
 array([15, 16, 17, 18]),
 array([19, 20, 21, 22])]
"""

めっちゃ簡単ですね。メソッドの戻り値はn分割した各グループのリストになります。
分割された各グループは array 型に変換されるのでその点だけ注意してください。
元のデータがarray型でなくても結果はarray型になります。

Pythonのリストをn個ずつに分割する

今回の記事はPythonのlistのスライスの小ネタです。リストを長さnのリストに分割する方法を紹介します。

これは次のようなコードで実現できます。
例として、長さが17のリストを生成し、n=5個ずつに分けてprintしています。

data = list(range(17))
print(data)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
print(len(data))
# 17

n = 5
for i in range(0, len(data), n):
    print(data[i: i+n])

"""
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]
[10, 11, 12, 13, 14]
[15, 16]
"""

range(0, len(data), n)によって、 0, 5, 10, 15 というn=5 ずつ増える数列を生成して、[i: i+5]というスライスで配列を切り出しているだけですね。

さっと書いて動くのを見ると何の変哲もないコードのように思えますが、実はPythonのlistのスライスの非常に便利な仕様を活用しています。
それは、スライスの範囲が元のリストのインデックスからはみ出していても問題なく動くと言うことです。

i=15の時、[i: i+5]は[15: 20]ですが、元のlist は長さが17なので、20番目の要素などありません。しかし、このスライスは、切り出せた分だけ切り出して動いてくれるのでこのコードがエラーにならずに動作するのです。

もしこの仕様がなければ、i+nが配列の長さを超えていないかどうかで分岐を一つ書く必要があるところでした。

ちなみに、スライスではなくインデックスで要素を取り出す場合は当然ですがインデックスの最大値を超えた値を入れるとエラーになります。

print(data[15: 20])  # スライスの範囲がlistの長さを超えていても動く
# [15, 16]

print(data[100: 200])  # これも動く
# []

try:
    print(data[20])  # これはエラーになる
except Exception as e:
    print(e)
# list index out of range

正直この技を使う機会はあまりないのですが、例えばAmazon Comprehendような1回に渡せるデータ数に上限があるAPIで大量のデータを処理するときなどに利用できます。
APIが受け入れてくれる上限のデータ数でリストを区切って順に実行したりできますね。

Pythonの所属検査演算(in)について

Pythonでは、ある要素が集合や配列に存在しているかどうか、inという式を使って判定できます。この度改めてドキュメントを読んでみたのと、配列の配列などちょっと特殊な用途について挙動を調べたのでまとめておきます。

ドキュメントはこちらです。in のことは所属検査演算と呼ぶようです。URLから推測すると英語名は、 membership test operations のようですね。
参考: 6.10.2. 所属検査演算 式 (expression) — Python 3.9.4 ドキュメント

演算子 in および not in は所属関係を調べます。とある通りで、
x in s は xがsの要素だったらTrue、そうでない場合はFalseを返します。not in は in の否定です。この記事ではこの s を色々変えながら挙動を見ていきましょう。

配列や集合、タプルに対する挙動

まずは一番基本的な配列や集合に対する挙動です。まず配列についてみていきますが、これは特に説明することもなく、配列sの要素のどれかとxが一致すれば x in s はTrueになります。

list_data = [1, 2, 3, 4, 5]
print(3 in list_data)
# True
print(8 in list_data)
# False
print(2 not in list_data)
# False

集合やタプルの場合も同様です。タプルはこれ以降コード例を省略しますが配列と同じように動きます。

set_data = {1, 2, 3, 4, 5}
print(3 in set_data)
# True
print(8 in set_data)
# False
print(2 not in set_data)
# False

tuple_data = (1, 2, 3, 4, 5)
print(3 in tuple_data)
# True
print(8 in tuple_data)
# False
print(2 not in tuple_data)
# False

ここで、少し注意が必要なのは、 None についても機能するということです。SQLの挙動に慣れていると、NULLが絡むとTrueでもFalseでもなくNULLが返ってくるので、Noneが絡むとNoneが返ってくるような気がしてしまいますが、None == None とみなすようでSQLとは違った動きになります。

print(None in [1, 2, None, 3])
# True
print(None in [1, 2, 4, 5])
# False
print(None in {1, 2, None, 3})
# True

もっと言うと、numpyの nan についても使えます。np.nan == np.nan は False なのでこれは不思議な挙動です。

import numpy as np


print(np.nan == np.nan)
# False
print(np.nan in [1, 2, 3, np.nan])
# True

配列の部分列や、集合の部分集合については使えません。 xがsの部分列や部分集合の場合も
x in s はFalseが返ってきます。
部分集合のジャッジをしたい場合は不等号が使えるのでそちらを使いましょう。

# 部分列はFalseになる
print([2, 3] in [1, 2, 3, 4])
# False

# 部分集合もFalseになる
print({2, 3} in {1, 2, 3, 4})
# False

# 部分集合は不等号で判定できる。
print({2, 3} <= {1, 2, 3, 4})
# True

次に、配列の配列について検証しましたが、なんとこれが正常に動作します。hashableな形でないとダメだと思い込んでいたので意外でした。

# 配列の配列も動く
print([1, 2] in [[1, 2], [3, 4], [5, 6]])
# True

# もちろん、含まない場合はFalse
print([2, 3] in [[1, 2], [3, 4], [5, 6]])
# False

# 要素の要素についてはFalseになる
print(3 in [[1, 2], [3, 4], [5, 6]])
# False

では、集合の集合は?と思ったのですが、集合(set)はhashableなものしか要素に持てないので、集合の集合自体作れません。なので気にしなくて大丈夫です。
タプルのタプルは、当然配列と同じように動作してくれます。

辞書(dict)に対する挙動

辞書sに対して、x in s を使うと、xが辞書sのキーに含まれていた場合にTrue、含まれていない場合にFalseを返してくれます。キーではなく値の中にあるかどうかを知りたいってばあいはvalues()、キーだけでなくキーと値のペアで含まれているかどうかを知りたいって場合はitems()をそれぞれ併用しましょう。

dict_data = {
    "apple": "りんご",
    "orange": "みかん",
    "banana": "バナナ" 
}

# キーの中に一致するものがあればTrue
print("apple" in dict_data)
# True

# keys()メソッドでキーの一覧を取得して判定しても挙動は同じ
print("apple" in dict_data.keys())
# True

# 値の中に一致するものがあったとしてもこれはFalse
print("みかん" in dict_data)
# False

# 値の中に一致するものがあるかどうか見る場合は、values()メソッドを使う
print("みかん" in dict_data.values())
# True

# キーと値のペアで判定をしたい場合はitems()メソッドを使う。
print(("apple", "りんご") in dict_data.items())
# True

# キーと値がそれぞれ存在していても組み合わせが違うとFalseになる
print(("apple", "バナナ") in dict_data.items())
# False

文字列に対する挙動

文字列sと、文字x対してinを使うと、xがsに含まれている場合にTrueを返してきます。これだけだと、配列と要素の場合と同じように見えるのですが、実は文字列の独特の挙動として、文字列xが文字列sの部分文字列の場合もTrueを返してくれると言うものがあります。便利ですね。実装としては、 x in y は y.find(x) != -1 と等価になっているそうです。

# 文字が含まれていればTrue
print("c" in "abcde" )
# True

# 部分列であればTrue
print("bcd" in "abcde" )
# True

# 個々の文字が含まれていても順番が違うとFalse
print("ba" in "abcde" )
# False

文字列についてはもう一つ注意があって、空文字列は他の任意の文字列の部分文字列とみなされます。要するに次の式はどちらもTrueです。

print("" in "abcde")
# True

print("" in "")
# True

その他の型 (ユーザー定義型)における in

これまで、Pythonの基本的な各型における所属検査演算子の使い方を見てきましたが、各ライブライで実装されているようなクラスにおいても in は使えますし、自分で実装するクラスにおいても、inの振る舞いを定義して実装することができます。
その方法は、 class において、 __contains__() メソッドを実装することです。

__contains__() メソッドが実装されているクラスにおいては、
x in y は、 y.__contains__(x) が Trueを返す場合にTrueになり、そうでない場合にFalseになります。

実験したところ、__contains__が、if文でTrueと判定されるようなもの、(空白ではない文字列、0ではない数値、空ではない配列など)を返した場合は Trueになり、if文でFalseと判定されるようなもの(False,None,0など)を返した場合はFalseになるので、 __contains__ と in の結果が一致する、と言うわけではないようです。

大変奇妙な例で恐縮ですが実験したのが次の結果です。

class myclass():
    def __contains__(self, y):
        return "含みます"


mc = myclass()

# __contains__ メソッドを呼び出すとメソッドの結果がそのまま返される
print(mc.__contains__("a"))
# 含みます

# in だと True か False に変換される
print("a" in mc)
# True

__contains__ が実装されていないが __iter__ が実装されているクラスの場合(要するにイテレーター)の場合は、反復の途中で x に等しい要素が登場した場合に Trueになります。
また変な例なのですが、__iter__(と、セットで使う__next__)だけ実装したようなクラスを作ったのでそれで実験します。このクラスは[1,2,3,4,5]を順番に返します。

class myclass2():
    def __init__(self):
        self._i = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._i == 5:
            raise StopIteration()
        self._i += 1
        return self._i


mc2 = myclass2()
print(2 in mc2)
# True
mc2 = myclass2()
print(6 in mc2)
# False

__contains__も__iter__も実装されていない場合は、最後に、__getitem__()が試されます。
__getitem__() は 辞書型のように[]でアクセスしてきた時の挙動を定義する特殊メソッドですね。これは単に x == y[i] となる iが見つかれば True, そうでない場合はFalseとなるようです。
これもまた変な例ですが、__getitem__だけ実装されたクラスで実験しました。

class myclass3():
    def __getitem__(self, i):
        # 無限ループを避けるためにiが大きくなったらエラーにする
        if i >= 100:
            raise
        return i**2


mc3 = myclass3()

# 平方数ならTrue、 mc3[4] == 16 だから。
print(16 in mc3)
# True

# 平方数でない場合はエラーになるまで探し続ける
print(18 in mc3)
# RuntimeError: No active exception to reraise

改めてドキュメントを読んでみて色々試した結果、それなりに理解が深まった気がします。

Pandasのデータを割合に変換する

業務で集計したデータを「実数だけではなく割合でも出して欲しい」というオーダーを受けることはよくあります。そんな時に、PandasのDataFrameのデータを列ごとや、行ごとの割合に変換する方法のまとめです。

DataFrameの話に入る前に、まずSeries型でやってみましょう。これは非常に簡単で、元のデータをその合計で割るだけです。

import pandas as pd


# 元のデータ
sr = pd.Series([30, 0, 40, 30, 10])
# 合計で割ると割合になる
print(sr/sr.sum())
"""
0    0.272727
1    0.000000
2    0.363636
3    0.272727
4    0.090909
dtype: float64
"""

続いて、DataFrame のデータを列ごとに、その列の値の和に占める割合に変換する方法を見ていきます。実はこれも簡単でDataFrameにたいしてsum()メソッドを実行すると列ごとの和が得られ、元のDataFrameをその和で割るといい感じにブロードキャストされて望む結果が得られます。
ブロードキャストについてはこちらも参照ください。今回の例で言えば、型が(5, 3) と (3,) なのでブロードキャストされます。
参考: NumPyのブロードキャストで変換できる型

# 元のデータを生成する
df = pd.DataFrame(
    {
        "col1": [0, 60, 80, 60, 0],
        "col2": [10, 80, None, 20, 40],
        "col3": [30, 0, 40, 30, 10],
    }
)
print(df)
"""
   col1  col2  col3
0     0  10.0    30
1    60  80.0     0
2    80   NaN    40
3    60  20.0    30
4     0  40.0    10
"""

# sum() すると列ごとの和が得られる
print(df.sum())
"""
col1    200.0
col2    150.0
col3    110.0
dtype: float64
"""

print(df/df.sum())
"""
   col1      col2      col3
0   0.0  0.066667  0.272727
1   0.3  0.533333  0.000000
2   0.4       NaN  0.363636
3   0.3  0.133333  0.272727
4   0.0  0.266667  0.090909
"""

ここからがこの記事の本題です。

列ごとに割合に変換するのは簡単でしたが、行ごとに割合に変換するのはこのようにはうまくいきません。sum(axis=1) で各行ごとの和は出せますが、それで元のデータフレームを割ろうとすると適切にブロードキャストされないからです。なんか変な結果が戻ってきます。

print(df/df.sum(axis=1))
"""
   col1  col2  col3   0   1   2   3   4
0   NaN   NaN   NaN NaN NaN NaN NaN NaN
1   NaN   NaN   NaN NaN NaN NaN NaN NaN
2   NaN   NaN   NaN NaN NaN NaN NaN NaN
3   NaN   NaN   NaN NaN NaN NaN NaN NaN
4   NaN   NaN   NaN NaN NaN NaN NaN NaN
"""

対処法はいくつかあると思います。一つは、「列ごとの処理は簡単で行ごとの処理が難しいなら行列入れ替えればいい」という発想に基づくものです。単純に転置して割合に変換した後もう一回転置します。

# 転置したDataFrameを作る
df_t = df.T
print((df_t/df_t.sum()).T)
"""
       col1      col2      col3
0  0.000000  0.250000  0.750000
1  0.428571  0.571429  0.000000
2  0.666667       NaN  0.333333
3  0.545455  0.181818  0.272727
4  0.000000  0.800000  0.200000
"""

もう一つ、applyメソッドをaxis=1を指定して使い行ごとのSeriesに対して、割合に変換する方法もあります。個人的にはこちらの方が若干スマートに思えます。
(ちなみに、axis=0 (デフォルト)で実行すると列ごとに割合に変換してくれます)

print(df.apply(lambda x: x/x.sum(), axis=1))
"""
       col1      col2      col3
0  0.000000  0.250000  0.750000
1  0.428571  0.571429  0.000000
2  0.666667       NaN  0.333333
3  0.545455  0.181818  0.272727
4  0.000000  0.800000  0.200000
"""

さて、 lambda 式の中で、 x.sum() ってメソッドが出てきますが、ここがちょっとしたコツです。ここで呼び出されるsum()はSeriesオブジェクトのメソッドのsum()なのですが、これはNaNを無視して和をとってくれます。そのため、index 2 の行(3行目)は、元の値が[80, NaN, 40] ですが、これの和を120として、元の値を割ってくれているわけです。

ここで、x.sum() とせずに、 sum(x)と、Python組み込みメソッドのsum()を呼び出すと結果が変わります。これはNaNを無視せず、NaNが含まれる和はNaNにしてしまうのです。そのため、sum(x)を使うと次のような結果になります。

print(df.apply(lambda x:x/sum(x), axis=1))
"""
       col1      col2      col3
0  0.000000  0.250000  0.750000
1  0.428571  0.571429  0.000000
2       NaN       NaN       NaN
3  0.545455  0.181818  0.272727
4  0.000000  0.800000  0.200000
"""

index 2 の行が全部 NaNになってしまいましたね。元のデータにNaNがなければ気にしなくて良い違いなのですが、うっかりしていると見落としがちな性質なので気をつけましょう。

当然ですが、Series型のデータに対しても、もし元のデータがNaNを含んでいたら、sum(sr)で割るのと、sr.sum()で割るのは結果が変わります。

sr = pd.Series([30, None, 40, 30, 10])
print(sr/sum(sr))
"""
0   NaN
1   NaN
2   NaN
3   NaN
4   NaN
dtype: float64
"""

print(sr/sr.sum())
"""
0    0.272727
1         NaN
2    0.363636
3    0.272727
4    0.090909
dtype: float64
"""

pandasの日付データを週単位で丸める(to_periodを使う方法)

前回の記事では、トレジャーデータで週単位の集計をする方法を紹介しましたが、今回はすでにDBから抽出が終わっているデータを週単位で集計する方法を紹介します。

日時の列をindexに設定してresampleするとか、方法はいろいろあるのですが、週単位の集計の場合、個人的にはto_period メソッドを使って丸めるのが一番気に入っているのでそれを紹介します。

ドキュメントはこちらです。
参考: pandas.Series.dt.to_period — pandas 1.3.2 documentation

とりあえずデータを作っておきます。

import pandas as pd


df = pd.DataFrame({
    'date': [
        '2021-08-01',
        '2021-08-02',
        '2021-08-03',
        '2021-08-04',
        '2021-08-05',
        '2021-08-06',
        '2021-08-07',
        '2021-08-08',
        '2021-08-09',
        '2021-08-10',
    ]
})
print(df)
"""
         date
0  2021-08-01
1  2021-08-02
2  2021-08-03
3  2021-08-04
4  2021-08-05
5  2021-08-06
6  2021-08-07
7  2021-08-08
8  2021-08-09
9  2021-08-10
"""

to_period メソッドは、 datetime系の型の列でなければ使えないので、pd.to_datetime() して型を変換します。

df["date"] = pd.to_datetime(df["date"])

さて、これで準備が整いました。1週間単位で日付を丸めたい場合は、最初の引数(freq)に”W”を指定して to_period メソッドを使えばOKです。

print(df["date"].dt.to_period("W"))
"""
0    2021-07-26/2021-08-01
1    2021-08-02/2021-08-08
2    2021-08-02/2021-08-08
3    2021-08-02/2021-08-08
4    2021-08-02/2021-08-08
5    2021-08-02/2021-08-08
6    2021-08-02/2021-08-08
7    2021-08-02/2021-08-08
8    2021-08-09/2021-08-15
9    2021-08-09/2021-08-15
Name: date, dtype: period[W-SUN]
"""

2021-08-01 は、 2021-07-26(月)〜2021-08-01(日)の週に丸められ、
2021-08-02〜2021-08-08は、2021-08-02(月)〜2021-08-08(日)の週に丸められましたね。
ちなみにこの結果は、Periodというデータ型になっています。
元と同じようにdatetime型で扱いたい場合や、丸めた週の頭の日付にしたいという場合はさらに変換する必要があります。自分はもっぱら次の形で使うことが多いです。

df["week"] = df["date"].dt.to_period("W").dt.to_timestamp()
print(df)
"""
        date       week
0 2021-08-01 2021-07-26
1 2021-08-02 2021-08-02
2 2021-08-03 2021-08-02
3 2021-08-04 2021-08-02
4 2021-08-05 2021-08-02
5 2021-08-06 2021-08-02
6 2021-08-07 2021-08-02
7 2021-08-08 2021-08-02
8 2021-08-09 2021-08-09
9 2021-08-10 2021-08-09
"""

あとは、この週単位に変換した列を使って groupbyして、sumなりcountなり望みの集計をすることで、Pandasのデータを週単位で集計することができます。

週単位以外の基準で集計したい場合、”W”以外の対応した文字を使えば実現可能です。
例えば月単位なら”M”、日単位なら”D”などです。
利用可能な集計基準と対応する文字は、こちらのページにまとまっています。
参考: Offset aliases

ただ、TD_TIME_TRUNCの記事でも似たような話を書きましたが、日単位や月単位で集計したいのであれば、to_periodして、timestumpに戻して、とやるよりも、strftimeなどを使った方が簡単だと思います。例えば月単位で集計したいなら次のように変換できます。

print(df["date"].dt.strftime("%Y-%m-01"))
"""
0    2021-08-01
1    2021-08-01
2    2021-08-01
3    2021-08-01
4    2021-08-01
5    2021-08-01
6    2021-08-01
7    2021-08-01
8    2021-08-01
9    2021-08-01
Name: date, dtype: object
"""

日単位の場合は、 “%Y-%m-%d”です。

ドキュメントになぜか記載がないのですが、週単位で丸める場合、週の始まりの曜日(実際にコードで指定するのは週の終わりの曜日)を指定することもできます。
方法は簡単で、”W”の代わりに、”W-WED”(水曜日終わり、木曜日始まり)、や、
“W-FRI”(金曜日終わり、土曜日始まり)などを指定します。
“W”は”W-SUN”(日曜日終わり、月曜日始まり)と同じ挙動になります。
基本的に”W”を使っていれば良いと思うのですが、開始日を変えたいこともあると思いますので覚えておくと役に立つ場面もあるかもしれません。

pandasのメソッドで、上位n件や下位n件のデータを取得する

先日紹介したbar chart raceのライブラリのドキュメントやソースコードを読んでいて、その中で nlargest というメソッドを見つけたのでその紹介です。その対となる nsmallest というメソッドもあります。

これが何をするメソッドとかというと、DataFrameやSeriesのデータの値が大きい方からn件(nlargest)や小さい方からn件(nsmallest)を取得してくれるものです。
え、sort_values() して、 head(n)やtail(n)すればいいじゃん、という声も聞こえてきそうですし、実際僕もそう思ってるのですが、多少の利点がちゃんとあるので読んでいただければ幸いです。

公式ドキュメントはこちらになります。
pandas.DataFrame.nlargest
pandas.DataFrame.nsmallest
pandas.Series.nlargest
pandas.Series.nsmallest

使い方は簡単で、Seriesの方であれば、取得したい件数を最初の引数nに渡してあげるだけ、DataFrameの方は、取得したい件数と合わせて、どの列の上位/下位を取得したのかを2つ目の引数columnsに渡してあげればOKです。

とりあえず、適当に作ったDataFrameに対して適当に列を指定して5項目ほど取得してみましょう。

import pandas as pd
import numpy as np


# 50行3列の乱数データを生成する
data = np.random.randint(1, 50, size=(50, 3))
df = pd.DataFrame(data, columns=["col1", "col2", "col3"])
print(df.shape)
# (50, 3)

print(df.nlargest(5, "col2"))
"""
    col1  col2  col3
46    30    48    28
17    47    47    31
33     9    45    30
16     3    44    33
26    16    44     2
"""

見ての通り、指定した”col2″でソートした上でその値が大きいものから順番に、5項目選択されています。

nlargest/ nsmallest にはもう一つ、keepという引数があります。これは、値が等し鋳物が複数あって、n位にランクインするものが一意に決められないときにその取り扱いを指定するものです。
“first”(デフォルト)を指定すると、元のデータで先に登場指定したものが優先され、”last”を指定すると、最後に登場したものが優先されます。また、”all”にすると、同率だったものが全部含まれます。

print(df.nlargest(5, "col2", keep="first"))
"""
    col1  col2  col3
46    30    48    28
17    47    47    31
33     9    45    30
16     3    44    33
26    16    44     2
"""

print(df.nlargest(5, "col2", keep="last"))
"""
    col1  col2  col3
46    30    48    28
17    47    47    31
33     9    45    30
40    48    44     1
26    16    44     2
"""

print(df.nlargest(5, "col2", keep="all"))
"""
    col1  col2  col3
46    30    48    28
17    47    47    31
33     9    45    30
16     3    44    33
26    16    44     2
40    48    44     1
"""

“col2″に値が44のレコードが3つ存在するのですが、”first”と”last”で選択されたレコードが違うのがわかりますね。そして”all”を指定すると3レコードとも返され、結果が6行になっています。

このkeep引数が存在することのほか、sort_values/head に比べると、速度面でも優れているそうです。

This method is equivalent to df.sort_values(columns, ascending=False).head(n), but more performant.

とドキュメントにもあります。
ソースを読んで無いので予想ですが、sort_values/headの方は最終的な結果に必要ない行まで全部ソートを完了させるに対して、nlargest/nsmallestの方は必要なデータだけ並べ替えてソートを打ち切ってるのではないかと思っています。

コードの実行例は載せませんでしたが、nsmallestもnlargestと同じように使うことができ、こちらは結果が小さい順に取得されます。

pandasのデータの順位を取得する

稀にではあるのですが、Pandasのデータ(DataFrame/Series)のデータの順位を取得したくなることがあります。
これまでは、DataFrameの列内の順位であれば、sort_valuesで並べ替えて、インデックスを振り直して、といった手順で対応することが多かったです。しかし、この方法では、値が等しい項目の扱いが少々厄介になります。また、最近、列内の順位ではなく、各行ごとに行内での順位を取得したいことがあり、ちょっと面倒だなと感じることがありました。

そこで、改めて調べてみたのですが、DataFrameもSeriesもそれぞれ、rankというメソッドを持っていて、これを使えば簡単に順位が取得できることがわかりました。
参考:
pandas.DataFrame.rank
pandas.Series.rank

使い方非常に簡単で、rank()を呼び出すだけです。適当なDataFrameでやってみます。

import pandas as pd


# 適当にデータを生成する
df = pd.DataFrame(
    {
        "col1": [20, 30, None, 20, 10, 20],
        "col2": [10, 50, 20, 20, 30, 60],
        "col3": [30, None, 60, None, 20, 80]
    }
)
print(df)
"""
   col1  col2  col3
0  20.0    10  30.0
1  30.0    50   NaN
2   NaN    20  60.0
3  20.0    20   NaN
4  10.0    30  20.0
5  20.0    60  80.0
"""

# 列内の順位を取得する
print(df.rank())
"""
   col1  col2  col3
0   3.0   1.0   2.0
1   5.0   5.0   NaN
2   NaN   2.5   3.0
3   3.0   2.5   NaN
4   1.0   4.0   1.0
5   3.0   6.0   4.0
"""

結果を見てわかる通り、順序は昇順で、値が小さいほど高順位(数値が小さい)ですね。

さて、このrank()メソッドはとても気が利いていて、多くの引数で細かく結果を制御できます。
まず、列ごとではなく、行ごとの順位が欲しい場合は、axis引数に1を渡します。
ちなみに、Seriesの方のドキュメントにも、axis引数があって、1を渡せるような記載があるのですがこれはおそらくドキュメントの誤りです。(普通にエラーになります。)

# 行内の順位を取得する
print(df.rank(axis=1))
"""
   col1  col2  col3
0   2.0   1.0   3.0
1   1.0   2.0   NaN
2   NaN   1.0   2.0
3   1.5   1.5   NaN
4   1.0   3.0   2.0
5   1.0   2.0   3.0
"""

昇順ではなく降順の順位が欲しい、という場合は、ascending にFalse を渡します。(デフォルトはTrueです。)

# 降順の順位を取得する
print(df.rank(ascending=False))
"""
   col1  col2  col3
0   3.0   6.0   3.0
1   1.0   2.0   NaN
2   NaN   4.5   2.0
3   3.0   4.5   NaN
4   5.0   3.0   4.0
5   3.0   1.0   1.0
"""

na_option という引数で、NaN値に対応する順位を指定できます。
“keep”(デフォルト) であれば、NaNのままです。
“top”にすると、最も高い順位(要するに1)がNaN値に振り分けられます。
“bottom”にすると、逆にもっとも低い順位が割り振られます。
それぞれ実行した結果が以下です。

df = pd.DataFrame(
    {"data":  [20, 30, None, 20, 10, 20]}
)
df["na_keep"] = df.data.rank(na_option="keep")
df["na_top"] = df.data.rank(na_option="top")
df["na_bottom"] = df.data.rank(na_option="bottom")

print(df)
"""
   data  na_keep  na_top  na_bottom
0  20.0      3.0     4.0        3.0
1  30.0      5.0     6.0        5.0
2   NaN      NaN     1.0        6.0
3  20.0      3.0     4.0        3.0
4  10.0      1.0     2.0        1.0
5  20.0      3.0     4.0        3.0
"""

さて、最初の方のコードの実行例で、2.5など小数点の順位のものがあるのがわかると思います。これは同率順位の項目に対して、デフォルトではその平均順位を返す設定になっているからです。
この設定は、 method 引数で制御できます。値はデフォルトの’average’の他、最小値(もっとも高順位)を採用する’min’、その逆に最大値を採用する’max’、元の配列に表示されていた順に順位がつく’first’、’min’と同じように、最小値が採用されるが、その次の順位の項目の順位が数が飛ばないように採番される’dense’の5種類の値が指定できます。
ちょっとわかりにくいと思うので実例でやってみます。

df = pd.DataFrame(
    {"data":  [20, 30, 40, 20, 10, 20, 40]}
)
df["m_average"] = df.data.rank(method="average")
df["m_min"] = df.data.rank(method="min")
df["m_max"] = df.data.rank(method="max")
df["m_first"] = df.data.rank(method="first")
df["m_dense"] = df.data.rank(method="dense")

print(df)
"""
   data  m_average  m_min  m_max  m_first  m_dense
0    20        3.0    2.0    4.0      2.0      2.0
1    30        5.0    5.0    5.0      5.0      3.0
2    40        6.5    6.0    7.0      6.0      4.0
3    20        3.0    2.0    4.0      3.0      2.0
4    10        1.0    1.0    1.0      1.0      1.0
5    20        3.0    2.0    4.0      4.0      2.0
6    40        6.5    6.0    7.0      7.0      4.0
"""

値が20の項目が3つあって順位的には、2位,3位,4位に相当するのですが、
averageであれば3、minであれば2、maxであれば4が割り振られているのが確認できましたね。firstであれば元の配列に出てきた通り、2,3,4位が当てられています。
そして、denseの結果を見ると、minと同様に20は2位になっているのですが、その次の30が、minの時は5位だったのに、denseでは欠番がなくこれが3位になっています。

あとは、あまり使わないと思うのですが、 pct という引数をTrueにすると、順位の数値ではなくパーセンタイルで結果が受け取れます。

df = pd.DataFrame(
    {"data":  [20, 30, 10, 20, 40]}
)
df["pct_false"] = df.data.rank(pct=False)
df["pct_true"] = df.data.rank(pct=True)
print(df)
"""
   data  pct_false  pct_true
0    20        2.5       0.5
1    30        4.0       0.8
2    10        1.0       0.2
3    20        2.5       0.5
4    40        5.0       1.0
"""

順位が一番低い項目が1になるのは想像通りですが、最高順位の項目は0では無いんですね。

スクラッチでBar Chart Raceを実装(コード供養)

前回の記事で、Bar Chart Raceを作るライブラリを紹介しましたが、実は僕はこのライブラリが登場するよりも前、スクラッチでBar Chart Raceを実装したことがあります。
便利なライブラリが登場したので、今後スクラッチで作ることはおそらく無いのですが、せっかく作ったコードが勿体無いので供養も兼ねて紹介させていただこうと思います。

棒の伸びもライブラリのように滑らかな動きでは無いですし、順位の入れ替わりなどもバーが上下に滑らかに移動して入れ替わるのではなく、パッと切り替わるなど、全体的にパラパラ漫画感が強く出てる出来栄えなのであまり期待せずによろしくお願いします。

データだけではライブラリ付属のコロナウィルス感染者のデータを拝借します。僕のコードはNaNに対応できないので、NaNは0埋めしておきます。

# データだけはライブラリから拝借
import bar_chart_race as bcr
# サンプルデータ読み込み
df = bcr.load_dataset('covid19')
# NaNに対応できてないので0埋めしておく
df.fillna(0, inplace=True)

では、早速作っていきます。実装としては、matplotlibのアニメーション機能を使います。
FuncAnimation を使うので、実装としては次の記事と似ています。
参考: matplotlibの3次元プロットを回転するアニメーションで保存する

まず、パラパラ漫画の各コマを生成する関数を実装します。

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker


def draw_barchart(date):
    target_row = df.loc[date]
    target_data = target_row.T.sort_values(ascending=True).tail(10)

    ax.clear()
    # 棒グラフを描写
    ax.barh(target_data.index, target_data.values)
    dx = target_data.max() / 200

    for i, (name, value) in enumerate(target_data.items()):
        # 棒の先端部に項目名を出力
        ax.text(value-dx, i, name, size=14, ha='right',
                va='bottom', color="white", weight=600)
        # 棒の先に値を出力
        ax.text(value+dx, i, f'{value:,.0f}', size=14, ha='left', va='center')

    # 日付を出力
    ax.text(1, 0.4, date.strftime("%Y-%m-%d"), transform=ax.transAxes,
            color='#777777', size=23, ha='right', weight=800)
    # x軸のメモリの設定
    ax.xaxis.set_major_formatter(ticker.StrMethodFormatter('{x:,.0f}'))
    ax.xaxis.set_ticks_position('top')
    ax.tick_params(axis='x', colors='#777777', labelsize=12)
    # y軸のメモリ(項目名)を消す
    ax.set_yticks([])

    ax.margins(0, 0.01)
    ax.grid(which='major', axis='x', linestyle='-')
    ax.set_axisbelow(True)

    # 外枠を消す
    plt.box(False)

コメントを多めに付けましたが、関数の中で順に、棒グラフを書いたり文字を挿入したりメモリを調整したりとコマを組み立てています。
上記の関数でパラパラ漫画のコマが描写できるので、それを使って、アニメーションにします。

import matplotlib.animation as animation

fig = plt.figure(figsize=(10, 6), facecolor="w")
ax = fig.add_subplot(111)

animator = animation.FuncAnimation(
    fig, draw_barchart, frames=df.index, interval=400)
animator.save('bar-chart-race.mp4', writer="ffmpeg")

これで出力されるのが次の動画です。

やっぱり全体的にカクカクなりますね。
データとデータの間を補完してコマ数をもっと増やすなどしないとなめらなかなアニメーションにならないようです。