অধ্যায় 4 · Python ও ডেটা হ্যান্ডলিং
ডেটা হ্যান্ডলিংয়ের মূল কথা: ফরম্যাট, মান আর উৎস
- পৃষ্ঠা 12 / 22
- 15 মিনিট পড়া
প্রতিটা মডেল আসলে তার ডেটার একটা সারসংক্ষেপ। দারুণ আর্কিটেকচারের মডেলও যদি ডুপ্লিকেট, ভুল লেবেল বা চুপচাপ কেটে যাওয়া ডেটায় ট্রেন হয়, তাহলে সে শেখে ঠিক ওই ডুপ্লিকেট, ভুল লেবেল আর ফাঁকগুলোই। এজন্যই ডেটা নিয়ে কাজ করা মানুষেরা বলেন "garbage in, garbage out", আর এজন্যই আসল AI প্রজেক্টের বেশিরভাগ সময় যায় ডেটা জোগাড়, বোঝা, যাচাই আর আকার দেওয়ায় — মডেলিংয়ে নয়।
ডেটা হ্যান্ডলিং হলো এই পুরো কাজটা: উৎস থেকে ডেটা সংগ্রহ, তার ফরম্যাট পড়া, মান যাচাই, পরিষ্কার আর রূপান্তর করা, আর কী করলেন তা লিখে রাখা, যাতে ফলাফলে ভরসা করা যায় আর আবার তৈরি করা যায়। এই পাতা আপনাকে শব্দভান্ডার আর অভ্যাসগুলো দেয়, প্রতিটা একটা ছোট উদাহরণে দেখিয়ে। পরের পাতাগুলো আরও গভীরে যায়: Python আর ডেটাবেস, pandas আর SQLAlchemy, ডেটা পরিষ্কারের পূর্ণ একটা প্রজেক্ট, ইমপোর্ট-এক্সপোর্ট, আর পাইপলাইন।
যা শিখবেন
- স্ট্রাকচার্ড, সেমি-স্ট্রাকচার্ড আর আনস্ট্রাকচার্ড ডেটা, আর ডেটা কোথা থেকে আসে
- সাধারণ ফাইল ফরম্যাট — CSV, JSON, Excel, XML, Parquet — আর প্রতিটা কী রাখে বা হারায়
- রেকর্ড, ফিল্ড, টাইপ আর রূপান্তর; অনুপস্থিত, অবৈধ, অসামঞ্জস্যপূর্ণ আর ডুপ্লিকেট মান
- ডেটা পরিষ্কারের একটা ওয়ার্কফ্লো: যাচাই, স্ট্যান্ডার্ডাইজেশন, রূপান্তর, মার্জ আর আকার বদল (wide বনাম long)
- ডেটার মানের ছয়টা মাত্রা, lineage আর provenance, আর পুনরুৎপাদনযোগ্য প্রসেসিং
ডেটার ধরন আর তা কোথা থেকে আসে
| ধরন | দেখতে কেমন | উদাহরণ | AI-এর কাজে |
|---|---|---|---|
| স্ট্রাকচার্ড | নির্দিষ্ট কলাম, প্রতি কলামে একটা টাইপ | শপের টেবিলগুলো, একটা স্প্রেডশিট, ব্যাংক স্টেটমেন্ট | প্রচলিত ML ফিচার, অ্যানালিটিক্স, লেবেল |
| সেমি-স্ট্রাকচার্ড | ফিল্ডের নাম ডেটার সাথেই লেখা থাকে, ভেতরে নেস্টেড স্তর, রেকর্ডভেদে ফিল্ড আলাদা হতে পারে | API-র JSON, LLM কলের লগ, ইভেন্ট স্ট্রিম, XML ফিড | AI অ্যাপের লগ আর ট্রেস, RAG চাঙ্কের মেটাডেটা |
| আনস্ট্রাকচার্ড | আগে থেকে ঠিক করা কোনো ফিল্ড নেই | প্রোডাক্ট রিভিউ, সাপোর্টের ইমেইল, PDF, ছবি, অডিও | LLM আর ভিশন মডেলের কাঁচামাল, সাধারণত এমবেডিংয়ে রূপান্তর করা হয় |
ডেটা আপনার কাছে আসে হাতে গোনা কয়েকটা উৎস থেকে, প্রত্যেকটার নিজস্ব স্বভাব নিয়ে: ডেটাবেস (টাইপ সামঞ্জস্যপূর্ণ, কিন্তু অ্যাক্সেস লাগে আর প্রোডাকশনের ওপর বেশি চাপ দেওয়া চলে না), API (JSON, পাতায় পাতায় ভাগ করা, রেট লিমিট, না জানিয়েই বদলে যায়), ফাইল (CSV আর Excel এক্সপোর্ট, যাতে যা খুশি থাকতে পারে), সেন্সর আর ডিভাইস (টাইম সিরিজ, ফাঁক, ঘড়ির গরমিল, নয়েজ) আর লগ (বিশাল, সেমি-স্ট্রাকচার্ড, প্রায়ই ব্যবহারকারীরা আসলে কী করেছেন তার সেরা দলিল)। উৎস যা-ই হোক, ডেটা এসে পৌঁছায় রেকর্ড (সারি: একটা অর্ডার, একটা ইভেন্ট, একটা ডকুমেন্ট) হিসেবে, যা গড়া ফিল্ড (কলাম: order_id, status, total) দিয়ে।
একই ডেটাসেট, পাঁচ ফরম্যাট
শপ থেকে ছোট একটা অংশ নিন — প্রতি অর্ডারে একটা সারি — আর প্রতিটা সাধারণ ফরম্যাটে সংরক্ষণ করুন, তারপর প্রতিটা ফাইল আবার পড়ে দেখুন কী টিকে রইল:
import pandas as pd
from sqlhelp import con
orders = pd.read_sql("""
SELECT o.order_id, o.order_date, o.status, SUM(oi.quantity * oi.unit_price) AS total
FROM orders o JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id, o.order_date, o.status
ORDER BY o.order_id""", con, parse_dates=["order_date"])
orders.to_csv("orders.csv", index=False)
orders.to_json("orders.json", orient="records", date_format="iso")
orders.to_excel("orders.xlsx", index=False)
orders.to_xml("orders.xml", index=False, parser="etree")
orders.to_parquet("orders.parquet", index=False)
readers = {
"orders.csv": pd.read_csv,
"orders.json": pd.read_json,
"orders.xlsx": pd.read_excel,
"orders.xml": lambda path: pd.read_xml(path, parser="etree"),
"orders.parquet": pd.read_parquet,
}
for path, read in readers.items():
back = read(path)
print(f"{path:15} order_date: {str(back['order_date'].dtype):15} total: {back['total'].dtype}")orders.csv order_date: str total: int64
orders.json order_date: str total: int64
orders.xlsx order_date: datetime64[us] total: int64
orders.xml order_date: str total: int64
orders.parquet order_date: datetime64[us] total: int64ডেটা একই; কিন্তু প্রতিটা ফাইল তার সম্পর্কে কী মনে রাখে, তা এক নয়। CSV, JSON আর XML-এ তারিখের কোনো টাইপ নেই, তাই আবার পার্স না করা পর্যন্ত তারিখগুলো টেক্সট হিসেবে ফেরে (pandas 3 টেক্সট কলামের dtype দেখায় str)। সংখ্যাগুলো এখানে সব ফরম্যাটেই টিকে গেছে, তবে শুধু এজন্য যে pandas ঠিক আন্দাজ করেছে। Excel-এ আসল তারিখের সেল আছে। Parquet ডেটার সাথে স্কিমাও রাখে, তাই প্রতিটা টাইপ ঠিক যেমন লেখা হয়েছিল তেমনই ফেরে। তা ছাড়া Parquet প্রতিটা কলাম আলাদা করে, সংকুচিত অবস্থায় রাখে, তাই ফাইল ছোট হয় আর দরকারি কলামগুলোই শুধু পড়া যায়। কোন ফরম্যাটে কী সুবিধা, কী অসুবিধা:
| ফরম্যাট | কাঠামো | টাইপ থাকে? | কীসের জন্য ভালো | সাবধান |
|---|---|---|---|---|
| CSV | টেক্সটের সাধারণ সারি, কোনো স্তর নেই | না — সবই টেক্সট | যে কারও সাথে আদান-প্রদান, ছোট এক্সপোর্ট | মানের ভেতরে ডিলিমিটার আর উদ্ধৃতিচিহ্ন, এনকোডিং, শুরুর শূন্য, তারিখ |
| JSON | স্তরে স্তরে অবজেক্ট আর লিস্ট | সংখ্যা, স্ট্রিং, বুলিয়ান, null (তারিখ নয়) | API, কনফিগ, লগ (JSON Lines: প্রতি লাইনে একটা অবজেক্ট) | বদলাতে থাকা ফিল্ড, গভীর স্তর, আকার |
| Excel | সেলের শিট | আংশিক (সংখ্যা, তারিখ) | ব্যবসার লোকজন, হাতে বানানো রেফারেন্স ডেটা | মার্জ করা সেল, ফর্মুলা, নিজে থেকে বদলে যাওয়া মান (জিনের নাম তারিখ হয়ে যায়) |
| XML | অ্যাট্রিবিউটসহ স্তরে স্তরে এলিমেন্ট | না (স্কিমা প্রয়োগ না করলে) | পুরোনো এন্টারপ্রাইজ সিস্টেম, সরকারি আর ব্যাংকের ফিড | অনেক বাড়তি লেখা (verbose), namespace |
| Parquet | কলামভিত্তিক, সংকুচিত, স্কিমাসহ | হ্যাঁ | অ্যানালিটিক্স আর ML ডেটাসেট, ডেটা লেক | বাইনারি (খুলে পড়া যায় না); pandas-এ পড়তে-লিখতে pyarrow (বা fastparquet) লাগে |
মডেলের কাছে যাবে এমন যেকোনো কিছুর জন্য এমন ফরম্যাট বেছে নিন, যা নিজের স্কিমা বয়ে বেড়ায় (Parquet, বা একটা ডেটাবেস টেবিল)। প্রতিটা নিয়ে আরও গভীরে যাবে ইমপোর্ট ও এক্সপোর্ট: CSV, JSON, Excel আর Parquet।
টাইপ আর রূপান্তর
বাস্তবের কলাম প্রায়ই আসে টেক্সট হিসেবে, আর সেটাকে জেনে-বুঝে রূপান্তর করতে হয়। বাংলাদেশে মানুষের টাইপ করা দামে থাকতে পারে হাজারের কমা, টাকার চিহ্ন, বাংলা অঙ্ক, বা শব্দ:
raw = pd.Series(["1200", "1,500", "৳ 900", "২৫০০", "free", None])
BANGLA_DIGITS = str.maketrans("০১২৩৪৫৬৭৮৯", "0123456789")
cleaned = (raw.str.translate(BANGLA_DIGITS)
.str.replace(r"[৳,\s]", "", regex=True))
price = pd.to_numeric(cleaned, errors="coerce") # যা পার্স করা যায় না তা NaN হয়
print(pd.DataFrame({"raw": raw, "price": price})) raw price
0 1200 1200.0
1 1,500 1500.0
2 ৳ 900 900.0
3 ২৫০০ 2500.0
4 free NaN
5 NaN NaNerrors="coerce" পার্স করা যায় না এমন সবকিছুকে ক্র্যাশ না করে অনুপস্থিত মানে বদলে দেয় — কিন্তু তারপর সেগুলো গুনে দেখতে হবে আর ঠিক করতে হবে এর মানে কী। "free" হয়তো সত্যিই ০; অনুপস্থিত দাম মানে অজানা। এই সিদ্ধান্ত আপনার, লাইব্রেরির নয়।
আসল ডেটায় কী কী ভুল থাকে
নিচে নিউজলেটারে সাইন-আপের একটা ছোট এক্সপোর্ট, ইচ্ছে করে এলোমেলো রাখা; এতে সেই সমস্যাগুলোই আছে, যা আপনি সব জায়গায় দেখবেন:
signups = pd.DataFrame({
"email": ["nadia@example.com", "TANVIR@example.com ", "farhana@example", None,
"nadia@example.com", "rafiq@example.com", "mitu@example.com",
"imran@example.com", "sadia@example.com"],
"city": ["Dhaka", "chattogram", "Dhaka ", "Sylhet", "Dhaka", "Dacca", "DHAKA",
"Khulna", "dhaka"],
"age": [29, 34, 27, 41, 29, -3, 25, 38, 22],
"signed_up": ["2026-05-01", "2026-05-03", "2026-05-03", "2026-05-04",
"2026-05-01", "2026-05-06", "2026-13-01", "2026-05-08", "2026-05-09"],
})
print(signups) email city age signed_up
0 nadia@example.com Dhaka 29 2026-05-01
1 TANVIR@example.com chattogram 34 2026-05-03
2 farhana@example Dhaka 27 2026-05-03
3 NaN Sylhet 41 2026-05-04
4 nadia@example.com Dhaka 29 2026-05-01
5 rafiq@example.com Dacca -3 2026-05-06
6 mitu@example.com DHAKA 25 2026-13-01
7 imran@example.com Khulna 38 2026-05-08
8 sadia@example.com dhaka 22 2026-05-09| সমস্যা | এই টেবিলে |
|---|---|
| অনুপস্থিত মান | ৩ নম্বর সারিতে ইমেইল নেই |
| অবৈধ মান (নিয়ম ভাঙে) | farhana@example-এর ডোমেইনের শেষাংশ নেই; বয়স −৩; মাস ১৩ |
| অসামঞ্জস্যপূর্ণ মান (একই জিনিস ভিন্নভাবে লেখা) | Dhaka, Dhaka , DHAKA, dhaka আর পুরোনো নাম Dacca; ছোট হাতের chattogram; ইমেইলে স্পেস আর বড় হাতের অক্ষর |
| ডুপ্লিকেট | ০ আর ৪ নম্বর সারি একই মানুষ |
মান মাপা: ছয়টা মাত্রা
"ডেটাটা কি ভালো?" — কোন দিক থেকে ভালো তা না বলা পর্যন্ত এর উত্তর নেই। ছয়টা প্রচলিত মাত্রা প্রশ্নটাকে এমন সংখ্যায় বদলে দেয়, যা সময়ের সাথে নজরে রাখা যায়:
EMAIL_RULE = r"^[^@\s]+@[^@\s]+\.[a-z]{2,}$"
KNOWN_CITIES = {"Dhaka", "Chattogram", "Sylhet", "Khulna", "Rajshahi"}
AS_OF = pd.Timestamp("2026-05-10") # রিপোর্টের তারিখ, স্থির রাখা যাতে ফলাফল বারবার একই আসে
dates = pd.to_datetime(signups["signed_up"], format="%Y-%m-%d", errors="coerce")
email_key = signups["email"].str.strip().str.lower()
report = {
"completeness: email present": signups["email"].notna().mean(),
"validity: email well-formed": email_key.str.match(EMAIL_RULE).fillna(False).astype(bool).mean(),
"validity: age 0-120": signups["age"].between(0, 120).mean(),
"validity: real date": dates.notna().mean(),
"consistency: city in reference list": signups["city"].isin(KNOWN_CITIES).mean(),
"uniqueness: distinct emails": email_key.dropna().nunique() / email_key.notna().sum(),
"timeliness: signed up in last 7 days": (dates >= AS_OF - pd.Timedelta(days=7)).mean(),
}
for check, share in report.items():
print(f"{check:38} {share:6.1%}")completeness: email present 88.9%
validity: email well-formed 77.8%
validity: age 0-120 88.9%
validity: real date 88.9%
consistency: city in reference list 44.4%
uniqueness: distinct emails 87.5%
timeliness: signed up in last 7 days 66.7%| মাত্রা | প্রশ্ন | সাধারণ যাচাই |
|---|---|---|
| সম্পূর্ণতা (completeness) | যা থাকার কথা, সব কি আছে? | অনুপস্থিত নয় এমন মানের অংশ; প্রত্যাশিত সারির সংখ্যা |
| বৈধতা (validity) | প্রতিটা মান কি তার নিয়ম মানে? | ফরম্যাট, সীমা, অনুমোদিত মান, আসল তারিখ |
| সামঞ্জস্য (consistency) | একই জিনিস কি একইভাবে লেখা, আর উৎসগুলো কি একমত? | রেফারেন্স তালিকায় থাকা মান; সিস্টেমগুলোর মধ্যে মোট মেলে কিনা |
| অনন্যতা (uniqueness) | প্রতিটা আসল জিনিস কি একবারই রেকর্ড হয়েছে? | নর্মালাইজ করার পর ডুপ্লিকেট key |
| সময়োপযোগিতা (timeliness) | ব্যবহারের জন্য ডেটা কি যথেষ্ট টাটকা? | সবচেয়ে নতুন রেকর্ডের বয়স; ঘটনার পর কতটা দেরি |
| নির্ভুলতা (accuracy) | এটা কি বাস্তবের সাথে মেলে? | একটা নমুনা বিশ্বস্ত উৎসের সাথে মেলানো (যাচাই করা ঠিকানা, হাতে পরীক্ষা) |
নির্ভুলতা হলো সেই মাত্রা, যা শুধু টেবিল থেকে হিসাব করা যায় না: rafiq@example.com ঠিকঠাক গড়নের, কিন্তু সেটা সঠিক কিনা তা বলবে কেবল ইমেইল পাঠিয়ে দেখা — ফেরত আসে কিনা।
ডেটা পরিষ্কারের ওয়ার্কফ্লো
ডেটা পরিষ্কার করা মানে বিচ্ছিন্ন কিছু এককালীন সংশোধন নয়। এটা ধাপে ধাপে চলা একটা নির্দিষ্ট ক্রম, কোড হিসেবে লেখা, যাতে পরের মাসেও হুবহু একইভাবে চালানো যায়:
- প্রোফাইল করুন: আকার, টাইপ, অনুপস্থিতের সংখ্যা আর মানের তালিকা দেখুন (ওপরের মতো)।
- স্ট্যান্ডার্ডাইজ করুন: স্পেস ছাঁটুন, অক্ষরের ছোট-বড় ঠিক করুন, ভিন্ন বানানগুলোকে একটায় আনুন, টাইপ রূপান্তর করুন।
- যাচাই করুন: নিয়মগুলো প্রয়োগ করুন; যে সারি নিয়ম ভাঙে, তাকে চুপচাপ ফেলে না দিয়ে কারণসহ একটা কোয়ারেন্টিন টেবিলে সরান।
- নর্মালাইজ করা একটা key ধরে ডুপ্লিকেট সরান।
- আবার যাচাই করুন আর সংখ্যাগুলো লিখে রাখুন: কয়টা সারি ঢুকল, কয়টা বেরোল, কয়টা কোয়ারেন্টিনে।
CITY_MAP = {"dhaka": "Dhaka", "dacca": "Dhaka", "chattogram": "Chattogram",
"chittagong": "Chattogram", "sylhet": "Sylhet", "khulna": "Khulna"}
def clean(df):
out = df.copy()
out["email"] = out["email"].str.strip().str.lower()
out["city"] = out["city"].str.strip().str.lower().map(CITY_MAP)
out["signed_up"] = pd.to_datetime(out["signed_up"], format="%Y-%m-%d", errors="coerce")
problems = pd.Series("", index=out.index)
problems[out["email"].isna()] += "no email; "
problems[out["email"].notna() & ~out["email"].str.match(EMAIL_RULE).fillna(False).astype(bool)] += "bad email; "
problems[~out["age"].between(0, 120)] += "bad age; "
problems[out["signed_up"].isna()] += "bad date; "
quarantine = out[problems != ""].assign(reason=problems[problems != ""].str.rstrip("; "))
good = out[problems == ""].drop_duplicates(subset="email", keep="first")
return good, quarantine
good, quarantine = clean(signups)
print(good)
print(quarantine[["email", "reason"]])
print(f"rows in: {len(signups)}, clean: {len(good)}, quarantined: {len(quarantine)}, "
f"duplicates removed: {len(signups) - len(good) - len(quarantine)}") email city age signed_up
0 nadia@example.com Dhaka 29 2026-05-01
1 tanvir@example.com Chattogram 34 2026-05-03
7 imran@example.com Khulna 38 2026-05-08
8 sadia@example.com Dhaka 22 2026-05-09
email reason
2 farhana@example bad email
3 NaN no email
5 rafiq@example.com bad age
6 mitu@example.com bad date
rows in: 9, clean: 4, quarantined: 4, duplicates removed: 1খেয়াল করুন, কোনো নিয়ম যাচাইয়ের আগেই স্ট্যান্ডার্ডাইজেশন কী করেছে: ঢাকার পাঁচ রকম বানান এক হয়ে গেছে, আর বড় হাতের অক্ষর ও শেষে স্পেসওয়ালা ইমেইলটা হয়ে গেছে একটা স্বাভাবিক key, যা দিয়ে ডুপ্লিকেট মেলানো যায়। প্রতিটা সারির হিসাব আছে: ৪টা পরিষ্কার + ৪টা কোয়ারেন্টিনে + ১টা ডুপ্লিকেট = ৯।
"Normalisation" শব্দের দুটো আলাদা মানে। ডেটাবেসে এটা হলো পুনরাবৃত্তি দূর করতে টেবিল ভাগ করার ডিজাইন প্রক্রিয়া (দেখুন ডেটাবেস ডিজাইন: কি, ER ডায়াগ্রাম আর নরমালাইজেশন)। ডেটা প্রসেসিং আর ML-এ এর মানে সংখ্যাগুলোকে একটা সাধারণ পরিসরে আনা, যেমন ০–১-এ min-max স্কেলিং বা z-score:
(x - x.min()) / (x.max() - x.min())। কোনটা বোঝাচ্ছেন, তা বলে দিন।
রূপান্তর, ফিল্টার, অ্যাগ্রিগেট আর মার্জ
পরিষ্কার হওয়ার পর ডেটাকে রূপান্তর করা হয় প্রশ্নের দরকার মতো আকারে: পুরোনো ফিল্ড থেকে নতুন ফিল্ড, সারি ফিল্টার করা, গ্রুপ ধরে অ্যাগ্রিগেট, আর অন্য উৎস মার্জ করা। মার্জেই নীরব ভুলের জন্ম হয়, তাই সারির সংখ্যা যাচাই করুন:
divisions = pd.DataFrame({"city": ["Dhaka", "Chattogram", "Sylhet", "Khulna"],
"division": ["Dhaka", "Chattogram", "Sylhet", "Khulna"],
"metro": [True, True, False, False]})
enriched = good.merge(divisions, on="city", how="left", validate="many_to_one")
assert len(enriched) == len(good), "the merge must not add or lose rows"
enriched["age_band"] = pd.cut(enriched["age"], bins=[0, 24, 34, 120], labels=["<25", "25-34", "35+"])
print(enriched[["email", "division", "metro", "age_band"]])
summary = (enriched[enriched["metro"]]
.groupby("city", as_index=False)
.agg(signups=("email", "count"), mean_age=("age", "mean")))
print(summary) email division metro age_band
0 nadia@example.com Dhaka True 25-34
1 tanvir@example.com Chattogram True 25-34
2 imran@example.com Khulna False 35+
3 sadia@example.com Dhaka True <25
city signups mean_age
0 Chattogram 1 34.0
1 Dhaka 2 25.5validate="many_to_one" থাকলে লুকআপ টেবিলে কোনো শহর দুবার থাকলে pandas এরর তোলে — অনেক টেবিল জয়েন, আর ডুপ্লিকেট সারির ফাঁদ পাতার fan-out ফাঁদের pandas সংস্করণ — আর assert ধরে ফেলে বাড়তি বা হারানো সারি।
Wide বনাম long
একই সংখ্যা দুভাবে সাজানো যায়। Wide: প্রতিটা জিনিসের জন্য একটা সারি, প্রতিটা মাপের জন্য একটা কলাম (পড়তে সুবিধা, আর বেশিরভাগ ML লাইব্রেরি ফিচারের জন্য এই আকারই চায়)। Long: প্রতিটা জিনিসের প্রতিটা মাপের জন্য একটা সারি (সংরক্ষণ, গ্রুপ করা আর চার্টের জন্য ভালো, আর SQL স্বাভাবিকভাবে এই আকারই দেয়):
long = pd.read_sql("""
SELECT c.name AS category, substr(o.order_date, 1, 7) AS month,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
JOIN categories c ON c.category_id = p.category_id
WHERE o.status <> 'cancelled' AND o.order_date < '2026-04-01'
GROUP BY category, month
ORDER BY category, month""", con)
print(long.head(4))
wide = long.pivot(index="category", columns="month", values="revenue").fillna(0).astype(int)
print(wide)
back_to_long = wide.reset_index().melt(id_vars="category", var_name="month", value_name="revenue")
print(len(long), "long rows,", wide.shape, "wide,", len(back_to_long), "rows after melt") category month revenue
0 Accessories 2026-02 1500
1 Accessories 2026-03 2200
2 Books 2026-01 1200
3 Books 2026-02 2500
month 2026-01 2026-02 2026-03
category
Accessories 0 1500 2200
Books 1200 2500 2400
Courses 0 3000 15000
Electronics 5400 0 1800
9 long rows, (4, 3) wide, 12 rows after meltpivot long থেকে wide করে; melt wide থেকে long। melt-এর পর সারি মূলের চেয়ে বেশি, কারণ wide টেবিলে না থাকা সমন্বয়গুলো ০ দিয়ে ভরা হয়েছিল — এটা মনে করিয়ে দেয় যে আকার বদলাতে গিয়ে চুপচাপ নতুন মান তৈরি হয়ে যেতে পারে।
Lineage, provenance আর পুনরুৎপাদনযোগ্যতা
ছয় মাস পর কেউ জিজ্ঞেস করবেন: "এই মডেল কোন ডেটায় ট্রেন হয়েছিল, আর সেই ডেটা এল কোথা থেকে?" Provenance হলো ডেটার উৎস (কোন সিস্টেম, কোন এক্সপোর্ট, কবে, কোন লাইসেন্স বা সম্মতির অধীনে)। Lineage হলো সেখান থেকে ফলাফল পর্যন্ত পথ (কোন ধাপ, কোন ক্রমে, কোন কোড দিয়ে)। কাজ চলতে চলতেই দুটোই লিখে রাখুন, আউটপুটের পাশে সংরক্ষিত ছোট একটা ম্যানিফেস্টে:
import hashlib
import json
def sha256_of(path):
return hashlib.sha256(open(path, "rb").read()).hexdigest()[:12]
signups.to_csv("signups_raw.csv", index=False)
def run_pipeline():
raw = pd.read_csv("signups_raw.csv")
good, quarantine = clean(raw)
good.sort_values("email").to_csv("signups_clean.csv", index=False)
return {
"source": "newsletter export (signups_raw.csv)",
"input_sha256": sha256_of("signups_raw.csv"),
"steps": ["strip/lower email", "map city", "parse date", "validate", "dedupe on email"],
"rows": {"in": len(raw), "clean": len(good), "quarantined": len(quarantine)},
"output_sha256": sha256_of("signups_clean.csv"),
"as_of": str(AS_OF.date()),
"pandas": pd.__version__.split(".")[0] + ".x",
}
first, second = run_pipeline(), run_pipeline()
print(json.dumps(first, indent=2))
print("same output on a second run:", first["output_sha256"] == second["output_sha256"]){
"source": "newsletter export (signups_raw.csv)",
"input_sha256": "066c33963c6c",
"steps": [
"strip/lower email",
"map city",
"parse date",
"validate",
"dedupe on email"
],
"rows": {
"in": 9,
"clean": 4,
"quarantined": 4
},
"output_sha256": "caa2d8ed5ba9",
"as_of": "2026-05-10",
"pandas": "3.x"
}
same output on a second run: Trueপাইপলাইন দুবার চালিয়ে বাইট-বাই-বাইট একই আউটপুট পাওয়া গেছে, আর ম্যানিফেস্ট তার প্রমাণ। এটাই পুনরুৎপাদনযোগ্য প্রসেসিং, আর এটা কয়েকটা অভ্যাসের ওপর নির্ভর করে:
- কাঁচা ইনপুট অপরিবর্তিত রাখুন; প্রতিটা ফলাফল কাঁচা ডেটা আর কোডের একটা ফাংশন।
- সংরক্ষণের আগে সাজান (
ORDER BYছাড়া ডেটাবেস সারি কোনো নির্দিষ্ট ক্রমে দেয় না, Spark-এর মতো সমান্তরাল টুলও দেয় না), র্যান্ডম seed স্থির করুন, আর ঘড়ি না পড়ে "আজ"-কে প্যারামিটার হিসেবে দিন। - কোডের ভার্সন রাখুন (Git) আর লাইব্রেরির ভার্সন বেঁধে দিন; দুটোই ম্যানিফেস্টে লিখুন।
- ইনপুট আর আউটপুট হ্যাশ করুন, যাতে "ডেটা কি বদলেছে?" প্রশ্নের উত্তর হয় হ্যাঁ বা না।
বড় টিম এর জন্য টুল ব্যবহার করে — lineage-এর জন্য dbt আর ডেটা ক্যাটালগ, ডেটাসেটের জন্য DVC বা lakehouse টেবিলের ভার্সন, মডেলকে তার ট্রেনিং ডেটার সাথে যুক্ত করতে MLflow — কিন্তু সবগুলোই এই ম্যানিফেস্টের মতো একই তথ্য লিখে রাখে।
সাধারণ ভুল
- খারাপ সারি চুপচাপ ফেলে দেওয়া।
df.dropna()কোনো সতর্কবার্তা ছাড়াই আপনার অর্ধেক ডেটা মুছে দিতে পারে, আর যে সারিগুলো মোছে, সেগুলো প্রায় কখনোই এলোমেলোভাবে বাছাই করা নয় — বাদ পড়ে নির্দিষ্ট ধরনের মানুষ বা ঘটনা। কারণসহ কোয়ারেন্টিনে রাখুন আর সংখ্যা জানান। - রিডারের আন্দাজ করা টাইপে ভরসা।
00123-এর মতো আইডির CSV কলাম হয়ে যায় ১২৩ সংখ্যা; মিশ্র কলাম হয়ে যায় লেখা।dtype=আর তারিখের ফরম্যাট স্পষ্ট করে দিন:pd.read_csv("x.csv", dtype={"phone": "string"})। - স্প্রেডশিটে হাতে হাতে ডেটা পরিষ্কার করা। এটা আবার করা বা রিভিউ করা যায় না। প্রতিটা ধাপ কোডে রাখুন, ছোটগুলোও।
- সংখ্যা না মিলিয়ে মার্জ করা। লুকআপ টেবিলে একটা ডুপ্লিকেট key সারি গুণ করে বাড়িয়ে দেয়। pandas-এ
validate=দিন, আর প্রতিটা জয়েনের আগে-পরে সারির সংখ্যা মেলান। - কাঁচা ডেটার ওপর লিখে ফেলা। মূল ডেটা হারিয়ে গেলে পরিষ্কারের কোডের বাগ আর শোধরানো যায় না। পরিষ্কার ডেটা নতুন ফাইল বা টেবিলে লিখুন।
নিজে চেষ্টা করুন
- সহজ: এই কলামটা সংখ্যায় রূপান্তর করুন আর জানান কয়টা মান পার্স করা যায়নি:
["12", "৪৫", "3.5", "N/A", "1,200"]। - মাঝারি:
pd.read_sqlদিয়ে পড়া শপেরcustomersটেবিলের জন্য সম্পূর্ণতা আর বৈধতার রিপোর্ট বানান (ইমেইল আছে আর ঠিক গড়নের, শহর আছে)। - কঠিন:
meltদিয়েwideটেবিলটাকে long করুন, pivot যে শূন্যের সারিগুলো বানিয়েছিল তা বাদ দিন, আর দেখান যে সাজানোর পর ফলাফলটা মূলlongটেবিলের সমান।
উত্তর
# ১. সহজ
values = pd.Series(["12", "৪৫", "3.5", "N/A", "1,200"])
numbers = pd.to_numeric(values.str.translate(BANGLA_DIGITS).str.replace(",", ""), errors="coerce")
print(numbers.tolist(), "unparsed:", numbers.isna().sum())# ২. মাঝারি
customers = pd.read_sql("SELECT customer_id, email, city FROM customers", con)
print(f"email present: {customers['email'].notna().mean():.0%}")
print(f"email well-formed: {customers['email'].str.match(EMAIL_RULE).mean():.0%}")
print(f"city present: {customers['city'].notna().mean():.0%}")# ৩. কঠিন
melted = wide.reset_index().melt(id_vars="category", var_name="month", value_name="revenue")
melted = melted[melted["revenue"] != 0].sort_values(["category", "month"]).reset_index(drop=True)
print(melted.equals(long.astype({"revenue": int})))সারসংক্ষেপ
- ডেটা হয় স্ট্রাকচার্ড, সেমি-স্ট্রাকচার্ড বা আনস্ট্রাকচার্ড, আর আসে ডেটাবেস, API, ফাইল, সেন্সর আর লগ থেকে — প্রত্যেকটার নিজস্ব চেনা সমস্যা নিয়ে।
- ফরম্যাটভেদে যা থাকে তা আলাদা: CSV আর XML টাইপ হারায়, JSON স্তর রাখে, Excel মানুষের জন্য সুবিধাজনক, Parquet স্কিমা রাখে আর ML-এর জন্য উপযুক্ত।
- অনুপস্থিত, অবৈধ, অসামঞ্জস্যপূর্ণ আর ডুপ্লিকেট মান আশা করুন; জেনে-বুঝে টাইপ রূপান্তর করুন আর কী ব্যর্থ হলো তা গুনুন।
- বারবার একইভাবে চালানো যায় এমন ওয়ার্কফ্লো দিয়ে ডেটা পরিষ্কার করুন: প্রোফাইল, স্ট্যান্ডার্ডাইজ, যাচাই (ফেলে দেবেন না, কোয়ারেন্টিন করুন), ডুপ্লিকেট সরানো, সংখ্যা মেলানো। মাপুন সম্পূর্ণতা, বৈধতা, সামঞ্জস্য, অনন্যতা, সময়োপযোগিতা আর নির্ভুলতা।
- Provenance আর lineage একটা ম্যানিফেস্টে লিখুন, কাঁচা ডেটা রেখে দিন, আর প্রতিটা রান পুনরুৎপাদনযোগ্য করুন: সাজানো আউটপুট, স্থির seed আর তারিখ, হ্যাশ।
এরপর: Python ও ডেটাবেস: কানেকশন, প্যারামিটার আর ট্রানজ্যাকশন পাতায় Python-কে ঠিকঠাক যুক্ত করবেন SQLite, MySQL আর PostgreSQL-এর সাথে — এই পাতার প্রতিটা পাইপলাইনের উৎস পড়তে আর ফলাফল নিরাপদে লিখতে যে প্লাম্বিং লাগে।