এই পর্বে যা শিখবেন
read_csv-এর যে ডজনখানেক option বাস্তবে সবচেয়ে কাজে লাগে, আর প্রতিটি কোন ভুল ঠেকায়to_csv,to_excel,to_json,to_parquetদিয়ে লেখা — index, sheet,orientআরlines=True- Python-এর নিজস্ব
sqlite3দিয়েto_sqlআরread_sql, parameterized query, আর কাজটি SQL-এ না pandas-এ করবেন - যা পড়লেন তা বিশ্বাস করার আগে যাচাই, আর লেখা-পড়ার round-trip test
- বড় ফাইল টুকরো করে পড়া (
chunksize) আর URL থেকে পড়ার সতর্কতা
১ · ফাইল থেকে DataFrame, DataFrame থেকে ফাইল
বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) একটি সাধারণ সকাল ভাবুন। ডেলিভারির অংশীদার পাঠিয়েছে একটি CSV, হিসাব বিভাগ একটি Excel ফাইল, ওয়েবসাইটের log জমেছে JSON Lines-এ, অর্ডারগুলো আছে একটি ডেটাবেসে, আর ডেটা দল গতকালের বিশ্লেষণ রেখেছে Parquet-এ। বিশ্লেষণ বা model শেখানোর আগে প্রথম কাজ: সবগুলোকে একই রূপে আনা — pandas-এর DataFrame (পর্ব ২০-এ পরিচয়)।
এই format-গুলোর ভেতরের গঠন — CSV-এর quote, JSON Lines, Parquet-এর কলাম ধরে রাখা, UTF-8 আর বাংলা লেখা — আপনি পর্ব ১০-এ দেখেছেন। এখানে সেগুলো আবার ব্যাখ্যা করা হবে না। এই পর্বের প্রশ্ন ব্যবহারিক: pandas-এর কোন function, কোন option, আর পড়ার পর কী যাচাই।
পাড়ার মুদি দোকানে মাল আসে নানা পথে: কেউ বস্তায়, কেউ কার্টনে, কেউ খোলা ভ্যানে। দোকানি সব মাল একই তাকে সাজান, কিন্তু তোলার আগে গুনে দেখেন — চালান অনুযায়ী বস্তা কয়টি, কোনোটি ভেজা কি না। তাকে তোলা মানে read_*, গুনে দেখা মানে shape আর dtypes যাচাই, আর অন্য দোকানে পাঠাতে আবার বস্তায় ভরা মানে to_*. ভুল চালানে সই করলে পরে হিসাব মেলে না — ডেটাতেও তাই।
pandas-এর পড়া-লেখার পরিবার
Every format has a pd.read_* and a df.to_* — and a check in between
| Format | পড়া | লেখা | বাড়তি package |
|---|---|---|---|
| CSV / TSV | pd.read_csv | df.to_csv | লাগে না |
| Excel (xlsx) | pd.read_excel | df.to_excel | openpyxl |
| JSON / JSON Lines | pd.read_json | df.to_json | লাগে না |
| Parquet | pd.read_parquet | df.to_parquet | pyarrow |
| SQL ডেটাবেস | pd.read_sql | df.to_sql | SQLite-এ লাগে না; অন্যগুলোয় SQLAlchemy ও driver |
নামের ছাঁচটি মনে রাখলে বাকিটা সহজ: পড়ার function থাকে pd-এর ওপর আর নতুন DataFrame ফেরত দেয়; লেখার function থাকে DataFrame-এর ওপর (method) আর একটি ফাইল বা টেবিল তৈরি করে। এই পর্বের সব কোড pandas 3 ধরে লেখা। পাতার ভেতরের Python চলে আপনার ব্রাউজারে (Pyodide, সেখানে pandas 3.0.2); যেগুলো ব্রাউজারে চলে না, সেগুলো একটি ল্যাপটপে সত্যিকারের run — Python 3.14.4, pandas 3.1.1, pyarrow 26.0.1, openpyxl 3.1.5 (লেখার সময়ের version; uv দিয়ে বানানো virtual environment, পর্ব ০৭).
২ · read_csv-এর যে option-গুলো আসলে দরকার
pd.read_csv-এর parameter পঞ্চাশটির মতো। সবগুলো মুখস্থ করার দরকার নেই; বাস্তবের বেশিরভাগ সমস্যা মেটে নিচের ডজনখানেক দিয়ে। প্রতিটির পেছনে একটি প্রশ্ন আছে, যার উত্তর ফাইলের মালিক জানেন কিন্তু pandas জানে না।
| Option | যে প্রশ্নের উত্তর | উদাহরণ |
|---|---|---|
sep | field কোন চিহ্নে আলাদা? | sep=";", tab হলে "\t" |
skiprows | শুরুতে কয়টি বাজে লাইন? | skiprows=1 |
header, names | কলামের নাম কোন লাইনে, নাকি নেই? | header=None, names=[...] |
usecols | সব কলাম কি দরকার? | usecols=["phone", "qty"] |
dtype | কোন কলামের type কী? | dtype={"phone": str} |
parse_dates | কোন কলাম তারিখ? | parse_dates=["date"] |
na_values | “নেই” কীভাবে লেখা? | na_values=["-", "N/A"] |
thousands | হাজারের বিভাজক আছে? | thousands="," |
encoding | byte কোন নিয়মে লেখা? | "utf-8", "utf-8-sig" |
nrows | আগে শুধু কয়েকটি সারি দেখব? | nrows=1000 |
chunksize | ফাইল কি মেমরির চেয়ে বড়? | chunksize=250_000 (অংশ ৮) |
একটি উদাহরণে দেখা যাক। বাজারঘরের ডেলিভারি অংশীদার একটি ফাইল পাঠিয়েছে, যার প্রথম লাইনে একটি মন্তব্য, delimiter সেমিকোলন, দামে হাজারের কমা, “নেই” বোঝাতে কোথাও - কোথাও N/A. (এই পর্বের সব ফোন নম্বর স্পষ্টতই বানানো।) ফাইলটি পাতার ভেতরে রাখা, কোড একে partner_orders.csv নামে পড়ে।
# partner export, 2026-11-01
order_id;date;district;phone;qty;price_tk;payment
1001;2026-10-03;ঢাকা;01700000000;2;1,250;bKash
1002;2026-10-03;খুলনা;01800000001;1;-;COD
1003;2026-10-04;সিলেট;01900000002;3;780;card
1004;2026-10-05;ঢাকা;01500000003;N/A;2,400;bKash
প্রথমে ধাপে ধাপে দেখুন, প্রতিটি option যোগ করলে DataFrame কীভাবে বদলায়। প্রতিটি ধাপের ফল নিচের কোড-বাক্সের সত্যিকারের আউটপুট থেকে নেওয়া।
shape (5, 2)
columns: ['# partner export',
' 2026-11-01']
প্রথম লাইনটিই header হয়ে গেছে
phone qty price_tk 0 1700000000 2.0 1,250 1 1800000001 1.0 - phone int64 · qty float64 price_tk str
phone
0 01700000000
1 01800000001
শুরুর শূন্য ফিরে এসেছে
qty price_tk 0 2.0 1250.0 1 1.0 NaN ← "-" 2 3.0 780.0 3 NaN 2400.0 ← "N/A"
date price_paisa 0 2026-10-03 125000 1 2026-10-03 <NA> 2 2026-10-04 78000 3 2026-10-05 240000
shape (4, 8)
missing: {'qty': 1,
'price_tk': 1,
'price_paisa': 1}
৪টি অর্ডার = ফাইলের ৪টি record ✓
- অনুমানের ফল। কোনো option ছাড়া pandas ধরে নেয় প্রথম লাইনই header আর delimiter কমা। মন্তব্যের লাইনে একটি কমা ছিল, তাই কলাম হলো দুটি:
'# partner export'আর' 2026-11-01'. কোনো error নেই — শুধু অর্থহীন টেবিল। - Delimiter আর বাজে লাইন।
sep=";"আরskiprows=1দিলে ৭টি কলাম ঠিকঠাক আসে। কিন্তু pandas এখনো অনুমান করছে: ফোন নম্বরint64হয়ে শূন্য হারিয়েছে,qtyএকটি ফাঁকা মানের জন্যfloat64, আরprice_tk“1,250” আর “-” থাকায় লেখা (str). - Type বলে দেওয়া।
dtype={"order_id": str, "phone": str}— ফোন নম্বর আর ID গণনার সংখ্যা নয়, পরিচয়। এদের যোগ-বিয়োগের কোনো অর্থ নেই, তাই লেখা হিসেবে রাখাই ঠিক। এখন01700000000অক্ষত। - “নেই” আর হাজারের কমা।
na_values=["-", "N/A"]বলে এই দুটি লেখা মানে missing;thousands=","বলে “1,250” মানে ১২৫০. ফলেprice_tkএখন সংখ্যা (float64), আর দুটি ফাঁকা ঘর NaN. - তারিখ আর পয়সা।
parse_dates=["date"]লেখাকে আসল তারিখে (datetime64[us]) রূপ দেয়, যাতে পরে মাস বা সপ্তাহ ধরে হিসাব করা যায়। টাকাকে পয়সায় নিয়ে পূর্ণসংখ্যা রাখা হলো: ১২৫০ × ১০০ = ১,২৫,০০০; ফাঁকা দামের জায়গায়<NA>. - বিশ্বাসের আগে গোনা। ৪টি সারি, ৮টি কলাম (মূল ৭ +
price_paisa); missing ঠিক সেখানেই, যেখানে ফাইলে-আরN/Aছিল। এই মিল না দেখে পরের ধাপে যাওয়া উচিত নয়।
পাতার Python কোড আপনার ব্রাউজারেই চলে। প্রথমবার “চালান” চাপলে Python আর pandas লোড হতে কয়েক সেকেন্ড লাগে; পরের বার অনেক দ্রুত।
import pandas as pd
pd.set_option("display.width", 60)
f = "partner_orders.csv"
d = pd.read_csv(f) # 1) all defaults
print("defaults:", d.shape, d.columns.tolist())
d0 = pd.read_csv(f, sep=";", skiprows=1) # 2) right separator
c0 = ["phone", "qty", "price_tk"]
print("before:", d0.dtypes[c0].astype(str).to_dict())
print(d0[c0].head(2))
df = pd.read_csv( # 3) say what you know
f, sep=";", skiprows=1, encoding="utf-8",
dtype={"order_id": str, "phone": str},
parse_dates=["date"],
na_values=["-", "N/A"], thousands=",",
)
df["price_paisa"] = (df["price_tk"] * 100).astype("Int64")
cols = ["date", "phone", "qty", "price_paisa"]
print("after :", df.dtypes[cols].astype(str).to_dict())
print(df[cols])
print("shape", df.shape)
print("missing:", df.isna().sum()[lambda s: s > 0].to_dict())আসল আউটপুট (Pyodide-এ, pandas 3.0.2):
defaults: (5, 2) ['# partner export', ' 2026-11-01']
before: {'phone': 'int64', 'qty': 'float64', 'price_tk': 'str'}
phone qty price_tk
0 1700000000 2.0 1,250
1 1800000001 1.0 -
after : {'date': 'datetime64[us]', 'phone': 'str', 'qty': 'float64', 'price_paisa': 'Int64'}
date phone qty price_paisa
0 2026-10-03 01700000000 2.0 125000
1 2026-10-03 01800000001 1.0 <NA>
2 2026-10-04 01900000002 3.0 78000
3 2026-10-05 01500000003 NaN 240000
shape (4, 8)
missing: {'qty': 1, 'price_tk': 1, 'price_paisa': 1}
তিনটি ধাপ, তিন রকম ফল। প্রথম ধাপ কোনো error ছাড়াই একটি অর্থহীন টেবিল দিয়েছে — read_csv-এর সবচেয়ে বিপজ্জনক আচরণ এটিই: সে প্রায় কখনো “পারলাম না” বলে না। দ্বিতীয় ধাপে কলাম ঠিক, কিন্তু type অনুমানের: ফোন নম্বর int64, দাম str. তৃতীয় ধাপে যা জানা ছিল সব বলে দেওয়া হয়েছে, আর ফল ফাইলের সঙ্গে হুবহু মেলে।
দুটি সূক্ষ্ম বিষয় লক্ষ করুন। এক, pandas 3-এ লেখার কলামের default dtype নতুন str (আগের version-এ ছিল object); পুরোনো টিউটোরিয়ালের আউটপুটে তাই object দেখবেন। দুই, qty এখনো float64, কারণ সাধারণ int64 কলামে NaN রাখা যায় না; price_paisa-তে আমরা pandas-এর nullable Int64 (বড় হাতের I) ব্যবহার করেছি, যা পূর্ণসংখ্যার সঙ্গে <NA> রাখতে পারে। Missing মান কী করবেন — বাদ দেবেন না ভরাবেন — সেটি পর্ব ২৩-এর (ডেটা পরিষ্কার) বিষয়।
যে option-গুলো উদাহরণে আসেনি
header আর names লাগে যখন ফাইলে কলামের নামই নেই, বা নাম এত খারাপ যে বদলাতে চান। header=None বলে “প্রথম লাইনও ডেটা”, আর names দেয় নিজের নাম। usecols শুধু দরকারি কলাম পড়ে — ৫০ কলামের ফাইলের ৩টি দরকার হলে মেমরি আর সময় দুটোই বাঁচে। nrows দিয়ে বড় ফাইলের প্রথম কয়েক হাজার সারি পড়ে আগে চেহারা দেখে নেওয়া যায়। একই ফাইলে ল্যাপটপে একটি সত্যিকারের run:
d2 = pd.read_csv("partner_orders.csv", sep=";", skiprows=2,
header=None,
names=["id", "date", "district", "phone",
"qty", "price", "pay"],
usecols=["id", "phone", "price"],
nrows=2, dtype={"phone": str})
print(d2) id phone price
0 1001 01700000000 1,250
1 1002 01800000001 -
এখানে skiprows=2 মন্তব্য আর আসল header দুটোই বাদ দিয়েছে, তাই নাম এসেছে names থেকে। thousands আর na_values দেওয়া হয়নি বলে price আবার লেখা — option-গুলো একে অপরের বদলি নয়, প্রতিটি আলাদা প্রশ্নের উত্তর।
Default-এ কোন লেখা NaN হয়? pandas নিজে থেকেই কয়েকটি লেখাকে missing ধরে: ফাঁকা ঘর, NA, N/A, n/a, null, NaN, None ইত্যাদি (পুরো তালিকা docs-এ na_values-এর নিচে)। কিন্তু -, ০ বা “নেই” নয়। উল্টো বিপদও আছে: কোনো জেলার কোড সত্যিই “NA” হলে সেটি নীরবে NaN হয়ে যাবে; তখন keep_default_na=False দিয়ে নিজের তালিকা দিন।
৩ · লেখা — to_csv আর বাড়তি index কলামের ফাঁদ
পড়ার উল্টো কাজ df.to_csv(path). এখানে একটি পরিচিত ফাঁদ: প্রতিটি DataFrame-এর একটি index থাকে (বাঁ দিকের ০, ১, ২ …), আর to_csv default-এ সেটিকেও একটি কলাম হিসেবে লেখে। আবার পড়লে pandas সেই কলামকে সাধারণ ডেটা ভাবে আর নাম দেয় Unnamed: 0. প্রতিবার পড়া-লেখায় এমন একটি করে কলাম জমতে থাকে — অনেক প্রকল্পের ফাইলে Unnamed: 0.1, Unnamed: 0.2 দেখা যায় ঠিক এই কারণে।
import pandas as pd
df = pd.DataFrame({"order_id": [1001, 1002, 1003],
"qty": [2, 1, 3]})
df.to_csv("a.csv") # default: index=True
with open("a.csv") as fh:
print(fh.read())
back = pd.read_csv("a.csv")
print("read 1:", back.columns.tolist())
back.to_csv("a.csv") # write it again...
print("read 2:", pd.read_csv("a.csv").columns.tolist())
df.to_csv("b.csv", index=False) # the fix
back2 = pd.read_csv("b.csv")
print("index=False:", back2.columns.tolist(), df.equals(back2))
# but a meaningful index must be kept: reset it first
s = df.assign(d=["Dhaka", "Khulna", "Dhaka"]).groupby("d")["qty"].sum()
print(s.to_frame().to_csv(index=False).split())
print(s.reset_index().to_csv(index=False).split())আসল আউটপুট (Pyodide-এ):
,order_id,qty
0,1001,2
1,1002,1
2,1003,3
read 1: ['Unnamed: 0', 'order_id', 'qty']
read 2: ['Unnamed: 0.1', 'Unnamed: 0', 'order_id', 'qty']
index=False: ['order_id', 'qty'] True
['qty', '5', '1']
['d,qty', 'Dhaka,5', 'Khulna,1']
ফাইলের প্রথম লাইন শুরু হয়েছে একটি কমা দিয়ে — নামহীন index কলাম। প্রথমবার পড়লে সেটি Unnamed: 0, আবার লিখে পড়লে আরেকটি Unnamed: 0.1. index=False দিলে ফাইলে শুধু আসল কলাম থাকে, আর ফিরে পড়া DataFrame মূলের সঙ্গে হুবহু মেলে (equals → True).
শেষ দুই লাইন উল্টো দিকের সতর্কতা। groupby-এর পর জেলার নাম থাকে index-এ (পর্ব ২৪-এ বিস্তারিত)। তখন অন্ধভাবে index=False দিলে জেলার নামই হারায় — শুধু 5 আর 1 থাকে, কোনটি কোন জেলার বোঝার উপায় নেই। প্রতিকার: আগে reset_index() দিয়ে index-কে সাধারণ কলাম বানানো। নিয়মটি তাই “সবসময় index=False” নয়, বরং “index-এ অর্থবহ কিছু থাকলে সেটিকে কলাম বানিয়ে লিখুন”। অন্য পথ হলো পড়ার সময় index_col=0 বলা।
to_csv-এর আরও দুটি কাজের option: Excel ব্যবহারকারী বাংলা লেখা খুলবেন জানলে encoding="utf-8-sig" (কেন, তা পর্ব ১০-এ), আর দশমিক সংখ্যার লম্বা লেজ কাটতে float_format="%.2f". তবে টাকা পয়সায় পূর্ণসংখ্যা হিসেবে রাখলে দ্বিতীয়টির দরকারই পড়ে না।
৪ · Excel — sheet, ExcelWriter আর openpyxl
অফিসের ডেটার বড় অংশ এখনো Excel-এ। pandas xlsx পড়া-লেখার জন্য openpyxl package ব্যবহার করে, যা আলাদা install করতে হয় (uv pip install openpyxl). একটি xlsx ফাইলে কয়েকটি sheet থাকতে পারে, তাই Excel-এর API-তে নতুন দুটি ধারণা:
pd.read_excel(path, sheet_name=...)— নাম ("orders"), ক্রমিক নম্বর (0= প্রথম sheet), বাNone= সব sheet একসঙ্গে, নাম থেকে DataFrame-এর একটি dict হিসেবে।pd.ExcelWriter— একটি ফাইলে একাধিক sheet লিখতে। প্রতিটিto_excelএকই writer-এ আলাদাsheet_nameদিয়ে লেখে;withব্লক শেষ হলে ফাইল সেভ হয়।
openpyxl ব্রাউজারের Pyodide-এ নেই, তাই এটি ল্যাপটপে চালানো। সত্যিকারের run:
import pandas as pd, openpyxl
orders = pd.DataFrame({
"order_id": [1001, 1002, 1003, 1004],
"district": ["ঢাকা", "খুলনা", "সিলেট", "ঢাকা"],
"phone": ["01700000000", "01800000001", "01900000002", "01500000003"],
"qty": [2, 1, 3, 1],
})
summary = orders.groupby("district", as_index=False)["qty"].sum()
with pd.ExcelWriter("out/shop.xlsx") as xw: # one file, two sheets
orders.to_excel(xw, sheet_name="orders", index=False)
summary.to_excel(xw, sheet_name="summary", index=False)
print("pandas", pd.__version__, "| openpyxl", openpyxl.__version__)
book = pd.read_excel("out/shop.xlsx", sheet_name=None) # every sheet
print({name: frame.shape for name, frame in book.items()})
o = pd.read_excel("out/shop.xlsx", sheet_name="orders",
dtype={"phone": str}, usecols=["order_id", "phone"])
print(o.head(2))
print(pd.read_excel("out/shop.xlsx", sheet_name=1))~/bazarghar-ai$ python excel_io.py
pandas 3.1.1 | openpyxl 3.1.5
{'orders': (4, 4), 'summary': (3, 2)}
order_id phone
0 1001 01700000000
1 1002 01800000001
district qty
0 খুলনা 1
1 ঢাকা 3
2 সিলেট 3
লক্ষ করার মতো তিনটি বিষয়। sheet_name=None একটি dict দিয়েছে — দুটি sheet, প্রতিটির নিজের আকার। read_excel-ও read_csv-এর মতো dtype, usecols, nrows, skiprows বোঝে; ফোন নম্বরে dtype={"phone": str} না দিলে xlsx-এর ভেতরে লেখা হিসেবে থাকলেও pandas তাকে সংখ্যা বানাতে পারে (পর্ব ১০-এর run-এ ঠিক এটি দেখা গেছে)। আর summary sheet-এ জেলাগুলো এসেছে Unicode-এর বর্ণক্রমে (খ, ঢ, স), কারণ groupby default-এ key সাজিয়ে দেয়।
Excel থেকে পড়ার বাস্তব ঝামেলা সাধারণত API-তে নয়, ফাইলের চেহারায়: ওপরে অফিসের নাম আর তারিখের কয়েকটি লাইন (skiprows), merge করা header cell, নিচে “মোট” সারি (পড়ার পর বাদ দিতে হয়), আর রং দিয়ে বোঝানো অর্থ, যা pandas দেখেই না। মানুষের জন্য সাজানো ফাইল আর যন্ত্রের জন্য ডেটা — দুটি আলাদা জিনিস। সম্ভব হলে উৎসের কাছ থেকে একটি সমতল sheet চেয়ে নিন। to_excel-এও index-এর একই ফাঁদ আছে: index=False না দিলে ফিরে পড়লে Unnamed: 0 কলাম আসে (একই ল্যাপটপে যাচাই করা)।
৫ · JSON — orient আর lines=True
একটি টেবিলকে JSON-এ সাজানোর কয়েকটি উপায় আছে: সারি ধরে record-এর list, কলাম ধরে dict, নাকি কলামের নাম আর ডেটা আলাদা? to_json-এর orient parameter ঠিক এটিই বেছে নেয়। একটি এক-সারির DataFrame বিভিন্ন orient-এ (ল্যাপটপে সত্যিকারের run, তারিখ কলাম বাদ দিয়ে):
orient | আকার | কখন |
|---|---|---|
"records" | [{"order_id":1001,"phone":"01700000000"}] | API, JSON Lines, অন্য ভাষার প্রোগ্রাম |
"split" | {"columns":[...],"index":[0],"data":[[...]]} | কলামের নাম একবারই, ছোট ফাইল |
"index" | {"0":{"order_id":1001,...}} | index-এর মান দিয়ে খোঁজা |
"columns" | {"order_id":{"0":1001},...} | DataFrame-এর default |
"table" | {"schema":{...},"data":[...]} | সঙ্গে schema (type) লিখে রাখা |
AI আর ডেটার কাজে সবচেয়ে বেশি লাগে orient="records" সঙ্গে lines=True — অর্থাৎ JSON Lines: প্রতিটি লাইনে একটি record. lines=True শুধু "records"-এর সঙ্গেই চলে; অন্য orient দিলে pandas পরিষ্কার error দেয়: ValueError: 'lines' keyword only valid when 'orient' is records.
JSON-এর ধরন ছোট — string, সংখ্যা, true/false, null — তাই ফেরত পড়ার সময় read_json-কেও অনুমান করতে হয়, আর এর অনুমান read_csv-এর চেয়েও আগ্রাসী। দুটি আচরণ জানা জরুরি (ল্যাপটপে pandas 3.1.1-এ যাচাই করা, পাতার ব্রাউজারেও অংশ ৬-এর কোডে দেখবেন):
- সংখ্যার মতো লেখা সংখ্যা হয়ে যায়। ফাইলে
"phone":"01700000000"quote-সহ লেখা থাকলেও defaultread_jsonকলামটিকেint64বানায় — শূন্য হারায়। প্রতিকার পরিচিত:dtype={"phone": str}. - নাম দেখে তারিখ।
date,datetime,modifiedনামের কলাম, কিংবা_atবা_time-এ শেষ হওয়া নামের কলামকেread_jsonতারিখে রূপ দেওয়ার চেষ্টা করে (keep_default_dates). সুবিধাজনক, কিন্তু “created_at” নামে একটি ID কলাম থাকলে বিপদ;convert_datesদিয়ে নিজে বলে দিন।
তারিখ লেখার সময় date_format="iso" স্পষ্ট দিন। পুরোনো default তারিখকে ১৯৭০ থেকে millisecond-এর সংখ্যায় (1790985600000) লেখে, যা মানুষ পড়তে পারে না। ল্যাপটপের pandas 3.1.1 এই default-এর জন্য একটি সতর্কবার্তা (Pandas4Warning) দেখায়: ভবিষ্যতের version-এ default বদলে ISO লেখা হবে। আর বাংলা লেখা ঢ-এর মতো escape না করে সরাসরি রাখতে force_ascii=False দিন।
৬ · Parquet — type অক্ষত, শুধু দরকারি কলাম
পর্ব ১০-এ দেখেছেন, Parquet কলাম ধরে রাখে আর schema-য় প্রতিটি কলামের type লেখা থাকে। pandas-এ এর API সবচেয়ে সরল, কারণ অনুমানের কিছু নেই: df.to_parquet(path) আর pd.read_parquet(path), ভেতরে pyarrow. পড়ার সময় দুটি দরকারি option: columns= (শুধু কয়েকটি কলাম পড়া, বাকিগুলো ডিস্ক থেকে তোলাই হয় না) আর filters= (শর্ত মেলে না এমন অংশ বাদ)। ল্যাপটপে ১,০০,০০০ সারির একটি seed-দেওয়া অর্ডার-টেবিলে সত্যিকারের run:
import numpy as np, pandas as pd, pyarrow as pa
rng = np.random.default_rng(42)
n = 100_000
orders = pd.DataFrame({
"order_id": np.arange(1, n + 1),
"date": pd.Timestamp("2026-01-01") + pd.to_timedelta(rng.integers(0, 300, n), unit="D"),
"district": pd.Categorical(rng.choice(["Dhaka", "Khulna", "Sylhet"], n)),
"qty": rng.integers(1, 6, n),
"price_paisa": rng.integers(50, 5000, n) * 100,
})
orders.to_parquet("out/orders.parquet", index=False) # pyarrow engine
print("pandas", pd.__version__, "| pyarrow", pa.__version__)
back = pd.read_parquet("out/orders.parquet")
print(back.dtypes.astype(str).to_dict())
pd.testing.assert_frame_equal(orders, back)
print("round trip: identical")
two = pd.read_parquet("out/orders.parquet", columns=["district", "qty"])
print("columns= ->", two.shape)
dhaka = pd.read_parquet("out/orders.parquet",
filters=[("district", "==", "Dhaka")])
print("filters= ->", dhaka.shape, dhaka["district"].unique().tolist())~/bazarghar-ai$ python parquet_io.py
pandas 3.1.1 | pyarrow 26.0.1
{'order_id': 'int64', 'date': 'datetime64[us]', 'district': 'category', 'qty': 'int64', 'price_paisa': 'int64'}
round trip: identical
columns= -> (100000, 2)
filters= -> (33200, 5) ['Dhaka']
তারিখ ফিরেছে তারিখ হয়ে, category ফিরেছে category হয়ে, আর assert_frame_equal কোনো আপত্তি তোলেনি — কোনো dtype বা parse_dates বলে দিতে হয়নি। filters=-এ শুধু ঢাকার ৩৩,২০০টি সারি এসেছে। (বড় ফাইলে filters row group-এর min/max দেখে পুরো অংশ না পড়েই বাদ দিতে পারে; এই ছোট ফাইলে একটিই row group, তাই এখানে কাজটি মূলত পড়ার পরে বাছাই।)
দুই format, একই পরীক্ষা — ব্রাউজারে চালিয়ে দেখুন
এবার একই ছোট টেবিল JSON Lines আর Parquet-এ লিখে আবার পড়া হবে, আর প্রতিবার মূলের সঙ্গে মেলানো হবে। একটি সৎ নোট: লেখার সময়ের Pyodide-এ df.to_parquet একটি ArrowKeyError দেয় (pandas আর pyarrow-এর version-অমিল), আর একই কারণে pd.read_parquet-ও। তাই পাতায় লেখা আর পড়া দুটোই সরাসরি pyarrow দিয়ে: লিখতে pa.Table.from_pandas তারপর pq.write_table, পড়তে pq.read_table(...).to_pandas(). নিজের কম্পিউটারে df.to_parquet আর pd.read_parquet ঠিকঠাক চলে (ওপরের run).
import pandas as pd
import pyarrow as pa, pyarrow.parquet as pq
from pandas.testing import assert_frame_equal
df = pd.DataFrame({
"order_id": [1001, 1002, 1003],
"phone": ["01700000000", "01800000001", "01900000002"],
"date": pd.to_datetime(["2026-10-03", "2026-10-03", "2026-10-04"]),
"district": ["ঢাকা", "খুলনা", "সিলেট"],
"price_paisa": [125000, 8000, 78000],
})
df.to_json("o.jsonl", orient="records", lines=True,
date_format="iso", force_ascii=False)
with open("o.jsonl", encoding="utf-8") as fh:
print(fh.readline().strip())
b1 = pd.read_json("o.jsonl", lines=True) # let pandas guess
print("guess :", b1["phone"].dtype, b1["phone"].tolist()[:1])
b2 = pd.read_json("o.jsonl", lines=True, dtype={"phone": str})
print("dtype :", b2["phone"].dtype, b2["phone"].tolist()[:1])
print("equals:", df.equals(b1), df.equals(b2))
try:
assert_frame_equal(df, b1)
except AssertionError as e:
print("assert:", str(e).splitlines()[0])
assert_frame_equal(df, b2)
print("JSONL round trip OK")
pq.write_table(pa.Table.from_pandas(df, preserve_index=False), "o.parquet")
b3 = pq.read_table("o.parquet").to_pandas()
assert_frame_equal(df, b3)
print("Parquet round trip OK:", b3.dtypes.astype(str).tolist())আসল আউটপুট (Pyodide-এ, pandas 3.0.2, pyarrow 22):
{"order_id":1001,"phone":"01700000000","date":"2026-10-03T00:00:00.000","district":"ঢাকা","price_paisa":125000}
guess : int64 [1700000000]
dtype : str ['01700000000']
equals: False True
assert: Attributes of DataFrame.iloc[:, 1] (column name="phone") are different
JSONL round trip OK
Parquet round trip OK: ['int64', 'str', 'datetime64[us]', 'str', 'int64']
প্রথম লাইনে ফাইলের আসল চেহারা: ফোন নম্বর quote-এর ভেতরে, অর্থাৎ JSON-এ সেটি string; তারিখ ISO রূপে; বাংলা জেলার নাম সরাসরি। তবু dtype ছাড়া পড়লে pandas ফোনকে int64 বানিয়েছে — ফাইলে type ঠিক থাকলেও পড়ার টুল নিজের অনুমান চাপিয়েছে। equals শুধু হ্যাঁ/না বলে; assert_frame_equal বলে কোথায় অমিল: দ্বিতীয় কলাম, নাম phone. খেয়াল করুন, date কলাম কোনো option ছাড়াই তারিখ হয়ে ফিরেছে — নাম দেখে অনুমানের ফল, অংশ ৫-এ যা বলা হয়েছে। Parquet-এ কিছুই বলে দিতে হয়নি: পাঁচটি কলামের type মূলের সঙ্গে হুবহু।
৭ · SQL — to_sql, read_sql আর parameterized query
বাজারঘরের আসল অর্ডারগুলো থাকে একটি ডেটাবেসে, কোনো ফাইলে নয়। pandas ডেটাবেসের সঙ্গে কথা বলে দুটি function দিয়ে: pd.read_sql(query, con) একটি SQL প্রশ্নের উত্তর DataFrame হিসেবে আনে, আর df.to_sql(name, con) DataFrame-কে একটি টেবিলে লেখে। এখানে con হলো ডেটাবেসের সঙ্গে একটি connection.
শেখার জন্য সবচেয়ে সহজ ডেটাবেস SQLite: একটি ফাইলেই পুরো ডেটাবেস, কোনো সার্ভার লাগে না, আর Python-এর সঙ্গেই আসে sqlite3 module হিসেবে — ব্রাউজারের Pyodide-এও। pandas-এর docs অনুযায়ী con হিসেবে একটি sqlite3 connection সরাসরি দেওয়া যায়; PostgreSQL বা MySQL-এর মতো অন্য ডেটাবেসের জন্য pandas একটি SQLAlchemy engine (বা ADBC connection) চায়। SQL ভাষা নিজে এই কোর্সে শেখানো হবে না — তার জন্য আছে DBMS & SQL কোর্স, বিশেষত GROUP BY পর্ব; Python থেকে SQLite ব্যবহারের পুরো ছবি আছে Python কোর্সের SQLite পর্বে.
মান কখনো string জোড়া দিয়ে নয়
ধরুন, ব্যবহারকারী একটি payment পদ্ধতি বেছে নেন, আর সেটি দিয়ে query বানাতে হবে। সহজ মনে হয় f-string: f"... WHERE payment = '{p}'". কিন্তু p-এর ভেতরেই যদি SQL-এর টুকরো থাকে — যেমন x' OR '1'='1 — তাহলে সেটি query-র অংশ হয়ে যায়, আর শর্তটি সব সারির জন্য সত্য। একে বলে SQL injection. প্রতিকার parameterized query: query-তে মানের জায়গায় শুধু একটি চিহ্ন (sqlite3-এ ? বা :name), আর মান আলাদাভাবে params= দিয়ে পাঠানো। ডেটাবেস তখন মানটিকে সবসময় ডেটা হিসেবে দেখে, কোড হিসেবে কখনো নয়। আক্রমণ আর প্রতিরক্ষার বিস্তারিত আছে Cybersecurity কোর্সের injection প্রতিরক্ষা পর্বে আর DBMS কোর্সের নিরাপত্তা পর্বে.
যা দরকার শুধু তা-ই আনা
দ্বিতীয় প্রশ্ন: filter আর যোগফল কোথায় করবেন? দুই পথ আছে। এক, SELECT * FROM orders দিয়ে সব সারি pandas-এ এনে সেখানে filter আর groupby. দুই, SQL-এই WHERE, GROUP BY আর SUM করে শুধু ছোট ফলটি আনা। ৮টি সারিতে পার্থক্য নেই; ২ কোটি সারির টেবিলে প্রথম পথে সব ডেটা নেটওয়ার্ক পেরিয়ে আপনার ল্যাপটপের মেমরিতে আসে, দ্বিতীয় পথে আসে হয়তো ৬৪টি জেলার ৬৪টি সারি। নিচে ধাপে ধাপে দেখুন, একটি query কীভাবে একটি ছোট DataFrame হয়ে ফেরে।
id district payment qty price_paisa 1001 Dhaka COD 2 72500 1002 Sylhet COD 1 61500 1003 Khulna bKash 3 42100 1004 Khulna card 2 75800 1005 Khulna card 2 53600 1006 Sylhet card 2 45400 1007 Dhaka card 1 46000 1008 Sylhet card 3 28100
SELECT district, COUNT(*) AS n, SUM(qty * price_paisa) AS total_paisa FROM orders WHERE payment = ? GROUP BY district ORDER BY total_paisa DESC ? ← "card" (ডেটা হিসেবে)
1004 Khulna 2 × 75800 = 151600 1005 Khulna 2 × 53600 = 107200 1006 Sylhet 2 × 45400 = 90800 1007 Dhaka 1 × 46000 = 46000 1008 Sylhet 3 × 28100 = 84300
Khulna: 151600 + 107200 Sylhet: 90800 + 84300 Dhaka : 46000
Khulna n=2 total=258800
Sylhet n=2 total=175100
Dhaka n=1 total= 46000
district n total_paisa 0 Khulna 2 258800 1 Sylhet 2 175100 2 Dhaka 1 46000 district str · n int64 total_paisa int64
- ডেটাবেসের টেবিল।
to_sqlদিয়ে লেখা বাজারঘরের ৮টি অর্ডার (seed 42 দিয়ে বানানো)। দাম পয়সায়, পূর্ণসংখ্যা। ডেটা এখন ডেটাবেসে, pandas-এর মেমরিতে নয়। - Query আর মান আলাদা পথে। Query-র লেখায় শুধু
?;"card"যায়paramsদিয়ে। ডেটাবেস আগে query-র গঠন বোঝে, তারপর?-এর জায়গায় মানটি বসায় — মানের ভেতরে quote বাORথাকলেও সেটি শুধু লেখা। - WHERE — আগে বাছাই। ৮টি সারির ৫টির payment
card. প্রতিটির মোট দামqty × price_paisa: যেমন ১০০৪-এ ২ × ৭৫,৮০০ = ১,৫১,৬০০ পয়সা। - GROUP BY — জেলা ধরে ভাগ। খুলনার দুটি, সিলেটের দুটি, ঢাকার একটি সারি। এই split-apply-combine ধারণার pandas রূপ আসবে পর্ব ২৪-এ।
- যোগফল আর সাজানো। খুলনা ১,৫১,৬০০ + ১,০৭,২০০ = ২,৫৮,৮০০; সিলেট ৯০,৮০০ + ৮৪,৩০০ = ১,৭৫,১০০; ঢাকা ৪৬,০০০.
ORDER BY ... DESCবড় থেকে ছোটতে সাজায়। টাকায়: খুলনা ২,৫৮৮ টাকা। - ফেরত DataFrame.
read_sqlকলামের নাম নেয়ASথেকে, আর type ডেটাবেসের মান থেকে: জেলাstr, সংখ্যাint64. ৮টি সারির বদলে এসেছে ৩টি — বড় টেবিলে এই পার্থক্যই আসল লাভ।
import sqlite3
import numpy as np, pandas as pd
rng = np.random.default_rng(42)
n = 8
orders = pd.DataFrame({
"order_id": np.arange(1001, 1001 + n),
"district": rng.choice(["Dhaka", "Khulna", "Sylhet"], n),
"payment": rng.choice(["COD", "bKash", "card"], n),
"qty": rng.integers(1, 4, n),
"price_paisa": rng.integers(100, 900, n) * 100,
})
con = sqlite3.connect(":memory:") # a database in RAM
print("rows written:", orders.to_sql("orders", con, index=False))
q = """SELECT district, COUNT(*) AS n,
SUM(qty * price_paisa) AS total_paisa
FROM orders WHERE payment = ?
GROUP BY district ORDER BY total_paisa DESC"""
res = pd.read_sql(q, con, params=("card",)) # ? gets "card"
print(res)
print(res.dtypes.astype(str).to_dict())
# the same answer computed in pandas
card = orders[orders["payment"] == "card"]
t = (card["qty"] * card["price_paisa"]).groupby(card["district"]).sum()
print(t.sort_values(ascending=False).to_dict())
evil = "x' OR '1'='1" # a hostile "payment" value
s = f"SELECT COUNT(*) FROM orders WHERE payment = '{evil}'"
print("f-string :", con.execute(s).fetchone()[0], "rows")
p = "SELECT COUNT(*) FROM orders WHERE payment = ?"
print("parameter:", con.execute(p, (evil,)).fetchone()[0], "rows")
con.close()আসল আউটপুট (Pyodide-এ):
rows written: 8
district n total_paisa
0 Khulna 2 258800
1 Sylhet 2 175100
2 Dhaka 1 46000
{'district': 'str', 'n': 'int64', 'total_paisa': 'int64'}
{'Khulna': 258800, 'Sylhet': 175100, 'Dhaka': 46000}
f-string : 8 rows
parameter: 0 rows
to_sql লেখা সারির সংখ্যা ফেরত দেয় (৮) — এটিও একটি ছোট যাচাই। SQL-এর উত্তর আর pandas-এর উত্তর হুবহু এক; পার্থক্য শুধু কাজটি কোথায় হলো আর কত ডেটা সরল। শেষ দুই লাইন injection-এর সারকথা: f-string-এ বানানো query “x' OR '1'='1”-কে কোড হিসেবে পড়েছে, তাই শর্ত সব ৮টি সারির জন্য সত্য। একই মান parameter হিসেবে পাঠালে ডেটাবেস খুঁজেছে এমন অর্ডার, যার payment-এর নাম আক্ষরিক অর্থেই “x' OR '1'='1” — একটিও নেই, তাই ০. এখানে ক্ষতি শুধু একটি গণনায়; আসল অ্যাপে একই কৌশলে অন্যের ডেটা পড়া বা মুছে ফেলা সম্ভব।
| কাজটি SQL-এ করুন, যখন… | pandas-এ আনুন, যখন… |
|---|---|
টেবিল বড়, দরকার তার ছোট একটি অংশ (WHERE) | পুরো টেবিলই ছোট, আর বারবার নানা দিক থেকে দেখবেন |
ফল একটি সারাংশ (GROUP BY, SUM, COUNT) | কাজটি SQL-এ কঠিন: জটিল লেখা পরিষ্কার, ML feature, chart |
কয়েকটি টেবিল JOIN করে ছোট ফল | ডেটা কয়েকটি আলাদা উৎস থেকে (CSV + ডেটাবেস) মেলাতে হবে |
| ডেটাবেসে index আছে, যা শর্তকে দ্রুত করে | একই ডেটা থেকে অনেক পরীক্ষা, ডেটাবেসে বারবার না গিয়ে |
to_sql-এর দুটি দরকারি option: if_exists ("fail" default — টেবিল থাকলে ValueError: Table 'orders' already exists.; "append" শেষে যোগ; "replace" মুছে নতুন করে) আর index — এখানেও default True, যা টেবিলে একটি বাড়তি "index" কলাম বানায় (ল্যাপটপে যাচাই করা)। আর SQLite-এ তারিখের আলাদা type নেই: to_sql তারিখকে লেখা হিসেবে রাখে, তাই ফেরত পড়তে read_sql(..., parse_dates=["date"]) দিন।
৮ · বিশ্বাসের আগে যাচাই — round trip, chunk আর URL
এই পর্বের প্রতিটি উদাহরণে একই শিক্ষা ফিরে এসেছে: পড়ার function প্রায় কখনো ব্যর্থ হয় না, কিন্তু প্রায়ই ভুল বোঝে। তাই প্রতিটি নতুন উৎস পড়ার পর চারটি জিনিস দেখার অভ্যাস করুন:
(১) df.shape — সারি আর কলামের সংখ্যা উৎসের সঙ্গে মেলে? (ফাইলের লাইন নয়, record গুনুন; ডেটাবেসে SELECT COUNT(*).) (২) df.dtypes — ফোন আর ID str, তারিখ datetime64, দাম সংখ্যা? (৩) df.isna().sum() — missing কোথায়, কতটি, আর সেটি কি প্রত্যাশিত? (৪) df.head() আর df.sample(5, random_state=42) — নিজের চোখে কয়েকটি সারি, বাংলা লেখা ঠিক আছে কি না।
Round-trip test — লিখে আবার পড়া
লেখার দিকের যাচাই হলো round-trip test: DataFrame লিখুন, সেই ফাইল আবার পড়ুন, আর মূলের সঙ্গে মেলান। df.equals(other) শুধু True/False দেয়; pandas.testing.assert_frame_equal অমিল পেলে assertion error-এ বলে কোন কলাম, কোন বৈশিষ্ট্য (dtype, মান, index). একে একটি ছোট unit test-এ রাখলে (পর্ব ১১, Python কোর্সের pytest পর্ব) কেউ পরে লেখার কোড বদলালেও ভুল ধরা পড়ে। নিচের ধাপগুলো ল্যাপটপের একটি সত্যিকারের run থেকে।
order_id int64 phone str date datetime64[us] district category
order_id,phone,date,district 1001,01700000000,2026-10-03,Dhaka 1002,01800000001,2026-10-04,Khulna
order_id int64 phone int64 ← ছিল str date str ← ছিল datetime district str ← ছিল category
False
কেন, কোথায় — কিছুই বলে না
Attributes of DataFrame.iloc[:, 1] (column name="phone") are different Attribute "dtype" are different [left]: <StringDtype(na_value=nan)> [right]: int64
pd.read_csv(f,
dtype={"phone": str,
"district": "category"},
parse_dates=["date"])
assert_frame_equal → OK
- মূল। চারটি কলাম, চার রকম type: পূর্ণসংখ্যা, লেখা, তারিখ আর category (কয়েকটি নির্দিষ্ট মানের কলাম, পর্ব ২৭-এ বিস্তারিত)।
- CSV-তে লেখা। ফাইলে type বলে কিছু নেই — সব লেখা। ফোন নম্বর শূন্যসহ ঠিকই লেখা হয়েছে; সমস্যা আসবে পড়ার সময়।
- আবার পড়া। কিছু না বলে পড়লে চারটির তিনটি কলামের type বদলে গেছে: ফোন সংখ্যা হয়ে শূন্য হারিয়েছে, তারিখ আর জেলা সাধারণ লেখা।
equals.False— ঠিক উত্তর, কিন্তু খোঁজার কোনো সূত্র নেই। দ্রুত পরীক্ষার জন্য ভালো, ভুল খোঁজার জন্য নয়।assert_frame_equal. প্রথম অমিলেই থামে আর বলে: কলাম ১ (phone), বৈশিষ্ট্য dtype, বাঁয়েstr, ডানেint64. এটি ঠিক করে আবার চালালে পরের অমিল বলবে —date, তারপরdistrict.- সমাধান। পড়ার সময় তিনটি কলামের type বলে দিলে test পাস করে। অথবা type রাখার কাজটি format-কেই দিন — Parquet-এ একই test কোনো option ছাড়াই পাস করে (অংশ ৬)।
বড় ফাইল — টুকরো করে পড়া
ফাইল ল্যাপটপের মেমরির চেয়ে বড় হলে read_csv(..., chunksize=N) পুরো ফাইল একবারে না এনে chunk ধরে ধরে DataFrame দেয়; প্রতিটি টুকরো থেকে দরকারি যোগফল রেখে টুকরোটি ফেলে দেওয়া যায়। ফেরত বস্তুটি একটি reader, তাই with দিয়ে খোলা ভালো, যাতে শেষে ফাইল বন্ধ হয়। ল্যাপটপে ১০ লাখ সারির একটি CSV-তে সত্যিকারের run:
import pandas as pd
total = 0
rows = 0
with pd.read_csv("out/orders.csv", chunksize=250_000,
usecols=["qty", "price_paisa"]) as reader:
for chunk in reader: # each chunk is a DataFrame
rows += len(chunk)
total += int((chunk["qty"] * chunk["price_paisa"]).sum())
print("chunk", chunk.shape)
print("rows", rows, "| revenue (taka)", f"{total / 100:,.0f}")~/bazarghar-ai$ python chunks.py
chunk (250000, 2)
chunk (250000, 2)
chunk (250000, 2)
chunk (250000, 2)
rows 1000000 | revenue (taka) 7,578,335,574
হিসাবটি নিজে করুন: ১০,০০,০০০ ÷ ২,৫০,০০০ = ৪টি chunk. প্রতিটিতে ২টি int64 কলাম, তাই মেমরিতে মোটামুটি ২,৫০,০০০ × ২ × ৮ byte = ৪০,০০,০০০ byte ≈ ৪ MB — পুরো ফাইলের সব কলাম একসঙ্গে আনার চেয়ে অনেক কম। ভাগ মিলে না গেলে শেষ chunk ছোট হয়: ১০ লাখ সারি ৩,০০,০০০ করে নিলে ৩টি পূর্ণ chunk আর শেষটিতে ১,০০,০০০, মোট ৪টি (⌈১০,০০,০০০ ÷ ৩,০০,০০০⌉ = ৪)। যোগফল, গণনা বা সর্বোচ্চ মানের মতো কাজ chunk ধরে সহজে চলে; median বা সাজানোর মতো কাজে পুরো ডেটা লাগে, সেখানে chunk যথেষ্ট নয়। বড় ডেটার বাকি কৌশল — dtype কমানো, category, Polars — আসবে পর্ব ২৭-এ।
URL থেকে পড়া — একটি সতর্ক নোট
pd.read_csv, read_json আর read_parquet একটি ফাইলের নামের জায়গায় একটি https:// URL-ও নেয়, আর সরাসরি ইন্টারনেট থেকে পড়ে। (এই পাতার কোড-বাক্সে নেটওয়ার্ক বন্ধ, তাই এখানে উদাহরণ নেই।) সুবিধাজনক, কিন্তু তিনটি সাবধানতা: URL-এর ফাইল কাল বদলে গেলে আপনার ফলও বদলাবে, তাই একবার নামিয়ে একটি স্থানীয় কপি আর তার hash রাখুন (পর্ব ১২); অচেনা উৎসের ফাইলকে অবিশ্বস্ত ডেটা ধরুন — বিশেষত pickle-এর মতো format কখনো অচেনা উৎস থেকে খুলবেন না, কারণ pickle খোলার সময় কোড চলতে পারে; আর URL-এ কখনো আসল password বা token লিখবেন না।
আরও গভীরে: pandas-এর CSV reader-এর ভেতরে তিনটি engine আছে: দ্রুত C engine (default), সব option বোঝে এমন ধীর "python" engine, আর বহু-thread-এ চলা "pyarrow" engine (engine="pyarrow"), যা সব option সমর্থন করে না। read_sql আসলে দুটি function-এর একটি সুবিধাজনক মোড়ক — read_sql_query আর read_sql_table — আর SQLAlchemy ছাড়াও ADBC (Arrow Database Connectivity) driver নিতে পারে, যা ডেটা সারি ধরে না এনে সরাসরি Arrow-এর কলাম হিসেবে আনে। to_sql-এর chunksize আর method="multi" একসঙ্গে অনেক সারি পাঠিয়ে বড় লেখা দ্রুত করতে পারে, তবে সেটি ডেটাবেসভেদে বদলায়; মাপার আগে ধরে নেবেন না।
পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় যে ধরনের প্রশ্ন সাধারণত আসে):
- CSV পড়ার পর
Unnamed: 0কলাম কেন? — আগেto_csvindex-সহ লিখেছিল; লেখার সময়index=Falseবা পড়ার সময়index_col=0. - ফোন নম্বরের শূন্য রাখতে কী দেবেন? —
dtype={"phone": str},read_csv,read_excelআরread_jsonতিনটিতেই। lines=Trueকোনorient-এর সঙ্গে চলে? — শুধু"records".- SQL injection ঠেকাতে pandas-এ কী করবেন? — query-তে
?বা:name, মানparams=দিয়ে; f-string বা+দিয়ে কখনো নয়। - ১০ লাখ সারি,
chunksize=300000— কয়টি chunk? — ⌈১০ ÷ ৩⌉ = ৪টি, শেষটিতে ১,০০,০০০ সারি।
৯ · AI-এর যুগে — ডেটা পড়ার কোডে AI সহকারী
“এই CSV pandas দিয়ে পড়ার কোড দাও” — AI সহকারীর কাছে সবচেয়ে সাধারণ অনুরোধগুলোর একটি। কোড প্রায় সবসময় চলে; বিপদ হলো ডেটা নীরবে বদলে যাওয়া। AI সহকারী এই কাজে সাধারণত যে ভুলগুলো করে:
| AI সহকারীর ভুল | কীভাবে ধরবেন |
|---|---|
পুরোনো API — df.append (pandas 2.0-এ সরানো), error_bad_lines= (এখন on_bad_lines=), বা অস্তিত্বহীন parameter | নিজের version-এর অফিসিয়াল docs-এ parameter খোঁজা, চালিয়ে দেখা |
dtype ছাড়া read_csv/read_json — ID আর ফোনের শূন্য হারায় | পড়ার পর dtypes আর ফোনের দৈর্ঘ্য দেখা |
index=False ভুলে যাওয়া, বা উল্টো, groupby-র index ফেলে দেওয়া | লেখা ফাইলের প্রথম দুই লাইন খুলে দেখা; round-trip test |
| f-string দিয়ে SQL বানানো | Query-তে { বা + খোঁজা; params= আছে কি না |
SELECT * এনে pandas-এ filter, বড় টেবিলেও | ফেরত সারির সংখ্যা দেখা; WHERE আর aggregate SQL-এ নেওয়া |
Warning চাপা দেওয়া (warnings.filterwarnings("ignore")) বা errors="ignore" | কোডে এমন লাইন খোঁজা; warning-এর অর্থ বোঝা |
AI সহকারীকে ফাইলের কাঠামো বলুন, আসল গ্রাহকের ডেটা নয়। নিচের prompt-এ শুধু কলামের নাম আর বানানো নমুনা আছে:
AI সহকারীর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১read_csv কেন অনুমান করে, error দেয় না? সুবিধা আর নিরাপত্তার এই টানাপোড়েন কোথা থেকে এল?
pandas-এর শুরু ২০০৮ সালে, Wes McKinney-র হাতে, একটি আর্থিক প্রতিষ্ঠানে কাজের সময়; পরের বছর এটি open source হয়। তখনকার লক্ষ্য ছিল R-এর data frame-এর মতো দ্রুত, হাতের কাছের বিশ্লেষণ — বিশ্লেষক একটি ফাইল খুলবেন, এক লাইনে টেবিল পাবেন, তারপর প্রশ্ন করবেন। এমন ব্যবহারে প্রতিটি কলামের type আগে থেকে লিখতে বলা বিরক্তিকর। তাই read_csv-এর নকশা দাঁড়াল অনুমানের ওপর: সব মান অঙ্ক হলে সংখ্যা, একটি ফাঁকা থাকলে float, বাকিটা লেখা।
এই নকশা interactive কাজে চমৎকার, কিন্তু pipeline-এ বিপজ্জনক। Notebook-এ বিশ্লেষক নিজের চোখে টেবিল দেখেন; কিন্তু রাতের বেলা নিজে-নিজে চলা একটি pipeline-এ কেউ দেখে না। আজ যে কলামে সব মান অঙ্ক, কাল একটি “N/A” এলে কলামটি নীরবে লেখা হয়ে যাবে, আর পরের যোগফলের কোড error দেবে — অথবা আরও খারাপ, কিছু না বলে ভুল ফল দেবে। পর্ব ১০-এ দেখা জিনের নাম তারিখ হয়ে যাওয়ার ঘটনাও একই পরিবারের: “সাহায্যকারী” অনুমান।
pandas নিজেও সময়ের সঙ্গে কঠোর হয়েছে। pandas 3.0-এ (২০২৬ সালের শুরুর দিকে প্রকাশিত) লেখার কলামের জন্য আলাদা str dtype default হয়েছে, যাতে “যেকোনো Python বস্তু” রাখা object কলাম আর লেখার কলাম গুলিয়ে না যায়; Copy-on-Write একমাত্র আচরণ হয়েছে। JSON-এর তারিখের default-ও ISO-র দিকে সরছে (অংশ ৫-এর সতর্কবার্তা)। প্রতিটি বদল একই দিকে: অনুমান কমানো, স্পষ্টতা বাড়ানো।
বাস্তবের ভালো অভ্যাস তাই দুই স্তরের। অনুসন্ধানের সময় অনুমানে চলুক, কিন্তু pipeline-এর কোডে প্রতিটি কলামের type লিখে দিন — একটি dict-এ, যা ফাইলের “চুক্তি” হিসেবেও কাজ করে। আরও এক ধাপ এগোলে pandera বা Great Expectations-এর মতো ডেটা-যাচাইয়ের library দিয়ে নিয়ম লেখা যায়: “phone ১১ অক্ষরের, 0 দিয়ে শুরু”, “qty ১ থেকে ১০০”. আর যেখানে সম্ভব, type বহন করে এমন format (Parquet) বেছে নেওয়া, যাতে অনুমানের প্রয়োজনই না থাকে।
মূল উপলব্ধি: অনুমান হলো অনুসন্ধানের বন্ধু আর উৎপাদনের শত্রু। যা আপনি জানেন, তা কোডে লিখে দিন; pandas-কে অনুমান করতে দিন শুধু তখন, যখন আপনি নিজে ফল দেখছেন।
প্র ০২SQL injection এত পুরোনো সমস্যা হয়েও কেন আজও ঘটে? Parameterized query আসলে কী আলাদা করে?
SQL injection-এর প্রথম দিকের প্রকাশ্য বিবরণগুলোর একটি হিসেবে সাধারণত ধরা হয় ১৯৯৮ সালে Phrack ম্যাগাজিনে Jeff Forristal-এর (ছদ্মনাম “rain.forest.puppy”) একটি লেখা। তারপর প্রায় তিন দশক কেটেছে, তবু OWASP-এর ওয়েব-নিরাপত্তার শীর্ষ ঝুঁকির তালিকায় “Injection” বছরের পর বছর রয়ে গেছে (২০২১ সালের তালিকায় তৃতীয়)। xkcd-এর ২০০৭ সালের একটি কমিকে এক মা ছেলের নাম রেখেছিলেন Robert'); DROP TABLE Students;-- — “Little Bobby Tables” — আর স্কুলের ডেটাবেস মুছে গিয়েছিল; প্রোগ্রামারদের মধ্যে এটি এখনো সতর্কবার্তা হিসেবে ঘোরে।
সমস্যার মূল একটি মিশ্রণ: query একটি লেখা, যার ভেতরে কোড (SQL-এর শব্দ) আর ডেটা (ব্যবহারকারীর মান) একই স্রোতে যায়। f-string বা + দিয়ে জোড়া দিলে ডেটাবেসের parser জানে না কোন অংশ প্রোগ্রামার লিখেছেন আর কোনটি ব্যবহারকারী দিয়েছেন। মানের ভেতরে একটি ' থাকলে সেটি লেখার সীমা ভেঙে বাকি অংশকে কোডে পরিণত করে।
Parameterized query এই দুটিকে আলাদা পথে পাঠায়। Query-র গঠন — WHERE payment = ? — আগে বিশ্লেষণ হয়; মান পরে আসে, আর ডেটাবেস জানে সেটি শুধুই একটি মান। তাই quote, OR, সেমিকোলন — সবই আক্ষরিক লেখা। এই পর্বের কোডে ঠিক এটিই দেখা গেছে: একই হুমকিময় লেখা f-string-এ ৮টি সারি মিলিয়েছে, parameter হিসেবে ০টি। হাতে quote “escape” করার চেষ্টা ভঙ্গুর, কারণ প্রতিটি ডেটাবেসের নিয়ম আলাদা আর ভুল করা সহজ।
তবু কেন ঘটে? প্রথমত, শেখার উপকরণ আর দ্রুত লেখা কোডে f-string সহজতম পথ — AI সহকারীও প্রায়ই সেটিই দেয়। দ্বিতীয়ত, parameter শুধু মানের জায়গায় বসে; টেবিল বা কলামের নাম parameter দিয়ে দেওয়া যায় না। সেখানে জানা নামের একটি তালিকা (allow-list) থেকে বাছাই করাই নিরাপদ। তৃতীয়ত, ডেটা বিশ্লেষকেরা ভাবেন, “এটি তো শুধু আমার notebook” — কিন্তু সেই কোডই পরে একটি ড্যাশবোর্ড বা API-তে চলে যায়।
মূল উপলব্ধি: নিরাপত্তা এখানে সতর্কতার প্রশ্ন নয়, কাঠামোর প্রশ্ন। কোড আর ডেটা আলাদা পথে পাঠালে সতর্ক থাকার দায়িত্ব মানুষের কাঁধ থেকে ডেটাবেসের কাঁধে যায়।
প্র ০৩“হিসাব ডেটার কাছে নিয়ে যাও” — কাজ SQL-এ করা ভালো, নাকি pandas-এ? এই বিতর্কের দুই পক্ষ কী বলে?
১৯৭০ সালে E. F. Codd relational model প্রস্তাব করেন, আর তার পরের দশকে IBM-এর System R প্রকল্প থেকে SQL-এর পূর্বসূরি ভাষা জন্ম নেয়। SQL-এর মূল ধারণা declarative: আপনি বলেন কী চান (“card-এ দেওয়া অর্ডারের জেলাভিত্তিক যোগফল”), কীভাবে খুঁজবে তা ঠিক করে ডেটাবেসের query optimizer — কোন index ব্যবহার করবে, কোন ক্রমে টেবিল মেলাবে। কয়েক দশকের প্রকৌশল এই optimizer-এর পেছনে।
SQL-এ কাজ করার পক্ষে প্রধান যুক্তি ডেটার নড়াচড়া। ২ কোটি সারির টেবিল নেটওয়ার্ক পেরিয়ে ল্যাপটপে আনতে সময়, মেমরি আর ডেটাবেসের ওপর চাপ লাগে; জেলাভিত্তিক যোগফল আনলে আসে কয়েক ডজন সারি। নিরাপত্তার যুক্তিও আছে: যে ডেটা আপনার ল্যাপটপে আসেনি, সেটি সেখান থেকে ফাঁসও হবে না — গ্রাহকের ফোন নম্বর দরকার না হলে SELECT-এই বাদ দিন।
pandas-এর পক্ষের যুক্তি নমনীয়তা। জটিল লেখা পরিষ্কার, ML-এর feature বানানো, chart, পরীক্ষামূলক প্রশ্ন একটার পর একটা — এসব Python-এ সহজ, SQL-এ কষ্টকর বা অসম্ভব। একবার ডেটা এনে মেমরিতে রাখলে প্রতিটি নতুন প্রশ্নে ডেটাবেসে যেতে হয় না, আর উৎপাদনের ডেটাবেসে বিশ্লেষকের ভারী query না চালানোও একটি ভদ্রতা।
মাঝামাঝি পথও এখন জনপ্রিয়। DuckDB (২০১৯ সালের দিকে প্রথম প্রকাশ) একটি in-process বিশ্লেষণী ডেটাবেস, যা সরাসরি Parquet বা এমনকি একটি pandas DataFrame-এর ওপর SQL চালাতে পারে; Polars (পর্ব ২৭) lazy query দিয়ে একই রকম optimizer-এর সুবিধা দেয়। বাস্তবের সাধারণ নিয়ম: প্রথমে SQL-এ বাছাই (WHERE), দরকারি কলাম (SELECT নির্দিষ্ট কলাম) আর সম্ভব হলে সারাংশ; তারপর ছোট হয়ে আসা ডেটা pandas-এ এনে বাকি কাজ।
মূল উপলব্ধি: প্রশ্নটি “কোন টুল ভালো” নয়, “কত ডেটা কোথায় সরবে”। ডেটা যত বড়, হিসাব তত ডেটার কাছে; ডেটা যত ছোট আর প্রশ্ন যত এলোমেলো, pandas তত উপযুক্ত।
প্র ০৪২০২০ সালে ইংল্যান্ডে কোভিড-১৯ রোগীর হিসাব হারানোর ঘটনা ফাইল-format সম্পর্কে কী শেখায়?
২০২০ সালের অক্টোবরে ইংল্যান্ডের Public Health England জানায়, কয়েক দিনের ব্যবধানে প্রায় ১৬,০০০ পজিটিভ কোভিড-১৯ ফলাফল দৈনিক হিসাবে সময়মতো যোগ হয়নি। BBC-সহ বিভিন্ন সংবাদমাধ্যমের প্রতিবেদন অনুযায়ী কারণ ছিল একটি ফাইল-format: পরীক্ষাগারের CSV ফলাফল একটি স্বয়ংক্রিয় প্রক্রিয়ায় পুরোনো Excel format (.xls)-এর টেমপ্লেটে তোলা হচ্ছিল, যার একটি sheet-এ সর্বোচ্চ ৬৫,৫৩৬টি সারি ধরে। সীমা ছাড়ানো সারিগুলো বাদ পড়ে যায় — কোনো error ছাড়াই। ফলে যাঁদের সংস্পর্শে আসা মানুষদের খুঁজে বের করার কথা, সেই কাজও কয়েক দিন পিছিয়ে যায় বলে জানা যায়।
ঘটনাটির শিক্ষা এই পর্বের প্রায় প্রতিটি অংশের সঙ্গে মেলে। প্রথমত, format-এর সীমা আছে, আর সীমা ছাড়ালে অনেক টুল চুপচাপ কেটে দেয়। আধুনিক xlsx-এ সীমা ১০,৪৮,৫৭৬ সারি — বড়, কিন্তু অসীম নয়। দ্বিতীয়ত, সবচেয়ে সহজ যাচাই — “ইনপুটে কয়টি record ছিল, আউটপুটে কয়টি এল” — এই ক্ষতি সঙ্গে সঙ্গে ধরতে পারত। অংশ ৮-এর প্রথম যাচাইটি (shape মেলানো) ঠিক এই কারণেই প্রথম।
তৃতীয়ত, Excel-কে ডেটার পথের মাঝখানে রাখা নিজেই ঝুঁকি। Excel মানুষের দেখা আর ছোট হিসাবের জন্য চমৎকার; কিন্তু যন্ত্র থেকে যন্ত্রে ডেটা পাঠানোর পথে এটি অপ্রয়োজনীয় রূপান্তর, type-এর অনুমান আর সীমা যোগ করে। CSV থেকে সরাসরি ডেটাবেস বা Parquet-এ নিলে সেই ধাপই থাকত না।
বাজারঘরের মতো একটি দোকানে এর সমতুল্য সহজেই কল্পনা করা যায়: উৎসবের মৌসুমে অর্ডার হঠাৎ বেড়ে গেলে কোনো পুরোনো রিপোর্ট-টেমপ্লেট বা কোনো API-র পৃষ্ঠার সীমা (ধরুন, প্রতি অনুরোধে সর্বোচ্চ ১,০০০টি record) নীরবে বাকিগুলো বাদ দিতে পারে — ঠিক যেদিন হিসাব সবচেয়ে জরুরি। প্রতিকার একই: প্রতিটি ধাপে সংখ্যা মেলানো, আর সংখ্যা না মিললে pipeline থামিয়ে দেওয়া।
মূল উপলব্ধি: ডেটা হারানো প্রায়ই নাটকীয় crash নয়, নিঃশব্দ ছাঁটাই। সারির সংখ্যা মেলানো এই পর্বের সবচেয়ে সস্তা, আর সম্ভবত সবচেয়ে মূল্যবান, পরীক্ষা।
প্র ০৫Round-trip test পাস করলেই কি লেখার কোড নির্ভুল? এই test কী ধরে, আর কী ধরে না?
Round-trip ধারণাটি সফটওয়্যারে পুরোনো: কোনো রূপান্তর আর তার উল্টো রূপান্তর পরপর চালালে মূল জিনিস ফিরে আসা উচিত — encode-decode, serialize-deserialize, compress-decompress. Property-based testing-এর ধারণায় (Haskell-এর QuickCheck, ২০০০ সালের দিকে Koen Claessen ও John Hughes; Python-এ Hypothesis library) এটি একটি ক্লাসিক “property”: যেকোনো বৈধ ইনপুটের জন্য read(write(x)) == x. এমন test হাজারো এলোমেলো ইনপুটে চালিয়ে কোণার ঘটনা খোঁজা যায় — ফাঁকা লেখা, বাংলা যুক্তাক্ষর, খুব বড় সংখ্যা, NaN.
এই পর্বে round-trip test যা ধরেছে: type-এর নীরব বদল (ফোন str থেকে int64, তারিখ থেকে লেখা), বাড়তি index কলাম, আর format-এর অনুমানের ফাঁদ (read_json-এর ফোন)। এগুলো সবচেয়ে সাধারণ ভুল, আর একটি ছোট test সেগুলো চিরতরে আটকাতে পারে।
কিন্তু test-টির সীমাও আছে। প্রথমত, এটি নিজের কোডের লেখা আর নিজের কোডের পড়া মেলায়; ফাইলটি যে অন্য দলের প্রোগ্রাম বা Excel ঠিকমতো পড়বে, তা বলে না। দ্বিতীয়ত, test-এর ডেটা যত সরল, ধরার ক্ষমতা তত কম — তিন সারির নিখুঁত টেবিলে NaN, ফাঁকা লেখা বা time zone না থাকলে সেগুলোর ভুল ধরা পড়বে না। তৃতীয়ত, দুই দিকেই একই ভুল থাকলে test পাস করে: যেমন, লেখার সময় সময়কে ঢাকা থেকে UTC-তে না বদলে offset ছাড়া লিখলেন, আর পড়ার সময়ও offset ছাড়া পড়লেন — মান মিলবে, কিন্তু অন্য কেউ ফাইলটি UTC ধরে পড়লে ছয় ঘণ্টার ভুল। চতুর্থত, assert_frame_equal default-এ দশমিক সংখ্যায় সামান্য পার্থক্য সহ্য করে (rtol), যা কখনো কখনো আপনি চান না।
তাই round-trip test-কে একটি স্তর ভাবুন, পুরো দেয়াল নয়। সঙ্গে রাখুন একটি লিখিত schema (কলাম, type, একক — যেমন “price_paisa: int64, পয়সায়”), অন্য পক্ষের সঙ্গে একটি নমুনা ফাইল বিনিময়, আর test ডেটায় ইচ্ছা করে কঠিন মান: NaN, বাংলা লেখা, শূন্য দিয়ে শুরু হওয়া ID, মাসের শেষ দিন।
মূল উপলব্ধি: Round-trip test প্রমাণ করে আপনার লেখা আর পড়া একে অপরের সঙ্গে সৎ; ফাইলটি পৃথিবীর সঙ্গে সৎ কি না, তা প্রমাণ করতে লাগে schema আর অন্য পক্ষের সঙ্গে যাচাই।
অনুশীলন
হিসাব, আগাম অনুমান, কোড আর নকশা-সমালোচনা — চার রকম কাজ।
-
হিসাব করুন: বাজারঘরের একটি CSV-তে ২৫,০০,০০০ সারি। আপনি
usecolsদিয়ে ৪টিint64কলাম পড়বেন,chunksize=300_000. (ক) কয়টি chunk আসবে, শেষটিতে কয়টি সারি? (খ) একটি পূর্ণ chunk-এর এই ৪টি কলাম মেমরিতে মোটামুটি কত MB? (গ) একই ৪টি কলাম একবারে পড়লে কত MB?(ক) ২৫,০০,০০০ ÷ ৩,০০,০০০ = ৮.৩৩…, তাই ৮টি পূর্ণ chunk আর একটি শেষ chunk — মোট ৯টি। শেষটিতে ২৫,০০,০০০ − ৮ × ৩,০০,০০০ = ১,০০,০০০ সারি।
(খ) ৩,০০,০০০ × ৪ × ৮ byte = ৯৬,০০,০০০ byte ≈ ৯.৬ MB (index আর অন্য বাড়তি খরচ বাদে)।
(গ) ২৫,০০,০০০ × ৪ × ৮ = ৮,০০,০০,০০০ byte = ৮০ MB. অর্থাৎ chunk-এ পড়লে একসঙ্গে মেমরিতে থাকে প্রায় আট ভাগের এক ভাগ। আর
usecolsনা দিয়ে ২০টি কলাম পড়লে প্রতিটি সংখ্যা পাঁচ গুণ হতো। -
আগাম বলুন (predict the output): নিচের কোড চালানোর আগে লিখে ফেলুন কী ছাপা হবে, তারপর ওপরের যেকোনো Pyodide কোড-বাক্সে পেস্ট করে মিলিয়ে নিন।
import io import pandas as pd x = pd.DataFrame({"d": ["Dhaka", "Khulna"], "n": [3, 5]}).set_index("d") x.to_csv("p.csv") y = pd.read_csv("p.csv") print(y.columns.tolist(), y.shape) z = pd.read_csv(io.StringIO("id,qty\n1,2\n2,-\n3,NA\n")) print(z["qty"].dtype, z["qty"].isna().sum())এখানে index-এর একটি নাম আছে (
d), তাইto_csvheader-এ সেই নামই লেখে —Unnamed: 0আসে না, আর জেলার নাম সাধারণ কলাম হয়ে ফেরে:['d', 'n'] (2, 2). দ্বিতীয় অংশেNAdefault-এ missing, কিন্তু-নয়; তাই কলামে একটি লেখা থেকে যায়, dtypestr, আর missing মাত্র ১টি। (ল্যাপটপের pandas 3.1.1-এ যাচাই করা।)['d', 'n'] (2, 2) str 1 -
কোড লিখুন: একটি function
load_partner(path)লিখুন, যা অংশ ২-এরpartner_orders.csvঠিকঠাক পড়বে,price_paisaকলাম বানাবে, আর তিনটি যাচাই করবে: সব ফোন ১১ অক্ষরের ও0দিয়ে শুরু,order_id-এ কোনো পুনরাবৃত্তি নেই, আর সারির সংখ্যা একটি দেওয়া প্রত্যাশিত সংখ্যার সমান। তারপর ফলটি Parquet-এ লিখে আবার পড়ে round-trip test করুন।একটি সম্ভাব্য সমাধান (পাতার প্রথম কোড-বাক্সে পেস্ট করে চালানো যায়; Pyodide-এ Parquet লেখা-পড়া হয়েছে সরাসরি pyarrow দিয়ে, অংশ ৬-এর কারণে):
import pandas as pd import pyarrow as pa, pyarrow.parquet as pq from pandas.testing import assert_frame_equal def load_partner(path, expected_rows): df = pd.read_csv(path, sep=";", skiprows=1, dtype={"order_id": str, "phone": str}, parse_dates=["date"], na_values=["-", "N/A"], thousands=",") df["price_paisa"] = (df["price_tk"] * 100).astype("Int64") bad = ~df["phone"].str.fullmatch(r"0\d{10}") if bad.any(): raise ValueError(f"bad phone rows: {df.index[bad].tolist()}") if df["order_id"].duplicated().any(): raise ValueError("duplicate order_id") if len(df) != expected_rows: raise ValueError(f"expected {expected_rows}, got {len(df)}") return df df = load_partner("partner_orders.csv", expected_rows=4) pq.write_table(pa.Table.from_pandas(df, preserve_index=False), "p.parquet") assert_frame_equal(df, pq.read_table("p.parquet").to_pandas()) print(df.shape, "round trip OK")প্রত্যাশিত ফল
(4, 8) round trip OK. খেয়াল করুন, যাচাই ব্যর্থ হলে function খারাপ সারি চুপচাপ ফেলে দেয় না — error দিয়ে থামে আর কারণ বলে। প্রত্যাশিত সারির সংখ্যা আসে বাইরের উৎস থেকে (অংশীদারের চালান, বা ডেটাবেসেরCOUNT(*)), ফাইল থেকে নয় — নইলে যাচাইটি নিজের সঙ্গে নিজেকেই মেলাবে। -
নকশা-সমালোচনা: একজন সহকর্মীর প্রতিদিনের রিপোর্টের কোড:
df = pd.read_sql("SELECT * FROM orders", con); তারপরdf[df.district == user_choice]; তারপরdf.groupby("payment").sum(); শেষেdf.to_csv("report.csv"), যা হিসাব বিভাগ Excel-এ খোলেন। ব্যবহারকারীর বাছাই করা জেলা অন্য একটি রিপোর্টে তিনিf"SELECT ... WHERE district = '{user_choice}'"দিয়ে পাঠান। কী কী সমস্যা দেখছেন, আর কী বদলাবেন?- সব সারি টেনে আনা:
SELECT *পুরো টেবিল আনে, যদিও দরকার একটি জেলার সারাংশ।WHERE district = ?, দরকারি কলাম আরGROUP BY paymentSQL-এ নিন; ফল কয়েকটি সারি। - Injection: f-string-এ ব্যবহারকারীর মান — সরাসরি SQL injection-এর দরজা।
params=(user_choice,)ব্যবহার করুন; জেলার নাম জানা তালিকা থেকে মিলিয়ে নিন। groupby().sum()সব কলামে:order_idবা ফোনের মতো কলামও যোগ হতে পারে, বা লেখার কলামে error. কোন কলামের কী হিসাব, তা নির্দিষ্ট করুন।- CSV-তে index আর Excel: groupby-র পর payment index-এ —
index=Falseদিলে নাম হারাবে, না দিলে ঠিক আছে, কিন্তু সিদ্ধান্তটি সচেতন হওয়া দরকার;reset_index()পরিষ্কার। বাংলা বা বিশেষ অক্ষর থাকলে Excel-এর জন্যencoding="utf-8-sig", বা সরাসরিto_excel. - যাচাই নেই: ফেরত সারির সংখ্যা, মোট টাকা ডেটাবেসের একটি স্বতন্ত্র
SUM-এর সঙ্গে মেলানো, আর একটি round-trip test যোগ করুন। Connection শেষে বন্ধ করুন, password কোডে নয়, environment variable বা secret store-এ রাখুন।
- সব সারি টেনে আনা:
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমনতুন একটি ডেটা উৎস থেকে নিরাপদে ডেটা আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔍 ফাইলের প্রথম কয়েকটি লাইন বা টেবিলের schema খুলে দেখা
- 📥
sep,dtype,parse_dates,na_valuesস্পষ্ট করে পড়া (SQL হলেparams-সহ) - ✅
shape,dtypes,isna().sum()উৎসের সঙ্গে মেলানো - 💾 ফল লেখা — index-এর সিদ্ধান্ত সচেতনভাবে, সম্ভব হলে Parquet-এ
- 🔁 লেখা ফাইল আবার পড়ে
assert_frame_equalদিয়ে round-trip test
একটি DataFrame-এ কলাম order_id আর qty, index 0, 1, 2. df.to_csv("a.csv") লিখে pd.read_csv("a.csv") দিয়ে পড়লে কলামের তালিকা কী হবে?
['order_id', 'qty']['Unnamed: 0', 'order_id', 'qty']['index', 'order_id', 'qty']- একটি
ParserError
একটি JSON Lines ফাইলে আছে {"phone": "01700000000"} (quote-সহ)। pandas 3-এ pd.read_json(path, lines=True) কোনো dtype ছাড়া পড়লে phone কলামে কী আসে?
str, মান'01700000000'- NaN
int64, মান1700000000float64, মান1700000000.0
pandas-এর df.to_json(path, lines=True)-এ lines=True কোন orient-এর সঙ্গে বৈধ?
"records""split""columns""table"
Python-এর sqlite3 connection con আর ব্যবহারকারীর দেওয়া জেলার নাম d. নিচের কোনটি নিরাপদ আর সঠিক?
pd.read_sql(f"SELECT * FROM orders WHERE district = '{d}'", con)pd.read_sql("SELECT * FROM orders WHERE district = '" + d + "'", con)- মান থেকে
'চিহ্নreplaceদিয়ে সরিয়ে তারপর f-string pd.read_sql("SELECT * FROM orders WHERE district = ?", con, params=(d,))
১০,০০,০০০ সারির একটি CSV pd.read_csv(path, chunksize=300_000) দিয়ে পড়লে কয়টি chunk আসবে?
- ৩টি
- ৪টি
- কলামের সংখ্যার সমান
- ১টি, কারণ chunksize শুধু মেমরির সীমা
একটি CSV-র price কলামে মানগুলো 350, -, 1,250. কোনো option ছাড়া read_csv-এ কলামটি লেখা (str) হয়ে আসছে। কোন option-জোড়া দিলে এটি সংখ্যার কলাম হবে, আর - হবে NaN?
dtype={"price": float}parse_dates=["price"]na_values=["-"]আরthousands=","encoding="utf-8-sig"
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- প্রতিটি format-এর জোড়া:
pd.read_*পড়ে DataFrame দেয়,df.to_*লেখে। Excel-এ openpyxl, Parquet-এ pyarrow লাগে। read_csv-কে বলে দিন:sep,skiprows,dtype,parse_dates,na_values,thousands. ফোন আর ID সবসময়str.to_csv,to_excelআরto_sql-এ index default-এ লেখা হয়:index=False, তবে অর্থবহ index থাকলে আগেreset_index().- JSON Lines =
orient="records", lines=True; পড়ার সময়dtypeদিন, লেখার সময়date_format="iso". SQL-এ মান সবসময়params=দিয়ে, আর filter ও যোগফল সম্ভব হলে ডেটাবেসেই। - পড়ার পর
shape,dtypes,isna().sum(); লেখার পর round trip আরassert_frame_equal. বড় ফাইলেchunksize.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২২ · বাছাই আর filter — loc, iloc, query আর boolean maskপরবর্তী পর্বDataFrame থেকে দরকারি সারি আর কলাম বের করা।
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ১০ · ডেটার ফাইল — CSV, JSON, Parquet আর বাংলা লেখার encodingআগের ভিত্তিformat-গুলোর ভেতরের গঠন, UTF-8 আর BOM.
- DBMS & SQL · Aggregation — GROUP BY আর HAVINGপাশের ট্র্যাকডেটাবেসেই সারাংশ বানানোর পুরো ভাষা।
- Cybersecurity · Injection প্রতিরক্ষাপাশের ট্র্যাকparameterized query কেন আর কোথায় যথেষ্ট নয়।