সারি সাজানো — এবং টাই হলে কী ঘটবে তা ঠিক করা
sort_values দিয়ে সারি সাজানো: এক বা একাধিক কলাম, প্রতিটির নিজস্ব দিক, মিসিং ডেটা ও সংখ্যার মতো দেখতে টেক্সট — সাথে "শীর্ষ N"-এর জন্য nlargest, এবং কাট-অফে টাই হওয়া কেন কোনো দুর্ঘটনা নয় বরং একটি সিদ্ধান্ত।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
দোকানের মালিক একটি ছোট বার্তা পাঠালেন: "এই মাসে আমাদের সবচেয়ে বড় অর্ডারগুলো কোনগুলো ছিল? সেরা তিনটি আমাকে পাঠান।"
ডেটাটি আমাদের সেই পরিচিত orders.csv — দোকানের তিনটি ব্রাঞ্চ (north, south ও east)-এর আটটি অর্ডার। আটটি সারি থাকলে চোখ বুলিয়েই উত্তর দেওয়া যায়। quantity কলাম ধরে নিচে নামুন, প্রথমে ৩০ খুঁজুন, তারপর ২০, তারপর ১২। ব্যস, হয়ে গেল।
এখন আসল ফাইলটির কথা ভাবুন: চার হাজার অর্ডার। তিনটি বৃহত্তম সংখ্যা খুঁজে বের করার জন্য কলাম ধরে চোখ বুলিয়ে নিচে নামা আর কোনো কার্যকর পদ্ধতি নয়, তা নিছক ভাগ্যের ওপর ভরসা করা। আর এদিকে মালিকের পরবর্তী বার্তাও চলে এসেছে: "তালিকাটি কি ব্রাঞ্চ অনুযায়ী গ্রুপ করে পাঠানো যাবে, প্রতি ব্রাঞ্চে সবচেয়ে বড়টি আগে থাকবে? আর সবার উপরে নতুন অর্ডারগুলো রাখবেন, প্লিজ।"
এই প্রতিটি অনুরোধ আসলে একই কাজ: সারিগুলোকে একটি দরকারি ক্রমানুসারে সাজানো, তারপর ওপর থেকে পড়া। এটাই হলো সর্টিং (sorting), আর পান্ডাসে এটি করার একটিমাত্র মেথড রয়েছে, sort_values()।
কিন্তু মেথডটি হলো সবচেয়ে সহজ অংশ। আসল প্রশ্নগুলো লুকিয়ে থাকে এর আশেপাশে, এবং এখানেই নানা রিপোর্টে ভুলভ্রান্তি ঘটে:
- কিসের ভিত্তিতে "সবচেয়ে বড়"? পরিমাণ (quantity), দাম (price), নাকি অর্ডার থেকে আসা মোট টাকার পরিমাণ? প্রতিটির ভিত্তিতে শীর্ষ তিনটি একদম আলাদা হবে।
- যখন দুটি অর্ডারের মান সমান (টাই) হয়, তখন কোনটি আগে আসবে — আর প্রতিবার কোড চালালে এই উত্তর কি বদলে যাবে?
- যে অর্ডারের পরিমাণ ফাঁকা (blank), তার ক্ষেত্রে কী ঘটবে?
- সংখ্যার একটি কলাম যদি ভুলবশত টেক্সট হিসেবে পড়া হয়ে থাকে, তবে কোনো সতর্কতা ছাড়াই
"9"আসবে"30"-এর পরে।
এই অধ্যায়ে সেই মেথডটি তো শেখানো হবেই, এবং তার চেয়েও গুরুত্বপূর্ণ হলো, এই সিদ্ধান্তগুলো কীভাবে নিতে হয় তা শেখানো হবে।
এই অধ্যায় শেষে আপনি পারবেন
sort_values()দিয়ে এক বা একাধিক কলামের ভিত্তিতে টেবিল সাজাতে, প্রতিটির জন্য নিজস্ব দিক নির্ধারণ করতে এবং ফলাফল না হারিয়ে সংরক্ষণ করতে- ব্যাখ্যা করতে কেন ইনডেক্স লেবেল তার সারির সাথে সাথে ঘোরে, এবং লেবেল ধরে রাখা,
sort_index()ওreset_index(drop=True)-এর মধ্যে উপযুক্তটি বেছে নিতে - উদ্দেশ্যমূলকভাবে টাই সমাধান করতে এবং
na_positionদিয়ে মিসিং ভ্যালু কোথায় যাবে তা নির্ধারণ করতে head(),nlargest()এবংnsmallest()ব্যবহার করে "শীর্ষ N" প্রশ্নের উত্তর দিতে — যার মধ্যে কোনো সাবসেটের শীর্ষ N এবং কাট-অফে টাই হওয়া অন্তর্ভুক্তkey=ব্যবহার করে ছোট-বড় হাতের অক্ষরের ভেদাভেদ ছাড়াই টেক্সট সর্ট করতে, এবং টেক্সট হিসেবে সংরক্ষিত সংখ্যাকে তার সর্ট হওয়ার ধরন দেখে চিনতে
আগে যা জানা লাগবে: মিসিং ডেটা — ফাঁকা খুঁজে বের করা, বাদ দেওয়া ও পূরণ করা।
আগে ফাইলটি তৈরি করে নিন
এই অধ্যায়ের প্রতিটি উদাহরণ 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পরের একটি সেকশনে দ্বিতীয় একটি ফাইল ব্যবহার করা হবে, orders_gaps.csv। এটি একই টেবিল, শুধু দুটি অর্ডারের পরিমাণ ফাঁকা রাখা হয়েছে — অর্ডার 1002 এবং 1007 — ঠিক যেমন ফাঁকা ষষ্ঠ অধ্যায়ে খুঁজে বের করতে শিখেছেন:
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,,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,,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0কোড লেখার আগে যা ভাবতে হবে
সর্টিং দেখতে এতোটাই সহজ মনে হয় যে কোনো পরিকল্পনার প্রয়োজন নেই বলে মনে হতে পারে। কিন্তু তা নয়, কারণ সর্ট কখনো ব্যর্থ হয় না — এটি সবসময় একটি না একটি ক্রম তৈরি করেই ফেলে। ভুল বিষয়ের ভিত্তিতে সর্ট করলেও ফলাফল দেখতে সঠিক সর্টের মতোই পরিপাটি লাগবে। তাই কোড লেখার আগেই চিন্তাটা সেরে নিতে হবে।
১. প্রশ্নটিকে কলামের নামসহ এক বাক্যে লিখুন। "সবচেয়ে বড় অর্ডার" এমন কোনো প্রশ্ন নয় যার উত্তর পান্ডাস সরাসরি দিতে পারে। কিন্তু "সর্বাধিক quantity থাকা তিনটি অর্ডার" এমন একটি প্রশ্ন। একইভাবে "সর্বোচ্চ price থাকা তিনটি অর্ডার"-ও তাই। এরা দুজন ভিন্ন উত্তর দেয় — ৮৫০ দামের ব্যাগটি সবচেয়ে দামি আইটেম হলেও বিক্রি হয়েছে মাত্র দুটি ব্যাগ। মালিক কোনটি বুঝিয়েছেন তা আগে নিশ্চিত করুন। বুঝতে না পারলে জিজ্ঞেস করুন; এতে মাত্র একটি মেসেজ খরচ হবে, কিন্তু ভুল শীর্ষ তিন পাঠালে বিশ্বাস নষ্ট হবে।
২. সর্ট করার আগে ইনপুট দেখে নিন। সর্টের ক্ষেত্রে তিনটি বিষয় গুরুত্বপূর্ণ: সারির সংখ্যা, যেসব কলাম দিয়ে সর্ট করবেন তাদের ডেটা টাইপ, এবং সেই কলামগুলোতে ফাঁকা মান আছে কিনা।
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)
print(orders["quantity"].isna().sum())(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: object
0আটটি সারি, সাতটি কলাম। quantity হলো int64 এবং price হলো float64, তাই এরা সংখ্যা হিসেবে সর্ট হবে। branch ও product হলো str — অর্থাৎ টেক্সট, যা বর্ণানুক্রমিকভাবে সর্ট হবে। date-ও str। এখানে সেটি কোনো সমস্যা নয়, এবং কারণটি জানা থাকা ভালো: YYYY-MM-DD ফরম্যাটে লেখা তারিখ টেক্সট হিসেবেও সঠিকভাবে সর্ট হয়, কারণ সবচেয়ে গুরুত্বপূর্ণ অংশটি (বছর) প্রথমে থাকে। 4/3/2024 ফরম্যাটে থাকলে তা হতো না। আর quantity-তে কোনো ফাঁকা মান নেই, তাই মিসিং ভ্যালু কোথায় যাবে তা এখনো ভাবতে হচ্ছে না।
৩. কাঙ্ক্ষিত আউটপুটের একটি খসড়া তৈরি করুন। "পরিমাণের ভিত্তিতে শীর্ষ তিন"-এর ক্ষেত্রে উত্তর হওয়া উচিত তিনটি সারি, বড় থেকে ছোট: ৩০ (ইরেজার, অর্ডার 1005), তারপর ২০ (কলম, 1007), তারপর ১২ (কলম, 1001)। কোনো কোড চালানোর আগেই এটি লিখে রাখলে কোডের ফলাফল মিলিয়ে দেখার একটি মাপকাঠি পাওয়া যায়।
৪. সর্টের জন্য প্রয়োজনীয় বিস্তারিত বিষয়গুলো ঠিক করুন।
- Key (কি) — কোন কলাম বা কলামগুলো ক্রম নির্ধারণ করবে? এটি যাবে
by=আর্গুমেন্টে। - Direction (দিক) — প্রতিটি কি-এর জন্য ছোট থেকে বড়, নাকি বড় থেকে ছোট? এটি যাবে
ascending=আর্গুমেন্টে। - Ties (টাই) — দুটি সারির মান সমান হলে কোনটি আগে আসবে? এটি দ্বিতীয় কি হিসেবে যাবে, অথবা
kind="stable"হবে। - Blanks (ফাঁকা মান) — মিসিং ভ্যালু কি শুরুতে যাবে নাকি শেষে? এটি যাবে
na_position=আর্গুমেন্টে। - How many (কতগুলো সারি) — সব সারি, নাকি শুধু শীর্ষ N? এটি হবে
.head(n), অথবাnlargest()। - Index (ইনডেক্স) — সর্ট করার পর কি পুরোনো সারির লেবেলগুলোর কোনো গুরুত্ব থাকবে? তাদের রেখে দিন, অথবা
reset_index(drop=True)দিয়ে ফেলে দিন।
৫. সঠিক টুল বেছে নিন।
- এক বা একাধিক কলামের ক্রমানুসারে সব সারি:
sort_values() - নিউমেরিক কলামের ভিত্তিতে কেবল শীর্ষ বা সর্বনিম্ন N:
nlargest()/nsmallest() - সারিগুলোকে আগের ইনডেক্স ক্রমে ফিরিয়ে নেওয়া:
sort_index() - কোনো সাবসেটের ভেতরের শীর্ষ N ("north ব্রাঞ্চে"): আগে ফিল্টার, তারপর সর্ট, তারপর
head()
৬. ফলাফল কীভাবে যাচাই করবেন তা জেনে রাখুন। সর্টের পর প্রথম সারিগুলো আপনার খসড়ার সাথে মিলতে হবে, shape অপরিবর্তিত থাকতে হবে (সর্ট কখনো সারি যোগ বা বিয়োগ করে না), এবং নিউমেরিক কি-এর ক্ষেত্রে কলামের মান পৃষ্ঠার ওপর থেকে নিচে ধারাবাহিকভাবে একক অভিমুখে পরিবর্তিত হতে হবে।
sort_values() — একটি কলাম
প্রিন্টআউটটি সংক্ষিপ্ত রাখতে উদাহরণগুলোতে সাতটি কলামের মধ্যে পাঁচটি দেখানো হয়েছে। cols হলো কলামের নামের একটি তালিকা, যা চতুর্থ অধ্যায়ের ডাবল-ব্র্যাকেট নির্বাচনের নিয়মে ব্যবহার করা হয়েছে।
cols = ["order_id", "branch", "product", "quantity", "price"]
by_qty = orders.sort_values("quantity")
print(by_qty[cols])order_id branch product quantity price
5 1006 south bag 1 850.0
2 1003 north bag 2 850.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
3 1004 east bottle 7 120.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0ডিফল্ট দিক হলো ascending — অর্থাৎ ছোট থেকে বড়। quantity কলাম বরাবর নিচে চোখ বুলান: 1, 2, 4, 5, 7, 12, 20, 30। এটাই পরিকল্পনার সেই যাচাই: একটি নিউমেরিক কি সর্বদা কেবল ওপরের দিকেই বাড়বে।
এখন বাঁ দিকের সীমানাটি দেখুন। ইনডেক্স আর 0, 1, 2, … নেই। এটি দেখাচ্ছে 5, 2, 7, 1, 3, 0, 6, 4।
কেন ইনডেক্স সারির সাথেই থাকে
পান্ডাস সারিগুলোর নতুন করে নাম্বারিং করেনি, এবং এটি ইচ্ছাকৃত। একটি ইনডেক্স লেবেল হলো সারির নাম, তার অবস্থান নয়। লেবেল 5 সর্ট করার আগেও ছিল অর্ডার 1006, এবং সর্ট করার পরও এটি অর্ডার 1006। সর্ট করলে যদি সারিগুলোর নতুন নাম্বারিং হতো, তবে প্রতিটি লেবেল হঠাৎ ভিন্ন কোনো অর্ডারকে নির্দেশ করত, এবং "৫ নম্বর সারি" সম্পর্কে আপনার যেকোনো নোট নীরবে ভুল হয়ে যেত।
তাই সর্ট করার পর "প্রথম সারি" বলার দুটি ভিন্ন উপায় তৈরি হয়, এবং তারা আর এক থাকে না:
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])1006
1001iloc[0] মানে শূন্যতম অবস্থান — পৃষ্ঠার একদম প্রথম সারি, অর্থাৎ অর্ডার 1006। আর loc[0] মানে 0 লেবেলযুক্ত সারিটি — অর্থাৎ অর্ডার 1001, যা এখন পৃষ্ঠার ষষ্ঠ স্থানে রয়েছে। চতুর্থ অধ্যায়ে এই পার্থক্যটি তুলে ধরা হয়েছিল; সর্টিং হলো সেই অপারেশন যা এই পার্থক্যটিকে অত্যন্ত গুরুত্বপূর্ণ করে তোলে। সর্ট করার পর যখন আপনি "শীর্ষ সারি" চাইবেন, তখন iloc অথবা head() ব্যবহার করুন, কারণ এরা উভয়ই অবস্থান গুনে কাজ করে।
বড় থেকে ছোট: ascending=False
print(orders.sort_values("quantity", ascending=False)[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0শীর্ষ তিনটি হলো 1005, 1007 এবং 1001 — হুবহু আমাদের পরিকল্পনার খসড়ার মতো। কেবল এই তিনটি অর্ডার পাঠাতে চাইলে শেষে head(3) যোগ করুন:
top3 = orders.sort_values("quantity", ascending=False).head(3)
print(top3[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0চেইনটি বাঁ দিক থেকে ডানে পড়লে পরিকল্পনাটি স্পষ্ট হয়ে ওঠে: orders নিন, সবচেয়ে বড় পরিমাণকে প্রথমে রাখুন, প্রথম তিনটি রেখে দিন। এই "সর্ট, তারপর head" প্যাটার্নটি বেশিরভাগ "শীর্ষ N" প্রশ্নের উত্তর দেয়।
টেক্সট বর্ণানুক্রমিকভাবে সর্ট হয়
একটি str কলাম অভিধানের ক্রমানুসারে সর্ট হয়:
print(orders.sort_values("product")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
4 1005 north eraser 30 8.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0bag এসেছে bottle-এর আগে কারণ a আসে o-এর আগে। দুটি ব্যাগ, দুটি বোতল এবং দুটি কলম — এগুলো হলো টাই (ties), এবং সমান হওয়া দুটি উপাদানের কোনটি আগে আসবে সেই প্রশ্নে আমরা একটু পরেই ফিরব।
সর্ট একটি নতুন টেবিল রিটার্ন করে
এই ভুলটি প্রায় সবাই জীবনে অন্তত একবার করে, এবং এর ফলে কোনো এররও দেখা দেয় না:
orders.sort_values("quantity", ascending=False)
print(orders[cols].head(3))order_id branch product quantity price
0 1001 north pen 12 15.0
1 1002 south notebook 5 60.0
2 1003 north bag 2 850.0কিছুই সর্ট হয়নি। sort_values() মূল orders-কে পরিবর্তন করে না; এটি নতুন ক্রমে একটি নতুন DataFrame তৈরি করে তা রিটার্ন করে। প্রথম লাইনটি সেই সর্ট করা টেবিল তৈরি করেছিল এবং কেউ তা সংরক্ষণ না করায় ফেলে দিয়েছিল। orders ঠিক তেমনই রয়ে গেছে যেমনটি read_csv তৈরি করেছিল।
পান্ডাস কেন এভাবে কাজ করে? কারণ মূল ক্রমটিও একটি গুরুত্বপূর্ণ তথ্য। orders.csv-তে এটি হলো অর্ডারগুলো যে ক্রমে এসেছিল সেই ক্রম। প্রতিবার সর্ট করলে যদি আপনার একমাত্র কপিটিই ওলটপালট হয়ে যেত, তবে ফাইলটি শুরুর দিকে কেমন ছিল তা আর কখনোই ফিরে পেতেন না, এবং একটি রিপোর্টের জন্য লেখা সর্ট নীরবে পরবর্তী রিপোর্টের ইনপুট বদলে দিত। একটি নতুন অবজেক্ট রিটার্ন করার অর্থ হলো সর্ট করে পরীক্ষা-নিরীক্ষা চালানো সবসময় নিরাপদ।
sort_values() কখনোই যে টেবিলের ওপর কল করা হয়েছে তাকে পরিবর্তন করে না। ফলাফল কোনো ভেরিয়েবলে না রাখলে সর্ট সম্পন্ন হয়ে সঙ্গে সঙ্গে বাতিল হয়ে যায় — এবং কোনো সতর্কবার্তাও আসে না।তাই ফলাফলটি সংরক্ষণ করুন, নতুন কোনো নামে অথবা আগের একই নামে:
by_qty_desc = orders.sort_values("quantity", ascending=False) # keep both
print(by_qty_desc["order_id"].head(3).tolist())
print(orders["order_id"].head(3).tolist())[1005, 1007, 1001]
[1001, 1002, 1003]সর্ট করা কপিতে বড় অর্ডারগুলো ওপরে রয়েছে; মূল টেবিলটি অক্ষত। orders = orders.sort_values(...) লেখাও পুরোপুরি ঠিক আছে, যখন আপনি নিশ্চিত যে পুরোনো ক্রমটি আপনার আর প্রয়োজন নেই।
পুরোনো কোডে আপনি হয়তো inplace=True দেখতে পাবেন। এটি টেবিলে সরাসরি পরিবর্তন ঘটায় এবং None রিটার্ন করে, যা নিজস্ব বাগ তৈরি করে (দেখুন কিছু ভাঙা অবস্থা ও তার সমাধান)। তাছাড়া পান্ডাস ৩-এর Copy-on-Write বৈশিষ্ট্যের কারণে inplace=True কোনো মেমোরিও সাশ্রয় করে না। ফলাফল অ্যাসাইন করা অনেক বেশি পরিষ্কার: লাইনের দিকে তাকালেই বোঝা যায় সর্ট করা টেবিলটি কোথায় যাচ্ছে।
একাধিক কলাম: অগ্রাধিকার এবং দিক
মালিকের দ্বিতীয় অনুরোধ ছিল: "ব্রাঞ্চ অনুযায়ী গ্রুপ করা, প্রতি ব্রাঞ্চের ভেতরে সবচেয়ে বড়টি আগে।" এটি দুটি কি, এবং তালিকার ক্রমটিই হলো অগ্রাধিকারের (priority) ক্রম:
by_branch = orders.sort_values(["branch", "quantity"], ascending=[True, False])
print(by_branch[cols])order_id branch product quantity price
6 1007 east pen 20 15.0
3 1004 east bottle 7 120.0
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
1 1002 south notebook 5 60.0
5 1006 south bag 1 850.0পান্ডাস যেভাবে টেবিলটি তৈরি করেছে সেভাবে পড়ুন। প্রথমে প্রতিটি সারি branch অনুযায়ী সাজানো হয়েছে, A থেকে Z: east, north, south। দ্বিতীয় কি, quantity, কেবল তখনই বিবেচনা করা হয়েছে যখন প্রথম কি-তে টাই হয়েছে — east-এর ভেতরে, north-এর ভেতরে, south-এর ভেতরে। north-এর ভেতরে এটি এসেছে 30, 12, 4, 2: বড় থেকে ছোট, কারণ ascending-এর দ্বিতীয় এন্ট্রিটি হলো False।
ascending প্রতিটি কি-এর জন্য একটি করে True/False নেয়, ঠিক by-এর মতো একই ক্রমে। শুধু একটি False দিলে তা উভয় কি-এর ওপর প্রযোজ্য হতো এবং south আগে চলে আসত।
তালিকার কলাম অদলবদল করলে পুরো প্রশ্নটিই বদলে যায়:
print(orders.sort_values(["quantity", "branch"], ascending=[False, True])[cols].head(4))order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0এখন পরিমাণের জয়জয়কার, আর branch কেবল তখনই কাজে আসত যদি দুটি অর্ডারের পরিমাণ সমান হতো — যা এখানে কারও নেই। ব্রাঞ্চগুলো ছড়িয়ে-ছিটিয়ে গেছে। তাই তালিকা লেখার আগে যে প্রশ্নটি করতে হবে তা হলো: "রিপোর্টটি কিসের ভিত্তিতে সাজানো?" সেই কলামটি প্রথমে বসবে।
টাই: ক্রম যেন একটি সিদ্ধান্ত হয়, কোনো দুর্ঘটনা নয়
price দিয়ে সর্ট করুন, সবচেয়ে দামিটি আগে:
print(orders.sort_values("price", ascending=False)[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
7 1008 north bottle 4 120.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0১২০ দামের দুটি বোতল লক্ষ্য করুন। ফাইলে অর্ডার 1004 এসেছে 1008-এর আগে। কিন্তু সর্ট করা টেবিলে 1008 আগে এসেছে। কোনো কিছু নষ্ট হয়ে যায়নি: একটিমাত্র কি-এর ক্ষেত্রে পান্ডাসের ডিফল্ট সর্টিং অ্যালগরিদম (quicksort) টাই হওয়া সারিগুলোকে মূল ফাইলে থাকা ক্রমে রাখার কোনো নিশ্চয়তা দেয় না, এবং এখানেও তা দেয়নি। ৮৫০ দামের দুটি ব্যাগ ফাইলের ক্রম বজায় রাখলেও দুটি বোতল জায়গা অদলবদল করেছে — এমন কোনো নির্দিষ্ট নিয়ম নেই যা দিয়ে আপনি আগে থেকে এটি অনুমান করতে পারতেন।
সাধারণত এতে কিছু যায় আসে না। সমস্যাটা শুরু হয় ঠিক যে মুহূর্তে আপনি তালিকায় কাট-অফ টানেন। রিপোর্টটি যদি হয় "সবচেয়ে দামি তিনটি অর্ডার", তবে তৃতীয় স্থানটি হলো 1004 ও 1008-এর মধ্যে একটি টাই, এবং কোনটি তালিকায় জায়গা পাবে তা নির্ধারণ করেছে অ্যালগরিদম, আপনি নন। একটু ভিন্ন কোনো ফাইলে একই কোড চালালে উত্তর বদলে যেতে পারে।
সেই সিদ্ধান্ত নিজের হাতে ফিরিয়ে আনার দুটি উপায় রয়েছে।
বিকল্প ১: স্টেবল সর্ট (stable sort)। kind="stable" নিশ্চয়তা দেয় যে টাই হওয়া সারিগুলো সর্ট করার আগের ক্রম বজায় রাখবে — এখানে, ফাইলের ক্রম:
stable = orders.sort_values("price", ascending=False, kind="stable")
print(stable["order_id"].tolist())[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]বিকল্প ২ (সাধারণত উত্তম): সুস্পষ্ট টাই-ব্রেকার। টাই হলে কে জিতবে তা স্পষ্টভাবে বলে দিন। একটি সেলস রিপোর্টের জন্য, "দাম সমান হলে বড় অর্ডারটি আগে আসবে" একটি যুক্তিসঙ্গত নিয়ম:
tie_broken = orders.sort_values(["price", "quantity"], ascending=[False, False])
print(tie_broken[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
4 1005 north eraser 30 8.0বোতল দুটি আবার দেখুন: 1004 এখন 1008-এর ওপরে, কারণ ৭ হারিয়ে দিয়েছে ৪-কে। আর ১৫ দামের দুটি কলম: আগে 1001 প্রথমে এসেছিল; এখন 1007 জিতে গেছে, কারণ ২০ হারিয়ে দিয়েছে ১২-কে — এবং যেকোনো মেশিনে, ফাইল যে ক্রমেই আসুক না কেন, এটি প্রতিবারই জিতবে। একটি টাই-ব্রেকার দুর্ঘটনাকে এমন একটি নিয়মে পরিণত করে যা আপনি মালিকের কাছে ব্যাখ্যা করতে পারবেন। (একাধিক কলাম দিয়ে সর্ট করার সময় পান্ডাসের সর্ট এমনিতেই স্টেবল থাকে, তাই কি-এর একটি তালিকা কখনোই আসল টাই-কে ওলটপালট করে না।)
যদি কোনো ব্যবসায়িক নিয়ম না থাকে, তবে order_id একটি চমৎকার শেষ কি হতে পারে: এটি ইউনিক বা অনন্য, তাই এর পরে আর কোনো টাই অবশিষ্ট থাকে না।
মিসিং ভ্যালু: na_position
দুটি ফাঁকা পরিমাণ থাকা ফাইলের সংস্করণটি পড়ুন:
gaps = pd.read_csv("orders_gaps.csv")
print(gaps["quantity"].isna().sum())
print(gaps.sort_values("quantity", ascending=False)[cols])2
order_id branch product quantity price
4 1005 north eraser 30.0 8.0
0 1001 north pen 12.0 15.0
3 1004 east bottle 7.0 120.0
7 1008 north bottle 4.0 120.0
2 1003 north bag 2.0 850.0
5 1006 south bag 1.0 850.0
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0দুটি বিষয় লক্ষ্য করার মতো। প্রথমত, quantity এখন 30.0, 12.0 হিসেবে প্রিন্ট হচ্ছে — ষষ্ঠ অধ্যায়ে যেমনটি ব্যাখ্যা করা হয়েছিল, ফাঁকা মান কলামটিকে float64-এ পরিণত করেছে, কারণ NaN একটি ফ্লোট। দ্বিতীয়ত, সবচেয়ে বড় মান আগে চাওয়া সত্ত্বেও NaN সারিগুলো চলে গেছে সবার নিচে। এটাই হলো ডিফল্ট আচরণ, na_position="last", এবং এটি উভয় দিকেই প্রযোজ্য: একটি মিসিং ভ্যালু "ছোট" বা "বড়" নয়, এটি অজ্ঞাত; তাই অনুমান করার চেয়ে পান্ডাস এটিকে একপাশে সরিয়ে রাখে।
লক্ষ্য করুন শীর্ষ তিনের ক্ষেত্রে এর কী প্রভাব পড়েছে। অর্ডার 1007-এ আসলে ২০টি কলম বিক্রি হয়েছিল, কিন্তু এই ফাইলে তার পরিমাণ ফাঁকা থাকায় এটি শীর্ষ তিন থেকে ছিটকে পড়েছে এবং 1004 তার জায়গা নিয়েছে। সর্ট কখনো মিসিং ডেটা ঠিক করতে পারে না; এটি কেবল দেখাতে পারে ফাঁকা মানগুলো কোথায় গেছে। এজন্যই পরিকল্পনার ২ নম্বর ধাপে সর্ট করার আগে ফাঁকা মান গুনে নিতে বলা হয়েছিল।
ডেটা যখন নিরীক্ষা (audit) করবেন, তখন আপনি প্রায়ই এর বিপরীতটি চাইবেন — সমস্যাগুলো যেন সবার ওপরে থাকে, যাতে চোখে পড়ে:
print(gaps.sort_values("quantity", na_position="first")[cols].head(3))order_id branch product quantity price
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0
5 1006 south bag 1.0 850.0সুতরাং na_position কেবল কোনো ফরম্যাটিংয়ের বিষয় নয়। "last" রিপোর্টের জন্য উপযুক্ত, যেখানে ফাঁকা মানগুলো যেন আসল মানগুলোকে আড়াল না করে। আর "first" ডেটা যাচাইয়ের জন্য উপযুক্ত, যেখানে ফাঁকা মানগুলোই হলো আপনার অনুসন্ধানের মূল লক্ষ্য।
শুরুতে ফিরে যাওয়া: sort_index() এবং reset_index()
যেহেতু প্রতিটি সারি তার লেবেল ধরে রেখেছিল, তাই মূল ক্রমটি কখনো হারিয়ে যায় না। ইনডেক্স অনুযায়ী সর্ট করলে তা ফিরে আসে:
restored = by_qty.sort_index()
print(restored["order_id"].tolist())[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]sort_index()-ও ascending=False সমর্থন করে, যার মাধ্যমে আপনি "নতুন লেবেল আগে" রেখে একটি টেবিল তালিকাভুক্ত করতে পারেন।
কখনো কখনো পুরোনো লেবেলগুলোর আর কোনো অর্থ থাকে না। একটি র্যাংকিং তালিকা তৈরি করে কাউকে পাঠাতে চাইলে পাশে 4, 6, 0 লেবেল থাকাটা বিভ্রান্তিকর। reset_index(drop=True) সেগুলো ফেলে দেয় এবং সারিগুলোকে তাদের নতুন ক্রমে 0, 1, 2, … দিয়ে নতুন নম্বর দেয়:
ranked = orders.sort_values("quantity", ascending=False).reset_index(drop=True)
print(ranked[cols].head(3))
print(ranked.loc[0, "order_id"])order_id branch product quantity price
0 1005 north eraser 30 8.0
1 1007 east pen 20 15.0
2 1001 north pen 12 15.0
1005এখন loc[0] এবং iloc[0] আবার একই সারিতে মিলে যায়: উভয়েই অর্ডার 1005, সবচেয়ে বড়টি। drop=True ছাড়া পান্ডাস পুরোনো লেবেলগুলোকে index নামের একটি নতুন কলাম হিসেবে রেখে দিত — আপনি যদি সারিগুলোকে মূল উৎসের সাথে মেলাতে চান তবে এটি দরকারি, আর অপ্রয়োজনীয় জঞ্জাল এড়াতে চাইলে বাদ দেওয়া ভালো।
কোনটি বেছে নেবেন? আপনি যখন ডেটা নিয়ে কাজ করছেন এবং সারিগুলোকে মূল ডেটার সাথে মেলানোর প্রয়োজন হতে পারে, তখন লেবেল রেখে দিন। আর যখন ক্রমটি নিজেই চূড়ান্ত ফলাফল — যেমন র্যাংকিং বা শীর্ষ দশের তালিকা — তখন তাদের রিসেট করুন।
"শীর্ষ N"-এর সংক্ষিপ্ত উপায়: nlargest() এবং nsmallest()
"পরিমাণের ভিত্তিতে শীর্ষ তিন" এতোটাই নিয়মিত প্রয়োজন হয় যে পান্ডাসে এমন একটি মেথড রয়েছে যা ঠিক এই কথাটিই বলে:
print(orders.nlargest(3, "quantity")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0এটি হুবহু sort_values("quantity", ascending=False).head(3)-এর মতো একই সারি দেয়। তাহলে এটি কেন ব্যবহার করবেন? লাইনটি মেকানিজমের বদলে মূল উদ্দেশ্যটি প্রকাশ করে — "সবচেয়ে বড় তিনটি"। আর একটি বড় টেবিলে এটি দ্রুত কাজ করে, কারণ কেবল তিনটি সারি রাখার জন্য এটিকে সব সারি সাজাতে হয় না।
nsmallest() হলো এর ঠিক বিপরীত: দোকানের সবচেয়ে সস্তা তিনটি আইটেম:
print(orders.nsmallest(3, "price")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0কাট-অফে টাই হওয়া: keep=
সবচেয়ে দামি তিনটি আইটেম চাইলে একটি অদ্ভুত পরিস্থিতি তৈরি হয়:
print(orders.nlargest(3, "price")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0তৃতীয় স্থানের দাম ১২০ — কিন্তু দুটি অর্ডারের দাম ১২০, 1004 এবং 1008। পান্ডাস তাদের একটি ফিরিয়ে দিল — ডিফল্ট keep="first" অনুযায়ী টেবিলে যেটি আগে এসেছিল — এবং অপরটিকে নীরবে বাদ দিল। আপনি যদি "সেরা তিন"-কে পুরস্কার দেন, তবে এটি 1008-এর প্রতি অন্যায়। keep="all" কাট-অফে টাই হওয়া প্রতিটি সারি রেখে দেয়, এমনকি তার ফলে সারির সংখ্যা N-এর চেয়ে বেশি হলেও:
print(orders.nlargest(3, "price", keep="all")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0"সেরা তিন"-এর জন্য চারটি সারি। এটি কোনো বাগ নয়: যখন তৃতীয় স্থানটি যৌথভাবে অর্জিত হয়, তখন এটাই সৎ উত্তর। আপনি এটি চান কিনা তা আবারও একটি পরিকল্পনামূলক সিদ্ধান্ত।
nlargest এবং nsmallest কেবল নিউমেরিক কলামের ওপর কাজ করে। টেক্সটের জন্য sort_values এবং head ব্যবহার করুন।
কোনো সাবসেটের শীর্ষ N: আগে ফিল্টার, তারপর সর্ট, তারপর কাট
"north ব্রাঞ্চের সবচেয়ে বড় দুটি অর্ডার কী কী ছিল?" এটি পঞ্চম অধ্যায়ের সাথে এই অধ্যায়ের সমন্বয় ঘটায়, এবং ধাপগুলোর ক্রম অত্যন্ত গুরুত্বপূর্ণ:
north = orders[orders["branch"] == "north"]
top_north = north.sort_values("quantity", ascending=False).head(2)
print(top_north[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0প্রথমে ফিল্টার করুন, কারণ প্রশ্নটি কেবল north ব্রাঞ্চকে নিয়ে। পুরো টেবিল সর্ট করে প্রথমে head(2) নিলে আপনি পেতেন 1005 এবং 1007 — আর 1007 হলো east ব্রাঞ্চের। পরে ফিল্টার করলে কেবল একটি সারি অবশিষ্ট থাকত, এবং আপনি একটি এন্ট্রি দিয়ে "শীর্ষ দুই"-এর রিপোর্ট দিতেন। নিয়মটি মনে রাখবেন: যেসব সারি নিয়ে প্রশ্ন কেবল সেগুলোতে সংকুচিত করুন, তারপর তাদের র্যাংক করুন, তারপর কাটুন।
একই কাজ nlargest-এর চেইন দিয়েও করা যায়: orders[orders["branch"] == "north"].nlargest(2, "quantity")।
ছোট-বড় হাতের অক্ষরের ভেদাভেদ ছাড়াই টেক্সট সর্ট করা: key=
টেক্সট সর্টিং ক্যারেক্টার কোডের ভিত্তিতে অক্ষর তুলনা করে, এবং প্রতিটি বড় হাতের অক্ষর প্রতিটি ছোট হাতের অক্ষরের আগে আসে। মানুষের টাইপ করা ডেটায় এই দুইয়ের মিশ্রণ থাকে:
names = pd.DataFrame({"product": ["pen", "Notebook", "bag", "Bottle", "eraser"]})
print(names.sort_values("product"))product
3 Bottle
1 Notebook
2 bag
4 eraser
0 penBottle এবং Notebook কেবল বড় হাতের অক্ষর দিয়ে শুরু হওয়ার কারণে bag-এর আগে চলে এসেছে। কোনো পাঠক একে বর্ণানুক্রমিক বলবেন না।
আপনি ডেটা ঠিক করতে পারতেন, কিন্তু অনেক সময় তা করা উচিত নয় — গ্রাহক যেভাবে লিখেছিলেন বানানটি হয়তো সেভাবেই রাখতে হবে। key= আর্গুমেন্ট মূল মান অক্ষুণ্ণ রেখেই কলামের একটি রূপান্তরিত (transformed) সংস্করণের ভিত্তিতে সর্ট করার সুবিধা দেয়। এটি এমন একটি ফাংশন গ্রহণ করে যা পুরো কলামটি পায় এবং সর্ট করার জন্য রূপান্তরিত সংস্করণটি রিটার্ন করে:
print(names.sort_values("product", key=lambda col: col.str.lower()))product
2 bag
3 Bottle
4 eraser
1 Notebook
0 penছোট হাতের সংস্করণটি কেবল তুলনার জন্য ব্যবহৃত হয় এবং পরে ফেলে দেওয়া হয়। টেবিলে এখনও Bottle এবং Notebook হুবহু সেভাবেই দেখাচ্ছে যেভাবে টাইপ করা হয়েছিল। (lambda col: col.str.lower() হলো একটি এক লাইনের ফাংশন: কলাম col দিলে এটি তা ছোট হাতের অক্ষরে রূপান্তর করে রিটার্ন করে।)
যখন সংখ্যা টেক্সট হিসেবে সংরক্ষিত থাকে
এখানেই সেই ফাঁদ যা ধরার জন্য dtypes-এর পরিকল্পনামূলক ধাপটি তৈরি করা হয়েছে। ধরা যাক quantity টেক্সট হিসেবে এসেছে — ফাইলে হয়তো একটি অনিচ্ছাকৃত "N/A" ছিল, অথবা এমন কোনো সিস্টেম যা সবকিছু স্ট্রিং হিসেবে রপ্তানি করে। এটি কী ঘটাতে পারে তা দেখতে astype দিয়ে ইচ্ছাকৃতভাবে এমন একটি কলাম তৈরি করা যাক:
as_text = orders.astype({"quantity": "str"})
print(as_text["quantity"].dtype)
print(as_text.sort_values("quantity")[["order_id", "quantity"]])str
order_id quantity
5 1006 1
0 1001 12
2 1003 2
6 1007 20
4 1005 30
7 1008 4
1 1002 5
3 1004 71, 12, 2, 20, 30, 4, 5, 7। কোনো এরর নেই, এবং দেখতে প্রায় ঠিকঠাক একটি ক্রম। টেক্সট অক্ষর ধরে ধরে তুলনা করা হয়: "12" শুরু হয় 1 দিয়ে, যা 2-এর চেয়ে ছোট; তাই ডিকশনারিতে "ab" যেমন "b"-এর আগে আসে, ঠিক তেমনি "12" আসে "2"-এর আগে। সর্টকে যা বলা হয়েছিল সে ঠিক তাই করেছে। কিন্তু তাকে ভুল জিনিস বলা হয়েছিল।
দুটি লক্ষণ এটি ধরিয়ে দেয়: প্রিন্টআউটে quantity বাঁয়ে সারিবদ্ধ রয়েছে, যেভাবে পান্ডাস টেক্সট প্রিন্ট করে, এবং dtype দেখাচ্ছে str। আসল সমাধান হলো কলামটিকে সংখ্যায় রূপান্তর করা (ষষ্ঠ অধ্যায়ের pd.to_numeric)। কলামটিকে যদি আগের মতোই রাখতে হয়, তবে key= ব্যবহার করে নিউমেরিক মানের ভিত্তিতে সর্ট করা যায়:
fixed = as_text.sort_values("quantity", key=pd.to_numeric)
print(fixed["quantity"].tolist())['1', '2', '4', '5', '7', '12', '20', '30']এখন সঠিক ক্রম এসেছে — তবে মানগুলো এখনও স্ট্রিং, তাই আপনি তাদের যোগ করতে পারবেন না। ফাইল পড়ার পরপরই একবার কলামটি রূপান্তর করে নেওয়া সবচেয়ে ভালো অভ্যাস। তাহলে পরবর্তী প্রতিটি সর্ট, ফিল্টার এবং যোগফল কোনো কিছু মনে রাখার ঝামেলা ছাড়াই সঠিকভাবে কাজ করবে।
একটা সম্পূর্ণ উদাহরণ
মালিকের পুরো অনুরোধটির উত্তর একটি প্রোগ্রামে: (১) অর্জিত আয়ের ভিত্তিতে শীর্ষ তিনটি অর্ডার, (২) নতুন অর্ডারগুলো আগে, (৩) ব্রাঞ্চভিত্তিক তালিকা যেখানে প্রতিটি ব্রাঞ্চে সবচেয়ে বড় অর্ডারটি আগে থাকবে।
"অর্জিত আয়" হলো পরিমাণ গুণ দাম। কলাম যোগ করার বিষয়টি কোর্সের পরবর্তী অংশে বিশদভাবে আলোচনা করা হবে; আপাতত একটি লাইনই যথেষ্ট — একটি নতুন কলামের নামে অ্যাসাইন করলেই তা তৈরি হয়ে যায়।
sorted_report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check the input before ranking anything
print("Rows, columns:", orders.shape)
print("Blank quantities:", orders["quantity"].isna().sum())
print()
orders["revenue"] = orders["quantity"] * orders["price"]
cols = ["order_id", "date", "branch", "product", "revenue"]
# 1. Top three by revenue, ties broken by the larger quantity, renumbered as a ranking
top3 = (
orders.sort_values(["revenue", "quantity"], ascending=[False, False])
.head(3)
.reset_index(drop=True)
)
print("Top three orders by revenue")
print(top3[cols])
print()
# 2. Newest first; within the same day, the bigger order first
newest = orders.sort_values(["date", "revenue"], ascending=[False, False])
print("Newest first")
print(newest[cols].head(4))
print()
# 3. By branch A-Z, biggest order first inside each branch
by_branch = orders.sort_values(["branch", "revenue"], ascending=[True, False])
print("By branch")
print(by_branch[cols])
print()
print("Still eight rows:", by_branch.shape[0] == orders.shape[0])Rows, columns: (8, 7)
Blank quantities: 0
Top three orders by revenue
order_id date branch product revenue
0 1003 2024-03-02 north bag 1700.0
1 1006 2024-03-03 south bag 850.0
2 1004 2024-03-02 east bottle 840.0
Newest first
order_id date branch product revenue
7 1008 2024-03-04 north bottle 480.0
6 1007 2024-03-04 east pen 300.0
5 1006 2024-03-03 south bag 850.0
4 1005 2024-03-03 north eraser 240.0
By branch
order_id date branch product revenue
3 1004 2024-03-02 east bottle 840.0
6 1007 2024-03-04 east pen 300.0
2 1003 2024-03-02 north bag 1700.0
7 1008 2024-03-04 north bottle 480.0
4 1005 2024-03-03 north eraser 240.0
0 1001 2024-03-01 north pen 180.0
5 1006 2024-03-03 south bag 850.0
1 1002 2024-03-01 south notebook 300.0
Still eight rows: Trueকেন এটি এভাবে লেখা হয়েছে:
- যাচাই আগে করা হয়েছে। কোনো র্যাংকিং করার আগেই
shapeএবং ফাঁকা মানের সংখ্যা প্রিন্ট করা হয়েছে। অর্ধেক পড়া ফাইল থেকে অথবা ফাঁকা মান নিচে ঠেলে দিয়ে তৈরি করা শীর্ষ তিন দেখতে সমান আত্মবিশ্বাসী মনে হতো। - "সবচেয়ে বড়"-কে একটি কলামে পরিণত করা হয়েছে।
quantityদিয়ে র্যাংক করলে ৩০টি ইরেজার সবার ওপরে থাকত — যা ২৪০ টাকার বিক্রি।revenueদিয়ে র্যাংক করলে ১,৭০০ টাকার দুটি ব্যাগ সবার ওপরে থাকে। পরিকল্পনার প্রথম ধাপ — কলামের নামসহ প্রশ্নটি লিখুন — ঠিক করে দেয় মালিক কোন উত্তরটি পাবেন। - প্রতিটি সর্টে একটি টাই-ব্রেকার রয়েছে। 2024-03-04 তারিখে দুটি অর্ডার রয়েছে;
revenueঠিক করে কোনটি আগে তালিকাভুক্ত হবে, ফলে প্রতিবার চালানোর সময় রিপোর্ট একই থাকে। - কেবল র্যাংকিংয়ের ইনডেক্স রিসেট করা হয়েছে।
top3একটি চূড়ান্ত তালিকা, তাই এর লেবেল হয়েছে 0, 1, 2।newestএবংby_branchতাদের মূল লেবেল ধরে রেখেছে, যাতে যেকোনো সারিকে মূল ফাইলের সাথে মিলিয়ে দেখা যায়। ordersনিজে কখনো পুনরায় সর্ট করা হয়নি। প্রতিটি রিপোর্ট একই অক্ষত ইনপুট থেকে তৈরি একটি নতুন টেবিল, তাই তিনটি উত্তর একে অপরকে প্রভাবিত করতে পারে না।- শেষ লাইনটি একটি সহজ অ্যাসারশন। সর্টের কারণে সারির সংখ্যা কখনোই বদলানো উচিত নয়।
Trueপ্রিন্ট করাতে কোনো খরচ নেই, কিন্তু এটি ভুল জায়গায় রয়ে যাওয়া কোনো অনিচ্ছাকৃতhead()সহজে ধরে ফেলতে পারে।
কিছু ভাঙা অবস্থা ও তার সমাধান
KeyError: 'Quantity'
orders.sort_values("Quantity")KeyError: 'Quantity'by=-এর নাম অবশ্যই কলামের নামের সাথে হুবহু মিলতে হবে — কেস, স্পেস সবকিছুসহ। কলামটির নাম হলো quantity, ছোট হাতের। তৃতীয় অধ্যায়ের পরামর্শ অনুযায়ী কোটেশন ও অনাকাঙ্ক্ষিত স্পেসসহ আসল নাম দেখতে list(orders.columns) প্রিন্ট করুন।
AttributeError: 'NoneType' object has no attribute 'head'
top = orders.sort_values("quantity", inplace=True)
top.head()AttributeError: 'NoneType' object has no attribute 'head'inplace=True দিলে sort_values সরাসরি orders-কে পরিবর্তন করে এবং None রিটার্ন করে। তাই top হলো None, আর None-এর কোনো head() নেই। যেকোনো একটি স্টাইল বেছে নিন: হয় নিজস্ব লাইনে orders.sort_values("quantity", inplace=True), অথবা — সবচেয়ে ভালো — inplace ছাড়া top = orders.sort_values("quantity")।
ValueError: Length of ascending (1) != length of by (2)
orders.sort_values(["branch", "quantity"], ascending=[False])ValueError: Length of ascending (1) != length of by (2)ascending-এর তালিকার প্রতিটি কি-এর জন্য একটি করে এন্ট্রি থাকা প্রয়োজন। দুটি কি, দুটি বুলিয়ান: ascending=[True, False]। (তালিকায় না রেখে শুধু একটি False দেওয়া বৈধ — তা প্রতিটি কি-এর ওপর প্রযোজ্য হয়।)
ValueError: For argument "ascending" expected type bool, received type str.
orders.sort_values("branch", ascending="False")ValueError: For argument "ascending" expected type bool, received type str.উদ্ধৃতি চিহ্নের ভেতরের "False" হলো একটি স্ট্রিং, বুলিয়ান False নয়। উদ্ধৃতি চিহ্নগুলো সরিয়ে দিন।
TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype
orders.nlargest(3, "branch")TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtypenlargest এবং nsmallest কেবল সংখ্যা র্যাংক করে। "বর্ণানুক্রমিকভাবে শেষ তিনটি ব্রাঞ্চ"-এর জন্য orders.sort_values("branch", ascending=False).head(3) ব্যবহার করুন। যে কলামটিকে নিউমেরিক মনে করেছিলেন তাতে যদি এই এরর আসে, তবে আসল সমস্যা হলো তা টেক্সট হিসেবে পড়া হয়েছে — dtypes পরীক্ষা করুন।
TypeError: '<' not supported between instances of 'str' and 'int'
mixed = pd.Series([12, "unknown", 5])
mixed.sort_values()TypeError: '<' not supported between instances of 'str' and 'int'এই কলামে আসল সংখ্যার পাশাপাশি টেক্সটও রয়েছে, তাই এর dtype হলো object, এবং পাইথন বলতে পারে না "unknown" কি 12-এর চেয়ে বড় নাকি ছোট। কলামটি আগে পরিষ্কার করুন: pd.to_numeric(mixed, errors="coerce") লিখলে "unknown" রূপান্তর হয়ে NaN হবে, এবং তখন সর্ট কাজ করবে ও na_position ঠিক করবে ফাঁকা মানটি কোথায় যাবে।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
দুটি ব্যাগেরই সর্বোচ্চ দাম ৮৫০.০। আউটপুট কী হবে?
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))
top = orders.sort_values("price", ascending=False)
print(list(top.index[:3]))- A[0, 1, 2]
- B[2, 5, 3]
- C[5, 2, 7]
- D[2, 5, 7]
উদ্দেশ্য হলো সবচেয়ে বেশি আইটেম থাকা অর্ডারটি প্রিন্ট করা। কী প্রিন্ট হবে?
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))
orders.sort_values("quantity", ascending=False)
print(orders.iloc[0]["order_id"])- A1005
- B1001 — `sort_values` একটি সর্ট করা কপি ফিরিয়ে দিয়েছিল যা কোথাও সংরক্ষণ করা হয়নি
- C1007
- Dএকটি `TypeError`: ফলাফল অবশ্যই কোনো ভেরিয়েবলে অ্যাসাইন করতে হবে
আপনি অর্ডারগুলোকে ব্রাঞ্চ অনুযায়ী A–Z ক্রমে এবং প্রতিটি ব্রাঞ্চের ভেতরে সবচেয়ে বড় পরিমাণ (quantity) আগে সাজাতে চান। কোন লাইনটি এটি করবে?
- Aorders.sort_values(["branch", "quantity"], ascending=False)
- Borders.sort_values(["quantity", "branch"], ascending=[False, True])
- Corders.sort_values("branch").sort_values("quantity", ascending=False)
- Dorders.sort_values(["branch", "quantity"], ascending=[True, False])
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
একজন শিক্ষক আপনাকে একটি পরীক্ষার নম্বর পাঠালেন। এটি results.csv নামে সংরক্ষণ করুন। দুজন শিক্ষার্থীর এখনও কোনো নম্বর নেই, এবং কিছু নাম ছোট হাতের অক্ষরে টাইপ করা হয়েছে:
name,section,marks
Rafi,B,78
anika,A,91
Tanvir,A,
Mitu,B,91
zara,A,65
Karim,B,
Nadia,A,88
Sabbir,B,54ranking.py লিখুন। কোনো কোড লেখার আগে ফাইলের শুরুতে কমেন্ট আকারে পরিকল্পনাটি লিখুন: প্রতিটি তালিকার জন্য কি কলাম, দিক, টাই কীভাবে ভাঙা হবে এবং ফাঁকা মান কোথায় যাবে। তারপর প্রোগ্রামটিকে এই চারটি শর্ত পূরণ করতে দিন:
- কোনো কিছু সর্ট করার আগে এটি
shape,dtypesএবং মিসিং নম্বরের সংখ্যা প্রিন্ট করবে। - এটি একটি মেধা তালিকা (merit list) প্রিন্ট করবে: সর্বোচ্চ নম্বর আগে, নম্বর ছাড়া শিক্ষার্থীরা সবার নিচে, নম্বরে টাই হলে ছোট-বড় হাতের অক্ষরের ভেদাভেদ ছাড়া নামের বর্ণানুক্রমে টাই ভাঙা হবে, এবং সারিগুলোকে 0, 1, 2, … দিয়ে নতুন নম্বর দেওয়া হবে।
- এটি
nlargestদিয়ে শীর্ষ তিন প্রিন্ট করবে, তৃতীয় স্থানে টাই হওয়া সবাইকে তালিকায় রেখে। - এটি নম্বর ছাড়া শিক্ষার্থীদের প্রিন্ট করবে ছোট-বড় হাতের অক্ষরের ভেদাভেদ ছাড়া নামের ক্রমানুসারে, যাতে শিক্ষক তাদের কাছে নম্বর চাইতে পারেন — এবং তারপর মূল টেবিলটি প্রিন্ট করবে, প্রমাণ করার জন্য যে এটি কখনো পরিবর্তিত হয়নি।
আপনার প্রোগ্রামের আউটপুট হুবহু এমন হওয়া উচিত:
(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0তারপর মেধা তালিকার ওপর দুটি পরীক্ষা চালান:
key=সরিয়ে দিন। ৯১ পাওয়া দুজন শিক্ষার্থীর মধ্যে এখন কে আগে আসে, এবং কেন?- কি-এর তালিকা থেকে
"name"পুরোপুরি সরিয়ে শুধুmarksদিয়ে সর্ট করুন। ৯১-তে কে আগে আসে — এবং এই ফলাফল কি নিশ্চিত?
সমাধান
পরিকল্পনা। ফাইলটিতে ৮ জন শিক্ষার্থী এবং ৩টি কলাম রয়েছে। marks-এ ফাঁকা মান রয়েছে, তাই এটি float64 হিসেবে পড়া হবে, এবং নম্বরের ভিত্তিতে সর্ট করা প্রতিটি তালিকায় ফাঁকা মান কোথায় যাবে তা স্পষ্ট থাকতে হবে। name-এ বড় ও ছোট হাতের অক্ষরের মিশ্রণ রয়েছে, তাই নামের প্রতিটি তুলনায় key= প্রয়োজন।
- মেধা তালিকা — কি হলো
marks, তারপরname; নম্বর বেশি থেকে কম, নাম A থেকে Z; নম্বরে টাই হলে নাম দিয়ে ভাঙা হবে; ফাঁকা মান শেষে (ডিফল্ট);reset_index(drop=True)দিয়ে নতুন নাম্বারিং। - শীর্ষ তিন —
keep="all"সহnlargest(3, "marks"), যাতে যৌথ তৃতীয় স্থান বাদ না পড়ে।nlargestনিজে থেকেই ফাঁকা মান বাদ দেয়। - এখনও নম্বর নেই — আগের অধ্যায়গুলোর মতো যেখানে
marksমিসিং সেই সারিগুলো ফিল্টার করুন, তারপর নাম অনুযায়ী সর্ট করুন। আগে ফিল্টার, তারপর সর্ট। - মূল টেবিল — কখনো পুনরায় অ্যাসাইন করা হয়নি, তাই শেষে
resultsপ্রিন্ট করলে ফাইলের ক্রম দেখায়।
একটি খুঁটিনাটি বিষয়: key= ফাংশনটি প্রতিটি কি কলামে প্রয়োগ করা হয়। মেধা তালিকার জন্য তার মানে এটি marks-এর ক্ষেত্রেও প্রয়োগ হবে, এবং সংখ্যার ওপর .str.lower() কাজ করবে না। তাই ফাংশনটি কেবল টেক্সট কলামকে লোয়ারকেস করে এবং অন্য যেকোনো কলামকে অপরিবর্তিত রাখে।
ranking.py:
import pandas as pd
# Merit list : marks high->low, ties by name A->Z ignoring case, blanks last, renumber
# Top three : nlargest on marks, keep="all" so a shared third place is not cut
# No mark yet : filter rows with no mark, then name A->Z ignoring case
# Original : never reassigned
results = pd.read_csv("results.csv")
print(results.shape)
print(results.dtypes)
print("Missing marks:", results["marks"].isna().sum())
print()
def lower_text(col):
return col.str.lower() if col.dtype == "str" else col
merit = results.sort_values(
["marks", "name"], ascending=[False, True], key=lower_text
).reset_index(drop=True)
print("Merit list")
print(merit)
print()
print("Top three")
print(results.nlargest(3, "marks", keep="all"))
print()
print("No mark yet")
missing = results[results["marks"].isna()]
print(missing.sort_values("name", key=lower_text))
print()
print("Original, unchanged")
print(results)(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0পরিকল্পনার সাথে মিলিয়ে আউটপুট পড়া:
marksহলোfloat64,int64নয়, এবংMissing marks: 2— কোনো র্যাংকিংয়ের আগেই ফাঁকা মানগুলো ধরা পড়েছে, তাই মেধা তালিকা যিনি পড়ছেন তিনি জানেন দুজন শিক্ষার্থী এখানে অনুপস্থিত, তারা তালিকার তলানিতে নেই।- ৯১-এর টাই নামের ভিত্তিতে নির্ধারিত হয়েছে।
anikaএসেছেMitu-র আগে কারণ তুলনাটি করা হয়েছিল"anika"এবং"mitu"-র মধ্যে। - শীর্ষ তিনে এখানে তিনটি সারি এসেছে, কারণ তৃতীয় স্থান (88) কারও সাথে ভাগাভাগি হয়নি। নদীয়ার নম্বরও যদি ৯১ হতো, তবে কোনো শিক্ষার্থীকে নীরবে বাদ না দিয়ে
keep="all"চারটি সারি ফিরিয়ে দিত। - নম্বরবিহীন তালিকা আগে ফিল্টার করেছে, তারপর সর্ট করেছে — সাবসেটের শীর্ষ N প্যাটার্নের মতোই সেই "আগে নির্দিষ্ট করুন, তারপর র্যাংক করুন" ক্রম।
- সর্বশেষ টেবিলটি হলো ফাইলের ক্রম। তিনটি তালিকা তৈরি করা হয়েছিল, এবং
resultsকখনো পুনরায় অ্যাসাইন করা হয়নি।
পরীক্ষা দুটি:
no_key = results.sort_values(["marks", "name"], ascending=[False, True])
marks_only = results.sort_values("marks", ascending=False)
print(no_key["name"].head(2).tolist())
print(marks_only["name"].head(2).tolist())['Mitu', 'anika']
['anika', 'Mitu']key= ছাড়া নামগুলো অক্ষর ধরে ধরে তুলনা করা হয়, এবং প্রতিটি বড় হাতের অক্ষর প্রতিটি ছোট হাতের অক্ষরের আগে আসে, তাই "Mitu" এগিয়ে যায় "anika"-র চেয়ে — একটি ক্রম যা নাম কীভাবে টাইপ করা হয়েছিল তার ওপর নির্ভরশীল, বর্ণমালার ওপর নয়। শুধু marks দিলে anika দৈবক্রমে আগে আসে কারণ সে ফাইলে আগে ছিল। এটি সঠিক মেধা তালিকার মতোই উত্তর, কিন্তু কেবল দুর্ঘটনাবশত: একক কি-এর ক্ষেত্রে ডিফল্ট সর্ট টাই সারিগুলোকে ফাইলের ক্রমে রাখার প্রতিশ্রুতি দেয় না, যেমনটি আগে ১২০ দামের দুটি বোতলের ক্ষেত্রে দেখা গেছে। আপনার নিজের লেখা নিয়ম নিশ্চিত ফলাফলের নিশ্চয়তা দেয়; যা কেবল দেখতে সঠিক মনে হয় তা নিশ্চিত নয়।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন