গ্রুপিং ও এগ্রিগেশন — প্রতি গ্রুপে একটি করে উত্তর
"প্রতি শাখায়", "প্রতি ক্যাটাগরিতে" কিংবা "প্রতি দিনে"র মতো প্রশ্নগুলোকে এক লাইনের groupby-তে রূপান্তর: কি (key), ভ্যালু ও এগ্রিগেশন, named agg দিয়ে একসাথে একাধিক হিসাব, size বনাম count, দুটি কি দিয়ে গ্রুপিং, এবং গড়ের গড় বের করার ভুল এড়ানো।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
দোকানের মালিক আপনাকে একটি লাইনের মেসেজ পাঠালেন: "এই সপ্তাহে আমাদের কোন শাখায় কয়টি পণ্য বিক্রি হয়েছে?"
আপনার কাছে আছে orders.csv — আটটি অর্ডার, প্রতি সারিতে একটি করে। গত চারটি অধ্যায়ে যা শিখেছেন, তা দিয়েই আপনি এর উত্তর দিতে পারেন। একটি শাখা ফিল্টার করুন, তার কলাম যোগ করুন, আর পুনরাবৃত্তি করুন:
import pandas as pd
orders = pd.read_csv("orders.csv")
for branch in ["north", "south", "east"]:
units = orders[orders["branch"] == branch]["quantity"].sum()
print(branch, units)north 48
south 6
east 27সংখ্যাগুলো সঠিক। কিন্তু এই পদ্ধতির তিনটি বড় সমস্যা রয়েছে, এবং ডেটার আকার বাড়ার সাথে সাথে সমস্যাগুলো আরও মারাত্মক হয়।
প্রথমত, শাখাগুলোর নাম আপনাকে আগে থেকেই জানতে হয়েছে। তালিকাটি আপনি নিজে হাতে টাইপ করেছেন। পরের সপ্তাহের ফাইলে যদি নতুন কোনো west শাখা থেকে অর্ডার আসে, তবে এই লুপ তা দেখতেই পাবে না — কোনো ত্রুটি বা এরর দেবে না, স্রেফ west-কে বাদ রেখে যাবে, আর মোট যোগফলের সাথে আলাদা আলাদা শাখার যোগফল আর মিলবে না।
দ্বিতীয়ত, প্রতিটি নতুন প্রশ্নের জন্য নতুন একটি লুপ লিখতে হবে। "প্রতি ক্যাটাগরিতে আয় (revenue)" জানার জন্য হাতে টাইপ করা দ্বিতীয় আরেকটি তালিকার ওপর দ্বিতীয় লুপ লাগবে। "প্রতি শাখা এবং প্রতি ক্যাটাগরিতে বিক্রি" জানতে গেলে লুপের ভেতর লুপ লাগবে।
তৃতীয়ত, উত্তরটি কোনো টেবিল নয়। এটি কেবল প্রিন্ট হওয়া কিছু লাইনের লেখা। এটিকে নতুন করে সাজানো (sort), ফিল্টার করা কিংবা অন্য কোনো ফলাফলের পাশে টেবিল আকারে বসানো যায় না।
আপনি আসলে যা বলতে চান তা এক লাইনেই বলা সম্ভব: সারিগুলোকে শাখা অনুযায়ী ভাগ করো, প্রতিটি টুকরোর ভেতরে quantity যোগ করো, এবং টুকরোগুলোকে একটি টেবিল হিসেবে ফিরিয়ে দাও। এটিই হলো groupby, এবং পান্ডাসে প্রায় প্রতিটি সারসংক্ষেপ বা সামারি এভাবেই লেখা হয় — কোনো রিপোর্ট, ড্যাশবোর্ডের সংখ্যা, কিংবা "ব্যবসার টাকা আসলে কোথা থেকে আসছে?" প্রশ্নের প্রথম উত্তর।
এই অধ্যায় শেষে আপনি পারবেন
- "প্রতি শাখায় মোট বিক্রি"-র মতো প্রশ্নকে তিনটি অংশে ভাগ করতে — কি (key), ভ্যালু কলাম ও এগ্রিগেশন — এবং গ্রুপিং করার আগে কি যাচাই করতে
df.groupby(key)[column].sum()লিখতে এবং কি-কে ইনডেক্স করে ফেরত আসা Series পড়তে.agg()এবং named aggregation দিয়ে একসাথে একাধিক সামারি হিসাব করতেsize()এবংcount()-এর পার্থক্য বুঝতে, এবং মিসিং কি ও মিসিং ভ্যালুর ক্ষেত্রে কী ঘটে তা নির্ধারণ করতে- দুটি কলামের ভিত্তিতে গ্রুপিং করতে, ফলাফল ফ্ল্যাট বা নতুন আকারে রূপান্তর (reshape) করতে, এবং "গড়ের গড়" করার মারাত্মক ভুল এড়াতে
আগে যা জানা লাগবে: সারি সাজানো।
যে ফাইলটি আমরা ব্যবহার করছি
আগের অধ্যায়গুলোর সেই একই 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প্রতিটি সারি একটি একক অর্ডার: কোন শাখা থেকে এসেছে, কোন পণ্য, পণ্যের ক্যাটাগরি, কয়টি ইউনিট এবং প্রতি ইউনিটের দাম।
কোড লেখার আগে
গ্রুপিং-সংক্রান্ত প্রতিটি প্রশ্নের কাঠামো একই রকম। groupby টাইপ করার আগেই প্রশ্নটিকে ভেঙে বিশ্লেষণ করুন।
১. প্রশ্নটিকে তিনটি অংশে ভাগ করুন
গ্রুপিংয়ের যেকোনো প্রশ্নে সর্বদা তিনটি বিষয় থাকে, এবং প্রশ্নের শব্দগুলোই বলে দেয় কোনটি কী:
- কি (Key) — যে কলামের মানগুলোর ওপর ভিত্তি করে গ্রুপ তৈরি হয়। বাংলায় প্রতি, বা ইংরেজিতে per, by, for each-এর পরের শব্দটি এটি নির্দেশ করে। যেমন "প্রতি শাখায় মোট ইউনিট"-এ এটি হলো
branch। - ভ্যালু (Value) — যে কলামটির সারসংক্ষেপ করা হচ্ছে: যা পরিমাপ করা হচ্ছে। এখানে সেটি হলো
quantity। - এগ্রিগেশন (Aggregation) — একাধিক মান মিলে কীভাবে একটি মানে পরিণত হবে: মোট, গড়, কতগুলো, সর্বোচ্চ। এখানে সেটি হলো
sum।
প্যাটার্নটি ভালোভাবে বোঝার জন্য আরও কয়েকটি উদাহরণ দেখা যাক:
- "প্রতি ক্যাটাগরিতে আয়" — কি
category, ভ্যালুrevenue, এগ্রিগেশনsum - "প্রতি শাখায় গড় অর্ডারের আকার" — কি
branch, ভ্যালুquantity, এগ্রিগেশনmean - "প্রতি দিনে কতগুলো অর্ডার" — কি
date, কোনো ভ্যালু কলাম নেই (সারি গুনবেন), এগ্রিগেশনsize - "প্রতি পণ্যের সবচেয়ে বড় একক অর্ডার" — কি
product, ভ্যালুquantity, এগ্রিগেশনmax
যদি এই তিনটি অংশ আলাদা করে বলতে না পারেন, তবে প্রশ্নটি এখনো কোড লেখার উপযোগী হয়নি। "কোন শাখাটি সেরা?" — এই প্রশ্নে কোনো ভ্যালু বা এগ্রিগেশন নেই। বিক্রির সংখ্যার দিক থেকে সেরা? আয়ের দিক থেকে সেরা? নাকি অর্ডারের সংখ্যার দিক থেকে সেরা? কোড লেখার আগেই তা স্পষ্ট করুন; কারণ তিনটি প্রশ্নের উত্তর তিনটি আলাদা শাখা হতে পারে।
২. গ্রুপিং করার আগে ইনপুট দেখে নিন
গ্রুপিং সারিগুলোকে আড়াল করে ফেলে। আটটি অর্ডার যখন তিনটি মোট সংখ্যায় রূপান্তরিত হয়ে যায়, তখন যোগফলের ভেতর কোনো ভাঙা বা ভুল সারি চোখে পড়ে না। তাই যাচাইয়ের কাজটি করতে হয় আগে:
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectএখানে দুটি বিষয় খেয়াল করার মতো। ভ্যালু কলাম quantity হলো int64 — অর্থাৎ সংখ্যাবাচক, তাই sum টেক্সট জোড়া না লাগিয়ে গাণিতিক যোগ করবে। আর কি কলাম branch হলো str, যা কি হিসেবে ঠিক আছে।
এরপর কি কলামের ভেতরের আসল মানগুলো দেখুন। এই যাচাইটি অনেকেই এড়িয়ে যান, অথচ এটিই সবচেয়ে বেশি গুরুত্বপূর্ণ:
print(orders["branch"].value_counts())
print(orders[["branch", "quantity"]].isna().sum())branch
north 4
south 2
east 2
Name: count, dtype: int64
branch 0
quantity 0
dtype: int64তিনটি আলাদা শাখা, এবং কোনো কলামেই কোনো মিসিং মান নেই। groupby প্রতিটি স্বতন্ত্র মানের জন্য হুবহু অক্ষরের মিল দেখে আলাদা গ্রুপ বানায় — ফলে "north" এবং "north " দুটি আলাদা গ্রুপ হয়ে যাবে। ভুল রিপোর্ট তৈরির আগেই value_counts() সেই সমস্যাটি চোখে আঙুল দিয়ে দেখিয়ে দেয়।
এই যাচাই বাদ দিলে কী ঘটতে পারে তা নিচে দেখুন। এখানে তিনটি সারিই আসলে north শাখাকে বোঝাচ্ছে — একটি ঠিকভাবে লেখা, একটির শেষে স্পেস আছে, এবং অন্যটির প্রথম অক্ষর বড় হাতের:
messy = pd.DataFrame({"branch": ["north", "north ", "North"], "quantity": [12, 2, 30]})
print(messy.groupby("branch")["quantity"].sum())
messy["branch"] = messy["branch"].str.strip().str.lower()
print(messy.groupby("branch")["quantity"].sum())branch
North 30
north 12
north 2
Name: quantity, dtype: int64
branch
north 44
Name: quantity, dtype: int64কোনো এরর বা ত্রুটি দেখা যায়নি, অথচ যেখানে একটি শাখা হওয়ার কথা ছিল সেখানে তৈরি হয়েছে তিনটি "শাখা"। পান্ডাসকে যা দেওয়া হয়েছিল তার জন্য প্রতি গ্রুপের সংখ্যা গাণিতিকভাবে সঠিক, কিন্তু ব্যবসায়িক রিপোর্টটি ভুল। গ্রুপিংয়ের আগে কি কলামটি পরিষ্কার করে নিলে — স্পেস ছেঁটে ফেলা ও ছোট হাতের অক্ষরে রূপান্তর — তিনটি সারি মিলে একটি একক গ্রুপে পরিণত হয় যার সঠিক মোট মান ৪৪।
৩. আউটপুট কেমন হবে তা আগেই কল্পনা করুন
ফলাফল দেখতে কেমন হবে তা আগেই ঠিক করে নিন, যাতে কাঙ্ক্ষিত রূপ না পেলে সঙ্গে সঙ্গে ধরতে পারেন:
branch units
east ?
north ?
south ?উপরের যাচাইগুলো থেকে আপনি ইতিমধ্যেই এর তিনটি বৈশিষ্ট্য জানেন: এতে ঠিক ৩টি সারি থাকবে (প্রতিটি স্বতন্ত্র শাখার জন্য একটি), শাখাগুলো আসবে বর্ণানুক্রমে (groupby ডিফল্টভাবে কি সাজিয়ে দেয়), এবং তিনটি সংখ্যার যোগফল অবশ্যই মূল কলামের মোট যোগফলের সমান হবে। এই শেষ বৈশিষ্ট্যটিই হলো পরে আপনার কাজ যাচাইয়ের প্রমাণ।
৪. সঠিক টুল বেছে নিন
- প্রতি গ্রুপে একটি কলাম থেকে একটি সংখ্যা:
df.groupby(key)[col].sum()(বাmean,max, ...) - একটি কলাম থেকে একাধিক সংখ্যা:
df.groupby(key)[col].agg(["sum", "mean"]) - একাধিক কলাম থেকে নিজের দেওয়া নামসহ একাধিক সংখ্যা:
df.groupby(key).agg(name=(col, "sum"), ...) - প্রতিটি গ্রুপে কয়টি সারি রয়েছে:
df.groupby(key).size() - যে কলামটি এখনো টেবিলে নেই (যেমন রাজস্ব/revenue): আগে কলামটি তৈরি করুন, তারপর গ্রুপিং করুন
এবার কোডে আসা যাক।
স্প্লিট, অ্যাপ্লাই, কম্বাইন (Split, apply, combine)
groupby ক্রমানুসারে তিনটি কাজ সম্পন্ন করে, এবং এই নামগুলো জানা দরকার কারণ গ্রুপিংয়ের প্রতিটি সমস্যা মূলত এই তিনটি ধাপেরই সমষ্টি:
- স্প্লিট (Split) — কি-এর স্বতন্ত্র মানের ভিত্তিতে সারিগুলোকে আলাদা আলাদা টুকরোয় বিভক্ত করা
- অ্যাপ্লাই (Apply) — প্রতিটি টুকরোর ওপর আলাদাভাবে হিসাবটি প্রয়োগ করা
- কম্বাইন (Combine) — ফলাফলগুলোকে কি-কে ইনডেক্স বানিয়ে একটি নতুন অবজেক্টে জোড়া লাগানো
স্প্লিট ধাপটি আপনি নিজে লুপ চালিয়েও দেখতে পারেন। একটি groupby অবজেক্টের ওপর লুপ চালালে প্রতি ধাপে একটি কি এবং টেবিলটির সংশ্লিষ্ট টুকরোটি পাওয়া যায়:
for branch, part in orders.groupby("branch"):
print(branch, part.shape, list(part["order_id"]))east (2, 7) [1004, 1007]
north (4, 7) [1001, 1003, 1005, 1008]
south (2, 7) [1002, 1006]প্রতিটি টুকরো হলো সাতটি কলামসহ একটি সাধারণ DataFrame — north টুকরোয় চারটি সারি রয়েছে, বাকি দুটিতে দুটি করে। কাউকে হাত দিয়ে শাখার নাম টাইপ করতে হয়নি; পান্ডাস নিজে ডেটা থেকে তা খুঁজে নিয়েছে। আগামী সপ্তাহে যদি নতুন কোনো west শাখা থেকে অর্ডার আসে, তবে স্রেফ চতুর্থ আরেকটি টুকরো তৈরি হবে।
বাস্তব কোডে এভাবে নিজে লুপ চালানোর প্রয়োজন প্রায় কখনোই হয় না। এটি এখানে দেখানো হয়েছে যাতে ভেতরের মানসিক মডেলটি পরিষ্কার হয়: নিচের সবকিছুই মূলত "প্রতিটি টুকরোর ওপর কিছু কাজ করো, তারপর ফলাফলগুলোকে একসাথে জোড়া লাগাও"।
প্রতি গ্রুপে একটি সংখ্যা
"প্রতি শাখায় মোট ইউনিট" প্রশ্নটির তিনটি অংশ — কি branch, ভ্যালু quantity, এগ্রিগেশন sum — ঠিক সেই ক্রমানুসারেই কোডে লেখা হয়:
units = orders.groupby("branch")["quantity"].sum()
print(units)branch
east 27
north 48
south 6
Name: quantity, dtype: int64বাম থেকে ডানে পড়ুন: branch অনুযায়ী গ্রুপ করো, quantity কলামটি নাও, এবং যোগ করো। লুপের সেই একই সংখ্যাগুলো এবার এক লাইনে পাওয়া গেল, কোনো শাখার নাম হাতে টাইপ না করেই।
এবার আপনার আগের কল্পিত রূপরেখার সাথে মিলিয়ে নিন। তিনটি সারি — হ্যাঁ। বর্ণানুক্রম — হ্যাঁ। আর যোগফলের প্রমাণ:
print(units.sum(), orders["quantity"].sum())81 81গ্রুপগুলোর যোগফল পুরো টেবিলের যোগফলের সমান, সুতরাং কোনো সারি হারিয়ে যায়নি এবং কোনো সারি দুবার গণনা করা হয়নি। গ্রুপিং করার সময় এই এক লাইনের যাচাইটি অভ্যাসে পরিণত করা অত্যন্ত মূল্যবান: যখনই গ্রুপিং করবেন, ফলাফলের মোট যোগফল ইনপুটের মোট যোগফলের সাথে মিলিয়ে দেখুন।
ফলাফল একটি Series, আর কি (key) হলো তার ইনডেক্স
print(type(units))
print(units.index)
print(units["north"])<class 'pandas.Series'>
Index(['east', 'north', 'south'], dtype='str', name='branch')
48কম্বাইন ধাপটি শাখাগুলোকে কোনো কলামে না রেখে ইনডেক্সে বসিয়ে দিয়েছে। এই কারণেই আউটপুটে মানের উপরে একটি আলাদা লাইনে branch শব্দটি দেখা যায় — এটি ইনডেক্সের নাম। এর আরেকটি অর্থ হলো, আপনি শাখার নাম দিয়ে মান খুঁজে নিতে পারেন, যেমন units["north"], ঠিক যেভাবে চতুর্থ অধ্যায়ে .loc দিয়ে খুঁজেছিলেন।
যেহেতু এটি একটি সাধারণ Series, তাই পূর্ববর্তী অধ্যায়গুলোর সবকিছুই এতে কাজ করে। এটিকে সাজালে (sort) "কোন শাখা সবচেয়ে বেশি বিক্রি করেছে" প্রশ্নের উত্তর পাওয়া যায়:
print(units.sort_values(ascending=False))
print(units.idxmax())branch
north 48
east 27
south 6
Name: quantity, dtype: int64
northidxmax() সর্বোচ্চ মানের লেবেলটি ফেরত দেয় — সংখ্যাটি নয়, শাখার নামটি। "কে শীর্ষে আছে" জানতে চাইলে সাধারণত এটাই প্রয়োজন হয়।
কেন আগেই কলাম বেছে নেবেন
অনেকেরই ইচ্ছা হতে পারে ["quantity"] অংশটি বাদ দিয়ে পান্ডাসকে সব কলাম একসাথে যোগ করতে বলার:
print(orders.groupby("branch").sum())order_id date ... quantity price
branch ...
east 2011 2024-03-022024-03-04 ... 27 135.0
north 4017 2024-03-012024-03-022024-03-032024-03-04 ... 48 993.0
south 2008 2024-03-012024-03-03 ... 6 910.0
[3 rows x 6 columns]কোনো এরর আসেনি — অথচ এর প্রায় প্রতিটি সংখ্যাই অর্থহীন। order_id যোগ করে ফেলা হয়েছে, যেন অর্ডারের সিরিয়াল নম্বর কোনো আর্থিক পরিমাণ। date-এর স্ট্রিংগুলো একটার সাথে আরেকটা জোড়া লেগে গেছে, কারণ টেক্সটের ক্ষেত্রে "যোগ" মানে হলো কনক্যাটেনেশন। price কলামটিও যোগ করা হয়েছে: north-এর জন্য 993.0 হলো চারটি ভিন্ন পণ্যের একক দামের যোগফল, যা কোনো অর্থ প্রকাশ করে না।
orders.groupby("branch").sum() কোনো এরর দেয় না। এটি আইডি নম্বর যোগ করে, তারিখগুলোকে জোড়া লাগায় এবং একক দামগুলোকে মোট করে ফেলে — আর ফলাফলটি এমনভাবে ফিরিয়ে দেয় যেন এটি একটি সুন্দর রিপোর্ট।কেবল quantity-র যোগফলটিই অর্থপূর্ণ ছিল, এবং আপনি চাইলে শুধু সেটাই চাইতে পারতেন। এই কারণেই নিয়ম হলো: এগ্রিগেশনের আগেই ভ্যালু কলাম বেছে নিন। প্রতিটি ডেটা টাইপের জন্য যোগের অর্থ যা দাঁড়ায়, sum ঠিক সেটাই করে ফেলে — অথচ বেশিরভাগ কলামের ক্ষেত্রেই তা কোনো অর্থবহ প্রশ্ন নয়।
mean মেথডটি আরও কঠোর; অর্থহীন ফলাফল দেওয়ার বদলে এটি কাজ করতে সরাসরি অস্বীকার করে:
print(orders.groupby("branch").mean())TypeError: dtype 'str' does not support operation 'mean'এই এররটি মূলত একই ভুলকে প্রকাশ করে দেয়। এটিকে এভাবে পড়ুন: আপনি টেক্সট কলামের গড় করতে বলেছেন। এর সমাধান numeric_only=True দেওয়া নয় — কারণ তা দিলেও order_id ও price-এর মতো কলামের অর্থহীন গড় হতো — বরং কোন কলামটির গড় চাইছেন তা স্পষ্টভাবে বলে দেওয়াই সঠিক সমাধান।
একসাথে একাধিক সংখ্যা: .agg()
একটিমাত্র সামারি অনেক সময়ই যথেষ্ট হয় না। "প্রতিটি শাখা কত বিক্রি করেছে, প্রতি অর্ডারে গড়ে কত এবং মোট কতগুলো অর্ডারে?" — এটি একই কলামের ওপর তিনটি এগ্রিগেশন। নামগুলোর একটি লিস্ট .agg()-এ পাস করুন:
print(orders.groupby("branch")["quantity"].agg(["sum", "mean", "count"]))sum mean count
branch
east 27 13.5 2
north 48 12.0 4
south 6 3.0 2এখন ফলাফলটি একটি DataFrame — প্রতি এগ্রিগেশনের জন্য একটি কলাম এবং প্রতি শাখার জন্য একটি সারি। এটি এমন একটি চিত্র তুলে ধরে যা কেবল মোট যোগফল আড়াল করেছিল: east শাখা north-এর অর্ধেকের কিছু বেশি পণ্য বিক্রি করেছে, কিন্তু এর গড় অর্ডার আকারে বড়। north শাখায় বেশি সংখ্যক অর্ডার আসে; আর east-এ কম সংখ্যক কিন্তু তুলনামূলক বড় অর্ডার আসে।
লিস্টের নামগুলো সাধারণ স্ট্রিং: "sum", "mean", "count", "min", "max", "median", "nunique" (অনন্য মানের সংখ্যা), "first", "last"। এগুলো হলো সেই একই মেথড যা আপনি আগে একক কলামে ব্যবহার করেছেন, যা এখন প্রতিটি টুকরোর ওপর প্রয়োগ করা হচ্ছে।
Named aggregation: আপনার কলাম, আপনার দেওয়া নাম
আসল ব্যবসায়িক রিপোর্টে ভিন্ন ভিন্ন কলামের ওপর ভিন্ন ভিন্ন হিসাব করতে হয়, এবং কলামের অর্থপূর্ণ নাম প্রয়োজন হয়। Named aggregation এই দুটি কাজই এক কলে করে দেয়। প্রতিটি কিওয়ার্ড আর্গুমেন্ট হলো একটি আউটপুট কলামের নাম, এবং তার মান হলো একটি জোড়া: (ইনপুট কলাম, এগ্রিগেশন)।
রিপোর্টে রাজস্ব (revenue) দেখানো খুব স্বাভাবিক একটি বিষয়, অথচ এটি টেবিলে কোনো কলাম হিসেবে নেই — এটি হলো প্রতিটি সারির quantity গুণ price। নতুন কলাম তৈরির জন্য পরবর্তী অধ্যায়ে বিস্তারিত আলোচনা রয়েছে; আপাতত এক লাইনের কোডই যথেষ্ট, যা আমাদের পরিকল্পনার ৪ নম্বর ধাপকে বাস্তবে দেখায়: যদি ভ্যালু কলামটি টেবিলে না থাকে, গ্রুপিং করার আগেই তা তৈরি করে নিন।
orders["revenue"] = orders["quantity"] * orders["price"]
summary = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
print(summary)orders units revenue
branch
east 2 27 1140.0
north 4 48 2600.0
south 2 6 1150.0এটি এমন একটি রিপোর্ট যা আপনি সরাসরি পাঠাতে পারেন। agg কলের প্রতিটি লাইন একটি বাক্যের মতো পড়া যায়: orders হলো order_id-এর count, units হলো quantity-র sum, revenue হলো revenue-র sum। আউটপুট কলামের নামগুলো আপনি নিজে বেছে নিয়েছেন, ডিফল্ট sum বা count নাম পায়নি — তাই পরে আলাদা করে নাম পরিবর্তনের প্রয়োজন নেই।
এবং এটি আগের উত্তরকে বদলে দেয়। বিক্রিত ইউনিটের দিক থেকে east পরিষ্কারভাবে দ্বিতীয় অবস্থানে ছিল; কিন্তু আয়ের দিক থেকে south সামান্য এগিয়ে গেছে — ৮৫০ মূল্যের একটি ব্যাগ বিশটি কলমের দামকে ছাড়িয়ে গেছে। এই কারণেই ১ নম্বর ধাপে ভ্যালুর নাম স্পষ্ট করার ওপর জোর দেওয়া হয়েছিল: বিক্রির দিক থেকে "সেরা শাখা" এবং আয়ের দিক থেকে "সেরা শাখা" দুটি ভিন্ন প্রশ্ন।
ফলাফলটি যেহেতু একটি DataFrame, তাই এটিকে সেভাবেই সাজানো যায়:
print(summary.sort_values("revenue", ascending=False))orders units revenue
branch
north 4 48 2600.0
south 2 6 1150.0
east 2 27 1140.0গণনা: size() বনাম count()
"প্রতি শাখায় কতগুলো অর্ডার" শুনলে মনে হয় একটিমাত্র প্রশ্ন। কিন্তু পান্ডাসে এর দুটি উত্তর রয়েছে, এবং ডেটায় কোনো মান মিসিং থাকলেই কেবল তারা ভিন্ন উত্তর দেয়।
size()প্রতিটি গ্রুপের মোট সারি গণনা করে, তাতে যে মানই থাকুক না কেন।count()প্রতিটি গ্রুপের একটি নির্দিষ্ট কলামের উপস্থিত (নন-মিসিং) মানগুলো গণনা করে।
নিখুঁত টেবিলে দুটিই এক ফলাফল দেয়। এদের পার্থক্য দেখার জন্য orders-এর পাশে একটি ছোট টেবিল তৈরি করা যাক, যাতে একটি মিসিং branch এবং একটি মিসিং quantity রয়েছে:
gaps = pd.DataFrame(
{
"order_id": [1001, 1002, 1003, 1004, 1005],
"branch": ["north", "south", None, "east", "north"],
"quantity": [12, 5, 2, None, 30],
}
)
print(gaps)order_id branch quantity
0 1001 north 12.0
1 1002 south 5.0
2 1003 NaN 2.0
3 1004 east NaN
4 1005 north 30.0(মিসিং মানের কারণে quantity ফ্লোটে রূপান্তরিত হয়েছে — ষষ্ঠ অধ্যায়ে এর কারণ ব্যাখ্যা করা হয়েছিল।)
print(gaps.groupby("branch").size())
print(gaps.groupby("branch")["quantity"].count())branch
east 1
north 2
south 1
dtype: int64
branch
east 0
north 2
south 1
Name: quantity, dtype: int64east শাখায় একটি অর্ডার আছে (size = 1), কিন্তু জানা quantity আছে শূন্যটি (count = 0)। দুটি কথাই সত্য; তারা ভিন্ন প্রশ্নের উত্তর দিচ্ছে। "কতগুলো অর্ডার এসেছে" জানতে size() ব্যবহার করুন, আর "কতগুলো অর্ডারে আসল সংখ্যা উল্লেখ আছে" জানতে count() ব্যবহার করুন।
মিসিং ডেটা যে দুটি কাজ নীরবে করে
উপরের আউটপুটগুলো আবার ভালো করে লক্ষ্য করুন। ১০০৩ নম্বর অর্ডারটি, যার কোনো branch ছিল না, সেটির দেখা কোনো ফলাফলেই নেই। পাঁচটি অর্ডার ইনপুট হিসেবে ঢুকেছিল; কিন্তু ফলাফল হিসেবে বের হলো চারটি:
print(gaps.groupby("branch").size().sum(), len(gaps))4 5ডিফল্টভাবে groupby যেসব সারির কি (key) মিসিং, সেগুলোকে বাদ দিয়ে দেয়। "unknown" নামে আলাদা কোনো গ্রুপ তৈরি হয় না, ফলে সেই সারিটি প্রতিটি ফলাফল থেকে অদৃশ্য হয়ে যায়। আগের সেই মোট যোগফল মেলানোর যাচাইটি অবিলম্বে এই সমস্যাটি ধরে ফেলে। যদি সেই সারিগুলো গুরুত্বপূর্ণ হয় — আর শাখা না থাকা সত্ত্বেও একটি অর্ডার তো আসলে একটি অর্ডারই — তবে dropna=False দিয়ে সেগুলো দাবি করুন:
print(gaps.groupby("branch", dropna=False)["quantity"].sum())branch
east 0.0
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64এখন চারটি গ্রুপই উপস্থিত, এবং NaN গ্রুপে সেই ২ ইউনিট পণ্য সংরক্ষিত আছে যা কোনো নির্দিষ্ট শাখার সাথে মেলানো যায়নি।
দ্বিতীয় নীরব ঘটনাটি একই আউটপুটে দেখা যায়: east-এর মান দেখাচ্ছে 0.0। east শাখা শূন্য বিক্রি করেনি; তার একমাত্র বিক্রির পরিমাণটি ছিল অজানা। sum মেথড "যোগ করার মতো কিছু নেই"-কে শূন্য হিসেবে গণ্য করে। যদি চান অজানা মানটি অজানাই থাকুক, তবে min_count=1 দিয়ে তা স্পষ্ট করুন — অর্থাৎ "কেবলমাত্র তখনই যোগফল দেবে যদি অন্তত একটি বাস্তব মান থাকে":
print(gaps.groupby("branch", dropna=False)["quantity"].sum(min_count=1))branch
east NaN
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64কোনটি সঠিক তা নির্ভর করে আপনার প্রশ্নের ওপর, আর এটাই মূল বিষয়: এটি কোনো দুর্ঘটনা নয়, পরিকল্পনার ধাপেই এটি নির্ধারণ করুন। এই দুটি সমস্যারই মূল সমাধান অবশ্য আরও আগের ধাপে নিহিত — গ্রুপিং করার আগেই মিসিং মানগুলো পরিষ্কার করে নিন (অধ্যায় ছয়)।
দুটি কি (key): গ্রুপের ভেতর গ্রুপ
"প্রতি শাখা এবং প্রতি ক্যাটাগরিতে রাজস্ব" — এই প্রশ্নে দুটি কি রয়েছে। একটি লিস্ট পাস করুন:
by_both = orders.groupby(["branch", "category"])["revenue"].sum()
print(by_both)branch category
east accessories 840.0
stationery 300.0
north accessories 2180.0
stationery 420.0
south accessories 850.0
stationery 300.0
Name: revenue, dtype: float64ডেটাতে প্রকৃতপক্ষে বিদ্যমান প্রতিটি কম্বিনেশনের জন্য একটি করে সারি তৈরি হয়েছে — এখানে ছয়টি। ইনডেক্সে এখন দুটি স্তর রয়েছে, branch এবং category; পান্ডাস একে বলে MultiIndex, এবং সহজে পড়ার সুবিধার্থে বাইরের স্তরটিকে প্রতি ব্লকে কেবল একবার প্রিন্ট করে। east-এর নিচের খালি জায়গাটির অর্থ হলো "এখনও east"।
স্বাভাবিক প্রত্যাশা অনুযায়ীই আপনি এখান থেকে ডেটা নির্বাচন করতে পারেন:
print(by_both.loc["north"])
print(by_both.loc[("north", "stationery")])category
accessories 2180.0
stationery 420.0
Name: revenue, dtype: float64
420.0একটি লেবেল দিলে north-এর পুরো অংশটি পাওয়া যায়; আর উভয় লেবেল নিয়ে একটি টাপল দিলে নির্দিষ্ট একটি একক সংখ্যা পাওয়া যায়।
সাধারণ কলাম ফিরিয়ে আনা
MultiIndex দেখতে সংক্ষিপ্ত হলেও অনেক পরবর্তী কাজের জন্য — যেমন CSV-তে সেভ করা, মাস্ক দিয়ে ফিল্টার করা, কিংবা অন্য কোনো টেবিলের সাথে যুক্ত করা — সাধারণ কলাম হিসেবে থাকাটাই বেশি সুবিধাজনক। এটি করার দুটি উপায় রয়েছে, এবং দুটিই একই টেবিল তৈরি করে:
flat = orders.groupby(["branch", "category"], as_index=False)["revenue"].sum()
print(flat)
print(flat.equals(by_both.reset_index()))branch category revenue
0 east accessories 840.0
1 east stationery 300.0
2 north accessories 2180.0
3 north stationery 420.0
4 south accessories 850.0
5 south stationery 300.0
Trueas_index=False groupby-কে শুরুতেই কি-গুলোকে ইনডেক্সে রূপান্তর না করার নির্দেশ দেয়; আর reset_index() গ্রুপিং শেষে সেগুলোকে আবার সাধারণ কলামে ফিরিয়ে আনে। আপনার কোডের যে জায়গায় যেটি পড়তে বেশি সহজ মনে হয়, সেটিই ব্যবহার করুন।
মানুষের পড়ার উপযোগী রূপ: unstack()
মানুষের পড়ার জন্য একটি দীর্ঘ তালিকার চেয়ে গ্রিড বা টেবিল কাঠামো অনেক বেশি সুবিধাজনক — যেমন বাম পাশে শাখা এবং ওপরের সারিতে ক্যাটাগরি:
print(by_both.unstack())category accessories stationery
branch
east 840.0 300.0
north 2180.0 420.0
south 850.0 300.0unstack() ভেতরের ইনডেক্স স্তর category-কে কলামে রূপান্তর করে দেয়। ভেতরের সংখ্যাগুলো আগের সেই ছয়টি সংখ্যাই। কোনো শাখা যদি কোনো ক্যাটাগরির পণ্য একেবারেই বিক্রি না করত, তবে সেই ঘরটিতে NaN আসত; unstack(fill_value=0) লিখলে তার বদলে সেখানে 0 বসে যাবে।
গড়ের গড় (average of averages) করার ফাঁদ
একটি সহজ মনে হওয়া প্রশ্ন: প্রতি শাখায় গড়ে একটি পণ্যের দাম কত ছিল? অনেকেই চট করে price কলামটির গড় করে ফেলেন:
print(orders.groupby("branch")["price"].mean())branch
east 67.50
north 248.25
south 455.00
Name: price, dtype: float64এটি হলো অর্ডার লাইনের দামগুলোর গড়, এবং প্রতিটি লাইন এখানে সমান গুরুত্ব পাচ্ছে — south শাখার একটিমাত্র ব্যাগের অর্ডার (১টি ইউনিট ৮৫০ টাকায়) তার খাতার অর্ডারের (৫টি ইউনিট ৬০ টাকায়) সমান গুরুত্ব বহন করছে। কিন্তু ক্রেতারা প্রতিটি লাইনের একটি করে পণ্য কেনেননি। তারা কিনেছেন একাধিক ইউনিট। বিক্রিত ইউনিট প্রতি গড় দাম বের করতে হলে মোট রাজস্বকে মোট ইউনিট দিয়ে ভাগ করতে হবে, এবং তা অবশ্যই যোগফল থেকে হিসাব করতে হবে:
per_unit = orders.groupby("branch").agg(
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
per_unit["avg_unit_price"] = (per_unit["revenue"] / per_unit["units"]).round(2)
print(per_unit)units revenue avg_unit_price
branch
east 27 1140.0 42.22
north 48 2600.0 54.17
south 6 1150.0 191.67south-এর জন্য হিসাবটি ৪৫৫ থেকে নেমে প্রায় ১৯২-এ চলে এসেছে। পাটিগণিতের দিক থেকে কোনো সংখ্যাই "ভুল" নয়; কিন্তু কেবল একটি সংখ্যাই প্রশ্নের আসল উত্তর দেয়। নিয়মটি মনে রাখুন: যখন আপনার কাঙ্ক্ষিত উত্তরটি একটি অনুপাত বা রেশিও, তখন লব ও হরকে আলাদাভাবে এগ্রিগেট করুন, তারপর ভাগ করুন। যে সংখ্যাটি নিজেই একটি গড়, কিংবা যে দামটি আগেই ইউনিট-প্রতি দাম, তার ওপর আবার গড় করলে প্রতিটি সারি সমান গুরুত্ব পায় — তা একটি ইউনিট হোক কিংবা ত্রিশটি।
একটি সম্পূর্ণ উদাহরণ
branch_report.py দোকানের মালিকের প্রশ্নের একটি পূর্ণাঙ্গ ও নির্ভরযোগ্য উত্তর তৈরি করে — প্রতি শাখায়: অর্ডারের সংখ্যা, মোট ইউনিট, মোট রাজস্ব, মোট রাজস্বে শাখার শতকরা হার এবং সর্বাধিক বিক্রিত পণ্য — এবং সবশেষে নিজের মোট যোগফল যাচাই করে নেয়।
import pandas as pd
orders = pd.read_csv("orders.csv")
# 1. Check the input before grouping hides anything.
print("Rows, columns:", orders.shape)
print("Branches:", sorted(orders["branch"].unique()))
print("Missing in key/value:", int(orders[["branch", "quantity", "price"]].isna().sum().sum()))
print()
# 2. Create the value column that does not exist yet.
orders["revenue"] = orders["quantity"] * orders["price"]
# 3. One grouped call, one row per branch.
report = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
report["share_pct"] = (report["revenue"] / report["revenue"].sum() * 100).round(1)
# 4. Best product per branch: group by both keys, then keep each branch's top row.
by_product = orders.groupby(["branch", "product"], as_index=False)["quantity"].sum()
top = by_product.sort_values("quantity", ascending=False).drop_duplicates("branch")
report["top_product"] = top.set_index("branch")["product"]
report = report.sort_values("revenue", ascending=False)
print(report)
print()
# 5. Prove nothing was lost or double-counted.
print("Revenue check:", report["revenue"].sum(), "==", orders["revenue"].sum())
print("Orders check :", report["orders"].sum(), "==", len(orders))Rows, columns: (8, 7)
Branches: ['east', 'north', 'south']
Missing in key/value: 0
orders units revenue share_pct top_product
branch
north 4 48 2600.0 53.2 eraser
south 2 6 1150.0 23.5 notebook
east 2 27 1140.0 23.3 pen
Revenue check: 4890.0 == 4890.0
Orders check : 8 == 8প্রোগ্রামটি কেন এভাবে লেখা হয়েছে:
- যাচাইগুলো সবার আগে করা হয়েছে এবং প্রিন্ট করা হয়েছে। তিনটি স্বতন্ত্র শাখা এবং শূন্য মিসিং মান হলো সেই শর্ত যার ওপর এই রিপোর্টের নির্ভরযোগ্যতা দাঁড়িয়ে আছে। পরের সপ্তাহের ফাইলে যদি চারটি শাখা বা দুটি মিসিং মান থাকে, কোনো যোগফল পড়ার আগেই আপনি সবার ওপরে তা দেখতে পাবেন।
- গ্রুপিংয়ের আগেই
revenueকলাম তৈরি করা হয়েছে। প্রতি সারিতেquantity * priceহিসাব করে তারপর গ্রুপিং করলে আসল রাজস্ব পাওয়া যায়। পরে যোগ করা পরিমাণকে গড় দাম দিয়ে গুণ করতে গেলে আবার সেই গড়ের গড়ের ফাঁদে পড়তে হতো। - একটি
aggকল, সুনির্দিষ্ট কলামের নাম। প্রতিটি আউটপুট কলাম একবার ঘোষণা করা হয়েছে, ঠিক সেই নাম দিয়ে যা পাঠক দেখতে পাবেন। চতুর্থ আরেকটি মেট্রিক যোগ করতে হলে স্রেফ আরেকটি লাইন লিখতে হবে। share_pctইনপুটের বদলে ফলাফলের ওপর হিসাব করা হয়েছে। গ্রুপিংয়ের পরreportহলো শাখা দিয়ে ইনডেক্স করা একটি সাধারণ DataFrame, তাই এতে সাধারণ কলামের গাণিতিক হিসাব চলে। এর শতকরা হার যোগ করলে ঠিক ১০০.০ হয়।- শীর্ষ পণ্য নির্ধারণে দুই-কি গ্রুপিং এবং সাজানো ব্যবহার করা হয়েছে।
groupby(["branch", "product"])প্রতিটি শাখা-পণ্য জোড়ার বিক্রি দেয়;quantity-র অবরোহী ক্রমে সাজিয়ে প্রতি শাখার প্রথম সারিটি রাখলে (drop_duplicates("branch")) প্রতিটি শাখার সেরা বিক্রেতা পণ্যটি থেকে যায়।top.set_index("branch")["product"]অ্যাসাইন করার ফলে পণ্যের নামগুলো অবস্থানের বদলে শাখার লেবেল ধরে মিলে যায় — ইনডেক্স নিজেই মেলানোর দায়িত্ব নেয়। - শেষ দুটি লাইন হলো সঠিকতার প্রমাণ। গ্রুপগুলোর মোট রাজস্ব ও অর্ডারের সংখ্যা মূল ইনপুটের সমান। কোনো শাখা হারিয়ে গেলে বা ফাইলে ফাঁকা শাখা থাকলে সংখ্যা মিলত না, এবং রিপোর্টটি পাঠানোর আগেই আপনি তা জেনে যেতেন।
কিছু ভাঙা অবস্থা ও তার সমাধান
KeyError: 'Branch' কি কলামটির বানান মূল ফাইলের সাথে মেলেনি — এখানে বড় হাতের B লেখা হয়েছে। কলামের নামগুলো কেস-সংবেদনশীল, এবং groupby সাথে সাথে তা পরীক্ষা করে। list(orders.columns) প্রিন্ট করে হুবহু নাম কপি করুন; লিস্টের কোটেশন চিহ্নগুলো 'branch '-এর মতো অনাকাঙ্ক্ষিত ফাঁকা স্পেসও ধরিয়ে দেয়।
KeyError: 'Column not found: Quantity' এক ধাপ পরের একই ভুল: কি কলামটি ঠিক ছিল, কিন্তু groupby("branch")["Quantity"]-এ ভ্যালু কলামটির অস্তিত্ব নেই। ত্রুটির বার্তায় কেবল নামের বদলে "Column not found" বলা হয়, যা বুঝিয়ে দেয় যে groupby-এর পরবর্তী সিলেকশনে সমস্যা হয়েছে।
KeyError: "Label(s) ['qty'] do not exist" Named aggregation-এ প্রতিটি জোড়ার প্রথম উপাদানটি অবশ্যই বাস্তব কলাম হতে হবে: units=("qty", "sum") ব্যর্থ হবে কারণ qty নামে কোনো কলাম নেই। বাম পাশের আউটপুটের নাম (units) যা খুশি হতে পারে; কিন্তু ডান পাশের ইনপুটটি টেবিলে থাকতে হবে।
ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead. আপনি লিখেছেন orders.groupby("branch")["quantity", "price"]। এক জোড়া বন্ধনীর ভেতর কমা দিয়ে দুটি নাম লিখলে তা একটি টাপল তৈরি করে। একাধিক কলাম নির্বাচন করতে লিস্ট প্রয়োজন — অর্থাৎ ডাবল বন্ধনী, ঠিক চতুর্থ অধ্যায়ের মতো: orders.groupby("branch")[["quantity", "price"]].sum()।
TypeError: dtype 'str' does not support operation 'mean' কোনো টেক্সট কলামের ওপর এগ্রিগেশন চালানো হয়েছে — হয় কোনো কলাম বেছে নেওয়া হয়নি (groupby("branch").mean()), নয়তো আপনি যে কলামটি বেছে নিয়েছেন তা টেক্সট হিসেবে লোড হয়েছে। দ্বিতীয় ক্ষেত্রে info() চালালে দেখা যাবে যেখানে সংখ্যা প্রত্যাশিত সেখানে str রয়েছে; ষষ্ঠ অধ্যায়ের pd.to_numeric(..., errors="coerce") দিয়ে এটি ঠিক করা যায়।
ফলাফল পড়ার সময় KeyError: 'quantity' units = orders.groupby("branch")["quantity"].sum() চালানোর পর শাখাগুলো হলো units-এর ইনডেক্স, আর quantity কেবল তার নাম। units["quantity"] লিখলে কোড quantity নামের একটি শাখা খোঁজে। নির্দিষ্ট শাখা দেখতে units["north"] লিখুন, অথবা মোট মানের জন্য units.sum() ব্যবহার করুন।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
কী ছাপা হবে?
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))
print(orders.groupby("branch")["quantity"].sum())- Abranch north 48 south 6 east 27 Name: quantity, dtype: int64
- Bbranch east 27 north 48 south 6 Name: quantity, dtype: int64
- C branch quantity 0 east 27 1 north 48 2 south 6
- D81
উদ্দেশ্য হলো প্রতি শাখার মোট quantity এবং মোট price বের করা। কী ঘটবে?
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))
by_branch = orders.groupby("branch")["quantity", "price"].sum()- Aএটি কাজ করে এবং দুটি কলামসহ একটি টেবিল ফেরত দেয়
- B`KeyError: ('quantity', 'price')`
- C`ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead.`
- Dএটি শুধুমাত্র quantity কলামটি ফেরত দেয়
groupby-এর পর size() এবং count()-এর মধ্যে পার্থক্য কী?
- Aকোনো পার্থক্য নেই; এগুলো একই জিনিসের দুটি ভিন্ন নাম
- B`size()` প্রতিটি গ্রুপের সারি গণনা করে, মিসিং মানসহ; `count()` শুধুমাত্র উপস্থিত (নন-মিসিং) মানগুলো গণনা করে
- C`count()` সারি গণনা করে; `size()` অনন্য মান গণনা করে
- D`size()` প্রতিটি গ্রুপের মেমরি ব্যবহারের পরিমাণ পরিমাপ করে
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
দোকানের মালিক এবার ক্যাটাগরি এবং তারিখ অনুযায়ী একই ধরনের উত্তর চান। orders.csv ব্যবহার করে category_report.py লিখুন যা ঠিক এই চারটি কাজ করবে:
- ফাইলটি পড়বে এবং
shape, স্বতন্ত্র ক্যাটাগরিগুলোর সাজানো তালিকা, এবংcategory,quantityওprice-এর মোট মিসিং মানের সংখ্যা প্রিন্ট করবে - একটি
revenueকলাম তৈরি করবে, তারপর প্রতি ক্যাটাগরিতে একটি সারিবিশিষ্ট একটি টেবিল তৈরি করবে যাতে রাজস্বের অবরোহী ক্রমে সাজানো এই কলামগুলো থাকবে:orders(অর্ডার সংখ্যা),units(মোট পরিমাণ),revenue(মোট রাজস্ব),avg_order_units(প্রতি অর্ডারে গড় পরিমাণ) এবংavg_unit_price(রাজস্বকে মোট ইউনিট দিয়ে ভাগ, ২ দশমিক স্থান পর্যন্ত) - প্রতি দিনের রাজস্ব প্রিন্ট করবে, এবং তারপর সর্বোচ্চ রাজস্বের দিনটি প্রিন্ট করবে
- একটি প্রমাণের লাইন প্রিন্ট করবে: ক্যাটাগরি টেবিলের মোট রাজস্ব, দৈনিক টেবিলের মোট রাজস্ব এবং পুরো ফাইলের মোট রাজস্ব
কোড লেখার আগে খাতায় পরিকল্পনা করুন: ২ ও ৩ নম্বর শর্তের জন্য প্রতিটি কলামের কি, ভ্যালু ও এগ্রিগেশনের নাম চিহ্নিত করুন, এবং প্রতিটি ফলাফলে কয়টি সারি থাকা উচিত তা লিখে রাখুন।
সব ঠিক থাকলে প্রোগ্রামটি হুবহু এই আউটপুট দেবে:
Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0এরপর ইচ্ছে করেই কোডটি ভেঙে দেখুন, এবং প্রতিবার কী ঘটে তা পর্যবেক্ষণ করুন:
- ১০০১ নম্বর অর্ডারের ক্যাটাগরি পরিবর্তন করে
"Stationery"(বড় হাতের S) করুন। আপনার টেবিলে এখন কয়টি সারি তৈরি হয়েছে, এবং প্রমাণের লাইনটি কি এখনও পাস করছে? units-এর জন্য"sum"-এর বদলে"mean"ব্যবহার করুন। কোন সংখ্যাটি পরিবর্তিত হয়, এবং এটি এখন কোন প্রশ্নের উত্তর দিচ্ছে?avg_unit_price-কেprice-এর সাধারণ গড় হিসেবে হিসাব করুন। কোন ক্যাটাগরির মান সবচেয়ে বেশি বদলে যায়, এবং কেন?
সমাধান
আগে পরিকল্পনা করুন।
- শর্ত ২: কি
category।orders=order_id-র count;units=quantity-র sum;revenue=revenue-র sum;avg_order_units=quantity-র mean;avg_unit_price= একক কলামের কোনো এগ্রিগেশন নয় — এটি একটি অনুপাত, তাই পরেrevenueওunitsথেকে হিসাব করা হয়। ফাইলে দুটি ক্যাটাগরি রয়েছে, তাই টেবিলে থাকবে ২টি সারি। - শর্ত ৩: কি
date, ভ্যালুrevenue, এগ্রিগেশনsum। চারটি স্বতন্ত্র তারিখ রয়েছে, তাই ৪টি সারি; সেরা দিনটি হলো সর্বোচ্চ মানের ইনডেক্স লেবেল, যা হলোidxmax()। - প্রমাণ: উভয় টেবিলের মোট রাজস্ব ফাইলের মোট ৪৮৯০.০-এর সমান হতে হবে।
category_report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("Rows, columns:", orders.shape)
print("Categories:", sorted(orders["category"].unique()))
print("Missing:", int(orders[["category", "quantity", "price"]].isna().sum().sum()))
print()
orders["revenue"] = orders["quantity"] * orders["price"]
table = orders.groupby("category").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
avg_order_units=("quantity", "mean"),
)
table["avg_unit_price"] = (table["revenue"] / table["units"]).round(2)
table = table.sort_values("revenue", ascending=False)
print(table)
print()
daily = orders.groupby("date")["revenue"].sum()
print(daily)
print("Best day:", daily.idxmax(), "with", daily.max())
print()
print("Proof:", table["revenue"].sum(), daily.sum(), orders["revenue"].sum())Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0পরিকল্পনা অনুযায়ী দুটি সারি ও চারটি সারি এসেছে, এবং তিনটি মোট রাজস্বই হুবহু মিলে গেছে। লক্ষ্য করুন টেবিলটি এমন কী তথ্য দিচ্ছে যা একক কোনো কলাম দিতে পারত না: stationery আইটেম প্রায় পাঁচ গুণ বেশি সংখ্যায় বিক্রি হয়েছে, অথচ accessories প্রায় চার গুণ বেশি রাজস্ব এনে দিয়েছে।
তিনটি ভুলের পরীক্ষা যা দেখায়:
- একটি সারিতে
"Stationery"লিখলে টেবিলে ৩টি সারি তৈরি হয় — বড় হাতেরStationeryএকটিমাত্র অর্ডার নিয়ে নিজস্ব আলাদা গ্রুপ বানায়। প্রমাণের লাইনটি এখনও পাস করে, কারণ কোনো সারি হারিয়ে যায়নি; স্রেফ ভুল গ্রুপে গেছে। এই কারণেই স্বতন্ত্র ক্যাটাগরির লাইনটি সবার ওপরে প্রিন্ট করা হয়: সেখানে এখন তিনটি ক্যাটাগরি দেখা যাবে, এবং কোনো সংখ্যা পড়ার আগেই আপনি সমস্যাটি ধরতে পারবেন। "mean"ব্যবহার করলেunitsহয়ে যায় ৩.৫ এবং ১৬.৭৫ — যাavg_order_units-এর সমান। এটি এখন "একটি সাধারণ অর্ডারের আকার কত" প্রশ্নের উত্তর দেয়, "মোট কত বিক্রি করেছি" প্রশ্নের নয়।avg_unit_price-ও বদলে যায়, কারণ এটি রাজস্বকে ভুল সংখ্যা দিয়ে ভাগ করে।price-এর সাধারণ গড় করলে accessories-এর জন্য আসে(850 + 120 + 850 + 120) / 4 = 485.0, যা আসল ২৭৬.৪৩-এর চেয়ে অনেক বেশি, কারণ একটি ও দুটি ব্যাগের অর্ডারগুলো সাতটি ও চারটি বোতলের অর্ডারের সমান গুরুত্ব পেয়ে গেছে। Stationery-র দাম ১৫.২২ থেকে বেড়ে ২৪.৫ হয় — কম নাটকীয় কেবল এই কারণে যে এর সব দামই কম ছিল। অর্ডার লাইনের ওপর প্রতি-ইউনিটের দামের গড় করাই হলো মূল ফাঁদ; যোগফলকে ভাগ করাই হলো তার সমাধান।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন