সরাসরি পর্বে যান
পর্ব ২৫ · ৭৯-এর মধ্যে · মডিউল ৪

Merge, join আর reshape — pivot, melt আর stack

pandas Merge, Join and Reshape — merge, validate, concat, Tidy Data, pivot_table, melt, stack and crosstab

Taught in Bangla, this lesson shows how to combine and reshape tables in pandas: merge with inner, left, right, outer and cross joins, counting result rows in advance, indicator=True, and validate to catch duplicate-key row explosion. It then covers suffixes, join, concat, tidy wide versus long data, pivot_table, melt, stack, unstack and crosstab, with real outputs.

এই পর্বে যা শিখবেন

  • merge-এর on, left_on/right_on আর পাঁচ রকম how, আর প্রতিটিতে ফলের সারি কত হবে তা আগেই হিসাব করা
  • indicator=True দিয়ে মিল না পাওয়া সারি খোঁজা, আর validate দিয়ে duplicate key-র সারি বিস্ফোরণ ধরা
  • নামের সংঘাতে suffixes, index ধরে join, আর concat কখন merge-এর বদলে ঠিক
  • Wide বনাম long (tidy) ডেটা; pivot বনাম pivot_table, fill_value, margins, আর melt
  • stack/unstack, crosstab-এর normalize, আর chart ও মডেলের জন্য কোন আকার কখন

আগের পর্বগুলোতে একটি টেবিল নিয়ে কাজ করেছেন: পর্ব ২০-এ Series আর DataFrame, পর্ব ২২-এ সারি বাছাই, পর্ব ২৩-এ পরিষ্কার করা, আর পর্ব ২৪-এ groupby দিয়ে সারাংশ। কিন্তু বাস্তবের ডেটা প্রায় কখনো এক টেবিলে থাকে না। বাজারঘরের অর্ডারের টেবিলে শুধু পণ্যের কোড, নাম বা দাম নয়; গ্রাহকের কোড, তাঁর জেলা নয়। প্রশ্নের উত্তর পেতে (“কোন জেলায় কোন ধরনের পণ্য বেশি বিকোয়?”) টেবিলগুলো জোড়া লাগাতে হয়। আবার জোড়ার পর টেবিলের আকার বদলাতে হয়: রিপোর্টে মাস যায় কলামে, chart-এর জন্য মাস ফেরে সারিতে। এই পর্ব এই দুই কাজের: জোড়া আর আকার বদল (reshape). SQL-এর JOIN-এর পুরো তত্ত্ব আছে DBMS কোর্সের JOIN — INNER, LEFT, RIGHT আর FULL আর cross, semi আর anti join পর্বে; এখানে তার এক-দুই লাইনের ঝালাই আর pandas-এর দিকটি।

১ · কেন টেবিল জোড়া লাগে — বাজারঘরের তিন টেবিল আর key

মুদি দোকানের হিসাব খাতার কথা ভাবুন। দোকানি প্রতিটি বিক্রির পাশে ক্রেতার পুরো ঠিকানা লেখেন না, লেখেন শুধু “রহিম ভাই, খাতা নং ১২”। ঠিকানা আছে আলাদা একটি খাতায়, যেখানে প্রতিটি ক্রেতা একবারই লেখা। রহিম ভাই বাসা বদলালে এক জায়গায় কাটাকুটি করলেই চলে। কিন্তু মাসের শেষে “কোন মহল্লা থেকে কত বিক্রি” জানতে হলে দুটি খাতা পাশাপাশি খুলে প্রতিটি বিক্রির খাতা নম্বর মিলিয়ে মহল্লা খুঁজতে হয়। ডেটাবেসের ভাষায় তথ্য এভাবে ভাগ করে রাখাকে বলে normalization, আর মিলিয়ে আবার জোড়া লাগানোকে বলে join.

বাজারঘরের ডেটাও তিন টেবিলে ভাগ। orders-এ প্রতিটি অর্ডারের একটি সারি: order_id, customer_id, product_id আর পরিমাণ (qty). products-এ প্রতিটি পণ্যের একটি সারি: নাম আর দাম (পয়সায়)। customers-এ প্রতিটি গ্রাহকের একটি সারি: জেলা। যে কলামের মান মিলিয়ে সারি জোড়া হয়, তাকে বলে join key. Products আর customers হলো lookup table: প্রতিটি key-র জন্য সেখানে একটিই সারি থাকার কথা, আর orders সেখান থেকে তথ্য “খুঁজে আনে”। একটি পণ্যের অনেক অর্ডার হতে পারে, কিন্তু একটি অর্ডারের পণ্য একটিই। এই সম্পর্ককে বলে one-to-many (পণ্যের দিক থেকে দেখলে), বা many-to-one (অর্ডারের দিক থেকে)।

বাজারঘরের তিন টেবিল, আর key দিয়ে তাদের সম্পর্ক

orders (many) → products, customers (one); red = key with no match

orders, products আর customers টেবিলের key-সম্পর্ক products (lookup) product_id name ৳ P1চাল420 P2তেল185 P3ডাল130 P4চা90 customers (lookup) cust district C1ঢাকা C2খুলনা C3সিলেট C4রাজশাহী many → one many → one key orders (প্রতিটি অর্ডার একটি সারি) order_idproduct_idcustomer_idqty 101P1C11 102P2C22 105P1C9 ✗2 106P5 ✗C21 P5 আর C9-এর কোনো সারি lookup টেবিলে নেই

ছবির লাল দুটি ঘর এই পর্বের প্রথম শিক্ষা। অর্ডার ১০৬-এর পণ্য P5 ক্যাটালগে এখনো যোগ হয়নি (হয়তো নতুন পণ্য, কেউ products-এ তোলেননি)। অর্ডার ১০৫-এর গ্রাহক C9 customers-এ নেই (হয়তো অতিথি হিসেবে কেনা)। আবার C4-এর কোনো অর্ডার নেই। জোড়া লাগানোর সময় এই “মিল না পাওয়া” সারিগুলোর কী হবে, সেটিই join-এর ধরন ঠিক করে। আর এটি কোনো ব্যতিক্রম নয়: বাস্তবের প্রায় প্রতিটি ডেটাসেটে কিছু key মেলে না। প্রশ্ন শুধু, আপনি তা জানেন কি না।

২ · merge-এর মূল কথা — on, left_on/right_on আর how

pandas-এ জোড়ার মূল হাতিয়ার merge: left.merge(right, on="key", how="..."), বা একই কাজ pd.merge(left, right, ...). যে টেবিলে .merge ডাকছেন সেটি “বাঁ” (left), যেটি ভেতরে দিচ্ছেন সেটি “ডান” (right) টেবিল। তিনটি parameter সবচেয়ে জরুরি:

  • on: দুই টেবিলে একই নামের key কলাম, যেমন on="product_id". একাধিক কলামে মেলাতে list দিন: on=["district", "month"]. কিছু না দিলে pandas দুই দিকের সব একই নামের কলামে মেলায়, যা প্রায়ই অনিচ্ছাকৃত; তাই সবসময় স্পষ্ট লিখুন।
  • left_on / right_on: key-র নাম দুই দিকে আলাদা হলে, যেমন orders-এ customer_id কিন্তু customers-এ cust: left_on="customer_id", right_on="cust". ফলে দুটি কলামই থেকে যায়; দরকার না হলে পরে একটি বাদ দিন।
  • how: মিল না পাওয়া সারির ভাগ্য। Default "inner".
howকোন key রাখেSQL-এবাজারঘরে কখন
"inner" (default)শুধু যারা দুই দিকেই আছেINNER JOINশুধু বৈধ পণ্যের অর্ডার নিয়ে বিশ্লেষণ, জেনেশুনে
"left"বাঁয়ের সব; ডানে না মিললে NaNLEFT JOINপ্রতিটি অর্ডারে নাম-দাম জোড়া, কোনো অর্ডার না হারিয়ে
"right"ডানের সব; বাঁয়ে না মিললে NaNRIGHT JOINপ্রতিটি পণ্য আর তার অর্ডার (বাঁ-ডান উল্টে left-ও চলে)
"outer"দুই দিকের সব keyFULL OUTER JOINদুই উৎসের হিসাব মেলানো: কোথায় কে বাদ
"cross"key নেই; প্রতিটির সঙ্গে প্রতিটিCROSS JOINসব জেলা × সব মাসের খালি ছক বানানো

Inner join মিল না পাওয়া সারি চুপচাপ ফেলে দেয়, আর default সেটিই। তাই অর্ডারে পণ্যের তথ্য জুড়তে বেশির ভাগ সময় দরকার left join: অর্ডার টেবিলের প্রতিটি সারি থাকবে, পণ্য না মিললে নাম আর দামের ঘর হবে missing, মানে NaN. Right join তার আয়না। Outer join দুই দিকের সব রাখে; দুটি তালিকা মেলাতে (গুদামের খাতা বনাম অনলাইনের খাতা) এটি কাজের। Cross join-এ key দেওয়া যায় না (দিলে MergeError), বাঁয়ের m আর ডানের n সারি থেকে হয় m × n সারি। pandas 3-এ এর সঙ্গে আছে "left_anti" আর "right_anti": শুধু সেই সারি, যার অন্যদিকে মিল নেই (SQL-এর anti join, DBMS কোর্সের advanced join পর্বে আছে)।

ফলের সারির ক্রমও নিয়ম মানে। pandas-এর নথি অনুযায়ী left আর inner বাঁ টেবিলের key-র ক্রম রাখে, right ডানের, আর outer key-গুলোকে বর্ণানুক্রমে সাজায়। এবার ধাপে ধাপে একটি left join দেখুন। ভেতরের আসল পদ্ধতি দ্রুততর (আরও গভীরে বাক্সে আছে), কিন্তু ফল ঠিক এই ছবির মতো।

orders (বাঁ) আর products (ডান)লাল = এই ধাপের সারি

            
হিসাব
বাঁয়ের সারি
key
ডানে মিল
ফলের সারি (left)
inner হলে
  1. দুটি টেবিল। বাঁয়ে চারটি অর্ডার, ডানে তিনটি পণ্য। প্রতিটি পণ্য ডানে একবারই আছে, তাই এটি সত্যিকারের lookup table. ফল এখনো খালি।
  2. অর্ডার 101, key P1. ডানে P1 একটিই, তাই ফলে একটি সারি: অর্ডারের কলামের পাশে “চাল”. দুই দিকেই key আছে, তাই এটি both.
  3. অর্ডার 102, key P2. আবার ঠিক একটি মিল, ফলের দ্বিতীয় সারি “তেল”.
  4. অর্ডার 103, key P5. ডানে P5 নেই। Left join বাঁয়ের সারি ফেলে না, তাই সারিটি থাকে, কিন্তু name-এর ঘর NaN, আর উৎস left_only. Inner join হলে এই সারি এখানেই বাদ পড়ত, কোনো সতর্কবার্তা ছাড়াই।
  5. অর্ডার 104, key আবার P2. Lookup-এর একই সারি দ্বিতীয়বার কাজে লাগল। একটি পণ্যের অনেক অর্ডার, তাই এটি স্বাভাবিক: many-to-one.
  6. ফল। Left join-এ ৪টি সারি, বাঁয়ের সমান, কারণ ডানে কোনো key দুবার নেই। Inner হলে ৩টি, কারণ 103 বাদ পড়ে। Outer হলে ৫টি: যে P3 ডাল কেউ কেনেনি, সেটিও right_only হয়ে ফলে আসত।

শেষ ধাপের কথাটি একটি সূত্রে লেখা যায়। বাঁয়ের i-তম সারির key ডানে mi বার থাকলে (mi = 0 মানে মিল নেই):

inner-এর সারি = ∑ mi   ·   left-এর সারি = ∑ max(1, mi)

এখানে ∑ মানে বাঁয়ের সব সারির ওপর যোগ, আর max(1, mi) মানে “মিল না থাকলেও একটি সারি থাকবে”। ওপরের উদাহরণে m = 1, 1, 0, 1: inner = ৩, left = ৪. সব mi যদি ০ বা ১ হয়, left join-এর সারি ঠিক বাঁয়ের সমান। কোনো mi ২ হলেই সারি বাড়ে; অংশ ৪ সেই বিপদের গল্প।

৩ · indicator=True — কোন সারি কোথা থেকে এল

বিয়ের দাওয়াতের তালিকা আর আসলে কারা এলেন, দুটি মেলানোর সময় তিন রকম মানুষ পাওয়া যায়: দাওয়াত পেয়ে এসেছেন, দাওয়াত পেয়েও আসেননি, আর দাওয়াত ছাড়াই এসেছেন। merge(..., indicator=True) ঠিক এই তিন ভাগ একটি নতুন কলামে লিখে দেয়। কলামের নাম _merge, মান both, left_only বা right_only, আর এর dtype category. নিচের কোডে বাজারঘরের তিন টেবিল (ছবির মতো, পুরো ৮টি অর্ডারসহ) জোড়া হচ্ছে। প্রথমবার চালালে pandas নামাতে কয়েক সেকেন্ড লাগতে পারে।

import pandas as pd
pd.set_option("display.width", 60)

products = pd.DataFrame({
    "product_id": ["P1", "P2", "P3", "P4"],
    "name": ["চাল", "তেল", "ডাল", "চা"],
    "price_paisa": [42000, 18500, 13000, 9000]})
customers = pd.DataFrame({
    "cust": ["C1", "C2", "C3", "C4"],
    "district": ["ঢাকা", "খুলনা", "সিলেট", "রাজশাহী"]})
orders = pd.DataFrame({
    "order_id": range(101, 109),
    "customer_id": ["C1", "C2", "C1", "C3", "C9", "C2", "C1", "C3"],
    "product_id": ["P1", "P2", "P3", "P2", "P1", "P5", "P4", "P2"],
    "qty": [1, 2, 3, 1, 2, 1, 4, 2]})

# ১) প্রতিটি অর্ডারের পাশে পণ্যের নাম আর দাম
op = orders.merge(products, on="product_id", how="left",
                  indicator=True)
print("সারি:", len(orders), "→", len(op))
print(op["_merge"].value_counts())
print(op.loc[op["_merge"] == "left_only",
             ["order_id", "product_id", "name"]])

# ২) দুই দিকের কলামের নাম আলাদা: left_on / right_on
full = orders.merge(customers, left_on="customer_id",
                    right_on="cust", how="outer", indicator=True)
print(full.loc[full["_merge"] != "both",
               ["order_id", "customer_id", "cust", "_merge"]])
print("order_id-এর dtype:", full["order_id"].dtype)

ব্রাউজারে (Pyodide, pandas 3.0.2) একটি run-এর আসল আউটপুট:

সারি: 8 → 8
_merge
both          7
left_only     1
right_only    0
Name: count, dtype: int64
   order_id product_id name
5       106         P5  NaN
   order_id customer_id cust      _merge
7       NaN         NaN   C4  right_only
8     105.0          C9  NaN   left_only
order_id-এর dtype: float64

প্রথম লাইন: left join-এর আগে-পরে ৮টি সারি, কারণ products-এ কোনো key দুবার নেই। value_counts বলছে ৭টি অর্ডারের পণ্য মিলেছে, ১টির মেলেনি; right_only ০, কারণ left join ডানের একা সারি রাখেই না (তবু category-র তিনটি মানই গোনায় দেখায়)। পরের টেবিলে সেই একটি: অর্ডার ১০৬, পণ্য P5, নাম NaN. Inner join দিলে এই অর্ডার নিঃশব্দে হারিয়ে যেত, আর দিনের বিক্রির যোগফল কম আসত।

দ্বিতীয় অংশে outer join আর left_on/right_on. দুটি অমিল একসঙ্গে ধরা পড়েছে: C4 গ্রাহক, যাঁর কোনো অর্ডার নেই (right_only, তাই order_id আর customer_id NaN), আর অর্ডার ১০৫, যার গ্রাহক C9 তালিকায় নেই (left_only). দুটি কলাম customer_id আর cust দুটিই থেকে গেছে। শেষ লাইনটি একটি সূক্ষ্ম ফাঁদ: order_id ছিল পূর্ণসংখ্যা (int64), কিন্তু outer join একটি NaN ঢোকানোয় পুরো কলাম হয়ে গেছে float64, তাই 105 এখন 105.0. পূর্ণসংখ্যা রাখতে চাইলে pandas-এর nullable Int64 dtype দরকার (পর্ব ২৩-এ missing value-এর আলোচনায় আছে)।

মূল ধারণা

প্রথমবার কোনো দুটি টেবিল জোড়ার সময় indicator=True দিন, আর _merge-এর গোনা দেখুন। left_only আর right_only শূন্য না হলে কারণ খুঁজুন: বানানের পার্থক্য (“P05” বনাম “P5”), বাড়তি space, বড়-ছোট হাতের অক্ষর, বা একদিকে int আর অন্যদিকে string. কারণ জানার পর কলামটি drop(columns="_merge") করে এগোন।

৪ · Duplicate key আর সারি বিস্ফোরণ — validate দিয়ে ধরা

ধরুন তেলের দাম ১৮৫ থেকে ১৯৫ টাকা হলো। কেউ products টেবিলে P2-এর নতুন একটি সারি যোগ করলেন, কিন্তু পুরোনোটি মুছলেন না। এখন lookup-এ P2 দুবার। পরের দিন বিশ্লেষক যথারীতি orders.merge(products, on="product_id", how="left") চালালেন। কোনো error নেই, কোনো সতর্কবার্তা নেই। কিন্তু P2-এর প্রতিটি অর্ডার এখন দুবার আছে, একবার পুরোনো দামে, একবার নতুন দামে। এর নাম row explosion বা fan-out, আর SQL-এ একই বিপদ DBMS কোর্সের fan-out অংশে আছে। ধাপে ধাপে দেখুন সারি কীভাবে গুণ হয়।

orders (৬টি) আর products (P2 দুবার)লাল = এই ধাপে যা যোগ হলো

            
হিসাব
এই অর্ডারে মিল (mi)
ফলের মোট সারি
P2-এর সারি
  1. Lookup আর lookup নেই। products-এ P2 দুবার, দুটি আলাদা দামে। products["product_id"].duplicated().sum() বলবে ১. Merge এটি দেখেও থামে না; সে শুধু নিয়ম মানে: প্রতিটি মিলের জন্য একটি সারি।
  2. অর্ডার 101, P1. ডানে একটি মিল, ফলে একটি সারি। এখন পর্যন্ত সব ঠিক।
  3. অর্ডার 102, P2. ডানে দুটি মিল, তাই ফলে দুটি সারি: একই অর্ডার, একবার ১৮৫ টাকায়, একবার ১৯৫ টাকায়। mi = ২.
  4. অর্ডার 103, P3. আবার একটি মিল। মোট ৪টি সারি, অথচ অর্ডার মাত্র ৩টি।
  5. অর্ডার 104 আর 105, দুটিই P2. প্রতিটির জন্য দুটি করে, মোট ৪টি নতুন সারি। ফল এখন ৮টি সারি, ৫টি অর্ডার থেকে।
  6. অর্ডার 106, P1. একটি মিল। শেষে ৯টি সারি: ১ + ২ + ১ + ২ + ২ + ১ = ৯, ঠিক অংশ ২-এর সূত্র ∑ max(1, mi).
  7. ক্ষতির হিসাব। ৬টি অর্ডার থেকে ৯টি সারি; P2-এর অর্ডার-গোনা ৩ থেকে ৬, দ্বিগুণ। পরিমাণ × দাম যোগ করলে ৩১৩০ টাকা, আসল ২২০৫ টাকা। validate="many_to_one" দিলে pandas merge-এর আগেই ডানের key অনন্য কি না দেখে, আর না হলে MergeError দিয়ে থামে।

validate নিজে কোনো জোড়া বদলায় না; এটি একটি দাবি (assertion), যে “এই দুই টেবিলের সম্পর্ক এমন হওয়ার কথা”। চারটি মান আছে: "one_to_one" (দুই দিকেই key অনন্য), "one_to_many" (শুধু বাঁ দিকে অনন্য), "many_to_one" (শুধু ডান দিকে অনন্য) আর "many_to_many" (কিছুই যাচাই করে না)। সংক্ষেপে "1:1", "1:m", "m:1", "m:m"-ও লেখা যায়। কোনটি দেবেন, তা নির্ভর করে কোন টেবিল বাঁয়ে: orders.merge(products)-এ products ডানে, তাই "many_to_one"; products.merge(orders)-এ products বাঁয়ে, তাই "one_to_many". নিচের কোডে দুটিই চালিয়ে দেখুন।

import pandas as pd

# দাম বদলের পর কেউ নতুন সারি যোগ করেছেন, পুরোনোটি মোছেননি
products = pd.DataFrame({
    "product_id": ["P1", "P2", "P2", "P3"],
    "price_paisa": [42000, 18500, 19500, 13000]})
orders = pd.DataFrame({
    "order_id": range(101, 107),
    "product_id": ["P1", "P2", "P3", "P2", "P2", "P1"],
    "qty": [1, 2, 3, 1, 2, 1]})

m = orders.merge(products, on="product_id", how="left")
print("সারি:", len(orders), "→", len(m))
print("P2-এর অর্ডার:", (orders["product_id"] == "P2").sum(),
      "→", (m["product_id"] == "P2").sum())
bill = (m["qty"] * m["price_paisa"]).sum()
print("মোট বিক্রি (টাকা):", bill / 100)

print("lookup-এ duplicate key:",
      products["product_id"].duplicated().sum())
try:
    orders.merge(products, on="product_id", validate="many_to_one")
except pd.errors.MergeError as e:
    print("MergeError:", e)
try:
    products.merge(orders, on="product_id", validate="one_to_many")
except pd.errors.MergeError as e:
    print("MergeError:", e)

latest = products.drop_duplicates("product_id", keep="last")
ok = orders.merge(latest, on="product_id", how="left",
                  validate="many_to_one")
assert len(ok) == len(orders)
print("ঠিক করার পর:", len(ok), "সারি, মোট",
      (ok["qty"] * ok["price_paisa"]).sum() / 100, "টাকা")

ব্রাউজারে (Pyodide, pandas 3.0.2) একটি run-এর আসল আউটপুট:

সারি: 6 → 9
P2-এর অর্ডার: 3 → 6
মোট বিক্রি (টাকা): 3130.0
lookup-এ duplicate key: 1
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge

Duplicates in right:
 product_id
        P2 ...
MergeError: Merge keys are not unique in left dataset; not a one-to-many merge
Duplicates in left:
 product_id
        P2 ...
ঠিক করার পর: 6 সারি, মোট 2205.0 টাকা

প্রথম তিন লাইন stepper-এর শেষ ধাপ হুবহু মেলায়: ৬টি অর্ডার থেকে ৯টি সারি, P2-এর অর্ডার ৩ থেকে ৬, আর মোট বিক্রি ৩১৩০ টাকা। এখন কেউ m.groupby("product_id")["order_id"].count() দিয়ে “পণ্যভিত্তিক অর্ডার” রিপোর্ট বানালে তেলের অর্ডার দ্বিগুণ দেখাবে, আর সেই রিপোর্ট দেখে কেউ তেলের মজুদ দ্বিগুণ করতে পারেন। দুটি validate-ই একই সমস্যা ধরেছে, শুধু দিক আলাদা: প্রথমটিতে duplicate “right dataset”-এ, দ্বিতীয়টিতে “left dataset”-এ, কারণ সেখানে products বাঁয়ে। এই সংস্করণে error-এর বার্তায় কোন key দুবার (P2), তাও দেখানো হয়েছে; পুরোনো সংস্করণে সম্ভবত শুধু প্রথম লাইনটি দেখবেন। শেষ লাইনে প্রতিটি পণ্যের শেষ সারি রেখে (keep="last", ধরে নিয়ে শেষটিই নতুন দাম) আবার merge: ৬টি সারি, মোট ২২০৫ টাকা, আর assert নিশ্চিত করেছে সারির সংখ্যা বদলায়নি।

কোনটি “ঠিক” সারি, তা কোড জানে না। drop_duplicates(keep="last") এখানে কাজ করেছে কারণ আমরা জানি শেষ সারিটি নতুন দাম। বাস্তবে আসল সমাধান হলো দামের সঙ্গে কার্যকর হওয়ার তারিখ রাখা, আর প্রতিটি অর্ডারকে তার তারিখের দামের সঙ্গে মেলানো (pandas-এ merge_asof, যা পর্ব ২৬-এর time series-এর বিষয়)। আর প্রতিটি গুরুত্বপূর্ণ merge-এর পর এই তিনটি লাইন অভ্যাস করুন: আগে-পরে len(), যোগফলের তুলনা, আর validate.

৫ · নামের সংঘাত, index-এ join আর concat

Suffixes. দুই টেবিলে key ছাড়া একই নামের আরেকটি কলাম থাকলে (যেমন পুরোনো আর নতুন দামের তালিকা, দুটিতেই price), merge দুটিকেই রাখে আর নামের শেষে _x আর _y জুড়ে দেয়। কোনটি কোন টেবিলের, তা মনে রাখা কঠিন, তাই নিজের নাম দিন। লেখকের ল্যাপটপে একটি সত্যিকারের run (Python 3.14.4, pandas 3.1.1):

~/bazarghar-ai$ python concat_join.py      # (শেষ অংশ)
  product_id  price_x  price_y
0         P1    42000    43000
1         P2    18500    18500
  product_id  price_old  price_new
0         P1      42000      43000
1         P2      18500      18500

দ্বিতীয় টেবিলটি এসেছে a.merge(b, on="product_id", suffixes=("_old", "_new")) থেকে। এখন df["price_new"] - df["price_old"] লিখলে কোডটি নিজেই পড়া যায়।

Index ধরে join. DataFrame.join হলো merge-এর একটি সুবিধাজনক রূপ, যা ডান টেবিলের index ধরে মেলায়, আর তার default how="left" (merge-এর default inner, এই পার্থক্যটি মনে রাখার মতো)। পণ্যের কোড যখন index-এ বসানো (পর্ব ২০-এ set_index দেখেছেন), তখন এটি সবচেয়ে ছোট লেখা। দুই দিকে একই নামের কলাম থাকলে join ValueError দেয়, যতক্ষণ না lsuffix/rsuffix দেন। orders.join(products, on="product_id") লিখলে বাঁয়ের একটি কলাম ডানের index-এর সঙ্গে মেলে।

concat: মেলানো নয়, জুড়ে দেওয়া। জানুয়ারির অর্ডার এক ফাইলে, ফেব্রুয়ারির আরেকটিতে; কলাম একই। এদের নিচে নিচে জুড়তে concat: pd.concat([jan, feb]). এখানে কোনো key মেলানো হয় না। একই লেখকের run-এর প্রথম অংশ:

~/bazarghar-ai$ python concat_join.py      # (প্রথম অংশ)
pandas 3.1.1
   order_id  paisa
0       101  42000
1       102  37000
0       201  18500
1       202   9000
   order_id  paisa
0       101  42000
1       102  37000
2       201  18500
3       202   9000
       order_id  paisa
Jan 0       101  42000
    1       102  37000
Feb 0       201  18500
    1       202   9000
    stock  rating
P1     12     4.5
P2      0     NaN
P3      7     3.9

প্রথম টেবিলে index 0, 1, 0, 1: দুটি ফাইলের নিজস্ব index জুড়ে গেছে, তাই এখন df.loc[0] দুটি সারি দেয়, যা পরে অদ্ভুত bug আনে। ignore_index=True নতুন করে 0, 1, 2, 3 দেয় (দ্বিতীয় টেবিল)। keys=["Jan", "Feb"] দিলে কোন সারি কোন ফাইলের, তা একটি বাইরের index স্তরে লেখা থাকে (তৃতীয় টেবিল, যা একটি MultiIndex; অংশ ৭-এ আবার আসবে)। শেষ টেবিলটি pd.concat([stock, rating], axis=1): পাশাপাশি জোড়া, যেখানে pandas সারি মেলায় index-এর label দিয়ে, আর P2-এর রেটিং না থাকায় NaN. stock.join(rating) এখানে হুবহু একই টেবিল দেয়।

কাজহাতিয়ারকী দিয়ে মেলায়
একই কলামের নতুন সারি (মাসের পর মাস)pd.concat([...])কিছু মেলায় না; কলামের নাম ধরে সাজায়
একই index-এর নতুন কলামpd.concat([...], axis=1) বা joinindex-এর label
কলামের মান মিলিয়ে তথ্য জোড়াmergeএক বা একাধিক key কলাম
সারির ক্রম ধরে পাশাপাশি বসানোপ্রায় কখনো নয়দুই টেবিলের ক্রম এক থাকার নিশ্চয়তা নেই

একটি পুরোনো অভ্যাস নিয়েও সতর্ক থাকুন: লুপের ভেতরে df = df.append(row). DataFrame.append pandas 2.0-তে সরানো হয়েছে; আর loop-এ বারবার concat করাও ধীর, কারণ প্রতিবার পুরো টেবিল নতুন করে কপি হয়। ঠিক পথ হলো টুকরোগুলো একটি list-এ জমিয়ে শেষে একবার pd.concat(list_of_frames, ignore_index=True).

৬ · Wide বনাম long — tidy data

ক্রিকেটের স্কোরকার্ড দুইভাবে লেখা যায়। একটিতে প্রতিটি ব্যাটারের একটি সারি, আর প্রতিটি ম্যাচের রান আলাদা কলামে: ম্যাচ ১, ম্যাচ ২, ম্যাচ ৩। চোখে পড়তে সুন্দর, তুলনা সহজ। অন্যটিতে প্রতিটি (ব্যাটার, ম্যাচ) জোড়ার একটি সারি, আর রান একটি কলামে। লম্বা, কিন্তু নতুন ম্যাচ এলে শুধু সারি বাড়ে, কলাম নয়। প্রথমটিকে বলে wide format, দ্বিতীয়টিকে long format. দুটিতে একই তথ্য; শুধু “ম্যাচ” কথাটি কোথায় লেখা, তা আলাদা: wide-এ কলামের নামে, long-এ একটি কলামের মানে।

Hadley Wickham ২০১৪ সালে Journal of Statistical Software-এ “Tidy Data” প্রবন্ধে এই পার্থক্যের একটি নাম আর নিয়ম দেন। তাঁর সংজ্ঞায় tidy data মানে: (১) প্রতিটি variable একটি কলাম, (২) প্রতিটি পর্যবেক্ষণ (observation) একটি সারি, (৩) প্রতিটি ধরনের পর্যবেক্ষণ একটি টেবিল। বাজারঘরের বিক্রিতে variable তিনটি: জেলা, মাস, বিক্রি। তাই tidy টেবিলের তিনটি কলাম, আর প্রতিটি (জেলা, মাস)-এর একটি সারি। “২০২৬-০১”, “২০২৬-০২” কলামের নামে থাকলে মাস নামের variable-টি লুকিয়ে আছে শিরোনামে; Wickham এটিকে অগোছালো ডেটার সবচেয়ে সাধারণ ধরন বলেছেন (“column headers are values, not variable names”). তিনি নিজেই প্রবন্ধে লিখেছেন, এই নিয়মগুলো Codd-এর relational database-এর normal form-এর খুব কাছের, শুধু পরিসংখ্যানবিদের ভাষায়।

Wide থেকে long-এ যাওয়ার pandas হাতিয়ার melt: wide.melt(id_vars="district", var_name="month", value_name="taka"). id_vars হলো যে কলামগুলো যেমন আছে থাকবে (পরিচয়ের কলাম); বাকি সব কলাম সারিতে নামবে। কলামের নাম যাবে var_name-এর কলামে, মান যাবে value_name-এর কলামে। ধাপে ধাপে দেখুন।

wide (২ × ৩) থেকে long (৪ × ৩)লাল = এই ধাপে সরানো

            
হিসাব
wide-এর shape
long-এর shape
“মাস” কোথায়
  1. Wide টেবিল। প্রতিটি জেলার একটি সারি, প্রতিটি মাসের একটি কলাম। id_vars="district" বলে district কলামটি পরিচয়ের, সেটি নামবে না।
  2. প্রথম মাসের কলাম নামল। “2026-01” কলামের প্রতিটি ঘর একটি নতুন সারি হলো: জেলা যেমন ছিল, month-এ কলামের নাম, taka-য় ঘরের মান। দুটি জেলা, তাই দুটি সারি।
  3. দ্বিতীয় মাসের কলাম। একইভাবে আরও দুটি সারি। melt কলামের পর কলাম কাজ করে, তাই ফলের ক্রম: আগে সব জেলার জানুয়ারি, তারপর সব জেলার ফেব্রুয়ারি।
  4. Long টেবিল তৈরি। সারি = জেলার সংখ্যা × মাসের কলামের সংখ্যা = ৪; কলাম ৩টি। এটি tidy: প্রতিটি variable একটি কলাম, প্রতিটি পর্যবেক্ষণ একটি সারি।
  5. উল্টো পথে pivot. প্রতিটি (district, month) জোড়া একবারই আছে, তাই pivot ঠিক আগের wide টেবিল ফেরত দেয়; শুধু district এখন index, আর কলামের নামের ওপরে লেখা “month”. melt আর pivot একে অপরের উল্টো।

৭ · pivot, pivot_table আর melt — রিপোর্ট বানানো আর ফেরানো

pivot শুধু আকার বদলায়, কোনো হিসাব করে না। তাই প্রতিটি (index, column) জোড়া একবারের বেশি থাকলে সে জানে না কোন মান বসাবে, আর ValueError দেয়। আসল অর্ডারের টেবিলে একই জেলায় একই মাসে অনেক অর্ডার থাকে, তাই সেখানে দরকার pivot table: pivot_table প্রতিটি ঘরের সব মান একটি aggregation দিয়ে এক করে (পর্ব ২৪-এর groupby-র মতোই, শুধু ফল wide আকারে)। চারটি parameter বিশেষভাবে জরুরি:

  • aggfunc: default হলো "mean", যোগফল নয়! বিক্রির রিপোর্টে প্রায় সবসময় aggfunc="sum" লিখতে হয়; গোনার জন্য "count" বা "size".
  • fill_value: যে ঘরে কোনো সারিই নেই, সেখানে NaN-এর বদলে কী বসবে। বিক্রিতে ০ যুক্তিসংগত; গড় ডেলিভারি-সময়ে ০ বসালে মিথ্যা বলা হবে।
  • margins=True: শেষে একটি মোট-সারি আর মোট-কলাম, যার নাম default-এ “All”; margins_name দিয়ে বদলানো যায়। মোটগুলো মূল ডেটা থেকে একই aggfunc দিয়ে হিসাব হয়।
  • observed: category কলামে শুধু যে জোড়া ডেটায় আছে সেগুলো দেখাবে কি না; pandas 3-এ default True.

নিচের কোডে seeded NumPy দিয়ে বাজারঘরের ৪০টি অর্ডার বানানো হয়েছে, তারপর মার্চে সিলেটের অর্ডারগুলো বাদ (ধরুন সেই মাসে বন্যায় ডেলিভারি বন্ধ ছিল), যাতে একটি ঘর সত্যিই খালি থাকে।

import numpy as np
import pandas as pd
pd.set_option("display.width", 60)

rng = np.random.default_rng(42)
n = 40
sales = pd.DataFrame({
    "district": rng.choice(["ঢাকা", "চট্টগ্রাম", "খুলনা", "সিলেট"], n),
    "month": rng.choice(["2026-01", "2026-02", "2026-03"], n),
    "paisa": rng.integers(5_000, 200_000, n)})
# ধরুন মার্চে সিলেটে ডেলিভারি বন্ধ ছিল
sales = sales[~((sales["district"] == "সিলেট")
                & (sales["month"] == "2026-03"))]
print("লম্বা টেবিল:", sales.shape)

try:
    sales.pivot(index="district", columns="month", values="paisa")
except ValueError as e:
    print("pivot:", e)

wide = sales.pivot_table(index="district", columns="month",
                         values="paisa", aggfunc="sum",
                         fill_value=0, margins=True,
                         margins_name="মোট")
print(wide // 100)          # পয়সা থেকে টাকা

long = (wide.drop(index="মোট", columns="মোট")
            .reset_index()
            .melt(id_vars="district", var_name="month",
                  value_name="paisa"))
print(long.shape)
print(long.head(4))
print("মোট মিলল?", long["paisa"].sum() == sales["paisa"].sum())

ব্রাউজারে (Pyodide, pandas 3.0.2) একটি run-এর আসল আউটপুট:

লম্বা টেবিল: (35, 3)
pivot: Index contains duplicate entries, cannot reshape
month      2026-01  2026-02  2026-03    মোট
district
খুলনা         3498     4009     3868  11376
চট্টগ্রাম     1278     3627     2924   7829
ঢাকা          4836     4151     3220  12208
সিলেট         2676     3755        0   6432
মোট          12290    15544    10012  37848
(12, 3)
    district    month   paisa
0      খুলনা  2026-01  349893
1  চট্টগ্রাম  2026-01  127804
2       ঢাকা  2026-01  483687
3      সিলেট  2026-01  267678
মোট মিলল? True

৪০টি অর্ডারের ৫টি ছিল মার্চে সিলেটে, তাই বাকি ৩৫. pivot সঙ্গে সঙ্গে থেমেছে, কারণ একই (জেলা, মাস)-এ একাধিক অর্ডার। pivot_table প্রতিটি ঘরে যোগফল বসিয়েছে, সিলেট-মার্চের খালি ঘরে fill_value-এর ০, আর margins দিয়েছে “মোট” সারি ও কলাম: সব জেলা মিলিয়ে তিন মাসে ৩৭,৮৪৮ টাকার মতো। একটি সূক্ষ্মতা খেয়াল করুন: খুলনার তিন মাস যোগ করলে ৩৪৯৮ + ৪০০৯ + ৩৮৬৮ = ১১,৩৭৫, অথচ মোট-কলামে ১১,৩৭৬. ভুল নয়: মোট হিসাব হয়েছে পয়সায়, আর // 100 প্রতিটি ঘর আলাদা করে নিচের দিকে গোল করেছে। এ কারণেই টাকা-পয়সা পূর্ণসংখ্যার পয়সায় রাখা আর শুধু ছাপার সময় টাকায় বদলানো। বাংলা জেলার নাম বর্ণানুক্রমে সাজানো (খুলনা আগে), কারণ pivot_table-এর default sort=True.

শেষ অংশে melt দিয়ে আবার long: মোট-সারি আর মোট-কলাম আগে বাদ দিতে হয়েছে (না হলে “মোট” নামে একটি ভুয়া জেলা আর ভুয়া মাস তৈরি হতো), আর reset_index() দিয়ে district-কে index থেকে কলামে নামাতে হয়েছে, কারণ melt-এর id_vars কলাম চায়। ফল ৪ জেলা × ৩ মাস = ১২টি সারি; ক্রম stepper-এর মতোই, আগে সব জেলার জানুয়ারি। শেষ লাইন যাচাই করেছে যোগফল একই। একটি পার্থক্য আছে: মূল ৩৫টি অর্ডার এখন ১২টি সারাংশ-সারি, আর সিলেট-মার্চের ০ এখন একটি সত্যিকারের সারি। Aggregate করা তথ্য আর ফেরানো যায় না; melt শুধু আকার ফেরায়।

stack আর unstack: MultiIndex দিয়ে একই কাজ। pandas-এ index-এর একাধিক স্তর থাকতে পারে, যাকে বলে MultiIndex; প্রতিটি সারির label তখন একটি tuple, যেমন খুলনা আর 2026-01-এর জোড়া। stack কলামের label-কে index-এর ভেতরের স্তরে নামায় (wide থেকে long), আর unstack index-এর একটি স্তরকে কলামে তোলে, ফলে long থেকে wide হয়। melt আর pivot কাজ করে সাধারণ কলাম নিয়ে, stack আর unstack কাজ করে index নিয়ে; groupby-র ফল যেহেতু প্রায়ই MultiIndex, সেখানে unstack খুব কাজের। লেখকের ল্যাপটপে সত্যিকারের run (pandas 3.1.1; জেলার নাম এখানে ইংরেজিতে):

~/bazarghar-ai$ python stack_demo.py
pandas 3.1.1
month     2026-01  2026-02
district
Khulna       3498     4009
Dhaka        4836     4151
district  month
Khulna    2026-01    3498
          2026-02    4009
Dhaka     2026-01    4836
          2026-02    4151
dtype: int64
2 ['district', 'month']
month     2026-01  2026-02
district
Khulna       3498     4009
Dhaka        4836     4151
district  Khulna  Dhaka
month
2026-01     3498   4836
2026-02     4009   4151

wide.stack() একটি Series দিয়েছে, যার index-এর দুটি স্তর: district আর month. s.unstack() default-এ সবচেয়ে ভেতরের স্তর (month) কলামে তোলে, তাই আগের টেবিল ফিরে এসেছে। s.unstack(level="district") উল্টো স্তর তুলেছে, ফলে টেবিলটি transpose হয়ে গেছে। একটি pandas 3-নির্দিষ্ট কথা: pandas 2.1-এ stack-এর একটি নতুন বাস্তবায়ন আসে (future_stack=True), যা pandas 3-এ default; এটি আর আগের মতো NaN-ওয়ালা সারি নিজে থেকে বাদ দেয় না, আর পুরোনো dropna parameter এর সঙ্গে চলে না। পুরোনো টিউটোরিয়ালের ফল তাই এক-দুই সারি আলাদা হতে পারে।

৮ · crosstab, আর কোন আকার কোন কাজে

“কোন জেলায় কোন পেমেন্ট পদ্ধতি কতবার?” এই প্রশ্নে কোনো সংখ্যার কলাম নেই, আছে দুটি শ্রেণিবাচক (categorical) কলাম। এর টেবিলকে পরিসংখ্যানে বলে contingency table, আর pandas-এ crosstab: pd.crosstab(df["district"], df["payment"]) প্রতিটি জোড়া কতবার এসেছে, তা গোনে। normalize গোনাকে অনুপাতে বদলায়: "index" দিলে প্রতিটি সারির যোগ ১ (জেলার ভেতরে কোন পদ্ধতির ভাগ কত), "columns" দিলে প্রতিটি কলামের যোগ ১, আর "all" বা True দিলে পুরো টেবিলের যোগ ১. নিচের ডেটা বানানো: আমরা নিজেরাই ধরে নিয়েছি ঢাকার বাইরে cash on delivery (COD) বেশি। এগুলো বাস্তব বাংলাদেশের কোনো পরিসংখ্যান নয়।

import numpy as np
import pandas as pd
pd.set_option("display.width", 60)

rng = np.random.default_rng(7)
n = 300
district = rng.choice(["ঢাকা", "চট্টগ্রাম", "খুলনা", "সিলেট"], n,
                      p=[0.4, 0.3, 0.15, 0.15])
probs = {"ঢাকা": [0.35, 0.40, 0.25], "চট্টগ্রাম": [0.50, 0.35, 0.15],
         "খুলনা": [0.60, 0.30, 0.10], "সিলেট": [0.55, 0.35, 0.10]}
pay = [rng.choice(["COD", "bKash", "card"], p=probs[d])
       for d in district]
orders = pd.DataFrame({"district": district, "payment": pay})

counts = pd.crosstab(orders["district"], orders["payment"],
                     margins=True, margins_name="মোট")
print(counts)

pct = pd.crosstab(orders["district"], orders["payment"],
                  normalize="index").mul(100).round(1)
print(pct)
print(pct.sum(axis=1).round(1).tolist())

# একই গোনা groupby দিয়ে: MultiIndex-এর Series, তারপর unstack
s = orders.groupby(["district", "payment"]).size()
print(s.head(3))
alt = s.unstack(fill_value=0)
print(alt.equals(counts.drop(index="মোট", columns="মোট")))

ব্রাউজারে (Pyodide, pandas 3.0.2) একটি run-এর আসল আউটপুট:

payment    COD  bKash  card  মোট
district
খুলনা       27     16     3   46
চট্টগ্রাম   36     42    11   89
ঢাকা        39     52    29  120
সিলেট       29     12     4   45
মোট        131    122    47  300
payment     COD  bKash  card
district
খুলনা      58.7   34.8   6.5
চট্টগ্রাম  40.4   47.2  12.4
ঢাকা       32.5   43.3  24.2
সিলেট      64.4   26.7   8.9
[100.0, 100.0, 100.0, 100.0]
district  payment
খুলনা     COD        27
          bKash      16
          card        3
dtype: int64
True

প্রথম টেবিল কাঁচা গোনা, মোটসহ: ৩০০টি অর্ডারের ১২০টি ঢাকার। কিন্তু কাঁচা গোনায় জেলাগুলোর তুলনা ঠকায়: চট্টগ্রামে COD ৩৬টি আর সিলেটে ২৯টি দেখে মনে হয় চট্টগ্রামে COD বেশি জনপ্রিয়, অথচ সিলেটে মোট অর্ডারই মাত্র ৪৫টি। দ্বিতীয় টেবিলে normalize="index" দিয়ে প্রতিটি সারি শতাংশে: সিলেটে ৬৪.৪% COD, চট্টগ্রামে মাত্র ৪০.৪%. খুলনার সারি মিলিয়ে দেখুন: ২৭ / ৪৬ = ০.৫৮৭, মানে ৫৮.৭%. প্রতিটি সারির যোগ ১০০, তৃতীয় লাইন তা নিশ্চিত করেছে। বানানোর সময় আমরা খুলনায় COD-এর সম্ভাবনা ধরেছিলাম ৬০%; ৪৬টি নমুনায় ৫৮.৭% এসেছে, যা নমুনার স্বাভাবিক ওঠানামা। শেষ অংশে একই গোনা groupby দিয়ে: size() দিয়েছে (জেলা, পেমেন্ট)-এর MultiIndex Series, আর unstack(fill_value=0) তাকে crosstab-এর হুবহু টেবিল বানিয়েছে, তাই True. অর্থাৎ crosstab হলো groupby + size + unstack-এর একটি সুবিধাজনক রূপ।

Long না wide — chart আর মডেলের চোখে

কোন আকার “ঠিক”, তার উত্তর নির্ভর করে পরের কাজের ওপর। Chart-এর জন্য সাধারণত long. seaborn-এর মতো library একটি tidy টেবিল চায়, যেখানে আপনি বলবেন x="month", y="taka", hue="district"; নতুন জেলা এলে কোড বদলায় না। কোন ধরনের chart কখন, তা আসবে পর্ব ২৯-এ। রিপোর্ট আর মানুষের চোখের জন্য wide: মাস কলামে, জেলা সারিতে, শেষে মোট। মডেলের জন্য সাধারণত wide, তবে একটি নির্দিষ্ট অর্থে: scikit-learn চায় একটি feature matrix, যেখানে প্রতিটি সারি একটি নমুনা (যেমন একজন গ্রাহক) আর প্রতিটি কলাম একটি feature. গ্রাহকের লম্বা অর্ডার-ইতিহাস থেকে “মাসওয়ারি খরচ” বা “পেমেন্ট পদ্ধতির ভাগ” কলাম বানানো ঠিক এই পর্বের pivot_table আর crosstab-এর কাজ; feature engineering-এর পুরো আলোচনা পর্ব ৬৩-এ।

আর মডেলের জন্য একটি জরুরি সতর্কতা: feature টেবিল বানাতে merge-এর সময় ভবিষ্যতের তথ্য যেন না ঢোকে। গ্রাহক পরের মাসে ফিরবেন কি না অনুমান করতে চাইলে, সেই মাসের অর্ডার থেকে বানানো কোনো কলাম জুড়ে দিলে মডেল পরীক্ষায় দারুণ দেখায়, বাস্তবে ব্যর্থ হয়। একে বলে data leakage, আর merge হলো এটি ঢোকার সবচেয়ে সাধারণ দরজাগুলোর একটি।

আরও গভীরে। pandas-এর merge সাধারণত দুই টেবিলের key-কে প্রথমে পূর্ণসংখ্যার code-এ বদলায় (factorize, একটি hash table দিয়ে), তারপর সেই code ধরে মেলায়; তাই খরচ মোটামুটি দুই টেবিলের সারির যোগফলের অনুপাতে, আর ফলের আকারের অনুপাতে, বাঁয়ের প্রতিটি সারিকে ডানের প্রতিটির সঙ্গে তুলনা করার মতো m × n নয়। ডেটাবেসের ভাষায় এটি hash join-এর কাছাকাছি; আরেকটি প্রচলিত পদ্ধতি sort-merge join, যা দুই দিক সাজিয়ে একসঙ্গে হাঁটে (DBMS কোর্সের query processing পর্বে আছে)। pandas-এর মূল নকশা (label-যুক্ত index, alignment আর join) Wes McKinney বর্ণনা করেন ২০১০ সালের SciPy সম্মেলনের প্রবন্ধ “Data Structures for Statistical Computing in Python”-এ। melt নামটি এসেছে R-এর reshape package থেকে, যা Wickham ২০০৭ সালে প্রকাশ করেন; সেখানে melt-এর উল্টো কাজের নাম ছিল cast.

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):

  • merge আর join-এর পার্থক্য? merge কলাম (বা index) ধরে মেলায়, default inner; join ডান টেবিলের index ধরে মেলায়, default left.
  • Merge-এর পর সারি বেড়ে গেছে, কেন? ডান টেবিলে key duplicate; left join-এর সারি = ∑ max(1, mi). ধরতে validate="many_to_one" আর আগে-পরে len().
  • concat আর merge-এর পার্থক্য? concat key না মিলিয়ে নিচে নিচে বা index ধরে পাশাপাশি জোড়ে; merge key-র মান মিলিয়ে জোড়ে।
  • pivot আর pivot_table-এর পার্থক্য? pivot duplicate জোড়ায় ValueError দেয়; pivot_table aggregate করে (default mean), আর fill_value, margins দেয়।
  • wide থেকে long কীভাবে, আর long থেকে wide? melt (বা stack); উল্টো পথে pivot/pivot_table (বা unstack).

৯ · AI-এর যুগে — AI সহকারীর merge আর pivot যাচাই

“এই দুটি CSV জোড়া লাগিয়ে জেলাভিত্তিক রিপোর্ট বানাও” লিখলে AI সহকারী কয়েক সেকেন্ডে চলার মতো কোড দেবে। চলার মতো, কিন্তু ঠিক কি না, তা আলাদা প্রশ্ন; কারণ merge-এর ভুল প্রায় কখনো error দেয় না, শুধু সংখ্যা বদলে দেয়। এই বিষয়ে AI সহকারীরা প্রায়ই যা করে:

  • how না লিখে default inner join ব্যবহার করে, ফলে মিল না পাওয়া অর্ডার নিঃশব্দে বাদ পড়ে; বা উল্টো, NaN দেখে fillna(0) বসিয়ে অমিলটি লুকিয়ে ফেলে।
  • Lookup টেবিলের key অনন্য কি না দেখে না; validate বা সারি-গোনার কোনো যাচাই রাখে না, আর যোগফল ফুলে উঠলেও আত্মবিশ্বাসী সারাংশ লেখে।
  • পুরোনো বা বানানো API লেখে: সরানো df.append, merge-এ না থাকা parameter, বা pandas 3-এ বদলানো stack(dropna=...).
  • দুই টেবিল pd.concat(..., axis=1) দিয়ে “পাশাপাশি” বসায়, যা key নয়, index মেলায়; ক্রম আলাদা হলে ভুল গ্রাহকের পাশে ভুল অর্ডার।
  • pivot_table-এ aggfunc না লিখে default mean নেয়, অথচ রিপোর্টের শিরোনাম লেখে “মোট বিক্রি”; আর margins-সহ টেবিল melt করে “All” নামে ভুয়া জেলা বানায়।

প্রম্পটে টেবিলের গঠন, key-র সম্পর্ক আর যাচাইয়ের শর্ত নিজেই দিন। আসল গ্রাহকের ডেটা নয়, বানানো নমুনা দিন।

আমি Python 3.14 আর pandas 3.0 ব্যবহার করছি। তিনটি DataFrame আছে: orders(order_id, customer_id, product_id, qty, date), products(product_id, name, price_paisa), customers(customer_id, district). products আর customers-এ প্রতিটি key একবার থাকার কথা, কিন্তু নিশ্চিত নই। কাজ: প্রতিটি জেলার মাসওয়ারি মোট বিক্রি (পয়সায়) একটি wide টেবিলে। শর্ত: ১. প্রতিটি merge-এ how আর validate স্পষ্ট লেখো, আর indicator=True দিয়ে অমিল সারির গোনা ছাপো। ২. প্রতিটি merge-এর আগে-পরে সারির সংখ্যা আর মোট পয়সা assert দিয়ে মেলাও। ৩. pivot_table-এ aggfunc="sum" স্পষ্ট লেখো; fill_value কেন দিচ্ছ, বলো। ৪. অমিল সারি fillna দিয়ে লুকাবে না; আলাদা টেবিলে দেখাবে। ৫. pandas 3-এ সরানো বা বদলানো কোনো API (append, পুরোনো stack-এর dropna) ব্যবহার করবে না, আর কোনো parameter নিয়ে নিশ্চিত না হলে বলো।

AI-এর merge আর reshape কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১ডেটা যদি শেষে জোড়াই লাগাতে হয়, তবে শুরুতে আলাদা টেবিলে ভাগ করা হয় কেন? এক বিশাল টেবিলে রাখলে ক্ষতি কী?

এই প্রশ্নের উত্তরের শিকড় ১৯৭০ সালে। IBM-এর গবেষক Edgar F. Codd সেই বছর Communications of the ACM-এ “A Relational Model of Data for Large Shared Data Banks” প্রবন্ধে প্রস্তাব করেন, ডেটা রাখা হবে সম্পর্ক (relation), মানে টেবিল হিসেবে, আর প্রোগ্রাম ডেটার ভৌত বিন্যাস নিয়ে মাথা ঘামাবে না। পরের কয়েক বছরে তিনি আর অন্যরা normal form-এর ধারণা গড়ে তোলেন: একই তথ্য যেন একাধিক জায়গায় না থাকে। কারণ খুব ব্যবহারিক। বাজারঘরের প্রতিটি অর্ডারের সারিতে পণ্যের নাম আর দাম লিখে রাখলে তেলের দাম বদলালে লাখো সারি বদলাতে হবে; কোথাও বাদ পড়লে একই পণ্যের দুই রকম দাম। একে বলে update anomaly. ভাগ করে রাখলে দাম থাকে এক জায়গায়।

কিন্তু বিশ্লেষণের সময় উল্টো চাহিদা। মডেল বা রিপোর্ট চায় এক টেবিল, যেখানে প্রতিটি সারিতে দরকারি সব তথ্য। তাই ডেটা ইঞ্জিনিয়ারিংয়ে দুটি জগৎ পাশাপাশি চলে: লেনদেনের সিস্টেমে normalized টেবিল (লেখা নিরাপদ), আর বিশ্লেষণের জন্য denormalized টেবিল (পড়া দ্রুত)। Ralph Kimball ১৯৯৬ সালের “The Data Warehouse Toolkit” বইয়ে star schema জনপ্রিয় করেন: মাঝে একটি fact টেবিল (যেমন orders, প্রতিটি ঘটনা একটি সারি) আর চারপাশে dimension টেবিল (products, customers, তারিখ)। লক্ষ করুন, এটি ঠিক এই পর্বের ছবি; আর fact টেবিলের সঙ্গে dimension জোড়া হয় ঠিক many-to-one সম্পর্কে।

pandas-এ কাজ করার সময়ও একই বিনিময়। এক বিশাল টেবিলে সব রাখলে মেমরি বেশি লাগে (প্রতিটি অর্ডারে পণ্যের নাম আবার আবার), আর কোন তথ্য কোন সময়ের, তা গুলিয়ে যায়। আবার প্রতিবার জোড়া লাগালে ভুল ঢোকার সুযোগ (এই পর্বের সারি বিস্ফোরণ)। ব্যবহারিক নিয়ম: উৎস টেবিলগুলো আলাদা রাখুন, আর বিশ্লেষণের শুরুতে একটি পরীক্ষিত function দিয়ে একবার জোড়া লাগান, যার ভেতরে validate আর সারি-গোনার assert আছে। DBMS কোর্সের normalization আর denormalization পর্বে দুই দিকের পুরো যুক্তি আছে।

মূল উপলব্ধি: ভাগ করে রাখা লেখার নিরাপত্তার জন্য, জোড়া লাগানো পড়ার সুবিধার জন্য; দুটিই দরকার, আর জোড়ার জায়গাটিই যাচাইয়ের সবচেয়ে জরুরি বিন্দু।

প্র ০২Wickham-এর “Tidy Data” এত প্রভাবশালী হলো কেন? সব ডেটাকেই কি long আকারে রাখা উচিত?

Hadley Wickham-এর ২০১৪ সালের প্রবন্ধটি নতুন কোনো গণিত আনেনি; এর শক্তি ছিল একটি পরিচিত কিন্তু নামহীন অভিজ্ঞতাকে নাম আর নিয়ম দেওয়া। প্রবন্ধের শুরুতেই তিনি একটি প্রচলিত ধারণা উল্লেখ করেন যে ডেটা বিশ্লেষণের বড় অংশ সময় যায় ডেটা পরিষ্কার আর গোছাতে। তাঁর যুক্তি: অগোছালো ডেটা হাজার রকমের, কিন্তু গোছানো ডেটার একটি মানক আকার থাকলে প্রতিটি টুল সেই এক আকার ধরে লেখা যায়, আর টুলগুলো একে অপরের সঙ্গে জোড়া লাগে। তিনি অগোছালো ডেটার পাঁচটি সাধারণ ধরন তালিকা করেন, যার প্রথমটি “column headers are values, not variable names”, মানে ঠিক এই পর্বের মাস-কলামওয়ালা wide টেবিল।

প্রবন্ধটি প্রভাবশালী হয় কারণ এর পেছনে ছিল কাজ করা টুল। R-এ Wickham-এর reshape2, পরে tidyr আর ggplot2 এই আকার ধরে গড়া; পরে “tidyverse” নামে একটি পুরো ঘরানা তৈরি হয়। Python-এ seaborn-এর আধুনিক API, plotly express আর অনেক টুল long টেবিল চায়। তিনি নিজেই দেখান, এই নিয়ম Codd-এর third normal form-এর খুব কাছের; পার্থক্য হলো, ডেটাবেসের লোকেরা ভাবেন লেখার নিরাপত্তা নিয়ে, পরিসংখ্যানবিদেরা ভাবেন বিশ্লেষণের সুবিধা নিয়ে।

তবু সব ডেটা long রাখা উচিত নয়, আর Wickham নিজেও তা বলেননি; তিনি স্বীকার করেন, কিছু কাজে অন্য আকার বেশি কার্যকর। Matrix-ভিত্তিক হিসাব (NumPy-র linear algebra, ছবির pixel, মডেলের feature matrix) স্বাভাবিকভাবেই wide; ১০০০টি feature-এর জন্য long টেবিল হবে ১০০০ গুণ লম্বা আর ধীর। মানুষের পড়ার রিপোর্টও wide. আর “কোনটি variable” প্রশ্নের উত্তর প্রসঙ্গনির্ভর: “জানুয়ারির বিক্রি” আর “ফেব্রুয়ারির বিক্রি” দুটি আলাদা variable, নাকি “মাস” আর “বিক্রি” দুটি variable? মাসওয়ারি তুলনার chart-এ দ্বিতীয়টি, কিন্তু “জানুয়ারির বিক্রি দিয়ে ফেব্রুয়ারি অনুমান” করার মডেলে প্রথমটি। Tidy তাই একটি অবস্থান নয়, একটি প্রশ্ন: “এই কাজে পর্যবেক্ষণ কী?”

মূল উপলব্ধি: Tidy data একটি মানক আকার দিয়ে টুলগুলোকে জোড়ে; কিন্তু কোনটি পর্যবেক্ষণ আর কোনটি variable, তা পরের কাজ ঠিক করে, তাই melt আর pivot দুটিই সমান দরকারি।

প্র ০৩সারি বিস্ফোরণ কেন এত বিপজ্জনক, অথচ এত সাধারণ? বড় প্রতিষ্ঠানের ডেটা দল এটি কীভাবে ঠেকায়?

বিপদের মূল কারণ নীরবতা। Merge-এর কাছে duplicate key কোনো ভুল নয়; many-to-many join গাণিতিকভাবে সম্পূর্ণ বৈধ, আর কখনো কখনো ইচ্ছাকৃতও (যেমন প্রতিটি পণ্যের সঙ্গে তার সব ট্যাগ)। তাই pandas বা SQL কেউই সতর্ক করে না। ফলাফল দেখতেও স্বাভাবিক: কলাম ঠিক আছে, NaN নেই, সংখ্যাগুলো বিশ্বাসযোগ্য মাত্রার। শুধু যোগফল কিছুটা বেশি। আর “কিছুটা বেশি” বিক্রির রিপোর্ট কেউ সাধারণত প্রশ্ন করে না, বরং খুশি হয়।

সাধারণ হওয়ার কারণ, duplicate key ঢোকার পথ অনেক। এই পর্বের মতো দাম বদলের নতুন সারি; একই গ্রাহক দুই ফোন নম্বরে দুবার নিবন্ধিত; একটি ফাইল ভুল করে দুবার পড়ে concat; বা key-টি আসলে অনন্য নয় (জেলার নাম দিয়ে join, অথচ দুই বিভাগে একই নামের উপজেলা)। Business intelligence-এর জগতে এর পুরোনো নামও আছে, “fan trap” আর “chasm trap”: একটি fact টেবিলকে দুটি one-to-many পথ দিয়ে জোড়ালে যোগফল গুণ হয়ে যায়। নামগুলো টুল ভেদে কিছুটা আলাদা অর্থে ব্যবহৃত হয়, কিন্তু মূল সমস্যা একই।

ঠেকানোর পদ্ধতিগুলো সফটওয়্যার টেস্টিংয়ের ভাষায় লেখা দাবি। প্রথম স্তর: merge-এর জায়গায় validate আর সারি-গোনার assert, ঠিক এই পর্বের মতো। দ্বিতীয় স্তর: ডেটা পাইপলাইনের প্রতিটি টেবিলের জন্য লিখিত পরীক্ষা, যেমন “product_id অনন্য আর null নয়”, “orders-এর প্রতিটি product_id products-এ আছে”। dbt-এর মতো জনপ্রিয় ডেটা-রূপান্তরের টুলে unique, not_null আর relationships নামে ঠিক এমন পরীক্ষা অন্তর্ভুক্ত; Python-এ pandera বা Great Expectations-এর মতো library একই কাজ করে। তৃতীয় স্তর: মোট সংখ্যার পুনর্মিলন (reconciliation), যেমন রিপোর্টের মোট বিক্রি হিসাব বিভাগের খাতার সঙ্গে মেলে কি না। ব্যাংকের হিসাব মেলানোর মতো, এটি পুরোনো কিন্তু কার্যকর।

আর একটি মানসিক অভ্যাস: প্রতিটি join লেখার আগে মুখে বলুন, “এটি many-to-one, তাই সারি বদলাবে না।” যদি বলতে না পারেন, আগে key-এর গঠন দেখুন। পরীক্ষার খাতা দেখার সময় শিক্ষক যেমন মোট নম্বর আবার যোগ করে দেখেন, তেমনি।

মূল উপলব্ধি: Join-এর ভুল error দেয় না, সংখ্যা বদলায়; তাই key-র অনন্যতা আর সারির সংখ্যা কোডে লিখিত দাবি হিসেবে রাখুন, মনে মনে নয়।

প্র ০৪Merge ভেতরে কীভাবে চলে? ১ কোটি অর্ডারকে ১০ হাজার পণ্যের সঙ্গে জোড়া লাগাতে কেন ১০১১টি তুলনা লাগে না?

সবচেয়ে সরল পদ্ধতি হলো nested loop join: বাঁয়ের প্রতিটি সারির জন্য ডানের প্রতিটি সারি দেখা। ১ কোটি × ১০ হাজার = ১০১১টি তুলনা, যা সাধারণ ল্যাপটপে ঘণ্টার পর ঘণ্টা। অথচ pandas-এ এমন merge সাধারণত কয়েক সেকেন্ডে হয়। কারণ ডাটাবেস গবেষণা কয়েক দশক আগেই দুটি ভালো পথ বের করেছে। Hash join: ছোট টেবিলের key দিয়ে একটি hash table বানানো (লাইব্রেরির ক্যাটালগের মতো, যেখানে নাম দেখেই তাক নম্বর মেলে), তারপর বড় টেবিলের প্রতিটি সারির key সেখানে একবার খোঁজা। খরচ মোটামুটি দুই টেবিলের সারির যোগফলের অনুপাতে, মানে প্রায় ১ কোটি ১০ হাজার ধাপ। Sort-merge join: দুই টেবিল key অনুযায়ী সাজিয়ে দুটি আঙুল একসঙ্গে নিচে নামানো, যেমন দুটি সাজানো হাজিরা খাতা মেলানো। সাজাতে n log n, মেলাতে n.

pandas-এর বাস্তবায়ন এই দুইয়ের মিশ্রণ। লেখার সময়ের উৎস কোড অনুযায়ী, merge প্রথমে দুই দিকের key-কে “factorize” করে: একটি hash table দিয়ে প্রতিটি আলাদা key-কে একটি ছোট পূর্ণসংখ্যা দেয় (P1 → 0, P2 → 1 …). তারপর সেই পূর্ণসংখ্যার code ধরে Cython-এ লেখা দ্রুত রুটিন দিয়ে জোড়া বানায়। একাধিক key কলাম হলে প্রতিটির code মিলিয়ে একটি সমন্বিত code বানায়। বিস্তারিত সংস্করণ ভেদে বদলায়, তাই নির্দিষ্ট অ্যালগরিদমের দাবি না করাই নিরাপদ; ধারণাটিই মূল।

এর দুটি ব্যবহারিক ফল আছে। প্রথমত, merge-এর খরচ ফলের আকারের ওপরও নির্ভর করে। সারি বিস্ফোরণে ফল যদি ১০ গুণ বড় হয়, সময় আর মেমরিও প্রায় ১০ গুণ; অনেক সময় “merge-এ MemoryError” আসলে লুকানো many-to-many join. দ্বিতীয়ত, key-র dtype গুরুত্বপূর্ণ: পূর্ণসংখ্যা বা category key-তে factorize সস্তা, লম্বা string-এ দামি। আর একদিকে int, অন্যদিকে string হলে pandas হয় error দেয়, নয়তো কিছুই মেলে না। খুব বড় ডেটায় অনেকে DuckDB বা Polars ব্যবহার করেন, যারা একাধিক CPU core-এ hash join চালায় আর মেমরির বাইরেও কাজ করতে পারে; Polars-এর পরিচয় আসবে পর্ব ২৭-এ।

মূল উপলব্ধি: ভালো join অ্যালগরিদম প্রতিটি জোড়া তুলনা করে না, key-কে hash বা sort করে খোঁজে; তাই খরচ নির্ভর করে ইনপুট আর ফলের আকারের ওপর, আর ফল ফুলে উঠলে খরচও ফুলে ওঠে।

প্র ০৫Spreadsheet-এর pivot table থেকে pandas-এর pivot_table: কোডে রিপোর্ট বানানোর আসল লাভ কী?

Pivot table-এর ধারণাটি spreadsheet-এর জগতে জনপ্রিয় হয় ১৯৯০-এর দশকের শুরুতে। সাধারণত বলা হয়, Lotus Improv (১৯৯১ সালের দিকে, NeXT কম্পিউটারের জন্য) প্রথম এমন একটি spreadsheet, যেখানে ডেটা আর তার সারি-কলামের বিন্যাস আলাদা, আর বিন্যাস টেনে বদলানো যায়; আর Microsoft Excel-এ “PivotTable” আসে ১৯৯৩ সালের দিকে Excel 5-এ। ইতিহাসের খুঁটিনাটি নিয়ে উৎসভেদে পার্থক্য আছে, কিন্তু মূল ধারণা একই: একটি লম্বা টেবিলের দুটি কলামকে সারি আর কলামের শিরোনাম বানিয়ে প্রতিটি ঘরে যোগফল বা গড়। pandas-এর pivot_table-এর নাম আর margins (মোট সারি-কলাম) সেই ঐতিহ্য থেকেই।

Spreadsheet দ্রুত, দৃশ্যমান, আর অনেকের জন্য সবচেয়ে সহজ টুল। তাহলে কোড কেন? প্রথম কারণ পুনরুৎপাদনযোগ্যতা। মাউসে টেনে বানানো pivot-এর ধাপগুলো কোথাও লেখা থাকে না; পরের মাসে নতুন ডেটায় একই রিপোর্ট বানাতে আবার হাতে সব করতে হয়, আর কোন ফিল্টার দেওয়া ছিল তা ভুলে যাওয়া সহজ। কোডে প্রতিটি ধাপ লেখা, Git-এ রাখা যায় (পর্ব ১২-এ দেখেছেন), আর একই কোড নতুন ডেটায় একই নিয়মে চলে।

দ্বিতীয় কারণ যাচাই। একটি বহুল আলোচিত ঘটনা: ২০১০ সালে অর্থনীতিবিদ Carmen Reinhart আর Kenneth Rogoff সরকারি ঋণ আর প্রবৃদ্ধি নিয়ে একটি প্রভাবশালী প্রবন্ধ প্রকাশ করেন। ২০১৩ সালে Thomas Herndon, Michael Ash আর Robert Pollin তাঁদের spreadsheet পুনরায় বিশ্লেষণ করে দেখান, একটি সূত্রের পরিসরে কয়েকটি দেশ বাদ পড়েছিল, আর আরও কিছু পদ্ধতিগত সিদ্ধান্ত নিয়ে প্রশ্ন তোলেন। মূল লেখকেরা ভুলটি স্বীকার করেন, যদিও সার্বিক উপসংহারের ওপর এর প্রভাব কতটা, তা নিয়ে তাঁদের দ্বিমত ছিল। শিক্ষাটি spreadsheet-বিরোধী নয়: ভুল কোডেও হয়। কিন্তু কোডে assert, সারি-গোনা আর পরীক্ষা লেখা যায়, আর অন্য কেউ ধাপগুলো পড়ে পর্যালোচনা করতে পারে।

তবু বিনিময় আছে। দ্রুত একবারের অনুসন্ধানে spreadsheet প্রায়ই জেতে, আর যাঁরা কোড পড়েন না, তাঁদের সঙ্গে ভাগ করতে Excel-ই বাস্তব। ভালো অভ্যাস হলো দুটি জোড়া: কোডে হিসাব, আর ফলটি to_excel দিয়ে Excel-এ পাঠানো (পর্ব ২১-এ ফাইল লেখার আলোচনা আছে)।

মূল উপলব্ধি: Pivot table একই, পার্থক্য প্রক্রিয়ায়: কোডে লেখা রিপোর্ট আবার চালানো, যাচাই আর পর্যালোচনা করা যায়, যা হাতে বানানো রিপোর্টে কঠিন।

অনুশীলন

প্রথমটি হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লেখার।

  1. হিসাব: বাঁ টেবিল o-র key কলামে ৭টি সারি: A, A, B, C, C, C, D. ডান টেবিল l-এর key কলামে ৫টি সারি: A, B, B, C, E. o.merge(l, on="k", how=...)-এর ফলে কতটি সারি হবে, যখন how = (ক) left, (খ) inner, (গ) outer, (ঘ) right? (ঙ) validate="many_to_one" দিলে কী হবে?

    প্রতিটি বাঁয়ের সারির জন্য ডানে মিল mi: A → ১ (দুটি সারি), B → ২, C → ১ (তিনটি সারি), D → ০. (খ) inner = ∑ mi = ১ + ১ + ২ + ১ + ১ + ১ + ০ = ৭. (ক) left = ∑ max(1, mi) = ৭ + D-এর ১ = ৮. (গ) outer = left-এর ৮ + ডানে যার বাঁয়ে মিল নেই (E) ১ = ৯. (ঘ) right: ডানের প্রতিটি সারি ধরে গুনুন: A → বাঁয়ে ২, B → ১, B → ১, C → ৩, E → ০ হলেও ১; মোট ২ + ১ + ১ + ৩ + ১ = ৮. (ঙ) ডানে B দুবার, তাই MergeError. লেখকের যন্ত্রে সত্যিকারের run (pandas 3.1.1): left 8, inner 7, outer 9, right 8, আর validate দিলে MergeError.

  2. আউটপুট অনুমান করুন: চালানোর আগে আউটপুট লিখুন।
    import pandas as pd
    a = pd.DataFrame({"k": ["x", "y", "z"], "v": [1, 2, 3]})
    b = pd.DataFrame({"k": ["y", "z", "w"], "v": [20, 30, 40]})
    m = a.merge(b, on="k", how="outer", indicator=True)
    print(m)
    print(pd.concat([a, b]).shape, pd.concat([a, b], axis=1).shape)

    লেখকের যন্ত্রে সত্যিকারের run (Python 3.14.4, pandas 3.1.1):

       k  v_x   v_y      _merge
    0  w  NaN  40.0  right_only
    1  x  1.0   NaN   left_only
    2  y  2.0  20.0        both
    3  z  3.0  30.0        both
    (6, 2) (3, 4)

    চারটি জিনিস লক্ষ করুন। (১) outer join key বর্ণানুক্রমে সাজায়, তাই w সবার আগে, যদিও সেটি ডান টেবিলের শেষ সারি। (২) দুই দিকেই v কলাম, তাই default suffix _x আর _y. (৩) NaN ঢোকায় দুটি v কলামই float হয়েছে (1.0, 20.0). (৪) concat নিচে নিচে জুড়লে ৬ সারি, ২ কলাম; পাশাপাশি (axis=1) জুড়লে index 0, 1, 2 মিলিয়ে ৩ সারি আর ৪ কলাম, যেখানে দুটি কলামের নামই k আর দুটিই v. লক্ষ করুন, পাশাপাশি concat x-এর পাশে y বসিয়েছে, কারণ এটি key নয়, সারির index মেলায়।

  3. নকশা ও সমালোচনা: একটি AI সহকারী গ্রাহকভিত্তিক মডেলের feature টেবিল বানাতে নিচের কোড দিয়েছে। অন্তত পাঁচটি সমস্যা চিহ্নিত করুন, আর ভালো সংস্করণের রূপরেখা দিন।
    df = orders.merge(customers, on="customer_id")
    df = pd.concat([df, products], axis=1)
    feat = df.pivot_table(index="customer_id",
                          columns="category", values="price_paisa")
    feat = feat.fillna(0)
    feat["next_month_spend"] = next_month.groupby("customer_id")["paisa"].sum()
    • Default inner join: customers-এ না থাকা গ্রাহকের অর্ডার নিঃশব্দে বাদ; how="left", indicator=True আর validate="many_to_one" দরকার।
    • concat দিয়ে পণ্য জোড়া: axis=1 সারির index মেলায়, product_id নয়; প্রথম অর্ডারের পাশে products-এর প্রথম পণ্য বসবে, যা সম্পূর্ণ ভুল। দরকার merge(products, on="product_id", how="left", validate="many_to_one").
    • aggfunc না দেওয়া: default mean, মানে “গড় দাম”, অথচ খরচের feature হওয়ার কথা পরিমাণ × দামের যোগফল। আগে paisa = qty * price_paisa কলাম, তারপর aggfunc="sum", আর সারি-গোনার assert.
    • fillna(0) সবকিছুতে: “কোনো অর্ডার নেই” আর “তথ্য নেই” এক হয়ে গেছে; যোগফলে ০ ঠিক হলেও গড়ের মতো feature-এ মিথ্যা। pivot_table-এর fill_value=0 শুধু যোগফলে, জেনেশুনে।
    • Data leakage: next_month_spend ভবিষ্যতের তথ্য। এটি target হতে পারে, কিন্তু feature টেবিলে রাখলে মডেল ভবিষ্যৎ দেখে ফেলবে; আর এটি index ধরে বসছে, তাই যে গ্রাহক পরের মাসে কেনেননি তাঁর ঘর NaN. Target আলাদা রাখুন, আর feature শুধু কাটঅফ তারিখের আগের অর্ডার থেকে।

    ভালো সংস্করণের ক্রম: (১) কাটঅফ তারিখের আগের orders বাছাই; (২) products আর customers-এর সঙ্গে left merge, validate আর indicator সহ; (৩) paisa কলাম; (৪) pivot_table(..., aggfunc="sum", fill_value=0); (৫) প্রতিটি ধাপে len() আর মোট পয়সার assert; (৬) target আলাদা টেবিলে, পরের মাসের ডেটা থেকে।

  4. কোড লিখুন: তিনটি জেলার মাসওয়ারি গড় ডেলিভারি-সময় (দিন) একটি wide টেবিলে আছে: district = Dhaka, Khulna, Sylhet; Jan = 2.1, 3.4, 4.0; Feb = 2.4, 3.1, 4.6; Mar = 1.9, 3.8, 3.2. (ক) melt দিয়ে long টেবিল বানান (কলাম: district, month, days) আর তার shape ছাপুন। (খ) যেসব সারিতে days > 3, সেগুলো দেখান। (গ) pivot দিয়ে আবার wide-এ ফিরুন, মাসের ক্রম Jan, Feb, Mar রেখে।
    import pandas as pd
    wide = pd.DataFrame({"district": ["Dhaka", "Khulna", "Sylhet"],
                         "Jan": [2.1, 3.4, 4.0],
                         "Feb": [2.4, 3.1, 4.6],
                         "Mar": [1.9, 3.8, 3.2]})
    long = wide.melt(id_vars="district", var_name="month",
                     value_name="days")
    print(long.shape)
    print(long[long["days"] > 3])
    back = long.pivot(index="district", columns="month", values="days")
    print(back[["Jan", "Feb", "Mar"]])

    লেখকের যন্ত্রে সত্যিকারের run (pandas 3.1.1):

    (9, 3)
      district month  days
    1   Khulna   Jan   3.4
    2   Sylhet   Jan   4.0
    4   Khulna   Feb   3.1
    5   Sylhet   Feb   4.6
    7   Khulna   Mar   3.8
    8   Sylhet   Mar   3.2
    month     Jan  Feb  Mar
    district
    Dhaka     2.1  2.4  1.9
    Khulna    3.4  3.1  3.8
    Sylhet    4.0  4.6  3.2

    ৩ জেলা × ৩ মাস = ৯ সারি। Long আকারে ফিল্টার এক লাইনের: “কোন জেলা-মাসে ৩ দিনের বেশি” প্রশ্নটি wide টেবিলে করতে তিনটি কলাম আলাদা দেখতে হতো। pivot কলামগুলো বর্ণানুক্রমে সাজায় (Feb, Jan, Mar), তাই শেষে [["Jan", "Feb", "Mar"]] দিয়ে ক্রম ঠিক করা হয়েছে। প্রতিটি (district, month) একবারই আছে, তাই pivot_table লাগেনি। একই long টেবিল থেকে জেলাভিত্তিক গড় পেতে পর্ব ২৪-এর groupby: long.groupby("district")["days"].mean(), যা দেয় Dhaka 2.13, Khulna 3.43, Sylhet 3.93.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমঅর্ডার টেবিলে পণ্যের তথ্য নিরাপদে জোড়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔍 Lookup টেবিলে key অনন্য কি না duplicated().sum() দিয়ে দেখা
  2. 🔢 Merge-এর আগে সারির সংখ্যা আর মোট পয়সা লিখে রাখা
  3. 🔗 how, validate আর indicator=True স্পষ্ট দিয়ে merge চালানো
  4. 🧐 _merge-এর গোনা দেখে অমিল সারির কারণ খোঁজা
  5. ✅ পরে সারির সংখ্যা আর মোট পয়সা আগের সঙ্গে মেলানো
  6. 🧹 _merge কলাম বাদ দিয়ে পরের ধাপে যাওয়া

orders টেবিলে ৫টি সারি, যার ২টির product_id = "P7". products টেবিলে "P7" দুবার আছে, বাকি সব key একবার, আর orders-এর প্রতিটি key products-এ আছে। orders.merge(products, on="product_id", how="left")-এর ফলে কতটি সারি?

  • ৫
  • ১০
  • ৭
  • ৬

pandas-এ left.merge(right, on="k", how="outer", indicator=True) চালানোর পর যে সারির key শুধু right-এ আছে, তার _merge কলামের মান কী?

  • left_only
  • right_only
  • both
  • NaN

একটি long টেবিলে একই (district, month) জোড়ার একাধিক সারি আছে। pivot আর pivot_table সম্পর্কে কোনটি সঠিক?

  • দুটিই default-এ মানগুলো যোগ করে
  • pivot প্রথম মান রাখে, pivot_table শেষ মান
  • pivot ValueError দেয়, pivot_table default-এ যোগফল নেয়
  • pivot ValueError দেয়, pivot_table default-এ গড় (mean) নেয়

দুটি DataFrame, প্রতিটির index 0, 1, 2 আর কলাম একই। pd.concat([jan, feb])-এর ফল সম্পর্কে কোনটি সঠিক?

  • ৬টি সারি, index 0, 1, 2, 0, 1, 2
  • ৬টি সারি, index 0 থেকে 5
  • ৩টি সারি, ৬টি কলাম
  • ৩টি সারি, কারণ index মিলিয়ে জোড়া হয়

একটি wide টেবিলে ৫টি সারি আর ৪টি কলাম: district, Jan, Feb, Mar. melt(id_vars="district")-এর ফলের shape কী?

  • (5, 4)
  • (15, 3)
  • (20, 3)
  • (15, 2)

pd.crosstab(df["district"], df["payment"], normalize="index")-এর ফল সম্পর্কে কোনটি সঠিক?

  • প্রতিটি কলামের যোগ ১
  • পুরো টেবিলের সব ঘরের যোগ ১
  • প্রতিটি সারির (জেলার) যোগ ১
  • গোনা বদলায় না, শুধু মোট-সারি যোগ হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Merge = key মিলিয়ে জোড়া: on বা left_on/right_on, আর how সবসময় স্পষ্ট; default inner অমিল সারি নিঃশব্দে ফেলে দেয়।
  2. সারি গুনুন: left join-এর সারি = ∑ max(1, mi); lookup-এ duplicate key মানে সারি বিস্ফোরণ। validate, আগে-পরে len() আর যোগফলের assert.
  3. indicator=True: both, left_only, right_only দিয়ে অমিল খুঁজুন; fillna দিয়ে লুকাবেন না।
  4. concat জুড়ে দেয়, মেলায় না: নিচে নিচে ignore_index=True, পাশাপাশি index ধরে; join ডানের index ধরে, default left.
  5. Wide ↔ long: pivot_table (aggfunc লিখুন, fill_value, margins), melt, stack/unstack, আর গোনা-শতাংশে crosstab; chart-এ সাধারণত long, রিপোর্ট আর feature matrix-এ wide.

পরের পর্বে: বাজারঘরের অর্ডারের তারিখ নিয়ে কাজ: তারিখ পড়া, সপ্তাহ আর মাসে resample, rolling গড়, আর Asia/Dhaka সময় অঞ্চল। দামের পরিবর্তনের তারিখ ধরে অর্ডার মেলানোর merge_asof-ও সেখানে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান