অধ্যায় 07

সারি সাজানো — এবং টাই হলে কী ঘটবে তা ঠিক করা

sort_values দিয়ে সারি সাজানো: এক বা একাধিক কলাম, প্রতিটির নিজস্ব দিক, মিসিং ডেটা ও সংখ্যার মতো দেখতে টেক্সট — সাথে "শীর্ষ N"-এর জন্য nlargest, এবং কাট-অফে টাই হওয়া কেন কোনো দুর্ঘটনা নয় বরং একটি সিদ্ধান্ত।

40 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 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 ফাইলটি পড়ে, যা চতুর্থ অধ্যায় থেকে ব্যবহার করা হচ্ছে। আপনার প্রজেক্ট ফোল্ডারে এটি এখনও না থাকলে হুবহু এই লাইনগুলো দিয়ে ফাইলটি তৈরি করে নিন:

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

পরের একটি সেকশনে দ্বিতীয় একটি ফাইল ব্যবহার করা হবে, orders_gaps.csv। এটি একই টেবিল, শুধু দুটি অর্ডারের পরিমাণ ফাঁকা রাখা হয়েছে — অর্ডার 1002 এবং 1007 — ঠিক যেমন ফাঁকা ষষ্ঠ অধ্যায়ে খুঁজে বের করতে শিখেছেন:

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,,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 থাকা তিনটি অর্ডার"-ও তাই। এরা দুজন ভিন্ন উত্তর দেয় — ৮৫০ দামের ব্যাগটি সবচেয়ে দামি আইটেম হলেও বিক্রি হয়েছে মাত্র দুটি ব্যাগ। মালিক কোনটি বুঝিয়েছেন তা আগে নিশ্চিত করুন। বুঝতে না পারলে জিজ্ঞেস করুন; এতে মাত্র একটি মেসেজ খরচ হবে, কিন্তু ভুল শীর্ষ তিন পাঠালে বিশ্বাস নষ্ট হবে।

২. সর্ট করার আগে ইনপুট দেখে নিন। সর্টের ক্ষেত্রে তিনটি বিষয় গুরুত্বপূর্ণ: সারির সংখ্যা, যেসব কলাম দিয়ে সর্ট করবেন তাদের ডেটা টাইপ, এবং সেই কলামগুলোতে ফাঁকা মান আছে কিনা।

python
import pandas as pd

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

print(orders.shape)
print(orders.dtypes)
print(orders["quantity"].isna().sum())
text
(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 হলো কলামের নামের একটি তালিকা, যা চতুর্থ অধ্যায়ের ডাবল-ব্র্যাকেট নির্বাচনের নিয়মে ব্যবহার করা হয়েছে।

python
cols = ["order_id", "branch", "product", "quantity", "price"]

by_qty = orders.sort_values("quantity")

print(by_qty[cols])
text
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। সর্ট করলে যদি সারিগুলোর নতুন নাম্বারিং হতো, তবে প্রতিটি লেবেল হঠাৎ ভিন্ন কোনো অর্ডারকে নির্দেশ করত, এবং "৫ নম্বর সারি" সম্পর্কে আপনার যেকোনো নোট নীরবে ভুল হয়ে যেত।

তাই সর্ট করার পর "প্রথম সারি" বলার দুটি ভিন্ন উপায় তৈরি হয়, এবং তারা আর এক থাকে না:

python
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])
text
1006
1001

iloc[0] মানে শূন্যতম অবস্থান — পৃষ্ঠার একদম প্রথম সারি, অর্থাৎ অর্ডার 1006। আর loc[0] মানে 0 লেবেলযুক্ত সারিটি — অর্থাৎ অর্ডার 1001, যা এখন পৃষ্ঠার ষষ্ঠ স্থানে রয়েছে। চতুর্থ অধ্যায়ে এই পার্থক্যটি তুলে ধরা হয়েছিল; সর্টিং হলো সেই অপারেশন যা এই পার্থক্যটিকে অত্যন্ত গুরুত্বপূর্ণ করে তোলে। সর্ট করার পর যখন আপনি "শীর্ষ সারি" চাইবেন, তখন iloc অথবা head() ব্যবহার করুন, কারণ এরা উভয়ই অবস্থান গুনে কাজ করে।

বড় থেকে ছোট: ascending=False

python
print(orders.sort_values("quantity", ascending=False)[cols])
text
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) যোগ করুন:

python
top3 = orders.sort_values("quantity", ascending=False).head(3)

print(top3[cols])
text
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 কলাম অভিধানের ক্রমানুসারে সর্ট হয়:

python
print(orders.sort_values("product")[cols])
text
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.0

bag এসেছে bottle-এর আগে কারণ a আসে o-এর আগে। দুটি ব্যাগ, দুটি বোতল এবং দুটি কলম — এগুলো হলো টাই (ties), এবং সমান হওয়া দুটি উপাদানের কোনটি আগে আসবে সেই প্রশ্নে আমরা একটু পরেই ফিরব।


সর্ট একটি নতুন টেবিল রিটার্ন করে

এই ভুলটি প্রায় সবাই জীবনে অন্তত একবার করে, এবং এর ফলে কোনো এররও দেখা দেয় না:

python
orders.sort_values("quantity", ascending=False)

print(orders[cols].head(3))
text
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() কখনোই যে টেবিলের ওপর কল করা হয়েছে তাকে পরিবর্তন করে না। ফলাফল কোনো ভেরিয়েবলে না রাখলে সর্ট সম্পন্ন হয়ে সঙ্গে সঙ্গে বাতিল হয়ে যায় — এবং কোনো সতর্কবার্তাও আসে না।

তাই ফলাফলটি সংরক্ষণ করুন, নতুন কোনো নামে অথবা আগের একই নামে:

python
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())
text
[1005, 1007, 1001]
[1001, 1002, 1003]

সর্ট করা কপিতে বড় অর্ডারগুলো ওপরে রয়েছে; মূল টেবিলটি অক্ষত। orders = orders.sort_values(...) লেখাও পুরোপুরি ঠিক আছে, যখন আপনি নিশ্চিত যে পুরোনো ক্রমটি আপনার আর প্রয়োজন নেই।

পুরোনো কোডে আপনি হয়তো inplace=True দেখতে পাবেন। এটি টেবিলে সরাসরি পরিবর্তন ঘটায় এবং None রিটার্ন করে, যা নিজস্ব বাগ তৈরি করে (দেখুন কিছু ভাঙা অবস্থা ও তার সমাধান)। তাছাড়া পান্ডাস ৩-এর Copy-on-Write বৈশিষ্ট্যের কারণে inplace=True কোনো মেমোরিও সাশ্রয় করে না। ফলাফল অ্যাসাইন করা অনেক বেশি পরিষ্কার: লাইনের দিকে তাকালেই বোঝা যায় সর্ট করা টেবিলটি কোথায় যাচ্ছে।


একাধিক কলাম: অগ্রাধিকার এবং দিক

মালিকের দ্বিতীয় অনুরোধ ছিল: "ব্রাঞ্চ অনুযায়ী গ্রুপ করা, প্রতি ব্রাঞ্চের ভেতরে সবচেয়ে বড়টি আগে।" এটি দুটি কি, এবং তালিকার ক্রমটিই হলো অগ্রাধিকারের (priority) ক্রম:

python
by_branch = orders.sort_values(["branch", "quantity"], ascending=[True, False])

print(by_branch[cols])
text
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 আগে চলে আসত।

তালিকার কলাম অদলবদল করলে পুরো প্রশ্নটিই বদলে যায়:

python
print(orders.sort_values(["quantity", "branch"], ascending=[False, True])[cols].head(4))
text
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 দিয়ে সর্ট করুন, সবচেয়ে দামিটি আগে:

python
print(orders.sort_values("price", ascending=False)[cols])
text
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" নিশ্চয়তা দেয় যে টাই হওয়া সারিগুলো সর্ট করার আগের ক্রম বজায় রাখবে — এখানে, ফাইলের ক্রম:

python
stable = orders.sort_values("price", ascending=False, kind="stable")

print(stable["order_id"].tolist())
text
[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]

বিকল্প ২ (সাধারণত উত্তম): সুস্পষ্ট টাই-ব্রেকার। টাই হলে কে জিতবে তা স্পষ্টভাবে বলে দিন। একটি সেলস রিপোর্টের জন্য, "দাম সমান হলে বড় অর্ডারটি আগে আসবে" একটি যুক্তিসঙ্গত নিয়ম:

python
tie_broken = orders.sort_values(["price", "quantity"], ascending=[False, False])

print(tie_broken[cols])
text
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

দুটি ফাঁকা পরিমাণ থাকা ফাইলের সংস্করণটি পড়ুন:

python
gaps = pd.read_csv("orders_gaps.csv")

print(gaps["quantity"].isna().sum())
print(gaps.sort_values("quantity", ascending=False)[cols])
text
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) করবেন, তখন আপনি প্রায়ই এর বিপরীতটি চাইবেন — সমস্যাগুলো যেন সবার ওপরে থাকে, যাতে চোখে পড়ে:

python
print(gaps.sort_values("quantity", na_position="first")[cols].head(3))
text
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()

যেহেতু প্রতিটি সারি তার লেবেল ধরে রেখেছিল, তাই মূল ক্রমটি কখনো হারিয়ে যায় না। ইনডেক্স অনুযায়ী সর্ট করলে তা ফিরে আসে:

python
restored = by_qty.sort_index()

print(restored["order_id"].tolist())
text
[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]

sort_index()-ও ascending=False সমর্থন করে, যার মাধ্যমে আপনি "নতুন লেবেল আগে" রেখে একটি টেবিল তালিকাভুক্ত করতে পারেন।

কখনো কখনো পুরোনো লেবেলগুলোর আর কোনো অর্থ থাকে না। একটি র‍্যাংকিং তালিকা তৈরি করে কাউকে পাঠাতে চাইলে পাশে 4, 6, 0 লেবেল থাকাটা বিভ্রান্তিকর। reset_index(drop=True) সেগুলো ফেলে দেয় এবং সারিগুলোকে তাদের নতুন ক্রমে 0, 1, 2, … দিয়ে নতুন নম্বর দেয়:

python
ranked = orders.sort_values("quantity", ascending=False).reset_index(drop=True)

print(ranked[cols].head(3))
print(ranked.loc[0, "order_id"])
text
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()

"পরিমাণের ভিত্তিতে শীর্ষ তিন" এতোটাই নিয়মিত প্রয়োজন হয় যে পান্ডাসে এমন একটি মেথড রয়েছে যা ঠিক এই কথাটিই বলে:

python
print(orders.nlargest(3, "quantity")[cols])
text
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() হলো এর ঠিক বিপরীত: দোকানের সবচেয়ে সস্তা তিনটি আইটেম:

python
print(orders.nsmallest(3, "price")[cols])
text
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=

সবচেয়ে দামি তিনটি আইটেম চাইলে একটি অদ্ভুত পরিস্থিতি তৈরি হয়:

python
print(orders.nlargest(3, "price")[cols])
text
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-এর চেয়ে বেশি হলেও:

python
print(orders.nlargest(3, "price", keep="all")[cols])
text
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 ব্রাঞ্চের সবচেয়ে বড় দুটি অর্ডার কী কী ছিল?" এটি পঞ্চম অধ্যায়ের সাথে এই অধ্যায়ের সমন্বয় ঘটায়, এবং ধাপগুলোর ক্রম অত্যন্ত গুরুত্বপূর্ণ:

python
north = orders[orders["branch"] == "north"]

top_north = north.sort_values("quantity", ascending=False).head(2)

print(top_north[cols])
text
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=

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

python
names = pd.DataFrame({"product": ["pen", "Notebook", "bag", "Bottle", "eraser"]})

print(names.sort_values("product"))
text
product
3    Bottle
1  Notebook
2       bag
4    eraser
0       pen

Bottle এবং Notebook কেবল বড় হাতের অক্ষর দিয়ে শুরু হওয়ার কারণে bag-এর আগে চলে এসেছে। কোনো পাঠক একে বর্ণানুক্রমিক বলবেন না।

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

python
print(names.sort_values("product", key=lambda col: col.str.lower()))
text
product
2       bag
3    Bottle
4    eraser
1  Notebook
0       pen

ছোট হাতের সংস্করণটি কেবল তুলনার জন্য ব্যবহৃত হয় এবং পরে ফেলে দেওয়া হয়। টেবিলে এখনও Bottle এবং Notebook হুবহু সেভাবেই দেখাচ্ছে যেভাবে টাইপ করা হয়েছিল। (lambda col: col.str.lower() হলো একটি এক লাইনের ফাংশন: কলাম col দিলে এটি তা ছোট হাতের অক্ষরে রূপান্তর করে রিটার্ন করে।)


যখন সংখ্যা টেক্সট হিসেবে সংরক্ষিত থাকে

এখানেই সেই ফাঁদ যা ধরার জন্য dtypes-এর পরিকল্পনামূলক ধাপটি তৈরি করা হয়েছে। ধরা যাক quantity টেক্সট হিসেবে এসেছে — ফাইলে হয়তো একটি অনিচ্ছাকৃত "N/A" ছিল, অথবা এমন কোনো সিস্টেম যা সবকিছু স্ট্রিং হিসেবে রপ্তানি করে। এটি কী ঘটাতে পারে তা দেখতে astype দিয়ে ইচ্ছাকৃতভাবে এমন একটি কলাম তৈরি করা যাক:

python
as_text = orders.astype({"quantity": "str"})

print(as_text["quantity"].dtype)
print(as_text.sort_values("quantity")[["order_id", "quantity"]])
text
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        7

1, 12, 2, 20, 30, 4, 5, 7। কোনো এরর নেই, এবং দেখতে প্রায় ঠিকঠাক একটি ক্রম। টেক্সট অক্ষর ধরে ধরে তুলনা করা হয়: "12" শুরু হয় 1 দিয়ে, যা 2-এর চেয়ে ছোট; তাই ডিকশনারিতে "ab" যেমন "b"-এর আগে আসে, ঠিক তেমনি "12" আসে "2"-এর আগে। সর্টকে যা বলা হয়েছিল সে ঠিক তাই করেছে। কিন্তু তাকে ভুল জিনিস বলা হয়েছিল।

দুটি লক্ষণ এটি ধরিয়ে দেয়: প্রিন্টআউটে quantity বাঁয়ে সারিবদ্ধ রয়েছে, যেভাবে পান্ডাস টেক্সট প্রিন্ট করে, এবং dtype দেখাচ্ছে str। আসল সমাধান হলো কলামটিকে সংখ্যায় রূপান্তর করা (ষষ্ঠ অধ্যায়ের pd.to_numeric)। কলামটিকে যদি আগের মতোই রাখতে হয়, তবে key= ব্যবহার করে নিউমেরিক মানের ভিত্তিতে সর্ট করা যায়:

python
fixed = as_text.sort_values("quantity", key=pd.to_numeric)

print(fixed["quantity"].tolist())
text
['1', '2', '4', '5', '7', '12', '20', '30']

এখন সঠিক ক্রম এসেছে — তবে মানগুলো এখনও স্ট্রিং, তাই আপনি তাদের যোগ করতে পারবেন না। ফাইল পড়ার পরপরই একবার কলামটি রূপান্তর করে নেওয়া সবচেয়ে ভালো অভ্যাস। তাহলে পরবর্তী প্রতিটি সর্ট, ফিল্টার এবং যোগফল কোনো কিছু মনে রাখার ঝামেলা ছাড়াই সঠিকভাবে কাজ করবে।


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

মালিকের পুরো অনুরোধটির উত্তর একটি প্রোগ্রামে: (১) অর্জিত আয়ের ভিত্তিতে শীর্ষ তিনটি অর্ডার, (২) নতুন অর্ডারগুলো আগে, (৩) ব্রাঞ্চভিত্তিক তালিকা যেখানে প্রতিটি ব্রাঞ্চে সবচেয়ে বড় অর্ডারটি আগে থাকবে।

"অর্জিত আয়" হলো পরিমাণ গুণ দাম। কলাম যোগ করার বিষয়টি কোর্সের পরবর্তী অংশে বিশদভাবে আলোচনা করা হবে; আপাতত একটি লাইনই যথেষ্ট — একটি নতুন কলামের নামে অ্যাসাইন করলেই তা তৈরি হয়ে যায়।

sorted_report.py:

python
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])
text
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'

python
orders.sort_values("Quantity")
text
KeyError: 'Quantity'

by=-এর নাম অবশ্যই কলামের নামের সাথে হুবহু মিলতে হবে — কেস, স্পেস সবকিছুসহ। কলামটির নাম হলো quantity, ছোট হাতের। তৃতীয় অধ্যায়ের পরামর্শ অনুযায়ী কোটেশন ও অনাকাঙ্ক্ষিত স্পেসসহ আসল নাম দেখতে list(orders.columns) প্রিন্ট করুন।

AttributeError: 'NoneType' object has no attribute 'head'

python
top = orders.sort_values("quantity", inplace=True)
top.head()
text
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)

python
orders.sort_values(["branch", "quantity"], ascending=[False])
text
ValueError: Length of ascending (1) != length of by (2)

ascending-এর তালিকার প্রতিটি কি-এর জন্য একটি করে এন্ট্রি থাকা প্রয়োজন। দুটি কি, দুটি বুলিয়ান: ascending=[True, False]। (তালিকায় না রেখে শুধু একটি False দেওয়া বৈধ — তা প্রতিটি কি-এর ওপর প্রযোজ্য হয়।)

ValueError: For argument "ascending" expected type bool, received type str.

python
orders.sort_values("branch", ascending="False")
text
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

python
orders.nlargest(3, "branch")
text
TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype

nlargest এবং nsmallest কেবল সংখ্যা র‍্যাংক করে। "বর্ণানুক্রমিকভাবে শেষ তিনটি ব্রাঞ্চ"-এর জন্য orders.sort_values("branch", ascending=False).head(3) ব্যবহার করুন। যে কলামটিকে নিউমেরিক মনে করেছিলেন তাতে যদি এই এরর আসে, তবে আসল সমস্যা হলো তা টেক্সট হিসেবে পড়া হয়েছে — dtypes পরীক্ষা করুন।

TypeError: '<' not supported between instances of 'str' and 'int'

python
mixed = pd.Series([12, "unknown", 5])
mixed.sort_values()
text
TypeError: '<' not supported between instances of 'str' and 'int'

এই কলামে আসল সংখ্যার পাশাপাশি টেক্সটও রয়েছে, তাই এর dtype হলো object, এবং পাইথন বলতে পারে না "unknown" কি 12-এর চেয়ে বড় নাকি ছোট। কলামটি আগে পরিষ্কার করুন: pd.to_numeric(mixed, errors="coerce") লিখলে "unknown" রূপান্তর হয়ে NaN হবে, এবং তখন সর্ট কাজ করবে ও na_position ঠিক করবে ফাঁকা মানটি কোথায় যাবে।