এই পর্বে যা শিখবেন
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
ছবির লাল দুটি ঘর এই পর্বের প্রথম শিক্ষা। অর্ডার ১০৬-এর পণ্য 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" | বাঁয়ের সব; ডানে না মিললে NaN | LEFT JOIN | প্রতিটি অর্ডারে নাম-দাম জোড়া, কোনো অর্ডার না হারিয়ে |
"right" | ডানের সব; বাঁয়ে না মিললে NaN | RIGHT JOIN | প্রতিটি পণ্য আর তার অর্ডার (বাঁ-ডান উল্টে left-ও চলে) |
"outer" | দুই দিকের সব key | FULL 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 দেখুন। ভেতরের আসল পদ্ধতি দ্রুততর (আরও গভীরে বাক্সে আছে), কিন্তু ফল ঠিক এই ছবির মতো।
| বাঁয়ের সারি | |
| key | |
| ডানে মিল | |
| ফলের সারি (left) | |
| inner হলে |
- দুটি টেবিল। বাঁয়ে চারটি অর্ডার, ডানে তিনটি পণ্য। প্রতিটি পণ্য ডানে একবারই আছে, তাই এটি সত্যিকারের lookup table. ফল এখনো খালি।
- অর্ডার 101, key P1. ডানে P1 একটিই, তাই ফলে একটি সারি: অর্ডারের কলামের পাশে “চাল”. দুই দিকেই key আছে, তাই এটি
both. - অর্ডার 102, key P2. আবার ঠিক একটি মিল, ফলের দ্বিতীয় সারি “তেল”.
- অর্ডার 103, key P5. ডানে P5 নেই। Left join বাঁয়ের সারি ফেলে না, তাই সারিটি থাকে, কিন্তু name-এর ঘর NaN, আর উৎস
left_only. Inner join হলে এই সারি এখানেই বাদ পড়ত, কোনো সতর্কবার্তা ছাড়াই। - অর্ডার 104, key আবার P2. Lookup-এর একই সারি দ্বিতীয়বার কাজে লাগল। একটি পণ্যের অনেক অর্ডার, তাই এটি স্বাভাবিক: many-to-one.
- ফল। 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 অংশে আছে। ধাপে ধাপে দেখুন সারি কীভাবে গুণ হয়।
| এই অর্ডারে মিল (mi) | |
| ফলের মোট সারি | |
| P2-এর সারি |
- Lookup আর lookup নেই। products-এ P2 দুবার, দুটি আলাদা দামে।
products["product_id"].duplicated().sum()বলবে ১. Merge এটি দেখেও থামে না; সে শুধু নিয়ম মানে: প্রতিটি মিলের জন্য একটি সারি। - অর্ডার 101, P1. ডানে একটি মিল, ফলে একটি সারি। এখন পর্যন্ত সব ঠিক।
- অর্ডার 102, P2. ডানে দুটি মিল, তাই ফলে দুটি সারি: একই অর্ডার, একবার ১৮৫ টাকায়, একবার ১৯৫ টাকায়। mi = ২.
- অর্ডার 103, P3. আবার একটি মিল। মোট ৪টি সারি, অথচ অর্ডার মাত্র ৩টি।
- অর্ডার 104 আর 105, দুটিই P2. প্রতিটির জন্য দুটি করে, মোট ৪টি নতুন সারি। ফল এখন ৮টি সারি, ৫টি অর্ডার থেকে।
- অর্ডার 106, P1. একটি মিল। শেষে ৯টি সারি: ১ + ২ + ১ + ২ + ২ + ১ = ৯, ঠিক অংশ ২-এর সূত্র ∑ max(1, mi).
- ক্ষতির হিসাব। ৬টি অর্ডার থেকে ৯টি সারি; 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) বা join | index-এর 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-এর shape | |
| long-এর shape | |
| “মাস” কোথায় |
- Wide টেবিল। প্রতিটি জেলার একটি সারি, প্রতিটি মাসের একটি কলাম।
id_vars="district"বলে district কলামটি পরিচয়ের, সেটি নামবে না। - প্রথম মাসের কলাম নামল। “2026-01” কলামের প্রতিটি ঘর একটি নতুন সারি হলো: জেলা যেমন ছিল, month-এ কলামের নাম, taka-য় ঘরের মান। দুটি জেলা, তাই দুটি সারি।
- দ্বিতীয় মাসের কলাম। একইভাবে আরও দুটি সারি। melt কলামের পর কলাম কাজ করে, তাই ফলের ক্রম: আগে সব জেলার জানুয়ারি, তারপর সব জেলার ফেব্রুয়ারি।
- Long টেবিল তৈরি। সারি = জেলার সংখ্যা × মাসের কলামের সংখ্যা = ৪; কলাম ৩টি। এটি tidy: প্রতিটি variable একটি কলাম, প্রতিটি পর্যবেক্ষণ একটি সারি।
- উল্টো পথে
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-এ defaultTrue.
নিচের কোডে 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-র সম্পর্ক আর যাচাইয়ের শর্ত নিজেই দিন। আসল গ্রাহকের ডেটা নয়, বানানো নমুনা দিন।
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 একই, পার্থক্য প্রক্রিয়ায়: কোডে লেখা রিপোর্ট আবার চালানো, যাচাই আর পর্যালোচনা করা যায়, যা হাতে বানানো রিপোর্টে কঠিন।
অনুশীলন
প্রথমটি হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লেখার।
-
হিসাব: বাঁ টেবিল
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. -
আউটপুট অনুমান করুন: চালানোর আগে আউটপুট লিখুন।
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 মেলায়। -
নকশা ও সমালোচনা: একটি 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 আলাদা টেবিলে, পরের মাসের ডেটা থেকে। - Default inner join: customers-এ না থাকা গ্রাহকের অর্ডার নিঃশব্দে বাদ;
-
কোড লিখুন: তিনটি জেলার মাসওয়ারি গড় ডেলিভারি-সময় (দিন) একটি 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.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমঅর্ডার টেবিলে পণ্যের তথ্য নিরাপদে জোড়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔍 Lookup টেবিলে key অনন্য কি না
duplicated().sum()দিয়ে দেখা - 🔢 Merge-এর আগে সারির সংখ্যা আর মোট পয়সা লিখে রাখা
- 🔗
how,validateআরindicator=Trueস্পষ্ট দিয়ে merge চালানো - 🧐
_merge-এর গোনা দেখে অমিল সারির কারণ খোঁজা - ✅ পরে সারির সংখ্যা আর মোট পয়সা আগের সঙ্গে মেলানো
- 🧹
_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শেষ মানpivotValueError দেয়,pivot_tabledefault-এ যোগফল নেয়- 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")-এর ফল সম্পর্কে কোনটি সঠিক?
- প্রতিটি কলামের যোগ ১
- পুরো টেবিলের সব ঘরের যোগ ১
- প্রতিটি সারির (জেলার) যোগ ১
- গোনা বদলায় না, শুধু মোট-সারি যোগ হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Merge = key মিলিয়ে জোড়া:
onবাleft_on/right_on, আরhowসবসময় স্পষ্ট; default inner অমিল সারি নিঃশব্দে ফেলে দেয়। - সারি গুনুন: left join-এর সারি = ∑ max(1, mi); lookup-এ duplicate key মানে সারি বিস্ফোরণ।
validate, আগে-পরেlen()আর যোগফলের assert. indicator=True:both,left_only,right_onlyদিয়ে অমিল খুঁজুন; fillna দিয়ে লুকাবেন না।- concat জুড়ে দেয়, মেলায় না: নিচে নিচে
ignore_index=True, পাশাপাশি index ধরে;joinডানের index ধরে, default left. - Wide ↔ long:
pivot_table(aggfunc লিখুন, fill_value, margins),melt,stack/unstack, আর গোনা-শতাংশেcrosstab; chart-এ সাধারণত long, রিপোর্ট আর feature matrix-এ wide.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৬ · Time series — তারিখ, resample, rolling আর Asia/Dhakaপরবর্তী পর্বতারিখ পড়া, Asia/Dhaka আর UTC, resample, rolling আর ভবিষ্যৎ না ঢোকানো lag feature.
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ; পরের পর্ব ২৬ · Time series — তারিখ, resample, rolling আর Asia/Dhaka.
- DBMS · JOIN — INNER, LEFT, RIGHT আর FULLতত্ত্বSQL-এ join-এর ধরন, fan-out আর Venn diagram-এর সীমা।
- DBMS · Self, cross, semi আর anti joinগভীরে যানCross join, anti join আর একাধিক কলামে join.
- DBMS · Normalizationভিত্তিডেটা কেন আলাদা টেবিলে ভাগ করে রাখা হয়।
- পর্ব ১৪ · Indexing আর slicingঝালাইBoolean mask, view আর copy, যা pandas-এর বাছাইয়ের ভিত্তি।