DataFrameのexpandingについて

以前の記事で、pandas DataFrameの window関数である、 rollingを紹介しました。
参考:pandasで移動平均や高値線、安値線を計算する
これは、直近n個の値に対して和や最大値、最小値、平均や分散を求めるものです。

場合によっては、直近n個ではなく最初の値から全てに対して順に行いたいことがあると思います。
演算が和であればcumsumで累積和を取るように。

どうやらpandasのversion 0.18.0からそのような関数が実装されたようです。
pandas.DataFrame.expanding

とりあえずやってみましょう。
和だったらcumsumを使えば済む話なので、
平均と標準偏差でやってみます。
(元データが乱数だとそんなに面白いものでも無いので、結果は省略。コピーして動かしてみてください。)


# データ生成
import pandas as pd
import numpy as np
df = pd.DataFrame(np.random.randn(100, 5))
print(df.shape)  # (100, 5)

# 平均
df.expanding().mean()

# 標準偏差
df.expanding().std()

# aggを使ってまとめて算出も可能。
df.expanding().agg(["mean", "std"])

pandasのDataFrameから特定のデータ型の列を抜き出す

読み込んだデータフレームが、列ごとにバラバラのデータ型を持っていて特定の型の列だけ抜き出したいという場面はよくあります。
以前は dtype 属性で調べて列名をピックアップして処理したりしていたのですが、専用のメソッドがあったので紹介します。

pandas.DataFrame.select_dtypes

これを使うと文字列型の列だけとか、数値型の列だけとか値を抽出できます。

とりあえず実験用のデータフレーム作成。


import pandas as pd
import numpy as np
df = pd.DataFrame(
        {
            'a': np.random.randint(100, size=10),
            'b': np.random.choice([True, False], size=10),
            'c': np.random.randn(10),
            'd': np.random.choice(list('abcd'), size=10),
        }
    )
print(df.dtypes)
'''
a      int64
b       bool
c    float64
d     object
dtype: object
'''

使い方は簡単で、
select_dtypesの引数に必要なデータ型を値かリストで渡すだけです。
また、excludeで逆に不要なデータ型を指定することもできます。

例えば数値だけ欲しい時(列aと、列c)次のように指定できます。
3つとも結果は同じです。


df.select_dtypes([int, float])
df.select_dtypes([np.int, np.float64])
df.select_dtypes(["int", "float"])

欲しいデータ型一種類のときは配列ではなくスカラーで渡しても大丈夫です。


df.select_dtypes(object)
df.select_dtypes(bool)

逆に不要な型を指定する例。


df.select_dtypes(exclude=object)

Page Analytics でGoogle アナリティクスの情報を可視化する

プライバシーポリシーに書いている通り、このブログにはGoogle アナリティクスを導入しています。
日々のアクセス数が見れて、僕のモチベーションに大いに貢献してくれています。

このGoogle アナリティクスの情報ですが、Chromeの拡張機能を使うと、GAのサイトに行くことなく、自分のブログ上に表示することができます。
(所有者にしか使えないので、ぜひご自身のサイトでお試しください。)

その拡張機能がこちらです。
Page Analytics (by Google)
Chromeでリンク先へ遷移し、拡張機能をインストールするとURLバーの右にオレンジのアイコンが追加されます。
自分のサイトを開いた状態でこれをONにすると、そのページのアクセス情報が観れるという優れものです。

各リンクのクリック率をページ上に可視化したり、クリック率によって着色したりできて楽しいでおすすめです。

圧縮行格納方式(CRS)の疎行列のデータ構造

今回も疎行列のお話です。
前回の記事で登場したcrsとcscについて、具体的にどのようなデータ構造なのかを紹介します。
ちなみにcrsとcscはそれぞれ、
圧縮行格納方式 (Compressed Sparse Row) と、
圧縮列格納方式 (Compressed Sparse Column) の略です。
ほぼ同じ処理を行方向に行うか列方向の違いしかなく、転置を取るとそれぞれ入れ替わるので、 CSRの方を紹介します。

ちなみに、 wikipediaの説明で理解したので、それをみながら記事を書いています。
例として取り上げる行列はこれ。(wikipediaの例と同じ。)
$$
\left(
\begin{matrix}
1 & 2 & 3 & 0 \\
0 & 0 & 0 & 1 \\
2 & 0 & 0 & 2 \\
0 & 0 & 0 & 1 \\
\end{matrix}
\right)
$$

まず、csr形式のデータで作りましょう。
今回はarrayで作って変換するのではなく、お作法にしたがい、lil形式で生成してから変換します。


from scipy import sparse
# 成分が全て0の 4 * 4 の lil形式の疎行列を作成。
M_lil = sparse.lil_matrix((4, 4))
# 各成分を代入。
M_lil[0, 0] = 1
M_lil[0, 1] = 2
M_lil[0, 2] = 3
M_lil[1, 3] = 1
M_lil[2, 0] = 2
M_lil[2, 3] = 2
M_lil[3, 3] = 1
# M_csr形式に変換
M_csr = sparse.csr_matrix(M_lil)
# 確認
print(M_csr)
# 出力
'''
  (0, 0)    1.0
  (0, 1)    2.0
  (0, 2)    3.0
  (1, 3)    1.0
  (2, 0)    2.0
  (2, 3)    2.0
  (3, 3)    1.0
'''

これで、csr形式の変数、M_csrに例の行列が格納されました。
printすると整形されて表示されるのですが、実際のデータ構造はこうはなっていません。
wikipediaの説明と、ドキュメントをみながら確認します。
まず、 実際のデータは、次の3つの属性に格納されています。

data ・・・ CSR format data array of the matrix
indices ・・・ CSR format index array of the matrix
indptr ・・・ CSR format index pointer array of the matrix

具体例を見てから説明します。


print(M_csr.data)
# [1. 2. 3. 1. 2. 2. 1.]
print(M_csr.indices)
# [0 1 2 3 0 3 3]
print(M_csr.indptr)
# [0 3 4 6 7]

まず、data が 疎行列の0では無い要素の値を、左上から行方向(右側へ)に順番に並べたものです。
(csrのrが対応。 cscの場合はここで列方向(下向き)に並べたものになります。)
そして、indices が、それぞれの要素が、何列目の要素なのかを示す配列です。

明らかにわかるように、data と indices の要素の数はその疎行列の0では無い成分の個数です。
あとは、dataの各要素が何行目なのかがわかれば、行列を復元できますが、
それを担っているのが、indptr です。
これだけ、wikipediaの説明と異なっていて非常にわかりにくいですが、次のように解釈できます。


# 行列の最初の行のデータは、indptrの最初の2個のデータで作ったスライスの値
print(M_csr.data[0: 3])
# [1. 2. 3.]
# 次の行のデータは、indptrの一つずらした2個のデータで作ったスライスの値
print(M_csr.data[3: 4])
# [1.]
# 以下繰り返し
print(M_csr.data[4: 6])
# [2. 2.]
print(M_csr.data[6: 7])
# [1.]

明らかにわかる通り、 indptr の要素の個数は行の数より1つ大きくなります。

これで、csr_matrixの中のデータの構造がわかりました。
また、data属性の中に行単位でデータが固まって存在してて、
行単位の取り出しや演算が得意なことにも納得できると思います。

csc_matrixとcsr_matrix

前回の記事に続いて疎行列の話です。
参考:scipyにおける疎行列

今回は疎行列を実装する型(クラス)のうち、csc_matrixcsr_matrixの紹介です。
前回紹介した、lil_matrixには効率的に疎行列を構成できるメリットがありますが、
計算が遅いというデメリットがあります。
ドキュメントに次のように書いてある通り、演算するときはcsrかcsc形式に変換した方が良いです。
arithmetic operations LIL + LIL are slow (consider CSR or CSC)

とりあえず本当に遅いのか時間を測っておきましょう。
今回は 10000*10000の大きめの疎行列を作り、実験します。
(arrayの場合の時間も測定したいので、最初にarrayで作成して変換していますが、本来は最初からlil型で生成してメモリを節約の恩恵を受けた方が良いです。)
また、csrと、cscはそれぞれ行方向、列方向の取り出しに有利という特徴があるので、実験用データによって有利不利無いように対称行列を作りました。


from scipy import sparse
import numpy as np
# 疎行列作成
M_array = np.random.choice(
    [0, 1, 2, 3, 4, 5],
    p=[0.99, 0.002, 0.002, 0.002, 0.002, 0.002],
    size=[10000, 10000]
)
# 自身の転置行列と足し合わせて対称行列にする
M_array = M_array + M_array.T

# それぞれの型に変換する
M_lil = sparse.lil_matrix(M_array)
M_csc = sparse.csc_matrix(M_lil)
M_csr = sparse.csr_matrix(M_lil)

# 和をとるのにかかる時間を計測
%timeit M_array + M_array
# 406 ms ± 2.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit M_lil + M_lil
# 618 ms ± 7.04 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit M_csc + M_csc
# 7.92 ms ± 17.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit M_csr + M_csr
# 7.94 ms ± 51.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

ご覧の通り、 lil 同士の演算は通常のarray形式よりも遅く、一方でcscやcsr同士の演算は圧倒的に速くなりました。

あとは、cscとcsrの違いですが、それぞれ、列方向のスライスと行方向のスライスが効率的に行えます。
(あまり指摘されないようですが、スライスをとるだけであればarrayが一番早い。)

これもそれぞれみておきましょう。


# 列方向のスライス
%timeit M_array[:, 400]
# 225 ns ± 3.06 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit M_lil[:, 400]
# 11.4 ms ± 201 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit M_csc[:, 400]
# 151 µs ± 2.73 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit M_csr[:, 400]
# 2.86 ms ± 127 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

列方向のスライスについては、array が最速ではありますが、 csc が csr に比べてかなり早いことが確認できます。

次は行方向。


# 行方向のスライス
%timeit M_array[400, :]
# 235 ns ± 10.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit M_lil[400, :]
# 36.6 µs ± 1.37 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit M_csc[400, :]
# 2.9 ms ± 112 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit M_csr[400, :]
# 66 µs ± 3.39 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

今度は、csr が csc より速くなりました。
とはいえ、lil が csrよりも速く処理できています。(array最速は変わらず。)

これは、前回の記事で説明した通り、lilがrows属性に、行ごとの値を保有しているからのようです。

csrの方が演算が速く、わざわざ疎行列を作ったうえでなんの演算もせずにスライスで値を取り出すだけという場面もあまり無いので、
csrかcscを選んで使うことが多いと思いますが、行方向のスライスだけならlilが速いことは一応覚えておこうと思います。

scipyにおける疎行列

自然言語処理やレコメンドシステムの開発などをやっていると、サイズは非常に大きいが成分のほとんどは0という行列が頻繁に登場します。
そのような行列を疎行列と言います。
疎行列 – Wikipedia
これをそのまま扱うと、メモリは無駄に消費するし無駄な計算は増えるしで非常に効率の悪いことになります。
そのため、scipyでは疎行列を専門に扱う scipy.sparse というモジュールが用意されています。

ドキュメント: Sparse matrices (scipy.sparse)

リンク先を見たらわかる通り、疎行列を表す型は結構色々な種類があります。(Sparse matrix classes 参照)
それぞれ、他の型との変換が高速とか、行方向/列方向の取り出しが早いとか個別にメリットを持っていますが、
共通しているのはデータ量を大幅に節約できる点です。

今後の記事でいくつか紹介する予定ですが、とりあえずデータ量を削減できるって特徴だけでも確認しておきましょう。

疎行列のメリットを感じるにはかなり小さいのですが、乱数で10*10の疎行列(array型)を作って、
それを lil_matrix に変換して、中のデータを見てみましょう。

まずデータ作成。
(今回はサンプルとして、array型でデータを作って変換していますが、
省メモリの恩恵をきちんと受けるには最初から疎行列でデータを生成するべきである点には注意してください。)


from scipy import sparse
import numpy as np
M_array = np.random.choice(
    [0, 1, 2, 3, 4, 5],
    p=[0.9, 0.02, 0.02, 0.02, 0.02, 0.02],
    size=[10, 10]
)
M_array

# 出力例
array([[0, 3, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 0, 4, 0, 0, 0, 0],
       [0, 0, 0, 0, 5, 0, 3, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
       [0, 5, 0, 0, 0, 3, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

これを型変換します。方法は、strとintの変換と似たような感じで、lil_matrix(D)のようにするとできます。


M_lil = sparse.lil_matrix(M_array)
M_lil
# 出力
<10x10 sparse matrix of type ''
	with 9 stored elements in LInked List format>

これで変換できました。

printすると、0ではない成分の座標と値を保有していることが確認できます。
(ただし、実際のデータ構造は結構違います。print用に整形しているようです。)


print(M_lil)
# 以下出力
  (0, 1)	3
  (1, 3)	1
  (1, 5)	4
  (2, 4)	5
  (2, 6)	3
  (3, 7)	2
  (4, 1)	5
  (4, 5)	3
  (8, 9)	2

0行1列目が3, 1行3列目が1,という風に読んでいくと、確かに0以外の成分が記録されているのがわかります。

なお、実際のデータ構造は型ごとに違うのですが、lil_matrixの場合は次ように、
dataとrowsとうい二つの属性を使ってデータを保有しています。


print(M_lil.data)
'''
[list([3]) list([1, 4]) list([5, 3]) list([2]) list([5, 3]) list([])
 list([]) list([]) list([2]) list([])]
'''

print(M_lil.rows)
'''
[list([1]) list([3, 5]) list([4, 6]) list([7]) list([1, 5]) list([])
 list([]) list([]) list([9]) list([])]
'''

data方は、各行の0ではない要素の値、rowsにそれぞれの要素が何番目に入っているのかの情報を保有しています。
合計18個の数字で、10*10=100個の要素を持つ行列を表現できています。

あと、lil_matrixの重要なメリットして、スライスで値を取り出せるし、代入もできるという点があります。


print(M_lil[0, 1])
# 3

(できて当然に思えますが、他の疎行列のデータ型によってはこれができないものがある。)
そのため、 lil_matrix で疎行列を作って、それを(その他のメリットを持つ)他の型に変換して使うというやり方をよくやります。
(Intended Usageにそう書いてあるので従っています。)

最後に、arrayに戻す方法は toarray()です。


M_lil.toarray()
# 出力
array([[0, 3, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 0, 4, 0, 0, 0, 0],
       [0, 0, 0, 0, 5, 0, 3, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
       [0, 5, 0, 0, 0, 3, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int64)

白金鉱業 Meetup Vol.8 に参加しました

先日ブレインパッドさんのオフィスで開催された、白金鉱業 Meetup Vol.8 に参加参加させていただきました。

Vol.7からの連続参加です。
参考:白金鉱業 Meetup Vol.7 に参加しました

今回は資料がそれぞれ公開されているので軽めに記録を残しておきます。
(共感した部分、気になった部分を書き残そうとすると、しんゆうさんの発表なんてすごい量になってしまうので。)

事業者視点で語る!音声×データの活用可能

発表者:小山内将宏さん(株式会社Voicy/データストラテジスト)

Voicyの小山内さんの発表。
・お恥ずかしながらVoicyを使ったことがなかったのですが、非常に面白いコンセプトのサービスだと思いました。
(そういえば前職に XXXXX Sound って似たようなコンセプトのアプリがあった)

・社内に一人しかデータアナリストがいらっしゃらないそうで、お一人で業務を回されている(=それだけ優先度つけて効率化されている)という点でも参考になります。
「データが絡む部分はなんでもやります」って点も今の自分の立ち位置に近いかも。

SuperQuery は初めて存在を知りました。
 (というか、BigQueryはクエリの履歴がストックされないのか。。。トレジャーデータだと残るのに。)

・音声データへのアプローチは面白そう。
自然言語処理的な要素も多分に含まれているのですね。

・レコメンドのところで、好きじゃないものを出さないようにすることが重要という視点は音声サービスならではと思います。
(ECサイトだと10個推薦した中に1個変なの混ざってもそこまで問題ないですが、音は1つずつしか聞かないからそれが嫌なのだと極端にUX下がります)

・iPadの手書きアプリも便利そうでした。

Metabaseが気になる。

それでも「データ分析」で仕事をしたい人のための業界長期予報

発表者:しんゆうさん(フリーランス)

発表者はデータ分析とインテリジェンスの人。
最近各所で語られるようになっているデータアナリスト・データサイエンティストのキャリアのお話。
非常に気になるところです。

・発表された内容に全面的に同意します。

・資料中の定義だと自分はかなりの時間を「データエンジニア」の仕事に費やしてることになる。
(データサイエンティストとしてもデータアナリストとしての仕事も持ってるので特に不満はないですが。)

・会社でうちのチームが作ってるレポートは結構使ってもらえていて、他チームからもアナリストの増員を要望されるような状況なので、
うちはかなり恵まれているんだなぁーというのが正直なところです。

・「データ分析を売り込む」って意識はもっと高く持たないといけないともいます。
そうでないと、どんどん単純なレポート作成の作業ばかりになってしまう。

データサイエンティストよ、震えて眠れ

発表者:辻陽行さん(株式会社ブレインパッド/シニアデータサイエンティスト)

毎回面白い辻さんの発表。
タイトルは仰々しいですが、要はGCPに機械学習系のサービスが充実してきてるよっていうお話。

僕自身はGCPよりAWSのほうが好きでGCP全然触ってないので、そのことに対して危機感を持ちました。
もっとGCPを覚えた方がいいのかもしれない。

・FIFAとかこゝろとか話が脱線しまくりですがきちんとGCPの話に戻ってきます。

・AutoMLシリーズがどの程度手軽に使えるのかをまだ触ってないので理解できてないのですが、
知識不要でこれだけ制度が出せるのなら確かに脅威です。

・自分はもともと機械学習以外の仕事のウェイトが大きいので特に震えませんが、
たまに超短納期でモデルを作ってアウトプットを出さないといけない仕事が降ってくるので、
使えるよになればGCPは強力なツールになりそうです。

こゝろに興味が湧いたのでちゃんと読みたい。

まとめ

最近盛り上がっているキャリアの話、自分と同じように何でも屋っぽくなっている小山内さんのお話、
自分にとってほぼ未経験のGCPの活用のお話と、今回もどれも楽しく聞かせていただきました。
また、今回の懇親会も色々なかたとお話ができて非常に有意義な時間でした。
このような場を提供してくださっているブレインパッドさんには感謝しかないです。

次回以降のスピーカーもすごい人が続くようなので、
出来るだけ業務調整して参加したいです。(あとは抽選さえ通れば。)

単語ごとにその単語が含まれるテキストの数を数える

前回の記事では単語の出現回数を求めましたが、
今回は単語が出現するテキストの数を算出してみます。

参考:テキストデータ中の単語の出現回数を数える

とても似ていますが、要は1つのテキストに同じ単語が何回出ても1回としてしか数えないというものです。
(tf-idfのidfの計算につかわ割れるやつですね。)

コードはとても似たものになります。


from sklearn.feature_extraction.text import CountVectorizer
from sklearn.datasets import fetch_20newsgroups
import numpy as np

# データの読み込み
remove = ('headers', 'footers', 'quotes')
twenty_news = fetch_20newsgroups(
                                subset='all',
                                remove=remove,
                            )
X = twenty_news.data
# 文章の数
print(len(X))  # 18846

# BoW化
tf_vectorizer = CountVectorizer(
                                min_df=50,
                                token_pattern='(?u)\\b\\w+\\b',  # 1文字の単語も対象とする
                               )
tf = tf_vectorizer.fit_transform(X)
# モデルが集計対象とした(min_df回以上出現した)単語の数
print(len(tf_vectorizer.get_feature_names()))  # 4476
print(tf.shape)  # (18846, 4476)

# 単語ごとにその単語が1回以上出現したドキュメント数を求める。
document_frequency = np.array((tf>0).sum(axis=0))[0]
print(document_frequency.shape)  # (4476,)

# 出現回数上位100位までの単語と出現回数を表示
for i in document_frequency.argsort()[:-100:-1]:
    print(document_frequency[i], "\t", tf_vectorizer.get_feature_names()[i])

# 以下出力
'''
15749 	 the
14108 	 to
13948 	 a
13015 	 i
12991 	 and
12809 	 of
11842 	 in
11685 	 is
11029 	 it
10974 	 that
10406 	 for
8722 	 have
8665 	 this
8596 	 on
8447 	 you
8140 	 be
-- (以下略) --
'''

ポイントはここ

np.array((tf>0).sum(axis=0))[0]

(tf>0) すると、行列の各要素が正の数ならTrue,0ならFalse になります。
それをsumすることで、Trueが1として計算されてTrueの数がもとまるというカラクリです。

テキストデータ中の単語の出現回数を数える

テキストが1個だけなら、scikit-lerarnでBoW作って終わりなので、テキストデータは複数あるものとします。
(とは言っても、やることは各テキストをBoWにして足すだけです。)
結果を全部列挙したらすごい量になるのと、数えるだけだと面白くないので、
アウトプットは出現回数が多い単語のランキングにしましょう。

今回もサンプルデータは20newsgroupsを使わせていただきます。
また、この記事では出現回数数えて、上位の単語を列挙するところををゴールとし、
機械学習にかけたりしないのでstop_wordsや、細かな前処理は省きます。

コードは以下のようになりました。


from sklearn.feature_extraction.text import CountVectorizer
from sklearn.datasets import fetch_20newsgroups
import numpy as np

# データの読み込み
remove = ('headers', 'footers', 'quotes')
twenty_news = fetch_20newsgroups(
                                subset='all',
                                remove=remove,
                            )
X = twenty_news.data
# 文章の数
print(len(X))  # 18846

# BoW化(今回は出現頻度が高い単語に関心があるので、min_dfは大きめに設定。
tf_vectorizer = CountVectorizer(
                                min_df=50,
                                token_pattern='(?u)\\b\\w+\\b',  # 1文字の単語も対象とする
                               )
tf = tf_vectorizer.fit_transform(X)
# モデルが集計対象とした(min_df回以上出現した)単語の数
print(len(tf_vectorizer.get_feature_names()))  # 4476
print(tf.shape)  # (18846, 4476)

# 単語ごとに各ドキュメントの出現回数を足し合わせる
term_frequency = np.array(tf.sum(axis=0))[0]
print(term_frequency.shape)  # (4476,)

# 出現回数上位100位までの単語と出現回数を表示
for i in term_frequency.argsort()[:-100:-1]:
    print(term_frequency[i], "\t", tf_vectorizer.get_feature_names()[i])

# 以下出力
'''
173592 	 the
86907 	 to
77192 	 of
73928 	 a
70083 	 and
59683 	 i
50898 	 in
48899 	 is
45942 	 that
38660 	 it
32302 	 for
30492 	 you
24449 	 s
23617 	 on
23519 	 this
22030 	 be
-- (以下略) --
'''

不要語の除去をやっていないので、当然のように冠詞や前置詞など超汎用的な単語が並びました。

最後の出力には以前の記事で紹介したargsortを使っています。
参考:numpyのarrayを並び替えた結果をインデックスで取得する
[:-100:-1]というスライスで、最後の100項を逆順にとっているのもポイント。

単語ごとに各ドキュメントの出現回数を足し合わせるところでは、少しだけ工夫をしました。
まず、BoWが格納されている変数tfですが、 csr_matrix というデータ型になっています。

これは疎行列というメモリを節約できて、うまく使えば計算時間も短縮できる便利なものなのですが、
一見わかりにくいので toarray()でarray型に変化してしまってる例をよく見かけます。

今回のコードでは、np.array(tf.sum(axis=0))[0]という風に、
先にsumした後に、arrayに変換して、1次元に変換しました。
こうすると、toarray()してから和を取るよりもかなり早く処理できます。

jupyterなら %timeit で手軽に時間を測れるので比較しましょう。


%timeit tf.toarray().sum(axis=0)
# 355 ms ± 5.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit np.array(tf.sum(axis=0))[0]
# 1.47 ms ± 75.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

240倍も違いますね。

列の和だけなら、 csr_matrix より csc_matrix の方が計算が早いと思うのですが、
変換にかかる時間もありますし、十分早いのでこれで良いでしょう。

scikit-learnのソースコードも確認しましたが、
csr_matrix で結果を返すようになっていて、
csc_matrix を使うオプションなどはなさそうです。

pythonで、2進法/8進法/16進法で数値を定義する

通常、pythonのコード中で数値を定義したい時は
a=123
のように10進法で数値を表します。

ただ、pythonにおいては、10進法以外で数値を定義する方法も用意されています。
特に、2,8,16進法については、それぞれ数値の前に、
0b, 0o, 0x をつけることで定義できます。


>>> 0b1010101
85
>>> 0o251
169
>>> 0xe3f8
58360

2進法、8進法の方は実用的に使ったことがないのですが、
16進法はユニコード表を読むときなどに使ったことがあります。