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

ডেটা পড়া আর লেখা — CSV, Excel, JSON, SQL আর Parquet

Reading and Writing Data with pandas — CSV, Excel, JSON, SQL and Parquet

Taught in Bangla, this lesson covers reading and writing tables with pandas: the read_csv options that prevent silent errors, the to_csv index trap, Excel sheets, JSON Lines, Parquet, and SQLite with parameterized queries. Every example is backed by real runs in the browser or on a laptop, plus round-trip tests and chunked reading.

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

  • 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

pd.read_* থেকে DataFrame, যাচাই, তারপর df.to_* CSV Excel JSON SQL Parquet pd.read_*() DataFrame idphoneqty 1001017000000002 যাচাই · check shape · dtypes · isna() df.to_*() লেখা ফাইল · file ↺ round trip: আবার পড়ে মেলান
ছবি ২১.১ — পাঁচটি উৎস, একটি DataFrame. মাঝের হলুদ ধাপটি বাদ দিলে ভুল ডেটা নিঃশব্দে পরের সব কাজে ঢোকে; লাল ভাঙা তীর হলো round-trip test — লেখা ফাইল আবার পড়ে মূলের সঙ্গে মেলানো।
Formatপড়ালেখাবাড়তি package
CSV / TSVpd.read_csvdf.to_csvলাগে না
Excel (xlsx)pd.read_exceldf.to_excelopenpyxl
JSON / JSON Linespd.read_jsondf.to_jsonলাগে না
Parquetpd.read_parquetdf.to_parquetpyarrow
SQL ডেটাবেসpd.read_sqldf.to_sqlSQLite-এ লাগে না; অন্যগুলোয় 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যে প্রশ্নের উত্তরউদাহরণ
sepfield কোন চিহ্নে আলাদা?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=","
encodingbyte কোন নিয়মে লেখা?"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 কীভাবে বদলায়। প্রতিটি ধাপের ফল নিচের কোড-বাক্সের সত্যিকারের আউটপুট থেকে নেওয়া।

কোনো option ছাড়াpd.read_csv(f)
shape (5, 2)
columns: ['# partner export',
          ' 2026-11-01']
প্রথম লাইনটিই header হয়ে গেছে
sep=";", skiprows=1৭টি কলাম
        phone  qty price_tk
0  1700000000  2.0    1,250
1  1800000001  1.0        -
phone int64 · qty float64
price_tk str
+ dtypephone, order_id → str
         phone
0  01700000000
1  01800000001
শুরুর শূন্য ফিরে এসেছে
+ na_values, thousandsprice_tk float64
   qty  price_tk
0  2.0    1250.0
1  1.0       NaN   ← "-"
2  3.0     780.0
3  NaN    2400.0   ← "N/A"
+ parse_datesdate datetime64[us]
        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 ✓
  1. অনুমানের ফল। কোনো option ছাড়া pandas ধরে নেয় প্রথম লাইনই header আর delimiter কমা। মন্তব্যের লাইনে একটি কমা ছিল, তাই কলাম হলো দুটি: '# partner export' আর ' 2026-11-01'. কোনো error নেই — শুধু অর্থহীন টেবিল।
  2. Delimiter আর বাজে লাইন। sep=";" আর skiprows=1 দিলে ৭টি কলাম ঠিকঠাক আসে। কিন্তু pandas এখনো অনুমান করছে: ফোন নম্বর int64 হয়ে শূন্য হারিয়েছে, qty একটি ফাঁকা মানের জন্য float64, আর price_tk “1,250” আর “-” থাকায় লেখা (str).
  3. Type বলে দেওয়া। dtype={"order_id": str, "phone": str} — ফোন নম্বর আর ID গণনার সংখ্যা নয়, পরিচয়। এদের যোগ-বিয়োগের কোনো অর্থ নেই, তাই লেখা হিসেবে রাখাই ঠিক। এখন 01700000000 অক্ষত।
  4. “নেই” আর হাজারের কমা। na_values=["-", "N/A"] বলে এই দুটি লেখা মানে missing; thousands="," বলে “1,250” মানে ১২৫০. ফলে price_tk এখন সংখ্যা (float64), আর দুটি ফাঁকা ঘর NaN.
  5. তারিখ আর পয়সা। parse_dates=["date"] লেখাকে আসল তারিখে (datetime64[us]) রূপ দেয়, যাতে পরে মাস বা সপ্তাহ ধরে হিসাব করা যায়। টাকাকে পয়সায় নিয়ে পূর্ণসংখ্যা রাখা হলো: ১২৫০ × ১০০ = ১,২৫,০০০; ফাঁকা দামের জায়গায় <NA>.
  6. বিশ্বাসের আগে গোনা। ৪টি সারি, ৮টি কলাম (মূল ৭ + 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-সহ লেখা থাকলেও default read_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 হয়ে ফেরে।

SQLite-এর orders টেবিল৮টি সারি
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
query আর মান আলাদাparams=("card",)
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"  (ডেটা হিসেবে)
WHERE payment = '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
GROUP BY district৩টি দল
Khulna: 151600 + 107200
Sylhet:  90800 +  84300
Dhaka :  46000
COUNT, SUM, ORDER BYবড় থেকে ছোট
Khulna  n=2  total=258800
Sylhet  n=2  total=175100
Dhaka   n=1  total= 46000
read_sql-এর ফল৩টি সারি এল, ৮টি নয়
  district  n  total_paisa
0   Khulna  2       258800
1   Sylhet  2       175100
2    Dhaka  1        46000
district str · n int64
total_paisa int64
  1. ডেটাবেসের টেবিল। to_sql দিয়ে লেখা বাজারঘরের ৮টি অর্ডার (seed 42 দিয়ে বানানো)। দাম পয়সায়, পূর্ণসংখ্যা। ডেটা এখন ডেটাবেসে, pandas-এর মেমরিতে নয়।
  2. Query আর মান আলাদা পথে। Query-র লেখায় শুধু ?; "card" যায় params দিয়ে। ডেটাবেস আগে query-র গঠন বোঝে, তারপর ?-এর জায়গায় মানটি বসায় — মানের ভেতরে quote বা OR থাকলেও সেটি শুধু লেখা।
  3. WHERE — আগে বাছাই। ৮টি সারির ৫টির payment card. প্রতিটির মোট দাম qty × price_paisa: যেমন ১০০৪-এ ২ × ৭৫,৮০০ = ১,৫১,৬০০ পয়সা।
  4. GROUP BY — জেলা ধরে ভাগ। খুলনার দুটি, সিলেটের দুটি, ঢাকার একটি সারি। এই split-apply-combine ধারণার pandas রূপ আসবে পর্ব ২৪-এ।
  5. যোগফল আর সাজানো। খুলনা ১,৫১,৬০০ + ১,০৭,২০০ = ২,৫৮,৮০০; সিলেট ৯০,৮০০ + ৮৪,৩০০ = ১,৭৫,১০০; ঢাকা ৪৬,০০০. ORDER BY ... DESC বড় থেকে ছোটতে সাজায়। টাকায়: খুলনা ২,৫৮৮ টাকা।
  6. ফেরত 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 থেকে।

মূল DataFrame৪টি কলাম
order_id  int64
phone     str
date      datetime64[us]
district  category
to_csv(index=False)ফাইলে শুধু লেখা
order_id,phone,date,district
1001,01700000000,2026-10-03,Dhaka
1002,01800000001,2026-10-04,Khulna
read_csv — কিছু না বলেpandas অনুমান করল
order_id  int64
phone     int64   ← ছিল str
date      str     ← ছিল datetime
district  str     ← ছিল category
df.equals(back)শুধু হ্যাঁ/না
False
কেন, কোথায় — কিছুই বলে না
assert_frame_equalপ্রথম অমিল
Attributes of DataFrame.iloc[:, 1]
(column name="phone") are different
Attribute "dtype" are different
[left]:  <StringDtype(na_value=nan)>
[right]: int64
পড়ার সময় বলে দেওয়াবা Parquet
pd.read_csv(f,
  dtype={"phone": str,
         "district": "category"},
  parse_dates=["date"])
assert_frame_equal → OK
  1. মূল। চারটি কলাম, চার রকম type: পূর্ণসংখ্যা, লেখা, তারিখ আর category (কয়েকটি নির্দিষ্ট মানের কলাম, পর্ব ২৭-এ বিস্তারিত)।
  2. CSV-তে লেখা। ফাইলে type বলে কিছু নেই — সব লেখা। ফোন নম্বর শূন্যসহ ঠিকই লেখা হয়েছে; সমস্যা আসবে পড়ার সময়।
  3. আবার পড়া। কিছু না বলে পড়লে চারটির তিনটি কলামের type বদলে গেছে: ফোন সংখ্যা হয়ে শূন্য হারিয়েছে, তারিখ আর জেলা সাধারণ লেখা।
  4. equals. False — ঠিক উত্তর, কিন্তু খোঁজার কোনো সূত্র নেই। দ্রুত পরীক্ষার জন্য ভালো, ভুল খোঁজার জন্য নয়।
  5. assert_frame_equal. প্রথম অমিলেই থামে আর বলে: কলাম ১ (phone), বৈশিষ্ট্য dtype, বাঁয়ে str, ডানে int64. এটি ঠিক করে আবার চালালে পরের অমিল বলবে — date, তারপর district.
  6. সমাধান। পড়ার সময় তিনটি কলামের 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_csv index-সহ লিখেছিল; লেখার সময় 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-এ শুধু কলামের নাম আর বানানো নমুনা আছে:

আমি pandas 3.x দিয়ে একটি অংশীদারের CSV পড়ব। ফাইলের প্রথম লাইনে একটি মন্তব্য, delimiter সেমিকোলন। কলাম: order_id, date (YYYY-MM-DD), district (বাংলা), phone (যেমন 01700000000, শুরুর শূন্যসহ রাখতে হবে), qty, price_tk (হাজারে কমা, যেমন 1,250), payment. "নেই" বোঝাতে "-" বা "N/A" থাকে। ১. read_csv-এর কোড দাও, প্রতিটি option কেন লাগছে এক লাইনে বলো। ২. পড়ার পর shape, dtypes, isna().sum() আর ফোনের দৈর্ঘ্য যাচাইয়ের কোড দাও। ৩. ফল Parquet-এ লিখে আবার পড়ে assert_frame_equal দিয়ে মেলানোর ছোট test দাও। ৪. শুধু বর্তমান pandas-এর API ব্যবহার করো; কোনো parameter নিয়ে নিশ্চিত না হলে "নিশ্চিত নই" লেখো। warning চাপা দেবে না।

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 আর অন্য পক্ষের সঙ্গে যাচাই।

অনুশীলন

হিসাব, আগাম অনুমান, কোড আর নকশা-সমালোচনা — চার রকম কাজ।

  1. হিসাব করুন: বাজারঘরের একটি CSV-তে ২৫,০০,০০০ সারি। আপনি usecols দিয়ে ৪টি int64 কলাম পড়বেন, chunksize=300_000. (ক) কয়টি chunk আসবে, শেষটিতে কয়টি সারি? (খ) একটি পূর্ণ chunk-এর এই ৪টি কলাম মেমরিতে মোটামুটি কত MB? (গ) একই ৪টি কলাম একবারে পড়লে কত MB?

    (ক) ২৫,০০,০০০ ÷ ৩,০০,০০০ = ৮.৩৩…, তাই ৮টি পূর্ণ chunk আর একটি শেষ chunk — মোট ৯টি। শেষটিতে ২৫,০০,০০০ − ৮ × ৩,০০,০০০ = ১,০০,০০০ সারি।

    (খ) ৩,০০,০০০ × ৪ × ৮ byte = ৯৬,০০,০০০ byte ≈ ৯.৬ MB (index আর অন্য বাড়তি খরচ বাদে)।

    (গ) ২৫,০০,০০০ × ৪ × ৮ = ৮,০০,০০,০০০ byte = ৮০ MB. অর্থাৎ chunk-এ পড়লে একসঙ্গে মেমরিতে থাকে প্রায় আট ভাগের এক ভাগ। আর usecols না দিয়ে ২০টি কলাম পড়লে প্রতিটি সংখ্যা পাঁচ গুণ হতো।

  2. আগাম বলুন (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_csv header-এ সেই নামই লেখে — Unnamed: 0 আসে না, আর জেলার নাম সাধারণ কলাম হয়ে ফেরে: ['d', 'n'] (2, 2). দ্বিতীয় অংশে NA default-এ missing, কিন্তু - নয়; তাই কলামে একটি লেখা থেকে যায়, dtype str, আর missing মাত্র ১টি। (ল্যাপটপের pandas 3.1.1-এ যাচাই করা।)

    ['d', 'n'] (2, 2)
    str 1
  3. কোড লিখুন: একটি 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(*)), ফাইল থেকে নয় — নইলে যাচাইটি নিজের সঙ্গে নিজেকেই মেলাবে।

  4. নকশা-সমালোচনা: একজন সহকর্মীর প্রতিদিনের রিপোর্টের কোড: 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 payment SQL-এ নিন; ফল কয়েকটি সারি।
    • 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-এ রাখুন।

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

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

ক্রমনতুন একটি ডেটা উৎস থেকে নিরাপদে ডেটা আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔍 ফাইলের প্রথম কয়েকটি লাইন বা টেবিলের schema খুলে দেখা
  2. 📥 sep, dtype, parse_dates, na_values স্পষ্ট করে পড়া (SQL হলে params-সহ)
  3. ✅ shape, dtypes, isna().sum() উৎসের সঙ্গে মেলানো
  4. 💾 ফল লেখা — index-এর সিদ্ধান্ত সচেতনভাবে, সম্ভব হলে Parquet-এ
  5. 🔁 লেখা ফাইল আবার পড়ে 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, মান 1700000000
  • float64, মান 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"

শব্দকোষ

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

মনে রাখুন

  1. প্রতিটি format-এর জোড়া: pd.read_* পড়ে DataFrame দেয়, df.to_* লেখে। Excel-এ openpyxl, Parquet-এ pyarrow লাগে।
  2. read_csv-কে বলে দিন: sep, skiprows, dtype, parse_dates, na_values, thousands. ফোন আর ID সবসময় str.
  3. to_csv, to_excel আর to_sql-এ index default-এ লেখা হয়: index=False, তবে অর্থবহ index থাকলে আগে reset_index().
  4. JSON Lines = orient="records", lines=True; পড়ার সময় dtype দিন, লেখার সময় date_format="iso". SQL-এ মান সবসময় params= দিয়ে, আর filter ও যোগফল সম্ভব হলে ডেটাবেসেই।
  5. পড়ার পর shape, dtypes, isna().sum(); লেখার পর round trip আর assert_frame_equal. বড় ফাইলে chunksize.

পরের পর্বে: ডেটা এখন DataFrame-এ, যাচাই করা। এবার তার ভেতর থেকে ঠিক যা দরকার তা বের করা — loc, iloc, boolean mask আর query দিয়ে সারি আর কলাম বাছাই।

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

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