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

リストの初期化

問題

以下の処理を意図する。

  • 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つにできる。

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

 

matplot.pyplot – 格子でないグラフの組み合わせ

通常、Figure.subplots()pyplot.add_subplot()でグラフの描画領域を指定するとき、m行n列の格子状のグラフエリアが生成される

これに対して、たとえば1行目に2つのグラフエリアを表示して2行目に全幅のグラフを1つ、だとか、1列目に2列ぶち抜きのグラフエリアを表示して2列目に縦2つのグラフエリアを表示したいときがある。

このような場合の1つの方法が、Figure.add_subplotで加えたいグラフエリアの構成自体を変える方法がある。

以下の例は、1行目に2つのグラフを並べ、2行目は全幅で1つのグラフエリアを表示させる方法。

また、1列目に2行分を占有する一つのグラフエリアと、2列目に2つのグラフエリアを縦に並べる方法。

 

ndarrayの書式設定 – printoptions

概要

配列をprintで表示させようとして、書式設定でよく間違える。たとえば以下のように。

配列の各要素の書式を指定して表示させたい場合、formatメソッドではなく、Numpyのset_printoptionsを使う必要がある。

get_printoptions()

配列の書式オプションの一覧は、numpy.get_printoptions()で得られる。各オプションは辞書形式で保存されている。

set_printoptions()

これらのオプションを個別に設定するにはnumpy.set_printoptions()メソッドでキーと値を指定する。

numpy.set_printoptions([キー]=[値])

よく使いそうないくつかのオプションについてまとめる。

supress

デフォルトでは要素にオーダーが小さい数値が含まれていると浮動小数点表示となり、1つの要素でも浮動小数点表示になるとすべての要素が浮動小数点表示になる。

オプションで'supress=True'を指定すると、強制的に固定小数点で表示される。

precision

precisionで精度の桁数を指定する。固定小数点数の場合は小数点以下の桁数、浮動小数点数の場合は仮数部の桁数。

floatmode

floatmodeでキーワードを指定し、あらかじめ定められた書式を設定する。

次のような配列でキーワードごとの挙動を確認する。配列aは最大でもprecision設定より低い精度、配列bprecisionを超える精度の要素を持ち、デフォルトのprecision=8で表示が丸められている。

maxprec

デフォルトの設定。各要素がそれぞれ最大の精度で表示される。いずれの配列も、最大精度となる最後尾の要素の桁幅に統一されていて、0埋めはされない。デフォルトはこの設定なので、結果は上と同じ。

maxprec_equal

maxplecは0埋めされなかったが、maxprec_equalは最大精度の桁数に統一された上で0で埋められる(equalの意味が曖昧、maxprec_zeroとでもしてくれればよかったのに)。

fixed

全ての要素の精度がprecisionに統一され、それより低い精度の場合は0で埋められる。下の例では、2つの配列のすべての要素が小数点以下8桁に統一され、0で埋められている。

unique

precisionは無視され、各要素で必要な分だけの精度が保たれ、桁数は最大精度に統一される。配列bの最後の要素が丸められていないことに注意。

formatter

書式設定文字列とformatを渡して、任意の書式を設定する。渡し方は以下の通り。

formatter={'型名' : "{:書式}".format }

型名としては'int''float'のほか'numpystr'で文字列も指定できる。

 

Ruby – クラス

基本形

クラスの基本形は以下の通り。

  • クラス定義はclassで始めてendで終える
  • メソッドはdefで初めてendで終える
  • 初期化メソッド(コンストラクター)は'initialize()'
  • プロパティー(インスタンス変数)は頭に'@'をつけて、initialise()で定義
  • インスタンスの生成は[クラス名].new([引数])

initialize()~コンストラクター

インスタンス生成時の初期化処理をinitialize()に書く(コンストラクター)。インスタンス生成時にinitialize()が内部で実行され、その内容に沿った初期化が行われる(initialize()についてはこちら)。

メソッド

インスタンスメソッドはdef...endで定義する。引数を持たないメソッドの場合、()を省略してメソッド名だけで呼び出せる(これはnewについてもあてはまる)。

インスタンス変数へのアクセス

インスタンス変数はカプセル化(encapsulation、隠蔽)されている。参照したり値をセットしようとするとエラー。

インスタンス変数にアクセスするのにアクセスメソッドによる方法とgetter/setterを定義する方法がある。

クラス変数・クラスメソッド

クラス変数は、クラスから生成された全インスタンスが共通して利用する変数。

クラスメソッドはクラスレベルで定義されるメソッド。

クラスの継承

継承は'<'を使う。クラスの継承の詳細についてはこちら

 

Ruby – public/protected/private

概要

Rubyにおけるprivateprotectedは、C++やJavaでの振る舞いと一部で異なる。慣れている言語との違いというよりも、Rubyの場合言葉の概念から予想される挙動がシンプルに想起できない。

自クラス内での挙動

以下のコードで自クラス内での挙動を確認する。

直接呼出し

まず次のメソッドに着目する。

これらのメソッドを、インスタンスから直接呼び出した結果。publicメソッドは呼び出せるが、private/protectedメソッドは隠蔽されている。これは想定通り。

関数アクセス

次に、同一クラスの中でこれら3つのメソッドに関数としてアクセスするメソッドの挙動を見てみる。

これらのメソッドをインスタンスから呼び出した結果はすべてエラー無く、同一クラス内からは全て利用可能なことがわかる。

レシーバーアクセス

さらに、関数としてのアクセスではなく、レシーバーのメソッドとしてアクセスするメソッドを試してみる。

同じParentClassの新たなインスタンスを生成し、これを渡してみると、privateメソッドのみエラーとなる。

継承クラスでの挙動

ParentClassの継承クラスChildClassを定義し親クラスのメソッドを呼び出すと、上と同じ結果になり、privateprotectedは隠蔽されている。

関数呼び出しの場合、親クラスのメソッドに全てアクセス可能で、これも自クラス呼び出しと同じ。

別のChildClassのインスタンスを生成し、これをレシーバーとしてメソッドを呼び出すと、privateのみエラーとなる。

なお、関数呼び出し、レシーバー呼び出しの各internalメソッドをChildClass内でオーバーライドしても結果は同じになる。

まとめ

これらをまとめると次のようになる。

public protected private
自クラス
直接コール
× ×
自クラス
関数コール
自クラス
レシーバー
×
継承クラス
直接コール
× ×
継承クラス
関数コール
継承クラス
レシーバー
×

すなわち、public想定通り、関数コールの場合protectedprivateも自クラス・継承クラスに関わらず実行可能。レシーバー呼び出しの場合protectedは実行可能だがprivateは実行不可で、これも自クラス・継承クラスに関わらない。

 

Ruby – クラスメソッド

概要

クラスメソッドは、インスタンスではなくクラスレベルで定義されるメソッド。

定義と利用の方法

インスタンスメソッドは、メソッド名の前に'self.'を付して定義する。

インスタンスメソッドは、[クラス名].[メソッド名]で呼び出す。

インスタンスからは実行不可

インスタンスメソッドから呼び出したり、インスタンスから直接呼び出すことはできない。