バグの素~間違いやすいところ

リストの初期化

問題

以下の処理を意図する。

  • 2次元リストを準備し、そのリストに1列追加する
  • 追加の際、各行を2行ずつに繰り返し、追加した列には全行を通したカウンター値を記録

これを意図したコードで以下のように意図しない結果となった。

原因

原因は14行目、19行目で複製すべきリストnew_rowを単純に代入しているためで、変数代入時に元のリストオブジェクトは複製されず重複して参照されるだけとなり、1つの変更が元のオブジェクトを通して全体に波及してしまう。

具体的にリストのi行目の処理を追うと以下の通り。

  1. 1回目のnew_row = source_list[i]で元リストのi行目が1次元リストとして共有される
  2. そのリストの最後尾にカウンター値が追加され、カウンターがインクリメントされる
    • この時点で1次元リストの最後尾にカウンター値が追加され、new_rowsource_list[i]のいずれにも参照されている
  3. 新しいリストに1次元リストが行として追加される
    • この時点で、source_list[i]new_rownew_list[2*i]が共通の1次元リストを参照している
  4. 2回目のnew_row = source_list[i]で元リストのi行目が共有される(実はこの処理は1番目の繰り返しであり意味がない)
  5. そのリストの最後尾にカウンター値が追加され、カウンターがインクリメントされる
    • この時点で、先に最後尾にカウンター値追加済みの1次元リストの最後尾に更にカウンター値が追加され、new_rowsource_list[i]のいずれにも参照されている
  6. 新しいリストに1次元リストが行として追加される
    • この時点で、source_list[i]new_rownew_list[2*i+1]が共通の1次元リストを参照している

この結果、source_listも以下のように変更される。

解決

この原因である重複参照を解消するため、元リスト各行の(参照を)代入するのではなく、copy()メソッドで新しいインスタンスを生成することで、想定した結果を得る。

結論

リストを代入するときに参照・複製を意識し、基本はcopy()で複製。

 

DataFrameのスピード~行の追加

概要

pandas.DataFrameで数千行のデータの組み換えをやろうとしたときにかなり時間がかかったので、簡単な例で実行時間を確認してみた。

結論から言うと、他の様々なサイトで言及されているように、「行単位の追加はかなり時間がかかるが、列単位の追加は圧倒的に早い」ということになる。また、先にリストなどでデータを構成しておいてからDataFrameを生成する方法も高速なことが分かった。

問題設定

次のように、3つの列を持つ行データを1万個、DataFrameに追加していく例を考える。

appendメソッド

appendメソッドは2つのDataFrameを結合するメソッドで、行の追加方法としてもよく紹介されている。実行結果は以下の通りで約7秒(3回繰り返して同程度)。

appendでリストをDataFrameにする際、リストをそのまま渡すと列と解釈されるので、2次元化して行であることを明示している。また列名を指定しないと新たな列として4~6列目に行が加えられていくので、加えるDataFrameでも列名を指定している。

リストをそのまま渡して列として生成し、行インデックスに列名を渡してDataFrameを生成してから'.T'で転置している例なども見られた。

なお、この場合のDataFrameの各要素は整数型となる。

locプロパティーはインデックス指定に注意

DataFramelocプロパティーは、スライスによって複数行・列の要素の参照・代入ができる。これを利用して、空のDataFrameに1行ずつ追加していく。実行時間は7秒台。

この場合のDataFrameの各要素も整数になる。

興味深いのことに、loc[i:, ]ではなくてloc[i]で指定すると実行時間が倍以上、20秒近くになる。

なお、locの代わりにilocを使うと"IndexError: iloc cannot enlarge its target object"とエラーになる。

DataFrameの領域を確保した場合

リストで確保した場合

予めデータのサイズがわかっている場合に、ダミーデータで埋めたリストで領域を確保してみる。領域を一気に確保して値を入れていくだけなので実行速度は速い。実行時間は0.7秒程度で、appendやlocで1行ずつ追加していくのに比べて1/10。

ここでloc[i, :]loc[i]とすると、実行時間は0.5秒程度と少し早くなる。これは1行ずつ追加する場合と逆の傾向だが、この場合はその差は追加の場合に比べて小さい。

なお、この方法では領域が既に確保されているのでilocに変更しても同じ結果となる。

ndarrayで確保した場合

リストではなくndarrayで領域を確保してみると、実行速度はリストの場合と同程度。

ただし、この場合各要素は実数となる。整数が必要ならndarrayのコンストラクターでdtype='int'を指定する。

ここでndarrayのdtypeを整数で指定すると実行時間が以下のような傾向となった。

  • int8, int16→4秒台
  • int32, int64→0.6秒台

ワード境界の中に値を埋め込んでいくのに時間がかかっていると考えられる。

列ごとのリストを加える方法はかなり速い

列ごとの辞書でDataFrameを生成する方法

列ごとのリストを作っておいて、それらから全体のデータを辞書として準備し、DataFrameを生成する方法。

これは更に速く、実行時間は0.015秒前後。loc[i, :]で行ごとに加えていく方法の1/1000の時間で済むことになる。

ただし辞書のキーで列名を指定するところがやや煩雑か。

列単位でリストを加えていく方法

列ごとのリストを、順次DataFrameに加えていく方法。

この場合もかなり速いが、上の方法では実行時間が一定しているのに対して、こちらは0.015~0.03秒と少しばらついて、ほんの僅かだが遅め。

列ごとのndarrayを加える方法

空のndarrayを準備して要素を加えていき、これを列単位でDataFrameに加える方法。

実行時間は0.25秒程度でリストの時の10倍の時間がかかっている。別途ndarrayの要素追加時部分だけの時間を計測すると、この部分だけで0.2秒台で、配列の要素追加のところで時間がかかっている。

2次元リストから生成する方法がベスト

リストなどを列ごとに加えるのではなく、2次元のリストを構成しておいて、それを使ってDataFrameを生成する方法。

実行時間は0.01~0.02秒程度で、最も早い部類に入る。順次行を追加するという発想にコードも近く、速度・可読性ともに最適のようである。

既存のDataFrameに追加する場合

既にデータがあるDataFrameに新たな行を追加する場合を考える。これまでの例で、どうやらリストの形で操作するのが速そうで、DataFrameからリストへの変換がそれなりに速いのなら、その方法が最もよさそうだと予想できる。

以下のコードはこのことを確認したもの。3つのパートに分かれていて、最初がこれまでと同じDataFrameの生成、次がDataFrameからリストへの変換、最後がリストへの追加と追加後のDataFrameの生成となっている。

DataFrameからリストへの変換は、to_numpy()メソッドでndarrayが得られるので(DataFramevaluesでもndarrayは非推奨)、それをtolist()メソッドでリストに変換している。

結果はかなり高速で、DataFrameのままでlocで追加するよりもはるかに速い。

まとめ

今回のケースの場合、1万行の追加でlocを使うと7秒で1行当たり0.0007秒。100行まとめて追加すると0.07秒で、この時点でリストに変換して追加した方が速くなる。

数少ない行を低頻度で追加するのでなければ、DataFrameにまとまった行を追加したり、既にあるDataFrameの構造を変換するには、一旦リストに変換してからデータを追加し、DataFrameに変換し直した方が速いと言える。

 

Python3 – zip関数

概要

組み込み関数zip()は、引数のコレクションやイテレーターの要素を組み合わせた値を返すイテレーター。ジッパーの左右の小さな金具が交互に合わさって一つになるイメージ。

使い方

zip()の引数に、1つにしたい複数のコレクションを与える。戻り値はイテレーター。

イテレーターなのでforループなどに使える。

引数はコレクションのほかイテレーターも可能。

引数が3つ以上でも可能。

リスト化した時の表現

zip()をリスト化した時の結果list(zip())を確認しておく。1つにまとめられた内容が、それぞれタプルとなっている。

引数の長さが違うとき

引数のコレクションやイテレーターの長さが異なるときは、短いものに合わされて、他のの残りの内容は無視される。

itertools.zip_longest()を使うと、最も長いものに合わされて、残りが指定された値で埋められる。

直接関係ないこと~zipper

“zipper”という言葉はAmerican Englishで、グッドリッチ社の商標登録だった言葉がいわゆるファスナーを指す名詞になったようだ。British Englishではzipが名詞としてファスナーの意味となる。

動詞のzipはzipperから「(ファスナーで)締める」という意味を持つ。そもそもzipという動詞には、素早く動く、飛んでいくといった意味があるらしい(informal to go somewhere or do something very quickly ~ LONGMAN)。が、日本語サイトの英和辞書では後者の訳が先に出ているが、LONGMAN/Camblidge/Oxfordなどのオンライン辞書では前者のファスナー関係の訳が先に来ている。日本語サイトがnativeの感覚と異っているのがわかる。

 

Python – リストの要素の削除

概要

リストの要素の削除をするのに、いくつかの方法がある。

  • clear()メソッド~リストの要素をすべて削除して空にする
  • pop()メソッド~引数で指定した位置の要素を削除し、その内容を返す
  • remove()メソッド~引数と一致する最初の要素を削除する
  • del()関数~引数でリストの要素・スライスを指定し、その範囲の要素を削除する

clear()メソッド

clear()メソッドはリストの全ての要素を削除して空にする。戻り値はない。

pop()メソッド

pop()メソッドは引数で指定した位置の要素を削除し、その内容を返す。

引数が要素位置の範囲を超えるとエラー。

remove()メソッド

remove()メソッドは引数に一致する要素を削除する。戻り値はない。

リスト中に存在しない要素を指定するとエラー。

delete()メソッド

delete()メソッドはリストの要素を指定して削除。戻り値はなく、結果を参照しようとするとエラー。

リストの要素範囲をスライスで指定して削除することも可能。

 

join()~リストの文字列の結合

文字列に対するjoin()メソッドは、その区切り文字を使って引数のリストの要素を結合する。

空の文字列''を使えば文字列同士をつなげて1つにできる。

もちろん文字だけでなく文字列も。

 

sklearn.preprocessing

使い方

機械学習のうち、ニューラルネットワークやSVMなどのモデルは、データの値の大きさやレンジが異なる場合、過学習になったり精度が悪くなることがあり、データを揃えるための前処理が必要になる(SVMの例ニューラルネットワークの例)。

scikit-learnのpreprocessingモジュールには、データの前処理を行う各種のクラスが準備されている。一般的な使い方は以下の通り。

  1. データを訓練データとテストデータに分ける
  2. 各preprocessorのfit()メソッドに訓練データを与えて変換用のパラメータを準備する(変換モデルを構築する)
    • fit()メソッドは、各列が特徴量、各行がデータレコードである2次元配列を想定している
  3. 変換器のtransform()メソッドに訓練データを与えて前処理を施す
  4. 同じ変換器のtransform()メソッドにテストデータを与えて前処理をほどこす

なお、fit()メソッドとtransform()メソッドをそれぞれ分けて行うほか、fit().transform()とメソッドチェーンで実行してもよい。またpreprocessorにはこれらを一体化したfit_transform()というメソッドも準備されている。

実行例

preprocessingのscaler系のクラスの1つ、MinMaxScalerを例にして、その挙動を追ってみる。

まず必要なライブラリーやクラスをインポートし、Breast cancerデータを読み込み、データを訓練データとテストデータに分ける。cancerデータは30の特徴量を列とし、569のレコードを持つが、それを3:1に分け、426セットの訓練データと143セットのテストデータとしている。

次にMinMaxScalerのインスタンスを生成し、fit()メソッドに訓練データX_trainを与えて、変換用のモデルを構築する。

preprocessingでいうモデルの構築とは、基準となるデータを与えて、変換用のパラメータを算出・保持するのに相当する。

今回の例のMinMaxScalerオブジェクトでは、特徴量数を要素数とする1次元配列で、データセット中の各特徴量の最小値(data_min_)、最大値(data_max_)、最大値-最小値のレンジ(data_range_)、レンジの逆数であるscales_がインスタンス内に保持されている。

これらのパラメーターは、30の特徴量について、426個のデータの最小値、最大値・・・などとなっている。たとえば1つ目の特徴量については、最大値-最小値は28.11−6.98=21.13となり、data_range_の1つ目の値と符合している。またscales_の各要素は、data_range_の各要素の逆数となっている。

構築された変換器によりX_trainを変換すると、すべての特徴量について最小値が0、最大値が1となる。

同じ変換器でテストデータも変換すると、変換後の特徴量の最小値・最大値は0、1になっていない。これはテストデータの最大値・最小値が必ずしも訓練データのそれらと一致しないので当然である。また、テストデータの最大値が訓練データの最大値よりも大きい場合は、テストデータの最大値が1を超えることになる。

テストデータで改めてfit()メソッドを実行してテストデータに適用するとレンジが0~1になるが、そうすると訓練データとテストデータで異なる変換を行うことになり、結果が歪んでしまう。

preprocessingの各種モデル

sklearn.preprocessingには多様な変換器が準備されているが、それらを目的ごとのカテゴリーに分けて整理する。

scaler~スケール変換

データの大きさやレンジを変換してそろえる。

MinMaxScaler
各特徴量が0~1の範囲になるよう正規化する(線形変換)。
StandardScaler
各特徴量の標本平均と標本分散を使って標準化する(線形変換)。
RobustScaler
各特徴量の中央値と4分位数を使って標準化する(線形変換)。

normalization~正則化

特徴量ベクトルのノルムをそろえる。レンジをそろえる目的のscalerに比べて、元のデータ分布の相似性はなくなる。

Normalizer
特徴量ベクトルのノルムを1にそろえる。

binalize~2値化

特徴量データを0/1の2値に分ける。

encoder~カテゴリーデータのエンコード

カテゴリーで与えられたデータ(性別、曜日など)をモデルで扱うために数値化する。

LabelEncoder
1次元配列で与えられた特徴量クラスデータを、数値ラベルに変換する。
OrdinalEncoder
2次元配列で与えられた特徴量クラスデータを、数値ラベルに変換する。
OneHotEncoder
2次元配列で与えられた特徴量クラスデータを、特徴量ごとのインジケーター列に変換する。

スケール変換の頑健性

MinMaxScalerは計算過程が簡明だが、飛び離れた異常値がわずかでもあるとそれが全体のレンジを規定し、本来適用したいデータの値が歪んでしまう。StandardScalerやRobustScalerはこのような異常値に対して頑健な変換を行う。これら3つの頑健性についてはこちらで確認している。

 

OneHotEncoder

概要

OneHotEncoderは、あるクラスデータの特徴量をエンコードする。LabelEncoderOrdinalEncoderが特徴量内のクラスに一連の数値を振るのに対して、OneHotEncoderはクラスの数だけ列を確保し、データごとに該当するクラスのみに1を立てる。エンコードされたデータは、該当するクラスのみに反応するインデックス引数となる。

なお、DataFrameget_dummis()メソッドでもone-hotエンコーディングができる。

使い方

fit()~インデックス列の生成

以下の例は、2つのクラス特徴量を持つ6個のデータセットをOneHotEncoderで変換。

  • sklearn.prreprocessingからOneHotEncoderをインポート
  • エンコーダーのインスタンスを生成
    • デフォルトではスパース行列になるので、オプションでsparse=Falseを指定
  • fit()メソッドでデータをフィッティングし、変換器を準備
  • この段階でcategories_プロパティーには各特徴量ごとのインデックス構成がセットされる

以下の例では、1つ目の特徴量は3つのクラス、2つ目の特徴量は2つのクラスを持つので、3要素、2要素の配列を要素に持つリストがcategories_にセットされる。

transform()~インデックスデータへの変換

fit()メソッドで準備された変換器によってデータを変換する。変換後のデータは特徴量のクラス数分の列を持つ2次元のndarrayで返される。なおfittransformを一度に行うfit_transform()メソッドも準備されている。

出力の右3列は3つの都市、それに続く2列は性別に対応していて、たとえば1行目のデータの都市はcategories_[0]の3番目'Tokyo'、性別はcategories_[1]の2番目の'Male'であることがあらわされている。

DataFrameによる操作

OneHotEncoderpandas.DataFrameも扱える。ただしtransfrom()fit_transform()メソッドの戻り値はndarrayなので、以下の例ではこれをDataFrameの形にしている。このときcolumns引数にエンコーダーのインスタンスのcategories_プロパティーを使うと個別のクラス名まで打ち込まずに済んで便利。

数値データとクラスデータが混在する場合

DataFrameの準備

以下の例では、2つのクラス特徴量と2つの数値特徴量を持つデータセットをDataFrameとして扱う。

クラスデータのヘッダーの準備

クラスデータを複数のインデックスデータの列にするための準備。

  • 特徴量のうち、クラスデータのものと数値データのもののヘッダーを分けておく
  • クラスデータ用のDataFrameを準備して、元データからクラスデータの列だけを切り出し
  • エンコーダーを生成してfit_trans()を実行
  • 実行後にエンコーダーのcategories_に保持されているクラスリストを取得

このクラスリストが変換後のデータのヘッダーになる。

クラスデータと数値データの合体

以下の処理では、変換されたクラスデータ列と元の数値データ列を合わせて最終的なデータセットとしている

  • クラスリストをヘッダーとして、変換後のクラスデータ(ndarray)をDataFrameとして読み込み
  • 上記DataFrameに元データの数値データを追加

この処理によって元データセットから特徴量の順番が変わるが、学習過程で特徴量の順番は影響しない。

inverse_transform()

上でdf_X_trans = df_X_class_trans.copy()としたので、df_X_class_transは保存されている。このデータをエンコーダーのinverse_transform()に与えると、複数列で表現されていたクラスが元の表現で得られる。

新しいデータの変換

訓練済みモデルにデータを与えて予測する場合、前処理のエンコーディングでは、フィッティング済みのエンコーダーに新しいデータを与えて変換する。

未知のクラスへの対処

フィッティング時になかったクラスに遭遇した場合の動作は、エンコーダーのインスタンス生成時に指定する。

OneHotEncoder(handle_unknown='error'/'ignore')

デフォルトは'error'で、未知のクラスに遭遇するとエラーを投げる。'ignore'を指定すると未知のクラスの場合はその特徴量のすべてのクラスラベルが0になる。

以下の例では、2行目のデータにフィッティングでは含まれていなかった”Nagoya”があるため、変換後のデータの2行目の1~3列が0となっている。

この変換データをinverse_transform()で逆変換すると、未知のクラスであったところは'None'に変換される。

 

 

OrdinalEncoder

概要

sklearn.preprocessingOrdinalEncoderは、2次元のデータ(行数×列数=データ数×特徴量数)を須知ラベルデータに変換する。

  • コンストラクターでencoderのインスタンスを生成
  • fit()メソッドに2次元の元データを与える(元データは2次元のリスト、ndarray、DataFrameは可)
  • 元データの特徴量ごと(列ごと)にデータが数値ラベル化される
  • 特徴量のカテゴリー数がn_classのとき、特徴量データが0~n_class−1の整数ラベルに変換される
  • 1次元のデータを変換する場合も2次元に変形する必要がある
  • 変換は全ての列が対象となり、定量的な数値データが含まれていてもそれらが数値ラベルに変換される

使い方

fit~ラベルの設定

以下の例では、3つの特徴量を持つ6つのデータを例題としている。特徴量は3つともクラスデータで、fit()メソッドで変換器の準備をする。

  • エンコーダーにおけるfit()は、特徴量ごとにクラスデータのラベルを設定し、変換器を準備する
  • フィッティングの後、categories_プロパティーにリストがセットされる
  • categories_ndarrayを要素とするリストで、各配列には特徴量ごとの重複を除いたクラス名が格納される
  • 各特徴量のクラスはcategories_各要素の配列の先頭から数値ラベル0, 1, 2, …に対応している。

transform~ラベルへの変換

この変換器のtransform()メソッドで元データを変換すると、元データと同じ次元・次数の2次元配列が得られ、各クラスデータが数値データに変換された結果が格納されている。

なお、OrdinalEncoderにもfit_transform()メソッドが準備されている。

1次元のデータを変換する場合でも、1×1の2次元とする必要があり、結果も2次元の配列で返される。

inverse_transform()で数値ラベルをクラスデータに逆変換可能。

categories_パラメーターについて

なおコンストラクターのcategories_パラメーターを指定できるが、これはあらかじめ特徴量のクラスデータがわかっている場合に、これらを全特徴量について指定する。この際、元データに含まれないクラスを含めてもよい。

数値データとクラスデータが混在する場合

クラスデータと数値データが混在する場合にOrdinalEncoderで変換すると、すべてのデータがクラスデータとみなされ、数値データもラベルに変換されてしまう。

以下の例では、最後の列の実数データも、1, 1.5, …, 5に対して0, 1, …, 5のラベルに変換されている。

このような場合は、クラスデータのみ取り出して変換させる。OrdinalEncoderpandas.DataFrameを扱うことができるので、列操作のために元データをDataFrameとする。

今回の例では、最初の3列がクラスデータなので、一時的なDataFrameにそれらを切出してOrdinalEncoderを適用する。transform()の結果はndarrayで戻るので、それを元のDataFrameの列に入れ替えている。

最後の列はそのままで、その前の3列がラベルデータに変換されている。

 

LabelEncoder

概要

sklearn.preprocessingLabelEncoderは、クラスデータ(カテゴリーデータ)を数値ラベルに変換する。

  • コンストラクターは引数をとらない
  • fit()メソッドに特徴量を要素とする1次元配列(特徴量数)の元データを与える
  • 特徴量のクラス数がn_classのとき、特徴量データが0~n_class−1の整数ラベルに変換される
  • 特徴量が定量的な数値データであっても整数ラベルに変換される

使い方

LabelEncoderを使うには、まずそのインスタンスを生成し、fit()メソッドで数値ラベルを生成する。fit()メソッドを実行すると、元データのクラスの重複を除いたクラスリストがclasses_プロパティーに保存され、transform()メソッドで任意のデータを変換する変換器が準備される。

準備された変換器で、変換したいデータにtransform()メソッドを適用して、変換された数値ラベルを得る。

このラベルデータにinverse_transform()を適用すると、数値ラベルが元のクラスデータに逆変換される。

transform()の引数に元データに存在しないクラスデータが含まれていた場合、エラーとなる。

注意

LabelEncoderは、元データに定量的な数値データを与えた場合でもこれらを数値ラベルに変換する。

transform()の引数に元データに存在しない数値が含まれている場合はエラーとなる。

 

Normalizer

概要

sklearn.preprocessorsモジュールのNormalizerは、特徴量ベクトルのノルムが1になるようにする。具体的には、データごとに特徴量Fiを以下の式によってFi*に変換する。

(1)    \begin{equation*} {F_i}^* = \frac{\sum F_i}{\left( \sum {|F_i|}^p \right) ^\frac{1}{p}} \end{equation*}

ノルムのタイプはコンストラクターの引数で指定する。デフォルトは'l2'で、その他に'l1''max'を指定可能。

Normalizer(norm='l2')

挙動

それぞれ異なる正規分布に従う2つの特徴量について、Normalizerを適用したときの挙動を以下に示す。

scalerのような相似性の変換ではないので左下の変換後のヒストグラムは変換前の形状と異なっている。

データの空間的な分布は、デフォルトのL2ノルムの指定によって全データが半径1の円周上に位置するよう変換される。

変換後のデータを拡大してみると以下の通りで、原点を中心とした半径1の円周上に各点が並んでいる。

他の2つ、L1ノルムと最大値ノルムを指定して実行した結果が下記の通りで、それぞれのノルムに応じた線上に各点が並んでいる。

コードは以下の通りで、データに対してfit()メソッドでスケールパラメーターを決定し、transform()メソッドで変換を行うところを、これらを連続して実行するfit_transform()メソッドを使っている。

特徴

Normalizerは特徴量ベクトルの方向だけが重要な場合に用いる。たとえば空間内の特定の方向範囲にあるクラスターの分離などかと思うが、抽象的なものになると想像がつかない。実際、サイト上で見ても、Normalizerの意義とデータの性質に基づいて適用しているケースは、検索上位には出てこない。

なおNormalizerによる変換は不可逆であり、scalerのようなinverse_transform()を持たない。