অধ্যায় 04

কলাম এবং সারি বেছে নেওয়া — নাম দিয়ে কিংবা অবস্থান দিয়ে

টেবিলের নির্দিষ্ট অংশে নির্দেশ করা: ব্র্যাকেট দিয়ে কলাম, loc দিয়ে লেবেল অনুযায়ী ও iloc দিয়ে অবস্থান অনুযায়ী সারি, at দিয়ে একক ঘর — এবং আপনি কোনটি ব্যবহার করছেন তা সবসময় স্পষ্ট জানা।

45 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

অধ্যায় তিনে যেভাবে শেখানো হয়েছিল, সেভাবে আপনি দোকানের অর্ডারের ফাইলটি পড়েছেন এবং যাচাই করে নিয়েছেন। এবার মানুষজন এটি নিয়ে নানা প্রশ্ন করতে শুরু করল।

স্টক টিম বলল: "আমাদের প্রতিটি অর্ডারের শুধু প্রডাক্ট আর পরিমাণ (quantity) পাঠিয়ে দিন — অন্য কিছু লাগবে না।" হিসাবরক্ষক বললেন: "১০০৬ নম্বর অর্ডারটি ঠিক কী ছিল, আর সেটার দাম কত?" আর আপনার ম্যানেজার বললেন: "সবশেষে যে তিনটি অর্ডার এসেছে, সেগুলো আমাকে দেখান।"

এগুলোর কোনোটিই পুরো টেবিল জুড়ে কোনো হিসাবনিকাশ নয়। প্রতিটির মূল কাজ হলো নির্দিষ্ট জায়গায় নির্দেশ করা (pointing): কিছু কলামের দিকে, একটি নির্দিষ্ট সারির দিকে, শেষের কয়েকটি সারির দিকে, কিংবা একটি একক ঘরের (cell) দিকে। পুরো টেবিলটা স্ক্রিনে প্রিন্ট করে আঙুল দিয়ে দেখে নেওয়া হয়তো আটটি সারির জন্য চলে। কিন্তু আট হাজার সারির ক্ষেত্রে সেটা চলে না, আর কোনো প্রোগ্রামের ভেতরেও সেটা চলে না — কারণ প্রোগ্রামের কোনো আঙুল নেই।

পান্ডাসে কোনো কিছু নির্দেশ করার কয়েকটি উপায় রয়েছে, আর কেন একাধিক উপায় রয়েছে — সেটাই এই অধ্যায়ের আসল বিষয়। একটি সারির দিকে দুটি ভিন্ন উপায়ে নির্দেশ করা যায়: তার নাম দিয়ে (ইনডেক্সে থাকা তার লেবেল) অথবা তার অবস্থান দিয়ে (প্রথম, দ্বিতীয়, শেষ)। সদ্য পড়া কোনো টেবিলে কাকতালীয়ভাবে এই দুটোই একই সংখ্যা হয়, তাই নতুনরা কখনোই খেয়াল করেন না যে এখানে দুটি আলাদা বিষয় রয়েছে। কিন্তু যেই মুহূর্তে আপনি শেষের তিনটি সারি আলাদা করবেন, কিংবা সর্ট বা ফিল্টার করবেন, তখনই তারা আর এক থাকে না — আর যে কোডে এই দুটোকে গুলিয়ে ফেলা হয়েছে, সেটি কোনো ত্রুটি (error) না দেখিয়েই নীরবে ভুল সারি দেখাতে শুরু করে।

তাই এই অধ্যায়টি সিলেকশন বা নির্দিষ্ট অংশ বেছে নেওয়ার বিষয়ে, তবে সবচেয়ে বড় কথা — আপনি এই দুটির কোনটি ব্যবহার করছেন, তা সবসময় স্পষ্টভাবে জেনে রাখা।

এই অধ্যায় শেষে আপনি পারবেন

  • একটি কলাম বেছে নিয়ে Series, কিংবা একাধিক কলাম বেছে নিয়ে DataFrame পেতে — এবং বুঝতে পারবেন আপনার হাতে কোনটি এসেছে
  • লেবেল দিয়ে .loc এবং অবস্থান দিয়ে .iloc ব্যবহার করে সারি নির্বাচন করতে, এবং তাদের স্লাইসের সমাপ্তি কেন ভিন্ন তা ব্যাখ্যা করতে
  • .loc[rows, columns] দিয়ে সারি ও কলাম একসাথে নির্বাচন করতে এবং ফলাফলের আকার (shape) আগে থেকেই অনুমান করতে — এমনকি .loc বা .at দিয়ে একটি একক মান বের করতে
  • set_index() দিয়ে সারিগুলোকে অর্থপূর্ণ লেবেল দিতে, এবং তা .loc-এর অর্থ কীভাবে বদলে দেয় তা বুঝতে
  • পান্ডাস ৩-এ কোনো সিলেকশন পরিবর্তন করলে কেন মূল টেবিল পরিবর্তিত হয় না তা ব্যাখ্যা করতে, এবং মূল টেবিলে সঠিকভাবে পরিবর্তন আনতে; সিলেকশনের সময় ঘটা KeyError ও IndexError চিনে ঠিক করতে

আগে যা জানা লাগবে: CSV ফাইল পড়া, আর পড়ার পর যাচাই করা।


আগে ফাইলটি তৈরি করে নিন

আপনার প্রজেক্ট ফোল্ডারে ঠিক এই নয়টি লাইন দিয়ে orders.csv নামে একটি ফাইল তৈরি করুন। এই ফাইলটি পুরো কোর্সের বাকি অংশে ব্যবহৃত হবে, তাই এমন কোথাও রাখুন যেন পরে সহজেই খুঁজে পান:

text
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

প্রতিটি লাইন একটি করে অর্ডার: অর্ডার নম্বর, তারিখ, দোকানের যে শাখা থেকে বিক্রি হয়েছে, কী বিক্রি হয়েছে, পণ্যের ক্যাটাগরি, কয়টি, এবং একটি পণ্যের দাম।


কোড লেখার আগে

সিলেকশনের কাজ দেখতে এতো ছোট মনে হয় যে মানুষ সাথে সাথেই ব্র্যাকেট টাইপ করতে শুরু করে দেয়। আগে এই ধাপগুলোতে এক মিনিট সময় দিন, কারণ এই অধ্যায়ের প্রতিটি সিলেকশন সংক্রান্ত ভুল এগুলোর কোনো একটি বাদ দেওয়ার কারণেই ঘটে।

১. প্রশ্নটি এক বাক্যে বলুন, এবং বিশেষ্যগুলোর (nouns) নিচে দাগ দিন। বিশেষ্যগুলোই বলে দেয় আপনি কোনটির দিকে নির্দেশ করছেন:

  • "প্রতিটি অর্ডারের product এবং quantity" — দুটি কলাম, সব সারি
  • "order 1006" — একটি সারি, তার অর্ডার নম্বর দিয়ে চিহ্নিত
  • "last three অর্ডার" — অবস্থান অনুযায়ী সারি, শেষ দিক থেকে গোনা
  • "order 1006-এর price" — একটি ঘর (cell): একটি সারি ও একটি কলাম একসাথে

২. টেবিলে নির্দেশ করার আগে টেবিলটি দেখে নিন। আপনি শুধু বিদ্যমান নামগুলোর দিকেই নির্দেশ করতে পারেন। সিলেকশনের জন্য তিনটি বিষয় জরুরি: আকার (size), কলামগুলোর নিখুঁত নাম, এবং ইনডেক্স — অর্থাৎ বাঁ পাশের লেবেলগুলো।

python
import pandas as pd

orders = pd.read_csv("orders.csv")

print(orders.shape)
print(list(orders.columns))
print(orders.index)
text
(8, 7)
['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
RangeIndex(start=0, stop=8, step=1)

কলামের নামগুলো কোটেশনের ভেতরে একটি তালিকা হিসেবে আসে — যাতে আপনি দেখতে পান 'price' ছোট হাতের অক্ষরে লেখা এবং এতে কোনো বাড়তি স্পেস নেই। ইনডেক্সটি হলো ০ থেকে ৭ পর্যন্ত একটি RangeIndex: লেবেলগুলো স্রেফ গণনা মাত্র। এটি মনে রাখুন, কারণ এই মিলটি নিয়েই পুরো অধ্যায়টি সাজানো। এই মুহূর্তে যে সারিটির লেবেল ২, সেই সারিটির অবস্থানও ২। এটি কিন্তু সবসময় সত্য থাকবে না।

৩. ফলাফল পাওয়ার আগেই তার রূপ কল্পনা করুন (sketch)। কী ফলাফল আসা উচিত — একটি একক সংখ্যা, একটি কলাম, নাকি একটি ছোট টেবিল? কয়টি সারি এবং কয়টি কলাম? স্টক টিমের অনুরোধের উত্তর হলো ৮ সারি ও ২ কলামের একটি টেবিল। "order 1006"-এর জন্য এটি একটি একক সারি। আর "order 1006-এর দাম"-এর জন্য এটি একটি একক সংখ্যা, 850.0। আপনি যদি প্রত্যাশিত আকারটি জানেন, তবে ফলাফল দেখার সাথে সাথেই ভুল সিলেকশন ধরা পড়ে যাবে।

৪. আপনার জানার ওপর ভিত্তি করে টুল বেছে নিন। এই তালিকাটি হলো পুরো অধ্যায়ের একটি রূপরেখা:

  • আপনার জানা আছে কলামের নাম → থার্ড ব্র্যাকেট: orders["quantity"], orders[["product", "quantity"]]
  • আপনার জানা আছে সারির লেবেল, ইনডেক্সে তার নাম → .loc: orders.loc[2]
  • আপনার জানা আছে সারির অবস্থান — প্রথম, শেষ, পঞ্চম → .iloc: orders.iloc[-3:]
  • আপনার সারি ও কলাম একসাথে প্রয়োজন → .loc[rows, cols] বা .iloc[rows, cols]: orders.loc[2:4, ["product", "price"]]
  • আপনার প্রয়োজন ঠিক একটি ঘর → .at[row, col] বা .loc[row, col]: orders.at[5, "price"]

৫. কীভাবে যাচাই করবেন তা ঠিক করে নিন। যেকোনো সিলেকশনের পর ফলাফলের .shape বা type() প্রিন্ট করে আপনার কল্পনার সাথে মিলিয়ে নিন। এটি মাত্র এক লাইনের কোড, কিন্তু নিচে উল্লেখিত বেশিরভাগ ভুলকে সংখ্যায় রূপান্তরিত হওয়ার আগেই আটকে দেয়।

অধ্যায়ের বাকি অংশে এই টুলগুলো একটি একটি করে আলোচনা করা হয়েছে, প্রতিটির সাথে রয়েছে সমাধানসহ উদাহরণ।


একটি কলাম: থার্ড ব্র্যাকেটে নাম

থার্ড ব্র্যাকেটে একটি কলামের নাম লিখলে আপনি সেই কলামটি পেয়ে যাবেন:

python
quantity = orders["quantity"]

print(type(quantity))
print(quantity)
text
<class 'pandas.Series'>
0    12
1     5
2     2
3     7
4    30
5     1
6    20
7     4
Name: quantity, dtype: int64

ফলাফল হিসেবে যা আসে তা একটি Series — অধ্যায় দুইয়ের ঠিক সেই অবজেক্টটি: মানগুলো, বাঁ পাশে একটি ইনডেক্স, একটি Name (যে কলাম থেকে এটি এসেছে) এবং একটি dtype। খেয়াল করুন, ইনডেক্সটিও এর সাথে এসেছে। 0 থেকে 7 কোনো সাজসজ্জা নয়: এগুলো এখনও বলে দেয় প্রতিটি সংখ্যা কোন সারির অংশ।

যেহেতু এটি একটি Series, তাই প্রতিটি Series মেথড এতে সরাসরি কাজ করে:

python
print(orders["quantity"].sum())
text
81

সাধারণত একটি কলাম বেছে নেওয়ার কারণ এটাই: সেটির কাছে কোনো নির্দিষ্ট প্রশ্ন করা বা হিসাব করা।

একাধিক কলাম: ব্র্যাকেটের ভেতরে একটি তালিকা

স্টক টিমের জন্য আপনার দুটি কলাম দরকার। ব্র্যাকেটের ভেতরে নামের একটি তালিকা (list) দিন:

python
stock = orders[["product", "quantity"]]

print(type(stock))
print(stock)
text
<class 'pandas.DataFrame'>
    product  quantity
0       pen        12
1  notebook         5
2       bag         2
3    bottle         7
4    eraser        30
5       bag         1
6       pen        20
7    bottle         4

ডাবল ব্র্যাকেট কোনো বিশেষ সিনট্যাক্স নয়, এবং এটি স্পষ্টভাবে বুঝে নেওয়া দরকার। বাইরের জোড়া ব্র্যাকেট হলো সিলেকশনের জন্য; ভেতরের জোড়া হলো সাধারণ পাইথন তালিকা, ["product", "quantity"]। আপনি চাইলে আগে তালিকাটি তৈরি করে তারপর তা দিতে পারতেন — wanted = ["product", "quantity"] এবং তারপর orders[wanted] — এর অর্থও হতো হুবহু এক। কলামগুলো তালিকার ক্রম অনুযায়ী ফিরে আসে, তাই কলামের ক্রম পরিবর্তনের জন্যও এটি ব্যবহার করা হয়।

ফলাফলটি একটি DataFrame, কারণ দুটি কলামের টেবিল আসলে একটি টেবিলই। আর এখানে এমন একটি পার্থক্য রয়েছে যা মানুষকে মাসের পর পর বিভ্রান্ত করে: একটিমাত্র নাম থাকা একটি তালিকা দিলেও কিন্তু ফলাফল হিসেবে একটি DataFrame পাওয়া যায়।

python
print(orders["product"].shape)
print(orders[["product"]].shape)
text
(8,)
(8, 1)

(8,) হলো একটি Series — এক মাত্রা (one dimension), আটটি মান। আর (8, 1) হলো আট সারি এবং এক কলামের একটি DataFrame। তারা ভিন্নভাবে প্রিন্ট হয়, তাদের মেথড ভিন্ন, এবং ফাইলে সেভ করার সময় DataFrame তার কলামের হেডার বজায় রাখে। নিয়মটি সহজ: একটি নাম দিলে Series পাওয়া যায়; একটি তালিকা দিলে DataFrame পাওয়া যায় — তালিকায় যতগুলো নামই থাকুক না কেন।

orders.quantity কেন নয়?

অনেক টিউটোরিয়ালে আপনি ডট দিয়ে কলাম অ্যাক্সেস করতে দেখবেন:

python
print(orders.branch.head(2))
text
0    north
1    south
Name: branch, dtype: str

এখানে এটি কাজ করছে, এবং এটি ছোট। কিন্তু এটি তখনই কাজ করে যখন কলামের নামটি ঘটনাক্রমে পাইথনের একটি বৈধ ভেরিয়েবল নাম হয় এবং যা ইতিমধ্যে অন্য কোনো কাজে ব্যবহৃত হয়নি — অথচ একটি DataFrame-এ শত শত মেথড ও অ্যাট্রিবিউট থাকে। নিচে একটি ছোট টেবিল দেখুন যার কলামগুলোর নাম count এবং size, দোকানের জন্য দুটি খুবই যৌক্তিক নাম:

python
d = pd.DataFrame({"count": [1, 2], "size": [3, 4]})

print(d.size)
print(d["size"])
text
4
0    3
1    4
Name: size, dtype: int64

d.size আপনাকে কলামটি মোটেও দেয়নি। এটি কোনো ত্রুটি না দেখিয়ে নীরবে DataFrame-এর নিজস্ব size অ্যাট্রিবিউট ফেরত দিয়েছে — ঘরের মোট সংখ্যা, ২ × ২ = ৪। কোনো এরর নেই, স্রেফ একটি ভুল উত্তর। আবার যে নামে স্পেস রয়েছে, যেমন unit price, সেটি তো ডট দিয়ে লেখাই যায় না। থার্ড ব্র্যাকেটের ক্ষেত্রে এই সমস্যাগুলোর কোনোটিই নেই, তাই কোড লেখার নিয়ম হলো: সবসময় df["col"] ব্যবহার করুন। টার্মিনালে চটজলদি একবার দেখে নেওয়ার জন্য ডট ঠিক আছে, এর বেশি কিছু নয়।


লেবেল দিয়ে সারি: .loc

সারি সাধারণ ব্র্যাকেট দিয়ে নির্বাচন করা যায় না। (সাধারণ ব্র্যাকেটে নাম লেখার অর্থ হলো "কলাম", এবং আপনি কোনটি বুঝিয়েছেন তা পান্ডাস নিজে থেকে আন্দাজ করবে না।) সারির জন্য দুটি আলাদা টুল রয়েছে, যার প্রথমটি হলো .loc — label-based location (লেবেল-ভিত্তিক অবস্থান)। আপনি ইনডেক্স থেকে লেবেলটি এতে দেবেন:

python
print(orders.loc[2])
text
order_id           1003
date         2024-03-02
branch            north
product             bag
category    accessories
quantity              2
price             850.0
Name: 2, dtype: object

একটি সারি ফিরে আসে কাৎ করা একটি Series হিসেবে: কলামের নামগুলো হয়ে গেছে এর ইনডেক্স, আর এর Name হলো সারিটির লেবেল, 2।

dtype: object-এর দিকে তাকান। একটি কলামে একটি নির্দিষ্ট ধরন থাকে, কিন্তু একটি সারি বিভিন্ন কলাম জুড়ে বিস্তৃত — একটি সংখ্যা, কিছু লেখা, একটি দশমিক সংখ্যা — তাই এগুলোর সব কটি ধারণ করার মতো একমাত্র ধরন হলো সাধারণ object। পান্ডাস যে কলামভিত্তিক চিন্তা করে এটি তার একটি কারণ: সারিতে ধরনের মিশ্রণ থাকে, কিন্তু কলামে থাকে না।

এবার গুরুত্বপূর্ণ অংশটি দেখুন। .loc-এর কাছে লেবেলের একটি রেঞ্জ চান:

python
print(orders.loc[2:4])
text
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
4      1005  2024-03-03  north  eraser   stationery        30    8.0

তিনটি সারি: ২, ৩ এবং ৪। একটি .loc স্লাইস তার শেষ মানটিকেও অন্তর্ভুক্ত করে। পাইথন জানা প্রত্যেকেই এতে অবাক হন, কারণ পাইথনে range(2, 4) এবং list[2:4] শেষ মান ৪-এর আগে থেমে যায়। এটি কিন্তু কোনো অসঙ্গতি নয়; এর পেছনে একটি সুনির্দিষ্ট কারণ আছে।

লেবেল সবসময় সংখ্যা হয় না। লেবেলগুলো যদি পণ্যের নাম হতো, তবে আপনি লিখতেন loc["bag":"eraser"]-এর মতো কিছু। শেষ মানটিকে বাদ দিতে হলে "eraser"-এর পরের লেবেলটির নাম আপনাকে লিখতে হতো — আর নামের ক্ষেত্রে আগে থেকে না দেখে "পরবর্তী নাম" কোনটি তা জানা অসম্ভব। তাই লেবেলের ক্ষেত্রে আপনি যেখান থেকে শুরু করতে চান এবং যেখানে শেষ করতে চান দুটোই বলে দেন, আর আপনি দুটোই পান। কিন্তু অবস্থানের ক্ষেত্রে বিষয়টি আলাদা: ৪-এর পরের অবস্থান সবসময়ই ৫, তাই সেখানে পাইথনের "শেষের আগে থামার" নিয়মে কোনো সমস্যা হয় না।

অবস্থান দিয়ে সারি: .iloc

দ্বিতীয় টুলটি হলো .iloc — integer location (পূর্ণসংখ্যা-ভিত্তিক অবস্থান)। এটি লেবেলগুলোকে পুরোপুরি উপেক্ষা করে এবং ঠিক পাইথন লিস্টের মতো ০ থেকে স্থান গণনা করে:

python
print(orders.iloc[2:4])
text
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

দুটি সারি, অবস্থান ২ এবং ৩; পাইথনের সাধারণ নিয়মের মতো অবস্থান ৪ এখানে বাদ পড়েছে। দুটি আউটপুট পাশাপাশি রেখে দেখুন: একই টেবিলে, ব্র্যাকেটের ভেতরে একই সংখ্যা থাকা সত্ত্বেও loc[2:4] দিয়েছে তিনটি সারি আর iloc[2:4] দিয়েছে দুটি সারি।

যেহেতু .iloc অবস্থানভিত্তিক, তাই ঋণাত্মক সংখ্যা শেষ দিক থেকে গণনা করে — যা সরাসরি আপনার ম্যানেজারের প্রশ্নের উত্তর দেয়:

python
print(orders.iloc[-3:])
text
order_id        date branch product     category  quantity  price
5      1006  2024-03-03  south     bag  accessories         1  850.0
6      1007  2024-03-04   east     pen   stationery        20   15.0
7      1008  2024-03-04  north  bottle  accessories         4  120.0

"শেষ তিনটি" হলো অবস্থান সম্পর্কিত একটি প্রশ্ন, তাই এর সাথে মানানসই টুল হলো .iloc। "শেষ" বলতে কী বোঝায় সে সম্পর্কে .loc-এর কোনো ধারণাই নেই; সে শুধু নাম চেনে।

লেবেল আর অবস্থান এক জিনিস নয়

এতক্ষণ পর্যন্ত লেবেল ২ এবং অবস্থান ২ একই সারি ছিল, তাই ফলাফল দেখে আপনি দুটি টুলের পার্থক্য ধরতে পারতেন না। শেষের তিনটি অর্ডারকে আলাদা টেবিল হিসেবে নিন, আর দেখুন কীভাবে তারা আলাদা হয়ে যায়:

python
last3 = orders.tail(3)

print(last3)
text
order_id        date branch product     category  quantity  price
5      1006  2024-03-03  south     bag  accessories         1  850.0
6      1007  2024-03-04   east     pen   stationery        20   15.0
7      1008  2024-03-04  north  bottle  accessories         4  120.0

সারিগুলো নিজেদের লেবেল ধরে রেখেছে: ৫, ৬, ৭। পান্ডাস ইচ্ছে করেই এদের নতুন করে নম্বর দেয়নি — টেবিল কাটার পরেও একটি সারি কীভাবে আগের সারি হিসেবেই চেনা যাবে, তা নিশ্চিত করে এই লেবেল। এবার উভয় উপায়ে "প্রথম সারিটি" চান:

python
print(last3.iloc[0])
text
order_id           1006
date         2024-03-03
branch            south
product             bag
category    accessories
quantity              1
price             850.0
Name: 5, dtype: object
python
last3.loc[0]
text
KeyError: 0

.iloc[0] মানে "প্রথম স্থানের সারি", যা হলো ৫ লেবেলযুক্ত অর্ডার। আর .loc[0] মানে "যে সারির লেবেল ০" — কিন্তু এই টেবিলে এমন কোনো সারি নেই, তাই পান্ডাস KeyError দেয়। কোর্সের পরবর্তী অংশে এই ভুলটিই বড় সমস্যা তৈরি করে: যখন আপনি সারি ফিল্টার করেন বা সর্ট করেন — এ দুটিই কোর্সে পরে আসছে — অবস্থান বদলে যায় কিন্তু লেবেল বদলায় না। ভবিষ্যতের জন্য মনে রাখার নিয়ম:

  • যদি স্থান বা অবস্থান নিয়ে প্রশ্ন হয় — প্রথম, শেষ, সর্ট করার পর শীর্ষ তিনটি? .iloc ব্যবহার করুন।
  • যদি পরিচয় বা সত্তা নিয়ে প্রশ্ন হয় — এই নির্দিষ্ট অর্ডার, এই পণ্য? .loc ব্যবহার করুন।

আর সবচেয়ে খারাপ পরিস্থিতি কিন্তু KeyError নয়। সবচেয়ে খারাপ পরিস্থিতি হলো যখন টেবিলের অন্য কোথাও লেবেল ০ বিদ্যমান থাকে: তখন .loc[0] আপনার কাঙ্ক্ষিত সারির বদলে নীরবে সম্পূর্ণ ভিন্ন একটি সারি এনে দেয়।

.loc চায় একটি নাম, আর .iloc চায় একটি স্থান। সদ্য পড়া কোনো টেবিলে কাকতালীয়ভাবে এ দুটি মিলে যায়, যার ফলে তাদের গুলিয়ে ফেলা কোড প্রথম দিন সব পরীক্ষায় পাস করে যায় — আর টেবিলটি যখনই কাটা, ফিল্টার বা সর্ট করা হয়, তখন কোনো ত্রুটি ছাড়াই ভুল সারি ফেরত দিতে শুরু করে।

সারিগুলোকে অর্থপূর্ণ লেবেল দেওয়া: set_index

হিসাবরক্ষক জানতে চেয়েছিলেন order 1006 সম্পর্কে। ডিফল্ট ইনডেক্স থাকলে আপনাকে জানতে হতো যে order 1006 রয়েছে ৫ নম্বর লেবেলের নিচে — যা ফাইলটির একটি কাকতালীয় তথ্য, অর্ডারের নিজস্ব বৈশিষ্ট্য নয়। একটি সারির আসল পরিচয় হলো তার অর্ডার নম্বর, তাই অর্ডার নম্বরটিকে ইনডেক্স বানিয়ে নিন:

python
by_id = orders.set_index("order_id")

print(by_id.head(3))
print(by_id.shape)
text
date branch   product     category  quantity  price
order_id
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
(8, 6)

তিনটি জিনিস পরিবর্তিত হয়েছে। অর্ডার নম্বরগুলো বাঁ দিকে ইনডেক্সে চলে গেছে। ইনডেক্সের এখন একটি নাম হয়েছে, order_id, যা তার উপরে নিজস্ব লাইনে প্রিন্ট হয়েছে। আর আকারটি (8, 7)-এর বদলে হয়েছে (8, 6) — order_id এখন আর কলাম নয়, এটি এখন ইনডেক্স। এটাও খেয়াল করুন যে set_index একটি নতুন টেবিল ফেরত দিয়েছে, যার নাম আমরা দিয়েছি by_id; মূল orders আগের মতোই অপরিবর্তিত আছে।

এখন .loc প্রশ্নের ভাষায় কথা বলে:

python
print(by_id.loc[1006])
text
date         2024-03-03
branch            south
product             bag
category    accessories
quantity              1
price             850.0
Name: 1006, dtype: object

আর একটি লেবেল স্লাইস এখন অর্ডার নম্বরের একটি রেঞ্জ, যার উভয় প্রান্তই অন্তর্ভুক্ত:

python
print(by_id.loc[1003:1005, ["product", "quantity"]])
text
product  quantity
order_id                  
1003         bag         2
1004      bottle         7
1005      eraser        30

.iloc-এ কিন্তু কোনো পরিবর্তন হয়নি — লেবেল যাই হোক না কেন, অবস্থান অবস্থানই থাকে:

python
print(by_id.iloc[0:2])
text
date branch   product    category  quantity  price
order_id
1001      2024-03-01  north       pen  stationery        12   15.0
1002      2024-03-01  south  notebook  stationery         5   60.0

এখানে টুল দুটি স্পষ্টভাবে আলাদা হয়ে গেছে: by_id.loc[0] এখন একটি KeyError দেবে, কারণ ০ কোনো অর্ডার নম্বর নয়, যেখানে by_id.iloc[0] হলো অর্ডার ১০০১। আপনার যদি কখনো অর্ডার নম্বরটিকে আবার কলাম হিসেবে ফিরিয়ে আনার দরকার হয়, তবে by_id.reset_index() এটিকে আবার কলামে ফিরিয়ে দেয়।

এমন একটি কলাম বেছে নিন যার মানগুলো ইউনিক (অনন্য)। ইনডেক্সের উদ্দেশ্য হলো সারিকে সুনির্দিষ্টভাবে চিহ্নিত করা, আর দুটি সারির একই নাম থাকলে তা দিয়ে নির্দিষ্ট একটিকে নির্দেশ করা যায় না। product-এর ক্ষেত্রে কী ঘটে দেখুন, যা একাধিকবার এসেছে:

python
by_product = orders.set_index("product")

print(by_product.index.is_unique)
print(by_product.loc["bag"])
text
False
         order_id        date branch     category  quantity  price
product
bag          1003  2024-03-02  north  accessories         2  850.0
bag          1006  2024-03-03  south  accessories         1  850.0

কোনো এরর নেই — কিন্তু loc["bag"] একটি সারিকে Series হিসেবে দেওয়ার বদলে দুই সারির একটি DataFrame ফেরত দিয়েছে। যে কোড একটিমাত্র সারির প্রত্যাশা করছিল (যেমন তার price একটি সংখ্যা হিসেবে পড়া), সে এখন দুটি সারি পাবে। এই কারণেই যখনই আপনি সারি খুঁজে বের করার উদ্দেশ্যে ইনডেক্স সেট করবেন, তখনই index.is_unique দিয়ে এক লাইনে যাচাই করে নেওয়া উচিত।


সারি এবং কলাম একসাথে

.loc এবং .iloc দুটোই কমা দিয়ে আলাদা করা দুটি অংশ নেয়: প্রথমে সারি, দ্বিতীয়তে কলাম। প্রতিটি অংশ একটি একক লেবেল, একটি তালিকা, বা একটি স্লাইস হতে পারে:

python
print(orders.loc[2:4, ["product", "price"]])
text
product  price
2     bag  850.0
3  bottle  120.0
4  eraser    8.0
python
print(orders.loc[[0, 3], ["branch", "product"]])
text
branch product
0  north     pen
3   east  bottle

কলামের ক্ষেত্রেও স্লাইস কাজ করে, .loc-এর প্রান্ত অন্তর্ভুক্ত করার নিয়ম মেনে — "branch থেকে category পর্যন্ত প্রতিটি কলাম"। শুধু একটি কোলন : মানে হলো "সব সারি":

python
print(orders.loc[:, "branch":"category"])
text
branch   product     category
0  north       pen   stationery
1  south  notebook   stationery
2  north       bag  accessories
3   east    bottle  accessories
4  north    eraser   stationery
5  south       bag  accessories
6   east       pen   stationery
7  north    bottle  accessories

.iloc অবস্থান দিয়ে একই কাজ করে। কলামগুলোও ০ থেকে গোনা হয়, তাই branch হলো ২ নম্বর কলাম এবং product হলো ৩ নম্বর কলাম:

python
print(orders.iloc[[0, 3], [2, 3]])
text
branch product
0  north     pen
3   east  bottle

উপরের দুটি ব্লক আগের .loc সংস্করণের মতোই একই ফলাফল — কিন্তু লক্ষ্য করুন এটি পড়া কতটা কঠিন। [2, 3] কিছুই বোঝায় না; কিন্তু ["branch", "product"] সবকিছু পরিষ্কার বলে দেয়, এমনকি ফাইলের শুরুতে কেউ নতুন একটি কলাম যোগ করলেও কোড ঠিকঠাক কাজ করে। তাই নাম জানা থাকলে নাম ব্যবহার করাই শ্রেয়।

কী আকারের ফলাফল ফেরত আসবে?

আপনি একটি একক মান, একটি Series নাকি একটি DataFrame পাবেন, তা নির্ভর করে কমার উভয় পাশে আপনি কী দিয়েছেন তার ওপর। একটি একক লেবেল সেই মাত্রাটি (dimension) বাদ দেয়; একটি তালিকা বা স্লাইস সেই মাত্রাটি বজায় রাখে:

python
print(type(by_id.loc[1006, "price"]).__name__)
print(type(by_id.loc[1006, ["product", "price"]]).__name__)
print(type(by_id.loc[[1003, 1006], "price"]).__name__)
print(type(by_id.loc[[1003, 1006], ["product", "price"]]).__name__)
text
float64
Series
Series
DataFrame
  • একটি লেবেল, একটি লেবেল → একটি একক মান
  • একটি লেবেল, একটি তালিকা বা স্লাইস → একটি Series (একটি সারি)
  • একটি তালিকা বা স্লাইস, একটি লেবেল → একটি Series (একটি কলাম)
  • একটি তালিকা বা স্লাইস, একটি তালিকা বা স্লাইস → একটি DataFrame

এটি হলো পরিকল্পনার ৩ নম্বর ধাপ — উত্তরের আকার কল্পনা করা — যা একটি নিয়মে পরিণত হয়েছে। আপনি যদি এক সারির হলেও একটি টেবিল ফেরত পেতে চান, তবে উভয় পাশে তালিকা দিন: by_id.loc[[1006], ["price"]]।

একটি একক মান: .loc এবং .at

হিসাবরক্ষকের প্রশ্ন — order 1006-এর দাম কত ছিল? — এটি একটিমাত্র ঘর: একটি সারির লেবেল এবং একটি কলামের নাম।

python
print(by_id.loc[1006, "price"])
print(by_id.at[1006, "price"])
text
850.0
850.0

দুটোই একই সংখ্যা দেয়। .at শুধুমাত্র একটি একক সারি এবং একটি একক কলাম গ্রহণ করে, অন্য কিছু নয়, যা একে কিছুটা দ্রুতগতির করে এবং আপনার উদ্দেশ্য স্পষ্ট করে: এটি একটিমাত্র ঘর। (অবস্থানভিত্তিক যমজ হলো .iat, যেমন by_id.iat[5, 5]।) যেটি পড়তে ভালো লাগে সেটি ব্যবহার করুন; .loc-ও চমৎকার।

একটি একক মান একটি সাধারণ সংখ্যা, তাই এটি দিয়ে আপনি সরাসরি হিসাবনিকাশ করতে পারেন। অর্ডার ১০০৪-এর মোট মূল্য — পরিমাণ গুণ দাম:

python
print(by_id.at[1004, "quantity"] * by_id.at[1004, "price"])
text
840.0

সিলেকশন একটি আলাদা অবজেক্ট

বাস্তব প্রোগ্রামে সিলেকশন ব্যবহার করার আগে আরও একটি বিষয় বোঝা দরকার: যখন আপনি কোনো সিলেকশনকে পরিবর্তন করেন তখন কী ঘটে।

python
import pandas as pd

orders = pd.read_csv("orders.csv")

prices = orders["price"]
prices[0] = 999.0

print(prices.head(2))
print(orders.loc[0, "price"])
text
0    999.0
1     60.0
Name: price, dtype: float64
15.0

prices পরিবর্তিত হয়েছে। orders হয়নি। পান্ডাস ৩-এ প্রতিটি সিলেকশন একটি পৃথক কপি (অনুলিপি) হিসেবে আচরণ করে: আপনি নির্দ্বিধায় এটি পরিবর্তন করতে পারেন এবং মূল টেবিল কখনোই স্পর্শিত হয় না। (অভ্যন্তরীণভাবে পান্ডাস যতক্ষণ না আপনি নতুন মান লিখছেন ততক্ষণ পর্যন্ত ডেটা কপি করা এড়িয়ে চলে — এই ব্যবস্থার নাম কপি-অন-রাইট (Copy-on-Write) — তবে আপনার যে নিয়মটি দরকার তা হলো এর বাহ্যিক আচরণ, অভ্যন্তরীণ মেকানিজম নয়।)

একাধিক কলামের সিলেকশনের ক্ষেত্রেও একই কথা প্রযোজ্য:

python
stock = orders[["product", "price"]]
stock["price"] = 0

print(orders["price"].head(2))
text
0    15.0
1    60.0
Name: price, dtype: float64

এটি একটি নিরাপদ আচরণ: একটি ফাংশন যা কোনো সিলেকশন নিয়ে তা সম্পাদনা করে, তা আপনার অজান্তে মূল টেবিল নষ্ট করতে পারে না। তবে এর একটি পরিণতি রয়েছে যা আপনার জানা আবশ্যক। আপনি যদি সত্যিই মূল টেবিলটি পরিবর্তন করতে চান, তবে আপনাকে মূল টেবিলেই .loc দিয়ে এক ধাপে তা করতে হবে:

python
orders.loc[0, "price"] = 16.0

print(orders.loc[0, "price"])
text
16.0

প্রলুব্ধকর দুই ধাপের সংস্করণটি — প্রথমে কলাম বেছে নেওয়া, তারপর তার একটি ঘরে মান বসানো — কাজ করে না, এবং পান্ডাস ৩ নিজেই আপনাকে তা জানিয়ে দেয়:

python
orders["price"][0] = 1.0
text
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
python
print(orders.loc[0, "price"])
text
16.0

এখনও 16.0 — অ্যাসাইনমেন্টটি একটি অস্থায়ী কপিতে চলে গিয়েছিল এবং ফেলে দেওয়া হয়েছে। orders["price"] একটি সিলেকশন তৈরি করেছিল, এবং [0] = 1.0 সেটাকে পরিবর্তন করেছে, orders-কে নয়। একে বলে চেইন্ড অ্যাসাইনমেন্ট (chained assignment): = চিহ্নের বাঁ দিকে পরপর দুটি ব্র্যাকেট। এর সমাধান সবসময় সেই একটাই যা সতর্কবার্তায় বলা হয়েছে: সারি ও কলাম উভয় সম্বলিত একটি একক .loc, যেমন orders.loc[0, "price"] = 1.0।

পুরোনো টিউটোরিয়াল পড়লে আপনি সেখানে SettingWithCopyWarning এবং কখনো মূল টেবিল বদলায় কখনো বদলায় না এমন পরামর্শ দেখতে পাবেন। সেটা ছিল পান্ডাস ১ এবং ২-এর ক্ষেত্রে। পান্ডাস ৩-এ নিয়মটি সহজ এবং সবসময় একই: সিলেকশন কখনোই মূল টেবিল পরিবর্তন করে না; মূল টেবিলে .loc ব্যবহার করলে তা পরিবর্তন হয়।


একটি সম্পূর্ণ উদাহরণ

অধ্যায়ের শুরুর তিনটি অনুরোধের উত্তর একটিমাত্র প্রোগ্রামে দেওয়া হলো। select_orders.py:

python
import pandas as pd

orders = pd.read_csv("orders.csv")

# 1. Check what arrived before pointing into it
print("Shape  :", orders.shape)
print("Columns:", list(orders.columns))

# 2. Name the rows by what they are
by_id = orders.set_index("order_id")
print("Order ids unique:", by_id.index.is_unique)
print()

# Stock team: two columns, every row
stock = orders[["product", "quantity"]]
print("Stock view:", stock.shape)
print(stock.head(3))
print()

# Accountant: one order by its number, then one cell
print(by_id.loc[1006, ["product", "quantity", "price"]])
print("Order 1006 total:", by_id.at[1006, "quantity"] * by_id.at[1006, "price"])
print()

# Manager: the last three orders, a question about position
print(by_id.iloc[-3:, [1, 2, 4]])
text
Shape  : (8, 7)
Columns: ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
Order ids unique: True

Stock view: (8, 2)
    product  quantity
0       pen        12
1  notebook         5
2       bag         2

product       bag
quantity        1
price       850.0
Name: 1006, dtype: object
Order 1006 total: 850.0

         branch product  quantity
order_id
1006      south     bag         1
1007       east     pen        20
1008      north  bottle         4

কোডটি কেন এভাবে লেখা হয়েছে:

  • নির্দেশ করার আগেই দেখে নেওয়া হয়েছে। shape এবং list(columns) আগেই প্রিন্ট করা হয়েছে, যাতে কোনো কলামের নামে বানান ভুল বা স্পেস থাকলে কোনো সিলেকশন ব্যর্থ হওয়ার আগেই তা ধরা পড়ে।
  • ইনডেক্স ইউনিক কি না তা যাচাই করা হয়েছে অর্ডার খুঁজে নেওয়ার জন্য ব্যবহার করার আগে, যাতে by_id.loc[1006] নিশ্চিতভাবে একটি সারি এবং by_id.at[...] একটি সংখ্যা হয়।
  • প্রতিটি অনুরোধের জন্য সেই টুল ব্যবহার করা হয়েছে যা প্রশ্নের সাথে মানানসই। নাম দিয়ে কলামের জন্য ব্র্যাকেট; অর্ডারের পরিচয় দিয়ে খুঁজতে .loc এবং .at; আর অবস্থান দিয়ে "শেষ তিনটি" বের করতে .iloc।
  • এটি স্টক ভিউয়ের shape প্রিন্ট করেছে — (8, 2) ঠিক ভাবনার সাথে মিলে গেছে: প্রতিটি অর্ডার, দুটি কলাম।
  • শেষ সিলেকশনটি উন্নত করার মতো একটি জায়গা। by_id-তে [1, 2, 4] দ্বারা বোঝাচ্ছে branch, product, quantity — লক্ষ্য করুন order_id ইনডেক্স হওয়ায় অবস্থানগুলো এক ঘর সরে গেছে। অবস্থান ভঙ্গুর। নিজের কোডে by_id.iloc[-3:][["branch", "product", "quantity"]] বা by_id.loc[by_id.index[-3:], ["branch", "product", "quantity"]] নাম ব্যবহারের মাধ্যমে একই কথা আরও স্পষ্টভাবে বলে দেয়।

কিছু ভাঙা অবস্থা ও তার সমাধান

KeyError: 'Price' ঠিক ওই নামের কোনো কলাম নেই। কলামের নাম কেস-সেনসিটিভ: price এবং Price দুটি আলাদা নাম। list(df.columns) প্রিন্ট করুন এবং সেখান থেকে নামটি হুবহু কপি করুন। নামের তালিকা দিলে, যেমন orders[["product", "Price"]], মেসেজটি হয় KeyError: "['Price'] not in index" এবং কোনটি অনুপস্থিত তা স্পষ্টভাবে দেখিয়ে দেয়; ডট দিলে, যেমন orders.Price, এটি হয় AttributeError: 'DataFrame' object has no attribute 'Price'। একই কারণ, একই সমাধান।

KeyError: 'price' — অথচ ফাইলে স্পষ্ট দেখা যাচ্ছে price আছে সম্ভবত হেডারে কমার পরে স্পেস রয়েছে, যেমন order_id, product, price, এবং সেই স্পেসগুলো নামের অংশ হয়ে গেছে:

python
from io import StringIO

import pandas as pd

raw = "order_id, product, price\n1001, pen, 15.0\n"
padded = pd.read_csv(StringIO(raw))

print(list(padded.columns))
padded["price"]
text
['order_id', ' product', ' price']
KeyError: 'price'

তালিকায় স্পষ্ট দেখা যাচ্ছে ' price'-এর শুরুতে স্পেস রয়েছে — আর ঠিক এই কারণেই অধ্যায় তিন আপনাকে কলামগুলোকে তালিকা হিসেবে প্রিন্ট করতে বলেছিল। ফাইল পড়ার সময়ই এটি ঠিক করুন pd.read_csv("file.csv", skipinitialspace=True) দিয়ে, অথবা পরে df.columns = df.columns.str.strip() দিয়ে।

KeyError: ('product', 'price') আপনি orders["product", "price"] লিখেছেন — এক জোড়া ব্র্যাকেট দিয়ে। ভেতরের তালিকাটি ছাড়া, পাইথন নাম দুটিকে একটি একক টাপল হিসেবে পাঠায়, আর পান্ডাস ('product', 'price') নামের একটি কলাম খুঁজতে থাকে। লিখুন orders[["product", "price"]]।

.loc থেকে KeyError: 0 আপনি .loc-এ অবস্থান ব্যবহার করেছেন। হয় টেবিলটি কেটে ছোট করা হয়েছিল (tail, ফিল্টার বা সর্ট) যার ফলে লেবেল ০ হারিয়ে গেছে, অথবা আপনি ইনডেক্স সেট করেছেন এবং লেবেলগুলো এখন অর্ডার নম্বর হয়ে গেছে। আপনি যদি "প্রথম সারি" বোঝাতে চান, তবে লিখুন .iloc[0]। যে টেবিলের ইনডেক্স টেক্সট, সেখানে স্লাইস নিয়ে একই ভুল করলে — যেমন orders.set_index("product").loc[0:2] — পরিবর্তে TypeError: cannot do slice indexing on Index with these indexers [0] of type int দেখা দেয়; এর সমাধানও একই, .iloc[0:2]।

IndexError: single positional indexer is out-of-bounds .iloc-এ এমন একটি অবস্থান দেওয়া হয়েছে যার কোনো অস্তিত্ব নেই — আট সারির টেবিলে orders.iloc[8], যার অবস্থান ০ থেকে ৭ পর্যন্ত। df.shape দেখুন; শেষ সারিটি সবসময় df.iloc[-1]। কলামের অবস্থান মাত্রাতিরিক্ত বড় হলেও একই বার্তা আসে, যেমন orders.iloc[:, 7]।

ValueError: Location based indexing can only have [integer, integer slice (START point is INCLUDED, END point is EXCLUDED), listlike of integers, boolean array] types আপনি .iloc-এ একটি কলামের নাম দিয়েছেন: orders.iloc[0, "branch"]। .iloc শুধুমাত্র অবস্থান গ্রহণ করে। লেবেল দিয়ে .loc ব্যবহার করুন — orders.loc[0, "branch"] — অথবা যদি সত্যিই অবস্থানের প্রয়োজন হয়, তবে তা বের করে নিন: orders.columns.get_loc("branch") দেয় 2।