重複のある配列の要素を順序を保ったまま一意化する

業務でタイトルの処理が必要になり、スマートなやり方を探したのでそのメモです。

まずおさらいですが、配列をユニーク化するだけなら集合に変換して戻せば完成します。


sample_list = ['b', 'c', 'c', 'd', 'a', 'b', 'd', 'e', 'a', 'b', ]
print(list(set(sample_list)))
# ['c', 'e', 'a', 'd', 'b']

この時、単に一意化するのではなく、元々の配列で最初の方に出てきた要素から順番に取り出したい、という要件がありました。
この時、昨日紹介した、配列のindexという関数が使えます。
要は元々の配列で何番目に登場していたかをこの関数でえて、その順番で並べかえれば良いです。
そして、ありがたいことに、sortedや、list.sort関数が、keyという
引数を取ってくれます。
keyに引数を一つとる関数を渡すと、各要素をその関数に適用させた結果で並べ替えてくれます。

それぞれやってみます。


sample_list = ['b', 'c', 'c', 'd', 'a', 'b', 'd', 'e', 'a', 'b', ]

# list.sortを使う方法
# 一度集合に変換して、ユニーク化
sorted_list = list(set(sample_list))
# 元々のインデックスでソート
sorted_list.sort(key=sample_list.index)
print(sorted_list)
# ['b', 'c', 'd', 'a', 'e']

# sortedを使う方法
sorted_list = sorted(set(sample_list), key=sample_list.index)
print(sorted_list)
# ['b', 'c', 'd', 'a', 'e']

sorted の方は、 戻り値は配列型なので、list()でキャストする必要はありません。
どちらかというとこちらの書き方の方がスマートだと思います。

Pythonの配列に含まれる特定の要素のindexを取得する

よく使っているnumpyのarrayではなく、Pythonデフォルトの配列(list)の話です。
だいたいいつもnumpyのarrayの方が便利なのですが、listにしかないメソッドもいくつかあります。

その一つがlist.index(x[, start[, end]])です。
これを使うと、listの中から、特定の要素(x)を探して、最初に現れたインデックスを得ることができます。
また、startやendを指定して、特定の区間から探すこともできます。


# サンプルの配列を準備する
sample_list = ['b', 'c', 'a', 'd', 'a', 'b', 'd', 'b', 'a', 'b', ]

# 最初に現れるdのindex
print(sample_list.index('d'))
# 3

# 次に現れるdは 3+1 = 4番目以降を探す
print(sample_list.index('d', 4))
# 6

配列内や、検索区間内に目当ての要素が存在しない場合はValueErrorが出ます。


sample_list.index('e')
"""
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
 in ()
----> 1 sample_list.index('e')

ValueError: 'e' is not in list
"""

ちなみに、普段はenumerateを使って次のようにして算出することが多いです。


print([i for i, value in enumerate(sample_list) if value == 'd'])
# [3, 6]

ただ、ご覧の通りコードが少し長くなるので、先頭の要素だけでいい時などはindexが手軽そうです。

Fashion-MNIST データセットの紹介

最近受けている講座の中で、kerasに同梱されているデータセットの中に、Fashion-MNISTという画像データがあることを知りました。
画像データを使って機械学習を試す時(と言っても、自分が画像データを扱うことはほぼないのですが)は、
大抵手書き数字のMNISTデータを使っていて、いささか飽きていたので、早速これを確認してみました。

ドキュメントはこちら。
Fashion-MNIST database of fashion articles
手書き数字のMNISTと完全互換らしく、クラス数は10個、画像サイズは28*28、データ件数も訓練データ60,000とテストデータ10,000のデータになっています。

読み込みもほぼ同じです。

今回はデータセットの紹介なので、読み込んで実際に画像を表示してみるところまでやってみます。


from tensorflow.keras.datasets import fashion_mnist
import matplotlib.pyplot as plt

(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
print("x_train.shape: ", x_train.shape)
print("y_train.shape: ", y_train.shape)
print("x_test.shape: ", x_test.shape)
print("y_test.shape: ", y_test.shape)
"""
x_train.shape:  (60000, 28, 28)
y_train.shape:  (60000,)
x_test.shape:  (10000, 28, 28)
y_test.shape:  (10000,)
"""
# 0〜9が どんなアイテムに対応しているかの対応はdatasetに含まれないので別途作る
target_name = {
        0: "T-shirt/top",
        1: "Trouser",
        2: "Pullover",
        3: "Dress",
        4: "Coat",
        5: "Sandal",
        6: "Shirt",
        7: "Sneaker",
        8: "Bag",
        9: "Ankle boot",
    }

fig = plt.figure(figsize=(10, 13), facecolor="w")
for i in range(100):
    ax = fig.add_subplot(10, 10, i+1)
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.imshow(x_train[y_train == i // 10][i % 10], cmap="gray_r")
    if i % 10 == 0:
        # アイテムの最初の画像にタイトルつける
        ax.set_title(target_name[i//10])

plt.show()

結果として表示される画像がこちらです。

手書き数字のMNISTよりちょっと楽しそうなデータですね。
そして結構難易度たかそうです。

ROLLUPやCUBEで発生したNULLと、元々あったNULLを区別する

今回もPrestoの話題です。
最近の更新で、
ROLLUPやCUBE,GROUPING SETなどを使って、総計を出したり、複数の条件での集計をまとめて行う方法を紹介しましたが、
これらの操作を行うと、多くのNULL値が発生します。
ROLLUPであれば、値が入ってるのが個別の集計で、NULLになっている行の値が総計と判定できるのですが、
ここで元々その列にNULLがあると、見分けがつかず、面倒なことになります。

単なるイメージですが、こんな感じの結果が出ます。
|gender|cnt|
|男性|100|
|女性|200|
|NULL|50| # これは元々NULL
|NULL|350| # ROLLUPで発生したNULL

このような、元々あったNULLと集計によって発生したNULLを見分けるのに、
GROUPING という専用の関数が用意されています。
ドキュメントはこちらのページのGROUPING Operationを参照。

SELECT句の中で、 grouping(col1, …, colN) のように使い、整数値を返します。
結果の数値が少し癖があるのですが、引数の一番右側の列(つまりcolN)が1,
そこから順番に、colN-1が2, colN-2が4($=2^2$)と、順番にビットの桁が割り当てられ、
その列の値が、ROLLUPやCUBEによって発生したNULLになっているビットの和を返します。
(わかりにくいですね。)

会社のDWHに打った実クエリを貼るわけにいかないのでドキュメントで紹介されている例をそのまま転載します。


SELECT
    origin_state,
    origin_zip,
    destination_state,
    SUM(package_weight),
    GROUPING(
        origin_state,
        origin_zip,
        destination_state
    )
FROM
    shipping
GROUP BY
    GROUPING SETS (
        (origin_state),
        (origin_state, origin_zip),
        (destination_state)
    );

-- 結果
origin_state | origin_zip | destination_state | _col3 | _col4
--------------+------------+-------------------+-------+-------
California   | NULL       | NULL              |  1397 |     3
New Jersey   | NULL       | NULL              |   225 |     3
New York     | NULL       | NULL              |     3 |     3
California   |      94131 | NULL              |    60 |     1
New Jersey   |       7081 | NULL              |   225 |     1
California   |      90210 | NULL              |  1337 |     1
New York     |      10002 | NULL              |     3 |     1
NULL         | NULL       | New Jersey        |    58 |     6
NULL         | NULL       | Connecticut       |  1562 |     6
NULL         | NULL       | Colorado          |     5 |     6
(10 rows)

grouping(
origin_state,
origin_zip,
destination_state
)
と指定されている各行が順番に、4,2,1のビットに対応していて、
例えば、結果の3行目、New Yorkの行であれば、
origin_zipとdestination_stateの行がGROUPING SETSによって発生したNULLになっているので、
2+1で3となっています。

この性質を使って、例えば NULL を’合計’とか’小計’とかに書き換えて返すクエリを構築することができます。

Prestoで複数種類の集約をまとめて行う

今回もPrestoのクエリのテクニックです。
前回の記事で、ROLLUPを使って集約(GROUP BY)した値と総計を同時に計算する方法を紹介しましたが、
もっと柔軟に、いろいろな組み合わせで集約をしたい場面があります。
面倒なのでいつも個別のクエリで出力してUNIONしたりpandasで結合したりしていますが、
Prestoにはそのような時に使える構文として、GROUPING SETSというのが用意されています。
ドキュメントのクエリをそのまま紹介させていただきますが、
次のように書きます。


SELECT
    origin_state,
    origin_zip,
    destination_state,
    SUM(package_weight)
FROM
    shipping
GROUP BY
    GROUPING SETS (
        (origin_state),
        (origin_state, origin_zip),
        (destination_state)
    );

こうすると、origin_state で集約したpackage_weightの合計 (この時origin_zipと、destination_stateはNULL)、
origin_stateと origin_zip で集約したpackage_weightの合計 (この時destination_stateはNULL)、
destination_state で集約したpackage_weightの合計 (この時origin_stateと、origin_zipはNULL)、
がまとめて出力されます。


 origin_state | origin_zip | destination_state | _col0
--------------+------------+-------------------+-------
 New Jersey   | NULL       | NULL              |   225
 California   | NULL       | NULL              |  1397
 New York     | NULL       | NULL              |     3
 California   |      90210 | NULL              |  1337
 California   |      94131 | NULL              |    60
 New Jersey   |       7081 | NULL              |   225
 New York     |      10002 | NULL              |     3
 NULL         | NULL       | Colorado          |     5
 NULL         | NULL       | New Jersey        |    58
 NULL         | NULL       | Connecticut       |  1562
(10 rows)

GROUPING SETS の中に () を入れてやれば全部の合計も出せます。
個別にクエリを書いてUNION ALLでつなげるのに比べると、記述量を劇的に減らせますね。

さらに、いくつかの列について、全ての組み合わせで、GROUPING SETS を作りたい場合、CUBE という演算子が使えます。


SELECT
    origin_state,
    destination_state,
    SUM(package_weight)
FROM
    shipping
GROUP BY
    CUBE(
        origin_state,
        destination_state
    );

と、次のクエリは同じ意味です。


SELECT
    origin_state,
    destination_state,
    SUM(package_weight)
FROM
    shipping
GROUP BY
    GROUPING SETS (
        (origin_state, destination_state),
        (origin_state),
        (destination_state),
        ()
    );

CUBEの中に入れてる列が2つだとそうでもないですが、これが3列も4列もとなっていくとかなり記述量が変わってきます。

ROLLUPを使った合計の計算

(注意)prestoを前提とします。 MySQLにもROLLUPはありますが少し書き方が違うようです。

最近よく使うようになった、ROLLUPという文法の紹介です。
(これまではTableauかPythonで計算するか、どうしてもSQLで関係つさせたい時はUNIONして対応していた。)

SQLでGROUP BYを使って何か集計した時、それらの合計(や、全体の平均、カウントなど)も出したいという場面はよくあります。
パソコンとスマホとか、男性と女性とか、で集計して、同時に全体の数値も見たいという場合ですね。

そのような時に ROLLUP を使えます。
ドキュメントはこのページの中。
実データを出せないのでイメージになってしまうのですが、
例えば、userテーブルのレコード数をgender列の値別に数える場合、通常は、


SELECT
    gender,
    COUNT(*) AS cnt
FROM
    user
GROUP BY
    gender

とやって、

|gender|cnt|
|男性|100|
|女性|200|

のような結果を得ると思います。
ここに、合計も一緒に出したい場合、


SELECT
    gender,
    COUNT(*) AS cnt
FROM
    user
GROUP BY
    gender
UNION ALL SELECT
    NULL AS gender
    COUNT(*) AS cnt
FROM
    user

のように書くと一応算出できるのですが、ちょっと要領の悪い書き方になります。

これが、ROLLUPを使って、


SELECT
    gender,
    COUNT(*) AS cnt
FROM
    user
GROUP BY
    ROLLUP(gender)

とすると、
|gender|cnt|
|男性|100|
|女性|200|
|NULL|300|
のような結果を得ることができます。

さらに ROLLUP はカンマ区切りで複数列指定することもでき、
そうすると段階的に小計を出してくれます。(これは何か手頃のデータで試すのが一番良いです。)


SELECT
    gender,
    age,
    COUNT(*) AS cnt
FROM
    user
GROUP BY
    ROLLUP(gender, age)

とすると、出力は次のようになります。
|gender|age|cnt|
|男性|20|30|
|男性|30|70|
|男性|NULL|100|
|女性|20|80|
|女性|30|120|
|女性|NULL|200|
|NULL|NULL|300|

(年齢が20と30の二通りなんてデータもなかなか無いでしょうが、ただの例なのでご了承ください。)
慣れると便利なのでためしてみてください。

ERAlchemyを使ってER図を描く

以前からER図をいい感じに出力できるツールを探していたのですが、ERAlchemyというのを知ったので使ってみました。
リポジトリ:Alexis-benoist/eralchemy
PyPI: ERAlchemy

Pythonで実装されたコマンドラインツールということで、Homebrewか、pipでインストールできます。


# どちらか行う。
$ brew install eralchemy
$ pip install eralchemy

自分はbrewのほうでインストールしました。
内部でgraphvizを使っているそうなので、もしかしたら事前にインストールしておく必要があるかもしれません。

既存のデータベースから作図する機能もあるようなのですが、自分はerファイルというテキストファイルから作成する方法で使っています。
erファイルのルールですが、ERAlchemyのページには(今のところ)簡単な例以上の説明は無いようです。

元々erdというツールの影響を受けて作られたそうで、erファイルの書式等はerdのドキュメントを読んで試すのが良さそうです。

ERAlchemy was inspired by erd, though it is able to render the ER diagram directly from the database and not just only from the ER markup language.

リレーションに使える記号の一覧を、ERAlchemyのソースコード中から見つけ出したりしてたのですが、無駄な手間でした。
僕も素直にerdのドキュメントを読めばよかったです。

ちなみにこちらから分かる通り、[*?+1]の4種類と未指定が使えます。
https://github.com/Alexis-benoist/eralchemy/blob/master/eralchemy/models.py


class Relation(Drawable):
    """ Represents a Relation in the intermediaty syntax """
    RE = re.compile(
        '(?P[^\s]+)\s*(?P[*?+1])--(?P[*?+1])\s*(?P[^\s]+)')  # noqa: E501
    cardinalities = {
        '*': '0..N',
        '?': '{0,1}',
        '+': '1..N',
        '1': '1',
        '': None
    }

さて、使い方は簡単で、次のコマンドで動きます。

eralchemy -i ‘入力ファイル(erファイル)’ -o ‘出力ファイル(pdfやpngなど)’
サンプルファイルを用意していただけてるので、curlでもってくるところからやってみましょう。


$ curl 'https://raw.githubusercontent.com/Alexis-benoist/eralchemy/master/example/newsmeme.er' > markdown_file.er
$ eralchemy -i markdown_file.er -o erd_from_markdown_file.png

これで、画像ファイルが生成されました。(ブログに貼るためにpngファイルにしましたが、通常はpdfの方が使いやすいかと思います。)

erファイルの中身も少しみておきましょう。
まず、テーブル定義は次のように指定します。


[tags]
    *id {label:"INTEGER"}
    slug {label:"VARCHAR(80)"}
    name {label:"VARCHAR(80)"}

{label:hogehoge}は省略できます。

そしてリレーションは次のように指定します。


users *--? posts
posts *--? comments
users *--? comments
comments *--? comments
tags *--? post_tags
posts *--? post_tags

この例は全て 0以上 対 0or1 のリレーションですが、+で1以上、1で厳密に一つを指定できます。

サンプルの中では使われていませんが、
erdの方のドキュメントを見る限りでは、背景色やフォントの指定などもできそうなので、色々試してみたいと思います。

scipyで階層的クラスタリングの樹形図を書く時に上位のクラスタのみ表示する

以前書いた、 scipyで階層的クラスタリング の記事の続きです。

階層的クラスタリングを行って結果を樹形図(デンドログラム)で表示すると、元のデータが多い場合は非常にみづらいものになります。
このような時は、樹形図の表示を途中で打ち切って必要なクラスタ分だけ表示するとクラスタ間の関係が掴みやすくなります。
scipyのdendrogram関数では、 truncate_mode というオプションが用意されており、これと、 値pを適切に指定することで実現できます。
ドキュメント: scipy.cluster.hierarchy.dendrogram

truncate_mode はNone, lastp, levelの3つの値を取ります。
Noneがデフォルト、lastpがデンドログラムの上から数えてp個のノードを残す、
levelは逆に下から数えて、各ノードがp回マージされるように動きます。

それぞれ動かしてみましょう。
truncate_mode=”lastp”, p=16

truncate_mode=”level”, p=3
の場合に、表示されるノードがどちらも16個になるので、動きの違いも見ておきます。


from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import linkage
from scipy.cluster.hierarchy import dendrogram

# データ取得。
X = load_iris().data

# ユークリッド距離とウォード法を使用してクラスタリング
z = linkage(X, metric='euclidean', method='ward')

# 結果を可視化
fig = plt.figure(figsize=(8, 15), facecolor="w")
ax = fig.add_subplot(3, 1, 1, title="樹形図: 全体")
dendrogram(z)
ax = fig.add_subplot(3, 1, 2, title="樹形図: lastp 16")
dendrogram(z, truncate_mode="lastp", p=16)
ax = fig.add_subplot(3, 1, 3, title="樹形図: level 3")
dendrogram(z, truncate_mode="level", p=3)
plt.show()

結果がこちら。

truncate_mode=”lastp” は、樹形図全体の上部の部分をそのまま切り出したものになっていて、
truncate_mode=”level” の方は、各枝に至るまでの分岐回数が一定になっているのがわかります。
また、どちらも図がすっきりしてみやすくなりました。

Optunaで学習曲線を可視化する

Optunaの機能や挙動についてもっとしっかり理解したいので、うまい可視化の方法を考えていたのですが、
ドキュメント中に、Visualizationのセクションを見つけたので、
まずはこれを試してみることにしました。

現段階(version 0.16.0)では、定義されている関数は
optuna.visualization.plot_intermediate_values(study)
だけのようなのでこれを試します。
(最初に試した時、 Plotly が入ってないという警告が出たのでpipインストールしておきます。)

早速使ってみると、次のようにトライアルごとの学習の進捗が可視化できました。

Optunaで枝刈りも使ってKerasのパラメータチューニングの記事で紹介したコードを動かした後に、jupyterで次のコードを実行します。


optuna.visualization.plot_intermediate_values(study)

jupyterで動かすと、マウスカーソルを当てた時にそれぞれの線が何回めのトライアルなどかなどの情報がポップアップされるので、
ぜひ試していただきたいです。
pngエクスポート機能もあり、それで出力した画像がこちら。

見込みがない試行がさっさと打ち切られているのがわかります。
(正解率が低いのに最後まで走ってるのは序盤の試行です。)

トレジャーデータで列名の一覧を出力する

注意:Prestoの方でクエリを書いていることを前提とします。

トレジャーデータを使っていて、各DBのそれぞれのテーブル毎の列名の一覧を取得したくなったのでその方法のメモです。

対象のテーブルが少なければ、
DESCRIBE table_name
を順番に実行すれば十分ですが、対象テーブルが多くなるとこれでは大変です。

この場合、Presotのメタデータにアクセスすると手軽に列名の一覧を得ることができます。

FAQの次の質問が参考になります。
23. How do I access TD table metadata using Presto?

クエリをそのまま引用します。


# List TD Databases
SELECT * from information_schema.schemata

# List TD Tables
SELECT * from information_schema.tables

# List all column metadata
SELECT * from information_schema.columns

このうち、3番目の
SELECT * from information_schema.columns
を使うと、DB、テーブル、列を含む情報を取得できます。
不要な情報もあるので、自分は次の形で使うことが多いです。


SELECT
    table_schema,
    table_name,
    column_name
FROM
    information_schema.columns

通常のSELECTと同じように、WHERE句で特定のDB(schema)のみなどの条件をつけることもできます。