行のフィルタリング — すべての行に同じ質問を投げかける
「はい」と答えた行だけを残す:ブールマスク、&、|、~による条件結合、isin、between、.strメソッド、.locによる行の絞り込み、そして何も変更されない連鎖代入を避けて該当する行だけを確実に更新する方法。
どうやって学びますか?
解決する問題
店舗のマネージャーから連絡が来ました。「north支店の注文のうち、3個を超える注文はどれですか?確認したいです。」
前章の8件の注文であれば、目で見て答えることができました。branch列を指でなぞり、northのところで止まり、横のquantityを確認します。3行。これで終わりです。
では、これが本番のファイル——1年分、1万2000件の注文だったらどうでしょうか。もう目で追うことはできません。そこで多くのPythonプログラマーが最初に思いつくのがループです。
import pandas as pd
orders = pd.read_csv("orders.csv")
keep = []
for i in range(len(orders)):
row = orders.iloc[i]
if row["branch"] == "north" and row["quantity"] > 3:
keep.append(int(row["order_id"]))
print(keep)[1001, 1005, 1008]答えは合っています。しかし、そのためにどれだけのコストがかかったか見てみましょう。カウンター、空のリスト、if、append、そして表を1行ずつ分解する処理です。しかも、結果はもはや表ではなく、注文番号だけの単なるリストになってしまいました。マネージャーに製品や価格も見せようとすれば、また別のループを書くことになります。1万2000行もあると、これでは処理も遅くなります。orders.iloc[i]を実行するたびに、たった2つの値を読み取るためだけに新しいSeriesが作成されるからです。
pandasには別の問い合わせ方法があり、それこそがDataFrameに対して最も頻繁に行う操作です。すべての行に一度に同じ「はい/いいえ」の質問を投げかけ、「はい」と答えた行だけを残すのです。これがフィルタリングであり、この章全体のテーマです。上記のループはたった1行になり、結果も表のまま保持され、マネージャーの質問文とほぼ同じように読めるようになります。
この章を終える頃には
- 質問をブールマスク(各行に1つの
True/Falseが並ぶ列)に変換し、それを使って行を残したり、mask.sum()やmask.mean()で件数を数えたりできるようになる &、|、~を使って条件を結合し、なぜ括弧が必須なのかを説明できるようになる- リスト、範囲、文字列パターンに対して
isin、between、.strメソッドを使えるようになる .loc[mask, columns]を使って、行のフィルタリングと列の選択を1つのステップで行えるようになる.loc[mask, column] = valueを使ってフィルタリングされた行の値を安全に変更できるようになり、pandas 3において安全でない書き方が引き起こすChainedAssignmentErrorを認識できるようになる
前提条件: 列と行の選択。
まずファイルを作る
この章では前章と同じorders.csvを使用します。まだ持っていない場合は、プロジェクトフォルダ内に以下の9行で作成してください。
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0コードを書く前に
フィルタとは、コードで書かれた質問です。フィルタリングの不具合の多くは、pandasのバグではありません。質問が明確に定まっていなかったり、列の中身が想定と違っていたりすることが原因です。そのため、フィルタリングの最初の1行を書く前に、次の5つのステップを行ってください。
1. 質問を1列につき1つの条件として書き出す。 マネージャーの文を分解してみましょう。
- 「north支店の注文」→ 列
branch、比較一致、値"north" - 「3個を超える注文」→ 列
quantity、比較より大きい、値3 - 「確認したい」→ 比較なし。行全体を残す
このリストから、決めなければならないことが2つあります。「3個を超える」は> 3であり、>= 3ではありません——ちょうど3個の注文はカウントされません。そして、2つの条件はandで結ばれます。つまり、注文は両方の条件を満たさなければなりません。この2つの決定こそがフィルタのすべてであり、コードはその書き方にすぎません。
2. 入力を確認する——形状、型、そして比較対象となる正確な値。
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectquantityはint64なので、> 3は数値を比較します。もしこれが(どこかのセルに誰かが"3 pcs"と入力したせいで)strとして読み込まれていた場合、比較はTypeErrorで失敗します。途中でつまずくのではなく、今それを把握しておく必要があります。branchはstrなので文字列として比較されますが、文字列の比較は完全一致です。"north"、"North"、"north "はすべて異なる3つの値です。そのため、実際に存在する値を確認します。
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: str3つの支店があり、表記はそれぞれ1通りで、余計な空白もありません。これでコード内の"north"が確実にいずれかの行に一致することが確認できました。
3. 結果を出す前に出力の形をスケッチする。 小さなファイルであれば、まず手作業で質問に答えてみます。north支店の注文は1001(12個)、1003(2個)、1005(30個)、1008(4個)です。3個を超えるものは、1001、1005、1008です。したがって、期待される結果は全7列で3行であり、その注文番号もわかっています。あとでコードを実行して4行や0行が返ってきたら、結果を鵜呑みにせず、すぐにコードが間違っていると気づくことができます。大きなファイルではすべてを手作業で行うことはできませんが、「north支店は注文の約半分を占めているので、答えは半分よりもかなり少ない行数になるはずだ」といった見積もりは立てられます。
4. 条件の形に合わせてツールを選ぶ。
- 1つの比較 → マスク:
df["col"] > value - 複数の条件 →
&(and)、|(or)、~(not)で結合したマスク - 「指定した値のいずれか」→
df["col"].isin([...]) - 「2つの値の間」→
df["col"].between(low, high) - 文字列内のパターン →
df["col"].str.contains(...)、.str.startswith(...) - 行の絞り込みかつ一部の列のみ →
df.loc[mask, ["col1", "col2"]] - それらの行の値を変更 →
df.loc[mask, "col"] = new_value
マネージャーの質問はandで結ばれた2つの比較なので、2つのマスクと&を使います。
5. 答えの検証方法を決める。 フィルタリング後、コストのかからない3つの確認を行うだけで、ほとんどのミスを検知できます。行数がスケッチと一致していること、残ったすべての行で条件が実際に満たされていること((result["quantity"] > 3).all()がTrueを出力すること)、期待していた行が欠落していないこと(結果の中に1008があることを確認)です。
それでは、コードを見ていきましょう——1つずつ順番に進めます。
比較によってマスクが得られる
列と値を比較すると、pandasは単一のTrueやFalseを返すわけではありません。すべての行を比較し、列全体分の答えを返します。
big = orders["quantity"] > 5
print(big)0 True
1 False
2 False
3 True
4 True
5 False
6 True
7 False
Name: quantity, dtype: boolこれはboolのSeriesであり、ブールマスク(boolean mask)と呼ばれます。ordersと同じインデックス(0から7)を持ち、1行につき1つの答えが入っています。行0は12個なのでTrue、行1は5個で5は5より大きくないためFalseです。
なぜpandasはループを書かせるのではなく、このように動作するのでしょうか?列は単一の型の数値ブロックとして格納されており、ブロック全体と5を比較する処理は高速なコンパイル済みコードで1回の操作として実行されるからです。先ほどのループでは、Pythonに各行を個別に8回(あるいは1万2000回)調べるよう指示していました。マスクなら一度問い合わせるだけで済みます。
どの比較演算子も同じように機能します:==、!=、<、<=、>、>=。これらは文字列の列に対しても機能します——orders["branch"] == "north"は行0、2、4、7がTrueになるマスクです。
まだ何もフィルタリングされていません。マスクは単なる答えにすぎません。次のステップは、それを使うことです。
マスクで行を残す
角括弧の中にマスクを入れると、pandasはマスクがTrueである行を残します。
print(orders[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
3 1004 2024-03-02 east bottle accessories 7 120.0
4 1005 2024-03-03 north eraser stationery 30 8.0
6 1007 2024-03-04 east pen stationery 20 15.08行中4行が残りました。左端のインデックスを見てください:0, 3, 4, 6。残った行は元のラベルをそのまま保持します。 フィルタリングによって番号が振り直されることはありません——行3は行3のままであり、表全体にあったときと同じ注文1004です。これが重要な意味を持つことがあとでわかります。
pandasはどのようにしてマスクを行と照合しているのでしょうか?位置ではなく、インデックスのラベルによってです。マスクのラベル0が行0を決定し、ラベル3が行3を決定します。ここではマスクがorders自体から作成されたため2つのインデックスが完全に一致しており、この仕組みは見えません。別の表から作成されたマスクを使ったときに初めて表面化します——それについては「問題が起きたとき」で扱います。
通常は、変数名を付けずにインラインで直接マスクを書くのが一般的です。
print(orders[orders["branch"] == "north"])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
2 1003 2024-03-02 north bag accessories 2 850.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0内側から外側へと読んでみてください:`orders["branch"] == "north"`がマスクを作成し、`orders[ … ]`がTrueの行を残します。条件が長い場合や2回使う場合はマスクに名前を付ける(big = …)価値がありますが、短い条件ならインラインで十分です。
フィルタリングせずにマスクで件数を数える
マネージャーが行そのものではなく、単に数値だけを知りたがることがよくあります。5個を超える注文は何件ありましたか? そのためにフィルタリングを行う必要はありません。計算において、Trueは1、Falseは0として扱われるため、次のように書けます。
print(big.sum())
print(big.mean())4
0.5sum()は1を足し合わせます:4件の注文です。mean()は行数で割ります:8件中4件で0.5となり、注文の半分が5個を超えていたことがわかります。「何件あるか」はマスクの.sum()、「割合はどれくらいか」はマスクの.mean()です。どちらも1行で済み、新しい表を作成することもありません。
そして、フィルタリングされた行の合計——それらの大口注文の数量の合計——を求めたいときは、まずフィルタリングし、列を選択してから合計します。
print(orders[big]["quantity"].sum())6912 + 7 + 30 + 20 = 69です。このようにフィルタリングされた表の列を読み取るだけであれば問題ありません。しかし、この方法で書き込むのは避けるべきです——それについてはこの章の最後のセクションで解説します。
複数の条件:&、|、~
マネージャーの質問には2つの条件があります。pandasでは3つの演算子を使ってマスクを結合します。
&はand(かつ)を意味します —— 両方のマスクがTrueのときに行を残します|はor(または)を意味します —— 少なくとも一方のマスクがTrueのときに行を残します~はnot(否定)を意味します —— すべてのTrueをFalseに、FalseをTrueに反転させます
いよいよマネージャーの質問に答えるコードです。
north_big = orders[(orders["branch"] == "north") & (orders["quantity"] > 3)]
print(north_big)order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.03行:1001、1005、1008 —— 計画ステップでスケッチした通りの結果です。ここでステップ5の検証を行います。
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
True件数が一致し、残ったすべての行で両方の条件が成り立っています。.all()は「このマスクのすべての値がTrueか?」と問い合わせるメソッドです —— フィルタが意図通りに機能したことを証明する最も手軽な方法です。
orの質問:east支店の注文、または価格が850以上の商品の注文。
print(orders[(orders["branch"] == "east") | (orders["price"] >= 850)])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.04行:east支店の2件の注文と、northおよびsouth支店からの2件のbagの注文です。両方を満たす行(今回はありません)があっても、2回表示されることはなく1回だけ表示されます —— フィルタは行を残すか捨てるかのどちらかしか行いません。
notの質問:stationery(文房具)以外のすべて。
print(orders[~(orders["category"] == "stationery")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0~はすべてのTrueをFalseに、FalseをTrueに反転させます。単一の一致判定であれば代わりに!=と書くことができ、そのほうがシンプルです。~が真価を発揮するのは、否定したい対象がより長い条件やisinである場合です(これについてはすぐに説明します)。
なぜ括弧が必須なのか
上記のすべての条件は、それぞれ個別の括弧で囲まれています。これはコーディングスタイルの好みの問題ではありません。括弧を外すとコードが壊れます。
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]上記に表示されているのはトレースバックの最後の行であり、エラー名を特定する行です。その上の行はエラーが発生した場所を示しているにすぎません。原因は演算子の優先順位 —— Pythonが演算子を適用する順序です。Pythonでは、&は>や<よりも強く結合します。これはpandasが存在するずっと前に決定された仕様です。なぜなら&は整数(ビット単位の「and」)のために設計され、最初に処理されるべきだったからです。そのため、Pythonはこの行を次のように解釈します。
orders["quantity"] > (3 & orders["price"]) < 100まず3 & orders["price"]を計算しようとします —— 数値3と小数の列との間のビット単位の「and」演算です。これがエラーの原因です。メッセージにはrand_(「right-hand and」)や配列のことが書かれていますが、原因は常に同じで、括弧が1組足りないことです。
エラーが出るのはまだ運が良いほうです。最初の条件だけを囲んで2つ目を忘れた場合、エラーは一切発生しません。
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
41行目は((quantity > 3) & price) < 100と解釈されます。&によってマスクと価格が混ざり合い、その結果がたまたますべて100未満になったため、この「フィルタ」は全8行を残してしまいます。正しいフィルタが残すのは4行です。同じ列、同じ数値、括弧が1組足りないだけで、間違ったバージョンは何の音沙汰もなく動いてしまいます。常に、すべての条件を個別の括弧で囲んでください。 そうすれば、エラーの発生も、警告なしの誤動作も防ぐことができます。
なぜandではないのか?
次に誰もがやりたくなるのは、Pythonの英単語をそのまま書くことです。
print(orders[(orders["branch"] == "north") and (orders["quantity"] > 3)])ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().Pythonのand、or、notは、両辺に単一のTrueまたはFalseを必要とします —— これらはif文のために作られたからです。andを使うために、Pythonは最初のマスクに「あなたは真ですか?」と問いかけますが、8つの答えが並んだ列は1つの答えを返すことができません。いずれかの値がTrueなら真なのでしょうか?すべてがTrueなら真なのでしょうか?pandasは勝手な推測を拒否し、判断に使える方法(a.any()、a.all())をエラーメッセージで一覧表示します。しかし、あなたが求めていたのは1つの答えではなく、行ごとの「and」です。それを行うのが&です。覚えておくべきルール:フィルタの内側では、and/or/notは&/|/~になります。
複数の値のいずれか:isin
south支店またはeast支店の注文。 |で結んだ2つの一致条件を書くこともできます。しかし支店が5つあれば5つの比較になります。isinはリストを受け取り、「この行の値はリストの中に含まれているか?」と問い合わせます。
print(orders[orders["branch"].isin(["east", "south"])])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0そして、ここで~が本領を発揮します —— northとeast以外のすべての支店は、isinの前に~を置くだけです。
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]2件の注文で、どちらもsouth支店のものです。~orders["branch"].isin([...])には余計な括弧が必要ないことに注目してください。~はメソッド呼び出しの結果に直接適用され、競合する&や|が存在しないためです。別の条件と結合した瞬間に、括弧が再び必要になります:(~orders["branch"].isin([...])) & (orders["quantity"] > 3)。
範囲の指定:between
価格が15から120の注文。 これは>= 15と<= 120という2つの比較です。betweenを使えば1つで表現できます。
print(orders[orders["price"].between(15, 120)])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0betweenはデフォルトで両端を含めます —— ちょうど15.0のpenもちょうど120.0のbottleも含まれます。これは日常で言う「15から120まで」という感覚に合致していますが、質問の意図と照らし合わせて確認してください。もし質問が厳密にその間(両端を含まない)を意図しているなら、それを明示します。
print(orders["price"].between(15, 120, inclusive="neither").sum())1厳密に間にあるのは60.0のnotebookだけです。inclusiveには半開区間を表す"left"や"right"も指定できます —— 価格帯を0–100、100–200のように分ける際、境界値が2つの価格帯に重複して入らないようにするための一般的な選択肢です。
文字列パターン:.strメソッド
文字列の等価比較は完全一致です。.strアクセサを使うと、Pythonでおなじみの文字列メソッドをテキスト列の全行に適用でき、それぞれがマスクを返します。
製品名が「b」で始まる製品:
print(orders[orders["product"].str.startswith("b")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0大文字・小文字を問わず、文字「o」を含む製品:
print(orders[orders["product"].str.contains("O", case=False)])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
7 1008 2024-03-04 north bottle accessories 4 120.0データの中には大文字のOはどこにもありませんが、case=Falseが大文字・小文字を無視するため、notebookと2つのbottleが見つかります。これを指定しないと、.str.contains("O")は何も見つけることができません。
完全一致の等価比較にはそのようなオプションはありません。これが「フィルタをかけたのに何も返ってこない」という最もよくある不具合の原因です。
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []エラーは出ず、行も返ってきません。表が間違っているわけでも、pandasが間違っているわけでもありません —— 単に大文字で始まる"North"という支店が存在しないだけです。計画のステップ2でunique()を出力したのはこのためです。比較対象の値は記憶を頼りに入力するのではなく、列の中に実際に存在する値からコピーするべきです。
文字列の日付は正しく比較できる——ISO形式の場合
date列はstrとして読み込まれました。これを>=でフィルタリングできるでしょうか?3月3日以降の注文:
print(orders["date"] >= "2024-03-03")0 False
1 False
2 False
3 False
4 True
5 True
6 True
7 True
Name: date, dtype: bool正常に動作しますが、その理由を理解してください。これらは文字列であるため、>=は辞書の並び順と同じように、1文字ずつテキストとして比較します。これが正しい答えを返すのは、日付が年-月-日の形式かつゼロ埋めで記述されている(2024-03-03)ため、テキストの順序がたまたま時系列の順序と一致しているからにすぎません。3/3/2024のように書かれた列では正しく比較できません —— "1"は"9"より前に来るため、"10/1/2024"は"9/1/2024"よりも前に並んでしまいます。単純な閾値による判定を超える処理を行う場合は、列の追加の章で扱うpd.to_datetimeを使って本物の日付型に変換してください。
行と列を同時に指定:.loc[mask, columns]
ここまでのフィルタはすべての列を残していました。マネージャーはnorth支店の注文を求めましたが、レポートに必要なのは製品と数量だけです。前章ではラベルを使った.loc[rows, columns]を学びました。行の部分にはマスクを指定することもできます。
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 41つのステップ、1つのオブジェクトで完結し、コードを読む人にとっても質問の全体像 —— どの行か、どの列か —— が1箇所で把握できます。リストではなく単一の列名を指定した.loc[mask, "quantity"]は代わりにSeriesを返し、すぐに計算に利用できます。
print(orders.loc[orders["branch"] == "north", "quantity"].sum())4812 + 2 + 30 + 4 = 48個がnorth支店で販売された商品数です。
query():文章のように書くフィルタ
フィルタの書き方にはもう1つ、条件を文字列として受け取るquery()があります。
print(orders.query("branch == 'north' and quantity > 3"))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0同じ3行が得られます。文字列の中には通常の列名を書くことができ、and/or/notを使うことも許されています。なぜなら、queryはPythonの演算子に処理を渡すのではなく、文字列自体を独自に構文解析するからです。Pythonの変数を使用する場合は、接頭辞として@を付けます。
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]queryは長い条件のときに読みやすくなります。その代償として、文字列内のタイプミスはコードが実行されるまで発見できず、エディタによる補完も効かず、名前に空白を含む列にはバッククォートが必要になります。このコースでは主なツールとしてマスクを採用しています。なぜなら、pandasの他のあらゆる機能 —— .loc、値の代入、集計など —— がマスクを基盤に構築されているからです。他の人のコードを読めるようにするために、queryの書き方も知っておいてください。
フィルタリング後の表は元のラベルを保持する
インデックスの話に戻りましょう。north支店の注文を変数に保存して中身を確認してみます。
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]ラベルは0, 2, 4, 7です。では、north.loc[1]はどうなるでしょうか?この表にはラベル1は存在しません —— 注文1002はsouth支店のものであり、フィルタリングによって除外されたからです。
print(north.loc[1])KeyError: 1.locはラベルを検索しますが、ここではラベル1が存在しません。もし「north支店の2番目の注文」という意味であれば、それは位置であり、位置の指定には.ilocを使います。
print(north.iloc[1])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: object位置による2番目の行はbagの注文です —— そしてそのNameは2であり、元の表でのラベルを示しています。これはまさに前章で学んだlocとilocの違いであり、フィルタリングを行ったあとでラベルと位置が一致しなくなるため、ここで問題になりやすいのです。
ラベルを保持するのは意図的な設計です。これにより、結果の中の任意の行を元の表まで遡って追跡できます。それが不要になった場合 —— たとえばフィルタリング後の表が最終レポートである場合などは、番号を振り直します。
print(north.reset_index(drop=True))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1003 2024-03-02 north bag accessories 2 850.0
2 1005 2024-03-03 north eraser stationery 30 8.0
3 1008 2024-03-04 north bottle accessories 4 120.0drop=Trueは古いラベルを破棄します。これを指定しないと、古いラベルはindexという新しい列として保持されますが、レポートでそれを必要とすることはめったにありません。
結果が空でもエラーではない
none = orders[orders["branch"] == "west"]
print(none)
print(none.shape)
print(none.empty)Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []
(0, 7)
Truewest支店は存在しないため、0行になります —— そしてエラーは出ません。これは正しい挙動ですが(0行も有効な結果です)、フィルタの綴りを間違えてもエラーが出ずに静かに失敗することを意味します。空の結果に対する合計は0になり、平均はnanになります。これは誰かが気づくまでレポートの奥深くまで伝播してしまう可能性があります。フィルタの結果を重要な処理に渡す場合は、result.emptyや行数を確認し、出力として明確に伝えるようにしてください。空の表を出力するよりも、"No orders from the west branch."(west支店の注文はありません)と表示するほうがはるかに親切です。
フィルタリングした行の変更:.loc[mask, column] = value
何かを変更するためにフィルタリングを行うこともあります。east支店で販売されたpenの価格が間違っていました。15.0ではなく13.5にする必要があります。 よくやってしまいがちなのは、フィルタリングしてから列を取り出すコードです —— 先ほど値を読み取ったときと同じように、角括弧を2つ並べます。
import pandas as pd
orders = pd.read_csv("orders.csv")
orders[orders["branch"] == "east"]["price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html#chained-assignment価格を見てみてください。何も変わっていません。そしてpandasは長文の警告を出しました。(ターミナルでは警告は標準エラー出力に出るため、代入の行が実行された瞬間にリストの上側にファイル名と行番号付きで表示されます。ここではそれらを除いた形で出力のあとに示しています。)これが連鎖代入(chained assignment)です —— orders[mask]のあとに["price"]という2段階のインデックス参照を行い、そのチェーンの末尾で代入を行っています。第1ステップのorders[mask]は、east支店の行を保持する新しい表を作成します。第2ステップはその新しい表の価格を設定します。そして、その新しい表はどこにも保持されていないため破棄されます。orders自体は一切変更されていません。
pandas 3では、これはCopy-on-Writeと呼ばれる保証された動作です。インデックス参照によって得られた表はすべて独自のコピーとして振る舞うため、そこに書き込んでも元の表に影響が及ぶことは決してありません。ChainedAssignmentErrorは例外ではなく警告であり、プログラムはそのまま動き続けます —— だからこそ危険なのです。この行を含むスクリプトは、古い価格のまま「正常に」終了してしまいます。
(古いチュートリアルを読むと、この状況がSettingWithCopyWarningとして説明され、「動作する場合と動作しない場合がある」と書かれているのを見かけるかもしれません。pandas 3では答えはシンプルです。絶対に動作しないので、決して書いてはいけません。)
修正方法は警告自身がアドバイスしている通りです。1回のインデックス参照ステップ.loc[rows, column]に対して代入を行います。
orders.loc[orders["branch"] == "east", "price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 13.5, 8.0, 850.0, 13.5, 120.0]ちょっと待ってください —— east支店の両方の注文、penとbottleの両方が変更されてしまいました。bottleの価格は間違っていませんでした。コードは指示通りのことを正確に行いましたが、コード内の質問が間違っていたのです。ここで計画のステップ1に戻ります。「east支店で販売されたpen」は2つの条件なのに、1つしか書かれていませんでした。新しいファイルからやり直して、両方の条件を記述しましょう。
import pandas as pd
orders = pd.read_csv("orders.csv")
fix = (orders["branch"] == "east") & (orders["product"] == "pen")
print(fix.sum())
orders.loc[fix, "price"] = 13.5
print(orders.loc[orders["branch"] == "east", ["product", "price"]])1
product price
3 bottle 120.0
6 pen 13.5ここでは2つの良い習慣が見られます。重要な処理に使われるためマスクに名前(fix)を付けていること、そして書き込む前にfix.sum()で何行が変更されようとしているかを確認していることです —— 想定通り1行です。上書きする前には、毎回必ず件数を数えてください。
読み取りにはdf[mask]["col"]で問題ありません。書き込みには、常にdf.loc[mask, "col"] = valueを使用してください。連鎖代入の形式は例外を発生させず、変更が静かに消えてしまいます。
フィルタリングした表を変数に保存して操作するのは問題ない
では、次のコードはどうでしょうか?
import pandas as pd
orders = pd.read_csv("orders.csv")
east = orders[orders["branch"] == "east"]
east["price"] = 0.0
print(east["price"].tolist())
print(orders["price"].tolist())[0.0, 0.0]
[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]警告は出ません。そしてこれで正しいのです。ここでは独立した別の表を作成する意図がありました。eastという名前を付け、それを変更してもeastだけが変更されます。ordersは影響を受けません。Copy-on-Writeのおかげで、この挙動は予測可能になっています —— フィルタリングによって得られた表は、常に元の表から独立しています。不具合になるのは常に連鎖代入の形式だけであり、中間の表に名前がなく、変更がその表とともに消えてしまう場合です。
完全な例
マネージャーから簡単なレポートの作成を依頼されました。report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check what arrived before asking it anything.
print("Rows, columns:", orders.shape)
print("Branches:", list(orders["branch"].unique()))
print()
# 1. The original question: north-branch orders of more than three items.
north_big = orders.loc[
(orders["branch"] == "north") & (orders["quantity"] > 3),
["order_id", "product", "quantity"],
]
print("North orders over 3 items:")
print(north_big.reset_index(drop=True))
print()
# 2. Counting with masks: no new table needed.
accessories = orders["category"] == "accessories"
print("Accessory orders :", accessories.sum())
print("Share of all orders:", accessories.mean())
print("Accessory items :", orders.loc[accessories, "quantity"].sum())
print()
# 3. Outside north, cheap items (under 100).
cheap_outside = orders[
(orders["branch"] != "north") & (orders["price"] < 100)
]
print("Cheap orders outside north:", cheap_outside["order_id"].tolist())
# 4. A branch we do not have: say so instead of printing an empty table.
west = orders[orders["branch"] == "west"]
if west.empty:
print("No orders from the west branch.")Rows, columns: (8, 7)
Branches: ['north', 'south', 'east']
North orders over 3 items:
order_id product quantity
0 1001 pen 12
1 1005 eraser 30
2 1008 bottle 4
Accessory orders : 4
Share of all orders: 0.5
Accessory items : 14
Cheap orders outside north: [1002, 1007]
No orders from the west branch.このように書かれている理由:
- まず確認し、それから問い合わせる。 フィルタをかける前に
shapeと支店の一覧を出力しています。もしファイル内でNorthが大文字で始まっていた場合、レポートが空の表を出力してしまう前に、支店の一覧の行でそれに気づくことができます。 - メインの質問には
.loc[mask, columns]を使用する。 答えとなる行と列を1箇所で決定し、これが途中経過ではなく最終レポートであるためreset_index(drop=True)で番号を振り直しています。 - 名前付きマスクを作成し、3回使用する。
accessoriesを一度計算したあと、件数の集計(sum)、割合の算出(mean)、列の選択に使い回しています。マスクに名前を付けることで、同じ条件を3回書いて微妙に食い違ってしまうミスを防ぐことができます。 - すべての条件を個別の括弧で囲む。 短い条件であっても括弧で囲んでおくことで、あとから3つ目の条件を追加したときに優先順位のエラーが発生するのを防ぎます。
- 結果が空の場合を明確に処理する。 0行も有効な結果であり、レポートではそれを言葉で伝えています。
問題が起きたとき
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). 2つのマスクの間でand、or、notを使用したか、マスク全体に対してifを使用した可能性があります。Pythonのキーワードは1つのTrue/Falseを必要としますが、マスクには行ごとに1つずつ値が存在します。フィルタの内側では&、|、~と書いてください。ifの内側では、何を意図しているかを判断して明示します:if mask.any():(「少なくとも1行がTrueか?」)またはif mask.all():(「すべての行がTrueか?」)。
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool](または'ror_'、[int64]) &や|の隣にある条件を囲む括弧が不足しています。Pythonが>よりも前に&を適用したため、数値と列の間でビット単位の「and」を計算しようとしました。それぞれの条件を個別の括弧で囲んでください:(a > 3) & (b < 100)。
フィルタがエラーなしで空の表を返す 入力した値と完全に一致する値が列内に存在していません。df["col"].unique()を出力してみてください。あるいは、各値が引用符付きで表示されるlist(df["col"].unique())を出力すると、'north 'のように末尾に空白があるかどうかが一目でわかります。df["col"].str.strip()でデータをクリーンアップするか、.str.contains(..., case=False)で柔軟に一致させてください。
フィルタリング直後にKeyError: 1(または別の数値)が発生する フィルタリングされた表がまだ0, 1, 2…というラベルを持っているかのように.loc[n]を使用しました。フィルタリング後の表は元のラベルを保持します。「n番目の行」を取得するには.iloc[n]を使用するか、古いラベルが不要であれば先にreset_index(drop=True)を実行してください。
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. df[mask]["col"] = valueと書きました。pandas 3ではこれでdfが変更されることは決してありません —— この警告は、その行が何も行わなかったことを伝えています。df.loc[mask, "col"] = valueと書いてください。
UserWarning: Boolean Series key will be reindexed to match DataFrame index. フィルタリング対象の表とは異なる表からマスクを作成しました —— 典型的な例は、表全体のordersから作成したマスクを、フィルタリング済みのnorthに対して使った場合です。pandasはラベルで照合し、見つからないラベルを暗黙のうちに破棄します。
import pandas as pd
orders = pd.read_csv("orders.csv")
north = orders[orders["branch"] == "north"]
big = orders["quantity"] > 5
print(north[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
UserWarning: Boolean Series key will be reindexed to match DataFrame index.(ターミナルでは、この警告は表の上に表示されます。)ここではラベルの並びが偶然合致していたため妥当な答えが得られましたが、それは運が良かっただけにすぎません。マスクはフィルタリング対象の表から作成してください:north[north["quantity"] > 5]。
ステップ 4 / 6 — 予想する
理解度をチェック
何が出力されますか?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
mask = orders["quantity"] > 5
print(mask.sum(), len(orders[mask]))- A5 5
- B4 8
- C4 4
- DTrue 4
これはnorth支店の3個を超える注文を残すことを意図したコードです。実行すると何が起こりますか?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
north_big = orders[orders["branch"] == "north" & orders["quantity"] > 3]- A`ValueError: The truth value of a Series is ambiguous`が発生する
- B空の表が返される
- C正常に動作し、注文1001、1005、1008が残る
- D`TypeError`が発生する:括弧がないため、`&`が`==`や`>`よりも先に適用される
orders自体のすべてのbagの価格を半額にする必要があります。それを実行する行はどれですか?
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Bbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Corders["price"][orders["product"] == "bag"] /= 2
- Dorders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
同じorders.csvを使用します。4つの質問に答えるfilters.pyを作成し、それぞれ短い見出しの下に出力してください。
- 10個以上の注文は何件ありましたか?表ではなく数値を出力してください。
- north支店以外のアクセサリー(accessories)の注文 ——
order_id、branch、product、priceの列のみを取り出し、0から番号を振り直してください。 - southまたはeast支店の注文で、価格が100未満のもの。支店の指定には
isinを使用してください。 - 本社が20個以上の文房具(stationery)の注文に対して10%の割引を適用することになりました。該当する行の
priceだけを現在の価格の90%に設定してください。ただし、最初に変更しようとしている行数を出力してください。そのあと、すべての文房具の注文についてorder_id、quantity、priceを出力してください。
コードを書く前に、各質問を紙の上で整理してください:どの列か、どのような比較か、何で結合するか、そしてファイルを目視してどの行が残るはずか。プログラムが正しければ、正確に次のように出力されます。
1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5そのあと、わざと1回ずつ壊してみてください。
- 質問3の括弧を外してみましょう。どのエラーが表示されますか?
- 質問4を
orders[mask]["price"] = ...と書いてみましょう。pandasは何を出力しますか?また、価格は変更されましたか? - 質問3で
"east"を"East"に変更してみましょう。何が起こりますか?また、それがなぜエラーよりも厄介なのでしょうか?
解答
まず計画を立てる。 各質問を分解します。
quantity >= 10、条件は1つ。目視:12、30、20なので3件。category == "accessories"かつbranch != "north"。目視:1004(east、bottle)と1006(south、bag)。branchがsouth/east かつprice < 100。目視:1002(notebook、60.0)と1007(pen、15.0)。category == "stationery"かつquantity >= 20。目視:1005(eraser、30)と1007(pen、20)なので2行。
質問4には注意が必要です。「20個以上」は>= 20なので、ちょうど20個の注文1007も含まれます。そして値を書き込むため、名前付きマスクと.loc[mask, "price"]を使用し、事前に件数を数えます。
filters.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("1. Orders of 10+ items:")
print((orders["quantity"] >= 10).sum())
print()
print("2. Accessory orders outside north:")
outside = orders.loc[
(orders["category"] == "accessories") & (orders["branch"] != "north"),
["order_id", "branch", "product", "price"],
]
print(outside.reset_index(drop=True))
print()
print("3. South or east, price under 100:")
cheap = orders[
(orders["branch"].isin(["south", "east"])) & (orders["price"] < 100)
]
print(cheap)
print()
print("4. Stationery discount:")
discount = (orders["category"] == "stationery") & (orders["quantity"] >= 20)
print("Rows to change:", discount.sum())
orders.loc[discount, "price"] = orders.loc[discount, "price"] * 0.9
print(orders.loc[orders["category"] == "stationery", ["order_id", "quantity", "price"]])1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5すべての答えが計画と一致しました。書き方の選択に関する補足:
- 質問1はマスクの
sum()を使用しており、len(orders[...])ではありません。どちらも3を返しますが、マスクを使うほうは直接「数える」意図を表し、表を作成しません。 - 質問2では「north支店以外」に
!=を使用しています。~(orders["branch"] == "north")も同じ意味になりますが、1つの値に対しては!=のほうが簡潔です。 - 質問3は元のラベル
1と6を保持しています。番号の振り直しが指示されていないためです。これらのラベルを見れば、ordersのどの行だったのかがわかります。 - 質問4は書き込む前に件数を数え、新しい値は右辺の同じ行から計算されています:
orders.loc[discount, "price"] * 0.9。両辺で同じマスクを使用しているため、各行は自分自身の価格の90%になります。8.0は7.2になり、15.0は13.5になります。penの注文1001(12個)やnotebook(5個)の価格はそのまま維持されます。
そして3つの破損実験について:
- 括弧を付けないと、質問3では
&が<より先に適用されるため、TypeError: Cannot perform 'rand_' …が発生します。 orders[discount]["price"] = …と書くと、ChainedAssignmentErrorの警告が出力されますがスクリプトはそのまま続行され、最終的な表には古い価格8.0と15.0が表示されたままになります。その行は何もしませんでした。"East"と書くと、質問3は静かにeast支店のpenを取りこぼし、2行ではなく1行だけを出力します。エラーも警告も出ず、一見正しそうに見える間違った答えが出るだけです。コードを実行する前に期待される行を計画として書き出しておくのはこのためです。この不具合に気づけるのは、答えが2行になるはずだと事前に知っている人間だけです。