এই পর্বে যা শিখবেন
- NumPy থাকতে pandas কেন লাগে, আর Series ও DataFrame-এর ভেতরের গঠন
- index ধরে alignment কীভাবে কাজ করে, কোথায়
NaNজন্মায়, আরfill_valueদিয়ে কীভাবে সামলাবেন - dict, dict-এর list আর NumPy array থেকে DataFrame বানানো, আর
head,info,describe,value_counts,nuniqueদিয়ে নতুন ডেটা চেনা - কলাম বাছাই, হিসাব করা কলাম যোগ, নাম বদল, বাদ দেওয়া;
set_indexআরreset_index - pandas 3-এর dtype (
int64,float64,bool,str,category,datetime64,Int64),assignআর method chaining, আর নতুনদের চেনা ভুল
মডিউল ৩-এ (পর্ব ১৩ থেকে পর্ব ১৯) আপনি NumPy array নিয়ে কাজ করেছেন: একই ধরনের সংখ্যার টানা সারি, যার একটি shape আর একটি dtype আছে। এই মডিউলের বিষয় টেবিলের ডেটা, আর তার প্রধান হাতিয়ার pandas. পর্ব ২০ থেকে ২৭ পর্যন্ত আমরা বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) অর্ডারের টেবিল নিয়ে কাজ করব। এই পর্বে শুধু ভিত: pandas-এর দুটি মূল কাঠামো, তাদের index, আর নতুন একটি টেবিল হাতে পেলে প্রথমে কী কী দেখবেন। এই পর্বের সব কোড পাতাতেই চলে, Pyodide দিয়ে, যেখানে pandas 3.0.2 আছে; প্রথমবার চালালে pandas নামতে কয়েক সেকেন্ড লাগতে পারে।
১ · কেন pandas — NumPy-র ওপর নাম আর মিশ্র কলাম
পাড়ার মুদি দোকানের হিসাব খাতার কথা ভাবুন। প্রতিটি লাইনে একটি বিক্রি: তারিখ, ক্রেতার নাম, পণ্য, কয়টি, দাম, নগদ না বাকি। খাতার ওপরে প্রতিটি কলামের নাম লেখা, আর প্রতিটি লাইনের একটি ক্রমিক নম্বর বা রসিদ নম্বর আছে। দোকানি “রসিদ ১০০৫-এর দাম কত” জিজ্ঞাসা করেন, “৫ নম্বর লাইনের ৪ নম্বর ঘর” নয়।
NumPy array এই খাতার জন্য পুরোপুরি মানানসই নয়, দুটি কারণে। প্রথমত, NumPy-র ঘরগুলোর নাম নেই, শুধু অবস্থান আছে: sales[2, 4]. পর্ব ১৪-এ (indexing আর slicing) আপনি জেলা আর দিনের নাম আলাদা array-তে রেখে মেলাতেন; কোন সারি কোন জেলা, সেটি মনে রাখার দায়িত্ব ছিল আপনার। দ্বিতীয়ত, একটি array-তে সব ঘরের dtype এক। “ঢাকা”, 2, 25000 আর True এক array-তে রাখলে NumPy সবকিছুকে লেখায় বদলে দেয়, আর তখন যোগ-গড় আর চলে না।
pandas এই দুটি অভাব পূরণ করে। এটি NumPy-র ওপরে (আর এখন অনেক ক্ষেত্রে Apache Arrow-এর ওপরে) একটি স্তর, যা তিনটি জিনিস যোগ করে:
- কলামের নাম:
orders["qty"], “৩ নম্বর কলাম” নয়। - সারির label, মানে index: প্রতিটি সারির একটি নাম বা নম্বর, যা সারি সরালে, ছাঁকলে বা সাজালেও সারির সঙ্গেই থাকে।
- প্রতিটি কলামের নিজের dtype: জেলার কলাম লেখা, qty পূর্ণসংখ্যা, prepaid সত্য-মিথ্যা। ভেতরে প্রতিটি কলাম একটি NumPy-ধরনের array, তাই কলামের ভেতরের হিসাব NumPy-র মতোই দ্রুত, vectorized.
একটু ইতিহাস। pandas-এর অফিসিয়াল “About” পাতা অনুযায়ী এর কাজ শুরু হয় ২০০৮ সালে AQR Capital Management-এ, আর ২০০৯-এর শেষে এটি open source হয়। কাজটি শুরু করেছিলেন Wes McKinney, যিনি পরে ২০১২ সালে “Python for Data Analysis” বইটি লেখেন। প্রচলিত বিবরণে, আর্থিক ডেটা বিশ্লেষণে R-এর data.frame-এর মতো সুবিধা Python-এ না পাওয়াই ছিল মূল প্রেরণা; নামটি “panel data” (অর্থনীতির একটি পরিভাষা) থেকে এসেছে বলে সাধারণভাবে বলা হয়। ২০১৫ সাল থেকে pandas NumFOCUS-এর সমর্থিত প্রকল্প। pandas 3.0 প্রকাশিত হয় ২১ জানুয়ারি ২০২৬; লেখার সময়ে সর্বশেষ স্থিতিশীল সংস্করণ 3.1.1, আর পাতার ভেতরের Python-এ 3.0.2.
নিচের ছবিতে একটি DataFrame-এর অঙ্গপ্রত্যঙ্গ। একটি জিনিস খেয়াল করুন: বাঁ দিকের index কোনো কলাম নয়। এটি সব কলামের সঙ্গে একসঙ্গে বাঁধা সারির label, আর প্রতিটি কলাম আসলে একটি Series, যার index ওই একই।
একটি DataFrame-এর গঠন — index, কলাম আর Series
Anatomy of a DataFrame: one shared index, column labels, and each column as a Series with its own dtype
qty কলাম একটি Series, হলুদ সারিটি একটি অর্ডার। লাল লেখা প্রতিটি কলামের dtype: একই টেবিলে লেখা আর পূর্ণসংখ্যা পাশাপাশি, যা একটি NumPy array পারে না।DataFrame হলো একই index ভাগ করা কয়েকটি Series-এর একটি dict. কলামের নাম হলো dict-এর key, প্রতিটি কলাম একটি Series, আর সারির label সবার জন্য একটিই। এই একটি বাক্য মনে রাখলে pandas-এর বেশিরভাগ আচরণ, বিশেষ করে alignment, নিজে থেকেই বোঝা যায়।
২ · Series — মান আর index-এর জোড়া
একটি Series হলো দুটি সমান দৈর্ঘ্যের জিনিসের জোড়া: মানের একটি array, আর প্রতিটি মানের একটি label, যার নাম index. ক্রিকেটের স্কোরকার্ডের কথা ভাবুন: বাঁ দিকে ব্যাটারের নাম, ডানে রান। রান-এর কলামটি একটি NumPy array হতে পারত, কিন্তু নামগুলো পাশে থাকায় “সাকিবের রান কত” সরাসরি জিজ্ঞাসা করা যায়।
import pandas as pd
oct_ = pd.Series([120, 80, 45], index=["ঢাকা", "চট্টগ্রাম", "সিলেট"])
print(oct_)
ঢাকা 120
চট্টগ্রাম 80
সিলেট 45
dtype: int64
oct_["ঢাকা"] # 120 label দিয়ে
oct_.iloc[0] # 120 অবস্থান দিয়ে (পর্ব ২২-এ বিস্তারিত)
oct_.index # Index(['ঢাকা', 'চট্টগ্রাম', 'সিলেট'], dtype='str')
oct_.to_numpy() # array([120, 80, 45]) ভেতরের মান, NumPy array হিসেবে
(ডেস্কটপে pandas 3.0.2-এ সত্যিকারের run.) oct নামটি Python-এ নিষিদ্ধ নয়, কিন্তু একটি built-in function-এর নাম, তাই শেষে _ দেওয়া হয়েছে। Series-এর তিনটি অংশ আলাদা করে দেখা যায়:
- মান:
.to_numpy()একটি NumPy array দেয়।.values-ও প্রায় একই কাজ করে, কিন্তু কিছু dtype-এ (যেমন category বা নতুন str) এটি NumPy array না দিয়ে pandas-এর নিজের array ফেরত দিতে পারে, তাই pandas-এর docs এখন.to_numpy()ব্যবহার করতে বলে। - index:
.index, এখানে লেখার label, dtypestr. - dtype:
.dtype, এখানেint64. Series-এর সব মানের dtype এক, ঠিক NumPy-র মতো। একটি.name-ও থাকতে পারে, যা DataFrame-এর কলাম হলে কলামের নাম হয়।
Alignment — যোগ হয় label ধরে, অবস্থান ধরে নয়
এখানেই pandas NumPy থেকে সবচেয়ে বেশি আলাদা। দুটি NumPy array যোগ করলে প্রথমটির প্রথম ঘর দ্বিতীয়টির প্রথম ঘরের সঙ্গে যোগ হয়, অবস্থান ধরে। দুটি Series যোগ করলে pandas আগে label মেলায়: ঢাকার সঙ্গে ঢাকা, সিলেটের সঙ্গে সিলেট, যে ক্রমেই থাকুক না কেন। এর নাম index alignment. দুই মাসের হিসাব খাতা পাশাপাশি রেখে নাম ধরে মেলানোর মতো: এক খাতায় যে দোকানের নাম নেই, তার যোগফল লেখা যায় না, তাই সেই ঘর ফাঁকা থাকে। pandas সেই ফাঁকা ঘরে বসায় NaN, মানে “মান নেই”। নিচের ধাপে দেখুন।
1oct_ = ঢাকা 120, চট্টগ্রাম 80, সিলেট 452nov = চট্টগ্রাম 95, ঢাকা 130, খুলনা 603total = oct_ + nov4oct_.add(nov, fill_value=0)5oct_.to_numpy() + nov.to_numpy()
| label | |
| dtype |
- দুটি Series. অক্টোবরে তিন জেলা, নভেম্বরে তিন জেলা, কিন্তু জেলাগুলো পুরোপুরি এক নয় (সিলেট শুধু অক্টোবরে, খুলনা শুধু নভেম্বরে), আর ক্রমও আলাদা।
- label-এর মিলন। pandas দুই index-এর সব label নিয়ে একটি নতুন index বানায় (union), আর সম্ভব হলে সাজায়: খুলনা, চট্টগ্রাম, ঢাকা, সিলেট. বাংলা লেখা Unicode-এর ক্রমে সাজে, অভিধানের ক্রমে নয়।
- যেখানে দুদিকেই আছে। ঢাকা: 120 + 130 = 250; চট্টগ্রাম: 80 + 95 = 175. nov-এ ঢাকা দ্বিতীয় অবস্থানে ছিল, তবু ঠিক ঢাকার সঙ্গেই যোগ হয়েছে।
- যেখানে একদিকে নেই। খুলনার অক্টোবরের মান নেই, সিলেটের নভেম্বরের মান নেই; “কিছু না + 60” pandas-এর কাছে অজানা, তাই
NaN. NaN একটি float, তাই পুরো ফলের dtype int64 থেকে float64 হয়ে গেছে। fill_value=0.addmethod-এ বলে দেওয়া যায়, একদিকে label না থাকলে সেদিকের মান 0 ধরা হবে। এখন খুলনা 60, সিলেট 45. dtype তবু float64 থাকে, কারণ মেলানোর ধাপেই NaN জন্মেছিল; পূর্ণসংখ্যা চাইলে পরেastype("int64").- অবস্থান ধরে যোগের ভুল। NumPy array-তে নামিয়ে যোগ করলে ঢাকার 120-এর সঙ্গে চট্টগ্রামের 95 জোড়া লাগে: 215, 210, 105, তিনটিই অর্থহীন। কোনো error নেই, এটিই সবচেয়ে বিপজ্জনক।
import pandas as pd
# অক্টোবর আর নভেম্বরে জেলাভিত্তিক অর্ডারের সংখ্যা
oct_ = pd.Series([120, 80, 45], index=["ঢাকা", "চট্টগ্রাম", "সিলেট"])
nov = pd.Series([95, 130, 60], index=["চট্টগ্রাম", "ঢাকা", "খুলনা"])
print(oct_.dtype, oct_.index.dtype, oct_["ঢাকা"])
print(oct_.to_numpy(), type(oct_.to_numpy()).__name__)
total = oct_ + nov # label ধরে মেলায়, ক্রম নয়
print(total)
print("NaN কয়টি:", total.isna().sum())
total0 = oct_.add(nov, fill_value=0) # না থাকলে 0 ধরে
print(total0.dtype, "->", total0.astype("int64").tolist())
wrong = oct_.to_numpy() + nov.to_numpy() # শুধু অবস্থান ধরে যোগ
print("ভুল যোগ:", wrong)আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):
int64 str 120
[120 80 45] ndarray
খুলনা NaN
চট্টগ্রাম 175.0
ঢাকা 250.0
সিলেট NaN
dtype: float64
NaN কয়টি: 2
float64 -> [60, 175, 250, 45]
ভুল যোগ: [215 210 105]
প্রথম লাইনে তিনটি তথ্য: মানের dtype int64, index-এর dtype str (pandas 3-এর নতুন লেখার dtype, অংশ ৭), আর label দিয়ে তোলা ঢাকার 120. দ্বিতীয় লাইনে to_numpy() একটি সাধারণ ndarray দিয়েছে, label ছাড়া। তারপর আসল শিক্ষা: যোগফলে চারটি label, তার দুটিতে NaN, আর dtype float64. isna().sum() NaN গোনে: 2. fill_value=0 দিলে NaN নেই, কিন্তু dtype এখনো float64, তাই শেষে astype("int64") দিয়ে পূর্ণসংখ্যা করা হয়েছে। শেষ লাইনটি সতর্কবার্তা: label ফেলে দিয়ে অবস্থান ধরে যোগ করলে চুপচাপ ভুল সংখ্যা আসে। বাস্তবে এই ভুল ঘটে যখন কেউ একটি টেবিল সাজিয়ে (sort) অন্য একটির পাশে NumPy দিয়ে বসান।
NaN সবসময় খারাপ নয়। খুলনার অক্টোবরের মান সত্যিই অজানা হতে পারে (সেই মাসে খুলনায় ডেলিভারি চালুই হয়নি), আর তখন 0 বসানো মিথ্যা তথ্য। “০টি অর্ডার” আর “তথ্য নেই” আলাদা কথা। fill_value দেওয়ার আগে ভাবুন কোনটি সত্য। Missing value কীভাবে খুঁজবেন, গুনবেন আর সামলাবেন, তা পর্ব ২৩ · ডেটা পরিষ্কার — missing value, duplicate, type আর বাংলা লেখার বিষয়।
৩ · DataFrame বানানো — dict, dict-এর list আর NumPy array
বাস্তবে DataFrame সাধারণত ফাইল থেকে পড়া হয়, যা পর্ব ২১ · ডেটা পড়া আর লেখা — CSV, Excel, JSON, SQL আর Parquet-এর বিষয়। কিন্তু হাতে বানানোর তিনটি পথ জানা দরকার, কারণ ছোট পরীক্ষা, test আর API-র ফল এভাবেই আসে।
পথ ১ — list-এর dict: প্রতিটি key একটি কলাম
সবচেয়ে চেনা পথ। প্রতিটি key কলামের নাম, প্রতিটি value সেই কলামের সব মানের একটি list. সব list-এর দৈর্ঘ্য সমান হতেই হবে। নিচের ধাপে একটি ছোট dict কীভাবে টেবিল হয়, দেখুন।
1data = {"district": ["ঢাকা", "সিলেট", "খুলনা", "ঢাকা"],2 "qty": [2, 5, 1, 4],3 "payment": ["COD", "card", "bKash", "COD"]}4df = pd.DataFrame(data)5print(df, df.shape)
| columns | |
| dtypes | |
| index |
- একটি সাধারণ dict. তিনটি key, প্রতিটির value একটি list, প্রতিটিতে ৪টি মান। এটি পর্ব ৯-এর (ডেটার জন্য Python) চেনা Python dict, এখনো pandas-এর কিছু নয়।
- key হয় কলামের নাম।
pd.DataFrame(data)dict-এর key-গুলো dict-এর ক্রমেই কলামের নাম হিসেবে নেয়।df.columnsনিজেও একটি Index, dtypestr. - প্রতিটি list হয় একটি Series. pandas প্রতিটি list দেখে dtype ঠিক করে: লেখা দেখে
str, পূর্ণসংখ্যা দেখেint64. এখানে দৈর্ঘ্যও যাচাই হয়; একটি list-এ ৩টি মান থাকলেValueError: All arrays must be of the same length. - index যোগ হয়। index না দিলে pandas 0 থেকে শুরু করে একটি RangeIndex দেয়: 0, 1, 2, 3. এটি memory-তে সংখ্যাগুলো রাখে না, শুধু start, stop আর step মনে রাখে, ঠিক Python-এর
range-এর মতো। - ফল। ৪টি সারি, ৩টি কলাম, shape
(4, 3). প্রতিটি সারি একটি অর্ডার, প্রতিটি কলাম একটি Series যার index 0–3. বাংলা অক্ষরের প্রস্থ ঠিকমতো না মাপায় টার্মিনালে কলাম একটু বাঁকা দেখাতে পারে; ডেটা ঠিকই আছে।
এবার বাজারঘরের আসল অর্ডারের টেবিলের একটি ছোট সংস্করণ, ৮টি অর্ডার। দাম রাখা হয়েছে পয়সায়, পূর্ণসংখ্যা হিসেবে: ২৫০ টাকা মানে 25000 পয়সা। কেন? Floating-point সংখ্যায় 0.1 + 0.2 ঠিক 0.3 হয় না, আর টাকার হিসাবে এক পয়সার গরমিলও গরমিল। তাই টাকা শুধু ছাপার সময়।
import pandas as pd
pd.set_option("display.width", 60) # ফোনের পর্দায় সরু রাখতে
orders = pd.DataFrame({
"order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
"district": ["ঢাকা", "চট্টগ্রাম", "ঢাকা", "সিলেট",
"খুলনা", "ঢাকা", "রাজশাহী", "চট্টগ্রাম"],
"product": ["চা", "ডাল", "তেল", "চাল", "চা", "সাবান", "ডাল", "তেল"],
"qty": [2, 1, 3, 5, 1, 4, 2, 1],
"price_paisa": [25000, 14000, 21000, 8500, 25000, 6000, 14000, 21000],
"payment": ["COD", "bKash", "COD", "card", "bKash", "COD", "COD", "bKash"],
})
print(orders.head(3))
print(orders.tail(2))
print("shape:", orders.shape, "| সারি:", len(orders))
print(orders.dtypes)
print(orders.index)
print(type(orders["qty"]).__name__, type(orders[["qty"]]).__name__)আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):
order_id district product qty price_paisa payment
0 1001 ঢাকা চা 2 25000 COD
1 1002 চট্টগ্রাম ডাল 1 14000 bKash
2 1003 ঢাকা তেল 3 21000 COD
order_id district product qty price_paisa payment
6 1007 রাজশাহী ডাল 2 14000 COD
7 1008 চট্টগ্রাম তেল 1 21000 bKash
shape: (8, 6) | সারি: 8
order_id int64
district str
product str
qty int64
price_paisa int64
payment str
dtype: object
RangeIndex(start=0, stop=8, step=1)
Series DataFrame
head(3) প্রথম ৩টি সারি দেখায় (কিছু না দিলে ৫টি), tail(2) শেষ ২টি; বড় টেবিলে পুরোটা না ছাপিয়ে এভাবেই দেখা হয়। shape একটি tuple, (সারি, কলাম), আর len(orders) শুধু সারির সংখ্যা। dtypes প্রতিটি কলামের dtype: সংখ্যাগুলো int64, লেখাগুলো str. শেষ লাইনে একটি গুরুত্বপূর্ণ পার্থক্য: orders["qty"] (এক জোড়া বন্ধনী, একটি নাম) একটি Series দেয়, আর orders[["qty"]] (ভেতরে একটি list) একটি DataFrame দেয়, যার একটিমাত্র কলাম। পর্ব ১৪-এ a[0] আর a[0:1]-এর shape-এর যে পার্থক্য দেখেছিলেন, এটি তারই pandas রূপ।
পথ ২ — dict-এর list: প্রতিটি dict একটি সারি
API বা JSON থেকে ডেটা প্রায়ই আসে record হিসেবে: প্রতিটি অর্ডার একটি dict. pandas সেগুলোকে সারি হিসেবে নেয়, আর সব dict-এর key-এর মিলন থেকে কলাম বানায়। কোনো dict-এ একটি key না থাকলে সেখানে missing value বসে:
rows = [
{"order_id": 1001, "district": "ঢাকা", "qty": 2},
{"order_id": 1002, "district": "সিলেট", "qty": 1, "payment": "COD"},
]
print(pd.DataFrame(rows))
order_id district qty payment
0 1001 ঢাকা 2 NaN
1 1002 সিলেট 1 COD
(ডেস্কটপে pandas 3.0.2-এ সত্যিকারের run.) প্রথম অর্ডারে payment ছিল না, তাই NaN. পথ ১-এ দৈর্ঘ্য না মিললে error হয়, কিন্তু পথ ২-এ key না মিললে চুপচাপ NaN. JSON-এর ডেটায় তাই একটি key-এর বানান ভুল ("paymnet") একটি নতুন, প্রায় ফাঁকা কলাম বানিয়ে দেয়; info() দেখলেই তা ধরা পড়ে (অংশ ৪)।
পথ ৩ — NumPy array থেকে, নাম যোগ করে
মডিউল ৩-এর array-কে নাম দিতে চাইলে columns আর index দিয়ে দিন। এখানে ৪ জেলা × ৩ পণ্যের বিক্রির পরিমাণ:
rng = np.random.default_rng(42)
arr = rng.integers(1, 6, size=(4, 3))
df = pd.DataFrame(arr, columns=["চা", "ডাল", "তেল"],
index=["ঢাকা", "চট্টগ্রাম", "সিলেট", "খুলনা"])
print(df)
চা ডাল তেল
ঢাকা 1 4 4
চট্টগ্রাম 3 3 5
সিলেট 1 4 2
খুলনা 1 3 5
np.shares_memory(arr, df.to_numpy()) # False (আমাদের run-এ)
(ডেস্কটপে pandas 3.0.2-এ সত্যিকারের run.) array-এর সব কলাম এক dtype-এর (int64), তাই এখান থেকে মিশ্র কলাম আসে না; মিশ্র টেবিলের জন্য পথ ১ বা ২. শেষ লাইনটি দেখায়, আমাদের run-এ DataFrame নিজের জন্য সংখ্যাগুলো কপি করে নিয়েছে, তাই পরে arr বদলালেও df বদলাবে না। pandas 3-এর Copy-on-Write নীতির সঙ্গে এটি মেলে; তবে কখন copy হয় আর কখন হয় না, তার খুঁটিনাটি version-ভেদে বদলেছে, তাই নির্ভর করার আগে নিজের version-এ np.shares_memory দিয়ে দেখে নিন।
৪ · প্রথম নজর — info, describe, value_counts আর nunique
ডাক্তার রোগী দেখার আগে তাপমাত্রা, রক্তচাপ আর ওজন মাপেন, তারপর রোগনির্ণয়। নতুন একটি টেবিল হাতে পেলেও প্রথমে কয়েকটি মাপ দরকার: কয়টি সারি, কোন কলামে কী dtype, কোথায় মান নেই, সংখ্যাগুলোর পরিসর কত, আর লেখার কলামে কয়টি আলাদা মান। এই মাপগুলোর নাম descriptive statistics, আর pandas এগুলো এক লাইনে দেয়।
৮টি সারিতে এসব মাপের মজা নেই, তাই নিচের কোড seed দিয়ে বাজারঘরের ১২০টি অর্ডার বানায়। জেলার সম্ভাবনা এমনভাবে দেওয়া হয়েছে যাতে ঢাকায় অর্ডার বেশি আসে, আর পেমেন্টে COD (ক্যাশ অন ডেলিভারি) সবচেয়ে বেশি। এগুলো বানানো সংখ্যা, বাস্তব বাজারের কোনো পরিসংখ্যান নয়।
import numpy as np
import pandas as pd
pd.set_option("display.width", 60)
pd.set_option("display.max_columns", 8)
rng = np.random.default_rng(42) # seed: সবার একই ডেটা
n = 120
districts = ["ঢাকা", "চট্টগ্রাম", "রাজশাহী", "খুলনা", "সিলেট"]
orders = pd.DataFrame({
"order_id": np.arange(1001, 1001 + n, dtype=np.int64),
"district": rng.choice(districts, size=n, p=[.4, .25, .15, .1, .1]),
"qty": rng.integers(1, 6, size=n),
"price_paisa": rng.choice([6000, 8500, 14000, 21000, 25000], size=n),
"delivery_days": rng.integers(1, 8, size=n),
"payment": rng.choice(["COD", "bKash", "card"], size=n, p=[.5, .35, .15]),
})
orders.info()
print(orders.describe().round(1))
print(orders.describe(include="all").T[["count", "unique", "top", "freq"]])
print(orders["district"].value_counts())
print(orders["payment"].value_counts(normalize=True).round(3))
print(orders.nunique())আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):
<class 'pandas.DataFrame'>
RangeIndex: 120 entries, 0 to 119
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 120 non-null int64
1 district 120 non-null str
2 qty 120 non-null int64
3 price_paisa 120 non-null int32
4 delivery_days 120 non-null int64
5 payment 120 non-null str
dtypes: int32(1), int64(3), str(2)
memory usage: 4.3 KB
order_id qty price_paisa delivery_days
count 120.0 120.0 120.0 120.0
mean 1060.5 2.9 14616.7 4.1
std 34.8 1.4 7591.4 2.1
min 1001.0 1.0 6000.0 1.0
25% 1030.8 2.0 6000.0 2.0
50% 1060.5 3.0 14000.0 4.0
75% 1090.2 4.0 21000.0 6.0
max 1120.0 5.0 25000.0 7.0
count unique top freq
order_id 120.0 NaN NaN NaN
district 120 5 ঢাকা 45
qty 120.0 NaN NaN NaN
price_paisa 120.0 NaN NaN NaN
delivery_days 120.0 NaN NaN NaN
payment 120 3 COD 56
district
ঢাকা 45
চট্টগ্রাম 30
রাজশাহী 27
খুলনা 10
সিলেট 8
Name: count, dtype: int64
payment
COD 0.467
bKash 0.367
card 0.167
Name: proportion, dtype: float64
order_id 120
district 5
qty 5
price_paisa 5
delivery_days 7
payment 3
dtype: int64
info() সবচেয়ে বেশি তথ্য দেয় এক জায়গায়: ১২০টি সারি, ৬টি কলাম, প্রতিটি কলামে কয়টি ঘর ফাঁকা নয় (এখানে সবগুলো 120, মানে কোনো missing নেই), আর প্রতিটির dtype. একটি চমক আছে: price_paisa এখানে int32, বাকি সংখ্যা int64. কারণ rng.choice-কে একটি Python list দিলে NumPy সেটিকে নিজের default integer-এ বদলায়, আর ব্রাউজারের 32-বিট Pyodide-এ সেই default int32; ডেস্কটপে একই কোড int64 দিয়েছে, আর memory 5.8 KB, এখানে 4.3 KB. rng.integers সব যন্ত্রে int64 দেয়, আর order_id-এ dtype স্পষ্ট বলা আছে। পয়সার যোগফল বড় হলে int32 উপচে পড়তে পারে (পর্ব ১৩-এর integer overflow), তাই আসল কাজে টাকার কলামে astype("int64") দিয়ে নিশ্চিত হোন। describe() শুধু সংখ্যার কলাম নেয়: qty-এর গড় 2.9, ন্যূনতম 1, সর্বোচ্চ 5, যা প্রত্যাশিত পরিসরের ভেতরে। কিন্তু order_id-এর গড় 1060.5 অর্থহীন; ID একটি নাম, পরিমাণ নয় (অংশ ৬-এ সমাধান)। include="all" দিলে লেখার কলামেও মাপ আসে: district-এ ৫টি আলাদা মান, সবচেয়ে বেশি ঢাকা, ৪৫ বার; payment-এ COD ৫৬ বার। সংখ্যার কলামে এই সারিগুলো NaN, কারণ সেখানে প্রশ্নটি খাটে না; ফোনে পড়ার সুবিধার জন্য ফলটি .T দিয়ে উল্টে চারটি সারি রাখা হয়েছে। value_counts() প্রতিটি মান গোনে, বড় থেকে ছোট: ঢাকা 45, চট্টগ্রাম 30 … সিলেট 8, মোট 120. normalize=True দিলে ভাগ: COD প্রায় ৪৭%, bKash ৩৭%, card ১৭%. আমরা সম্ভাবনা দিয়েছিলাম 0.5, 0.35, 0.15; ১২০টি নমুনায় একটু এদিক-ওদিক হওয়াই স্বাভাবিক। শেষে nunique(): order_id-এ ১২০টি আলাদা মান, মানে প্রতিটি ID অনন্য, যা ID-র জন্য প্রথম যাচাই।
প্রতিটি কাজ কী বলে
| কাজ | কী দেয় | কী ধরতে সাহায্য করে |
|---|---|---|
head(), tail() | প্রথম বা শেষ কয়েকটি সারি | কলামের নাম, মানের চেহারা, ফাইলের শেষে বাড়তি সারি |
shape | (সারি, কলাম) | প্রত্যাশিত সারি এসেছে কি না |
dtypes, info() | dtype, non-null গণনা, memory | সংখ্যা লেখা হয়ে পড়া, ফাঁকা ঘর |
describe() | count, mean, std, min, quartile, max | অসম্ভব মান (ঋণাত্মক qty), outlier |
describe(include="all") | লেখার কলামে unique, top, freq-ও | কলামটিতে কয়টি আলাদা মান, কোনটি সবচেয়ে বেশি |
value_counts() | প্রতিটি মান কতবার, বড় থেকে ছোট | বানানভেদ (“Dhaka” বনাম “ঢাকা”), class imbalance |
nunique() | প্রতিটি কলামে আলাদা মানের সংখ্যা | ID কলাম সত্যিই অনন্য কি না, কোনটি category হওয়ার মতো |
দুটি সূক্ষ্ম বিষয়। প্রথমত, value_counts() default-এ missing value গোনে না; গুনতে চাইলে dropna=False. দ্বিতীয়ত, describe()-এর std হলো নমুনার standard deviation, ddof=1 দিয়ে, যেখানে NumPy-র np.std-এর default ddof=0; একই ডেটায় দুটি আলাদা সংখ্যা আসার এটিই কারণ। পর্ব ১৬-এ (aggregation আর random) ddof-এর অর্থ দেখেছেন।
৫ · কলাম নিয়ে কাজ — বাছাই, যোগ, নাম বদল আর বাদ
একটি কলাম বনাম কলামের list
orders["qty"] # Series, shape (8,)
orders[["qty"]] # DataFrame, shape (8, 1)
orders[["district", "qty"]] # DataFrame, দুটি কলাম, এই ক্রমে
orders.qty # Series, কিন্তু এই রূপটি এড়ানো ভালো
বিন্দু দিয়ে লেখা (orders.qty) পড়তে সুবিধার, কিন্তু তিনটি জায়গায় ভাঙে: কলামের নামে ফাঁকা জায়গা বা বাংলা থাকলে, নামটি কোনো method-এর নামের সঙ্গে মিলে গেলে (একটি কলামের নাম count বা size হলে orders.count method-টিই দেয়), আর নতুন কলাম বানাতে orders.total = ... লিখলে, যা কলাম বানায় না, শুধু একটি attribute জুড়ে দেয় (pandas তখন একটি warning দেয়)। তাই বন্ধনী আর উদ্ধৃতি চিহ্ন, সবসময়। সারি বাছাই আর শর্ত দিয়ে ছাঁকা (loc, iloc, boolean mask) পর্ব ২২ · বাছাই আর filter — loc, iloc, query আর boolean mask-এর বিষয়।
হিসাব করা কলাম যোগ
প্রতিটি অর্ডারের মোট দাম = qty × price_paisa. pandas-এ দুটি কলাম গুণ করলে ঘরে ঘরে গুণ হয়, index মিলিয়ে, কোনো loop ছাড়া:
orders["total_paisa"] = orders["qty"] * orders["price_paisa"]
orders["prepaid"] = orders["payment"] != "COD" # bool কলাম
# অর্ডার 1001: 2 × 25000 = 50000 পয়সা = ৫০০ টাকা
# অর্ডার 1004: 5 × 8500 = 42500 পয়সা = ৪২৫ টাকা
৮টি অর্ডারের total_paisa: 50000, 14000, 63000, 42500, 25000, 24000, 28000, 21000; মোট 267500 পয়সা, মানে ২,৬৭৫ টাকা। আগে বিক্রি হওয়া অর্ডারগুলোর (bKash আর card) মোট 14000 + 42500 + 25000 + 21000 = 102500 পয়সা, ১,০২৫ টাকা। এই সংখ্যাগুলো অংশ ৮-এর কোড আর অনুশীলন ৪-এ আবার মিলিয়ে দেখবেন।
খেয়াল করুন, orders["total_paisa"] = ... মূল টেবিলটিকেই বদলায়, জায়গায়। এটি সহজ, কিন্তু notebook-এ একই cell দুবার চালালে বা একটি function-এর ভেতরে লিখলে মূল ডেটা নিঃশব্দে বদলে যায়, যা একটি side effect. অংশ ৮-এর assign নতুন টেবিল ফেরত দেয়, মূলকে না ছুঁয়ে।
নাম বদল আর বাদ দেওয়া
orders.rename(columns={"district": "zila"}) # নতুন DataFrame
orders.drop(columns=["price_paisa"]) # নতুন DataFrame
orders.drop(columns="price_paisa").columns.tolist()
# ['order_id', 'district', 'qty', 'payment', 'total_paisa', 'prepaid']
orders.columns.tolist() # মূল টেবিলে price_paisa এখনো আছে
rename আর drop দুটিই একটি নতুন DataFrame ফেরত দেয়; মূলটি বদলায় না। নতুনদের সবচেয়ে চেনা ভুল হলো orders.drop(columns="price_paisa") লিখে ধরে নেওয়া যে কলামটি চলে গেছে। ফল রাখতে হয়: orders = orders.drop(...). inplace=True নামে একটি বিকল্প এখনো অনেক method-এ আছে, কিন্তু pandas-এর উন্নয়ন দল একে নিরুৎসাহিত করে: এতে সাধারণত কোনো গতি বা memory-র লাভ হয় না, আর method chain ভেঙে যায় (এটি None ফেরত দেয়)। তাই এই কোর্সে আমরা সবসময় ফল নতুন নামে রাখব।
৬ · Index — RangeIndex, set_index আর reset_index
এখন পর্যন্ত আমাদের index ছিল 0, 1, 2, … অর্থাৎ RangeIndex. এটি শুধু অবস্থানের নম্বর, অর্ডারের কোনো তথ্য বহন করে না। কিন্তু অর্ডারের নিজের একটি পরিচয় আছে: order_id. সেটিকে index বানালে রসিদ নম্বর ধরে সরাসরি অর্ডার খোঁজা যায়, ঠিক দোকানির মতো। নিচের ধাপে ৪টি অর্ডারের একটি টেবিল নিয়ে দেখুন।
1o # order_id একটি কলাম2oi = o.set_index("order_id")3oi.loc[1005] # label দিয়ে4oi.iloc[2] # অবস্থান দিয়ে5oi.reset_index()
| index | |
| কলাম |
- শুরুর টেবিল। index হলো RangeIndex 0–3, আর
order_idঅন্য সব কলামের মতো একটি সাধারণ কলাম। এই অবস্থায়o.loc[1005]লিখলেKeyError: 1005, কারণ index-এ 1005 নামে কোনো label নেই। set_index("order_id"). কলামটি কলামের তালিকা থেকে উঠে index-এর জায়গায় বসে; কলাম এখন দুটি। index-এর একটি নামও হয়েছে,order_id, যা ছাপায় আলাদা লাইনে দেখা যায়। ফল একটি নতুন DataFrame;oঅক্ষত।- label দিয়ে একটি সারি।
oi.loc[1005]রসিদ নম্বর ধরে খুলনার অর্ডারটি দেয়। একটি সারি তুললে ফল একটি Series, যার index এখন কলামের নামগুলো; লেখা আর সংখ্যা মিশে আছে বলে dtypeobject. - অবস্থান দিয়ে একই সারি।
oi.iloc[2]তৃতীয় সারি, যা ঘটনাক্রমে একই অর্ডার। পার্থক্যটি দেখা যায় সাজানোর পর: টেবিল উল্টে সাজালেloc[1005]তখনো খুলনার অর্ডার দেবে, কিন্তুiloc[2]অন্য কিছু দেবে। reset_index(). উল্টো কাজ: index আবার একটি কলাম হয়ে ফিরে আসে, আর নতুন RangeIndex 0–3 বসে। groupby-র ফল আবার সাধারণ টেবিলে আনতে (পর্ব ২৪) এই কাজটি খুব কাজে লাগে।
অর্থবহ index কেন কাজে লাগে
- খোঁজা সহজ ও স্পষ্ট:
orders.loc[1005]পড়লেই বোঝা যায় কী চাওয়া হচ্ছে; “পঞ্চম সারি” সাজানো বা ছাঁকার পর বদলে যায়, রসিদ নম্বর বদলায় না। - alignment সঠিক হয়: অংশ ২-এর মতো, দুটি টেবিল বা Series-এর হিসাব অর্থবহ label ধরে মেলে। জেলাকে index বানানো দুটি মাসিক Series-এর যোগ এই কারণেই ঠিক ফল দিয়েছিল।
- ID-কে সংখ্যা ভাবার ভুল এড়ায়: অংশ ৪-এ
describe()order_id-এর গড় 1060.5 দিয়েছিল, যা অর্থহীন। ID index-এ থাকলে সে আর হিসাবে ঢোকে না।
তবে একটি সতর্কতা: pandas index-এর label অনন্য হওয়া বাধ্যতামূলক নয়। একই label দুবার থাকলে loc একটি সারির বদলে দুটি সারি দেয়, আর alignment-এ সারি গুণিতক হারে বাড়তে পারে। ID-কে index বানানোর পর orders.index.is_unique দেখে নিন, বা set_index("order_id", verify_integrity=True) লিখুন, যা পুনরাবৃত্তি পেলে error দেয়। আর একটি ছোট তথ্য: আমাদের ৮টি অর্ডারের order_id 1001 থেকে 1008 টানা, তাই pandas 3.0.2 set_index-এর পর সেটিকে RangeIndex(start=1001, stop=1009, step=1, name='order_id') হিসেবে রেখেছে (ডেস্কটপে সত্যিকারের run); ফাঁক থাকলে (১০০১, ১০০৪, …) সাধারণ Index, dtype int64.
৭ · pandas 3-এর dtype — int64 থেকে str, আর Int64-এর কথা
প্রতিটি কলামের dtype ঠিক করে কোন কাজ চলবে, কত memory লাগবে, আর missing value কীভাবে বসবে। নিচের সারণির সব উদাহরণ ডেস্কটপে pandas 3.0.2-এ চালিয়ে দেখা।
| dtype | কখন আসে | বাজারঘরের উদাহরণ | missing হলে |
|---|---|---|---|
int64 | পূর্ণসংখ্যার list বা array | qty, price_paisa | NaN আসে না; একটি ফাঁকা থাকলে পুরো কলাম float64 |
float64 | দশমিক সংখ্যা, বা NaN-সহ পূর্ণসংখ্যা | গড় rating | NaN |
bool | True/False, তুলনার ফল | prepaid | NaN বসলে object হয়ে যায় |
str | লেখার list (pandas 3-এর নতুন default) | district, payment | NaN |
category | astype("category") | payment: ৩টি মান, হাজারো সারি | NaN |
datetime64[us] | pd.to_datetime(...) | order_date | NaT |
Int64 (বড় I) | dtype="Int64" | ফাঁকা থাকতে পারে এমন qty | <NA>, পূর্ণসংখ্যা থাকে |
object | মিশ্র ধরনের মান | [1, "a"] | যেকোনো Python object |
নতুন default — লেখা এখন str
pandas 2 পর্যন্ত লেখার কলামের dtype ছিল object, মানে “যেকোনো Python object-এর ঠিকানার array”. সেই কলামে লেখা, সংখ্যা, list, সবই বসানো যেত, আর dtype দেখে বোঝা যেত না কলামটিতে আসলে কী আছে। pandas 3.0-এর release notes অনুযায়ী লেখার কলাম এখন str dtype হিসেবে চেনা হয়: PyArrow install থাকলে ভেতরে Arrow-এর string array, না থাকলে Python object দিয়ে, আর missing value সবসময় NaN. ডেস্কটপে, যেখানে আমরা PyArrow install করিনি, repr দেখায় <StringDtype(storage='python', na_value=nan)>; পাতার ভেতরের Pyodide-এ pyarrow package থাকলেও আমাদের code সেটি load করে না, তাই সেখানেও storage python দেখায় (পর্ব ২৭-এ মেপে দেখা)। দুই ক্ষেত্রেই ছাপায় শুধু str.
এর একটি ব্যবহারিক ফল: পুরোনো tutorial-এর df["district"].dtype == object পরীক্ষা pandas 3-এ False দেয় (আমাদের run-এ তাই দিয়েছে)। লেখার কলাম খুঁজতে pd.api.types.is_string_dtype(...) নিরাপদ।
Category আর datetime — এক নজরে
pd.Series(["COD", "bKash", "COD"], dtype="category")
0 COD
1 bKash
2 COD
dtype: category
Categories (2, str): ['COD', 'bKash']
pd.to_datetime(pd.Series(["2026-10-01", "2026-10-02"]))
0 2026-10-01
1 2026-10-02
dtype: datetime64[us]
(ডেস্কটপে pandas 3.0.2-এ সত্যিকারের run.) category dtype প্রতিটি আলাদা মান একবার রাখে, আর প্রতিটি সারিতে শুধু একটি ছোট সংখ্যা: ১০ লাখ সারির payment কলামে তিনটি মান থাকলে memory অনেক কমে। কখন এটি লাভজনক আর কখন নয়, তা পর্ব ২৭ · বড় ডেটায় pandas-এর বিষয়। তারিখের কলাম datetime64; খেয়াল করুন একক [us], মাইক্রোসেকেন্ড। pandas 3.0-এর release notes অনুযায়ী লেখা থেকে তারিখ পড়লে default একক এখন মাইক্রোসেকেন্ড, আগের মতো সবসময় ন্যানোসেকেন্ড নয়। সময় অঞ্চল, resample আর Asia/Dhaka পর্ব ২৬-এর বিষয়।
NaN পূর্ণসংখ্যাকে float বানায় — আর Int64
pd.Series([1, 2, None]).dtype # float64 [1.0, 2.0, nan]
pd.Series([1, 2, None], dtype="Int64")
0 1
1 2
2 <NA>
dtype: Int64
NumPy-র int64-এ “মান নেই” বোঝানোর কোনো বিট-নকশা নেই; NaN শুধু float-এর জগতে আছে। তাই একটি ফাঁকা ঘর থাকলেই পুরো কলাম float64 হয়ে যায়, আর 2 ছাপা হয় 2.0 হিসেবে। এটিই অংশ ২-এ oct_ + nov-এর float64-এর কারণ। pandas-এর nullable integer dtype, Int64 (বড় হাতের I), প্রতিটি মানের পাশে একটি আলাদা “আছে/নেই” mask রাখে, তাই পূর্ণসংখ্যা পূর্ণসংখ্যাই থাকে, আর ফাঁকা ঘরে বসে pd.NA. পয়সার হিসাবের মতো জায়গায়, যেখানে float-এ যাওয়া বিপজ্জনক, এটি কাজের। তবে pandas 3-এ default এখনো NumPy-র int64; Int64 নিজে থেকে বেছে নিতে হয়।
আরও গভীরে। নতুন str dtype-এর প্রস্তাব pandas-এর উন্নয়ন-নথি PDEP-14 (“Dedicated string data type for pandas 3.0”)-এ লেখা, আর Copy-on-Write-এর প্রস্তাব PDEP-7-এ। ভেতরে একটি DataFrame বহু বছর ধরে “BlockManager” নামের একটি কাঠামো দিয়ে চলে, যা একই dtype-এর কলামগুলোকে একটি 2-D block-এ একসঙ্গে রাখতে পারে; এই কারণেই একটি কলাম যোগ বা বাদ দেওয়ার খরচ সবসময় সমান নয়। Arrow-ভিত্তিক কলাম বিন্যাস নিয়ে আরও পড়তে চাইলে Apache Arrow-এর columnar format-এর নির্দেশিকা আর পর্ব ১০-এর (ডেটার ফাইল) Parquet অংশ দেখুন।
৮ · Method chaining আর assign — আর pandas বনাম spreadsheet ও SQL
রান্নার রেসিপির কথা ভাবুন: “চাল ধুয়ে, পানি দিয়ে, লবণ মিশিয়ে, ২০ মিনিট ফোটান”। প্রতিটি ধাপ আগের ধাপের ফল নিয়ে কাজ করে, আর মাঝের প্রতিটি অবস্থার আলাদা নাম দরকার হয় না। pandas-এর বেশিরভাগ method একটি নতুন DataFrame ফেরত দেয়, তাই এগুলো একটির পর একটি বিন্দু দিয়ে জোড়া যায়। এর নাম method chaining. পুরো শিকল বন্ধনীর ভেতরে রাখলে প্রতিটি ধাপ নিজের লাইনে লেখা যায়, পড়তে রেসিপির মতো।
শিকলের ভেতরে নতুন কলাম বানানোর হাতিয়ার assign. এটি মূল টেবিল না বদলে নতুন কলামসহ একটি নতুন টেবিল দেয়। মান হিসেবে একটি lambda দিলে pandas সেটিকে শিকলের সেই মুহূর্তের টেবিল দিয়ে ডাকে, তাই আগের ধাপে বানানো কলামও ব্যবহার করা যায়।
import pandas as pd
pd.set_option("display.width", 60)
orders = pd.DataFrame({
"order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
"district": ["ঢাকা", "চট্টগ্রাম", "ঢাকা", "সিলেট",
"খুলনা", "ঢাকা", "রাজশাহী", "চট্টগ্রাম"],
"qty": [2, 1, 3, 5, 1, 4, 2, 1],
"price_paisa": [25000, 14000, 21000, 8500, 25000, 6000, 14000, 21000],
"payment": ["COD", "bKash", "COD", "card", "bKash", "COD", "COD", "bKash"],
})
report = (
orders
.assign(total_paisa=lambda d: d["qty"] * d["price_paisa"],
prepaid=lambda d: d["payment"] != "COD")
.rename(columns={"district": "zila"})
.drop(columns=["price_paisa"])
.set_index("order_id")
.sort_values("total_paisa", ascending=False)
)
print(report.head(4))
print(report.dtypes)
print("মূল কলাম:", list(orders.columns))
top = report.iloc[0]
print("সবচেয়ে বড় অর্ডার:", report.index[0], top["zila"],
f"{top['total_paisa'] / 100:,.2f} টাকা")
print("অর্ডার 1004:", report.loc[1004, "total_paisa"], "পয়সা")
print("মোট:", report["total_paisa"].sum() / 100, "টাকা")আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):
zila qty payment total_paisa prepaid
order_id
1003 ঢাকা 3 COD 63000 False
1001 ঢাকা 2 COD 50000 False
1004 সিলেট 5 card 42500 True
1007 রাজশাহী 2 COD 28000 False
zila str
qty int64
payment str
total_paisa int64
prepaid bool
dtype: object
মূল কলাম: ['order_id', 'district', 'qty', 'price_paisa', 'payment']
সবচেয়ে বড় অর্ডার: 1003 ঢাকা 630.00 টাকা
অর্ডার 1004: 42500 পয়সা
মোট: 2675.0 টাকা
শিকলটি ছয় ধাপে পড়ুন: মূল টেবিল নেওয়া, দুটি নতুন কলাম যোগ, district-এর নাম বদলে zila, price_paisa বাদ, order_id-কে index বানানো, আর মোট দাম ধরে বড় থেকে ছোট সাজানো। ফলের প্রথম সারি অর্ডার 1003: ঢাকা, 3 × 21000 = 63000 পয়সা, ৬৩০ টাকা। dtype-এ নতুন prepaid কলাম bool. “মূল কলাম” লাইনটি প্রমাণ করে orders একটুও বদলায়নি: district আর price_paisa সেখানে আছে, নতুন কলাম নেই। index এখন order_id, তাই সাজানোর পরও report.loc[1004, ...] ঠিক সিলেটের অর্ডারটিই দিয়েছে: 42500 পয়সা। মোট ২,৬৭৫ টাকা, অংশ ৫-এর হাতের হিসাবের সঙ্গে মেলে। টাকায় রূপান্তর শুধু ছাপার লাইনে, ডেটায় নয়।
শিকলের সুবিধা: মাঝের অবস্থার জন্য df2, df3, df_final নাম লাগে না, আর notebook-এ cell বারবার চালালেও ফল একই থাকে, কারণ মূল টেবিল বদলায় না। অসুবিধা: খুব লম্বা শিকলে ভুল কোন ধাপে, তা খুঁজতে কষ্ট হয়। সমাধান সহজ: শিকলের যেকোনো লাইন সাময়িকভাবে comment করে দেখুন, বা মাঝে .pipe(lambda d: print(d.shape) or d) বসিয়ে shape ছাপুন। পাঁচ-সাত ধাপের বেশি হলে দুটি নামযুক্ত ধাপে ভাগ করাই ভালো; পর্ব ১১-এর পরিচ্ছন্ন কোডের নিয়ম এখানেও খাটে।
pandas, spreadsheet আর SQL — একই টেবিল, তিন দৃষ্টি
| বিষয় | Spreadsheet (Excel) | pandas | SQL টেবিল |
|---|---|---|---|
| সারির পরিচয় | সারির নম্বর (১, ২, …) | index (যেকোনো label) | primary key; সারির নিজস্ব ক্রম নেই |
| কলামের ধরন | প্রতিটি ঘর আলাদা; একই কলামে লেখা ও সংখ্যা মিশতে পারে | প্রতিটি কলামে একটি dtype | schema-তে আগে থেকে ঠিক করা |
| নতুন কলাম | সূত্র লিখে নিচে টেনে দেওয়া | assign, পুরো কলামে একসঙ্গে | SELECT qty * price AS total |
| ডেটার আকার | প্রায় ১০ লাখ সারির সীমা | যন্ত্রের RAM পর্যন্ত | disk পর্যন্ত, server-এ |
| পুনরায় চালানো | হাতের ধাপ, ইতিহাস কঠিন | কোড, Git-এ রাখা যায় | query, কোড হিসেবে রাখা যায় |
| সবচেয়ে ভালো যেখানে | দ্রুত দেখা, ছোট হিসাব, ভাগাভাগি | বিশ্লেষণ, পরিষ্কার, model-এর প্রস্তুতি | বহু ব্যবহারকারী, লেনদেন, স্থায়ী সংরক্ষণ |
pandas-এর একটি DataFrame অনেকটা SQL-এর একটি টেবিলের মতো, কিন্তু তিনটি বড় পার্থক্য: pandas-এ সারির একটি নির্দিষ্ট ক্রম আছে, index থাকে, আর পুরো টেবিল memory-তে থাকে। বাস্তবে দুটি একসঙ্গে চলে: ডেটা থাকে ডেটাবেসে, SQL দিয়ে দরকারি অংশ তুলে pandas-এ বিশ্লেষণ। SQL-এর মূল ধারণা শিখতে DBMS আর SQL কোর্স, বিশেষ করে তার SELECT-এর পর্ব দেখুন; pandas থেকে SQL পড়া পর্ব ২১-এ আসবে।
৯ · নতুনদের চেনা ভুল
pandas-এর ভুলের বড় অংশ error দেয় না, শুধু ভুল টেবিল দেয়। নিচের সবগুলো ডেস্কটপে pandas 3.0.2-এ চালিয়ে দেখা।
ভুল ১ — method-এর ফল না রাখা
orders.drop(columns="price_paisa") # ফল কোথাও রাখা হয়নি
"price_paisa" in orders.columns # True — কলাম এখনো আছে
orders = orders.drop(columns="price_paisa") # ঠিক
ভুল ২ — শিকলবাঁধা assignment, যা pandas 3-এ কখনো কাজ করে না
orders["qty"][0] = 77
# ChainedAssignmentError: A value is being set on a copy of a DataFrame or
# Series through chained assignment. Such chained assignment never works ...
orders["qty"].iloc[0] # 2 — বদলায়নি
orders.loc[0, "qty"] = 77 # ঠিক: এক ধাপে, এক বন্ধনীতে
Chained assignment পর্ব ১৪-এর NumPy-র fancy index-এর ফাঁদের মতো, কিন্তু pandas 3-এ নিয়ম আরও সরল: Copy-on-Write-এর কারণে প্রথম ধাপের ফল সবসময় copy-র মতো আচরণ করে, তাই শিকলে লেখা কখনোই মূল টেবিল বদলায় না, আর pandas একটি warning দেয় (ওপরে তার প্রথম লাইনগুলো)। একই কারণে s = orders["qty"] নিয়ে s.iloc[0] = 99 লিখলে orders বদলায় না (আমাদের run-এ orders["qty"].iloc[0] 2-ই ছিল)। পুরোনো pandas-এ এটি বদলাতে পারত, তাই পুরোনো কোড pandas 3-এ নিঃশব্দে অন্যরকম আচরণ করতে পারে।
ভুল ৩ — অবসরপ্রাপ্ত API
orders.append(orders)
# AttributeError: 'DataFrame' object has no attribute 'append'
pd.concat([orders, orders]) # ঠিক
DataFrame.append pandas 2.0-তে সরিয়ে দেওয়া হয়েছে, কিন্তু অনেক পুরোনো tutorial আর AI সহকারীর উত্তরে এখনো আসে। লুপের ভেতরে বারবার concat করাও ধীর; সারিগুলো আগে একটি Python list-এ জমিয়ে শেষে একবার DataFrame বানান।
ভুল ৪ — Series-কে if-এ দেওয়া, আর বানানের ভুল
if orders["qty"] > 2: ...
# ValueError: The truth value of a Series is ambiguous. Use a.empty,
# a.bool(), a.item(), a.any() or a.all().
orders["Qty"] # KeyError: 'Qty' — নাম case-sensitive
পর্ব ১৪-এর NumPy-র and-এর সমস্যার মতোই: অনেক True/False-এর একটি Series “সত্য” কি না, প্রশ্নটি অস্পষ্ট। “কোনো অর্ডারে ২-এর বেশি?” জানতে (orders["qty"] > 2).any().
ভুল ৫ — label আর অবস্থান গুলিয়ে ফেলা
অংশ ২-এর শিক্ষা এক লাইনে: pandas-এর হিসাব pandas-এর ভেতরেই রাখুন, আর অপ্রত্যাশিত NaN দেখলে প্রথমে দুই দিকের index তুলনা করুন।
পরীক্ষা ও ইন্টারভিউতে। বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এমন প্রশ্ন প্রায়ই আসে:
● Series আর DataFrame-এর পার্থক্য কী? — Series একটি label-যুক্ত 1-D array; DataFrame একই index ভাগ করা কয়েকটি Series-এর 2-D টেবিল।
● দুটি Series-এর index আলাদা হলে যোগ করলে কী হয়? — label ধরে মেলে; যে label একদিকে নেই সেখানে NaN, আর dtype float64. add(..., fill_value=0) দিয়ে এড়ানো যায়।
● df["a"] আর df[["a"]]-এর পার্থক্য? — প্রথমটি Series, দ্বিতীয়টি এক কলামের DataFrame.
● একটি NaN থাকলে int কলাম কেন float হয়? — NumPy-র int-এ missing বোঝানোর উপায় নেই; Int64 nullable dtype দিয়ে পূর্ণসংখ্যা রাখা যায়।
● pandas 3-এ লেখার কলামের default dtype কী? — str, আগে ছিল object.
১০ · AI-এর যুগে — AI সহকারীর pandas কোড যাচাই
pandas হলো সেই library, যার কোড AI সহকারীরা সবচেয়ে বেশি লেখে, আর যেখানে তাদের ভুলও সবচেয়ে চেনা। কারণটি সহজ: ইন্টারনেটে pandas-এর কোড বিপুল, আর তার বড় অংশ pandas 1.x-এর যুগের। AI সহকারীর pandas কোডে এগুলো প্রায়ই দেখা যায়:
- পুরোনো API:
df.append,inplace=True-এর ছড়াছড়ি, লেখার কলাম খুঁজতেdtype == object, warning চাপা দিতেpd.options.mode.chained_assignment = None, বা শিকলবাঁধাdf[...][...] = x, যা pandas 3-এ কিছুই বদলায় না। - বানানো function বা parameter: শুনতে বিশ্বাসযোগ্য কিন্তু অস্তিত্বহীন নাম। একটি অচেনা method দেখলে docs-এ খুঁজুন।
- নিঃশব্দ alignment-এর ভুল: দুটি টেবিল sort করে
.valuesদিয়ে পাশাপাশি বসানো, বা ভিন্ন index-এর Series যোগ করে NaN-কে 0 দিয়ে ঢেকে দেওয়া, কারণ জিজ্ঞাসা না করেই। - ভুল পরিসংখ্যান: ID কলামের গড়, টাকায় float দিয়ে হিসাব করে পয়সার গরমিল,
describe()-এর std আর NumPy-র std-কে এক ধরা। আর পরে model-এর কাজে পুরো ডেটার ওপরdescribeবা স্কেলিং করে তারপর train-test ভাগ করে data leakage ঘটানো।
প্রম্পটে version, কলামের নাম আর dtype, এককের নিয়ম (পয়সা), আর কোন পুরোনো পথ চলবে না, সব বলে দিন। আসল ক্রেতার ডেটা নয়, বানানো ছোট উদাহরণ দিন।
AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১NumPy তো আগেই ছিল, R-এর data.frame-ও ছিল। তবু ২০০৮ সালে pandas তৈরির প্রয়োজন কেন হলো, আর এটি কীভাবে Python-কে ডেটা বিশ্লেষণের প্রধান ভাষাগুলোর একটি করে তুলল?
২০০০-এর দশকের শেষে Python-এ সংখ্যার হিসাবের জন্য NumPy ছিল, বিজ্ঞানের জন্য SciPy ছিল, ছবি আঁকার জন্য matplotlib ছিল। কিন্তু বাস্তবের ব্যবসায়িক বা আর্থিক ডেটা দেখতে NumPy array-এর মতো নয়। এতে তারিখ থাকে, নাম থাকে, ফাঁকা ঘর থাকে, আর একটি টেবিলে নানা ধরনের কলাম থাকে। সেই কাজের জন্য পরিসংখ্যানবিদেরা ব্যবহার করতেন R, যার data.frame ঠিক এমন টেবিলের জন্য তৈরি, কিংবা Excel বা বাণিজ্যিক পরিসংখ্যান-সফটওয়্যার।
pandas-এর অফিসিয়াল ইতিহাস অনুযায়ী এর কাজ শুরু হয় ২০০৮ সালে AQR Capital Management নামের একটি বিনিয়োগ প্রতিষ্ঠানে, আর ২০০৯-এর শেষে এটি open source হয়। কাজটি শুরু করেন Wes McKinney. প্রচলিত বিবরণে তাঁর লক্ষ্য ছিল R-এর সুবিধাগুলো, বিশেষ করে label-যুক্ত টেবিল আর সময়ের ধারা নিয়ে কাজ, একটি সাধারণ প্রোগ্রামিং ভাষায় আনা, যেখানে একই কোডে ডেটা পড়া, বিশ্লেষণ, ওয়েব আর বাকি সব করা যায়। আর্থিক ডেটায় index ধরে alignment ছিল কেন্দ্রীয় সমস্যা: দুটি শেয়ারের দামের সারিতে ছুটির দিন আলাদা হলে অবস্থান ধরে মেলানো ভুল ফল দেয়, তারিখ ধরে মেলানো ঠিক ফল দেয়। অংশ ২-এর alignment এই চাহিদা থেকেই এসেছে।
pandas-এর সাফল্যের পেছনে কয়েকটি জিনিস একসঙ্গে কাজ করেছে। ২০১২ সালে McKinney-র “Python for Data Analysis” বই অসংখ্য মানুষকে একটি পথ দেখায়। IPython notebook (পরে Jupyter, পর্ব ৮) টেবিল দেখার একটি সহজ পরিবেশ দেয়। আর ২০১০-এর দশকে scikit-learn-এর মতো machine learning library Python-এ আসায় ডেটা পরিষ্কার থেকে model পর্যন্ত পুরো পথ এক ভাষায় সম্ভব হয়। তবে মনে রাখা দরকার, R আজও পরিসংখ্যানে খুব শক্তিশালী, আর “কোনটি ভালো” বিতর্কের উত্তর কাজের ধরনের ওপর নির্ভর করে। pandas নিজেও সমালোচনার ঊর্ধ্বে নয়: এর API বড় আর কখনো অসংগত, আর memory-র ব্যবহার বেশি, যা পরে Polars-এর মতো বিকল্পের জন্ম দিয়েছে (পর্ব ২৭)।
মূল উপলব্ধি: pandas কোনো নতুন গণিত আনেনি; এটি label, মিশ্র কলাম আর ফাঁকা ঘরের মতো বাস্তবের অগোছালো ডেটার চাহিদাকে NumPy-র গতির সঙ্গে একটি সাধারণ ভাষায় জুড়েছে, আর সেই জোড়াই একে জনপ্রিয় করেছে।
প্র ০২Index ধরে স্বয়ংক্রিয় alignment কি আশীর্বাদ, নাকি ফাঁদ? কেন কিছু নতুন library ইচ্ছা করে index রাখেনি?
আশীর্বাদের দিকটি অংশ ২-এ দেখেছেন। দুটি মাসের জেলাভিত্তিক হিসাব আলাদা ক্রমে থাকলেও pandas ঠিক ঢাকার সঙ্গে ঢাকা মেলায়। আর্থিক সময়-ধারায়, যেখানে বিভিন্ন বাজারের ছুটির দিন আলাদা, বা সেন্সরের ডেটায়, যেখানে কিছু মাপ হারিয়ে যায়, এই alignment অসংখ্য নিঃশব্দ ভুল আটকায়। NumPy-তে একই কাজ করতে আপনাকে নিজে নাম মিলিয়ে সাজাতে হতো।
ফাঁদের দিকটিও বাস্তব। প্রথমত, alignment নিঃশব্দে NaN বানায়। একটি জেলার নাম এক টেবিলে “Chattogram” আর অন্যটিতে “Chittagong” হলে কোনো error আসে না, শুধু দুটি NaN আসে, আর পরে sum() NaN বাদ দিয়ে যোগ করে, ফলে মোট সংখ্যা কম দেখায়, কেউ টেরও পায় না। দ্বিতীয়ত, index-এ একই label দুবার থাকলে alignment সারি গুণ করতে পারে। তৃতীয়ত, index নতুনদের জন্য একটি বাড়তি ধারণা: কোনটি কলাম আর কোনটি index, কখন reset_index লাগবে, groupby-র পর index কোথায় গেল, এসব প্রশ্ন অনেক সময় নেয়।
এই কারণে কিছু নতুন library ভিন্ন পথ নিয়েছে। Rust-এ লেখা Polars-এর docs স্পষ্ট বলে যে Polars-এ index নেই; সারি শুধু তার অবস্থান দিয়ে চেনা, আর দুটি টেবিল মেলাতে চাইলে join দিয়ে স্পষ্টভাবে বলতে হয় কোন কলাম ধরে মেলানো হবে। SQL-ও তাই করে: সারির কোনো অন্তর্নিহিত ক্রম বা label নেই, সব মেলানো JOIN ... ON দিয়ে। এই নকশার যুক্তি: যা স্পষ্ট, তা যাচাই করা সহজ। বিপরীত যুক্তি: সময়-ধারা আর label-যুক্ত হিসাবে pandas-এর index অনেক কোড ছোট আর পড়তে সহজ করে। সম্প্রদায়ে এ নিয়ে একমত নেই, আর দুটি পথই সক্রিয়ভাবে ব্যবহার হয়।
ব্যবহারিক মধ্যপথ হলো: অর্থবহ, অনন্য index রাখুন (order_id, তারিখ), যাচাই করুন (is_unique, verify_integrity=True), আর দুটি টেবিল জোড়ার পর NaN গুনুন। জটিল জোড়ায় index-এর ওপর ভরসা না করে merge দিয়ে কলামের নাম বলে দিন, যা পর্ব ২৫-এর বিষয়।
মূল উপলব্ধি: স্বয়ংক্রিয় alignment একটি শক্তিশালী সুবিধা, কিন্তু এটি ভুলও স্বয়ংক্রিয়ভাবে লুকাতে পারে; তাই সুবিধাটি নিন, আর জোড়ার পর প্রতিবার NaN আর সারির সংখ্যা যাচাই করুন।
প্র ০৩pandas বহু বছর লেখার কলাম object dtype-এ রেখেছিল। pandas 3.0 কেন str dtype-কে default করল, আর এর পেছনে Apache Arrow-এর ভূমিকা কী?
pandas-এর শুরুর দিনগুলোতে NumPy ছিল একমাত্র ভিত্তি, আর NumPy-তে পরিবর্তনশীল দৈর্ঘ্যের লেখার জন্য ভালো কোনো dtype ছিল না। NumPy-র নিজের <U dtype প্রতিটি লেখার জন্য সমান জায়গা রাখে, সবচেয়ে লম্বাটির মাপে, যা বাস্তবের ডেটায় অপচয়। তাই pandas লেখার কলামকে রাখত object হিসেবে: একটি array, যার প্রতিটি ঘরে একটি Python string object-এর ঠিকানা। এটি কাজ করত, কিন্তু তিনটি সমস্যা ছিল। প্রতিটি লেখা আলাদা Python object বলে memory বেশি লাগত, আর লেখার ওপর কাজ চলত Python-এর গতিতে। object কলামে যেকোনো কিছু বসানো যেত, তাই dtype দেখে বোঝা যেত না কলামে শুধু লেখা আছে, নাকি সংখ্যা আর list-ও মিশে আছে। আর missing value কখনো None, কখনো NaN, যা অসংগত।
এর মধ্যে ২০১৬ সালে Apache Arrow প্রকল্প শুরু হয়, যার প্রতিষ্ঠাতাদের একজন ছিলেন Wes McKinney নিজে। Arrow হলো memory-তে কলামভিত্তিক ডেটার একটি ভাষা-নিরপেক্ষ মান, যেখানে লেখার কলাম রাখা হয় একটি টানা byte-এর buffer আর একটি offset-এর array হিসেবে, আলাদা Python object ছাড়া। pandas 1.x-এ একটি ঐচ্ছিক "string" dtype আসে, আর পরে Arrow-ভিত্তিক সংস্করণ। দীর্ঘ আলোচনার পর PDEP-14 প্রস্তাব করে যে pandas 3.0-তে লেখার কলাম default হিসেবে একটি নিবেদিত string dtype পাবে, যা PyArrow থাকলে Arrow দিয়ে আর না থাকলে Python object দিয়ে চলবে, আর missing value হবে NaN, যাতে পুরোনো কোড যতটা সম্ভব কম ভাঙে।
লাভ: dtype দেখেই বোঝা যায় কলামে লেখা আছে, Arrow থাকলে memory কম আর অনেক লেখার কাজ দ্রুত, আর Parquet-এর মতো Arrow-ভিত্তিক ফাইলের সঙ্গে আদান-প্রদান সহজ। দাম: dtype == object-এর ওপর নির্ভর করা পুরোনো কোড ভাঙে, একটি লেখার কলামে এখন আর সংখ্যা বসানো যায় না যেভাবে আগে যেত, আর কিছু খুঁটিনাটি আচরণ বদলেছে। বড় একটি library-তে default বদলানো তাই বহু বছরের প্রস্তুতি, সতর্কবার্তা আর ঐচ্ছিক পর্যায়ের পর হয়।
মূল উপলব্ধি: str dtype হলো “যেকোনো কিছু রাখা যায়” থেকে “ঠিক কী আছে তা জানা যায়”-এর দিকে যাত্রা; একটি dtype যত স্পষ্ট, ভুল তত আগে ধরা পড়ে, আর Arrow সেই স্পষ্টতাকে দ্রুতও করেছে।
প্র ০৪একটি ফাঁকা ঘর পুরো পূর্ণসংখ্যার কলামকে float বানিয়ে দেয়। এই নকশা কেন, এর বিপদ কোথায়, আর Int64 ও pd.NA কীভাবে সমাধানের চেষ্টা করে?
কম্পিউটারের একটি 64-বিটের পূর্ণসংখ্যায় প্রতিটি বিট-নকশা একটি বৈধ সংখ্যা; “মান নেই” বোঝানোর জন্য কোনো নকশা খালি নেই। Floating-point-এর জগতে কিন্তু IEEE 754 মান (১৯৮৫) NaN নামের বিশেষ নকশা রেখেছে, মূলত 0/0-এর মতো অসংজ্ঞায়িত হিসাবের ফল বোঝাতে। pandas শুরু থেকে এই NaN-কেই “missing” হিসেবে ব্যবহার করেছে, কারণ এটি NumPy-তে বিনা খরচে পাওয়া যেত আর সব float হিসাবে স্বাভাবিকভাবে ছড়িয়ে পড়ে। ফলে একটি পূর্ণসংখ্যার কলামে একটি ফাঁকা ঘর এলে pandas পুরো কলামকে float64 বানায়, যাতে NaN বসানো যায়।
এর বিপদ তিনটি। প্রথমত, 2^53-এর চেয়ে বড় পূর্ণসংখ্যা float64-এ ঠিকভাবে ধরা যায় না, তাই বড় ID বা ব্যাংক হিসাব নম্বর float হলে শেষের অঙ্ক নিঃশব্দে বদলে যেতে পারে। দ্বিতীয়ত, 2 ছাপা হয় 2.0, আর পরে কেউ সেই কলাম ফাইলে লিখলে অন্য সিস্টেম তাকে দশমিক সংখ্যা ভাবে। তৃতীয়ত, NaN-এর অর্থ দুটি: “তথ্য নেই” আর “অসংজ্ঞায়িত হিসাব”। একই চিহ্নে দুই অর্থ মিশে যায়।
সমাধানের চেষ্টা হিসেবে pandas 0.24 (২০১৯) থেকে nullable integer dtype, Int64, যোগ হয়েছে, আর pandas 1.0 (২০২০) থেকে আলাদা missing চিহ্ন pd.NA. ধারণাটি পুরোনো ডেটাবেসের মতো: মানের array-এর পাশে একটি আলাদা bool mask, কোন ঘরে মান আছে আর কোনটিতে নেই। SQL-এর NULL অনেক আগে থেকেই এভাবে কাজ করে, আর Arrow-ও প্রতিটি কলামে একটি validity bitmap রাখে। এর দাম হলো বাড়তি mask-এর memory, আর কিছু অভ্যাস বদল: pd.NA-এর সঙ্গে তুলনা True বা False না দিয়ে <NA> দেয়, যেমন SQL-এ NULL. pandas 3-এ এখনো default NumPy-র int64; nullable dtype সর্বত্র default করার প্রশ্নটি নিয়ে উন্নয়ন দলে আলোচনা চলছে, আর ভবিষ্যতে কী হবে তা লেখার সময় নিশ্চিত নয়।
বাজারঘরের মতো টাকার হিসাবে ব্যবহারিক নিয়ম: পয়সা পূর্ণসংখ্যায় রাখুন, ফাঁকা থাকতে পারলে Int64 দিন, আর কোনো কলাম হঠাৎ float64 হয়ে গেলে কোথায় NaN ঢুকেছে খুঁজুন।
মূল উপলব্ধি: NaN দিয়ে missing বোঝানো ছিল একটি সুবিধাজনক আপস, যার দাম পূর্ণসংখ্যার নির্ভুলতা; Int64 আর pd.NA “মান” আর “মান আছে কি না” আলাদা রেখে সেই আপস ভাঙার চেষ্টা।
প্র ০৫Excel-এ জিনের নাম তারিখ হয়ে যাওয়ার ঘটনা কী শেখায়? spreadsheet থেকে কোড-ভিত্তিক টেবিলে যাওয়ার যুক্তি কতটা শক্ত, আর কোথায় spreadsheet-ই ভালো?
জীববিজ্ঞানে কিছু জিনের নাম দেখতে তারিখের মতো, যেমন SEPT2 বা MARCH1. Excel-এ এমন নাম লিখলে বা CSV খুললে Excel নিজে থেকে এগুলোকে তারিখ ভেবে “2-Sep” বানিয়ে দেয়। ২০১৬ সালে Ziemann, Eren ও El-Osta “Genome Biology” পত্রিকায় একটি সমীক্ষা প্রকাশ করেন, যেখানে জিনের তালিকাসহ Excel-এর সম্পূরক ফাইল থাকা প্রকাশিত প্রবন্ধগুলোর প্রায় এক-পঞ্চমাংশে এমন রূপান্তরের ভুল পাওয়া যায় বলে তাঁরা জানান। সমস্যাটি এত বিস্তৃত ছিল যে ২০২০ সালে মানব জিনের নামকরণ কমিটি (HGNC) কিছু জিনের নামই বদলে দেয়, যেমন SEPT1 হয় SEPTIN1, যাতে spreadsheet আর তাদের তারিখ না ভাবে।
ঘটনাটির শিক্ষা হলো, spreadsheet প্রতিটি ঘরের ধরন নিজে আন্দাজ করে, ঘর ধরে, আর সেই আন্দাজ নিঃশব্দে হয়। pandas-এ dtype কলাম ধরে, আর তা দেখা যায় (dtypes, info()); তারিখে রূপান্তর হয় শুধু আপনি pd.to_datetime বা parse_dates বললে। তার ওপর pandas-এর প্রতিটি ধাপ কোডে লেখা, তাই অন্য কেউ একই ফাইল থেকে একই ফল আবার তৈরি করতে পারেন, যা পর্ব ১২-এর পুনরুৎপাদনযোগ্যতার মূল কথা। spreadsheet-এ কেউ হাতে একটি ঘর বদলালে তার কোনো চিহ্ন থাকে না। Hadley Wickham-এর ২০১৪ সালের প্রবন্ধ “Tidy Data” এই কোড-ভিত্তিক দৃষ্টিভঙ্গিকে একটি নিয়মে বাঁধে: প্রতিটি চলক একটি কলাম, প্রতিটি পর্যবেক্ষণ একটি সারি, যা বাজারঘরের অর্ডারের টেবিল ঠিক মেনে চলে।
তবু spreadsheet-কে হেয় করার কারণ নেই। ছোট হিসাব, দ্রুত দেখা, যাঁরা কোড লেখেন না তাঁদের সঙ্গে ভাগাভাগি, আর হাতে ডেটা এন্ট্রিতে এটি অতুলনীয়। pandas-এরও নিজস্ব নিঃশব্দ ভুল আছে, যেমন অংশ ২-এর alignment-এর NaN বা CSV পড়ার সময় শূন্য দিয়ে শুরু হওয়া ফোন নম্বর সংখ্যা হয়ে যাওয়া। ব্যবহারিক পথ হলো দুটির মিলন: spreadsheet থেকে আসা ডেটা pandas-এ পড়ার সময় dtype স্পষ্ট বলে দেওয়া, প্রথম নজরের পাঁচ কাজ দিয়ে যাচাই করা, আর বিশ্লেষণ কোডে রাখা।
মূল উপলব্ধি: যন্ত্র যখন নিঃশব্দে ধরন আন্দাজ করে, তখন ভুলও নিঃশব্দে ঢোকে; pandas-এর সুবিধা কোনো জাদু নয়, বরং dtype আর প্রতিটি ধাপ দৃশ্যমান রাখা, যাতে ভুল ধরা পড়ার সুযোগ থাকে।
অনুশীলন
প্রথমটি হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লেখার।
-
হিসাব — দুই গুদামের মজুত: গুদাম ১-এ
g1 = pd.Series([40, 15], index=["ঢাকা", "খুলনা"]), গুদাম ২-এg2 = pd.Series([10, 25, 5], index=["খুলনা", "সিলেট", "ঢাকা"])বস্তা চাল আছে। (ক)g1 + g2-এর প্রতিটি মান আর dtype লিখুন। (খ)g1.add(g2, fill_value=0)? (গ) দুটিরsum()কত, আর পার্থক্যের কারণ কী? (ঘ) একটি অর্ডারে qty 3, price_paisa 8500 হলে total_paisa আর টাকা কত?(ক) label-এর মিলন: খুলনা, ঢাকা, সিলেট (Unicode ক্রমে)। খুলনা 15 + 10 = 25, ঢাকা 40 + 5 = 45, সিলেট শুধু g2-তে, তাই NaN. dtype float64.
(খ) খুলনা 25.0, ঢাকা 45.0, সিলেট 0 + 25 = 25.0; dtype তখনো float64.
(গ)
(g1 + g2).sum()= 70.0, কারণsumdefault-এ NaN বাদ দেয়;fill_value-সহ 95.0. আসল মোট মজুত 40 + 15 + 10 + 25 + 5 = 95 বস্তা। প্রথমটিতে সিলেটের ২৫ বস্তা নিঃশব্দে হারিয়েছে, আর কোনো error আসেনি।(ঘ) 3 × 8500 = 25500 পয়সা = ২৫৫ টাকা। ডেস্কটপে pandas 3.0.2-এ (ক)–(গ) চালিয়ে ঠিক এই সংখ্যাগুলোই এসেছে।
-
আউটপুট অনুমান করুন: চালানোর আগে তিনটি
print-এর আউটপুট লিখুন।df = pd.DataFrame({"qty": [2, 1, 3], "price_paisa": [25000, 14000, 21000]}, index=[10, 20, 30]) df2 = df.assign(total=df["qty"] * df["price_paisa"]) print(df.shape, df2.shape) print(df2.loc[20, "total"], df2.iloc[0]["total"]) s = pd.Series([1, 1, 1], index=[0, 10, 20]) print((df["qty"] + s).tolist())(3, 2) (3, 3) 14000 50000 [nan, 3.0, 2.0, nan]assignনতুন টেবিল দেয়, তাইdfএখনো (3, 2), আরdf2-তে একটি বাড়তি কলাম।loc[20, "total"]label 20-এর সারি: 1 × 14000.iloc[0]প্রথম অবস্থান, label 10: 2 × 25000. শেষ লাইনে alignment: index-এর মিলন 0, 10, 20, 30. label 0 শুধুs-এ, 30 শুধুdf-এ, তাই দুটিই NaN; 10-এ 2 + 1 = 3, 20-এ 1 + 1 = 2, আর NaN-এর কারণে সব float. ডেস্কটপে pandas 3.0.2-এ ঠিক এই আউটপুট এসেছে। -
নকশা ও সমালোচনা: একটি AI সহকারী “প্রতিটি অর্ডারের মোট দাম টাকায় যোগ করো আর একটি নতুন অর্ডার জুড়ে দাও” চাওয়ায় নিচের কোড দিয়েছে। অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো সংস্করণ লিখুন।
def add_totals(orders, new_order): orders["total"] = orders.qty * orders.price_paisa / 100 orders = orders.append(new_order, ignore_index=True) orders[orders.payment == "COD"]["total"] = 0 orders.drop(columns="price_paisa") return orders- মূল টেবিল বদলায়: প্রথম লাইন ডাকা জায়গার
orders-এ সরাসরি কলাম যোগ করে, একটি side effect.assignদিয়ে নতুন টেবিল দিন। - টাকায় float:
/ 100পয়সাকে float টাকা বানায়; হিসাব পয়সায় পূর্ণসংখ্যায় রাখুন, টাকা শুধু ছাপায়। appendনেই: pandas 2.0 থেকেDataFrame.appendসরানো, তাইAttributeError. লাগবেpd.concat.- শিকলবাঁধা লেখা:
orders[...]["total"] = 0pandas 3-এ কখনো মূলকে বদলায় না। আর COD অর্ডারের দাম শূন্য করাই ভুল ব্যবসায়িক যুক্তি; দরকার হলে আলাদাprepaidকলাম। drop-এর ফল ফেলে দেওয়া: ফল রাখা হয়নি, তাই কলাম থেকে যায়।- বিন্দু দিয়ে কলাম আর যাচাই নেই:
orders.qtyএড়ানো ভালো; নতুন অর্ডারেরorder_idপুনরাবৃত্তি কি না যাচাই নেই।
def add_totals(orders: pd.DataFrame, new_order: dict) -> pd.DataFrame: out = ( pd.concat([orders, pd.DataFrame([new_order])], ignore_index=True) .assign(total_paisa=lambda d: d["qty"] * d["price_paisa"], prepaid=lambda d: d["payment"] != "COD") .drop(columns=["price_paisa"]) ) assert out["order_id"].is_unique, "order_id পুনরাবৃত্তি" return out - মূল টেবিল বদলায়: প্রথম লাইন ডাকা জায়গার
-
কোড লিখুন — বাজারঘরের ছোট রিপোর্ট: অংশ ৮-এর ৮টি অর্ডারের
ordersথেকে, মূল টেবিল না বদলে, (ক)total_paisaআরprepaidকলাম যোগ করুন; (খ) আগে বিক্রি হওয়া অর্ডারের ভাগ আর payment অনুযায়ী অর্ডারের ভাগ ছাপুন; (গ) মোট দামে সেরা ৩টি অর্ডার (order_id, district, total_paisa); (ঘ) কয়টি আলাদা জেলা, আর আগে বিক্রি হওয়া অর্ডারের মোট টাকা।out = orders.assign( total_paisa=lambda d: d["qty"] * d["price_paisa"], prepaid=lambda d: d["payment"] != "COD", ) print("prepaid share:", out["prepaid"].mean()) print(out["payment"].value_counts(normalize=True)) top3 = out.sort_values("total_paisa", ascending=False).head(3) print(top3[["order_id", "district", "total_paisa"]]) print("districts:", out["district"].nunique()) print(out.loc[out["prepaid"], "total_paisa"].sum() / 100)ডেস্কটপে pandas 3.0.2-এ সত্যিকারের run-এর আউটপুট:
prepaid share: 0.5 payment COD 0.500 bKash 0.375 card 0.125 Name: proportion, dtype: float64 order_id district total_paisa 2 1003 ঢাকা 63000 0 1001 ঢাকা 50000 3 1004 সিলেট 42500 districts: 5 1025.0bool কলামের
mean()True-র ভাগ দেয়: ৮টির ৪টি, 0.5. সেরা তিনটির পাশে পুরোনো RangeIndex label (2, 0, 3) রয়ে গেছে, যা দেখায় সাজানোর পরও label সারির সঙ্গে থাকে। শেষ লাইনেরloc[শর্ত, কলাম]একটি boolean mask দিয়ে সারি ছাঁকে, পর্ব ২২-এর একটি আগাম স্বাদ; ফল ১,০২৫ টাকা, অংশ ৫-এর হাতের হিসাবের সঙ্গে মেলে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নতুন অর্ডারের টেবিল হাতে পেয়ে বিশ্লেষণ শুরুর আগের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 👀
head()দিয়ে কয়েকটি সারি আর কলামের নাম দেখা - 📐
shapeদেখে প্রত্যাশিত সারি-কলাম মিলিয়ে নেওয়া - 🧬
info()দিয়ে dtype আর ফাঁকা ঘর যাচাই - 📊
describe()দিয়ে সংখ্যার পরিসর আর অসম্ভব মান খোঁজা - 🏷️
value_counts()দিয়ে লেখার কলামের মান আর বানানভেদ দেখা - 🔑 অর্থবহ ID-কে
set_indexকরেis_uniqueযাচাই
a = pd.Series([1, 2], index=["x", "y"]) আর b = pd.Series([10, 20], index=["y", "z"]). (a + b).tolist() কী দেয়?
- [11, 22]
- [12.0]
- [nan, 12.0, nan]
- [1.0, 12.0, 20.0]
একটি DataFrame df-এর shape (100, 5). df[["qty"]]-এর ধরন আর shape কী?
- Series, (100,)
- DataFrame, (100, 1)
- DataFrame, (1, 100)
- KeyError
pandas-এ pd.Series([3, 7, None])-এর dtype কী, আর কেন?
- int64, কারণ সব মান পূর্ণসংখ্যা
- Int64, কারণ pandas নিজে nullable dtype বেছে নেয়
- object, কারণ None একটি Python object
- float64, কারণ ফাঁকা ঘরে NaN বসে, যা float
pandas 3-এ orders.drop(columns="price_paisa") লেখার পর ফল কোনো নামে রাখা হলো না। orders-এর কী হবে?
- কিছুই বদলাবে না; price_paisa কলাম থেকে যাবে
- price_paisa কলামটি মুছে যাবে
- KeyError হবে
- orders হয়ে যাবে None
pandas 3.0-তে pd.Series(["COD", "bKash"]).dtype ছাপলে কী দেখা যায়?
- object
- category
- str
- <U5
একটি DataFrame-এ order_id একটি সাধারণ কলাম, আর index হলো RangeIndex 0–7. df.set_index("order_id") কী করে?
- order_id-এর মানগুলো 0 থেকে নতুন করে নম্বর দেয়
- order_id-কে index বানানো একটি নতুন DataFrame দেয়, কলাম একটি কমে
- df-কে জায়গায় বদলে দেয়, কিছু ফেরত দেয় না
- order_id কলাম হিসেবেও থাকে আর index-এও যায়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Series = মান + index; DataFrame = একই index ভাগ করা কয়েকটি Series. প্রতিটি কলামের নিজের dtype, তাই এক টেবিলে লেখা, সংখ্যা, bool আর তারিখ।
- হিসাব label ধরে মেলে, অবস্থান ধরে নয়। একদিকে না থাকা label-এ NaN আর float64;
fill_valueদেওয়ার আগে ভাবুন “০” সত্য কি না। - প্রথম নজরের পাঁচ কাজ:
head,shape,info(),describe(),value_counts(), আর ID অনন্য কি নাnuniqueবাis_uniqueদিয়ে। - method-এর ফল রাখুন।
drop,rename,set_index,assignনতুন টেবিল দেয়; শিকলবাঁধা লেখা pandas 3-এ কখনো কাজ করে না, লিখতেlocএক ধাপে। - pandas 3-এ লেখা
str, তারিখdatetime64[us], missing-সহ পূর্ণসংখ্যায়Int64; টাকা পয়সায় পূর্ণসংখ্যা, টাকা শুধু ছাপায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২১ · ডেটা পড়া আর লেখা — CSV, Excel, JSON, SQL আর Parquetপরবর্তী পর্বফাইল আর ডেটাবেস থেকে DataFrame পড়া আর লেখা।
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ১৪ · Indexing আর slicing — boolean mask, fancy index, view আর copyঝালাইview, copy আর শিকলবাঁধা লেখার NumPy-র রূপ।
- Python · dictঝালাইDataFrame বানানোর সবচেয়ে চেনা উপকরণ।
- DBMS আর SQL · SELECT-এর মূল কথাপাশের ট্র্যাকটেবিল থেকে কলাম আর সারি বাছাইয়ের SQL রূপ।
- DBMS আর SQL · Relational modelগভীরে যানটেবিল, key আর schema-র তত্ত্ব।