সারি ফিল্টার করা — প্রতিটি সারিকে একই প্রশ্ন করা
যেসব সারি 'হ্যাঁ' বলে শুধু সেগুলো রাখা: বুলিয়ান মাস্ক, &, | এবং ~ দিয়ে শর্ত জোড়া লাগানো, isin, between ও .str মেথড, .loc দিয়ে ফিল্টার করা — এবং চেইন্ড অ্যাসাইনমেন্টের নীরব ফাঁদ এড়িয়ে সরাসরি সেই সারিগুলোর মান পরিবর্তন করা।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
দোকানের ম্যানেজার একটি মেসেজ পাঠালেন: "উত্তর শাখা (north branch) থেকে কোন কোন অর্ডারে তিনটির বেশি আইটেম কেনা হয়েছিল? আমি সেগুলো দেখতে চাই।"
গত অধ্যায়ের আটটি অর্ডারের ক্ষেত্রে আপনি চোখ বুলিয়েই উত্তর দিয়ে দিতে পারতেন। আঙুল দিয়ে branch কলাম বরাবর নিচে নামুন, প্রতিটি north এলে থামুন, আর পাশের quantity কলামে একবার চোখ রাখুন। তিনটি সারি পাওয়া গেল। কাজ শেষ।
এখন কল্পনা করুন আসল ফাইলের কথা — যেখানে বারোটি নয়, সারা বছরের বারো হাজার অর্ডার রয়েছে। তখন আর খালি চোখে দেখে কাজ হবে না। আর বেশিরভাগ পাইথন প্রোগ্রামারের মাথায় প্রথম যে বুদ্ধিটি আসে, তা হলো একটি লুপ (loop) চালানো:
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, এবং পুরো টেবিলটাকে এক এক সারি করে ভেঙে দেখা। আর এর ফলাফলটাও কিন্তু আর টেবিল রইল না — হয়ে গেল শুধু অর্ডার নম্বরের একটা সাধারণ লিস্ট। ম্যানেজারকে পণ্যের নাম আর দামও দেখাতে চাইলে আপনাকে আবার লুপ চালাতে হবে। বারো হাজার সারির ডেটাতে এটি অত্যন্ত ধীরগতিরও হবে, কারণ প্রতিটি orders.iloc[i] মাত্র দুটি মান পড়ার জন্য প্রতিবার একটি করে নতুন Series তৈরি করে।
পান্ডাসে এই প্রশ্নটা করার একেবারেই আলাদা একটি উপায় রয়েছে, আর কোনো DataFrame নিয়ে কাজ করার সময় আপনি সবচেয়ে বেশি এই কাজটিই করবেন: একসাথে প্রতিটি সারিকে একই 'হ্যাঁ' বা 'না' প্রশ্ন করা, আর যে সারিগুলো 'হ্যাঁ' বলেছে কেবল সেগুলোকে রেখে দেওয়া। এটাই হলো ফিল্টারিং (filtering), আর এই পুরো অধ্যায়টি ঠিক এই নিয়েই। ওপরের পুরো লুপটি নেমে আসে মাত্র এক লাইনে, ফলাফল সবসময় টেবিল আকারেই বজায় থাকে, এবং কোডটি পড়তে ঠিক ম্যানেজারের বলা বাক্যের মতোই শোনায়।
এই অধ্যায় শেষে আপনি পারবেন
- একটি প্রশ্নকে বুলিয়ান মাস্ক (boolean mask)-এ রূপান্তর করতে — অর্থাৎ প্রতি সারির জন্য একটি করে
True/Falseমানের কলাম তৈরি করতে — এবং সারিগুলো ফিল্টার করতে কিংবাmask.sum()ওmask.mean()দিয়ে সেগুলো গণনা করতে - একাধিক শর্তকে
&,|এবং~দিয়ে যুক্ত করতে, এবং কেন প্রতিটি শর্তে বন্ধনী দেওয়া বাধ্যতামূলক তা ব্যাখ্যা করতে - তালিকা, সীমা এবং লেখার প্যাটার্নের জন্য
isin,betweenও.strমেথডগুলো ব্যবহার করতে .loc[mask, columns]দিয়ে এক ধাপেই নির্দিষ্ট সারি ফিল্টার করতে ও নির্দিষ্ট কলাম বেছে নিতে.loc[mask, column] = valueব্যবহার করে ফিল্টার করা সারির মান নিরাপদে পরিবর্তন করতে, এবং অনিরাপদ উপায়ে কোড লিখলে পান্ডাস ৩-এ কেনChainedAssignmentErrorদেখা দেয় তা বুঝতে
আগে যা জানা লাগবে: কলাম আর সারি বেছে নেওয়া।
ফাইলটি আগে তৈরি করে নিন
এই অধ্যায়েও গত অধ্যায়ের orders.csv ফাইলটি ব্যবহার করা হয়েছে। ফাইলটি আপনার ফোল্ডারে না থাকলে ঠিক এই নয়টি লাইন দিয়ে আপনার প্রজেক্ট ফোল্ডারে এটি তৈরি করে নিন:
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কোড লেখার আগে যে প্রস্তুতি নেবেন
ফিল্টার হলো কোডের ভাষায় লেখা একটি প্রশ্ন। ফিল্টারিংয়ের বেশিরভাগ বাগ বা সমস্যা আসলে পান্ডাসের ত্রুটি নয় — বরং প্রশ্নটি ঠিকমতো নির্দিষ্ট না করা, অথবা যে কলাম নিয়ে কাজ করছেন তাতে আপনার ধারণার বাইরে অন্য কিছু থাকা। তাই ফিল্টারিংয়ের প্রথম লাইন কোড লেখার আগেই এই পাঁচটি কাজ সেরে নিন।
১. প্রশ্নটিকে প্রতিটি কলামের আলাদা আলাদা শর্ত হিসেবে সাজিয়ে নিন। ম্যানেজারের বাক্যটিকে ভেঙে ফেলুন:
- "উত্তর শাখা (north branch) থেকে অর্ডার" → কলাম
branch, তুলনা সমান কি না, মান"north" - "তিনটির বেশি আইটেম" → কলাম
quantity, তুলনা বৃহত্তর কি না (greater than), মান3 - "আমি সেগুলো দেখতে চাই" → কোনো পরীক্ষা নয়; পুরো সারিটাই দেখতে চান
এই তালিকাটি আপনাকে দুটি বিষয়ে স্পষ্ট সিদ্ধান্ত নিতে বাধ্য করে। "তিনটির বেশি" মানে > 3, >= 3 নয় — অর্থাৎ ঠিক তিনটি আইটেমের অর্ডার কিন্তু গণনা করা যাবে না। আর এই দুটি শর্ত যুক্ত হয়েছে and (এবং) দিয়ে: একটি অর্ডারকে এই দুটি পরীক্ষাতেই পাস করতে হবে। এই দুটি সিদ্ধান্তই হলো পুরো ফিল্টারের মূল কাঠামো; বাকিটা শুধুই কোডের বানান।
২. ইনপুট যাচাই করুন — আকার (shape), ডেটার ধরন (types), এবং যেসব মানের সাথে তুলনা করবেন সেগুলোর আসল চেহারা।
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 সংখ্যা হিসেবে তুলনা করবে। এটি যদি ভুলবশত str হিসেবে আসত — ধরা যাক কোনো সেলে কেউ "3 pcs" লিখে রেখেছিল — তবে তুলনা করার সময় TypeError দিয়ে কোড আটকে যেত; আর সেই তথ্যটি কাজের মাঝপথে নয়, শুরুতেই জানা দরকার। branch কলামটি str, তাই এটি টেক্সট হিসেবে তুলনা করবে, আর টেক্সটের তুলনা হুবহু হতে হয়: "north", "North" এবং "north " তিনটি সম্পূর্ণ ভিন্ন মান। তাই কলামে আসলেই কী কী মান আছে তা দেখে নিন:
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: strতিনটি শাখা, প্রতিটির বানান একক নিয়মে লেখা, কোনো বাড়তি স্পেস নেই। এখন আপনি নিশ্চিত যে কোডে "north" লিখলে তা ঠিকঠাক মিলবে।
৩. আউটপুট তৈরি করার আগেই তার একটি খসড়া অনুমান করে নিন। ছোট ফাইলে আগে হাতে-কলমে প্রশ্নটির উত্তর বের করুন। উত্তর শাখার অর্ডারগুলো হলো 1001 (১২টি আইটেম), 1003 (২টি), 1005 (৩০টি) এবং 1008 (৪টি)। তিনটির বেশি আইটেম রয়েছে: 1001, 1005, 1008-এ। সুতরাং প্রত্যাশিত ফলাফল হলো ৩টি সারি, সবকটি ৭টি কলাম, এবং কোন কোন অর্ডার নম্বর আসবে তা-ও জানা হয়ে গেল। কোড যদি পরে ৪টি সারি দেয়, বা ০টি সারি দেখায়, আপনি অন্ধের মতো বিশ্বাস না করে সাথে সাথেই বুঝবেন যে কোডে ভুল হয়েছে। বড় ফাইলে হয়তো সব সারি এভাবে হাতে গোনা সম্ভব নয়, তবে আপনি একটি যৌক্তিক অনুমান করতে পারেন: "উত্তর শাখায় প্রায় অর্ধেক অর্ডার আসে, তাই উত্তরটি অবশ্যই মোট সারির অর্ধেকের চেয়ে অনেক কম হবে।"
৪. শর্তের ধরন অনুযায়ী সঠিক টুলটি বেছে নিন।
- একটি একক তুলনা → একটি মাস্ক:
df["col"] > value - একাধিক শর্ত →
&(and),|(or),~(not) দিয়ে যুক্ত করা মাস্ক - "এই মানগুলোর যেকোনো একটি" →
df["col"].isin([...]) - "দুটি মানের মধ্যবর্তী" →
df["col"].between(low, high) - লেখার ভেতরে কোনো প্যাটার্ন খোঁজা →
df["col"].str.contains(...),.str.startswith(...) - নির্দিষ্ট সারি ফিল্টার এবং শুধু কয়েকটি কলাম নেওয়া →
df.loc[mask, ["col1", "col2"]] - ফিল্টার করা সারিগুলোর মান পরিবর্তন করা →
df.loc[mask, "col"] = new_value
ম্যানেজারের প্রশ্নে দুটি তুলনা and দিয়ে যুক্ত হয়েছে, তাই আমাদের লাগবে: দুটি মাস্ক এবং &।
৫. উত্তরটি কীভাবে যাচাই করবেন তা ঠিক করুন। ফিল্টার করার পর তিনটি সহজ পরীক্ষা প্রায় সব ভুল ধরে ফেলে: সারির সংখ্যা আপনার অনুমানের সাথে মিলল কি না; টিকে থাকা প্রতিটি সারিতে শর্তটি সত্যি সত্যি বহাল আছে কি না ((result["quantity"] > 3).all() অবশ্যই True দেবে); এবং আপনি যে সারির উপস্থিতি আশা করেছিলেন তা বাদ পড়েনি তো (ফলাফলে 1008 নম্বরটি আছে কি না দেখুন)।
এবার চলুন কোডে যাই — এক এক করে প্রতিটি ধারণা বোঝা যাক।
তুলনা করলে একটি মাস্ক পাওয়া যায়
একটি কলামের সাথে কোনো মানের তুলনা করলে পান্ডাস কেবল একটি একক 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), এবং প্রতি সারির জন্য একটি করে উত্তর রয়েছে: সারি 0-এ ১২টি আইটেম আছে, তাই True; সারি 1-এ ৫টি আছে, আর ৫ তো ৫-এর চেয়ে বড় নয়, তাই False।
পান্ডাস কেন লুপ চালানোর বদলে এভাবে কাজ করে? কারণ একটি কলাম মেমরিতে একটি একক ধরনের অবিচ্ছিন্ন সংখ্যার ব্লক হিসেবে জমা থাকে, আর পুরো ব্লকের সবকটি সংখ্যার সাথে 5-এর তুলনা করা একটি একক অপারেশন যা কম্পাইল করা অপ্টিমাইজড সি/সি++ কোডে চোখের পলকে সম্পন্ন হয়। আপনার লুপটি পাইথনকে প্রতিটি সারি আলাদা আলাদাভাবে ৮ বার দেখতে বাধ্য করেছিল — বা আসল ডেটায় ১২,০০০ বার। আর মাস্ক প্রশ্নটি করে মাত্র একবার।
সবগুলো তুলনামূলক অপারেটর ঠিক একইভাবে কাজ করে: ==, !=, <, <=, >, >=। এগুলো লেখার কলামেও একইভাবে চলে — orders["branch"] == "north" একটি মাস্ক তৈরি করে যার 0, 2, 4 এবং 7 নম্বর সারিতে থাকবে True।
এখনও কিন্তু কোনো ডেটা ফিল্টার করা হয়নি। মাস্কটি হলো কেবল উত্তরের তালিকা। পরবর্তী ধাপ হলো এই উত্তরগুলোকে কাজে লাগানো।
মাস্ক সারিগুলোকে আলাদা করে
মাস্কটিকে একটি স্কয়ার ব্র্যাকেটের (তৃতীয় বন্ধনী) ভেতরে রাখুন, আর পান্ডাস ঠিক সেই সারিগুলো রেখে দেবে যেখানে মাস্কের মান 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.0আটটি সারির মধ্যে চারটি সারি টিকে রইল। বাঁ পাশের ইনডেক্সগুলো খেয়াল করুন: 0, 3, 4, 6। টিকে থাকা সারিগুলো তাদের মূল লেবেল ধরে রাখে। ফিল্টারিং কিন্তু কোনো কিছুর নতুন নম্বর দেয় না — সারি 3 এখনও সারি 3-ই রয়ে গেছে, মূল টেবিলে এটি যে অর্ডার 1004 ছিল এখানেও ঠিক তাই। এই বিষয়টি অত্যন্ত গুরুত্বপূর্ণ, এবং আমরা একটু পরেই এতে ফিরে আসব।
পান্ডাস কীভাবে মাস্কটিকে টেবিলের সারির সাথে মেলায়? অবস্থানের ভিত্তিতে নয় — বরং ইনডেক্স লেবেলের ভিত্তিতে। মাস্কের লেবেল 0 সিদ্ধান্ত নেয় টেবিলের সারি 0-এর ভাগ্য, লেবেল 3 সিদ্ধান্ত নেয় সারি 3-এর ভাগ্য। এখানে দুটি ইনডেক্স হুবহু এক, কারণ মাস্কটি তৈরিই করা হয়েছিল সরাসরি orders থেকে, তাই বিষয়টি চোখে পড়ে না। এটি তখনই দৃশ্যমান হয় যখন আপনি অন্য কোনো টেবিল থেকে তৈরি করা মাস্ক ব্যবহার করতে যান — যা আলোচনা করা হয়েছে "কিছু ভাঙা অবস্থা ও তার সমাধান" অংশে।
সাধারণত কোডে আলাদা নাম না দিয়ে মাস্কটি সরাসরি ব্র্যাকেটের ভেতর এক লাইনে লেখা দেখতে পাবেন:
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 হওয়া সারিগুলোকে রেখে দেয়। শর্তটি বড় হলে বা একাধিকবার ব্যবহারের প্রয়োজন হলে মাস্কের একটি নাম দেওয়া (big = …) সুবিধাজনক; আর ছোট শর্তের জন্য সরাসরি এক লাইনে লেখাই যথেষ্ট।
ফিল্টার না করেই মাস্ক দিয়ে গণনা করা
অনেক সময় ম্যানেজারের পুরো সারিগুলোর দরকার হয় না, কেবল একটি সংখ্যার প্রয়োজন হয়: পাঁচটির বেশি আইটেম কেনা হয়েছে এমন অর্ডার কয়টি? এর জন্য ফিল্টার করে নতুন টেবিল বানানোর কোনো দরকার নেই। গণিতের হিসাবে True মানে 1 আর False মানে 0, সুতরাং:
print(big.sum())
print(big.mean())4
0.5sum() সবগুলো ১ যোগ করে: ৪টি অর্ডার। mean() মোট সারির সংখ্যা দিয়ে ভাগ করে গড় বের করে: ৮টির মধ্যে ৪টি হলো 0.5, অর্থাৎ মোট অর্ডারের অর্ধেক অর্ডারে পাঁচটির বেশি আইটেম ছিল। "কয়টি অর্ডার" জানতে চাইলে মাস্কের .sum() নিন; "শতকরা কত অংশ বা কোন ভগ্নাংশ" জানতে চাইলে মাস্কের .mean() নিন। দুটিই এক লাইনের কাজ এবং কোনোটিই নতুন কোনো টেবিল তৈরি করে মেমরি নষ্ট করে না।
আর যখন আপনি ফিল্টার করা সারিগুলোর কোনো কলামের মোট যোগফল বের করতে চান — যেমন সেই বড় অর্ডারগুলোতে মোট কতটি আইটেম বিক্রি হয়েছিল — তখন আগে ফিল্টার করুন, তারপর কলামটি বেছে নিন, এবং সবশেষে যোগ করুন:
print(orders[big]["quantity"].sum())69১২ + ৭ + ৩০ + ২০ = ৬৯। ফিল্টার করা টেবিলের কোনো কলাম এভাবে পড়া (read) সম্পূর্ণ নিরাপদ। কিন্তু এই পদ্ধতিতে ফিল্টার করা টেবিলে কোনো মান লেখা (write) বিপজ্জনক — যা আমরা এই অধ্যায়ের শেষ অংশে শিখব।
একাধিক শর্ত: &, |, ~
ম্যানেজারের প্রশ্নে দুটি শর্ত ছিল। পান্ডাসে মাস্কগুলোকে জোড়া লাগানোর জন্য তিনটি অপারেটর রয়েছে:
&মানে 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.0তিনটি সারি: 1001, 1005, 1008 — ঠিক যেমনটি আমরা পরিকল্পনার ধাপে অনুমান করেছিলাম। এবার প্রস্তুতি পর্বের ৫ নম্বর ধাপের পরীক্ষাগুলো চালিয়ে দেখে নেওয়া যাক:
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
Trueসারির সংখ্যা মিলে গেছে, এবং টিকে থাকা প্রতিটি সারিতে দুটি শর্তই সত্য। .all() মেথডটি প্রশ্ন করে "এই মাস্কের প্রতিটি মানই কি True?" — কোনো ফিল্টার আপনার মনের মতো কাজ করেছে কি না তা নিশ্চিত করার এটি সবচেয়ে দ্রুত ও নির্ভরযোগ্য উপায়।
এবার একটি or (বা)-এর প্রশ্ন দেখা যাক: পূর্ব শাখার (east branch) সব অর্ডার, অথবা যেকোনো শাখার ৮৫০ বা তার বেশি দামের অর্ডার।
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.0চারটি সারি: পূর্ব শাখার দুটি অর্ডার, এবং উত্তর ও দক্ষিণ শাখার দুটি ব্যাগের অর্ডার। যদি কোনো সারি উভয় শর্তই পূরণ করত (এখানে যদিও নেই), তবে সেটি কিন্তু একবারই আসত, দুইবার নয় — কারণ ফিল্টার কোনো সারিকে শুধুমাত্র রাখে অথবা বাদ দেয়।
একটি 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 বানিয়ে দেয় এবং বিপরীতটাও করে। একটি একক সমতার ক্ষেত্রে আপনি != ব্যবহার করতে পারতেন, যা আরও সহজ; তবে যখন আপনি একটি বড় শর্ত বা কোনো isin-কে উল্টে দিতে চাইবেন, তখন ~-এর প্রয়োজনীয়তা বোঝা যায় (একটু পরেই আমরা তা দেখব)।
কেন বন্ধনী দেওয়া বাধ্যতামূলক
ওপরের প্রতিটি শর্তকে আলাদা বন্ধনীর ( ... ) মধ্যে রাখা হয়েছে। এটি কোনো সৌন্দর্য বা কোডিং স্টাইল নয়। বন্ধনী তুলে দিলে কোড সরাসরি ভেঙে যাবে:
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]ওপরে যা দেখছেন তা হলো ট্রেসব্যাকের শেষ লাইন, যেখানে এররটির নাম বলা হয়েছে; এর আগের লাইনগুলোতে শুধু থাকে এটি কোথায় ঘটেছে। এই ত্রুটির আসল কারণ হলো অপারেটরের অগ্রাধিকার (operator precedence) — অর্থাৎ পাইথন কোন অপারেটরটি আগে হিসাব করে। পাইথনে &-এর অগ্রাধিকার > এবং <-এর চেয়ে বেশি। পান্ডাস তৈরির অনেক আগেই পাইথনে এই নিয়ম করা হয়েছিল, কারণ & মূলত পূর্ণসংখ্যার বিটওয়াইজ "and"-এর জন্য তৈরি হয়েছিল, যা গাণিতিক তুলনার আগেই ঘটা উচিত। ফলে পাইথন লাইনটিকে এভাবে পড়ে:
orders["quantity"] > (3 & orders["price"]) < 100এটি প্রথমে 3 & orders["price"] হিসাব করার চেষ্টা করে — অর্থাৎ সংখ্যা ৩ এবং দশমিক সংখ্যার একটি কলামের মধ্যে বিটওয়াইজ "and" অপারেশন — যা একেবারেই অসম্ভব এবং ঠিক এই কারণেই ত্রুটিটি দেখা দেয়। ত্রুটি বার্তায় rand_ (ডান পাশের 'and') এবং অ্যারের কথা উল্লেখ থাকে, তবে এর মূল কারণ একটাই: এক জোড়া বন্ধনী বাদ পড়া।
ত্রুটি বার্তা আসা আসলে সৌভাগ্যের ব্যাপার। কিন্তু প্রথম শর্তে বন্ধনী দিলেন আর দ্বিতীয়টিতে দিতে ভুলে গেলেন, তখন কোনো ত্রুটিই আসবে না — আর সেটাই আসল বিপদ:
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
4প্রথম লাইনটি পাইথনের কাছে পড়া হয় ((quantity > 3) & price) < 100 হিসেবে। & অপারেটরটি একটি বুলিয়ান মাস্কের সাথে দামের কলামটি গুলিয়ে ফেলে, এবং ঘটনাচক্রে প্রতিটি মানই ১০০-এর নিচে হওয়ায় এই "ফিল্টার" আটটি সারির আটটিই রেখে দেয়! অথচ সঠিক ফিল্টারে পাওয়ার কথা ছিল চারটি সারি। একই কলাম, একই সংখ্যা, শুধু এক জোড়া বন্ধনী বাদ পড়েছে — আর ভুল কোডটি কোনো ধরনের শব্দ বা ত্রুটি ছাড়াই ভুল উত্তর দিয়ে দিল। প্রতিটি শর্তকে সবসময় নিজের বন্ধনীতে মুড়ে রাখুন। তাহলে ত্রুটি বা এই ধরনের নীরব ভুল কোনোটিই ঘটতে পারবে না।
and ব্যবহার করলে কী সমস্যা?
অনেকেরই স্বাভাবিক প্রবণতা থাকে ইংরেজি শব্দ and লিখে ফেলা:
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().পাইথনের and, or এবং not কিওয়ার্ডগুলোর দুই পাশে একটি একক True বা False মান লাগে — এগুলো তৈরিই করা হয়েছিল সাধারণ if শর্তের জন্য। and ব্যবহার করলে পাইথন প্রথম মাস্কটিকে জিজ্ঞেস করে "তুমি কি সত্য?", কিন্তু আটটি উত্তরের একটি কলাম তো একক কোনো উত্তর দিতে পারে না। এর যেকোনো একটি মান True হলেই কি পুরো কলামকে সত্য ধরা হবে? নাকি সবগুলো সত্য হতে হবে? পান্ডাস নিজে থেকে কোনো অনুমান করতে নারাজ, এবং ত্রুটি বার্তায় সিদ্ধান্ত নেওয়ার উপায়গুলো উল্লেখ করে দেয় (a.any(), a.all())। কিন্তু আপনি তো কোনো একক সিদ্ধান্ত চাননি; আপনি চেয়েছিলেন সারি-ধরে-ধরে 'and'। আর সেটাই হলো &। এই সহজ নিয়মটি মনে রাখবেন: ফিল্টারের ভেতরে and/or/not-এর জায়গায় সবসময় বসবে &/|/~।
একাধিক মানের যেকোনো একটি: isin
দক্ষিণ বা পূর্ব শাখার অর্ডারগুলো। আপনি | দিয়ে দুটি সমতা যুক্ত করে লিখতে পারেন। কিন্তু পাঁচটি শাখা থাকলে পাঁচটি তুলনা লিখতে হবে। 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আর ঠিক এই জায়গাতেই ~-এর চমৎকার ব্যবহার দেখা যায় — উত্তর ও পূর্ব ছাড়া বাকি সব শাখা বোঝাতে isin-এর সামনে একটি ~ বসিয়ে দিলেই হয়ে যায়:
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]দুটি অর্ডার, দুটিই দক্ষিণ শাখার। লক্ষ্য করুন ~orders["branch"].isin([...])-এর চারপাশে আলাদা কোনো বন্ধনীর দরকার পড়েনি: কারণ মেথড কলের ফলাফলের উপর সরাসরি ~ কাজ করে এবং এখানে কোনো & বা |-এর সাথে অগ্রাধিকারের লড়াই নেই। তবে আপনি এটিকে অন্য কোনো শর্তের সাথে যুক্ত করার সাথে সাথেই বন্ধনী ফেরত আনতে হবে: (~orders["branch"].isin([...])) & (orders["quantity"] > 3)।
একটি নির্দিষ্ট পরিসীমা: between
যেসব অর্ডারের দাম ১৫ থেকে ১২০-এর মধ্যে। এটি দুটি তুলনা, >= 15 এবং <= 120। between এক শব্দেই এটি প্রকাশ করে:
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 ডিফল্টভাবে উভয় প্রান্তের মানকেই অন্তর্ভুক্ত করে — অর্থাৎ ঠিক ১৫.০ দামের কলম এবং ঠিক ১২০.০ দামের বোতল দুটিই ফলাফলে রয়েছে। মানুষ সাধারণত "১৫ থেকে ১২০" বলতে যেভাবে বোঝে এটি তার সাথে মিলে যায়, তবে প্রশ্নের সাথে এটি মিলিয়ে নেওয়া জরুরি। প্রশ্নে যদি প্রান্তের মান বাদ দিয়ে কঠোরভাবে মধ্যবর্তী মান চাওয়া হয়, তবে তা স্পষ্ট বলে দিন:
print(orders["price"].between(15, 120, inclusive="neither").sum())1কেবল ৬০.০ দামের নোটবুকটিই কঠোরভাবে মধ্যবর্তী। কোনো এক পাশ খোলা রাখার জন্য inclusive-এ "left" বা "right" ব্যবহার করা যায় — যা সাধারণত টাকার স্ল্যাব বা ব্যান্ডের ক্ষেত্রে প্রয়োজন হয় (যেমন ০–১০০, ১০০–২০০), যাতে কোনো সীমানার মান দুটি ব্যান্ডের কোনোটিতেই দ্বিগুণ গণনা না হয়।
লেখার প্যাটার্ন: .str মেথডসমূহ
টেক্সটের উপর সাধারণ সমতা হুবহু মিল খোঁজে। আর .str অ্যাক্সেসরটি টেক্সট কলামে পাইথনের চেনা স্ট্রিং মেথডগুলো ব্যবহার করার সুযোগ দেয়, যা প্রতিটি সারির উপর প্রযুক্ত হয় এবং প্রতিটির জন্য একটি মাস্ক ফেরত দেয়।
যেসব পণ্যের নাম "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 বড় বা ছোট হাতের অক্ষরের ভেদাভেদ ভুলে যায়। এটি না দিলে .str.contains("O") কিছুই খুঁজে পেত না।
সরাসরি সমতায় (==) কিন্তু এমন কোনো অপশন থাকে না। আর ফিল্টারিংয়ে "আমার কোড কোনো সারি দেখাচ্ছে না কেন" জাতীয় সবচেয়ে সাধারণ বাগটি ঘটে ঠিক এখানেই:
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []কোনো ত্রুটি নেই, আবার কোনো সারিও নেই। টেবিলটিও ভুল নয়, পান্ডাসও ভুল নয় — ডেটাতে বড় হাতের অক্ষরে লেখা "North" নামের কোনো শাখাই নেই। এই কারণেই পরিকল্পনার ২ নম্বর ধাপে আমরা unique() প্রিন্ট করে দেখে নিয়েছিলাম: কোডে যেসব মানের সাথে তুলনা করবেন তা স্মৃতি থেকে টাইপ না করে কলামে আসলেই কী রয়েছে সেখান থেকে হুবহু কপি করা উচিত।
টেক্সট তারিখের তুলনা সঠিকভাবে কাজ করে — যখন তা ISO ফরম্যাটে থাকে
date কলামটি str হিসেবে লোড হয়েছে। আপনি কি >= দিয়ে এটি ফিল্টার করতে পারবেন? ৩ মার্চ বা তার পরের সব অর্ডার:
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এটি কাজ করেছে, তবে কারণটি বোঝা জরুরি। এগুলো সাধারণ স্ট্রিং, তাই >= এদের টেক্সট হিসেবে তুলনা করে — অক্ষর ধরে ধরে, যেভাবে ডিকশনারিতে শব্দ সাজানো থাকে। এটি সঠিক উত্তর দিতে পেরেছে কেবল এই কারণে যে তারিখগুলো বছর-মাস-দিন আকারে শূন্য যোগ করে লেখা হয়েছে (2024-03-03), ফলে টেক্সটের ক্রম কাকতালীয়ভাবে সময়ের ক্রমের সাথে হুবহু মিলে গেছে। কিন্তু তারিখ যদি 3/3/2024 আকারে লেখা থাকত, তবে ভুল উত্তর আসত — কারণ "1" অক্ষরটি "9"-এর আগে আসায় "10/1/2024" টেক্সটটি "9/1/2024"-এর চেয়ে ছোট হিসেবে বিবেচিত হতো। তাই সাধারণ ফিল্টারের বাইরে যেকোনো জটিল কাজের জন্য pd.to_datetime দিয়ে আসল তারিখে রূপান্তর করে নেওয়া উচিত, যা নতুন কলাম তৈরির অধ্যায়ে বিস্তারিত রয়েছে।
সারি এবং কলাম একসাথে: .loc[mask, columns]
এতক্ষণ পর্যন্ত সবকটি ফিল্টার টেবিলের প্রতিটি কলামকেই রেখে দিচ্ছিল। কিন্তু ম্যানেজার উত্তর শাখার অর্ডার চাইলেও রিপোর্টে হয়তো শুধু পণ্যের নাম আর পরিমাণ দরকার। গত অধ্যায়ে আপনি লেবেল দিয়ে .loc[rows, columns] ব্যবহার করা শিখেছিলেন। এই সারির জায়গায় আপনি চাইলে একটি মাস্ক বসিয়ে দিতে পারেন:
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 4এক ধাপ, এক অবজেক্ট, এবং আপনার কোড যে পড়বে সে এক জায়গাতেই পুরো প্রশ্নটি দেখতে পাবে — কোন কোন সারি, আর কোন কোন কলাম। কলামের জায়গায় লিস্টের বদলে একক নাম দিয়ে .loc[mask, "quantity"] লিখলে একটি Series পাওয়া যায়, যা সরাসরি গণিতের জন্য প্রস্তুত:
print(orders.loc[orders["branch"] == "north", "quantity"].sum())48১২ + ২ + ৩০ + ৪ = উত্তর শাখায় বিক্রি হওয়া মোট ৪৮টি পণ্য।
query(): ফিল্টারকে সাধারণ বাক্যের মতো লেখা
পান্ডাসে ফিল্টার লেখার আরেকটি বিকল্প বাক্যরীতি রয়েছে, যার নাম 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হুবহু একই তিনটি সারি। স্ট্রিংয়ের ভেতরে সরাসরি কলামের নাম লেখা যায় এবং এখানে সাধারণ ইংরেজি and/or/not ব্যবহার করা যায়, কারণ query স্ট্রিংটিকে পাইথনের অপারেটরের হাতে না দিয়ে নিজেই বিশ্লেষণ করে। কোনো পাইথন ভেরিয়েবল ব্যবহার করতে চাইলে তার নামের আগে @ বসাতে হয়:
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]বড় ও জটিল শর্তের ক্ষেত্রে query() বেশ সুন্দর ও পাঠযোগ্য দেখায়। তবে এর কিছু সীমাবদ্ধতাও আছে: স্ট্রিংয়ের ভেতরে কোনো টাইপো থাকলে তা কোড চালানোর আগ পর্যন্ত ধরা পড়ে না, আপনার কোড এডিটর এর ভেতরে অটো-কমপ্লিশনে সাহায্য করতে পারে না, এবং কলামের নামে স্পেস থাকলে ব্যাকটিক ব্যবহার করতে হয়। এই কোর্সে আমরা মূলত বুলিয়ান মাস্ককেই প্রধান হাতিয়ার হিসেবে ব্যবহার করছি, কারণ পান্ডাসের বাকি সবকিছু — .loc, নতুন মান নির্ধারণ, গণনা — এই মাস্কের ওপর ভিত্তি করেই গড়ে উঠেছে। তবে query() জেনে রাখা দরকার যাতে অন্যের লেখা কোড সহজে বুঝতে পারেন।
ফিল্টার করা টেবিল মূল লেবেলগুলো ধরে রাখে
চলুন ইনডেক্সের প্রসঙ্গে ফিরে আসা যাক। উত্তর শাখার অর্ডারগুলো একটি ভেরিয়েবলে সেভ করে তার ইনডেক্স দেখা যাক:
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]ইনডেক্স লেবেলগুলো হলো 0, 2, 4, 7। তাহলে north.loc[1] লিখলে কী হবে? এই টেবিলে 1 নামের কোনো লেবেলই তো নেই — কারণ অর্ডার 1002 ছিল দক্ষিণ শাখার এবং ফিল্টারে তা বাদ পড়ে গেছে:
print(north.loc[1])KeyError: 1.loc খোঁজে লেবেল, আর লেবেল 1 এখানে অনুপস্থিত। আপনি যদি বোঝাতে চেয়ে থাকেন "উত্তর শাখার দ্বিতীয় অর্ডারটি দাও", তবে সেটি হলো একটি অবস্থান (position), আর অবস্থানের জন্য রয়েছে .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অবস্থানের বিচারে দ্বিতীয় সারিটি হলো ব্যাগের অর্ডার — এবং লক্ষ্য করুন এর 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)
Trueপশ্চিমে কোনো শাখা নেই, তাই কোনো সারি নেই — এবং কোনো ত্রুটিও নেই। পান্ডাসের দিক থেকে এটি সম্পূর্ণ সঠিক আচরণ (শূন্য তো একটি বৈধ উত্তর হতেই পারে), তবে এর মানে হলো ফিল্টারে ভুল বানান লিখলে কোনো সতর্কতা ছাড়াই কোড পার হয়ে যায়। একটি খালি ফলাফলের কলাম যোগ করলে যোগফল আসে 0 এবং গড় আসে nan, যা নজরে পড়ার আগেই রিপোর্টের গভীরে ঢুকে যেতে পারে। যখন কোনো ফিল্টারের ফলাফলের ওপর গুরুত্বপূর্ণ কিছু নির্ভর করে, তখন result.empty বা সারির সংখ্যা পরীক্ষা করে দেখুন, এবং আউটপুটে তা স্পষ্ট ভাষায় বলে দিন: একটি ফাঁকা টেবিল দেখানোর চেয়ে "No orders from the west branch." লিখে দেওয়া অনেক বেশি পেশাদার।
ফিল্টার করা সারির মান পরিবর্তন: .loc[mask, column] = value
অনেক সময় আমরা কিছু মান পরিবর্তন করার উদ্দেশ্যে ফিল্টার করি। পূর্ব শাখায় বিক্রি হওয়া কলমগুলোর দামে ভুল ছিল; এগুলোর দাম ১৫.০ নয়, ১৩.৫ হওয়া উচিত। অনেকের মনে যে কোডটি লেখার ইচ্ছা জাগে তা হলো আগে ফিল্টার করা এবং তারপর কলামটি বেছে নেওয়া — অর্থাৎ পরপর দুটি ব্র্যাকেট, ঠিক যেভাবে আমরা শুরুতে মান পড়ার কাজ করেছিলাম:
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দামের লিস্টটি দেখুন: কোনো কিছুই পরিবর্তিত হয়নি! আর পান্ডাস বিশদভাবে সতর্কবার্তা দিল। (টার্মিনালে এই ওয়ার্নিংটি stderr-এ যায়, তাই এটি তালিকার ওপরে এবং ফাইলের নাম ও লাইন নম্বরসহ দেখা যায়। এখানে আউটপুটের সুবিধার্থে তা নিচে দেখানো হলো।) একে বলে চেইন্ড অ্যাসাইনমেন্ট (chained assignment) — অর্থাৎ পরপর দুটি ইনডেক্সিং ধাপ, প্রথমে orders[mask] এবং তারপর ["price"], আর চেইনের শেষে অ্যাসাইনমেন্ট চিহ্ন =। প্রথম ধাপটি, orders[mask], পূর্ব শাখার সারিগুলো নিয়ে একটি নতুন অস্থায়ী টেবিল তৈরি করে। দ্বিতীয় ধাপটি সেই নতুন অস্থায়ী টেবিলে নতুন দাম বসায়। এরপর সেই নতুন টেবিলটি মেমরি থেকে হারিয়ে যায়, কারণ কোনো ভেরিয়েবলে সেটিকে ধরে রাখা হয়নি। মূল orders টেবিলটি অপরিবর্তিতই থেকে যায়।
পান্ডাস ৩-এ এই নিয়মটি সুনির্দিষ্ট ও নিশ্চিত, যার নাম কপি-অন-রাইট (Copy-on-Write): ইনডেক্সিং করে পাওয়া যেকোনো টেবিল নিজস্ব একটি স্বাধীন কপি হিসেবে কাজ করে, তাই এতে কোনো কিছু লিখলে তা কখনোই মূল টেবিলে প্রতিফলিত হয় না। এই ChainedAssignmentError হলো একটি সতর্কবার্তা, কোনো এক্সেপশন নয় — ফলে প্রোগ্রামটি বন্ধ না হয়ে স্বাভাবিকভাবেই চলতে থাকে — আর ঠিক এই কারণেই এটি এত বিপজ্জনক: এই লাইনটি থাকা একটি স্ক্রিপ্ট পুরোনো দাম রেখেই "সফলভাবে" রান শেষ করবে।
(পুরোনো টিউটোরিয়ালগুলোতে এই পরিস্থিতিকে SettingWithCopyWarning দিয়ে বোঝানো হতো এবং বলা হতো এটি "কাজ করতেও পারে আবার নাও করতে পারে"। পান্ডাস ৩-এ উত্তর একেবারেই পরিষ্কার: এটি কখনোই কাজ করে না, তাই কখনোই এভাবে লিখবেন না।)
এর সমাধান সতর্কবার্তার নিজের পরামর্শেই রয়েছে: একক ইনডেক্সিং ধাপ, .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]একটু দাঁড়ান — এটি তো পূর্ব শাখার উভয় অর্ডারের দামই বদলে দিয়েছে, কলমের দামও বদলেছে আবার বোতলের দামও! বোতলের দামে তো কোনো ভুল ছিল না। কোডটি কিন্তু যা বলা হয়েছিল হুবহু তাই করেছে; আসল গলদ ছিল কোডের শর্তে। এবার পরিকল্পনার ১ নম্বর ধাপটি মনে করুন: "পূর্ব শাখায় বিক্রি হওয়া কলম" আসলে দুটি শর্ত, আর আমরা লিখেছিলাম মাত্র একটি। নতুন করে ফাইলটি লোড করে দুটি শর্তই একসাথে লিখুন:
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এখানে দুটি চমৎকার অভ্যাস দেখা যাচ্ছে। মাস্কটির একটি স্পষ্ট নাম দেওয়া হয়েছে (fix) কারণ এটি একটি গুরুত্বপূর্ণ কাজে ব্যবহৃত হচ্ছে; এবং মান পরিবর্তনের আগেই fix.sum() দিয়ে দেখে নেওয়া হয়েছে কয়টি সারি বদলাতে যাচ্ছে — ঠিক ১টি, যেমনটা আমরা আশা করেছিলাম। কোনো কিছু ওভাররাইট করার আগে সবসময় সারি গুনে নিন।
পড়ার সময়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 অপরিবর্তিত থাকে। কপি-অন-রাইট এই আচরণকে অত্যন্ত নির্ভরযোগ্য ও অনুমানযোগ্য করে তুলেছে — ফিল্টার করে পাওয়া টেবিল সবসময় মূল টেবিল থেকে স্বাধীন। ভুলটি শুধু তখনই হয় যখন চেইন্ড পদ্ধতিতে নামহীন কোনো অস্থায়ী টেবিলে মান লিখে তা হারিয়ে ফেলা হয়।
একটা সম্পূর্ণ উদাহরণ
ম্যানেজার এবার একটি সংক্ষিপ্ত রিপোর্ট দেখতে চেয়েছেন। 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]। উত্তরের জন্য প্রয়োজনীয় সারি ও কলাম এক জায়গাতেই নির্বাচন করা হয়েছে, এবং এটি যেহেতু চূড়ান্ত রিপোর্ট তাইreset_index(drop=True)দিয়ে ফলাফলকে নতুন নম্বর দেওয়া হয়েছে। - একটি নামযুক্ত মাস্ক, যা তিনবার ব্যবহৃত হয়েছে।
accessoriesমাস্কটি একবার তৈরি করে তা দিয়ে গণনা করা হয়েছে (sum), অনুপাত বের করা হয়েছে (mean), এবং একটি কলাম যোগ করা হয়েছে। নামযুক্ত মাস্ক ব্যবহারের সুবিধা হলো একই শর্ত বারবার লিখে ভুল হওয়ার কোনো সুযোগ থাকে না। - প্রতিটি শর্ত নিজস্ব বন্ধনীতে আবদ্ধ, এমনকি ছোট শর্তগুলোর ক্ষেত্রেও, যাতে ভবিষ্যতে কোনো তৃতীয় শর্ত যোগ করলেও অগ্রাধিকারের ভুল না হয়।
- খালি ফলাফল সুন্দরভাবে সামলানো হয়েছে। কোনো সারি না পাওয়াও একটি সঠিক উত্তর হতে পারে, এবং রিপোর্টে তা স্পষ্ট ভাষায় জানানো হয়েছে।
কিছু ভাঙা অবস্থা ও তার সমাধান
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). দুটি মাস্কের মাঝে and, or বা not ব্যবহার করেছেন — অথবা পুরো একটি মাস্কের ওপর if চালিয়েছেন। পাইথনের এই কিওয়ার্ডগুলোর একটি একক True/False দরকার হয়, অথচ মাস্কে প্রতি সারির জন্য একটি করে মান থাকে। ফিল্টারের ভেতরে লিখুন &, |, ~। আর কোনো if-এর ভেতরে স্পষ্ট করে বলুন আপনি কী চান: "অন্তত একটি সারি কি সত্য?" বোঝাতে if mask.any():, অথবা "সবগুলো সারিই কি সত্য?" বোঝাতে if mask.all():।
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool] (বা 'ror_', কিংবা [int64]) & বা |-এর পাশের শর্তে বন্ধনী বাদ পড়েছে। পাইথন তুলনামূলক অপারেটর >-এর আগেই & প্রয়োগ করায় একটি সংখ্যা ও একটি কলামের মধ্যে বিটওয়াইজ "and" করার চেষ্টা করেছে। প্রতিটি শর্তকে নিজস্ব বন্ধনীতে মুড়ে নিন: (a > 3) & (b < 100)।
ফিল্টারে কোনো ত্রুটি নেই, কিন্তু খালি টেবিল রিটার্ন করছে আপনি কলামের যে মানটি খুঁজছেন তা ডেটাতে হুবহু ওই বানানে নেই। df["col"].unique() প্রিন্ট করুন — অথবা আরও ভালো হয় list(df["col"].unique()) প্রিন্ট করলে, যা প্রতিটি মানকে কোটেশনের মধ্যে দেখায়, ফলে 'north '-এর পেছনের অতিরিক্ত স্পেসটি স্পষ্ট ধরা পড়ে। df["col"].str.strip() দিয়ে ডেটা পরিষ্কার করুন, অথবা .str.contains(..., case=False) দিয়ে শিথিলভাবে খুঁজুন।
ফিল্টার করার ঠিক পরেই KeyError: 1 (বা অন্য কোনো সংখ্যা) ফিল্টার করা টেবিলে আপনি এমনভাবে .loc[n] চালিয়েছেন যেন এর লেবেলগুলো এখনও 0, 1, 2… আছে। অথচ ফিল্টারের পরেও মূল লেবেলগুলোই বহাল থাকে। "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। পান্ডাস ৩-এ এটি কখনোই df-কে পরিবর্তন করে না — এই সতর্কবার্তা আপনাকে জানাচ্ছে যে লাইনটি কোনো কাজ করেনি। লিখুন df.loc[mask, "col"] = value।
UserWarning: Boolean Series key will be reindexed to match DataFrame index. যে টেবিলটি ফিল্টার করছেন মাস্কটি তার বদলে অন্য কোনো টেবিল থেকে তৈরি করা হয়েছে — যেমন মূল orders থেকে তৈরি মাস্ক দিয়ে ফিল্টার করা north টেবিলকে ফিল্টার করা। পান্ডাস লেবেল মিলিয়ে সারিগুলো সাজায় এবং যে লেবেলগুলো পায় না সেগুলো নীরবে বাদ দেয়:
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]।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
আউটপুটে কী প্রিন্ট হবে?
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]))- A4 8
- B5 5
- C4 4
- DTrue 4
এর উদ্দেশ্য ছিল north শাখার তিনটির বেশি আইটেমের অর্ডারগুলো রাখা। কোডটি চালালে কী ঘটবে?
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এটি সঠিকভাবে কাজ করবে এবং ১০০১, ১০০৫ ও ১০০৮ নম্বর অর্ডারগুলো রাখবে
- Bএটি একটি খালি টেবিল রিটার্ন করবে
- Cএকটি `TypeError`: বন্ধনী ছাড়া `&` অপারেটর `==` এবং `>`-এর আগেই কার্যকর হয়ে যায়
- Dএকটি `ValueError: The truth value of a Series is ambiguous`
আপনাকে orders টেবিলের ভেতরের প্রতিটি bag-এর দাম অর্ধেক করতে হবে। কোন লাইনটি এটি সঠিকভাবে করবে?
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Borders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
- Cbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Dorders["price"][orders["product"] == "bag"] /= 2
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
একই orders.csv ব্যবহার করুন। filters.py নামে একটি স্ক্রিপ্ট লিখুন যা চারটি প্রশ্নের উত্তর দেবে এবং প্রতিটির ওপরে একটি করে ছোট হেডিং প্রিন্ট করবে:
- কতটি অর্ডারে ১০টি বা তার বেশি আইটেম ছিল? কোনো টেবিল নয়, শুধু একটি সংখ্যা প্রিন্ট করুন।
- উত্তর শাখা ব্যতীত অন্য শাখার সব এক্সেসরিজ অর্ডার — শুধু
order_id,branch,productএবংpriceথাকবে, এবং ইনডেক্স ০ থেকে নতুন করে শুরু হবে। - দক্ষিণ অথবা পূর্ব শাখার যেসব অর্ডারের দাম ১০০-এর নিচে। শাখাগুলোর জন্য
isinব্যবহার করুন। - প্রধান কার্যালয় ২০টি বা তার বেশি আইটেমের প্রতিটি স্টেশনারি অর্ডারে ১০% ছাড় দিচ্ছে। ঠিক সেই সারিগুলোর
priceবর্তমান মূল্যের ৯০%-এ নির্ধারণ করুন, তবে প্রথমে প্রিন্ট করুন কতটি সারি আপনি পরিবর্তন করতে যাচ্ছেন। তারপর সব স্টেশনারি অর্ডারের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তারপর ইচ্ছাকৃতভাবে তিনটি ভুল করে দেখুন, প্রতিটির ফলাফল যাচাই করুন:
- ৩ নম্বর প্রশ্নে বন্ধনীগুলো তুলে দিন। কোন এররটি দেখা যাচ্ছে?
- ৪ নম্বর প্রশ্নে
orders[mask]["price"] = ...লিখে দেখুন। পান্ডাস কী প্রিন্ট করে, এবং কোনো দাম কি আসলেই বদলেছে? - ৩ নম্বর প্রশ্নে
"east"-এর জায়গায়"East"লিখে দেখুন। কী ঘটে, এবং কেন এটি কোনো সাধারণ এররের চেয়েও বেশি বিপজ্জনক?
সমাধান
আগে পরিকল্পনা করুন। প্রতিটি প্রশ্ন আলাদা করে দেখা যাক:
quantity >= 10, একটি একক শর্ত। চোখে দেখে: ১২, ৩০ এবং ২০, অর্থাৎ ৩।category == "accessories"এবংbranch != "north"। চোখে দেখে: ১০০৪ (পূর্ব, বোতল) এবং ১০০৬ (দক্ষিণ, ব্যাগ)।branchদক্ষিণ/পূর্বে এবংprice < 100। চোখে দেখে: ১০০২ (নোটবুক, ৬০.০) এবং ১০০৭ (কলম, ১৫.০)।category == "stationery"এবংquantity >= 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প্রতিটি উত্তর পরিকল্পনার সাথে নিখুঁতভাবে মিলে গেছে। কোডের পছন্দগুলো নিয়ে কিছু দরকারি বিষয়:
- ১ নম্বর প্রশ্নে মাস্কের
sum()ব্যবহার করা হয়েছে,len(orders[...])নয়। দুটিই ৩ দিলেও মাস্কটি সরাসরি সংখ্যা গুনে ফেলে এবং বাড়তি টেবিল তৈরি করে না। - ২ নম্বর প্রশ্নে
!=ব্যবহার করা হয়েছে "উত্তর শাখা ব্যতীত" বোঝাতে।~(orders["branch"] == "north")একই অর্থ বহন করলেও একক মানের জন্য!=লেখা সহজ। - ৩ নম্বর প্রশ্নে মূল লেবেল
1এবং6অপরিবর্তিত রাখা হয়েছে, কারণ প্রশ্নে নতুন নম্বর দিতে বলা হয়নি। এই লেবেলগুলো বলে দেয় মূলordersটেবিলের কোন কোন সারি এগুলো ছিল। - ৪ নম্বর প্রশ্নে লেখার আগেই গণনা করা হয়েছে, এবং ডান পাশে একই সারিগুলো থেকে নতুন মান হিসাব করা হয়েছে:
orders.loc[discount, "price"] * 0.9। উভয় পাশেই একই মাস্ক থাকায় প্রতিটি সারি তার নিজের মূল্যের ৯০% পায়: ৮.০ হয়ে যায় ৭.২, ১৫.০ হয়ে যায় ১৩.৫। কলমের অর্ডার1001(১২টি আইটেম) এবং নোটবুক (৫টি আইটেম) তাদের আগের দামেই বহাল থাকে।
আর ইচ্ছাকৃত তিনটি ভুলের পর্যালোচনা:
- বন্ধনী ছাড়া ৩ নম্বর প্রশ্নে
TypeError: Cannot perform 'rand_' …তৈরি হয়, কারণ<-এর আগেই&কাজ করার চেষ্টা করেছে। orders[discount]["price"] = …লিখলেChainedAssignmentErrorসতর্কবার্তা প্রিন্ট হয়, স্ক্রিপ্ট চালু থাকে, কিন্তু শেষ টেবিলে আগের দাম8.0এবং15.0-ই থেকে যায়। লাইনটি নীরবে কোনো কাজই করে না।"East"লিখলে ৩ নম্বর প্রশ্নটি পূর্ব শাখার কলমটি নীরবে হারিয়ে ফেলে এবং দুটির বদলে মাত্র একটি সারি প্রিন্ট করে। কোনো এরর নেই, কোনো ওয়ার্নিং নেই: কেবল একটি ভুল উত্তর যা দেখতে সম্পূর্ণ সঠিক মনে হয়। এই কারণেই কোড চালানোর আগে পরিকল্পনায় প্রত্যাশিত সারি লিখে নেওয়া জরুরি। এই বাগটি একমাত্র সেই প্রোগ্রামারই ধরতে পারবেন যিনি আগে থেকেই জানতেন যে ফলাফল দুটি সারি হওয়া উচিত।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন