CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: pandas の DataFrame を scikit-learn で KFold するときの注意点

今回は pandas の DataFrame を scikitl-learn で交差検証しようとしてハマった話について。 だいぶ平凡なミスなんだけど、またやるとこわいので自分用にメモしておく。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V                         
Python 3.6.5

下準備

まずは pandas と scikit-learn をインストールしておく。

$ pip install pandas scikit-learn scipy

Python のインタプリタを起動する。

$ python

なんか適当に DataFrame を作っておく。

>>> import pandas as pd
>>> data = [
...     ('Ant'),
...     ('Beetle'),
...     ('Cat'),
...     ('Deer'),
...     ('Eagle'),
...     ('Fox'),
... ]
>>> columns = ['name']
>>> df = pd.DataFrame(data, columns=columns)

できた DataFrame はこんな感じ。

>>> df
     name
0     Ant
1  Beetle
2     Cat
3    Deer
4   Eagle
5     Fox

特に指定しない限り、デフォルトではインデックスとして 0 から始まる整数が連番で振られる。

sklearn.model_selection.KFold の使い方

scikit-learn で交差検証するとき基本は KFold クラスを使う。 このクラスはインスタンス化するときに分割数を指定し、その上で KFold#split() メソッドに分割するものを渡す。 返り値としてはイテラブルなオブジェクトが返ってきて、それぞれ学習用データと検証用データ用のインデックスが取り出せる。

>>> from sklearn.model_selection import KFold
>>> kf = KFold(n_splits=3)
>>> for train, test in kf.split(df):
...     print(train, test)
... 
[2 3 4 5] [0 1]
[0 1 4 5] [2 3]
[0 1 2 3] [4 5]

pandas との連携 (ダメなパターン)

KFold#split() で手に入るのは単なるインデックスなので、それを元に DataFrame から対象データを抽出しないといけない。 このとき、次のようなコードを書いてしまうと一見すると上手くいっているように見えて後でハマることになる。

>>> from sklearn.model_selection import KFold
>>> kf = KFold(n_splits=3)
>>> for train, test in kf.split(df):
...     # これはやっちゃダメ!
...     train_df = df[df.index.isin(train)]
...     test_df = df[df.index.isin(test)]
...     # 結果確認 (一見すると上手くいっているように見える)
...     print('(train)', train_df)
...     print('(test)', test_df)
...     print('-----')
... 
(train)     name
2    Cat
3   Deer
4  Eagle
5    Fox
(test)      name
0     Ant
1  Beetle
-----
(train)      name
0     Ant
1  Beetle
4   Eagle
5     Fox
(test)    name
2   Cat
3  Deer
-----
(train)      name
0     Ant
1  Beetle
2     Cat
3    Deer
(test)     name
4  Eagle
5    Fox
-----

実は、上記はインデックスが 0 からの連番で振られているために動いているに過ぎない。 その前提が変わると途端に動かなくなる。 試しにインデックスの番号を変更してみよう。

>>> # インデックスを 0 から始まる連番から変える (以下なら 10, 20, 30...)
... df.index = df.index * 10 + 10

変更後のインデックスはこんな感じ。

>>> df
      name
10     Ant
20  Beetle
30     Cat
40    Deer
50   Eagle
60     Fox

先ほどと全く同じコードを使って動作を確認してみよう。

>>> kf = KFold(n_splits=3)
>>> for train, test in kf.split(df):
...     # OMG!!
...     train_df = df[df.index.isin(train)]
...     test_df = df[df.index.isin(test)]
...     # 結果確認
...     print('(train)', train_df)
...     print('(test)', test_df)
...     print('-----')
... 
(train) Empty DataFrame
Columns: [name]
Index: []
(test) Empty DataFrame
Columns: [name]
Index: []
-----
(train) Empty DataFrame
Columns: [name]
Index: []
(test) Empty DataFrame
Columns: [name]
Index: []
-----
(train) Empty DataFrame
Columns: [name]
Index: []
(test) Empty DataFrame
Columns: [name]
Index: []
-----

中身が全て空っぽになってしまっている!

pandas との連携 (正解)

上記のようなパターンでも動くようにするには DataFrame の絞り込みで DataFrame#iloc を使う。 これなら、本来のインデックスの値ではなく中身の順序にもとづいたインデックスで絞り込みができる。

>>> kf = KFold(n_splits=3)
>>> for train, test in kf.split(df):
...     # これなら上手くいく
...     train_df = df.iloc[train]
...     test_df = df.iloc[test]
...     print('(train)', train_df)
...     print('(test)', test_df)
...     print('-----')
... 
(train)      name
30    Cat
40   Deer
50  Eagle
60    Fox
(test)       name
10     Ant
20  Beetle
-----
(train)       name
10     Ant
20  Beetle
50   Eagle
60     Fox
(test)     name
30   Cat
40  Deer
-----
(train)       name
10     Ant
20  Beetle
30     Cat
40    Deer
(test)      name
50  Eagle
60    Fox
-----

ばっちり。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonデータサイエンスハンドブック ―Jupyter、NumPy、pandas、Matplotlib、scikit-learnを使ったデータ分析、機械学習

Pythonデータサイエンスハンドブック ―Jupyter、NumPy、pandas、Matplotlib、scikit-learnを使ったデータ分析、機械学習

色々と調べた末にダイヤモンドを個人輸入した話

今回は、いつもと違って技術系ではない話。

少し前のことだけど、一身上の都合によりダイヤモンドが必要になる機会があった。 正直なところ、自分自身は炭素の結晶に興味はない。 また、その資産価値についても (現時点でも) 懐疑的に見ている。 とはいえ、世間的にはずいぶんと高値で取引されているらしい。 もし購入するとなれば、なかなか高い買い物になる。 そこで色々と調べた結果、最終的には海外から個人輸入することになった。

今回は、それに至る経緯や、調べた内容についてせっかくなのでまとめてみることにする。 先に断っておくと、かなり長い。 この記事で伝えたいことの本質ではないけど、安くダイヤモンドを購入する方法についても途中に記載している。 もし、今後同じようにダイヤモンドが必要になる人にとって参考になれば幸いに思う。

TL;DR

  • 日本のジュエリーショップで販売されているダイヤモンドは高い
    • 具体的には、世界的な市場取引価格から大きく乖離した値付けがされている
  • 今は天然ダイヤモンドを購入するにはおそらく最悪のタイミング
    • もう少しで宝飾用途の人工ダイヤモンドが安価に手に入るようになるため
  • 全てを理解した上でそれでも購入するなら、なるべく安価な販売チャネルを選ぶのが望ましい

もくじ

はじまり

きっかけは、パートナーがどうやらダイヤモンドの指輪を欲しているらしい、ということを理解したため。 そこで、まずはいくつかのジュエリーショップに足を運んでみた。

ジュエリーショップでは、指輪にはまっていない状態のダイヤモンドが販売されている。 その状態をルース (裸石) と呼び、目当てのデザインの枠 (指輪部分のこと) と組み合わせて購入する。 ちなみに、ルースは和製英語らしい。

指輪において、中央の大きな宝石はセンターストーンという。 日本の婚約指輪では、センターストーンにダイヤモンド、枠がプラチナという組み合わせが一般的。

そして、いくつかのお店でダイヤモンドを眺めていたところ気になる点があった。 それは、似たようなグレードのルースでも価格がお店によってずいぶんと異なるところ。 一体、その値段の違いはどこからくるのだろうか?

先に結論から書くと、日本の一般的なジュエリーショップが販売するダイヤモンドのルースは高い。 ここで「高い」というのは、具体的には世界的な市場取引価格から乖離した値付けになっている、という意味を指す。 とはいえ、値段が高いの安いのと言うには、まずダイヤモンドの前提知識から知っておく必要がある。

ダイヤモンドについて

ダイヤモンドは、炭素の原子が集まって結晶化したもの。 天然のものであれば、地球内部の高温・高圧の環境下で生成される。

光を反射しやすいので、宝飾用途で用いられる。 また、その硬さから工業用途でも需要がある。 同じ宝飾用途で用いられる貴金属と異なるのは、炭素という元素が地球上に比較的ありふれているという点。

詳しくは後述するものの、工業用途では人工ダイヤモンドの利用が一般的になっている。 また、宝飾用途でも技術の進歩と共に一部で利用が進みつつある。

宝飾用途のダイヤモンドについて

ダイヤモンドは、生成された環境と加工方法によって個体それぞれ見た目が異なる。 宝飾用途のダイヤモンドの場合、それを 4C という基準で評価するのが世界的に一般的となっている。 この名前は、評価の基準の頭文字が 4 種類で、全て C から始まるためにそう呼ばれている。 評価は世界にいくつかある鑑定機関が有償で行う。 その結果は鑑定書という形で石に付与される。

Carat (カラット)

ルースの重さを用いた評価基準。

勘違いしやすいのは「大きさ」ではなく「重さ」の基準というところ。 具体的には 0.2 グラム = 1 カラットになる。

カラットと価格の関係は非線形になっている。 どういうことかというと、カラットの数字が大きくなるほど、値段の上がり幅も大きくなる。 これは、大きなダイヤモンドの原石ほど算出されにくいという希少性がその根拠らしい。

以下は、とあるジュエリーショップに記載されているダイヤモンドの価格表をスクレイピングして散布図にしたもの。 x 軸にカラット数、y 軸に価格をプロットしている。

f:id:momijiame:20180617181002p:plain

あくまで一例ではあるものの、カラット数が大きくなるにつれて価格が非線形に高くなる関係が見て取れる。

ところで、面積は長さの二乗に比例するのに対して体積は三乗に比例する。 だとすると、カラットが大きくなるほど見た目での差異は判別しにくくなるといえる。 指輪の状態で視認できるのは基本的に表面部分だけなので。 なのに大きくなるほど値段の上がり幅は大きいということになる。

Color (カラー)

ルースの色を用いた評価基準。

ダイヤモンドは、それに含まれる不純物や結晶構造の歪みなどによって色がつく。 一般的な天然ダイヤの場合、窒素の含有によって黄色くなったものが多い。

カラーはアルファベットで表される。 より無色透明に近い D カラーから E, F, G ... Z と進むにつれて黄色くなる。 一般的には無色透明に近いほど値段は高くなり、黄色になるほど安い。 以下のページを見るとイメージがつかみやすい。 日本で指輪の用途で用いられるのは G ~ I 以上のものが多いような印象を受ける。

GIA 4Cs Color D-to-Z

ただし、これには例外もあって、発色の良い色がしっかりついたものには反対に高い値段がつく場合がある。 ようするに D ~ Z のスケールでは測れないもので、これはファンシーカラーダイヤモンドと呼ばれている。 カラーダイヤモンドには黄色以外にもブルーやピンクなど、様々な色がある。

ファンシーカラーダイヤモンドの説明

珍しい色のファンシーカラーダイヤモンドで大きなものは、それこそ大衆が購入できような値段ではない。 ただし、ごく小さなもの (メレーダイヤモンド) であれば、手が届く場合もある。

Clarity (クラリティ)

ルースの内部にある内包物や傷を用いた評価基準。

ダイヤモンドは、多くの場合に内包物や傷、空洞などの見た目で分かる欠陥を持っている。 それらの欠陥を、見えやすさという基準で評価したもの。 以下のグレードに分かれていて、下に行くほど内包物が視認しやすいことを表している。

  • Flawless (FL)
  • Internally Flawless (IF)
  • Very, Very Slightly Included (VVS1, VVS2)
  • Very Slightly Included (VS1, VS2)
  • Slightly Included (SI1, SI2)
  • Included (I1, I2, I3)

内包物や傷がない・あるいはほとんどない FL や IF は数が少なく、身につけるジュエリーに供される機会はさほど多くない。 定義の上では VS2 以前であれば 10 倍ルーペを使わない限り内包物は視認が難しいとされる。 日本で指輪の用途として供されるのは VVS1 ~ VS2 のレンジが多いような印象を受ける。

GIA 4Cs Clarity

Cut (カット)

ルースの形状を用いた評価基準。

ダイヤモンドは原石のままでは光り輝かないため、カッティングや研磨といった加工を必要とする。 まず、大まかな形状としてラウンドブリリアントやペアー、オーバル、ハートなどがある。 最も一般的なのはラウンドブリリアントで、それ以外はパートナーがもし興味があれば、という感じ。

で、原石をその形状に加工した結果がどれだけ上手くいったかがカットの評価基準になる。 ただし、カットは単一の何かを見た評価項目ではなく、複合的なものとされている。 例を挙げると、カットでは各部の理想的な比率などがあらかじめ決まっている。 そのため、それにどこまで近づけることができたかは評価の一つとなる。 それ以外にも、入射した光をどれだけ反射するかも評価の一つとされる。

カットは以下のグレードに分かれていて、下にいくほど評価が低い。

  • Excellent
  • Very Good
  • Good
  • Fair
  • Poor

日本で指輪の用途として用いられるのは、ほとんど例外なく Excellent になる。 ただ、実際に Good あたりと見比べても、正直外見的な区別はつかない。 「言われてみれば少し輝きが弱いような気がしないでもない?」という程度のもの。 まあ、正直これは他の 3C でも同じことが言える。

4C 以外の評価基準

加えて、上記の 4C 以外にも補助的な評価基準がいくつかある。

Polish (ポリッシュ) / Symmetry (シンメトリー)

カットに近い概念として Polish (ポリッシュ) と Symmetry (シンメトリー) という評価基準がある。 ポリッシュは研磨、シンメトリーは各部位の対称性を元にした評価基準になっている。 グレードはカットと同様に Excellent ~ Poor までに分かれている。 販売するお店によってはカット、ポリッシュ、シンメトリーがすべて Excellent のダイヤモンドを 3EX (Triple Excellent) と呼ぶこともある。

また、これもお店によるものの 3EX に加えて H&C (Heart and Cupid) という単語がさらに後ろに付与されることもある。 これは、上手くカットしたルースを下から見るとハートが、上から見ると矢のような形が見えることに起因している。 ただし、これは世界的な評価基準ではなく、単なる付加価値をつけるための宣伝文句なので無視して良い。

Fluorescence (蛍光性)

天然ダイヤモンドは紫外線をあてると発光するものが多く、その光の強さや色を用いた評価基準。

日本においては蛍光性が弱いもしくはない方が値段が高い傾向にあるとされる。 ほとんどの場合に蛍光色は青色で、これは結晶の中に窒素が含まれる場合とされる。 蛍光の仕方は、後ほど人工ダイヤモンドのところで説明するダイヤモンドのタイプに関係する。

鑑定機関

上記の 4C やその他の基準を評価するのが鑑定機関になる。 鑑定機関は依頼を受けて有償でダイヤモンドを評価し、その結果を鑑定書の形で石に付与する。

鑑定機関は世界各国にあるけど GIA (Gemological Institute Of America: 米国宝石学会) という所がデファクトスタンダードになっている。 日本国内だと CGL (Central Gem Laboratory: 中央宝石研究所) が有名だけど、基本的に国内でしか通用しない。

鑑定書のフォーマットも機関ごとにいくつかあって、高いものほどより詳細なレポートが載る。 鑑定書つきの宝石は、少なくとも鑑定料 (数千円前後) の分はついていないものよりも高くなる。

ダイヤモンドの質を評価する機関なので、後述する人工ダイヤモンドの検出技術には深い関心を寄せており知見を有する。

ダイヤモンドの値段の決まり方

ダイヤモンドは市場において売買する人が自由に価格を決めることができる。 ただし、市場に供給されるダイヤモンドの質と量は、カルテルによって統制を受けているとされる。 供給が統制されることで、市場で取引されるダイヤモンドの価格は安定することになる。

また、カルテルの中でも特に有名なデビアス社はラパポート・ダイヤモンド・レポートという業界誌を発行している。 その中には、各グレード (4C) ごとのダイヤモンドの取引相場が記載されている。 つまり、ダイヤモンドは 4C が分かれば大体の値段が決まるようになっている。

世界中のダイヤモンドの取引価格は、供給量や業界誌といった形で直接的・間接的に影響を受ける。 それらが良いか悪いかは別として、少なくともダイヤモンドには世界的な相場があるということ。

では、日本のジュエリーショップに並ぶダイヤモンドは相場に則した価格になっているか、というとなっていない。 ほとんどの場合、相場を知っていればおよそ買わないような数倍の値付けが一般的となっている。

ジュエリーショップでは、あの手この手でその値付けを正当化する材料を用意する。 例えば、宝石の形で一度も他人の手に渡ったことがない点をバージンと称してみたり。 あるいは、独自の鑑定基準にもとづいて他社よりも高品質であることをアピールする。 当然ながら、それらは世界的な取引基準の上では、なんら評価の対象にはならない。

何処であれば市場価格に近い値段で手に入るのか?

少なくとも、宝石業界向けの競売や宝石問屋からの直接の購入であれば近い値付けになるようだ。

ただし、前者はそもそも業界人でないと参加できない。 そのため、購入するには参加する権利を持った人に依頼して、代わりに競り落としてもらう必要がある。 依頼には手間賃がかかるし、いつ目当てのグレードが出てくるかも分からない。 そうした都合から、このやり方は大口でなければ難しい気がする。

宝石問屋からの購入は、比較的現実的な方法かもしれない。 ただし、これには注意点が二つほどある。 まずひとつ目は、そもそも問屋は普段から一般人を相手に商売をしているわけではないという点。 この方法を試した人のレポートを読むと、店にもよるだろうけどかなりの塩対応は覚悟する必要があるようだ。 また、問屋は平日しか営業していないので、そう何度も企業勤めの人間が赴くのも難しい。 二つ目は、問屋「風」を装って一般人をターゲットにしたお店がある点。 そういったお店は問屋風を装ってはいるが実態はただの小売で、値付けも一般的なジュエリーショップと大して変わらない。 安さを求めて宝石問屋街に行くとしても、そうしたお店は避ける必要がある。

上記のような事情から、なかなか難しいなと思っていたところで次に見つけたのはマージンを抑えた小売店だった。 例えば、国内にも市場価格に比較的近い値付けをしている店舗はいくつかあるみたい。 とはいえ、地理的にさほど近くなかったりオンライン限定販売だったり、なかなかこれはと思うものがない。

で、行き着いた先は Blue Nile という米国のオンラインショップだった。 当初、さすがにインターネットで買うのは...と不安に思っていたものの、アメリカでは有名な企業らしい。 例えば NASDAQ にも上場していたりする (シンボル: NILE)。 日本から購入した人のレポートも、検索すると結構出てくる。

Web サイト経由で購入すると、だいたい 2 週間ほどでルースが手元に届いた。 注意点としては、サイトでの購入価格の額面以外に税金が 10% 弱かかる。 それでも、日本国内の一般的なジュエリーショップで買うよりは大幅に安い。 ちなみに、タイトルにある通り日本から購入すると形式としては個人輸入という形になる。

f:id:momijiame:20180517025355j:plain

f:id:momijiame:20180517025233j:plain

ただし、このエントリは決してダイヤモンドの海外通販は良いぞ、と言うことが目的ではない。 むしろ、絶対に必要でない限り今ダイヤモンドを購入するのは避けた方が良いと思う。 これは、後述する昨今の人工ダイヤモンド事情からそう考えている。 とはいえ、全ての事情を理解した上で、あえて購入するときの選択肢であればアリかもしれない。

ダイヤモンドのルースを購入する際の注意点

続いては、購入したルースを持ち込んで指輪にする際の注意点について。

まず前提として、ルースの持ち込みに対応しているお店はジュエリーショップの中でも少ない。 もし、パートナーが持ち込みに対応していないブランドや枠のデザインがお気に入りなら、あきらめる他ないと思う。

また、ルースの持ち込みが可能でも、それに別途料金がかかるお店もある。 あとは、持ち込む石のグレードについて一定の基準を設けているお店もある。 これは、例えばカラーなら G 以上でクラリティは VS2 以上でないと受け付けません、みたいな感じ。

人工ダイヤモンド事情

話は変わって人工ダイヤモンド (Synthetic diamond) について。 自分がダイヤモンドの資産価値に懐疑的な理由が人工ダイヤモンドの存在だった。 そして、その考えは今でも変わっていない。

元々、ダイヤモンドはその硬さから工業用途で需要がある。 工業用途の人工ダイヤモンドは、既にカラットあたり数百円というレベルで量産されている。 また、近年は技術の進歩と共に宝飾用途でも人工ダイヤモンドの利用が進みつつある。

特に、最近になって「マジか」というような話題があった。 ダイヤモンドカルテルでも代表的な企業であるデビアス社が、CVD 法 (後述) で作った人工ダイヤモンドを売り出すというのだ。

gigazine.net

「ライトボックス」というブランドで販売されるそれは、同じグレードの天然ダイヤモンドに対して数分の一の価格で販売される予定になっている。

現状でも、人工ダイヤモンドを売るメーカーというのはいくつかある。 例えば、米国内だと Gemesis 社や Apollo Diamond 社が販売している。

とはいえ、ダイヤモンドのドンといえるデビアス社が率先して人工ダイヤモンドを売り出すというのは、驚きだった。 きっと、日本でも米国から数年遅れで人工ダイヤモンドの波がくることになるだろう。

(2018/06/24 追記)

上記のライトボックスジュエリーは米国内で 9 月から発売されるらしい。

www.nikkei.com

(2018/6/28 追記)

ライトボックスジュエリーの公式サイトができていた。

lightboxjewelry.com

人工ダイヤモンドの作り方

ここから先の内容は、自分が興味から調べたものなので、ほとんどの人にとっては誰得だと思う。 読み飛ばしてもらって構わない。 人工ダイヤモンド関連で読み物として面白かったものは末尾に記載しておく。

以下に、宝飾用途の商業ベースで用いられる人工ダイヤモンドを作る手法を紹介する。 共通して言えるのは、以前はかかるコストとできあがりのダイヤモンドの質が釣り合わず、採算がとれなかった。 それが近年は技術の進歩と共に変化しつつある。 勘違いしやすいのは、人工ダイヤモンドといっても簡単にカラーレスで大きな原石ができるわけではないという点。

www.nikkei.com

HPHT (High Pressure High Temperature: 高温高圧合成) 法

炭素のペレットを高温・高圧で処理することで結晶化させる方法。 結晶構造が生み出される原理的には、天然ダイヤモンドと何ら変わらない。 ダイヤモンドになるまでには 1, 2 週間かかるので、その間ずっと加熱・加圧し続ける必要がある。

亡くなった人の遺骨からダイヤモンドを作る、みたいな話を聞いたことがあると思うけど、あれにはこの手法を使う。

また、天然ダイヤモンドや後述する CVD 法を使った人工ダイヤモンドの色調改善にも用いられる。

CVD (Chemical Vapor Deposition: 化学気相蒸着) 法

核となるダイヤモンドに炭素を蒸着することで少しずつ育てていく方法。 端的に言うと、水素とメタンガスの入ったチャンバーに核ダイヤを入れて、マイクロウェーブを照射すると少しずつ大きくなる。

HPHT 法と比べると、育成環境によってできあがるダイヤモンドの特徴を調整しやすい。 また、同じ育成環境であれば、できあがるダイヤモンドの質も揃えることができる。 そのため、宝飾用途の人工ダイヤモンドの生成では、こちらの手法が主流となっている。

育成速度の向上や、同時にチャンバー内で育成できるダイヤの数の増加によってコスト低減が図られてきた。 また、育成環境の調整によってできあがるダイヤモンドのグレードを改善することでより高価値を生み出せる。 天然ものなら数千万円するようなファンシーカラーダイヤモンドを人の手で作り出せるとしたら? 夢が広がる。

また、前述した通り手法として CVD 法 or HPHT 法という二者択一ではない。 CVD 法で育てたダイヤモンドを HPHT 法で調整する、といった手法も一般的になっている。

人工ダイヤモンドの判別方法

宝飾用途でも人工ダイヤが作れる、ということが分かったところで続いてはその判別について。

まず大前提として、人間の裸眼で人工ダイヤモンドの判別はできない。 これは、一般人なら無理でも鑑定士なら、といったレベルの話ではない。 人工ダイヤモンドかそうでないかの判別は、専門の機械がなければ不可能になっている。 つまり、天然ダイヤモンドと人工ダイヤモンドの外見的な差異は全くない。

それでは、専門の機械ではどのように判別しているかというと、これには色々な手法がある。 例えば、含有する不純物にもとづいた判別方法は簡易的な検査器具でも用いられる。 というのも、ダイヤモンドは含有する不純物によっていくつかの型 (タイプ) に分けられるため。

ダイヤモンドの型 (タイプ)

ダイヤモンドは、ざっくり言うと窒素を含むか含まないかで二つの型 (タイプ) に分けられる。 窒素を含むものを I 型、全く含まないものを II 型と呼ぶ。 それぞれのタイプは、窒素の含有量や窒素以外の元素を含むかで、さらに細分化されている。

天然で産出するダイヤモンドは、そのほとんど (約98%) が窒素を明瞭に含む Ia 型に分類される。 そして、人工ダイヤモンドでは今のところ Ia 型を作り出すことはできていない (とされる)。 これにもとづいて Ia 型以外、特に II 型については人工ダイヤモンドの疑いが強くなる。

そのため、人工ダイヤモンドの判別では、まず簡易的な検査器具を用いてダイヤモンドのタイプを判定する。 その上で Ia 型でないことが分かったら、それを鑑定機関のラボに送って、より詳細な検査をすることになる。 鑑定機関のラボではフォトルミネッセンス (PL) 法などを用いてダイヤモンドの特徴を調べている。 もし仮に、人工的に安価な Ia 型のダイヤモンドを作ることができれば、そのときは市場がパニックに陥ることだろう。

I 型 (Ia / Ib)

結晶構造の中に窒素原子を含むものを I 型と呼ぶ。 そして、その含有量や含有の仕方によって Ia 型と Ib 型に分けられる。

Ia 型は窒素が明瞭 (~5000ppm) に集合体として含まれるもの。 天然ダイヤモンドのほとんど (約98%) が、この型に分類される。

Ib 型は結晶構造の中に窒素原子が単独でわずかに (~500ppm) 含まれるもの。 HPHT 法で作る・あるいは処理した人工ダイヤモンドは、この型になることが多い。

II 型 (IIa / IIb)

結晶構造の中に窒素原子を含まないものを II 型と呼ぶ。 そして、ホウ素を含むか含まないかで IIa 型と IIb 型に分けられる。

IIa 型は窒素が全く含まれないもの。 CVD 法で作った人工ダイヤモンドは基本的にこれになる。

IIb 型は窒素が含まれない代わりにホウ素が含まれるもの。 その他の型のダイヤモンドが絶縁体なのに対し、この型だけは半導体の特性を持つ。

まとめ

これで、書きたかったことはだいたい書けたと思う。 伝えたかったことは、最初の TL;DR に記載した通り。

  • 日本のジュエリーショップで販売されているダイヤモンドは高い
    • 具体的には、世界的な市場取引価格から大きく乖離した値付けがされている
  • 今は天然ダイヤモンドを購入するにはおそらく最悪のタイミング
    • もう少しで宝飾用途の人工ダイヤモンドが安価に手に入るようになるため
  • 全てを理解した上でそれでも購入するなら、なるべく安価な販売チャネルを選ぶのが望ましい

経済的な合理性からいえば、今のタイミングでダイヤモンドを購入したのは明らかな誤りだったと思う。 とはいえ、今回に関しては資産を入手することが目的で購入したわけではない。 パートナーに喜んでもらう、という当初の目的は果たせた。 そうした点からは、現状で取りうるベターな選択肢は選べたように感じる。

参考

人工ダイヤモンド関連で、読み物として興味深かったものを以下に記載しておく。

合成ダイヤモンドの最新事情

CVD成長法による合成ダイヤモンド、パート1: 歴史

CVD成長法による合成ダイヤモンド、パート2: 特性

CVD合成ダイヤモンド、パート3: 検出

合成ダイヤモンド:品質の向上と鑑別の課題

HPHT成長法による大型の合成ダイヤモンドをGIA香港ラボが検査

Identifying Lab-Grown Diamonds | Research & News

CVD Synthetic Diamond Over 5 Carats Identified by GIA | Gems & Gemology

普段は技術系のことも書いてます。

Python: LightGBM でカテゴリ変数を扱ってみる

以前このブログで LightGBM を使ってみる記事を書いた。 ただ、この記事で使っている Iris データセットにはカテゴリ変数が含まれていなかった。

blog.amedama.jp

そこで、今回はマッシュルームデータセットを使ってカテゴリ変数が含まれる場合を試してみる。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V
Python 3.6.5

マッシュルームデータセットについて

マッシュルームデータセットはAgaricales Agaricaceae Lepiota という品種のキノコの外見的な特徴と毒の有無を記録したもの。 具体的には、次のような特徴が全てカテゴリ変数の形で入っている。 要するに、外見的な特徴から毒の有無を判定するモデルを作りたい、ということ。

  • 毒の有無
  • 傘の形
  • 傘の表面
  • 傘の色
  • 部分的な変色の有無
  • 匂い
  • ひだの形状
  • ひだの間隔
  • ひだの大きさ
  • ひだの色
  • 柄の形
  • 柄の根本
  • 柄の表面 (つぼより上)
  • 柄の表面 (つぼより下)
  • 柄の色 (つぼより上)
  • 柄の色 (つぼより下)
  • 覆いの種類
  • 覆いの色
  • つぼの数
  • つぼの種類
  • 胞子の色
  • 群生の仕方
  • 生息地

データセットは次の Web サイトから得られる。

UCI Machine Learning Repository: Mushroom Data Set

まずは wget や curl を使ってデータセットをダウンロードしておこう。

$ wget https://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.data

下準備

続いて、今回使う Python のパッケージをインストールしておく。

$ brew install cmake gcc@7
$ export CXX=g++-7 CC=gcc-7
$ pip install --no-binary lightgbm lightgbm pandas

インストールできたら Python のインタプリタを起動する。

$ python

データセットの CSV をパースする

続いてはダウンロードしてきた CSV を pandas の DataFrame に変換する。 ここで注目すべきは dtype として 'category' を指定しているところ。 これによってパースされた各次元がカテゴリ型として扱われる。

>>> import pandas as pd
>>> columns = [
...     'poisonous',
...     'cap-shape',
...     'cap-surface',
...     'cap-color',
...     'bruises',
...     'odor',
...     'gill-attachment',
...     'gill-spacing',
...     'gill-size',
...     'gill-color',
...     'stalk-shape',
...     'stalk-root',
...     'stalk-surface-above-ring',
...     'stalk-surface-below-ring',
...     'stalk-color-above-ring',
...     'stalk-color-below-ring',
...     'veil-type',
...     'veil-color',
...     'ring-number',
...     'ring-type',
...     'spore-print-color',
...     'population',
...     'habitat',
... ]
>>> df = pd.read_csv('agaricus-lepiota.data', header=None, names=columns, dtype='category')

上記でマッシュルームデータセットの 23 次元の特徴を持ったデータが読み込まれた。

>>> df.head()
  poisonous cap-shape cap-surface   ...   spore-print-color population habitat
0         p         x           s   ...                   k          s       u
1         e         x           s   ...                   n          n       g
2         e         b           s   ...                   n          n       m
3         p         x           y   ...                   k          s       u
4         e         x           s   ...                   n          a       g

[5 rows x 23 columns]
>>> df.dtypes
poisonous                   category
cap-shape                   category
cap-surface                 category
cap-color                   category
bruises                     category
odor                        category
gill-attachment             category
gill-spacing                category
gill-size                   category
gill-color                  category
stalk-shape                 category
stalk-root                  category
stalk-surface-above-ring    category
stalk-surface-below-ring    category
stalk-color-above-ring      category
stalk-color-below-ring      category
veil-type                   category
veil-color                  category
ring-number                 category
ring-type                   category
spore-print-color           category
population                  category
habitat                     category
dtype: object

pandas のカテゴリ型は Series#cat#categories で水準を確認できたりする。

>>> df.poisonous.cat.categories
Index(['e', 'p'], dtype='object')

整数値にラベルエンコードする

ただし、上記ではカテゴリ型の中身が 'object' になっているため、そのままでは LightGBM に食べさせることができない。 LightGBM では int, float, boolean しか扱うことができないため。 そこで scikit-learnLabelEncoder を使って対応する整数値に変換する。

>>> from sklearn import preprocessing
>>> for column in df.columns:
...     target_column = df[column]
...     le = preprocessing.LabelEncoder()
...     le.fit(target_column)
...     label_encoded_column = le.transform(target_column)
...     df[column] = pd.Series(label_encoded_column).astype('category')
... 

これで DataFrame がカテゴリ型のまま中身が整数になった。

>>> df.head()
   poisonous  cap-shape   ...     population  habitat
0          1          5   ...              3        5
1          0          5   ...              2        1
2          0          0   ...              2        3
3          1          5   ...              3        5
4          0          5   ...              0        1

[5 rows x 23 columns]

あとは普通に LightGBM で学習して汎化性能を検証するだけ。

まずはデータセットを説明変数と目的変数に分割する

>>> X, y = df.drop('poisonous', axis=1), df.poisonous

続いてデータセットを学習用、ハイパーパラメータ調整用、ホールドアウト検証用の 3 つに分割する。

>>> from sklearn.model_selection import train_test_split
>>> # データセットを学習用と検証用に分割する
... X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.7, random_state=42)
>>> # 検証用データセットをハイパーパラメータ調整用とバリデーション用に分割する
... X_eval, X_test, y_eval, y_test = train_test_split(X_val, y_val, test_size=0.5, random_state=42)

LightGBM を学習させる

続いて、分割したデータを元に LightGBM を学習させる。 学習に使う元ネタが pandas の DataFrame で、適切にカテゴリ型になっていれば LightGBM はそれを識別してくれる。

>>> import lightgbm as lgb
>>> lgb_train = lgb.Dataset(X_train, y_train)
>>> lgb_eval = lgb.Dataset(X_eval, y_eval, reference=lgb_train)
>>> # 学習用パラメータ
... lgbm_params = {
...     # 二値分類問題
...     'objective': 'binary',
...     # 評価方法
...     'metric': 'binary_error',
... }
>>> # 学習
... model = lgb.train(lgbm_params, lgb_train, valid_sets=lgb_eval)

ホールドアウト検証を用いた性能評価

学習させたモデルをホールドアウト検証用のデータセットで評価する。

>>> # バリデーション用データセットで汎化性能を確認する
... y_pred_proba = model.predict(X_test, num_iteration=model.best_iteration)
>>> import numpy as np
>>> # しきい値 0.5 で最尤なクラスに分類する
... y_pred = np.where(y_pred_proba > 0.5, 1, 0)

今回、精度で評価したところ全てのデータを正しく判別できた。 分割方法などにも依存するものの、このデータセットとモデルであればほぼ 100% に近い精度が得られるようだった。

>>> # 精度を確認する
... from sklearn.metrics import accuracy_score
>>> accuracy_score(y_test, y_pred)
1.0

また、カテゴリ型が指定されていない pandas の DataFrame や、それ以外をデータセットの元ネタにするときは、次のように categorical_feature を指定すると良い。

>>> lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=list(X.columns))

いじょう。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Python: pandas で縦持ちのデータを横持ちにする

データ処理の世界では、データの持ち方に縦持ちと横持ちという考え方がある。 縦持ちでは、レコードに種類といったカラムを持たせてデータを追加していく。 それに対し横持ちでは種類ごとにカラムを用意した上でデータを追加する形を取る。 一般的にはデータの持ち方としては縦持ちのものが多いと思う。 今回は pandas で縦持ちのデータを横持ちに直す方法について書く。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V    
Python 3.6.5

下準備

まずは pandas をインストールしておく。

$ pip install pandas

続いて Python のインタプリタを起動する。

$ python

あとは縦持ちの形式になったサンプルの DataFrame オブジェクトを用意する。 今回は、時系列データで商品が売れた数っぽいものを用意した。

>>> import pandas as pd
>>> data = [
...     ('2018-05-30 20:00:00', 'Onigiri', 1),
...     ('2018-05-30 20:00:00', 'Pan', 2),
...     ('2018-05-30 20:00:00', 'Pasta', 1),
...     ('2018-05-30 20:00:10', 'Onigiri', 3),
...     ('2018-05-30 20:00:20', 'Pan', 2),
...     ('2018-05-30 20:00:30', 'Pasta', 1),
...     ('2018-05-30 20:01:00', 'Onigiri', 2),
...     ('2018-05-30 20:01:00', 'Pan', 2),
...     ('2018-05-30 20:01:00', 'Pasta', 2),
... ]
>>> columns = ['datetime', 'code', 'amount']
>>> df = pd.DataFrame(data, columns=columns)
>>> df = df.assign(datetime = pd.to_datetime(df.datetime, format='%Y-%m-%d %H:%M:%S'))

出来上がる DataFrame はこんな感じ。

>>> df
             datetime     code  amount
0 2018-05-30 20:00:00  Onigiri       1
1 2018-05-30 20:00:00      Pan       2
2 2018-05-30 20:00:00    Pasta       1
3 2018-05-30 20:00:10  Onigiri       3
4 2018-05-30 20:00:20      Pan       2
5 2018-05-30 20:00:30    Pasta       1
6 2018-05-30 20:01:00  Onigiri       2
7 2018-05-30 20:01:00      Pan       2
8 2018-05-30 20:01:00    Pasta       2
>>> df.dtypes
datetime    datetime64[ns]
code                object
amount               int64
dtype: object

横持ちにする

それでは上記を横持ちに直してみる。 pandas でデータを横持ちにするときは DataFrame#pivot_table() を使う。

インデックスには時刻 (datetime) を、集計対象は売れた数 (amount)、カラムの種類は商品 (code) でテーブルを組んでみよう。 集計用の関数には加算 (sum) を指定している。 fill_value オプションを指定しておくと、値が存在しないときにそれで埋めてくれる。

>>> df.pivot_table(values=['amount'], index=['datetime'], columns=['code'], aggfunc='sum', fill_value=0)
                     amount          
code                Onigiri Pan Pasta
datetime                             
2018-05-30 20:00:00       1   2     1
2018-05-30 20:00:10       3   0     0
2018-05-30 20:00:20       0   2     0
2018-05-30 20:00:30       0   0     1
2018-05-30 20:01:00       2   2     2

いいかんじ。

それぞれのオプションを変えるとテーブルの形を変えられる。 例えばインデックスを指定しない場合は上記から時系列情報が抜ける。

>>> df.pivot_table(values=['amount'], columns=['code'], aggfunc='sum')
code    Onigiri  Pan  Pasta
amount        6    6      4

インデックスを商品にすれば縦横が入れ替わる。

>>> df.pivot_table(values=['amount'], index=['code'], aggfunc='sum')
         amount
code           
Onigiri       6
Pan           6
Pasta         4

まあ、とはいえ上2つのような集計なら DataFrame#groupby() でも十分かな。

>>> df.groupby('code').sum()
         amount
code           
Onigiri       6
Pan           6
Pasta         4

めでたしめでたし。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

前処理大全[データ分析のためのSQL/R/Python実践テクニック]

前処理大全[データ分析のためのSQL/R/Python実践テクニック]

Python: pandas で DataFrame を連結したら dtype が int から float になって驚いた話

今回は pandas を使っているときに二つの DataFrame を pd.concat() で連結したところ int のカラムが float になって驚いた、という話。 先に結論から書いてしまうと、これは片方の DataFrame に存在しないカラムがあったとき、それが全て NaN 扱いになることで発生する。 NaN は浮動小数点数型にしか存在しない概念なので、それが元で整数型と浮動小数点数型の演算になりキャストされてしまった。

使った環境は次の通り。

$ sw_vers  
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V
Python 3.6.5

下準備

まずは下準備として pandas と numpy をインストールしておく。

$ pip install pandas numpy

インストールできたら Python の REPL を起動する。

$ python

これで下準備はおわり。

問題の起こらないパターン

それでは早速 pandas.concat() を使って二つの DataFrame を連結してみよう。 最初は問題の起こらないパターンについて概要を説明する。

まずは pandas をインポートしておく。

>>> import pandas as pd

続いて、次のように同じカラムを持った DataFrame を二つ用意する。 カラム名は両者で順番が異なっている。 最初の DataFrame はユーザ名、年齢の順になっているが、二つ目は年齢、ユーザ名の順になっている。

>>> users_df1 = pd.DataFrame([
...   ['alice', 10],
...   ['bob', 20],
...   ['carol', 30]
... ], columns=['name', 'age'])
>>> users_df2 = pd.DataFrame([
...   [40, 'daniel'],
...   [50, 'emily'],
...   [60, 'frank'],
... ], columns=['age', 'name'])

それぞれの DataFrame に含まれる年齢 (age) の dtype は int64 になっている。

>>> users_df1.age.dtype
dtype('int64')
>>> users_df2.age.dtype
dtype('int64')

それでは pandas.concat() 関数を使って上記二つの DataFrame を縦に連結してみよう。 この関数は、カラム名さえ一致すれば順番が異なっていても上手いこと連結してくれることが分かる。

>>> concat_df = pd.concat([users_df1, users_df2])
>>> concat_df
   age    name
0   10   alice
1   20     bob
2   30   carol
0   40  daniel
1   50   emily
2   60   frank

カラムの dtype も、ちゃんと int64 のままだ。

>>> concat_df.age.dtype
dtype('int64')

問題の起こるパターン

では、続いて問題の起こるパターンについて見てみよう。

連結に使う DataFrame は次の通り。 先ほどとほとんど同じだが users_df2 に関しては user_id というカラムが追加されている。 そしてポイントは user_id というカラムが users_df1 には存在しないことだ。

>>> users_df1 = pd.DataFrame([
...   ['alice', 10],
...   ['bob', 20],
...   ['carol', 30]
... ], columns=['name', 'age'])
>>> users_df2 = pd.DataFrame([
...   [10, 40, 'daniel'],
...   [20, 50, 'emily'],
...   [30, 60, 'frank'],
... ], columns=['user_id', 'age', 'name'])

追加された users_df2user_id カラムの dtype は int64 になっている。

>>> users_df2.user_id.dtype
dtype('int64')

それでは二つの DataFrame を結合してみよう。

>>> concat_df = pd.concat([users_df1, users_df2])

結合された結果を確認してみよう。 すると user_id カラムについては一部に NaN が入っており、入っていないものについては小数点以下が表示されている。

>>> concat_df
   age    name  user_id
0   10   alice      NaN
1   20     bob      NaN
2   30   carol      NaN
0   40  daniel     10.0
1   50   emily     20.0
2   60   frank     30.0

おや、と思いながら dtype を確認すると、結合前の int64 が結合後には float64 に変換されている。

>>> concat_df.user_id.dtype
dtype('float64')

何が起こったのか

NumPy をインポートして numpy.nan を参照すると、たしかに型は float になっている。

>>> import numpy as np
>>> type(np.nan)
<class 'float'>

NaN を整数型にキャストしようとしても、上手くいかない。 そもそも、整数型には NaN という概念が存在しないため。

>>> np.int64(np.nan)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: cannot convert float NaN to integer

で、よくよく考えるとそもそも整数型と浮動小数点型の演算結果は浮動小数点型に合わせて出てくるものだった。

>>> 1 + 1.0
2.0
>>> type(1 + 1.0)
<class 'float'>

つまり、最初にやっていたことをよりシンプルな形で表すと、次のようになる。 まず、numpy.int64numpy.float64pandas.Series を用意する。

>>> s1 = pd.Series([0, 1, 2], dtype=np.int64)
>>> s2 = pd.Series([np.nan, np.nan, np.nan], dtype=np.float64)

そして、それらを結合する。 結果は浮動小数点型に合わせられる。

>>> pd.concat([s1, s2])
0    0.0
1    1.0
2    2.0
0    NaN
1    NaN
2    NaN
dtype: float64

なるほど、これなら違和感なし。

じゃあ、どうすれば良いのか?

これを解決するためのアプローチとしては次の二つの方法になると思う。 - あらかじめ存在しないカラムを追加しておく - 結合した上で値を置換、キャストする

最初の方法は自明として、二つ目に関して補足する。 まず、さきほどの DataFrame があったとする。

>>> concat_df
   age    name  user_id
0   10   alice      NaN
1   20     bob      NaN
2   30   carol      NaN
0   40  daniel     10.0
1   50   emily     20.0
2   60   frank     30.0

浮動小数点になっちゃったカラムについて NaN になっているところを Series#fillna() を使って適当な値に置換する。 DataFrame.fillna() するとカラムに関係なく埋められてしまう点に注意する。

>>> concat_df.user_id.fillna(-1, inplace=True)
>>> concat_df
   age    name  user_id
0   10   alice     -1.0
1   20     bob     -1.0
2   30   carol     -1.0
0   40  daniel     10.0
1   50   emily     20.0
2   60   frank     30.0

あとは Series#astype() で元々の型にキャストする。

>>> df.assign(user_id = df.user_id.astype(np.int64))
   age    name  user_id
0   10   alice       -1
1   20     bob       -1
2   30   carol       -1
0   40  daniel       10
1   50   emily       20
2   60   frank       30

いじょう。

めでたしめでたし。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Python: pickle を使って pandas の CSV 読み込み時間を削減する

機械学習やデータ分析に使うデータセットは CSV などの形式で提供される場合がある。 それを Python で処理するときは pandas の DataFrame 形式に変換することが多い。 このとき CSV から DataFrame に変換する処理は意外と時間がかかる。 特に大きなデータセットでこれを毎回やっていると効率が悪い。 今回は、一旦読み込んだ DataFrame を pickle を使って直接ファイルに保存することで時間を節約できるという話について。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V                                
Python 3.6.5

下準備

まずは pandas と iptyhon をインストールしておく。 ipython を入れているのは、実行にかかる時間を測るのに便利なため。

$ pip install pandas ipython

サンプルとして大きな CSV のデータセットが欲しかったので適当に Kaggle から見繕ってきた。 これは大気の汚染状況に関するデータセット。

$ kaggle datasets download -d epa/hazardous-air-pollutants

kaggle コマンドについては以下の記事を参照のこと。

blog.amedama.jp

もちろん Kaggle の Web サイトからダウンロードしてきても問題はない。

Hazardous Air Pollutants | Kaggle

ダウンロードしてきたデータセットを解凍する。

$ mv ~/.kaggle/datasets/epa/hazardous-air-pollutants/hazardous-air-pollutants.zip .
$ unzip hazardous-air-pollutants.zip 
Archive:  hazardous-air-pollutants.zip
  inflating: epa_hap_daily_summary.csv

これで 2GB 強の CSV ファイルが表れる。

$ du -m epa_hap_daily_summary.csv 
2350   epa_hap_daily_summary.csv

CSV を DataFrame に変換する

それでは上記の CSV を DataFrame に変換してみよう。

まずは IPython を起動する。

$ ipython

起動したら pandas をインポートしよう。

>>> import pandas as pd

あとは pandas.read_csv() 関数で CSV を DataFrame に変換する。 このとき IPython の %time マジックコマンドで処理にかかる時間を測っておこう。

>>> %time df = pd.read_csv('epa_hap_daily_summary.csv')
CPU times: user 55 s, sys: 9.76 s, total: 1min 4s
Wall time: 1min 7s

読み込みには 1 分強かかった。

読み込まれた DataFrame は次の通り。

>>> df.head()
state_code  county_code  site_num  parameter_code  poc   latitude  \
0          42          125      5001           88103    5  40.445278   
1          48          439      1002           43843   15  32.805818   
2          22          127      9000           88128    1  32.057581   
3          18           89        22           45201    1  41.606680   
4           6           89      3003           88136    1  40.539990   

 longitude  datum      parameter_name sample_duration         ...           \
0  -80.420833  WGS84    Arsenic PM2.5 LC         24 HOUR         ...            
1  -97.356568  WGS84  Ethylene dibromide         24 HOUR         ...            
2  -92.435157  WGS84       Lead PM2.5 LC         24 HOUR         ...            
3  -87.304729  WGS84             Benzene          1 HOUR         ...            
4 -121.576460  NAD83     Nickel PM2.5 LC         24 HOUR         ...            
...(省略)

さて、今回かかった時間は 1 分とはいえちりも積もればという話もある。 また、より複雑なパースを指定すれば必要な時間はどんどん増えていく。

DataFrame を pickle で保存・復元する

そこで、次は DataFrame を pickle で保存・復元してみる。 CSV から逐一変換するのに比べて、どれくらい速くなるだろうか。

pandas の DataFrame には to_pickle() というメソッドがあるので、それを使えば DataFrame をファイルに保存できる。

>>> df.to_pickle('hazardous-air-pollutants.pickle')

約 1.5GB の pickle ファイルができた。

$ du -m hazardous-air-pollutants.pickle 
1501   hazardous-air-pollutants.pickle

それでは、上記のファイルを改めて読み込んでみる。 そのために、ここで一旦インタプリタを終了しておこう。

>>> exit()

もう一度 IPython を起動し直す。

$ ipython

まずは pickle モジュールをインポートする。

>>> import pickle

そして、次のようにして保存したファイルから DataFrame を復元する。 こちらも、処理に要する時間を %%time マジックコマンドで計測しておこう。

>>> %%time
... with open('hazardous-air-pollutants.pickle', mode='rb') as fp:
...     df = pickle.load(fp)
...
CPU times: user 4.5 s, sys: 2.24 s, total: 6.74 s
Wall time: 7.21 s

今度は約 7 秒で処理が終わった。 ここまで速くなる理由は CSV の各データを識別・パースする処理が不要になるため。

あるいは pandas の API を使って次のように一行で読み込むこともできる。

>>> %time df = pd.read_pickle('hazardous-air-pollutants.pickle')
CPU times: user 4.51 s, sys: 4.07 s, total: 8.58 s
Wall time: 9.03 s

ちゃんと DataFrame が復元されている。

>>> df.head()
state_code  county_code  site_num  parameter_code  poc   latitude  \
0          42          125      5001           88103    5  40.445278   
1          48          439      1002           43843   15  32.805818   
2          22          127      9000           88128    1  32.057581   
3          18           89        22           45201    1  41.606680   
4           6           89      3003           88136    1  40.539990   

 longitude  datum      parameter_name sample_duration         ...           \
0  -80.420833  WGS84    Arsenic PM2.5 LC         24 HOUR         ...            
1  -97.356568  WGS84  Ethylene dibromide         24 HOUR         ...            
2  -92.435157  WGS84       Lead PM2.5 LC         24 HOUR         ...            
3  -87.304729  WGS84             Benzene          1 HOUR         ...            
4 -121.576460  NAD83     Nickel PM2.5 LC         24 HOUR         ...            

めでたしめでたし。

Python: 機械学習の学習済みモデルを pickle でファイルに保存する

今回は機械学習において学習済みのモデルを取り回す方法の一つとして pickle を扱う方法を取り上げてみる。 尚、使うフレームワークによっては pickle 以外の方法があらかじめ提供されている場合もある。 例えば学習済みモデルのパラメータを文字列などの形でダンプできるようになっているものとか。

ちなみに pickle という機能自体は機械学習に限らず色々な用途に応用が効く。 より汎用な解説については以前に別の記事でまとめたことがある。

blog.amedama.jp

使う環境は以下の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V               
Python 3.6.5

下準備

今回はサンプルとして scikit-learn を使うため、まずはインストールしておく。

$ pip install scikit-learn

インストールが終わったら Python のインタプリタを起動する。

$ python

学習済みモデルを用意する

まずは学習済みモデルを用意する。 今回はサンプルとして Iris データセットを k-NN 分類器で学習させることにした。

まずは Iris データセットを読み込む。 この際、一番最後のデータについては学習から除外しておく。 これは、後ほど保存・復元したモデルに識別させるため。

>>> from sklearn import datasets
>>> iris = datasets.load_iris()
>>> X, y = iris.data[:-1], iris.target[:-1]

続いては k-NN 分類器のモデルを用意する。

>>> from sklearn.neighbors import KNeighborsClassifier
>>> clf = KNeighborsClassifier()

Iris データセットを使ってモデルを学習させる。

>>> clf.fit(X, y)
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=1, n_neighbors=5, p=2,
           weights='uniform')

これで学習済みモデルが用意できた。

学習済みモデルを保存する

続いては用意できた学習済みモデルを pickle を使って保存する。 以下のコードで model.pickle というファイル名でモデルを保存できる。

>>> import pickle
>>> with open('model.pickle', mode='wb') as fp:
...     pickle.dump(clf, fp)
... 

別のバージョンの Python からも読み込むことが考えられるときは protocol オプションに 2 を指定しておくと良い。 こうすれば Python 2.3 以降であれば保存したモデルを復元できるようになる。

>>> with open('model.pickle', mode='wb') as fp:
...     pickle.dump(clf, fp, protocol=2)
... 

これでモデルがファイルに保存できたので一旦インタプリタを終了しておく。

>>> exit()

学習済みモデルを復元する

続いては、先ほどファイルに保存した学習済みモデルを別のインタプリタから復元できることを確認しよう。

$ python

次のコードで、先ほど保存した model.pickle というファイルから学習済みモデルを復元できる。

>>> import pickle
>>> with open('model.pickle', mode='rb') as fp:
...     clf = pickle.load(fp)
... 

たしかに k-NN 分類器のモデルが得られた。

>>> clf
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=1, n_neighbors=5, p=2,
           weights='uniform')

先ほど学習に使わなかった一番最後のデータを識別させてみよう。 まずはデータセットからデータを取り出す。

>>> from sklearn import datasets
>>> iris = datasets.load_iris()
>>> X_last, y_last = iris.data[-1], iris.target[-1]

学習済みモデルで識別させてみる。

>>> clf.predict([X_last])
array([2])

モデルはデータをラベル 2 と判断した。

正解ラベルも 2 なので正しく識別できている。

>>> y_last
2

めでたしめでたし。