numpyの配列をファイルに保存する

日常の実際で必要になったことはないのですが、
numpyに配列(ndarray)をファイルに保存する機能があるのでその紹介です。

(実用上、ファイルに保存したくなるようなデータは pandasのデータフレームに変換したあと、
to_csvやto_excelを使ってcsvかエクセルにしてしまうことが多いです。)

ドキュメントはこの辺り
numpy.save
numpy.load
numpy.savez

簡単に言ってしまえば、
numpy.save(ファイル名, 保存したい配列) で保存して、
numpy.load(ファイル名) で読み込める。
numpy.savez(ファイル名, (名前=)保存したい配列, (名前=)保存したい配列, …) で、
1ファイルに複数配列保存できる、と言った使い方になります。(名前=)は省略可能。

実際にやってみましょう。
まず検証用にダミーデータを作ります。


import numpy as np

# 保存のテスト用データを作成
data0 = np.arange(12).reshape(3, 4)
data1 = data0 ** 2
print(data0)
'''
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
'''
print(data1)
'''
[[  0   1   4   9]
 [ 16  25  36  49]
 [ 64  81 100 121]]
'''

まずは、save関数で配列を一つ保存し、それを別の変数に読み込んでみましょう。
特に難しいことはなく非常に直感的な使い方で上手く動きます。


# .npyフォーマットで保存
np.save("data0.npy", data0)

# 読み込み
data0_load = np.load("data0.npy")
print(data0_load)
'''
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
'''

続いて、savez関数で複数配列を1ファイルに保存し、復元してみます。
こちらは、loadした時に返されるオブジェクトが保存した配列そのものではないので注意です。
まずは名前をつけずに保存するパターン。


# savez を使うと複数の配列を.npzフォーマットの1ファイルにまとめて保存できる
np.savez("data0_1.npz", ary0, ary1)

# .npzファイルを読み込むと、配列ではなく、NpzFile オブジェクトが返される。
npzfile = np.load("data0_1.npz")
# .filesプロパティを見ると、中に二つの配列を持っていることがわかる
print(npzfile.files)
# ['arr_0', 'arr_1']

# それぞれの配列の取り出し
print(npzfile["arr_0"])
'''
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
'''
print(npzfile["arr_1"])
'''
[[  0   1   4   9]
 [ 16  25  36  49]
 [ 64  81 100 121]]
'''

また、名前をつけて保存すると次のようになります。


# 名前(例ではxとy)をつけて保存することも可能
np.savez("named_data0_1.npz", x=ary0, y=ary1)
# .npzファイルを読み込むと、配列ではなく、NpzFile オブジェクトが返される。
npzfile2 = np.load("named_data0_1.npz")
# .filesプロパティを見ると、中に二つの配列を持っていることがわかる
print(npzfile2.files)
# ['x', 'y']

# 保存時の名前で取り出すことが可能
print(npzfile2["x"])
'''
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
'''
print(npzfile2["y"])
'''
[[  0   1   4   9]
 [ 16  25  36  49]
 [ 64  81 100 121]]
'''

勝手にarr_0とか命名されるよりも、自分で名前をつけておいた方が混乱がなさそうですね。

pythonでMD5

業務でMD5を使って文字列をハッシュ化する機会があったのでそのメモです。
(最近ではMD5自体があまり安全では無く、暗号学的な強度が必要なときは使うべきでは無いのでご注意ください。)

python3では、標準ライブラリにhashlibというものがあり、これにMD5が実装されています。
ドキュメント: hashlib — セキュアハッシュおよびメッセージダイジェスト

使い方は簡単で、インポートして hashlib.md5(“text”) するだけです、
となれば楽なのですが、これでは動きません。こんなふうにエラーになります。


import hashlib
hashlib.md5("text")

# 以下出力
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 hashlib.md5("text")

TypeError: Unicode-objects must be encoded before hashing

これは、bytes型でデータを渡す必要があるからです。
そのため、b"text""text".encode("utf-8")のようにする必要があります。

これで一応エラーは無くなりますが、まだ少し不十分です。
というのも、戻り値がHASH object という型で返ってきます。


import hashlib
text = "Hello, World!"
bytes_text = text.encode("utf-8")
print(hashlib.md5(bytes_text))

# 出力
<md5 HASH object @ 0x10e3f7620>

欲しいのは、 16進法で表記された文字列だと思うので、もう一手間かけて、hexdigestという関数を実行します。


import hashlib
text = "Hello, World!"
bytes_text = text.encode("utf-8")
hash_obj = hashlib.md5(bytes_text)
print(hash_obj.hexdigest())

# 出力
65a8e27d8879283831b664bd8b7f0ad4

# 1行でやる方法
print(hashlib.md5(b"Hello, World!").hexdigest())

# 出力は同じ
65a8e27d8879283831b664bd8b7f0ad4

これでできました。

sha1やsha256もhashlibに実装されているので同じように使えます。

pythonで累積和

数列に対して、最初の項からn番目の項まで足したものの数列が必要になることがあります。
日々の売上データからその日までの累計の売上データを出すような計算ですね。

イメージとしては、
1,2,3,4,5,…に対して、1,3,6,10,15,… を得るような操作です。

スライスと内包表記を組み合わせてもいいし、足し算くらいならfor文を回しても問題ないのですが、
numpyやpandas に専用の関数が用意されているのでその紹介です。

ドキュメントはこの辺
numpy.cumsum
pandas.Series.cumsum

早速実行してみました。


import numpy as np
import pandas as pd

ary = np.arange(1, 11)
print(ary)
# [ 1  2  3  4  5  6  7  8  9 10]

print(ary.cumsum())
# [ 1  3  6 10 15 21 28 36 45 55]

series = pd.Series(np.arange(1, 11))
print(series)
'''
0     1
1     2
2     3
3     4
4     5
5     6
6     7
7     8
8     9
9    10
dtype: int64
'''

print(series.cumsum())
'''
0     1
1     3
2     6
3    10
4    15
5    21
6    28
7    36
8    45
9    55
dtype: int64
'''

よく似た関数として、
numpyには累積の積を返すcumprodが用意されており、
さらにpandasの方にはcumprodに加えて、そこまでの最大値と最小値を得られるcummax, cumminが用意されています。

JSON文字列をオブジェクトに変換する

昨日の記事の逆です。(まとめて書けばよかった)

ドキュメントも同じ場所。
json — JSON エンコーダおよびデコーダ

JSON型の文字列でデータが渡されたとき、それをオブジェクトに変換するには、
json.loads という関数を使います。

早速やってみましょう。
printするだけだと型が変わっていることがわかりにくいので、
typeの結果も出しました。


import json
json_data = '{"key1": "value1", "key2": "value2", "ary1": ["element1", "element2"]}'
data = json.loads(json_data)

print(data)
print(type(data))

# 出力
# {'key1': 'value1', 'key2': 'value2', 'ary1': ['element1', 'element2']}
# <class 'dict'>

辞書型のオブジェクトをJSON文字列に変換する

仕事で必要になったのでメモです。

pythonには jsonという標準ライブラリがあり、
それを使うことで、配列や辞書型のオブジェクトをJSON文字列に簡単に変換することができます。

ドキュメントはこちら。
json — JSON エンコーダおよびデコーダ

インポートして dumps関数を使うだけなので早速やってみましょう。


import json
data = {
        "key1": "value1",
        "key2": "value2",
        "ary1": ["element1", "element2"],
    }
json_data = json.dumps(data)
print(json_data)

# 出力
# {"key1": "value1", "key2": "value2", "ary1": ["element1", "element2"]}

ちなみに、json.dumpsを使わず、str(data)で文字列に変換すると、結果は
"{'key1': 'value1', 'key2': 'value2', 'ary1': ['element1', 'element2']}"
になります。
JSONのルールでは文字列はダブルクオーテーションで囲まないといけないので、
これはJSONではありません。

厳密にJSON型を要求する関数やAPIに渡すときはこれだと受け付けられないので、
json.dumpsを使いましょう。

numpyの乱数生成関数の設計について

このブログに登場するコードでも頻繁にnumpyで乱数を生成していますが、
そのドキュメントは一回読んでおいたほうがいいよという話です。

Random sampling (numpy.random)

pythonの学習を始めた頃は、本に載っているサンプルコードや検索したらでてくる各サイトを参考に、
個々の関数の使い方を覚えて使ってました。

例えばrandであれば次のような感じ。


import numpy as np
# スカラーで一つ値が欲しい場合
np.random.rand()
# 配列で5個値が欲しい場合
np.random.rand(5)
# 引数を複数指定すると多次元配列で乱数が生成される
np.random.rand(2, 2, 2)

標準正規分布に従う乱数が欲しい時はrandnがあり、randと同じように使えます。

その一方で、normalという関数も準備されていて、
これは使い方が違います。

引数が
normal([loc, scale, size])となっていて、
最初に平均と標準偏差を与える必要があり、
欲しい乱数の個数は3個目の引数に与えます。

一方、randやrandnは複数個の乱数を得るには便利ですが、パラメータを渡すことができません。
(なので、得た乱数に対して、定数を足したり掛けたりして調整する)

特に難しくも複雑でもないので、本に出てきた通りに暗記して使っていましたが、
関数の設計が統一されてないので不便だとずっと思ってました。

それでこの度ドキュメントをみたのですが、各関数が、
Simple random data と、Distributionsというカテゴリに分けて定義されていることを知りました。
(あと二つ、PermutationsとRandom generatorがありますが割愛)
そして、(完全に揃ってる訳ではないのですが、)それぞれのカテゴリ内である程度使い方を揃えて作られていることがわかりました。

要は、Simple random dataのほうは欲しいデータの数だけ渡せばよく、
Distributionsの方は最初に分布のパラメーターを指定して、データの個数はsize引数で渡す。

今更なんですが、最初にpython勉強し始めた頃に、
公式ドキュメントもきちんと読んでおけば、もう少し楽に覚えられたかなと思いますね。

pandasで数値データを区間ごとに区切って数える

今日偶然知って便利だった関数を紹介します。

まずやりたいこと。
配列でもDatafarameの列でも、数値データを区間(bins)ごとに区切って数えたい場面は多々あると思います。
例えば、
[455,133,666,111,645,236]
みたいなデータを、
100以上,200未満が2個、200以上,300未満が1個、、、といった具合です。
ヒストグラム書くためにやる操作ですね。

これまで僕がこれをやる時100単位でやるのであれば、
各データを100で割り算して小数点以下を切り捨てて、再度100倍するといった面倒なことをやってました。

これを pandas.cut という関数を使うと、非常に容易に行えます。
ドキュメントはこちら。
pandas.cut

早速これを使ってみましょう。
次のコードは、乱数で生成した10個の数値を、100ごとに区切って数えるものです。
結果に出てくる “[300, 400)”といった出力は、文字列に見えますが、Intervalというオブジェクトです。
right=False は指定しないと、n以上m未満ではなく、nより大きいm以下、って区切り方になります。
引数のbinsには区切りたい点のリストを渡していますが、arrange関数で生成して渡しています。


import pandas as pd
import numpy as np  # ダミーデータ生成用

# 300〜999 の整数を10個生成
data = np.random.randint(300, 1000, 10)
# 100 ごとに区切る。
category = pd.cut(data, bins=np.arange(300, 1100, 100), right=False)

print("データとそれが所属する区間")
for d, c in zip(data, category):
    print(d, "・・・", c)

print("\n区間ごとのデータ件数")
print(category.value_counts())

# 以下出力
データとそれが所属する区間
309 ・・・ [300, 400)
305 ・・・ [300, 400)
874 ・・・ [800, 900)
953 ・・・ [900, 1000)
727 ・・・ [700, 800)
950 ・・・ [900, 1000)
384 ・・・ [300, 400)
789 ・・・ [700, 800)
486 ・・・ [400, 500)
501 ・・・ [500, 600)

区間ごとのデータ件数
[300, 400)     3
[400, 500)     1
[500, 600)     1
[600, 700)     0
[700, 800)     2
[800, 900)     1
[900, 1000)    2
dtype: int64

pythonでアルファベットの大文字小文字変換

自然言語処理系の機械学習を行うときやテキストマイニングをやるとき、
前処理としてアルファベットの大文字小文字を統一することがよくあります。
(失われる情報もあるのでしない方が良いという主張も見たことがありますが、僕は大抵の場合小文字に統一します。)

これはpythonの文字列が持っている
str.lower()str.upper()
を使うことで実現できます。
pythonの組み込み型のドキュメントを見ると乗っています。


text = "Hello World!"
print(text.lower())  # hello world!
print(text.upper())  # HELLO WORLD!

これだけだと、記事にしなかったと思うのですが、ドキュメントを読んでいると他にも関数が準備されていることがわかりました。


text = "Hello World!"
# 大文字と小文字を入れ替える
print(text.swapcase())  # hELLO wORLD!

text = "HELLO world!"
# 各単語の1文字目を大文字に、残りを小文字に変換する
print(text.title())  # Hello World!

text = "HELLO world!"
# 最初の文字を大文字に、残りを小文字に変換する
print(text.capitalize())  # Hello world!

正直、使う場面を思いつかないのですが面白いですね。
英語ネイティブな人たちが使うのでしょうか。

pythonで文字と文字コードの相互変換

pythonで文字を文字コードに変えたり、文字コード(整数)をそれが表す文字に変換したりする方法のメモです。
これらはそれぞれ、 ord と chr という組み込み関数で実現できます。

ドキュメントはこちら。
組み込み関数

これらをアスキーコードへの変換やアスキーコードから文字への変換だと説明しているサイトもあるようですが、
ドキュメントに書かれている通りUnicodeに対応しています。
(もしかしたらpython2系の時代はアスキー文字だけだったのかな?)

組み込み関数なので何もインポートせずに利用可能です。
ただし、ordは文字のみを受けつけ、文字列を渡すとエラーになるので注意してください。

単純なサンプル。


print(ord("a"))  # 97
print(ord("あ"))   # 12354
print(chr(97))   # a
print(chr(12354))   # あ

numpyの線形代数モジュールで連立一次方程式を解く

numpyで作成できる配列によく似たオブジェクトのarrayですが、配列同様にネストして多次元のarrayを作成できます。
(日頃から意識せず普通に使ってますね。)
特に2次元のarrayについては、行列として扱うことが可能です。

そしてnumpyには、numpy.linalgという線形代数のモジュールがあり、
行列式や固有値、逆行列の計算などができます。

ドキュメントはこちら。
Linear algebra (numpy.linalg)

今回は基本的な例として次の連立一次方程式を解いてみましょう。
$$
\begin{eqnarray}
&x_0-4&x_1+2&x_2&=7\\
9&x_0-5&x_1+2&x_2&=-2\\
3&x_0-10&x_1+5&x_2&=3
\end{eqnarray}
$$

これは
$$
A = \left[ \begin{matrix}
1&-4&2\\
9&-5&2\\
3&-10&5
\end{matrix}\right]
$$
$$
b = \left[ \begin{matrix}
7\\
-2\\
-3
\end{matrix}\right]
$$
とおいて、 Aに逆行列が存在すれば、(つまりAの行列式が0でなければ)$A^{-1}b$を計算することで解けます。

それではやってみましょう。
行列式はlinalg.det(A)
逆行列はlinalg.inv(A)で算出できます。
行列の式はnp.dot(A,B)A.dot(B)か、A@Bなどの書き方があります。


# 行列の定義
A = np.array(
        [
            [1,  -4, 2],
            [9,  -5, 2],
            [3, -10, 5]
        ]
    )
b = np.array([7, -2, 3]).T

# Aの行列式の確認 (実装の都合でわずかに誤差が出ます。)
print("dat(A)=", np.linalg.det(A))

# 方程式の解
print("[x_0, x_1, x_2]=", np.linalg.inv(A)@b)

# 以下出力
dat(A)= 0.9999999999999893
[x_0, x_1, x_2]= [ -29. -223. -428.]

これで、連立一次方程式が解けました。
Aの行列式が微妙に1にならないのは実装されていているアルゴリズムの都合のようです。