DataFrames একত্র করা — concat দিয়ে সাজানো, merge দিয়ে মেলানো
একাধিক টেবিল একত্র করা: concat দিয়ে একই ধরনের মাসের ডেটা পরপর সাজানো, আর merge দিয়ে অন্য টেবিল থেকে তথ্য যোগ করা — সঠিক ধরনের merge বেছে নেওয়া, কোন সারিগুলো মেলেনি তা দেখা, এবং ডুপ্লিকেট কী-র কারণে নিঃশব্দে সারি বেড়ে যাওয়া ঠেকানো।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
দোকানের যাবতীয় ডেটা কখনোই একটা ফাইলে থাকে না। কখনোই না।
মার্চের অর্ডারগুলো রয়েছে orders.csv-তে, যে টেবিলটি আপনি চার নম্বর অধ্যায় থেকে ব্যবহার করছেন। এপ্রিলের অর্ডারগুলো এই সপ্তাহে একটি আলাদা ফাইল হিসেবে এসেছে — orders_april.csv। প্রতিটি পণ্যের পেছনে দোকানের কত খরচ (cost) পড়ে — এবং কোন সরবরাহকারী (supplier) সেটি বিক্রি করে — তা কেনাকাটা দল (purchasing team) তাদের নিজস্ব ফাইলে রাখে, products.csv। অর্ডারের ফাইলে খরচের হিসাব কেউ রাখেনি, কারণ খরচ হলো কোনো পণ্যের নিজস্ব তথ্য, কোনো নির্দিষ্ট অর্ডারের নয়।
এখন দোকানের মালিক একটি প্রশ্ন করলেন: মার্চ ও এপ্রিল মাস মিলিয়ে প্রতিটি সরবরাহকারী মোট কত লাভ (profit) এনে দিয়েছে?
একটিমাত্র ফাইল দিয়ে এর উত্তর দেওয়া অসম্ভব। লাভের হিসাবের জন্য দরকার পণ্যের পরিমাণ ও বিক্রয়মূল্য (যা আছে অর্ডারের ফাইলে), কেনা খরচ (যা আছে পণ্যের ফাইলে), এবং সরবরাহকারীর নাম (সেটিও পণ্যের ফাইলে)। আর অর্ডারগুলো তো আবার দুই মাসের ফাইলে ভাগ করা।
স্প্রেডশিটে হলে আপনি মার্চের সারির নিচে এপ্রিলের সারিগুলো কপি করে পেস্ট করতেন, তারপর নতুন একটি কলামে প্রতিটি সারির জন্য খরচ টেনে আনতে VLOOKUP লিখতেন, নিচে টেনে দিতেন, আর আশা করতেন যেন সব ঠিক থাকে। পান্ডাস এই দুটি কাজই মাত্র দুই লাইনে করে দেয়:
- stack বা একের নিচে অন্যটি সাজানো: একই ধরণের সারি ধারণ করা টেবিলগুলো — যেমন মার্চ ও এপ্রিল — একত্র করে একটি দীর্ঘ টেবিল তৈরি করা:
pd.concat - match বা মিলিয়ে দেখা: একটি সাধারণ কলাম ধরে একটি টেবিলের প্রতিটি সারির সাথে অন্য টেবিলের সঠিক সারির মিল ঘটানো — যেমন প্রতিটি অর্ডারের সাথে তার পণ্য মেলানো — এবং অন্য টেবিলের কলামগুলোকে জুড়ে দেওয়া:
pd.merge
কোডের লাইনগুলো খুবই সংক্ষিপ্ত। কিন্তু এই অধ্যায়টি গুরুত্বপূর্ণ হওয়ার আসল কারণ হলো: কোনো ত্রুটি বা এরর বার্তা ছাড়াই এখানে মারাত্মক ভুল হয়ে যেতে পারে। একটি merge নিঃশব্দে এমন সব অর্ডার বাদ দিয়ে (drop) দিতে পারে যেগুলোর পণ্য লুকআপ টেবিলে নেই; আবার লুকআপ টেবিলে কোনো পণ্য দুবার থাকলে নিঃশব্দে অর্ডারের সারি দ্বিগুণ বা বহুগুণ (duplicate) করে ফেলতে পারে। উভয় ক্ষেত্রেই এমন একটি যোগফল তৈরি হয় যা দেখতে বেশ বাস্তবসম্মত মনে হলেও আসলে সম্পূর্ণ ভুল। তাই এই অধ্যায়ের মূল উদ্দেশ্য হলো merge করার আগে এবং পরে ঠিক কতগুলো সারি থাকার কথা তা সুনির্দিষ্টভাবে জানা — এবং তা যাচাই করে দেখা।
এই অধ্যায় শেষে আপনি পারবেন
- দুটি অপারেশনের পার্থক্য বুঝতে: সারি একের নিচে অন্যটি সাজানো (
concat) এবং কোনো কী ধরে মেলানো (merge) pd.concatদিয়ে টেবিল সাজানো,ignore_index=Trueদিয়ে ইনডেক্স ঠিক করা, এবং কলাম অমিল থাকলে কী ঘটে তা দেখাpd.mergeদিয়ে কোনো কী ধরে দুটি টেবিল একত্র করা, এবং প্রয়োজন অনুযায়ী সচেতনভাবেhow="inner","left","right"বা"outer"বেছে নেওয়াindicator=Trueদিয়ে একটি merge নিরীক্ষা (audit) করা এবংvalidate="many_to_one"দিয়ে সুরক্ষা নিশ্চিত করা- ডুপ্লিকেট কী-র কারণে রো বিস্ফোরণ (row explosion) শনাক্ত করা ও তার সমাধান করা
left_on/right_onদিয়ে ভিন্ন নামের কী-তে merge করা, এবংsuffixesদিয়ে একই নামের কলামের দ্বন্দ্ব মেটানো- merge-এর পরিচিত ভুলগুলো শনাক্ত করা — কী-র ধরনের অমিল, কী কলাম অনুপস্থিত থাকা — এবং নিঃশব্দে ঘটে যাওয়া বিপদগুলো চেনা যা কোনো এরর দেয় না
আগে যা জানা লাগবে: নতুন কলাম যোগ করা।
আগে ফাইলগুলো তৈরি করে নিন
এই অধ্যায়ে চারটি ছোট ফাইল ব্যবহার করা হয়েছে। আপনার স্ক্রিপ্টের পাশেই এগুলো তৈরি করুন।
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.0orders_april.csv — এপ্রিল মাস, কলামগুলো একই। খেয়াল করুন স্ট্যাপলার (stapler): দোকানটি এপ্রিল থেকে এটি বিক্রি শুরু করেছে।
order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0products.csv — প্রতি পণ্যের জন্য একটি করে সারি, যা কেনাকাটা দলের তৈরি। বাস্তব জীবনের মতো এখানেও ইচ্ছে করেই দুটি জিনিস "ভুল" রাখা হয়েছে: নতুন স্ট্যাপলারটি এখনো তালিকায় তোলা হয়নি, এবং এমন একটি মার্কার (marker) রয়েছে যা দোকানটি কখনোই বিক্রি করেনি।
product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alphabranches.csv — দোকানের তিনটি শাখার ব্যবস্থাপক (manager) কারা। এই ফাইলটি অন্য একজন তৈরি করায় কলামটির নাম branch-এর বদলে branch_name রাখা হয়েছে।
branch_name,manager
north,Mira
south,Omar
east,Lenaকোড লেখার আগে
একাধিক টেবিল একত্র করার সময়ই ঝটপট লেখা এক লাইনের কোড সবচেয়ে বেশি বিভ্রান্তিকর ও আত্মবিশ্বাসী ভুল উত্তর তৈরি করে। তাই পরিকল্পনাটাই আগে করতে হয়, এবং এর বেশিরভাগই হলো চোখ বুলিয়ে কয়েকটি প্রশ্নের উত্তর খুঁজে নেওয়া।
১. প্রশ্নটি এক বাক্যে বলুন
"মার্চ এবং এপ্রিল একত্র করে সরবরাহকারী প্রতি লাভ কত।" এই একটি বাক্যই চূড়ান্ত টেবিলের রূপ বলে দেয়: সরবরাহকারী প্রতি একটি সারি, প্রতি সারিতে একটি সংখ্যা। দুটি সরবরাহকারী আছে, Alpha এবং Bravo, তাই ফলাফলে প্রায় দুটি সারি থাকা উচিত। যদি তিনটি হয় বা একটি হয়, তবে বুঝতে হবে মাঝে কোথাও গোলমাল হয়েছে।
২. প্রতিটি টেবিল-জোড়ার জন্য কোন অপারেশন দরকার তা ঠিক করুন
টেবিল জোড়া লাগানোর মূলত দুটি ধরন আছে, আর পরীক্ষাটি খুব সোজা: টেবিল দুটি কি একই ধরণের সারি ধারণ করছে, নাকি একই বিষয়ের বিভিন্ন ভিন্ন তথ্য ধারণ করছে?
- একই কলাম, ভিন্ন সারি — মার্চের অর্ডার + এপ্রিলের অর্ডার। ব্যবহার করুন
pd.concat([a, b])। এর ফলে টেবিলটি নিচের দিকে বাড়ে: সারি বাড়ে, কলাম একই থাকে। - একটি সাধারণ কী (key) কলাম, ভিন্ন তথ্য — অর্ডার + পণ্যতালিকা, যা
productকলাম দিয়ে যুক্ত। ব্যবহার করুনpd.merge(a, b, on="product")। এর ফলে টেবিলটি পাশে বাড়ে: সারি সংখ্যা একই থাকে, কলামের সংখ্যা বাড়ে।
মার্চ ও এপ্রিল উভয়ই "অর্ডার" — কলাম এক, সারি বেশি — তাই এদের একের নিচে অন্যটি সাজানো হয় (stack)। আর অর্ডার এবং পণ্য হলো দুটি ভিন্ন সত্তা যা পণ্যের নাম দিয়ে সম্পর্কিত, তাই এদের মিলিয়ে জোড়া লাগানো হয় (match)।
৩. একত্র করার আগে প্রতিটি টেবিল দেখে নিন
তিন নম্বর অধ্যায়ে আপনি শিখেছিলেন ফাইল পড়ামাত্রই তা যাচাই করে নিতে হয়। একাধিক ফাইলের বেলায় এই অভ্যাসটি আর ঐচ্ছিক থাকে না: ফলাফলের রূপ কেমন হবে তা আঁচ করতে প্রতিটি টেবিলের আকার জানা অপরিহার্য।
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
for name, df in [("march", march), ("april", april), ("products", products)]:
print(name, df.shape, list(df.columns))march (8, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
april (4, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
products (6, 3) ['product', 'cost', 'supplier']মার্চ ও এপ্রিল উভয়েরই হুবহু একই সাতটি কলাম রয়েছে, একই ক্রমে — ওপর-নিচ সাজানোর জন্য একেবারে মোক্ষম। products তাদের সাথে ঠিক একটি কলামের নাম শেয়ার করে: product। এটিই হলো কী (key), যে কলামের ওপর ভিত্তি করে মেলানো হবে।
যাচাই করুন উভয় পাশে কী-এর ডেটা টাইপ (dtype) এক কি না। যে কী এক টেবিলে সংখ্যা এবং অন্য টেবিলে টেক্সট, তাদের কখনোই মেলানো সম্ভব নয়:
print(march["product"].dtype, products["product"].dtype)str strউভয়টিই str। এদের তুলনা করা যাবে।
৪. লুকআপ টেবিলের কী (key) অনন্য (unique) কি না যাচাই করুন
পুরো অধ্যায়ের সবচেয়ে গুরুত্বপূর্ণ যাচাই এটি। প্রতিটি অর্ডারের জন্য একটিমাত্র খরচ প্রয়োজন। products.csv-তে যদি pen দুবার তালিকাভুক্ত থাকে, তবে কলমের প্রতিটি অর্ডার দুবার মিলবে এবং ফলাফলে দুবার করে আবির্ভূত হবে।
print(products["product"].is_unique)
print(products["product"].duplicated().sum())True
0is_unique হলো True এবং কোনো ডুপ্লিকেট নেই (0): প্রতি পণ্যে একটিই সারি। প্রতিটি অর্ডার সর্বোচ্চ একটি পণ্য সারির সাথে মিলতে পারে।
৫. কোন কী-গুলোর জোড়া মিলবে না তা আগে থেকেই দেখুন
merge করার আগেই প্রশ্ন করুন: এমন কোনো অর্ডার কি আছে যার পণ্য products.csv-তে নেই, কিংবা এমন কোনো পণ্য যা কেউ অর্ডারই করেনি? ফিল্টারিং অধ্যায়ের isin দিয়ে দুটিরই উত্তর পাওয়া যায়:
orders = pd.concat([march, april], ignore_index=True)
no_cost = ~orders["product"].isin(products["product"])
print(orders.loc[no_cost, ["order_id", "product"]])
never_sold = ~products["product"].isin(orders["product"])
print(products.loc[never_sold, "product"].tolist())order_id product
10 1011 stapler
['marker']একটি অর্ডারের — স্ট্যাপলার, 1011 — নথিতে কোনো খরচ নেই। আর একটি পণ্য, মার্কার, কখনোই বিক্রি হয়নি। এখন আপনি merge করার আগেই জানেন merge করার সময় কী কী পরিস্থিতির মুখে পড়তে হবে।
৬. ফলাফলের পূর্বাভাস দিন, তারপর সিদ্ধান্ত নিন
পূর্বাভাসটি সংখ্যায় লিখে রাখুন:
orders-এ 8 + 4 = 12টি সারি আছে।products-এ প্রতি পণ্যে একটি সারি আছে, তাই মেলানোর কারণে অর্ডারের সংখ্যা বাড়তে পারে না।- যদি প্রতিটি অর্ডার রাখা হয়, তবে মার্জ করা টেবিলে 12টি সারি থাকবে, যার একটিতে (স্ট্যাপলার) খরচের কোনো মান থাকবে না।
- যদি কেবল মিলে যাওয়া অর্ডারগুলো রাখা হয়, তবে সারি হবে 11টি।
আপনি কোনটি চান? একটি লাভের রিপোর্টের জন্য, স্ট্যাপলারের অর্ডারটি নিঃশব্দে বাদ দিয়ে দিলে বিক্রির পরিমাণ কম দেখানো হবে। বরং সেটিকে অক্ষত রাখা, তার খরচের ঘর ফাঁকা রয়েছে তা দেখা এবং তারপর সিদ্ধান্ত নেওয়া অনেক বেশি বুদ্ধিমানের কাজ — আর ঠিক এই পছন্দটিই করা হয় how= আর্গুমেন্টের মাধ্যমে। merge করার পর সবার প্রথমে যে জিনিসটি আপনি প্রিন্ট করবেন তা হলো এর আকার (shape), এবং আপনি মিলিয়ে দেখবেন তা পূর্বাভাসে সাথে মিলেছে কি না।
পরিকল্পনাটি এটুকুই: এক বাক্যের প্রশ্ন, প্রতিটি জোড়ার অপারেশন নির্ধারণ, প্রতিটি টেবিল পর্যবেক্ষণ, কী-র অনন্যতা যাচাই, অমিল কী শনাক্তকরণ এবং প্রত্যাশিত সারির সংখ্যার পূর্বাভাস। অধ্যায়ের বাকি অংশে রয়েছে এই পরিকল্পনা বাস্তবায়নের হাতিয়ারগুলো।
pd.concat দিয়ে সারিগুলো একের নিচে অন্যটি সাজানো
pd.concat টেবিলের একটি লিস্ট গ্রহণ করে এবং সেগুলোকে একের নিচে অন্যটি বসিয়ে দেয়:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
both = pd.concat([march, april])
print(both.shape)
print(both[["order_id", "date", "product", "quantity"]])(12, 7)
order_id date product quantity
0 1001 2024-03-01 pen 12
1 1002 2024-03-01 notebook 5
2 1003 2024-03-02 bag 2
3 1004 2024-03-02 bottle 7
4 1005 2024-03-03 eraser 30
5 1006 2024-03-03 bag 1
6 1007 2024-03-04 pen 20
7 1008 2024-03-04 bottle 4
0 1009 2024-04-01 pen 15
1 1010 2024-04-01 notebook 3
2 1011 2024-04-02 stapler 6
3 1012 2024-04-02 bag 1বারোটি সারি, সেই একই সাতটি কলাম। পান্ডাস কলামগুলোকে তাদের অবস্থানের বদলে নাম অনুসারে মিলিয়েছে — এপ্রিলের ফাইলে কলামগুলো ভিন্ন ক্রমে থাকলেও সেগুলো সঠিক শিরোনামের নিচেই বসত।
কিন্তু বাঁ দিকের প্রান্তে তাকান। ইনডেক্সটি 0 থেকে 7 পর্যন্ত চলেছে, তারপর আবার 0 থেকে শুরু হয়েছে। concat প্রতিটি টেবিলের নিজস্ব ইনডেক্স ধরে রেখেছে, ফলে 0 থেকে 3 লেবেলগুলো এখন দুবার করে উপস্থিত। এটি কিন্তু কেবল দেখতে খারাপ লাগার মতো কোনো সাধারণ সমস্যা নয়:
print(both.loc[0, ["order_id", "date"]])order_id date
0 1001 2024-03-01
0 1009 2024-04-01আপনি 0 নম্বর সারিটি চেয়েছিলেন কিন্তু পেলেন দুটি সারি। যে কোড ধরে নেয় যে একটি লেবেল মানে একটিই সারি — যেমন loc, ইনডেক্স ধরে পরবর্তীতে কোনো merge, অথবা একটি নির্দিষ্ট সেলের মান পরিবর্তন — তা এখন এমন কিছু করে বসবে যা আপনি চাননি। আপনি এটি পরীক্ষা করে দেখতে পারেন:
print(both.index.is_unique)Falseignore_index=True — সারিগুলোকে নতুন করে নম্বর দেওয়া
পুরোনো ইনডেক্সটি যখন কেবলই সারির ক্রম গণনা করছিল (একটি RangeIndex, যেমনটি এ পর্যন্ত প্রতিটি read_csv-তে হয়েছে), তখন তার নিজস্ব কোনো বিশেষ অর্থ থাকে না। তাই সেটি ফেলে দিন এবং concat-কে ফলাফলটিতে শূন্য থেকে নতুন নম্বর দিতে বলুন:
orders = pd.concat([march, april], ignore_index=True)
print(orders.index.is_unique)
print(orders[["order_id", "date", "product"]].tail(5))True
order_id date product
7 1008 2024-03-04 bottle
8 1009 2024-04-01 pen
9 1010 2024-04-01 notebook
10 1011 2024-04-02 stapler
11 1012 2024-04-02 bagএখন লেবেলগুলো 0 থেকে 11 পর্যন্ত সুশৃঙ্খলভাবে সাজানো, প্রতিটি ঠিক একবার করে। একাধিক ফাইল জোড়া লাগানোর সময় ignore_index=True-কে আপনার স্বাভাবিক নিয়ম বানিয়ে নিন; কেবল তখনই এটি বাদ দেবেন যখন ইনডেক্সের বিশেষ কোনো অর্থ থাকে (যেমন set_index("order_id") করার পর, যেখানে অর্ডারের আইডিগুলো এমনিতেই স্বতন্ত্র)।
প্রতিটি সারি কোথা থেকে এসেছে তা মনে রাখা
একত্র করার পর টেবিলের কোনো কিছুই বলে দেয় না কোন সারিটি কোন মাসের। এখানে তারিখ দেখে বোঝা যাচ্ছে, তবে বাস্তব ক্ষেত্রে প্রায়ই কোনো চিহ্ন থাকে না। কনক্যাট করার আগেই একটি কলাম যোগ করে নিন — যা আপনি আগের অধ্যায়ে শিখেছেন — এবং তথ্যটি সারিগুলোর সাথে অক্ষত থাকবে:
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print(orders["month"].value_counts())month
March 8
April 4
Name: count, dtype: int64গণনাটি নিজেই একটি যাচাই হিসেবে কাজ করে: ৮ + ৪ = ১২, কিছুই হারিয়ে যায়নি।
যখন কলামগুলো হুবহু মেলে না
Concat কলামগুলোকে নাম মিলিয়ে জোড়া লাগায়। কোনো কলাম যদি কেবল কয়েকটি টেবিলে থাকে, তবুও তা ফলাফলে দেখা যাবে — আর যে টেবিলগুলোতে সেটি ছিল না তাদের সারিতে সেখানে NaN বসে যাবে। ধরা যাক একটি বিশেষ অর্ডার হাতে টাইপ করা হয়েছিল, যাতে একটি discount কলাম আছে কিন্তু date, product, category বা price নেই:
manual = pd.DataFrame({
"order_id": [2001],
"branch": ["north"],
"quantity": [3],
"discount": [0.1],
})
mixed = pd.concat([march.head(2), manual], ignore_index=True)
print(mixed[["order_id", "branch", "price", "discount"]])
print(mixed.isna().sum())order_id branch price discount
0 1001 north 15.0 NaN
1 1002 south 60.0 NaN
2 2001 north NaN 0.1
order_id 0
date 1
branch 0
product 1
category 1
quantity 0
price 1
month 1
discount 2
dtype: int64কোনো এরর নেই। ফলাফলটিতে সব কলামের সংযোগ (union) তৈরি হয়েছে, এবং খালি জায়গাগুলো NaN দিয়ে পূরণ হয়েছে। এজন্যই কনক্যাট করার পর সেই একই isna().sum() চালানো উচিত যা আপনি মিসিং ডেটার অধ্যায়ে শিখেছিলেন: কোনো একটি ফাইলে কলামের বানানে সামান্য পার্থক্য থাকলে (যেমন quantity-র বদলে Quantity), কোড ফেল করে না, বরং দুটি আধ-খালি কলাম তৈরি করে। কনক্যাট করার পর কলামের সংখ্যা যদি যেকোনো ইনপুটের চেয়ে বেশি হয়ে যায়, তবে বুঝবেন কোথাও বানানে মিল পড়েনি।
axis=1 — এবং কেন সাধারণত এটি আপনি চাইবেন না
pd.concat([a, b], axis=1) টেবিলগুলোকে ওপর-নিচ সাজানোর বদলে পাশাপাশি বসায়, এবং সারিগুলোকে মেলায় তাদের ইনডেক্স লেবেল দিয়ে। "অর্ডারের সাথে পণ্যের কলামগুলো যোগ করো" শুনতে এটি আকর্ষণীয় মনে হলেও, এটি পণ্যের নামের দিকে বিন্দুমাত্র তাকায় না — একটি টেবিলের 0 নম্বর সারি অন্য টেবিলের 0 নম্বর সারির পাশেই বসে যায়, ভেতরে যা-ই থাকুক না কেন। কোনো কলামের তথ্যের ওপর ভিত্তি করে মেলানোর জন্য সবসময় merge ব্যবহার করুন।
pd.merge দিয়ে সারি মেলানো
merge দুটি টেবিল এবং একটি কী (key) গ্রহণ করে। বাঁ দিকের টেবিলের প্রতিটি সারির জন্য, এটি ডান দিকের টেবিলে একই কী-মান যুক্ত সারিটি খুঁজে বের করে এবং তাদের কলামগুলোকে একসাথে আঠার মতো জোড়া লাগিয়ে দেয়:
products = pd.read_csv("products.csv")
merged = pd.merge(orders, products, on="product")
print(merged.shape)
print(merged[["order_id", "product", "quantity", "price", "cost", "supplier"]])(11, 10)
order_id product quantity price cost supplier
0 1001 pen 12 15.0 9.0 Alpha
1 1002 notebook 5 60.0 40.0 Alpha
2 1003 bag 2 850.0 600.0 Bravo
3 1004 bottle 7 120.0 80.0 Bravo
4 1005 eraser 30 8.0 5.0 Alpha
5 1006 bag 1 850.0 600.0 Bravo
6 1007 pen 20 15.0 9.0 Alpha
7 1008 bottle 4 120.0 80.0 Bravo
8 1009 pen 15 15.0 9.0 Alpha
9 1010 notebook 3 60.0 40.0 Alpha
10 1012 bag 1 850.0 600.0 Bravoপ্রতিটি অর্ডারের পাশে এখন তার খরচ এবং সরবরাহকারীর নাম এসে গেছে, কোনো লুপ ছাড়াই এবং ফাইলগুলোর সারির ক্রমের ওপর নির্ভর না করেই। কী কলাম product একবারই এসেছে, কারণ এটি উভয় পাশেই অভিন্ন ছিল।
এখন পরিকল্পনার সাথে আকারটি মিলিয়ে দেখুন। আমরা পূর্বাভাস দিয়েছিলাম সব কিছু থাকলে ১২টি সারি হবে — কিন্তু এখানে আছে ১১টি। স্ট্যাপলারের অর্ডারটি, 1011, গায়েব হয়ে গেছে। কোনো সতর্কবার্তা নেই, কোনো ত্রুটি নেই।
এটি কোনো বাগ নয়। এটি হলো ডিফল্ট মার্জ, how="inner", যা তার সংজ্ঞায়িত নিয়ম মেনে কাজ করেছে: কেবল সেই কী-গুলোকেই রাখা যা উভয় টেবিলেই উপস্থিত। স্ট্যাপলার products-এ নেই, তাই তার অর্ডারটি টিকতে পারেনি; মার্কার orders-এ নেই, তাই সেটিও দেখা যায়নি। আপনি যদি আগে থেকে ১২টি সারির পূর্বাভাস না রাখতেন, তবে এই হারিয়ে যাওয়া বিক্রির হিসাবটি কখনোই আপনার নজরে আসত না।
চার ধরনের merge
যেসব সারির কী অন্য টেবিলে কোনো সঙ্গী খুঁজে পায় না, তাদের কী হবে তা ঠিক করে how= আর্গুমেন্টটি। দুটি সেটের কথা কল্পনা করুন:
how="inner"(ডিফল্ট) কেবল উভয় টেবিলে পাওয়া কী-গুলো রাখে। কোনো এক পাশে অমিল থাকা সারিগুলো বাদ দিয়ে দেওয়া হয়। কেবল সম্পূর্ণ রেকর্ড চাইলে এটি ব্যবহার করুন।how="left"বাঁ দিকের টেবিলের প্রতিটি সারি অক্ষত রাখে। সঙ্গীহীন বাঁ দিকের সারিগুলো ডান টেবিলের কলামগুলোতেNaNপায়; আর ডান টেবিলের অমিল সারিগুলো বাদ যায়। প্রধান একটি টেবিলে বাড়তি তথ্য যোগ করতে এটি ব্যবহার করা হয় — এটিই সবচেয়ে বেশি ব্যবহৃত বিকল্প।how="right"ঠিক এর উল্টো আয়না: ডান টেবিলের প্রতিটি সারি রাখা হয়, অমিল বাঁ দিকের সারিগুলো বাদ যায়।how="outer"উভয় পাশের প্রতিটি কী সংরক্ষণ করে, এবং শূন্যস্থানগুলোNaNদিয়ে পূরণ করে। দুটি তালিকা পরস্পরের সাথে মিলিয়ে নিরীক্ষা করার জন্য এটি ব্যবহার করুন।
নিজেকে এই প্রশ্নটি করতে হবে: কোন টেবিলটির সারি কোনোভাবেই হারিয়ে যাওয়া চলবে না? এখানে, অর্ডার টেবিল। প্রতিটি অর্ডারই একটি বাস্তব বিক্রি; পণ্যতালিকা হলো কেবল একটি লুকআপ। সুতরাং সঠিক মার্জটি হলো লেফট মার্জ, যেখানে অর্ডার থাকবে বাঁ দিকে:
merged = pd.merge(orders, products, on="product", how="left")
print(merged.shape)
print(merged[["order_id", "product", "cost", "supplier"]])(12, 10)
order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 Bravo
3 1004 bottle 80.0 Bravo
4 1005 eraser 5.0 Alpha
5 1006 bag 600.0 Bravo
6 1007 pen 9.0 Alpha
7 1008 bottle 80.0 Bravo
8 1009 pen 9.0 Alpha
9 1010 notebook 40.0 Alpha
10 1011 stapler NaN NaN
11 1012 bag 600.0 Bravoবারোটি সারি, যেমনটি পূর্বাভাসে ছিল। স্ট্যাপলারের অর্ডারটি এখনও সেখানে রয়েছে, খরচ এবং সরবরাহকারীর ঘরে NaN সহ — যা অত্যন্ত সৎ ও স্বচ্ছ: খরচটি অজানা, এবং আপনি স্পষ্টভাবে তা দেখতে পাচ্ছেন। চোখের সামনে দেখতে পাওয়া একটি খালি মান না দেখতে পেয়ে হারিয়ে যাওয়া একটি পুরো সারির চেয়ে শতগুণ শ্রেয়। এখান থেকে মিসিং ডেটার অধ্যায়ের হাতিয়ারগুলো কাজে লাগানো যায়: আপনি isna().sum() দিয়ে ফাঁকা ঘরগুলো গুনতে পারেন, কেনাকাটা দল খরচ জানালে তা পূরণ করতে পারেন, অথবা এই অর্ডারটির কথা আলাদাভাবে রিপোর্ট করতে পারেন।
একটি রাইট মার্জ হলো এর উল্টো রূপ — প্রতিটি পণ্য সংরক্ষিত হয়, আর মার্কার থাকে যার কোনো অর্ডার নেই:
right = pd.merge(orders, products, on="product", how="right")
print(right.shape)
print(right[["order_id", "product", "cost"]].tail(3))(12, 10)
order_id product cost
9 1008.0 bottle 80.0
10 1005.0 eraser 5.0
11 NaN marker 25.0খেয়াল করুন order_id পূর্ণসংখ্যা থেকে 1008.0, 1005.0 ইত্যাদিতে রূপান্তরিত হয়ে গেছে। মার্কারের সারিতে কোনো অর্ডারের আইডি নেই, তাই কলামটিতে এখন একটি NaN এসেছে — আর মিসিং ডেটার অধ্যায়ে যেমনটি দেখেছেন, একটি পূর্ণসংখ্যার কলামে NaN ঢুকলে তা float64 হয়ে যায়। মার্জ করার পর যদি হঠাৎ দেখতে পান আইডিগুলোতে একটি .0 যোগ হয়েছে, তবে বুঝতে হবে কিছু সারি কোনো জোড়া খুঁজে পায়নি।
indicator=True — প্রতিটি সারি কোথা থেকে এসেছে তা দেখা
একটি আউটার মার্জ উভয় পাশের সবকিছু সংরক্ষণ করে। এর সাথে indicator=True যোগ করলে পান্ডাস _merge নামে একটি কলাম যোগ করে, যা প্রতি সারির জন্য বলে দেয় তার কী উভয় টেবিলে পাওয়া গেছে (both), কেবল বাঁ দিকে (left_only), নাকি কেবল ডান দিকে (right_only):
audit = pd.merge(orders, products, on="product", how="outer", indicator=True)
print(audit[["order_id", "product", "cost", "_merge"]])
print(audit["_merge"].value_counts())order_id product cost _merge
0 1003.0 bag 600.0 both
1 1006.0 bag 600.0 both
2 1012.0 bag 600.0 both
3 1004.0 bottle 80.0 both
4 1008.0 bottle 80.0 both
5 1005.0 eraser 5.0 both
6 NaN marker 25.0 right_only
7 1002.0 notebook 40.0 both
8 1010.0 notebook 40.0 both
9 1001.0 pen 9.0 both
10 1007.0 pen 9.0 both
11 1009.0 pen 9.0 both
12 1011.0 stapler NaN left_only
_merge
both 11
left_only 1
right_only 1
Name: count, dtype: int64এটি হলো মার্জ অপারেশনের নিজস্ব রিপোর্ট কার্ড। এগারোটি সারি মিলেছে; একটি অর্ডারের কোনো পণ্য সারি নেই (left_only: স্ট্যাপলার); একটি পণ্যের কোনো অর্ডার নেই (right_only: মার্কার)। এটি ঠিক পরিকল্পনার ৫ নম্বর ধাপের তথ্য, যা স্বয়ং মার্জ অপারেশন তৈরি করে দিয়েছে।
আউটার মার্জ সারির মূল ক্রম ধরে রাখার বদলে কী অনুযায়ী সারিগুলোকে সাজিয়ে (sort) নিয়েছে — bag, bottle, eraser, …। ইনার এবং লেফট মার্জ বাঁ দিকের টেবিলের ক্রম অক্ষত রাখে; আউটার মার্জ সেই নিশ্চয়তা দেয় না। যদি ক্রম গুরুত্বপূর্ণ হয়, তবে সাত নম্বর অধ্যায়ের মতো মার্জ করার পর সাজিয়ে নিন।
_merge কলামটি সাধারণ ডেটার মতোই, তাই এর ওপর ভিত্তি করে ফিল্টার করা যায়। সমস্যাগুলো এক লাইনে তালিকাভুক্ত করা সম্ভব:
problems = audit[audit["_merge"] != "both"]
print(problems[["order_id", "product", "_merge"]])order_id product _merge
6 NaN marker right_only
12 1011.0 stapler left_onlyঅন্য কারো তৈরি করা ডেটা মার্জ করার সময় সবসময় এই নিরীক্ষাটি চালান। সমস্যার তালিকা যখন শূন্য হবে, তখন আপনার হাতে প্রমাণ থাকবে যে ফাইল দুটি পুরোপুরি সামঞ্জস্যপূর্ণ।
সেই নিঃশব্দ বিপদ: ডুপ্লিকেট কী
এতক্ষণ products-এ প্রতি পণ্যে একটি করে সারি ছিল। ধরুন কেনাকাটা দল একটু বেশি দামে কলমের দ্বিতীয় একজন সরবরাহকারী যোগ করল এবং তালিকার নিচে একটি সারি জুড়ে দিল, ফলে pen দুবার উপস্থিত হলো:
products_dup = pd.concat(
[products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Gamma"]})],
ignore_index=True,
)
print(products_dup["product"].is_unique)
exploded = pd.merge(march, products_dup, on="product", how="left")
print(march.shape, "->", exploded.shape)
print(exploded[["order_id", "product", "quantity", "cost", "supplier"]])False
(8, 8) -> (10, 10)
order_id product quantity cost supplier
0 1001 pen 12 9.0 Alpha
1 1001 pen 12 10.0 Gamma
2 1002 notebook 5 40.0 Alpha
3 1003 bag 2 600.0 Bravo
4 1004 bottle 7 80.0 Bravo
5 1005 eraser 30 5.0 Alpha
6 1006 bag 1 600.0 Bravo
7 1007 pen 20 9.0 Alpha
8 1007 pen 20 10.0 Gamma
9 1008 bottle 4 80.0 Bravoমার্চে ৮টি অর্ডার ছিল; মার্জ ফিরিয়ে দিল ১০টি সারি। অর্ডার 1001 এবং 1007 — দুটি কলম অর্ডার — প্রত্যেকে দুবার করে এসেছে, প্রতি পেন সারির জন্য একবার করে। কোনো কিছুতেই ত্রুটি দেখা যায়নি। কিন্তু এখন পরিমাণের যোগফল নিন, দেখবেন কলম দুবার গণনা করা হয়েছে: ৩২টি অতিরিক্ত কলম যা কখনোই বিক্রি হয়নি।
print(march["quantity"].sum(), exploded["quantity"].sum())81 11381-এর বিপরীতে 113। এটিকে বলে রো বিস্ফোরণ (row explosion), এবং এটি মার্জের সবচেয়ে মারাত্মক ভুল কারণ ফলাফলটি দেখতে একেবারেই স্বাভাবিক মনে হয়। এর পেছনের নিয়মটি সোজা: মার্জ বাঁ দিকের প্রতিটি মিলে যাওয়া সারির সাথে ডান দিকের প্রতিটি মিলে যাওয়া সারির জোড়া তৈরি করে। ওয়ান-টু-ওয়ান বা মেনি-টু-ওয়ান মার্জে সারির সংখ্যা অপরিবর্তিত থাকে; উভয় পাশে একটি কী পুনরাবৃত্ত হলেই তা গুণ হয়ে সারি বাড়িয়ে দেয়।
একটি মার্জের ফলে সারি বেড়ে গেলে তা কখনো নিজে থেকে ঘোষণা দেয় না। এর একমাত্র লক্ষণ হলো একটি স্ফীত মোট যোগফল, আর খুব বড় একটি যোগফল চোখে সহজে অস্বাভাবিক ঠেকে না। প্রতিটি মার্জের আগে ও পরে len() তুলনা করুন — এই একটি সতর্কতাই এই বিপদটি ধরতে পারে।validate= — পান্ডাসকে দিয়ে নিজের ধারণা যাচাই করানো
আপনি যে সম্পর্কের প্রত্যাশা করছেন তা স্পষ্টভাবে কোডে লিখে দিতে পারেন, আর ডেটা সেই নিয়ম ভাঙলে পান্ডাস মার্জ করতে অস্বীকৃতি জানাবে। অনেকগুলো অর্ডার একটি একক পণ্য ভাগ করে নেয়, তাই এই মার্জটি হওয়া উচিত many-to-one:
try:
pd.merge(march, products_dup, on="product", how="left", validate="many_to_one")
except Exception as error:
print(type(error).__name__ + ":", error)MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
Duplicates in right:
product
pen ...একটি MergeError, এমনকি এটি দোষী কী-টির নামও উল্লেখ করে দেয়। অন্যান্য সম্ভাব্য মানগুলো হলো "one_to_one", "one_to_many" এবং "many_to_many"। validate= লিখতে কোনো অতিরিক্ত খরচ নেই এবং এটি নিঃশব্দ ভুলকে একটি স্পষ্ট এররে রূপান্তর করে, তাই লুকআপ টেবিলের সাথে প্রতিটি মার্জে এটি ব্যবহার করা উচিত।
সমাধান: কোন ডুপ্লিকেটটি সঠিক তা বেছে নেওয়া
এরর আপনাকে জানায় যে কোথাও ভুল হয়েছে; কিন্তু কী সঠিক তা নির্ধারণ করা আপনার দায়িত্ব। প্রথমে ডুপ্লিকেটগুলো দেখুন:
print(products_dup[products_dup["product"].duplicated(keep=False)])product cost supplier
0 pen 9.0 Alpha
6 pen 10.0 Gammaduplicated(keep=False) প্রতিটি কপিকে চিহ্নিত করে, কেবল দ্বিতীয়টিকে নয়। এখন একটি ব্যবসায়িক সিদ্ধান্ত নিতে হবে। যদি নতুন সারিটি পুরোনোটির স্থলাভিষিক্ত হয়, তবে শেষ কপিটি রাখুন:
products_clean = products_dup.drop_duplicates(subset="product", keep="last")
fixed = pd.merge(march, products_clean, on="product", how="left", validate="many_to_one")
print(fixed.shape)
print(fixed.loc[fixed["product"] == "pen", ["order_id", "cost", "supplier"]])(8, 10)
order_id cost supplier
0 1001 10.0 Gamma
6 1007 10.0 Gammaআবার ৮টি সারিতে ফিরে এসেছে, প্রতি অর্ডারে একটি করে। আর যদি উভয় সরবরাহকারীই কলম বিক্রি করে এবং অর্ডারে তা উল্লেখ থাকে, তবে অর্ডারের ফাইলেও supplier থাকা প্রয়োজন, এবং তখন মার্জ কী হবে দুটি কলাম: on=["product", "supplier"]। কোড নিজে এই সিদ্ধান্ত নিতে পারে না; পরিকল্পনাই তা পারে।
যখন কী কলামগুলোর নাম আলাদা হয়
branches.csv-তে কলামটির নাম branch_name, অথচ অর্ডারে এর নাম branch। on=-এর জন্য উভয় পাশে একই নাম থাকা দরকার, তাই left_on= এবং right_on= দিয়ে প্রতিটি পাশের নাম আলাদাভাবে নির্দিষ্ট করে দিন:
branches = pd.read_csv("branches.csv")
with_manager = pd.merge(orders, branches, left_on="branch", right_on="branch_name", how="left")
print(list(with_manager.columns))['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'month', 'branch_name', 'manager']উভয় কী কলামই রাখা হয়েছে, কারণ পান্ডাস নিজে থেকে জানতে পারে না যে আপনি এদের একই কলাম মনে করেন। branch এবং branch_name এখন হুবহু একই মান বহন করছে, তাই একটি কলাম মুছে ফেলুন:
with_manager = with_manager.drop(columns="branch_name")
print(with_manager[["order_id", "branch", "manager"]].head(4))order_id branch manager
0 1001 north Mira
1 1002 south Omar
2 1003 north Mira
3 1004 east Lenaএর বিকল্প হলো মার্জ করার আগেই কলামটির নাম পরিবর্তন করে নেওয়া — branches.rename(columns={"branch_name": "branch"}) — এবং তারপর সরাসরি on="branch" ব্যবহার করা। দুটি উপায়ই চমৎকার; আগে নাম বদলালে পরে আর বাড়তি কলাম মোছার ঝামেলা থাকে না।
যখন দুটি টেবিলেই একই নামের সাধারণ কলাম থাকে
যদি কোনো সাধারণ কলাম (যা কী নয়) উভয় টেবিলেই থেকে থাকে, তবে পান্ডাস এক টেবিলে price নামের দুটি কলাম রাখতে পারে না। তাই সেগুলোর নামের শেষে _x (বাঁ দিক) এবং _y (ডান দিক) সাফিক্স যোগ করে দেয়। ধরা যাক একটি খুচরা মূল্যের তালিকা রয়েছে যাতে কলামটির নামও price:
list_prices = pd.DataFrame({"product": ["pen", "bag"], "price": [14.0, 800.0]})
compared = pd.merge(march, list_prices, on="product")
print(compared[["order_id", "product", "price_x", "price_y"]])order_id product price_x price_y
0 1001 pen 15.0 14.0
1 1003 bag 850.0 800.0
2 1006 bag 850.0 800.0
3 1007 pen 15.0 14.0price_x এবং price_y কাজ চালালেও তিন সপ্তাহ পর কেউ মনে রাখতে পারবে না কোনটি আসল বিক্রয়মূল্য আর কোনটি তালিকাভুক্ত মূল্য। suffixes= ব্যবহার করে নিজেই অর্থপূর্ণ নাম দিন:
compared = pd.merge(march, list_prices, on="product", suffixes=("_sold", "_list"))
print(compared[["order_id", "price_sold", "price_list"]])order_id price_sold price_list
0 1001 15.0 14.0
1 1003 850.0 800.0
2 1006 850.0 800.0
3 1007 15.0 14.0এখন কলামের নামগুলো তাদের প্রকৃত অর্থ প্রকাশ করছে, এবং প্রতিটি অর্ডারে প্রদত্ত ছাড় হিসাব করা একদম সহজ: price_list - price_sold। (পরবর্তীতে কোনো কোড যদি compared["price"] খোঁজে তবে KeyError খাবে, কারণ মার্জের পর এই অবিকল নামে কোনো কলাম আর থাকে না — নামগুলো সচেতনভাবে বেছে নেওয়ার এটি আরেকটি কারণ।)
join — সংক্ষেপে ইনডেক্স ধরে merge করা
DataFrames-এর একটি .join() মেথডও রয়েছে। এটি আসলে এমন একটি মার্জ যা ডান টেবিলের ইনডেক্সকে কী হিসেবে ব্যবহার করে — যা বেশ সুবিধাজনক যখন লুকআপ টেবিলটি আগেই সেই কী দিয়ে ইনডেক্স করা থাকে:
by_product = products.set_index("product")
joined = orders.join(by_product, on="product")
print(joined[["order_id", "product", "cost", "supplier"]].head(3))order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 Bravojoin স্বাভাবিকভাবেই how="left" ধরে কাজ করে। এটি pd.merge(..., how="left")-এর মতোই কাজ সম্পন্ন করে; তবে merge অনেক বেশি সার্বজনীন হাতিয়ার, এবং এই কোর্সের বাকি অংশে এটিই ব্যবহার করা হবে।
একটি সম্পূর্ণ উদাহরণ
আবার সেই মালিকের প্রশ্নে ফিরে যাওয়া যাক: মার্চ ও এপ্রিল মাস মিলিয়ে প্রতিটি সরবরাহকারীর কাছ থেকে কত লাভ এসেছে, যেখানে প্রতিটি অর্ডারের সঠিক হিসাব থাকবে।
supplier_profit.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
# 1. Stack the two months, remembering where each row came from.
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. The lookup key must be unique, or the merge will duplicate orders.
assert products["product"].is_unique
# 3. Attach cost and supplier to every order. Keep every order.
sales = pd.merge(
orders, products, on="product", how="left",
validate="many_to_one", indicator=True,
)
print("after merge:", sales.shape)
assert len(sales) == len(orders)
# 4. Report the orders that found no product row, then set them aside.
unmatched = sales[sales["_merge"] == "left_only"]
print("no cost on file:", unmatched["order_id"].tolist(), unmatched["product"].tolist())
sales = sales[sales["_merge"] == "both"].drop(columns="_merge")
# 5. Profit per row, then per supplier and month.
sales["profit"] = sales["quantity"] * (sales["price"] - sales["cost"])
report = (
sales.groupby(["supplier", "month"], as_index=False)["profit"]
.sum()
.sort_values(["supplier", "profit"], ascending=[True, False])
.reset_index(drop=True)
)
print()
print(report)
print()
print(sales.groupby("supplier")["profit"].sum().sort_values(ascending=False))orders: (12, 8)
after merge: (12, 11)
no cost on file: [1011] ['stapler']
supplier month profit
0 Alpha March 382.0
1 Alpha April 150.0
2 Bravo March 1190.0
3 Bravo April 250.0
supplier
Bravo 1440.0
Alpha 532.0
Name: profit, dtype: float64কোডটি কেন এভাবে লেখা হলো:
- প্রতিটি কম্বাইনিং ধাপের পরেই আকার (
shape) যাচাই করা হয়েছে।orders: (12, 8)ওপর-নিচ সাজানোর বিষয়টি নিশ্চিত করে (৮ + ৪ সারি, ৭টি কলাম +month)।after merge: (12, 11)নিশ্চিত করে যে কোনো সারি হারিয়ে যায়নি বা দ্বিগুণ হয়নি — ৩টি নতুন কলাম যুক্ত হয়েছে:cost,supplier,_merge। আকার যাচাই করাই হলো প্রতিটি ধাপ পরিকল্পনামতো কাজ করেছে কি না তা নিশ্চিত করার সবচেয়ে সহজ ও কার্যকর উপায়। - অনুমানগুলো কোড হিসেবে রূপায়িত হয়েছে।
assert products["product"].is_uniqueএবংvalidate="many_to_one"উভয়ই রো বিস্ফোরণ থেকে সুরক্ষা দেয়;assert len(sales) == len(orders)পরিকল্পনার পূর্বাভাস রক্ষা করে। যদি কেউ আগামী মাসের পণ্যতালিকায় কোনো ডুপ্লিকেটসহ ফাইল পাঠায়, তবে ভুল স্ফীত লাভের হিসাব ছাপানোর বদলে স্ক্রিপ্টটি সেখানেই থেমে যাবে। how="left"স্ট্যাপলার অর্ডারটি বাঁচিয়ে রাখে, এবংindicator=Trueসেটির নাম সামনে আনে। স্ক্রিপ্টটি সেটিকে চুপচাপ মুছে দেয়নি; এটি স্পষ্টভাবে[1011] ['stapler']প্রিন্ট করেছে, যাতে মালিক জানতে পারেন যে কেনাকাটা দল খরচ না দেওয়া পর্যন্ত এপ্রিলের মোট হিসাবে একটি বিক্রির হিসাব বাদ রয়েছে। এই লাইনটি আউটপুটের একটি অংশ, কোনো ডিবাগিং জঞ্জাল নয়।concat-এর আগেইmonthকলামটি যোগ করা হয়েছিল। অন্যথায় রিপোর্টে মাসভিত্তিক ভাগ করতে তারিখগুলো আলাদা করে রূপান্তর করতে হতো — যা করা যেত ঠিকই, তবে একই ফলের জন্য বাড়তি পরিশ্রম হতো।- লাভের হিসাব মার্জ করার পর করা হয়েছে, কারণ এর জন্য উভয় ফাইলের কলাম প্রয়োজন। এটিই প্রাকৃতিক ক্রম: একত্র করা, তারপর নতুন কলাম যোগ করা (অধ্যায় নয়), তারপর গ্রুপ করা (অধ্যায় আট), তারপর সাজানো (অধ্যায় সাত)।
চূড়ান্ত উত্তর: Bravo লাভ এনেছে 1,440 এবং Alpha এনেছে 532, আর এপ্রিলের একটি বিক্রি এখনও খরচের অপেক্ষায় রয়েছে। Bravo কম পণ্য বিক্রি করে, কিন্তু কলম ও ইরেজারের চেয়ে ব্যাগ ও বোতলে প্রতি পণ্যে লাভের মার্জিন অনেক বেশি।
কিছু ভাঙা অবস্থা ও তার সমাধান
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat
উভয় পাশে কী-র ডেটা টাইপ আলাদা। এখানে ডেলিভারি দলের পাঠানো একটি ফাইল রয়েছে যেখানে অর্ডার আইডির শুরুতে # চিহ্ন দেওয়া হয়েছে। এটিকে deliveries.csv নামে সেভ করুন — এটি "নিজে করুন" বিভাগেও ব্যবহার করা হবে। (মিসিং ডেটার অধ্যায়ে একই নামের রাইডারদের লগ ফাইলটি যদি এখনো থেকে থাকে, তবে সেটি ওভাররাইট করুন; এটি একটি ভিন্ন ফাইল।)
order_id,status
#1001,delivered
#1002,delivered
#1003,returned
#1005,delivered
#1006,pendingimport pandas as pd
march = pd.read_csv("orders.csv")
deliveries = pd.read_csv("deliveries.csv")
print(march["order_id"].dtype, deliveries["order_id"].dtype)
try:
pd.merge(march, deliveries, on="order_id", how="left")
except ValueError as error:
print("ValueError:", error)int64 str
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat1001 এবং "#1001" দুটি ভিন্ন মান, এবং পান্ডাস কোনো অনুমান করতে রাজি নয়। (pd.concat সম্পর্কিত ইঙ্গিতটি একটি ভিন্ন পরিস্থিতির জন্য; এটিকে উপেক্ষা করুন।) সমাধান হলো কী-গুলোকে একই ধরন এবং একই মানে রূপান্তর করা — # সরিয়ে সংখ্যায় রূপান্তর করুন:
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
status = pd.merge(march, deliveries, on="order_id", how="left")
print(status[["order_id", "product", "status"]])order_id product status
0 1001 pen delivered
1 1002 notebook delivered
2 1003 bag returned
3 1004 bottle NaN
4 1005 eraser delivered
5 1006 bag pending
6 1007 pen NaN
7 1008 bottle NaNযেসব অর্ডারের কোনো ডেলিভারি রেকর্ড নেই তারা status-এর ঘরে NaN পায় — লেফট মার্জ সেগুলোকে বাঁচিয়ে রাখে, যা ঠিক সেটাই যখন প্রশ্ন হয় "কোন কোন অর্ডার ডেলিভারি করা হয়নি?"
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge আপনি validate="many_to_one" ব্যবহার করেছেন এবং ডান টেবিলটিতে একটি কী পুনরাবৃত্ত হয়েছে। বার্তায় ডুপ্লিকেটগুলোর তালিকা দেওয়া থাকে। এররটি সরানোর জন্য validate মুছে ফেলবেন না — বরং duplicated(keep=False) দিয়ে ডুপ্লিকেট সারিগুলো দেখুন এবং রো বিস্ফোরণ সেকশনের মতো সিদ্ধান্ত নিন কোনটি সঠিক।
KeyError: 'branch' on=-এ দেওয়া কলামটি কোনো একটি টেবিলে অনুপস্থিত:
branches = pd.read_csv("branches.csv")
try:
pd.merge(march, branches, on="branch")
except KeyError as error:
print("KeyError:", error)KeyError: 'branch'branches ফাইলে এর নাম branch_name। উভয় টেবিলের জন্য list(df.columns) প্রিন্ট করুন; left_on="branch", right_on="branch_name" ব্যবহার করুন, অথবা একপাশের নাম পরিবর্তন করুন। কলামের নামের শেষে একটি অদৃশ্য স্পেস ("branch ") থাকলেও একই এরর হয়, যা কেবল লিস্ট আকারে দেখলে ধরা পড়ে।
মার্জ চলল ঠিকই, কিন্তু নতুন কলামগুলো সব NaN কোনো এরর নেই, অথচ প্রতিটি সারিই মেলেনি। কী-গুলো দেখতে একই মনে হলেও আসলে এক নয়: শেষে স্পেস থাকা "north ", বড় হাতের অক্ষরে লেখা "South", অথবা এক ফাইলে টেক্সট হিসেবে থাকা আইডি অন্য ফাইলে প্রিফিক্সযুক্ত থাকা। মার্জ করার আগেই isin দিয়ে যাচাই করুন:
branches_messy = pd.DataFrame({"branch_name": ["north ", "South", "east"], "manager": ["Mira", "Omar", "Lena"]})
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")
branches_messy["branch_name"] = branches_messy["branch_name"].str.strip().str.lower()
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")2 of 8 orders match
8 of 8 orders matchপরিষ্কার করার আগে আটটির মধ্যে দুটি মিলেছিল, পরে আটটিতে আটটিই মিলেছে। মার্জ করার আগেই উভয় পাশের কী একইভাবে পরিষ্কার করুন — str.strip() এবং একই কেস (case) ব্যবহার করুন।
মার্জের পর সারির সংখ্যা বেড়ে গেল ডান টেবিলটিতে একটি ডুপ্লিকেট কী রয়েছে; ওপরের "সেই নিঃশব্দ বিপদ" অনুচ্ছেদটি দেখুন। প্রতিটি মার্জের আগে ও পরে len() তুলনা করুন, এবং validate= ব্যবহার করুন।
মার্জের পর কিছু সারি হারিয়ে গেল আপনি ডিফল্ট how="inner" ব্যবহার করেছেন, এবং কিছু কী-র কোনো জোড়া ছিল না। মূল টেবিলটিকে বাঁ দিকে রেখে how="left" ব্যবহার করুন, এবং কোন সারিগুলো মেলেনি তা দেখতে indicator=True দিন।
TypeError: concat() takes 1 positional argument but 2 were given
try:
pd.concat(march, deliveries)
except TypeError as error:
print("TypeError:", error)TypeError: concat() takes 1 positional argument but 2 were givenconcat একটিমাত্র আর্গুমেন্ট গ্রহণ করে, যা হলো টেবিলগুলোর একটি তালিকা (list)। লিখুন pd.concat([march, april]) — তৃতীয় বন্ধনী [] দেওয়াটাই এখানে আসল বিষয়।
concat-এর পর ডুপ্লিকেট ইনডেক্স, এবং loc দুটি সারি ফিরিয়ে দিচ্ছে প্রতিটি ইনপুট তার নিজস্ব 0, 1, 2, … ধরে রেখেছে। ignore_index=True যোগ করুন।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
মার্চে ৮টি অর্ডার আছে এবং এপ্রিলে ৪টি। কী প্রিন্ট হবে?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """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
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
both = pd.concat([march, april])
print(both.shape, both.index[-1])- A(12, 7) 11
- B(12, 7) 3
- C(12, 14) 3
- D(8, 7) 7
পণ্যতালিকায় কেউ pen-এর জন্য দ্বিতীয় একটি সারি যোগ করেছে। কী প্রিন্ট হবে?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """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
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
products = pd.concat([products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Charlie"]})])
report = pd.merge(march, products, on="product", how="left")
print(len(march), len(report))- A8 8
- B8 6
- C8 10 — প্রতিটি pen অর্ডার দুটি pen সারির সাথেই মিলেছে এবং দ্বিগুণ হয়েছে, কোনো ত্রুটি ছাড়াই
- Dডুপ্লিকেট কী সম্পর্কিত একটি `MergeError`
মার্চের প্রতিটি অর্ডারের জন্য তার পণ্যের খরচ (cost) প্রয়োজন, এবং কোনো অর্ডারই হারানো যাবে না, এমনকি পণ্যটি তালিকায় না থাকলেও। আপনি কোন how= ব্যবহার করবেন?
- A`how="inner"`
- B`how="right"`
- C`how="outer"`
- D`how="left"`, যেখানে অর্ডারগুলো থাকবে বাঁ দিকের টেবিলে
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
ডেলিভারি দল deliveries.csv ফাইলটি পাঠিয়েছে ("কিছু ভাঙা অবস্থা ও তার সমাধান" অংশে দেখানো) — যাদের রেকর্ড আছে তাদের প্রতি অর্ডারে একটি করে সারি রয়েছে, যেখানে অর্ডার আইডি লেখা #1001 ফরম্যাটে। দোকানের মালিক মার্চ ও এপ্রিল মিলিয়ে একটি সামগ্রিক ডেলিভারি রিপোর্ট চেয়েছেন।
delivery_report.py এমনভাবে লিখুন যাতে:
- উভয় মাসের প্রতিটি অর্ডার অক্ষত থাকে — প্রতি অর্ডারে ঠিক একটি করে সারি, সারির সংখ্যার ওপর
assertদিয়ে যাচাই করা, এবং মার্জটিকেvalidate=দিয়ে সুরক্ষিত ওindicator=Trueদিয়ে নিরীক্ষা করা। - কীগুলো সত্যিই মেলে — ডেলিভারি কী পরিষ্কার করে
order_id-র সমমানের করা হয়েছে, এবং ডেলিভারি ফাইলে কোনো অর্ডার দুবার এলে স্ক্রিপ্টটি থেমে যায়। - অনুপস্থিত থাকার অর্থ "রেকর্ড নেই" — যেসব অর্ডারের কোনো ডেলিভারি সারি নেই তারা স্ট্যাটাস হিসেবে
"no record"পায়, কখনোই কোনো নিঃশব্দNaNনয়। - এটি প্রিন্ট করবে, ঠিক এই ক্রমে: সাজানোর পরের আকার (stacked shape), মার্জের পরের আকার (merged shape),
_merge-এর গণনা, প্রতি স্ট্যাটাসে অর্ডারের সংখ্যা, রেকর্ডহীন অর্ডারগুলো (order_id,branch,product,order_idঅনুযায়ী সাজানো), এবং প্রতি শাখায় ডেলিভারি হওয়া অর্ডারের সংখ্যা।
সব ঠিক থাকলে আউটপুটটি হুবহু হবে:
orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64কোনো কোড লেখার আগে, শুধুমাত্র ফাইলগুলো দেখে আউটপুটের প্রতিটি সংখ্যা ব্যাখ্যা করুন: কনক্যাট করার পর কেন ১২টি সারি, মার্জ করার পরও কেন ১২টি সারি, এবং কেন ৭টি অর্ডারের কোনো রেকর্ড নেই। এটি না পারলে পরিকল্পনা এখনও অসমাপ্ত।
তারপর ইচ্ছে করে কোডটি দুবার ভাঙুন:
.str.removeprefix("#")সরিয়ে দিন। কোন এররটি আসে এবং কোন লাইনে সেটি ঘটে?deliveries.csv-তে#1003-এর জন্য দ্বিতীয় একটি সারি যোগ করুন (যেমন#1003,delivered)। এখনvalidate=কী করে? এটি ছাড়া রিপোর্টটি কী দেখাত?
সমাধান
পরিকল্পনা। প্রশ্নটি হলো: "মার্চ ও এপ্রিলের প্রতিটি অর্ডারের ডেলিভারি স্ট্যাটাস কী?" — সুতরাং ফলাফলে প্রতি অর্ডারে একটি করে সারি থাকবে, মোট ১২টি সারি। মার্চ ও এপ্রিলের কলামগুলো অভিন্ন: concat এবং ignore_index=True দিয়ে এদের ওপর-নিচ সাজান (৮ + ৪ = ১২)। ডেলিভারি ফাইলটি অর্ডার আইডি দিয়ে চিহ্নিত, তবে # যুক্ত টেক্সট হিসেবে, তাই পূর্ণসংখ্যা order_id-র সাথে মেলানোর আগে কী পরিষ্কার করা দরকার। প্রতিটি অর্ডার এতে সর্বোচ্চ একবারই থাকা উচিত, তাই সম্পর্কটি হলো one-to-one (প্রতিটি অর্ডারে সর্বোচ্চ একটি ডেলিভারি সারি, প্রতি ডেলিভারি সারিতে একটি অর্ডার)। প্রতিটি অর্ডার টিকে থাকা আবশ্যক, তাই অর্ডারগুলো বাঁ দিকে যাবে এবং মার্জটি হবে how="left"। পাঁচটি ডেলিভারি সারি আছে, যার সবগুলোই মার্চের, সুতরাং ১২ − ৫ = ৭টি অর্ডারের কোনো রেকর্ড থাকবে না।
delivery_report.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
deliveries = pd.read_csv("deliveries.csv")
# 1. Stack the months.
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. Clean the key, then check it.
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
assert deliveries["order_id"].is_unique, "an order appears twice in deliveries.csv"
# 3. Keep every order.
report = pd.merge(
orders, deliveries, on="order_id", how="left",
validate="one_to_one", indicator=True,
)
print("after merge:", report.shape)
assert len(report) == len(orders)
print(report["_merge"].value_counts())
# 4. Missing status means the delivery team has no record.
report["status"] = report["status"].fillna("no record")
# 5. Orders per status.
print()
print(report["status"].value_counts())
# 6. Orders with no record.
missing = report.loc[report["status"] == "no record", ["order_id", "branch", "product"]]
print()
print(missing.sort_values("order_id"))
# 7. Delivered orders per branch.
delivered = report[report["status"] == "delivered"]
print()
print(delivered.groupby("branch").size())orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64প্রতিটি পূর্বাভাস সঠিক প্রমাণিত হয়েছে: কনক্যাটের পর ১২ সারি, মার্জের পরও ১২ সারি, এবং ৭টির কোনো রেকর্ড নেই। _merge-এর গণনা স্বতন্ত্রভাবে এটি নিশ্চিত করে — ৫টি both, ৭টি left_only, ০টি right_only (একটি লেফট মার্জ কখনোই right_only তৈরি করে না)।
সিদ্ধান্তগুলোর পেছনের যুক্তি:
validate="one_to_one","many_to_one"নয়। অর্ডার টেবিলেও প্রতিটি অর্ডার আইডির জন্য একটিই সারি থাকে, তাই উভয় পাশেই মান অনন্য। এই কঠোরতর যাচাইটি অর্ডার টেবিলেও কোনো ডুপ্লিকেট অর্ডার আইডি থাকলে তা ধরে ফেলবে — উদাহরণস্বরূপ, যদি এপ্রিলের ফাইলে ভুলবশত মার্চের কোনো অর্ডার পুনরাবৃত্ত হতো।is_unique-এর ওপরassertমার্জের আগেই রাখা হয়েছে, একটি বার্তা সহ, যাতে ভুল হলে তা মার্জের জটিল পরিভাষার বদলে সাধারণ ভাষায় কারণ ব্যাখ্যা করে।fillna("no record")মার্জের পর প্রয়োগ করা হয়েছে, আগে কখনোই নয়: ডেলিভারি সারিহীন অর্ডারগুলোকে লেফট মার্জ অক্ষত রাখার পরেই কেবল অনুপস্থিত স্ট্যাটাসগুলো অস্তিত্ব লাভ করে।- পূর্ব শাখায় (east) কোনো ডেলিভারি হওয়া অর্ডার নেই, তাই এটি শেষ টেবিলে মোটেই উপস্থিত হয়নি। গ্রুপিং কেবল ডেটাতে উপস্থিত থাকা গ্রুপগুলোকেই তুলে ধরে — যদি মালিকের পূর্ব শাখার জন্য ০ দেখার প্রয়োজন হয়, তবে শাখার তালিকার সাথে এই ফলাফলকে
how="left"দিয়ে মার্জ করতে হবে এবংfillna(0)দিতে হবে।
দুটি ভাঙার পরীক্ষা। removeprefix বাদ দিলে astype("int64") লাইনটি সবার আগে ValueError: invalid literal for int() with base 10: '#1001' দিয়ে ব্যর্থ হয়, কারণ "#1001"-কে সংখ্যায় রূপান্তর করা যায় না — এররটি পরিষ্কার করার লাইনটির দিকে নির্দেশ করে, যেখানটি আসলে ঠিক করা প্রয়োজন। আপনি যদি astype-ও বাদ দেন, তবে স্বয়ং মার্জ অপারেশনটি ValueError: You are trying to merge on int64 and str columns for key 'order_id' দেখাবে। দ্বিতীয় একটি #1003 সারি যোগ করলে, assert স্ক্রিপ্টটিকে AssertionError: an order appears twice in deliveries.csv দিয়ে থামিয়ে দেয়; assert তুলে দিলে validate="one_to_one" এরর দেবে MergeError: Merge keys are not unique in right dataset; not a one-to-one merge। কোনো সুরক্ষা না থাকলে, রিপোর্টটি ১২টি অর্ডারের জায়গায় ১৩টি সারি দেখাত এবং অর্ডার 1003-কে দুবার গণনা করত — একবার returned, আরেকবার delivered — এবং এর নিচের প্রতিটি যোগফল নিঃশব্দে ভুল হয়ে যেত।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন