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

pandas-এর শুরু — Series, DataFrame আর index

Getting Started with pandas — Series, DataFrame, the Index, Label Alignment and pandas 3 dtypes

Taught in Bangla, this lesson introduces pandas Series and DataFrame, shows how arithmetic aligns on index labels and creates NaN, builds DataFrames from dicts, records and NumPy arrays, and inspects a table with head, info, describe and value_counts. It covers columns, set_index, pandas 3 dtypes such as str and Int64, Copy-on-Write, method chaining with assign, and common beginner mistakes, all run with pandas 3.0.2.

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

  • 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

DataFrame-এর গঠন: index, columns, Series আর dtype orders · shape (4, 4) columns (একটি Index) districtqtyprice_paisapayment index 0123 ঢাকা225000COD সিলেট58500card খুলনা125000bKash ঢাকা46000COD strint64int64str প্রতিটি কলামের নিজের dtype orders["qty"] — একটি Series (মান + index) একটি সারি = একটি অর্ডার (label 2) index: সব কলাম একই label ভাগ করে ছবির লেখা English শব্দ = কোডের আসল নাম
ছবি ২০.১ — বাজারঘরের চারটি অর্ডারের একটি ছোট DataFrame. ওপরের বেগুনি সারি কলামের নাম (pandas-এ এটিও একটি Index object), বাঁয়ের নীল অংশ সারির index, সবুজ দাগে ঘেরা 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, dtype str.
  • 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()
মান (Watch)
label
dtype
  1. দুটি Series. অক্টোবরে তিন জেলা, নভেম্বরে তিন জেলা, কিন্তু জেলাগুলো পুরোপুরি এক নয় (সিলেট শুধু অক্টোবরে, খুলনা শুধু নভেম্বরে), আর ক্রমও আলাদা।
  2. label-এর মিলন। pandas দুই index-এর সব label নিয়ে একটি নতুন index বানায় (union), আর সম্ভব হলে সাজায়: খুলনা, চট্টগ্রাম, ঢাকা, সিলেট. বাংলা লেখা Unicode-এর ক্রমে সাজে, অভিধানের ক্রমে নয়।
  3. যেখানে দুদিকেই আছে। ঢাকা: 120 + 130 = 250; চট্টগ্রাম: 80 + 95 = 175. nov-এ ঢাকা দ্বিতীয় অবস্থানে ছিল, তবু ঠিক ঢাকার সঙ্গেই যোগ হয়েছে।
  4. যেখানে একদিকে নেই। খুলনার অক্টোবরের মান নেই, সিলেটের নভেম্বরের মান নেই; “কিছু না + 60” pandas-এর কাছে অজানা, তাই NaN. NaN একটি float, তাই পুরো ফলের dtype int64 থেকে float64 হয়ে গেছে।
  5. fill_value=0. add method-এ বলে দেওয়া যায়, একদিকে label না থাকলে সেদিকের মান 0 ধরা হবে। এখন খুলনা 60, সিলেট 45. dtype তবু float64 থাকে, কারণ মেলানোর ধাপেই NaN জন্মেছিল; পূর্ণসংখ্যা চাইলে পরে astype("int64").
  6. অবস্থান ধরে যোগের ভুল। 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)
মান (Watch)
columns
dtypes
index
  1. একটি সাধারণ dict. তিনটি key, প্রতিটির value একটি list, প্রতিটিতে ৪টি মান। এটি পর্ব ৯-এর (ডেটার জন্য Python) চেনা Python dict, এখনো pandas-এর কিছু নয়।
  2. key হয় কলামের নাম। pd.DataFrame(data) dict-এর key-গুলো dict-এর ক্রমেই কলামের নাম হিসেবে নেয়। df.columns নিজেও একটি Index, dtype str.
  3. প্রতিটি list হয় একটি Series. pandas প্রতিটি list দেখে dtype ঠিক করে: লেখা দেখে str, পূর্ণসংখ্যা দেখে int64. এখানে দৈর্ঘ্যও যাচাই হয়; একটি list-এ ৩টি মান থাকলে ValueError: All arrays must be of the same length.
  4. index যোগ হয়। index না দিলে pandas 0 থেকে শুরু করে একটি RangeIndex দেয়: 0, 1, 2, 3. এটি memory-তে সংখ্যাগুলো রাখে না, শুধু start, stop আর step মনে রাখে, ঠিক Python-এর range-এর মতো।
  5. ফল। ৪টি সারি, ৩টি কলাম, 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()
মান (Watch)
index
কলাম
  1. শুরুর টেবিল। index হলো RangeIndex 0–3, আর order_id অন্য সব কলামের মতো একটি সাধারণ কলাম। এই অবস্থায় o.loc[1005] লিখলে KeyError: 1005, কারণ index-এ 1005 নামে কোনো label নেই।
  2. set_index("order_id"). কলামটি কলামের তালিকা থেকে উঠে index-এর জায়গায় বসে; কলাম এখন দুটি। index-এর একটি নামও হয়েছে, order_id, যা ছাপায় আলাদা লাইনে দেখা যায়। ফল একটি নতুন DataFrame; o অক্ষত।
  3. label দিয়ে একটি সারি। oi.loc[1005] রসিদ নম্বর ধরে খুলনার অর্ডারটি দেয়। একটি সারি তুললে ফল একটি Series, যার index এখন কলামের নামগুলো; লেখা আর সংখ্যা মিশে আছে বলে dtype object.
  4. অবস্থান দিয়ে একই সারি। oi.iloc[2] তৃতীয় সারি, যা ঘটনাক্রমে একই অর্ডার। পার্থক্যটি দেখা যায় সাজানোর পর: টেবিল উল্টে সাজালে loc[1005] তখনো খুলনার অর্ডার দেবে, কিন্তু iloc[2] অন্য কিছু দেবে।
  5. 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 বা arrayqty, price_paisaNaN আসে না; একটি ফাঁকা থাকলে পুরো কলাম float64
float64দশমিক সংখ্যা, বা NaN-সহ পূর্ণসংখ্যাগড় ratingNaN
boolTrue/False, তুলনার ফলprepaidNaN বসলে object হয়ে যায়
strলেখার list (pandas 3-এর নতুন default)district, paymentNaN
categoryastype("category")payment: ৩টি মান, হাজারো সারিNaN
datetime64[us]pd.to_datetime(...)order_dateNaT
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)pandasSQL টেবিল
সারির পরিচয়সারির নম্বর (১, ২, …)index (যেকোনো label)primary key; সারির নিজস্ব ক্রম নেই
কলামের ধরনপ্রতিটি ঘর আলাদা; একই কলামে লেখা ও সংখ্যা মিশতে পারেপ্রতিটি কলামে একটি dtypeschema-তে আগে থেকে ঠিক করা
নতুন কলামসূত্র লিখে নিচে টেনে দেওয়া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, এককের নিয়ম (পয়সা), আর কোন পুরোনো পথ চলবে না, সব বলে দিন। আসল ক্রেতার ডেটা নয়, বানানো ছোট উদাহরণ দিন।

আমি Python 3.14 আর pandas 3.0-এ কাজ করছি। আমার একটি DataFrame orders আছে, কলাম: order_id (int64), district (str), qty (int64), price_paisa (int64, দাম পয়সায়), payment (str: "COD", "bKash", "card"). বানানো উদাহরণ: [{"order_id": 1001, "district": "ঢাকা", "qty": 2, "price_paisa": 25000, "payment": "COD"}, ...]। ১. একটি method chain লেখো, যা assign দিয়ে total_paisa = qty * price_paisa আর prepaid = payment != "COD" যোগ করবে, order_id-কে index করবে (verify_integrity=True), আর total_paisa ধরে বড় থেকে ছোট সাজাবে। ২. টাকা হিসাব করবে শুধু ছাপার সময়; ডেটায় সব হিসাব পূর্ণসংখ্যা পয়সায় থাকবে। ৩. মূল orders বদলাবে না: inplace=True, chained assignment (df[a][b] = ...) বা df.append ব্যবহার করবে না। ৪. প্রতিটি ধাপের পর shape কী হবে, এক লাইনে বলো, আর শেষে মোট পয়সা যাচাই করার একটি assert দাও। ৫. কোনো warning চাপা দেবে না। কোনো method বা parameter pandas 3.0-তে আছে কি না নিশ্চিত না হলে বলো; বানিয়ে লিখবে না।

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 আর প্রতিটি ধাপ দৃশ্যমান রাখা, যাতে ভুল ধরা পড়ার সুযোগ থাকে।

অনুশীলন

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

  1. হিসাব — দুই গুদামের মজুত: গুদাম ১-এ 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, কারণ sum default-এ NaN বাদ দেয়; fill_value-সহ 95.0. আসল মোট মজুত 40 + 15 + 10 + 25 + 5 = 95 বস্তা। প্রথমটিতে সিলেটের ২৫ বস্তা নিঃশব্দে হারিয়েছে, আর কোনো error আসেনি।

    (ঘ) 3 × 8500 = 25500 পয়সা = ২৫৫ টাকা। ডেস্কটপে pandas 3.0.2-এ (ক)–(গ) চালিয়ে ঠিক এই সংখ্যাগুলোই এসেছে।

  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-এ ঠিক এই আউটপুট এসেছে।

  3. নকশা ও সমালোচনা: একটি 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"] = 0 pandas 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
  4. কোড লিখুন — বাজারঘরের ছোট রিপোর্ট: অংশ ৮-এর ৮টি অর্ডারের 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.0

    bool কলামের mean() True-র ভাগ দেয়: ৮টির ৪টি, 0.5. সেরা তিনটির পাশে পুরোনো RangeIndex label (2, 0, 3) রয়ে গেছে, যা দেখায় সাজানোর পরও label সারির সঙ্গে থাকে। শেষ লাইনের loc[শর্ত, কলাম] একটি boolean mask দিয়ে সারি ছাঁকে, পর্ব ২২-এর একটি আগাম স্বাদ; ফল ১,০২৫ টাকা, অংশ ৫-এর হাতের হিসাবের সঙ্গে মেলে।

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

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

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

  1. 👀 head() দিয়ে কয়েকটি সারি আর কলামের নাম দেখা
  2. 📐 shape দেখে প্রত্যাশিত সারি-কলাম মিলিয়ে নেওয়া
  3. 🧬 info() দিয়ে dtype আর ফাঁকা ঘর যাচাই
  4. 📊 describe() দিয়ে সংখ্যার পরিসর আর অসম্ভব মান খোঁজা
  5. 🏷️ value_counts() দিয়ে লেখার কলামের মান আর বানানভেদ দেখা
  6. 🔑 অর্থবহ 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-এও যায়

শব্দকোষ

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

মনে রাখুন

  1. Series = মান + index; DataFrame = একই index ভাগ করা কয়েকটি Series. প্রতিটি কলামের নিজের dtype, তাই এক টেবিলে লেখা, সংখ্যা, bool আর তারিখ।
  2. হিসাব label ধরে মেলে, অবস্থান ধরে নয়। একদিকে না থাকা label-এ NaN আর float64; fill_value দেওয়ার আগে ভাবুন “০” সত্য কি না।
  3. প্রথম নজরের পাঁচ কাজ: head, shape, info(), describe(), value_counts(), আর ID অনন্য কি না nunique বা is_unique দিয়ে।
  4. method-এর ফল রাখুন। drop, rename, set_index, assign নতুন টেবিল দেয়; শিকলবাঁধা লেখা pandas 3-এ কখনো কাজ করে না, লিখতে loc এক ধাপে।
  5. pandas 3-এ লেখা str, তারিখ datetime64[us], missing-সহ পূর্ণসংখ্যায় Int64; টাকা পয়সায় পূর্ণসংখ্যা, টাকা শুধু ছাপায়।

পরের পর্বে: হাতে বানানো টেবিল থেকে আসল ফাইলে। CSV, Excel, JSON, SQL আর Parquet থেকে বাজারঘরের অর্ডার পড়া আর লেখা, বাংলা লেখার encoding আর dtype ঠিক রেখে।

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

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