অধ্যায় 10

DataFrames একত্র করা — concat দিয়ে সাজানো, merge দিয়ে মেলানো

একাধিক টেবিল একত্র করা: concat দিয়ে একই ধরনের মাসের ডেটা পরপর সাজানো, আর merge দিয়ে অন্য টেবিল থেকে তথ্য যোগ করা — সঠিক ধরনের merge বেছে নেওয়া, কোন সারিগুলো মেলেনি তা দেখা, এবং ডুপ্লিকেট কী-র কারণে নিঃশব্দে সারি বেড়ে যাওয়া ঠেকানো।

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

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_april.csv — এপ্রিল মাস, কলামগুলো একই। খেয়াল করুন স্ট্যাপলার (stapler): দোকানটি এপ্রিল থেকে এটি বিক্রি শুরু করেছে।

text
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.csv — প্রতি পণ্যের জন্য একটি করে সারি, যা কেনাকাটা দলের তৈরি। বাস্তব জীবনের মতো এখানেও ইচ্ছে করেই দুটি জিনিস "ভুল" রাখা হয়েছে: নতুন স্ট্যাপলারটি এখনো তালিকায় তোলা হয়নি, এবং এমন একটি মার্কার (marker) রয়েছে যা দোকানটি কখনোই বিক্রি করেনি।

text
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

branches.csv — দোকানের তিনটি শাখার ব্যবস্থাপক (manager) কারা। এই ফাইলটি অন্য একজন তৈরি করায় কলামটির নাম branch-এর বদলে branch_name রাখা হয়েছে।

text
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)।

৩. একত্র করার আগে প্রতিটি টেবিল দেখে নিন

তিন নম্বর অধ্যায়ে আপনি শিখেছিলেন ফাইল পড়ামাত্রই তা যাচাই করে নিতে হয়। একাধিক ফাইলের বেলায় এই অভ্যাসটি আর ঐচ্ছিক থাকে না: ফলাফলের রূপ কেমন হবে তা আঁচ করতে প্রতিটি টেবিলের আকার জানা অপরিহার্য।

python
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))
text
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) এক কি না। যে কী এক টেবিলে সংখ্যা এবং অন্য টেবিলে টেক্সট, তাদের কখনোই মেলানো সম্ভব নয়:

python
print(march["product"].dtype, products["product"].dtype)
text
str str

উভয়টিই str। এদের তুলনা করা যাবে।

৪. লুকআপ টেবিলের কী (key) অনন্য (unique) কি না যাচাই করুন

পুরো অধ্যায়ের সবচেয়ে গুরুত্বপূর্ণ যাচাই এটি। প্রতিটি অর্ডারের জন্য একটিমাত্র খরচ প্রয়োজন। products.csv-তে যদি pen দুবার তালিকাভুক্ত থাকে, তবে কলমের প্রতিটি অর্ডার দুবার মিলবে এবং ফলাফলে দুবার করে আবির্ভূত হবে।

python
print(products["product"].is_unique)
print(products["product"].duplicated().sum())
text
True
0

is_unique হলো True এবং কোনো ডুপ্লিকেট নেই (0): প্রতি পণ্যে একটিই সারি। প্রতিটি অর্ডার সর্বোচ্চ একটি পণ্য সারির সাথে মিলতে পারে।

৫. কোন কী-গুলোর জোড়া মিলবে না তা আগে থেকেই দেখুন

merge করার আগেই প্রশ্ন করুন: এমন কোনো অর্ডার কি আছে যার পণ্য products.csv-তে নেই, কিংবা এমন কোনো পণ্য যা কেউ অর্ডারই করেনি? ফিল্টারিং অধ্যায়ের isin দিয়ে দুটিরই উত্তর পাওয়া যায়:

python
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())
text
order_id  product
10      1011  stapler
['marker']

একটি অর্ডারের — স্ট্যাপলার, 1011 — নথিতে কোনো খরচ নেই। আর একটি পণ্য, মার্কার, কখনোই বিক্রি হয়নি। এখন আপনি merge করার আগেই জানেন merge করার সময় কী কী পরিস্থিতির মুখে পড়তে হবে।

৬. ফলাফলের পূর্বাভাস দিন, তারপর সিদ্ধান্ত নিন

পূর্বাভাসটি সংখ্যায় লিখে রাখুন:

  • orders-এ 8 + 4 = 12টি সারি আছে।
  • products-এ প্রতি পণ্যে একটি সারি আছে, তাই মেলানোর কারণে অর্ডারের সংখ্যা বাড়তে পারে না।
  • যদি প্রতিটি অর্ডার রাখা হয়, তবে মার্জ করা টেবিলে 12টি সারি থাকবে, যার একটিতে (স্ট্যাপলার) খরচের কোনো মান থাকবে না।
  • যদি কেবল মিলে যাওয়া অর্ডারগুলো রাখা হয়, তবে সারি হবে 11টি।

আপনি কোনটি চান? একটি লাভের রিপোর্টের জন্য, স্ট্যাপলারের অর্ডারটি নিঃশব্দে বাদ দিয়ে দিলে বিক্রির পরিমাণ কম দেখানো হবে। বরং সেটিকে অক্ষত রাখা, তার খরচের ঘর ফাঁকা রয়েছে তা দেখা এবং তারপর সিদ্ধান্ত নেওয়া অনেক বেশি বুদ্ধিমানের কাজ — আর ঠিক এই পছন্দটিই করা হয় how= আর্গুমেন্টের মাধ্যমে। merge করার পর সবার প্রথমে যে জিনিসটি আপনি প্রিন্ট করবেন তা হলো এর আকার (shape), এবং আপনি মিলিয়ে দেখবেন তা পূর্বাভাসে সাথে মিলেছে কি না।

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


pd.concat দিয়ে সারিগুলো একের নিচে অন্যটি সাজানো

pd.concat টেবিলের একটি লিস্ট গ্রহণ করে এবং সেগুলোকে একের নিচে অন্যটি বসিয়ে দেয়:

python
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"]])
text
(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 লেবেলগুলো এখন দুবার করে উপস্থিত। এটি কিন্তু কেবল দেখতে খারাপ লাগার মতো কোনো সাধারণ সমস্যা নয়:

python
print(both.loc[0, ["order_id", "date"]])
text
order_id        date
0      1001  2024-03-01
0      1009  2024-04-01

আপনি 0 নম্বর সারিটি চেয়েছিলেন কিন্তু পেলেন দুটি সারি। যে কোড ধরে নেয় যে একটি লেবেল মানে একটিই সারি — যেমন loc, ইনডেক্স ধরে পরবর্তীতে কোনো merge, অথবা একটি নির্দিষ্ট সেলের মান পরিবর্তন — তা এখন এমন কিছু করে বসবে যা আপনি চাননি। আপনি এটি পরীক্ষা করে দেখতে পারেন:

python
print(both.index.is_unique)
text
False

ignore_index=True — সারিগুলোকে নতুন করে নম্বর দেওয়া

পুরোনো ইনডেক্সটি যখন কেবলই সারির ক্রম গণনা করছিল (একটি RangeIndex, যেমনটি এ পর্যন্ত প্রতিটি read_csv-তে হয়েছে), তখন তার নিজস্ব কোনো বিশেষ অর্থ থাকে না। তাই সেটি ফেলে দিন এবং concat-কে ফলাফলটিতে শূন্য থেকে নতুন নম্বর দিতে বলুন:

python
orders = pd.concat([march, april], ignore_index=True)
print(orders.index.is_unique)
print(orders[["order_id", "date", "product"]].tail(5))
text
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") করার পর, যেখানে অর্ডারের আইডিগুলো এমনিতেই স্বতন্ত্র)।

প্রতিটি সারি কোথা থেকে এসেছে তা মনে রাখা

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

python
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)

print(orders["month"].value_counts())
text
month
March    8
April    4
Name: count, dtype: int64

গণনাটি নিজেই একটি যাচাই হিসেবে কাজ করে: ৮ + ৪ = ১২, কিছুই হারিয়ে যায়নি।

যখন কলামগুলো হুবহু মেলে না

Concat কলামগুলোকে নাম মিলিয়ে জোড়া লাগায়। কোনো কলাম যদি কেবল কয়েকটি টেবিলে থাকে, তবুও তা ফলাফলে দেখা যাবে — আর যে টেবিলগুলোতে সেটি ছিল না তাদের সারিতে সেখানে NaN বসে যাবে। ধরা যাক একটি বিশেষ অর্ডার হাতে টাইপ করা হয়েছিল, যাতে একটি discount কলাম আছে কিন্তু date, product, category বা price নেই:

python
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())
text
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) গ্রহণ করে। বাঁ দিকের টেবিলের প্রতিটি সারির জন্য, এটি ডান দিকের টেবিলে একই কী-মান যুক্ত সারিটি খুঁজে বের করে এবং তাদের কলামগুলোকে একসাথে আঠার মতো জোড়া লাগিয়ে দেয়:

python
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"]])
text
(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 দিয়ে পূরণ করে। দুটি তালিকা পরস্পরের সাথে মিলিয়ে নিরীক্ষা করার জন্য এটি ব্যবহার করুন।

নিজেকে এই প্রশ্নটি করতে হবে: কোন টেবিলটির সারি কোনোভাবেই হারিয়ে যাওয়া চলবে না? এখানে, অর্ডার টেবিল। প্রতিটি অর্ডারই একটি বাস্তব বিক্রি; পণ্যতালিকা হলো কেবল একটি লুকআপ। সুতরাং সঠিক মার্জটি হলো লেফট মার্জ, যেখানে অর্ডার থাকবে বাঁ দিকে:

python
merged = pd.merge(orders, products, on="product", how="left")
print(merged.shape)
print(merged[["order_id", "product", "cost", "supplier"]])
text
(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() দিয়ে ফাঁকা ঘরগুলো গুনতে পারেন, কেনাকাটা দল খরচ জানালে তা পূরণ করতে পারেন, অথবা এই অর্ডারটির কথা আলাদাভাবে রিপোর্ট করতে পারেন।

একটি রাইট মার্জ হলো এর উল্টো রূপ — প্রতিটি পণ্য সংরক্ষিত হয়, আর মার্কার থাকে যার কোনো অর্ডার নেই:

python
right = pd.merge(orders, products, on="product", how="right")
print(right.shape)
print(right[["order_id", "product", "cost"]].tail(3))
text
(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):

python
audit = pd.merge(orders, products, on="product", how="outer", indicator=True)
print(audit[["order_id", "product", "cost", "_merge"]])
print(audit["_merge"].value_counts())
text
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 কলামটি সাধারণ ডেটার মতোই, তাই এর ওপর ভিত্তি করে ফিল্টার করা যায়। সমস্যাগুলো এক লাইনে তালিকাভুক্ত করা সম্ভব:

python
problems = audit[audit["_merge"] != "both"]
print(problems[["order_id", "product", "_merge"]])
text
order_id  product      _merge
6        NaN   marker  right_only
12    1011.0  stapler   left_only

অন্য কারো তৈরি করা ডেটা মার্জ করার সময় সবসময় এই নিরীক্ষাটি চালান। সমস্যার তালিকা যখন শূন্য হবে, তখন আপনার হাতে প্রমাণ থাকবে যে ফাইল দুটি পুরোপুরি সামঞ্জস্যপূর্ণ।


সেই নিঃশব্দ বিপদ: ডুপ্লিকেট কী

এতক্ষণ products-এ প্রতি পণ্যে একটি করে সারি ছিল। ধরুন কেনাকাটা দল একটু বেশি দামে কলমের দ্বিতীয় একজন সরবরাহকারী যোগ করল এবং তালিকার নিচে একটি সারি জুড়ে দিল, ফলে pen দুবার উপস্থিত হলো:

python
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"]])
text
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 — দুটি কলম অর্ডার — প্রত্যেকে দুবার করে এসেছে, প্রতি পেন সারির জন্য একবার করে। কোনো কিছুতেই ত্রুটি দেখা যায়নি। কিন্তু এখন পরিমাণের যোগফল নিন, দেখবেন কলম দুবার গণনা করা হয়েছে: ৩২টি অতিরিক্ত কলম যা কখনোই বিক্রি হয়নি।

python
print(march["quantity"].sum(), exploded["quantity"].sum())
text
81 113

81-এর বিপরীতে 113। এটিকে বলে রো বিস্ফোরণ (row explosion), এবং এটি মার্জের সবচেয়ে মারাত্মক ভুল কারণ ফলাফলটি দেখতে একেবারেই স্বাভাবিক মনে হয়। এর পেছনের নিয়মটি সোজা: মার্জ বাঁ দিকের প্রতিটি মিলে যাওয়া সারির সাথে ডান দিকের প্রতিটি মিলে যাওয়া সারির জোড়া তৈরি করে। ওয়ান-টু-ওয়ান বা মেনি-টু-ওয়ান মার্জে সারির সংখ্যা অপরিবর্তিত থাকে; উভয় পাশে একটি কী পুনরাবৃত্ত হলেই তা গুণ হয়ে সারি বাড়িয়ে দেয়।

একটি মার্জের ফলে সারি বেড়ে গেলে তা কখনো নিজে থেকে ঘোষণা দেয় না। এর একমাত্র লক্ষণ হলো একটি স্ফীত মোট যোগফল, আর খুব বড় একটি যোগফল চোখে সহজে অস্বাভাবিক ঠেকে না। প্রতিটি মার্জের আগে ও পরে len() তুলনা করুন — এই একটি সতর্কতাই এই বিপদটি ধরতে পারে।

validate= — পান্ডাসকে দিয়ে নিজের ধারণা যাচাই করানো

আপনি যে সম্পর্কের প্রত্যাশা করছেন তা স্পষ্টভাবে কোডে লিখে দিতে পারেন, আর ডেটা সেই নিয়ম ভাঙলে পান্ডাস মার্জ করতে অস্বীকৃতি জানাবে। অনেকগুলো অর্ডার একটি একক পণ্য ভাগ করে নেয়, তাই এই মার্জটি হওয়া উচিত many-to-one:

python
try:
    pd.merge(march, products_dup, on="product", how="left", validate="many_to_one")
except Exception as error:
    print(type(error).__name__ + ":", error)
text
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= লিখতে কোনো অতিরিক্ত খরচ নেই এবং এটি নিঃশব্দ ভুলকে একটি স্পষ্ট এররে রূপান্তর করে, তাই লুকআপ টেবিলের সাথে প্রতিটি মার্জে এটি ব্যবহার করা উচিত।

সমাধান: কোন ডুপ্লিকেটটি সঠিক তা বেছে নেওয়া

এরর আপনাকে জানায় যে কোথাও ভুল হয়েছে; কিন্তু কী সঠিক তা নির্ধারণ করা আপনার দায়িত্ব। প্রথমে ডুপ্লিকেটগুলো দেখুন:

python
print(products_dup[products_dup["product"].duplicated(keep=False)])
text
product  cost supplier
0     pen   9.0    Alpha
6     pen  10.0    Gamma

duplicated(keep=False) প্রতিটি কপিকে চিহ্নিত করে, কেবল দ্বিতীয়টিকে নয়। এখন একটি ব্যবসায়িক সিদ্ধান্ত নিতে হবে। যদি নতুন সারিটি পুরোনোটির স্থলাভিষিক্ত হয়, তবে শেষ কপিটি রাখুন:

python
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"]])
text
(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= দিয়ে প্রতিটি পাশের নাম আলাদাভাবে নির্দিষ্ট করে দিন:

python
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))
text
['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'month', 'branch_name', 'manager']

উভয় কী কলামই রাখা হয়েছে, কারণ পান্ডাস নিজে থেকে জানতে পারে না যে আপনি এদের একই কলাম মনে করেন। branch এবং branch_name এখন হুবহু একই মান বহন করছে, তাই একটি কলাম মুছে ফেলুন:

python
with_manager = with_manager.drop(columns="branch_name")
print(with_manager[["order_id", "branch", "manager"]].head(4))
text
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:

python
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"]])
text
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.0

price_x এবং price_y কাজ চালালেও তিন সপ্তাহ পর কেউ মনে রাখতে পারবে না কোনটি আসল বিক্রয়মূল্য আর কোনটি তালিকাভুক্ত মূল্য। suffixes= ব্যবহার করে নিজেই অর্থপূর্ণ নাম দিন:

python
compared = pd.merge(march, list_prices, on="product", suffixes=("_sold", "_list"))
print(compared[["order_id", "price_sold", "price_list"]])
text
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() মেথডও রয়েছে। এটি আসলে এমন একটি মার্জ যা ডান টেবিলের ইনডেক্সকে কী হিসেবে ব্যবহার করে — যা বেশ সুবিধাজনক যখন লুকআপ টেবিলটি আগেই সেই কী দিয়ে ইনডেক্স করা থাকে:

python
by_product = products.set_index("product")

joined = orders.join(by_product, on="product")
print(joined[["order_id", "product", "cost", "supplier"]].head(3))
text
order_id   product   cost supplier
0      1001       pen    9.0    Alpha
1      1002  notebook   40.0    Alpha
2      1003       bag  600.0    Bravo

join স্বাভাবিকভাবেই how="left" ধরে কাজ করে। এটি pd.merge(..., how="left")-এর মতোই কাজ সম্পন্ন করে; তবে merge অনেক বেশি সার্বজনীন হাতিয়ার, এবং এই কোর্সের বাকি অংশে এটিই ব্যবহার করা হবে।


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

আবার সেই মালিকের প্রশ্নে ফিরে যাওয়া যাক: মার্চ ও এপ্রিল মাস মিলিয়ে প্রতিটি সরবরাহকারীর কাছ থেকে কত লাভ এসেছে, যেখানে প্রতিটি অর্ডারের সঠিক হিসাব থাকবে।

supplier_profit.py:

python
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))
text
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 নামে সেভ করুন — এটি "নিজে করুন" বিভাগেও ব্যবহার করা হবে। (মিসিং ডেটার অধ্যায়ে একই নামের রাইডারদের লগ ফাইলটি যদি এখনো থেকে থাকে, তবে সেটি ওভাররাইট করুন; এটি একটি ভিন্ন ফাইল।)

text
order_id,status
#1001,delivered
#1002,delivered
#1003,returned
#1005,delivered
#1006,pending
python
import 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)
text
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.concat

1001 এবং "#1001" দুটি ভিন্ন মান, এবং পান্ডাস কোনো অনুমান করতে রাজি নয়। (pd.concat সম্পর্কিত ইঙ্গিতটি একটি ভিন্ন পরিস্থিতির জন্য; এটিকে উপেক্ষা করুন।) সমাধান হলো কী-গুলোকে একই ধরন এবং একই মানে রূপান্তর করা — # সরিয়ে সংখ্যায় রূপান্তর করুন:

python
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"]])
text
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=-এ দেওয়া কলামটি কোনো একটি টেবিলে অনুপস্থিত:

python
branches = pd.read_csv("branches.csv")

try:
    pd.merge(march, branches, on="branch")
except KeyError as error:
    print("KeyError:", error)
text
KeyError: 'branch'

branches ফাইলে এর নাম branch_name। উভয় টেবিলের জন্য list(df.columns) প্রিন্ট করুন; left_on="branch", right_on="branch_name" ব্যবহার করুন, অথবা একপাশের নাম পরিবর্তন করুন। কলামের নামের শেষে একটি অদৃশ্য স্পেস ("branch ") থাকলেও একই এরর হয়, যা কেবল লিস্ট আকারে দেখলে ধরা পড়ে।

মার্জ চলল ঠিকই, কিন্তু নতুন কলামগুলো সব NaN কোনো এরর নেই, অথচ প্রতিটি সারিই মেলেনি। কী-গুলো দেখতে একই মনে হলেও আসলে এক নয়: শেষে স্পেস থাকা "north ", বড় হাতের অক্ষরে লেখা "South", অথবা এক ফাইলে টেক্সট হিসেবে থাকা আইডি অন্য ফাইলে প্রিফিক্সযুক্ত থাকা। মার্জ করার আগেই isin দিয়ে যাচাই করুন:

python
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")
text
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

python
try:
    pd.concat(march, deliveries)
except TypeError as error:
    print("TypeError:", error)
text
TypeError: concat() takes 1 positional argument but 2 were given

concat একটিমাত্র আর্গুমেন্ট গ্রহণ করে, যা হলো টেবিলগুলোর একটি তালিকা (list)। লিখুন pd.concat([march, april]) — তৃতীয় বন্ধনী [] দেওয়াটাই এখানে আসল বিষয়।

concat-এর পর ডুপ্লিকেট ইনডেক্স, এবং loc দুটি সারি ফিরিয়ে দিচ্ছে প্রতিটি ইনপুট তার নিজস্ব 0, 1, 2, … ধরে রেখেছে। ignore_index=True যোগ করুন।