NumPy where() で条件式を使用する方法
NumPy where()
関数は、リスト、タプル、および NumPy 配列内の配列要素をフィルタリングするための強力なツールです。これは、SQL クエリの WHERE 句または HAVING 句で使用されるロジックと同様の、条件付き述語を使用して機能します。 SQL に詳しくなくても大丈夫です。このチュートリアルを進めるために SQL を知る必要はありません。
通常、配列があり、その要素の値に応じて異なる方法で要素を分析する必要がある場合は、np.where()
を使用します。たとえば、負の数値をゼロに置き換えたり、None
や np.nan
などの欠損値をより意味のある値に置き換えたりする必要がある場合があります。 where()
を実行すると、分析結果を含む新しい配列が生成されます。
where()
を使用する場合、通常は 3 つのパラメータを指定します。まず、元の配列の各要素が照合される条件を指定します。次に、2 つの追加パラメータを指定します。1 つ目は、要素が条件に一致した場合に実行することを定義し、2 つ目は、 要素が条件に一致しない場合に実行することを定義します。 >。
これがすべて Python の三項演算子に似ていると思われるなら、それは正しいです。ロジックは同じです。
注: このチュートリアルでは、2 次元配列を操作します。ただし、同じ原則を任意の次元の配列に適用できます。
始める前に、NumPy 配列とその使用方法についてよく理解しておく必要があります。また、特にこのチュートリアルの後半では、ブロードキャストの主題を理解していると役立ちます。
さらに、このチュートリアルの例を実行するときに、データ分析ツール Jupyter Notebook を使用することもできます。あるいは、JupyterLab を使用すると、強化されたノートブック エクスペリエンスが提供されますが、任意の Python 環境を自由に使用できます。
NumPy ライブラリはコア Python の一部ではないため、インストールする必要があります。 Jupyter Notebook を使用している場合は、新しいコード セルを作成し、そこに !python -m pip install numpy
と入力します。セルを実行すると、ライブラリがインストールされます。コマンド ラインで作業している場合は、感嘆符 (!) を付けずに同じコマンドを使用します。
これらの準備が完了したら、準備完了です。
NumPy where()
を使用して条件式を記述する方法
where()
を使用する最も一般的なシナリオの 1 つは、条件に応じて NumPy 配列内の特定の要素を他の値に置き換える必要がある場合です。
次の配列を考えてみましょう。
>>> import numpy as np
>>> test_array = np.array(
... [
... [3.1688358, 3.9091694, 1.66405549, -3.61976783],
... [7.33400434, -3.25797286, -9.65148913, -0.76115911],
... [2.71053173, -6.02410179, 7.46355805, 1.30949485],
... ]
... )
まず、NumPy ライブラリをプログラムにインポートする必要があります。エイリアス np
を使用してこれを行うのが標準的な方法です。これにより、この短縮形式を使用してライブラリを参照できるようになります。
結果の配列は 3 行 4 列の形状になり、それぞれに浮動小数点数が含まれます。
ここで、すべての負の数値をそれらに相当する正の数値に置き換えたいとします。
>>> np.where(
... test_array < 0,
... test_array * -1,
... test_array,
... )
array([[3.1688358 , 3.9091694 , 1.66405549, 3.61976783],
[7.33400434, 3.25797286, 9.65148913, 0.76115911],
[2.71053173, 6.02410179, 7.46355805, 1.30949485]])
結果は、負の数値が正の数値に置き換えられた新しい NumPy 配列になります。元の test_array
を注意深く確認し、次に新しい all_positives
配列の対応する要素を注意深く確認すると、結果がまさに望んでいたものであることがわかります。
注: 上記の例は、where()
関数がどのように機能するかを示しています。実際にこれを行う場合は、代わりに np.abs()
関数または np.absolute()
関数を使用することになるでしょう。前者は後者の短縮形であるため、どちらも同じことを行います。
>>> np.abs(test_array)
array([[3.1688358 , 3.9091694 , 1.66405549, 3.61976783],
[7.33400434, 3.25797286, 9.65148913, 0.76115911],
[2.71053173, 6.02410179, 7.46355805, 1.30949485]])
もう一度、すべての負の値が削除されました。
where()
の他の使用例に進む前に、これがどのように機能するかを詳しく見てみましょう。前の例の目的を達成するために、条件として test_array < 0
を渡しました。 NumPy では、これにより where()
が使用するブール配列が作成されます。
>>> test_array < 0
array([[False, False, False, True],
[False, True, True, True],
[False, True, False, False]])
ブール配列は、マスク とも呼ばれ、True
または False
のいずれかの要素のみで構成されます。要素が条件に一致する場合、ブール配列内の対応する要素は True
になります。それ以外の場合は、False
になります。
このマスク配列は、ベースとなる元の配列と常に同じ形状であり、2 つの間に 1 対 1 の対応関係が生じます。これは、マスク内の要素を test_array
内の対応する要素と照合して、条件が各 test_array
要素にどのように適用されるかを決定できることを意味します。
これを確認するには、test_array
の左上の要素、3.1688358
を見てください。これはゼロ以上であるため、ブール配列の左上の要素は False
になります。逆に、-3.61976783
は確かにゼロより小さいため、test_array
の一番上の行の最後の要素は条件に一致します。したがって、ブール配列の最上行の最後の要素は True
になります。
この例では、このブール配列の True
値に一致する各要素に test_array * -1
を適用します。逆に、元の要素が 0 以上の場合は、代わりに元の test_array
要素が適用されます。言い換えれば、変わらないままになります。
元の test_array
とその結果の all_positives
配列を注意深く振り返ってください。 test_array
のすべてのネガティブな要素がポジティブな要素に置き換えられていますが、元のポジティブな要素は変更されていないことがわかります。要素が 0
だった場合も変更されません。
注: このチュートリアルでは、事前定義された配列を使用して、結果が表示されているものと一致することを確認します。 where()
関数を試して乱数を使用して配列を生成したい場合は、NumPy の一部として提供される組み込みの乱数ジェネレーターを使用して実行できます。
たとえば、次のコードを使用して、独自のランダム化されたバージョンの test_array
を作成できます。
>>> test_array = np.random.uniform(low=-10, high=10, size=(3, 4))
>>> test_array
array([[-2.71697178, -2.49701546, -7.57662054, -9.41817892],
[ 2.43095102, 0.7143025 , 0.25938839, -4.78215376],
[-7.13802191, -5.47446998, -0.47173589, -0.36727671]])
このコードを実行すると、上に示したものとは異なる乱数を含む独自の 3 行 4 列の配列が生成されます。表示される最小値は -10
ですが、最大値は 10
よりわずかに小さくなります。
where()
関数は、以前に使用したより制御されたバージョンと同じ方法でこの配列を操作しますが、結果は test_array
を再生成するたびに異なります。
おめでとう!これで、where()
関数の基本的な使用例を示すコードが作成できました。さらに準備ができている場合は、さらに複雑な条件の使用方法を学習してください。
複数の条件式の使用方法
前のセクションでは、すべての負の数値を正の数値に置き換えることに成功しました。 -2 から 3 までの値に対してのみこれを実行し、他の値は変更しないままにしたいとします。これを行うには、より複雑な条件を適用する必要があります。
if-else
に関する既存の知識があれば、次のようなことを試してみたくなるかもしれません。
>>> import numpy as np
>>> test_array = np.array(
... [
... [3.1688358, 3.9091694, 1.66405549, -3.61976783],
... [7.33400434, -3.25797286, -9.65148913, -0.76115911],
... [2.71053173, -6.02410179, 7.46355805, 1.30949485],
... ]
... )
>>> np.where(
... (test_array > -2) and (test_array < 3),
... test_array * -1,
... test_array,
... )
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ValueError: The truth value of an array with more than one element is ambiguous.
答えを与える代わりに、コードは ValueError
例外を発生させてクラッシュしました。期待していたものとは違います。
この問題が発生した理由は、and
演算子が個々の要素でしか機能しないためです。値の配列は理解できません。 test_array > -2
などのコードを記述すると、Python はバックグラウンドでブール配列を作成します。 where()
関数は条件パラメータとしてこれに対処できますが、これを and
と一緒に使用するとエラーが発生します。
解決策は、代わりにビット単位の AND 演算子 (&) を使用することです。 NumPy では、この演算子は要素ごとの AND 演算を行うためにオーバーロードされます。両方のブール配列の値を要素ごとに比較し、結果の単一のブール配列を返します。この結果は、where()
関数によって解釈され、元の配列に安全に適用できます。
以下のコードは、そのような配列を作成するために必要な手順を示しています。
>>> test_array > -2
array([[ True, True, True, False],
[ True, False, False, True],
[ True, False, True, True]])
>>> test_array < 3
array([[False, False, True, True],
[False, True, True, True],
[ True, True, False, True]])
>>> (test_array > -2) & (test_array < 3)
array([[False, False, True, False],
[False, False, False, True],
[ True, False, False, True]])
すでにご存知のとおり、test_array > -2
と test_array < 3
を実行すると、2 つのブール配列が生成されます。今回は、両方の論理積を計算するために、&
演算子を使用しました。これにより、次の各ペアに対して &
を適用した結果に基づいて、3 番目のブール配列が正常に作成されました。要素。両方のブール配列の対応する要素が True
である場合、結果の配列には True
値が含まれます。他のすべての値は False
になります。
このブール配列が where()
に渡されると、結果はより快適なものになります。
>>> np.where(
... (test_array > -2) & (test_array < 3),
... test_array * -1,
... test_array,
... )
array([[ 3.1688358 , 3.9091694 , -1.66405549, -3.61976783],
[ 7.33400434, -3.25797286, -9.65148913, 0.76115911],
[-2.71053173, -6.02410179, 7.46355805, -1.30949485]])
今回は、-2
と 3
の間の値のみが符号を変更しました。つまり、-2
より大きく、3
より小さい値の両方が置き換えられます。
複数の条件の使用方法を理解し、かっこを使用して演算子の優先順位を制御する方法を理解すると、非常に複雑な分析条件を作成し、where()
の真の力を発揮できるようになります。
別の例として、元の test_array
の符号を反転したいとします。ただし、その数値が -2 以下または 3 以上の場合に限るとします。
>>> np.where(
... (test_array <= -2) | (test_array >= 3),
... test_array * -1,
... test_array,
... )
array([[-3.1688358 , -3.9091694 , 1.66405549, 3.61976783],
[-7.33400434, 3.25797286, 9.65148913, -0.76115911],
[ 2.71053173, 6.02410179, -7.46355805, 1.30949485]])
ここでは、ビットごとの OR 演算子 (|
) を使用しました。 &
と同様に、この演算子は要素ごとの OR 演算を行うためにオーバーロードされています。式 (test_array <= -2) | (test_array >= 3)
は、|
演算子を使用してそれらを 1 つに結合する前に、もう一度 2 つのブール配列を生成します。今回は、対応する要素の少なくとも 1 つが True
である場合に限り、結果のブール配列に True
が表示されます。これを or
で試行すると、以前の and
と同じ理由で、再び ValueError
が生成されます。
元の test_array
と結果の配列の両方を注意深く見てみると、(-2, 3) の範囲外にある数値にのみ変換が適用されていることがわかります。
演習で学習を定着させましょう。これを試してみてください:
次のコードを使用して、5 行×4 列の配列を作成します。
>>> question_1 = np.arange(-10, 10).reshape(5, 4)
配列には、-10
から 9
までのすべての数値が連続して含まれます。次に、これを使用して次の課題を解決します。
where()
を使用して、question_1
内の要素が負または偶数の場合に数値9
に置き換える配列を作成します。コードを実行する前に、新しい配列に 9 がいくつあるかを予測できるかどうかを確認してください。正しかったですか?次に、
where()
を使用して、question_1
内の負の奇数を 2 乗した配列を作成します。最後に、
where()
を使用して配列を作成し、question_1
内の3
から7
までのすべての要素を置き換えます。または1
(-10
) と等しい。他のすべての要素については、それらから 1 を減算します。ああ、演算子の優先順位には注意してください。
最初の質問に対する考えられる解決策の 1 つは次のとおりです。
>>> np.where(
... (question_1 < 0) | (question_1 % 2 == 0),
... 9,
... question_1,
... )
array([[9, 9, 9, 9],
[9, 9, 9, 9],
[9, 9, 9, 1],
[9, 3, 9, 5],
[9, 7, 9, 9]])
ここでは、小なり演算子 (<) を使用して負の数値をフィルター処理し、モジュロ演算子 (%) を使用して各偶数をフィルター処理しました。要素ごとの |
演算子を使用して、いずれかの条件に一致する値をフィルター処理しました。
結果にシックスナインがあるだろうとあなたが言ったなら、それはうまくいきました。 15 個しかないと言った場合、where()
がどのように機能するかを理解できたことになります。残念ながら、既存の 9
を数えるのを忘れていました。
2 番目の質問に対する考えられる解決策の 1 つは次のとおりです。
>>> np.where(
... (question_1 < 0) & (question_1 % 2 != 0),
... np.square(question_1),
... question_1,
... )
array([[-10, 81, -8, 49],
[ -6, 25, -4, 9],
[ -2, 1, 0, 1],
[ 2, 3, 4, 5],
[ 6, 7, 8, 9]])
今回は、小なり演算子 (<) を使用して負の数値をフィルター処理し、モジュロ演算子 (%) を使用して各奇数をフィルター処理しました。 &
演算子を使用して、両方の条件に一致する値をフィルター処理しました。 np.square()
関数は、フィルターされた要素の二乗を行いました。
3 番目の質問に対する考えられる解決策の 1 つは次のとおりです。
>>> np.where(
... ((question_1 > 3) & (question_1 < 7)) | (question_1 == 1),
... -10,
... question_1 - 1,
... )
array([[-11, -10, -9, -8],
[ -7, -6, -5, -4],
[ -3, -2, -1, -10],
[ 1, 2, -10, -10],
[-10, 6, 7, 8]])
&
演算子を使用して、3
から 7
までの値をフィルタリングしました。次に、結果のブール配列を取得し、|
演算子を使用して 1
に等しい値を含めました。
このワークアウトが完了したら、次は条件付きでアレイ ブロードキャストを実行する方法を学びましょう。
条件式で配列ブロードキャストを使用する方法
これまでに見てきた例では、条件は既存の配列の要素に対して計算を実行して、新しい値を生成しました。これは where()
の非常に一般的な使用例ですが、where()
関数を使用して配列内の要素を他の配列の要素と置き換えることもできます。症状の結果に応じてです。
これを可能にするには、条件で使用する配列が、値を置き換える元の配列とブロードキャスト互換性がある必要があります。ブロードキャストを使用すると、複雑なループを作成することなく、形状の異なる配列間で演算を実行できます。
2 つの配列は、右端の次元が同一であるか、これらの次元のいずれかが 1 である場合、ブロードキャスト互換性があります。配列がブロードキャスト互換性があると、where()
関数と一緒に使用できます。
例として、次の配列があるとします。
>>> booking_data = np.array(
... [
... [np.nan, np.nan, 1],
... [1, 1, np.nan],
... [1, np.nan, 1],
... [1, 1, 1],
... ]
... )
次に、booking_data
配列にホテルの食事予約の詳細が含まれていると想像してください。各行は個別のゲストを表し、各列はメニュー要件を表します。左端の列で 1
を使用して朝食のリクエストを表し、中央の列で 1
を使用して昼食のリクエストを表し、1
を使用します。夕食のリクエストを表す右端の列。 np.nan
は、食事がリクエストされていないことを示します。
配列には 4 行と 3 列が含まれています。これは、booking_data
配列の .shape
インスタンス変数によって定義されます。
>>> booking_data.shape
(4, 3)
次に、次の配列を考えてみましょう。
>>> meal_prices = np.array([5.1, 8.2, 20.3]).reshape(1, 3)
>>> no_charge = 0
meal_prices
配列には、1 行 3 列の価格情報が含まれます。料金は朝食が5.10ドル、昼食が8.20ドル、夕食が20.30ドルです。今回の配列の形状は(1,3)です。
ここで注意すべき点は、booking_data
と meal_prices
は、3
の右端の寸法が同一であるため、ブロードキャスト互換性があるということです。これにより、一方の配列の要素をもう一方の配列の要素に置き換えることができます。
no_charge
変数も作成し、値 0
を割り当てました。これは単一の数値であり配列ではありませんが、単一の数値は任意のサイズの配列にわたってブロードキャストできます。つまり、常にブロードキャスト互換性があります。
ここで、各 1
を対応する価格と各 に置き換える新しい
と booking_prices
配列を作成して、booking_data
配列をクリーンアップするとします。 >np.nan0
。 where()
関数を使用すると、これを行うことができます。
>>> booking_prices = np.where(booking_data == 1, meal_prices, no_charge)
>>> booking_prices
array([[ 0. , 0. , 20.3],
[ 5.1, 8.2, 0. ],
[ 5.1, 0. , 20.3],
[ 5.1, 8.2, 20.3]])
ご覧のとおり、booking_data == 1
の部分、meal_prices
の対応する要素が booking_prices
に挿入されています。それ以外の場合は、0
が挿入されます。
これは確かに where()
の強力な使用法ですが、ここでの原則は以前の使用例と同じです。 booking_data == 1
パラメータはブール配列を作成しました。このブール配列の要素が True
の場合、meal_prices
配列の対応する要素が結果で使用されます。要素が False
の場合、代わりに no_charge
の値、つまり 0
が使用されます。
no_charge
の挿入値が float
であることに気づいたかもしれません。これは、配列内にすでに浮動小数点が存在するため、配列の均一性を保つために、整数は自動的に float
型にサイズ変更されます。
別のトレーニングの時間:
次のコードを使用して question_2 配列
を作成します。
>>> question_2 = np.arange(12).reshape(3, 4)
次に、2 つの変数 high
と low
を作成し、次のように文字列を割り当てます。
>>> high = "HIGH"
>>> low = "LOW"
ここで、where()
関数を使用して、次の 3 つの手法を使用して、6
より大きいすべての数値を文字列「HIGH」に置き換え、その他すべてを「LOW」に置き換えます。
上で定義したように、
question_2
、high
、およびlow
変数を使用します。新しい
high
変数とlow
変数に、それぞれ文字列「HIGH」と「LOW」を含む配列を割り当てます。追加の課題として、これらの配列の両方を異なる形状にしながらも、
question_2
とのブロードキャスト互換性を維持できるかどうかを確認してください。
いずれの場合も、結果は同じになるはずです。
最初の質問に対する考えられる解決策の 1 つは次のとおりです。
>>> question_2 = np.arange(12).reshape(3, 4) # Shape (3, 4)
>>> high = "HIGH"
>>> low = "LOW"
>>> np.where(question_2 > 6, high, low)
array([['LOW', 'LOW', 'LOW', 'LOW'],
['LOW', 'LOW', 'LOW', 'HIGH'],
['HIGH', 'HIGH', 'HIGH', 'HIGH']], dtype='<U4')
2 番目の質問に対する考えられる解決策の 1 つは次のとおりです。
>>> question_2 = np.arange(12).reshape(3, 4) # Shape (3, 4)
>>> high = np.array(["HIGH"]) # Shape (1,)
>>> low = np.array(["LOW"]) # Shape (1,)
>>> np.where(question_2 > 6, high, low)
array([['LOW', 'LOW', 'LOW', 'LOW'],
['LOW', 'LOW', 'LOW', 'HIGH'],
['HIGH', 'HIGH', 'HIGH', 'HIGH']], dtype='<U4')
3 番目の質問に対する考えられる解決策の 1 つは次のとおりです。
>>> question_2 = np.arange(12).reshape(3, 4) # Shape (3, 4)
>>> high = np.array(["HIGH", "HIGH", "HIGH", "HIGH"]) # Shape (4,)
>>> low = np.array(["LOW"]) # Shape (1,)
>>> np.where(question_2 > 6, high, low)
array([['LOW', 'LOW', 'LOW', 'LOW'],
['LOW', 'LOW', 'LOW', 'HIGH'],
['HIGH', 'HIGH', 'HIGH', 'HIGH']], dtype='<U4')
この最後の解決策では、high
と low
の形状を入れ替えることもできます。
最後に、where()
の実質的に最も単純な使用例を見てみましょう。また、ドキュメントを注意深く読んでそのような使用例を強調することの重要性も学びます。
np.where() を使用しない方法 - 最後の癖
where()
の公式ドキュメントを読むと、関数の定義が実際よりも少し複雑に見えるかもしれません。
numpy.where(condition, [x, y, ]/)
すべての Python ドキュメントと同様に、この情報をスキップして、代わりにいくつかの例を参照したくなります。ただし、時間をかけて読んでみると、この関数のさまざまな使用方法についてよりよく理解できるようになります。
まず、定義はスラッシュ (/) 文字で終わります。これは分割記号や線継続記号を表していると思われるかもしれませんが、どちらでもありません。スラッシュ特殊パラメータを最後に置くことで、ドキュメントでは、渡される各パラメータはキーワードではなく位置によって渡される必要があることを示しています。 。
最初のパラメータは要素がテストされる条件であり、2 番目と 3 番目のパラメータは正式に x
および y
< として文書化されています。 は、条件の結果に応じて実行される true または false のアクションを定義します。ただし、コード内でこれらのパラメーター名を使用することはできません。
x
パラメータと y
パラメータが角かっこで囲まれていることにも気づくでしょう。これは、これらのパラメーターを Python リストとして提供するように指示していると考えるのも無理はありません。実際、ここで角括弧は、x
と y
の両方がオプションであることを示しています。また、どちらか一方だけを通過することはできないことにも注意してください。
このチュートリアルでは、これが最も一般的なアプローチであるため、常に 3 つのパラメーターを使用してきました。ただし、最初のパラメータだけが必須であることがわかったので、他の 2 つを省略するとどうなるのか疑問に思うかもしれません。これを確認するには、以下に示すコードを見てください。
>>> import numpy as np
>>> mostly_zeroes = np.array(
... [[9, 0, 0],
... [0, 8, 5],
... [0, 0, 7]])
>>> np.where(mostly_zeroes != 0)
(array([0, 1, 1, 2]), array([0, 1, 2, 2]))
where()
関数に condition
パラメーターのみを指定すると、値がゼロ以外の要素のインデックスの配列を含む Python タプルが返されます。次元ごとに 1 つの配列が存在します。これが、上記の例で 2 つの配列が返される理由です。mostly_zeroes
には 2 つの次元 (3, 3) があります。
この少しわかりにくい出力は、位置 (0, 0)、(1, 1)、(1, 2)、および (2, 2) の要素がすべて 0 以外であることを示しています。つまり、条件によって生成される基礎となるブール配列の True
値に対応します。他の要素はゼロです。
これは、データ分析でゼロ以外の要素を強調表示するのに非常に役立ちます。
ドキュメントでは、この方法で where()
を使用することは推奨されていませんが、代わりに nonzero()
関数を直接使用することを推奨しています。
>>> np.nonzero(mostly_zeroes)
(array([0, 1, 1, 2]), array([0, 1, 2, 2]))
condition
引数のみを where()
に渡すと、バックグラウンドで nonzero()
が呼び出されるため、結果は前の例と同じになります。 。これを行うために where()
を使用しても、nonzero()
呼び出しにオーバーヘッドが追加されるだけなので、ほとんど意味がありません。 nonzero()
を使用して他の条件のインデックスを見つけることもできます。
>>> np.nonzero(mostly_zeroes == 5)
(array([1]), array([2]))
この場合、(1, 2) の要素のみが 5 に等しくなります。これが機能するのは、前に見たように、条件 mostly_zeroes == 5
がブール配列として解釈されるためです。次に、そのマスクの True
は 1
として解釈され、False
は 0
として解釈されます。つまり、条件を満たすすべての要素は非ゼロになります。
結論
これで、NumPy の where()
関数とそのパラメータの使用方法、およびこれらのパラメータを使用して、それらの要素の値に応じて配列要素に対してタスクを実行する方法について包括的に理解できました。
このチュートリアルの完了おめでとうございます。これらの新しく見つけたスキルを今後のデータ分析プロジェクトに適用して楽しんでください。