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

বড় ডেটায় ML — out-of-core, incremental learning আর Polars/DuckDB

Machine Learning at Scale — Memory, Chunked Processing, Incremental Learning, Parquet, Polars and DuckDB

Taught in Bangla, this lesson handles data that does not fit in RAM: memory per row with dtypes, exact chunked aggregation, Welford's streaming variance, sampling and incremental learning with partial_fit, all run in the browser. It shows Parquet row groups and pushdown, compares pandas, Polars and DuckDB on a real ten-million-row desktop run, and covers when a cluster is worth it.

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

  • DataFrame-এর memory প্রতি সারির byte ধরে হিসাব করা, আর category, downcast ও bool দিয়ে কমানো
  • Chunk-এ পড়ে সঠিক জেলাভিত্তিক হিসাব, কোন statistic জোড়া লাগে আর কোনটি লাগে না, আর Welford-এর streaming variance
  • Prototype-এর জন্য sampling: সরল, stratified আর reservoir sampling, আর কখন নমুনা ভুল পথ দেখায়
  • partial_fit, SGDClassifier আর HashingVectorizer দিয়ে RAM-এ না ধরা ডেটায় model শেখানো, আর তার ফাঁদ
  • Parquet-এর row group আর statistics, Polars lazy আর DuckDB SQL, core-এ ভাগ করা কাজ, Dask/Spark, আর তিন আকারের ডেটায় টুল বাছাই

এই মডিউলের অন্য পর্বগুলো model-কে কাজে নামায়: পর্ব ৬৮ (Model serving — FastAPI, batch scoring আর ONNX) আর পর্ব ৬৯ (ML system-এর test আর নজরদারি — data test, model test আর drift). এই পর্বের প্রশ্ন আলাদা: ডেটা যখন ল্যাপটপের RAM-এ ধরে না, তখন কী করবেন? pandas-এর dtype, category, chunk আর Polars-এর প্রথম পরিচয় AI-এর ভিত্তি কোর্সের পর্ব ২৭-এ আছে, Parquet পড়া-লেখা একই কোর্সের পর্ব ২১-এ। এখানে সেগুলো ধরে নিয়ে আরও গভীরে যাব। চারটি চালানো যায় এমন ব্লক পাতাতেই চলে (ব্রাউজারের Python); প্রথমবার scikit-learn, Polars আর DuckDB নামতে প্রতিটিতে দশ সেকেন্ডের মতো লাগতে পারে। ১ কোটি সারির মাপ ডেস্কটপে সত্যিকারের run.

১ · ডেটা কখন “বড়” — RAM, কাজের memory আর চারটি পথ

ধান কাটার পর পুরো ফসল রান্নাঘরে আনা হয় না। গোলায় থাকে, আর রান্নার জন্য একবারে এক ঝুড়ি আসে। হিসাব রাখা হয় খাতায়: কত মণ উঠল, কত খরচ হলো। বড় ডেটার কাজও তেমন: পুরো ডেটা থাকে ডিস্কে (গোলা), RAM-এ আসে একবারে এক টুকরো (ঝুড়ি), আর মনে রাখা হয় শুধু ছোট একটি সারাংশ (খাতা)।

“বড় ডেটা” কোনো নির্দিষ্ট সংখ্যা নয়; এটি আপনার যন্ত্রের সঙ্গে তুলনা। ১০ GB-এর CSV একটি ৬৪ GB RAM-এর server-এ সাধারণ ডেটা, কিন্তু ৮ GB-এর ল্যাপটপে বড়। আর ফাইলের আকার RAM-এর চাহিদার সমান নয়, তিনটি কারণে। (ক) CSV পড়ার পর আকার বদলায়: সংখ্যা ছোট হতে পারে, লেখার কলাম অনেক বড়। (খ) pandas-এর অনেক কাজ (filter, merge, নতুন কলাম) নতুন copy বানায়, তাই কাজের সময় memory টেবিলের কয়েক গুণ। pandas-এর স্রষ্টা Wes McKinney ২০১৭ সালের একটি লেখায় মোটামুটি নিয়ম দিয়েছিলেন: ডেটার আকারের ৫ থেকে ১০ গুণ RAM রাখুন। এটি অভিজ্ঞতার নিয়ম, মাপা সূত্র নয়। (গ) model শেখানোর সময় feature matrix আর তার ভেতরের copy আরও জায়গা নেয়।

ডেটা RAM ছাড়িয়ে গেলে চারটি পথ, সরল থেকে জটিল। (১) ছোট করুন: ঠিক dtype, দরকারি কলাম, Parquet. (২) টুকরো করুন: chunk-এ পড়ে প্রতিটি থেকে ছোট সারাংশ; model-এর ক্ষেত্রে out-of-core learning, অর্থাৎ ডিস্ক থেকে টুকরো টুকরো এনে শেখা। (৩) নমুনা নিন ধারণা যাচাইয়ের জন্য। (৪) টুল বা যন্ত্র বদলান: Polars বা DuckDB, সব core, আর সত্যিই দরকার হলে cluster. এই পর্ব এই ক্রমেই চলবে।

২ · প্রতি সারিতে কত byte — dtype দিয়ে memory-র হিসাব

একটি DataFrame-এর memory মোটামুটি সারির সংখ্যা × প্রতি সারির byte, আর তা আসে প্রতিটি কলামের dtype থেকে: int64 আর float64 ৮ byte, float32 ৪, uint8 আর bool ১. লেখার কলামে লেখাটি নিজে আর একটি ঠিকানা। pandas-এ এই আসল আকার দেখায় memory_usage(deep=True); deep ছাড়া লেখার কলামের ভেতরের অংশ গোনা হয় না। কমানোর তিনটি হাতিয়ার AI-এর ভিত্তি কোর্সের পর্ব ২৭-এ শেখানো: downcasting (সবচেয়ে ছোট যে dtype-এ সব মান ধরে), অল্প আলাদা মানের লেখায় category dtype, আর হ্যাঁ/না কলামে bool. নিচের ব্লক বাজারঘরের (কাল্পনিক দোকান) ১ লাখ অর্ডারে তিনটিই চালিয়ে প্রতি কলামের প্রতি সারির byte দেখায়।

import numpy as np
import pandas as pd

rng = np.random.default_rng(70)            # বাজারঘরের (কাল্পনিক) ১ লাখ অর্ডার
n = 100_000
DISTRICTS = ["Dhaka", "Chattogram", "Sylhet", "Rajshahi",
             "Khulna", "Barishal", "Rangpur", "Mymensingh"]
df = pd.DataFrame({
    "order_id": np.arange(n, dtype=np.int64),
    "district": rng.choice(DISTRICTS, n),
    "qty": rng.integers(1, 6, n, dtype=np.int64),
    "price": rng.lognormal(6, 0.8, n).round(2),
    "returned": rng.choice(["yes", "no"], n, p=[0.04, 0.96]),
})
small = df.assign(
    order_id=pd.to_numeric(df.order_id, downcast="unsigned"),
    district=df.district.astype("category"),
    qty=pd.to_numeric(df.qty, downcast="unsigned"),
    price=df.price.astype("float32"),
    returned=df.returned.eq("yes"))

per_row = lambda d: d.memory_usage(deep=True, index=False) / n
out = pd.DataFrame({"dtype_before": df.dtypes.astype(str),
                    "B/row": per_row(df).round(1),
                    "dtype_after": small.dtypes.astype(str),
                    "B/row_after": per_row(small).round(1)})
print(out)
print("total MiB:", round(per_row(df).sum() * n / 2**20, 2),
      "->", round(per_row(small).sum() * n / 2**20, 2))
err = (small.price - df.price).abs().max()
print("max price change after float32:", round(float(err), 5), "taka")
print("same revenue by district:", np.allclose(
    df.groupby("district").price.sum(),
    small.groupby("district", observed=True).price.sum(), rtol=1e-6))

আসল আউটপুট (ব্রাউজারের Python):

         dtype_before  B/row dtype_after  B/row_after
order_id        int64    8.0      uint32          4.0
district          str   32.5    category          1.0
qty             int64    8.0       uint8          1.0
price         float64    8.0     float32          4.0
returned          str   27.0        bool          1.0
total MiB: 7.97 -> 1.05
max price change after float32: 0.00047 taka
same revenue by district: True

মোট memory ৭.৯৭ MiB থেকে ১.০৫ MiB, প্রায় সাড়ে সাত ভাগের এক ভাগ। সবচেয়ে বড় লাভ লেখার দুটি কলামে: জেলার নাম প্রতি সারিতে ৩২.৫ byte থেকে ১ byte, কারণ category আটটি নাম একবার রাখে আর প্রতি সারিতে শুধু ০–৭-এর একটি কোড। দামের ক্ষতি সর্বোচ্চ ০.০০০৪৭ টাকা: float32 প্রায় ৭টি অর্থবহ অঙ্ক রাখে, তাই কয়েক হাজার টাকার দামে পয়সার হিসাব ঠিক থাকে, কিন্তু কোটি টাকার যোগফল float32-এ জমালে ভুল জমতে পারে। শেষ লাইন যাচাই করে, জেলাভিত্তিক আয় দুই টেবিলে একই (আপেক্ষিক পার্থক্য ১০−৬-এর মধ্যে)। Category-র groupby-তে observed=True দেওয়া হয়েছে, যাতে শুধু যে জেলা ডেটায় আছে সেগুলো আসে।

লেখার কলামের সংখ্যা যন্ত্রভেদে আলাদা: ব্রাউজারের Python ৩২-bit WebAssembly-তে চলে, আর এখানে pyarrow load হয়নি, তাই pandas 3-এর str কলাম প্রতিটি লেখাকে Python object হিসেবে রাখে: ৪ byte-এর ঠিকানা আর একটি ছোট Python string (যেমন "Dhaka" এখানে ২৬ byte). একই কোড ডেস্কটপে (pandas 3.1.1, pyarrow আছে) চালালে str কলাম Arrow-এ থাকে: জেলা ১৫.৫ আর returned ১০.০ byte প্রতি সারি, মোট ৪.৭৩ MiB. পুরোনো object dtype-এ জেলা ৫৬.৫ byte. সংখ্যার কলাম আর পরের সব সংখ্যা দুই জায়গাতেই এক। শিক্ষা: memory নিজের যন্ত্রে নিজে মাপুন; ব্লগের সংখ্যা ধার করবেন না।

নিচের stepper প্রতিটি কলামের সিদ্ধান্ত আর ১ কোটি সারির হিসাব দেখায়; এমন হিসাব বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় প্রায়ই আসে।

প্রতি সারি (B), ব্রাউজারের Python১ লাখ সারি

            
  1. শুরু। প্রতি সারিতে মোট ৮৩.৫ byte; লেখার দুটি কলামই ৫৯.৫, সংখ্যার তিনটি মিলে মাত্র ২৪।
  2. order_id. সবচেয়ে বড় মান ৯৯,৯৯৯, যা uint32-এ (সর্বোচ্চ প্রায় ৪৩০ কোটি) আরামে ধরে। ৮ থেকে ৪ byte. সাবধান: ভবিষ্যতে id ৪৩০ কোটি পেরোলে overflow হবে, তাই id-তে সীমার কাছাকাছি গেলে downcast না করাই ভালো।
  3. district. আটটি আলাদা মান, এক লাখ সারি। Category-তে প্রতি সারিতে ১ byte-এর কোড: ৩২.৫ থেকে ১.০, সবচেয়ে বড় লাভ। প্রায় সব মান আলাদা হলে (ফোন নম্বর) category উল্টো ক্ষতি করে।
  4. qty আর price. পরিমাণ ১–৫, তাই uint8. দাম float32-এ, সর্বোচ্চ বদল ০.০০০৪৭ টাকা। টাকার হিসাব যেখানে পয়সা পর্যন্ত মেলাতে হয়, সেখানে পয়সায় পূর্ণসংখ্যা (int32/int64) রাখা নিরাপদ বিকল্প।
  5. returned. “yes”/“no” লেখার বদলে bool: ২৭ থেকে ১ byte. মোট ৮৩.৫ থেকে ১১.০ byte প্রতি সারি, পাতার মাপের সঙ্গে মেলে (৭.৯৭ থেকে ১.০৫ MiB).
  6. এক কোটি সারি। ৮৩৫ MB থেকে ১১০ MB. কাজের সময় ৫–১০ গুণ ধরলে আগে ৪–৮ GB (৮ GB-এর ল্যাপটপে বিপদ), এখন ১ GB-এর কাছাকাছি। প্রায়ই টুল বদলানোর আগেই সমস্যা মেটে।

৩ · Chunk-এ হিসাব — কোন statistic জোড়া লাগে, কোনটি লাগে না

ছোট করার পরও না ধরলে ফাইল টুকরো করে পড়ুন: pd.read_csv(..., chunksize=70_000) একবারে ৭০ হাজার সারির একটি DataFrame দেয়। প্রতিটি chunk থেকে একটি ছোট সারাংশ রাখুন, আর শেষে সারাংশগুলো জুড়ুন। মূল প্রশ্ন: কোন সারাংশ জোড়া লাগানো যায়? যে সারাংশ দুটি অংশের সারাংশ থেকে পুরো অংশের সঠিক সারাংশ দেয়, তাকে বলে partial aggregate (জোড়া লাগানো যায় এমন সারাংশ)।

Statisticপ্রতি chunk-এ কী রাখবেনজোড়া লাগানো
count, sum, min, maxনিজেইযোগ, যোগ, min-এর min, max-এর max
গড় (mean)(sum, count)মোট sum ÷ মোট count; গড়ের গড় নয়
Variance, SD(n, mean, M2)Chan-এর সূত্র (অংশ ৪); শুধু variance-এর গড় নয়
Median, quantileপুরো মান লাগেসঠিকভাবে জোড়া লাগে না; আনুমানিক sketch (t-digest-এর মতো) লাগে
আলাদা মানের সংখ্যামানগুলোর setset-এর union; খুব বড় হলে HyperLogLog-এর মতো আনুমানিক sketch

Chunk থেকে সঠিক গড় — sum আর count জমান, গড় নয়

ঢাকার সংখ্যা, নিচের ব্লকের আসল run থেকে; টাকার মান কোটিতে গোল করা।

Chunked aggregation: keep count and sum, not means orders.csv ২,০০,০০০ সারি chunk ১৭০ হাজার chunk ২৭০ হাজার chunk ৩৬০ হাজার ঢাকারcount, sum ঢাকারcount, sum ঢাকারcount, sum মোট count = ১,২০,৩০৯ মোট sum ≈ ৬.৬৯ কোটি টাকা গড় = sum ÷ count = ৫৫৬.২২ ✓ পুরো ফাইল একবারে পড়লেও ৫৫৬.২২ ভুল পথ: প্রতিটি chunk-এর গড়, তারপর তিনটি গড়ের গড় = ৫৫৬.১৩ ✗ (chunk-এর আকার আলাদা)
ছবি ৩.১ — প্রতিটি chunk শুধু দুটি সংখ্যা দেয়, তাই memory-তে থাকে chunk-এর আকার নয়, জেলার সংখ্যার সমান ছোট একটি টেবিল। গড়ের গড় তখনই ঠিক, যখন প্রতিটি chunk-এ সেই জেলার সারি সমান; বাস্তবে তা প্রায় কখনো হয় না।

MapReduce (Google, ২০০৪) আর database-এর সমান্তরাল GROUP BY একই নিয়মে চলে: প্রতিটি অংশ আংশিক সারাংশ বানায়, তারপর সেগুলো মেলে। Variance-এর সারাংশ পরের অংশে; দুটির ব্লক অংশ ৪-এর শেষে।

৪ · Welford — এক পাসে mean আর variance

চায়ের দোকানি দিনের সব বিক্রির কাগজ জমিয়ে রাখেন না। প্রতিটি কাপের পর খাতায় শুধু দুটি সংখ্যা হালনাগাদ করেন: মোট কাপ আর মোট টাকা। ডেটার streaming statistics-ও তেমন: প্রতিটি নতুন মান একবার দেখে কয়েকটি সংখ্যা হালনাগাদ, পুরোনো মান আর লাগে না।

গড়ের জন্য এটি সহজ। Variance-এর জন্য পাঠ্যবইয়ের একটি সূত্র লোভনীয়: Σx আর Σx² জমান, শেষে variance = (Σx² − n·x̄²) ÷ (n − ১). সমস্যা হলো দুটি প্রায় সমান বিশাল সংখ্যার বিয়োগ। Floating-point সংখ্যা মোটামুটি ১৬টি অর্থবহ অঙ্ক রাখে; মানগুলো যদি ১০০ কোটির কাছে আর ছড়ানো মাত্র ১, তাহলে x² প্রায় ১০১৮, আর আসল পার্থক্যটুকু শেষ অঙ্কগুলোর সঙ্গে হারিয়ে যায়। একে বলে catastrophic cancellation. ১৯৬২ সালে B. P. Welford Technometrics-এ একটি সমাধান দেন, যা আজ Welford-এর algorithm নামে পরিচিত। প্রতিটি নতুন মান x-এর জন্য (k = এ পর্যন্ত দেখা মানের সংখ্যা, mean = চলতি গড়, M2 = গড় থেকে বর্গ দূরত্বের চলতি যোগ):

k ← k + ১;  d = x − mean;  mean ← mean + d ÷ k;  M2 ← M2 + d · (x − নতুন mean)

শেষে sample variance = M2 ÷ (k − ১). এখানে কোনো বিশাল সংখ্যা জমে না; শুধু গড় থেকে দূরত্ব, যা ছোট। দুটি chunk-এর (n, mean, M2) জোড়া লাগাতে Chan, Golub আর LeVeque-এর (১৯৭৯) সূত্র আছে: δ = meanB − meanA, তারপর M2 = M2A + M2B + δ² · nAnB ÷ (nA + nB). নিচের stepper চারটি সংখ্যায় Welford হাতে করে, যাতে খাতায়ও করতে পারেন।

1k = mean = m2 = 02for x in [4, 7, 13, 16]:3    k += 14    d = x - mean5    mean += d / k6    m2 += d * (x - mean)7var = m2 / (k - 1)
অবস্থা
x
k
d
mean
m2
  1. প্রথম মান ৪। k = ১, d = ৪ − ০ = ৪, গড় হয় ৪. M2-তে যোগ হয় ৪ × (৪ − ৪) = ০, কারণ একটি মানের কোনো ছড়ানো নেই।
  2. দ্বিতীয় মান ৭। d = ৭ − ৪ = ৩ (পুরোনো গড় থেকে দূরত্ব)। নতুন গড় ৪ + ৩/২ = ৫.৫. M2-তে যোগ d × (x − নতুন গড়) = ৩ × ১.৫ = ৪.৫. দুটি গুণনীয়ক আলাদা: একটি পুরোনো গড় থেকে, একটি নতুন গড় থেকে।
  3. তৃতীয় মান ১৩। d = ১৩ − ৫.৫ = ৭.৫, গড় ৫.৫ + ৭.৫/৩ = ৮. M2-তে ৭.৫ × (১৩ − ৮) = ৩৭.৫, মোট ৪২. চলতি variance ৪২/২ = ২১।
  4. চতুর্থ মান ১৬। d = ১৬ − ৮ = ৮, গড় ৮ + ৮/৪ = ১০. M2-তে ৮ × (১৬ − ১০) = ৪৮, মোট ৯০।
  5. ফল। Sample variance = ৯০ ÷ (৪ − ১) = ৩০. সরাসরি যাচাই: গড় ১০ থেকে দূরত্ব −৬, −৩, ৩, ৬; বর্গের যোগ ৩৬ + ৯ + ৯ + ৩৬ = ৯০. পুরো সময় memory-তে ছিল শুধু তিনটি সংখ্যা।

এবার দুটি ধারণা একসঙ্গে পরীক্ষা। ব্লকটি পাতার ভেতরেই একটি CSV লেখে (২ লাখ অর্ডার, তিনটি জেলা), তারপর তা ৭০ হাজার সারির chunk-এ পড়ে, আর পুরো ফাইল একবারে পড়ার উত্তরের সঙ্গে মেলায়। শেষে ১০০ কোটির কাছের ১ লাখ মানে Welford, NumPy আর পাঠ্যবইয়ের সূত্রের তুলনা।

import numpy as np
import pandas as pd

rng = np.random.default_rng(70)               # বাজারঘরের (কাল্পনিক) ২ লাখ অর্ডার
n = 200_000
pd.DataFrame({"district": rng.choice(["Dhaka", "Khulna", "Sylhet"], n, p=[.6, .25, .15]),
              "price": rng.lognormal(6, 0.8, n).round(2)}).to_csv("orders.csv", index=False)

parts, means = [], []
for chunk in pd.read_csv("orders.csv", chunksize=70_000):   # একবারে ৭০ হাজার সারি
    g = chunk.groupby("district").price
    parts.append(g.agg(["count", "sum"]))     # জোড়া লাগানো যায়: count, sum
    means.append(g.mean())                    # জোড়া লাগানো যায় না: mean
agg = pd.concat(parts).groupby(level=0).sum()
agg["mean"] = agg["sum"] / agg["count"]
full = pd.read_csv("orders.csv").groupby("district").price.agg(["count", "sum", "mean"])
print(len(parts), "chunks; same as full read:", np.allclose(agg, full))
print(agg.round(2))
print("mean of chunk means:", pd.concat(means).groupby(level=0).mean().round(2).to_dict())

def welford(xs):                              # এক পাসে mean আর variance
    k = mean = m2 = 0.0
    for x in xs:
        k += 1
        d = x - mean                          # পুরোনো mean থেকে দূরত্ব
        mean += d / k
        m2 += d * (x - mean)                  # পুরোনো আর নতুন দূরত্বের গুণফল
    return mean, m2 / (k - 1)

x = 1e9 + rng.normal(0, 1, 100_000)           # বড় মান, ছোট ছড়ানো
naive = (np.sum(x**2) - len(x) * x.mean()**2) / (len(x) - 1)
m, v = welford(x.tolist())
print(f"welford: mean={m:.4f}  var={v:.6f}")
print(f"numpy  : mean={x.mean():.4f}  var={x.var(ddof=1):.6f}")
print("naive sum-of-squares variance:", naive)

আসল আউটপুট (ব্রাউজারের Python):

3 chunks; same as full read: True
           count          sum    mean
district                             
Dhaka     120309  66918292.63  556.22
Khulna     49740  27641208.70  555.71
Sylhet     29951  16629507.18  555.22
mean of chunk means: {'Dhaka': 556.13, 'Khulna': 555.51, 'Sylhet': 555.41}
welford: mean=999999999.9981  var=0.991460
numpy  : mean=999999999.9981  var=0.991460
naive sum-of-squares variance: 0.0

তিনটি chunk (৭০, ৭০ আর ৬০ হাজার সারি) থেকে count আর sum জুড়ে পাওয়া টেবিল পুরো ফাইলের উত্তরের হুবহু সমান। গড়ের গড় ঢাকায় ৫৫৬.১৩, আসল ৫৫৬.২২: ছোট, কিন্তু সতর্কবার্তা ছাড়াই ভুল, আর chunk বেশি অসমান হলে বড়। Welford আর NumPy একই variance দেয় (০.৯৯১৪৬০; আসল variance ১, নমুনার কারণে সামান্য কম)। পাঠ্যবইয়ের সূত্র দেয় ঠিক ০.০: দুটি প্রায় ১০২৩ আকারের সংখ্যার বিয়োগে আসল পার্থক্য হারিয়ে গেছে। অন্য ডেটায় একই সূত্র ঋণাত্মক variance-ও দিতে পারে, আর তার বর্গমূল nan. NumPy-র var দুই পাসে (আগে গড়, পরে দূরত্বের বর্গ) হিসাব করে বলে নিরাপদ, কিন্তু streaming-এ ডেটা দুবার পড়া যায় না।

৫ · Sampling — প্রথমে ছোট নমুনায় ধারণা যাচাই

পুরো ডেটায় একটি ধারণা পরীক্ষায় এক ঘণ্টা লাগলে দিনে আটটির বেশি ধারণা যাচাই হয় না। তাই প্রথমে একটি ছোট, ভালোভাবে বাছা নমুনায় feature, model আর pipeline ঠিক করুন; পুরো ডেটা আসে শেষে। কয়েক লাখ সারিতে learning curve (AI-এর ভিত্তি কোর্সের পর্ব ৬৬) যদি সমতল হয়ে যায়, তাহলে পুরো ১০ কোটি সারি নিয়ে কষ্ট করার দরকারই নাও হতে পারে।

  • Simple random sampling: প্রতিটি সারির বাছাইয়ের সম্ভাবনা সমান। DuckDB-র USING SAMPLE 1% ফাইল থেকেই নমুনা নিতে পারে; DataFrame-এ pandas আর Polars-এর sample. Seed লিখে রাখুন (পর্ব ০৪)।
  • Stratified sampling: জেলা বা শ্রেণি ধরে আলাদা নমুনা। ফেরতের হার ৪% হলে সরল ১% নমুনায় ছোট জেলার ফেরত-অর্ডার হাতে গোনা কয়েকটি আসতে পারে।
  • বিরল শ্রেণির downsampling: সব fraud রেখে সাধারণ অর্ডারের মাত্র ৫% রাখা। কিন্তু তখন model-এর সম্ভাবনা আর আসল হার নয়; সংশোধন আর threshold পর্ব ৫০-এর বিষয়।
  • Reservoir sampling: ডেটা স্রোতের মতো আসছে, মোট কত সারি জানা নেই, তবু ঠিক k-টির একটি সমান-সম্ভাবনার নমুনা চাই। প্রথম k-টি রাখুন; তারপর i-তম সারিকে (০ থেকে গুনে) k ÷ (i + ১) সম্ভাবনায় নমুনার একটি এলোমেলো জায়গায় বসান। এক পাসে, k-টির বেশি memory ছাড়া। Jeffrey Vitter ১৯৮৫ সালে এর বিশ্লেষণ আর দ্রুত রূপ প্রকাশ করেন; সরল রূপটি (Algorithm R) Donald Knuth-এর বইয়ে Alan Waterman-এর নামে উল্লেখিত। অনুশীলন ৪-এ নিজে লিখবেন।

নমুনা যেখানে ভুল পথ দেখায়: (ক) সময়: এলোমেলো নমুনা নিয়ে train/test ভাগ করলে ভবিষ্যতের অর্ডার train-এ ঢুকে যায়; সময় ধরে ভাগ করুন (পর্ব ৫২)। (খ) একই ক্রেতার অনেক অর্ডার: সারি ধরে নমুনা নিলে একজনের অর্ডার দুই দিকেই পড়ে; ক্রেতা ধরে নমুনা নিন। (গ) লম্বা লেজ: বিরল পণ্য বা জেলা নমুনায় না থাকলে model সেগুলো কখনো দেখে না। (ঘ) চূড়ান্ত সংখ্যা: নমুনায় মাপা accuracy অনুমান মাত্র; রিপোর্টের সংখ্যা আসে পুরো বা যথেষ্ট বড় test set থেকে।

৬ · Incremental learning — partial_fit দিয়ে টুকরো টুকরো শেখা

সারাংশ জোড়া লাগানো যায়, কিন্তু model? অনেক model-এর জন্য উত্তর হ্যাঁ, যদি শেখার নিয়মটি নিজেই একবারে অল্প ডেটা দেখে। Stochastic gradient descent ঠিক তাই করে: প্রতিটি mini-batch-এর gradient দিয়ে একটি ছোট পা (পর্ব ১৩; বড় ডেটায় kernel-এর বদলে linear model আর SGD কেন, তা পর্ব ৩১-এ)। scikit-learn-এ এর দরজা partial_fit: fit পুরোনো সব ভুলে নতুন করে শেখে, আর partial_fit যা শিখেছে তার ওপর একটি chunk-এর এক পাস যোগ করে। এভাবে শেখানোকে বলে incremental learning.

partial_fit আছে, এমন কয়েকটি: SGDClassifier, SGDRegressor, Perceptron, MultinomialNB আর অন্য Naive Bayes, MiniBatchKMeans, IncrementalPCA, MLPClassifier, আর preprocessing-এ StandardScaler, MinMaxScaler. Random forest বা HistGradientBoosting-এর নেই (XGBoost-এর external memory-র মতো আলাদা ব্যবস্থা আছে, এই পর্বের বাইরে)। MiniBatchKMeans এই পাতায় চালানো হয়নি, কারণ ব্রাউজারের Python-এ KMeans পরিবার একটি threadpoolctl সতর্কবার্তা ছাপে।

লেখার feature-এ আরেক সমস্যা: TfidfVectorizer আগে পুরো ডেটা দেখে শব্দভান্ডার বানায়। সমাধান feature hashing (AI-এর ভিত্তি কোর্সের পর্ব ৬৩): প্রতিটি শব্দকে একটি hash function দিয়ে নির্দিষ্ট সংখ্যক কলামের একটিতে পাঠানো। HashingVectorizer কিছুই শেখে না, তাই নতুন শব্দও প্রতিবার একই কলামে যায়। দাম দুটি: কখনো দুটি শব্দ এক কলামে পড়ে (hash collision), আর কলাম থেকে শব্দে ফেরা যায় না। বাংলা শব্দের কার-চিহ্ন regex-এর \w-এ ঠিকমতো ধরা পড়ে না, তাই এখানে ফাঁকা জায়গা ধরে ভাগ করা হয়েছে (token_pattern=r"\S+"; বাংলা tokenization AI-এর ভিত্তি কোর্সের পর্ব ৬৭-এ)।

নিচের ব্লক বাজারঘরের বানানো পণ্য-শিরোনাম (যেমন “অফার বড় কাগুগু”; শেষ শব্দটি বানানো brand) আর দাম থেকে পণ্যের শ্রেণি অনুমান করে। প্রতি শ্রেণিতে ১৫০টি বানানো brand, তাই শুরুর chunk-গুলো সব brand দেখেনি। ১০ হাজার শিরোনাম আসে ১,০০০-এর ১০টি chunk-এ; প্রতিটির পর একটি স্থির ৪,০০০ শিরোনামের test set-এ accuracy মাপা হয়। দামের StandardScaler-ও partial_fit দিয়ে ধাপে ধাপে শেখে। তুলনা: সব ১০ হাজার সারি একসঙ্গে RAM-এ রেখে সাধারণ fit, যা ডেটার ওপর অনেকবার ঘোরে (অনেক epoch).

import numpy as np
import matplotlib.pyplot as plt
from scipy import sparse
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifier
from sklearn.preprocessing import StandardScaler

rng = np.random.default_rng(70)     # বাজারঘরের (কাল্পনিক) পণ্যের শিরোনাম থেকে category
WORDS = [["চাল", "ডাল", "আটা", "চিনি"], ["শাড়ি", "পাঞ্জাবি", "কুর্তি", "জুতা"],
         ["মোবাইল", "চার্জার", "হেডফোন", "ল্যাপটপ"], ["উপন্যাস", "অভিধান", "কবিতা", "গাইড"]]
SYL = ["কা", "রি", "মো", "তু", "লে", "সা", "নি", "বো", "দা", "পি", "গু", "হে"]
BRANDS = [["".join(rng.choice(SYL, 3)) for _ in range(150)] for _ in WORDS]  # বানানো brand
EXTRA = ["নতুন", "অফার", "সেরা", "প্রিমিয়াম", "ছাড়", "আসল", "বড়", "ছোট"]
def make(n):
    y = rng.integers(0, 4, n)
    t = [" ".join([*rng.choice(EXTRA, 2), rng.choice(BRANDS[c])] +
                  ([rng.choice(WORDS[c])] if rng.random() < 0.3 else [])) for c in y]
    return t, rng.normal(np.array([6.0, 7.0, 7.5, 5.5])[y], 0.9)[:, None], y
(T, P, Y), (Tt, Pt, Yt) = make(10_000), make(4_000)
hv = HashingVectorizer(n_features=2**12, token_pattern=r"\S+", alternate_sign=False)
X = lambda t, p, sc: sparse.hstack([hv.transform(t), sc.transform(p)]).tocsr()

sc, clf, acc = StandardScaler(), SGDClassifier(loss="log_loss", random_state=0), []
for s in range(0, 10_000, 1_000):                 # ১০টি chunk, প্রতিটি একবার দেখা
    sc.partial_fit(P[s:s + 1000])                 # scaler-এর গড়ও ধাপে ধাপে
    clf.partial_fit(X(T[s:s + 1000], P[s:s + 1000], sc), Y[s:s + 1000], classes=range(4))
    acc.append(clf.score(X(Tt, Pt, sc), Yt))
sc2 = StandardScaler().fit(P)                     # তুলনা: সব সারি একসঙ্গে, অনেক epoch
full = SGDClassifier(loss="log_loss", random_state=0).fit(X(T, P, sc2), Y)
print("example title:", T[0], "->", ["grocery", "fashion", "electronics", "books"][Y[0]])
print("after each chunk:", np.round(acc, 3))
fa = full.score(X(Tt, Pt, sc2), Yt); print("full batch, all rows in memory:", round(fa, 3))
plt.plot(range(1000, 10_001, 1000), acc, "o-", label="partial_fit, one pass")
plt.axhline(fa, ls="--", color="gray", label="full-batch fit")
plt.xlabel("rows seen"); plt.ylabel("test accuracy"); plt.legend(); plt.show()

আসল আউটপুট (ব্রাউজারের Python; ছবিটি ব্লকের নিচে আঁকা হয়):

example title: অফার বড় কাগুগু -> fashion
after each chunk: [0.691 0.707 0.782 0.757 0.824 0.878 0.877 0.904 0.906 0.892]
full batch, all rows in memory: 0.938

প্রথম chunk-এর পর accuracy ০.৬৯১: বেশির ভাগ brand অচেনা, model চলে শুধু শ্রেণির শব্দ (৩০% শিরোনামে থাকে) আর দামের ওপর। প্রতিটি chunk নতুন brand শেখায়, তাই রেখা ওপরে ওঠে, তবে সরলরেখায় নয় (৪র্থ আর শেষ chunk-এ একটু নামা), কারণ SGD-এর ধাপ এলোমেলো আর প্রতিটি শব্দ একবারই দেখা। দশ chunk-এর পর ০.৮৯২, অথচ সব ডেটা একসঙ্গে রেখে অনেক epoch ঘোরানো model ০.৯৩৮. মূল কারণ পাস-সংখ্যা: partial_fit প্রতিটি সারি একবার দেখেছে। Chunk-গুলো আবার পড়ে দ্বিতীয় পাস চালালে ফাঁক সাধারণত কমে, সময়ের দামে। Memory: যেকোনো মুহূর্তে RAM-এ শুধু একটি chunk আর ৪,০৯৬টি hash-কলামের ওজন, ডেটা যত বড়ই হোক।

partial_fit-এর পাঁচ ফাঁদ: (১) প্রথম ডাকে classes= দিতেই হবে, কারণ প্রথম chunk-এ সব শ্রেণি না-ও থাকতে পারে। (২) chunk-এর ক্রম: ফাইল যদি শ্রেণি বা তারিখ ধরে সাজানো থাকে, model শেষের chunk-গুলোর দিকে হেলে পড়ে আর আগের শেখা অনেকটা ভোলে; সম্ভব হলে chunk-এর ক্রম আর ভেতরের সারি এলোমেলো করুন। (৩) Scaler ধাপে ধাপে শিখলে প্রথম দিকের chunk একটু ভিন্ন মাপে scale হয়েছিল; আগে এক পাসে scaler শিখে নেওয়া পরিষ্কার পথ। (৪) ডেটা চিরকাল এলে খুব কমে যাওয়া learning rate নতুন ধরনের ডেটা ধরতে পারে না, যা drift-এর প্রশ্ন (পর্ব ৫৯)। (৫) Test set শুরুতেই আলাদা আর স্থির রাখুন; শেখানো chunk-এ মাপা accuracy রিপোর্ট নয়।

৭ · Parquet — কলামে রাখা, row group আর min-max

CSV প্রতিটি সারি একটি লাইনে লেখে। দুটি কলাম লাগলেও প্রতিটি লাইন পুরো পড়ে ভাঙতে হয়, আর প্রতিটি সংখ্যা লেখা থেকে আবার সংখ্যায় রূপান্তর করতে হয়। Parquet উল্টো: columnar storage, অর্থাৎ একই কলামের মান পাশাপাশি, binary-তে, dtype সহ, চাপা (compressed) অবস্থায়। ফাইলটি অনেকগুলো row group-এ ভাগ (প্রতিটি কয়েক হাজার থেকে কয়েক লাখ সারি), আর ফাইলের শেষে একটি footer-এ প্রতিটি row group-এর প্রতিটি কলামের column statistics লেখা: min, max, null-এর সংখ্যা।

CSV বনাম Parquet — একটি প্রশ্নে কতটুকু পড়তে হয়

প্রশ্ন: day ≥ ৩৩০ হলে district আর price. Row group-এর সীমা নিচের ব্লকের আসল run থেকে।

Row-wise CSV versus columnar Parquet with row groups CSV: সারি ধরে লেখা 0,Dhaka,fashion,412.5,2 0,Sylhet,grocery,95.0,1 … ৩ লাখ লাইন … সব লাইন পুরো পড়ে ভাঙতে হয় Parquet: row group → কলাম daydistcatpriceqty RG ১RG ২RG ৩RG ৪RG ৫RG ৬ Footer: day-এর min–max ০–৬০ · ৬০–১২১ · ১২১–১৮১ ১৮১–২৪২ · ২৪২–৩০৩ ৩০৩–৩৬৪ ← শুধু এখানে ≥ ৩৩০ পড়া হয় filter-এর জন্য বাদ dist = district, cat = category; ৩০টি টুকরোর মাত্র ৩টি ডিস্ক থেকে আসে
ছবি ৭.১ — দুই রকম বাদ দেওয়া: অপ্রয়োজনীয় কলাম (projection pushdown) আর footer-এর min-max দেখে অপ্রয়োজনীয় row group (predicate pushdown). দ্বিতীয়টি কাজ করে কারণ অর্ডার দিনের ক্রমে লেখা; day এলোমেলো হলে প্রতিটি row group-এর min-max প্রায় ০–৩৬৪ হতো, কিছুই বাদ যেত না।

পাতার Python-এ Parquet লেখা হয় pyarrow দিয়ে (pyarrow.parquet.write_table; এখানে df.to_parquet চলে না)। নিচের ব্লক ৩ লাখ অর্ডার দিনের ক্রমে লেখে, প্রতি ৫০ হাজার সারিতে একটি row group. তারপর দুই ফাইলের আকার আর দুটি কলাম পড়ার সময়, footer-এর min-max, আর একটি filter. শেষে একই প্রশ্ন (electronics-এর জেলাভিত্তিক আয়) তিনটি টুলে: pandas, Polars-এর lazy পথ আর DuckDB-র SQL. সময় একবার গরম করে নেওয়ার পর দ্বিতীয় run-এর; প্রতিবার চালালে একটু বদলাবে।

import os, time
import duckdb, numpy as np, pandas as pd, polars as pl
import pyarrow as pa, pyarrow.parquet as pq

rng = np.random.default_rng(70); n = 300_000   # দিনের ক্রমে আসা অর্ডার (কাল্পনিক)
df = pd.DataFrame({"day": np.sort(rng.integers(0, 365, n)),
    "district": rng.choice(["Dhaka", "Khulna", "Sylhet", "Rangpur"], n),
    "category": rng.choice(["grocery", "fashion", "electronics"], n),
    "price": rng.lognormal(6, 0.8, n).round(2), "qty": rng.integers(1, 6, n)})
df.to_csv("orders.csv", index=False)
pq.write_table(pa.Table.from_pandas(df, preserve_index=False), "orders.parquet",
               row_group_size=50_000)              # প্রতি ৫০ হাজার সারিতে একটি row group
def ms(f):                                         # দ্বিতীয়বারের সময় (প্রথমবার গরম করা)
    f(); t = time.perf_counter(); r = f(); return r, 1000 * (time.perf_counter() - t)
print({k: round(os.path.getsize(k) / 2**20, 2) for k in ["orders.csv", "orders.parquet"]}, "MiB")
_, t1 = ms(lambda: pd.read_csv("orders.csv", usecols=["district", "price"]))
_, t2 = ms(lambda: pq.read_table("orders.parquet", columns=["district", "price"]))
print(f"read 2 of 5 columns: CSV {t1:.0f} ms, Parquet {t2:.0f} ms")
meta = pq.ParquetFile("orders.parquet").metadata
print("day min-max per row group:", [(meta.row_group(i).column(0).statistics.min,
      meta.row_group(i).column(0).statistics.max) for i in range(meta.num_row_groups)])
print("rows with day >= 330:", pq.read_table("orders.parquet", filters=[("day", ">=", 330)]).num_rows)

cols = lambda: pq.read_table("orders.parquet", columns=["district", "category", "price", "qty"])
def with_pandas():                                 # তিন টুলে একই প্রশ্ন, একই ফাইল থেকে
    d = cols().to_pandas()
    return d[d.category == "electronics"].assign(rev=d.price * d.qty).groupby("district").rev.sum()
a, ta = ms(with_pandas)
b, tb = ms(lambda: pl.from_arrow(cols()).lazy().filter(pl.col("category") == "electronics")
           .group_by("district").agg((pl.col("price") * pl.col("qty")).sum().alias("rev"))
           .sort("district").collect())
c, tc = ms(lambda: duckdb.sql("SELECT district, sum(price * qty) AS rev FROM 'orders.parquet' "
           "WHERE category = 'electronics' GROUP BY district ORDER BY district").df())
print(f"pandas {ta:.0f} ms, Polars lazy {tb:.0f} ms, DuckDB {tc:.0f} ms")
print("same answer:", np.allclose(a, b["rev"].to_numpy()) and np.allclose(a, c.rev), a.round().to_dict())

আসল আউটপুট (ব্রাউজারের Python; সময় প্রতি run-এ আলাদা):

{'orders.csv': 8.3, 'orders.parquet': 1.94} MiB
read 2 of 5 columns: CSV 96 ms, Parquet 33 ms
day min-max per row group: [(0, 60), (60, 121), (121, 181), (181, 242), (242, 303), (303, 364)]
rows with day >= 330: 28694
pandas 196 ms, Polars lazy 74 ms, DuckDB 102 ms
same answer: True {'Dhaka': 41689747.0, 'Khulna': 41961071.0, 'Rangpur': 41387004.0, 'Sylhet': 41695871.0}

আকার: একই ডেটা CSV-তে ৮.৩ MiB, Parquet-এ ১.৯৪ MiB. সাজানো day কলাম আর তিনটি মাত্র মানের category খুব ভালো চাপা যায় (একই মান বারবার এলে শুধু “এই মান, এতবার” লেখা যথেষ্ট)। দুটি কলাম পড়া: CSV-কে প্রতিটি লাইন ভাঙতে হয়, Parquet শুধু দুটি কলামের টুকরো খোলে; এই run-এ কয়েক গুণ দ্রুত। min-max দেখায় ছয়টি row group দিনের ক্রমে; ২৮,৬৯৪টি সারির filter শুধু শেষ row group খোলে। তিন টুলের উত্তর এক, আর এত ছোট ডেটায় সময়ের পার্থক্য প্রায় অর্থহীন: পাতার run-এ run-এ pandas-এর সময় কখনো দ্বিগুণ হয়। একটি নতুন তথ্য এখানে: পাতার Polars build-এ Parquet পড়ার অংশ নেই আর একাধিক thread চালাতে পারে না (pl.scan_parquet চালালে AttributeError), তাই ফাইল pyarrow দিয়ে পড়ে pl.from_arrow(...).lazy() দিয়ে Polars-কে দেওয়া হয়েছে। DuckDB সরাসরি ফাইলের নাম নিয়ে SQL চালায়।

ভালো Parquet-এর তিন অভ্যাস: (ক) যে কলামে বেশি filter হয় (তারিখ), সেই ক্রমে লিখুন। (খ) row group খুব ছোট নয় (footer বড়, পড়া ধীর), খুব বড়ও নয় (একটি খুলতেই অনেক memory); pyarrow-এর ডিফল্ট সীমা প্রায় ১০ লাখ সারি। (গ) খুব বড় ডেটা মাস ধরে আলাদা ফোল্ডারে (month=2026-10/), যাতে ফোল্ডারের নাম দেখেই পুরো মাস বাদ যায়; একে partitioning বলে (System Design কোর্সের partitioning পর্ব)।

৮ · এক কোটি সারি — pandas, Polars আর DuckDB-র আসল মাপ

পাতার ৩ লাখ সারি টুলের পার্থক্য দেখাতে খুব ছোট। তাই ডেস্কটপে একটি আলাদা uv project-এ (AI-এর ভিত্তি কোর্সের পর্ব ১২) seed দিয়ে বাজারঘরের ১ কোটি অর্ডার বানানো হয়েছে: আটটি জেলা, ছয়টি শ্রেণি, দাম, পরিমাণ, অনলাইনে পেমেন্ট আর ফেরত। লেখার সময়ে Python 3.13.5, pandas 3.1.1, Polars 2.1.0, DuckDB 2.0.0, pyarrow 26.0.1, psutil 7.2.2. CSV ৫১,৬০,১৪,০৫৪ byte (৪৯২ MiB), Parquet ১১,১২,৪৭,০৪৯ byte (১০৬ MiB, ১০টি row group). প্রশ্ন: ফেরত হয়নি এমন electronics অর্ডারে প্রতি জেলায় অর্ডার, মোট আয় আর গড় দাম। Polars-এর lazy query আর তার explain() AI-এর ভিত্তি কোর্সের পর্ব ২৭-এ ধাপে ধাপে আছে; এখানে নতুন মাপ memory.

METHODS = {
    "pandas_csv": lambda: query_pandas(pd.read_csv("orders.csv")),
    "pandas_parquet": lambda: query_pandas(pd.read_parquet("orders.parquet", columns=COLS)),
    "polars_lazy": lambda: lazy.collect().to_pandas(),   # pl.scan_parquet(...) থেকে
    "duckdb_sql": lambda: duckdb.sql(SQL).df(),           # FROM 'orders.parquet'
}
proc = psutil.Process()
base = proc.memory_info().rss                 # import-এর পরের memory
for _ in range(3):                            # তিনবার, প্রতিটির সময়
    t0 = time.perf_counter(); METHODS[sys.argv[1]](); ts.append(time.perf_counter() - t0)
peak = proc.memory_info().peak_wset - base    # Windows-এ সর্বোচ্চ working set

সত্যিকারের run (ডেস্কটপ; প্রতিটি পদ্ধতি নিজের আলাদা process-এ, যাতে memory মিশে না যায়):

~/bazarghar-scale$ for m in pandas_csv pandas_parquet polars_lazy duckdb_sql; do uv run python bench.py $m; done
pandas_csv      first  0.74 s  median  0.74 s  extra peak memory   1645 MiB
pandas_parquet  first  0.54 s  median  0.46 s  extra peak memory   1119 MiB
polars_lazy     first  0.04 s  median  0.02 s  extra peak memory    252 MiB
duckdb_sql      first  0.08 s  median  0.04 s  extra peak memory    108 MiB
~/bazarghar-scale$ uv run python bench.py check
python 3.13.5  pandas 3.1.1  polars 2.1.0  duckdb 2.0.0
  district  orders     revenue  avg_price
     Dhaka  561440 933848042.2      554.5
Chattogram  287418 478261585.0      554.7
    Khulna  160086 267040535.5      557.9
…
  Barishal   95755 159620970.4      558.2
same answer in all four: True

Memory-ই আসল খবর। পুরো CSV পড়ে pandas সর্বোচ্চ প্রায় ১,৬৪৫ MiB বাড়তি memory নিল; পুরো DataFrame-টি নিজেই memory_usage(deep=True)-এ প্রায় ৬০৭ MiB, বাকিটা পড়ার সময়ের অস্থায়ী জায়গা আর filter-এর copy. Parquet থেকে শুধু পাঁচটি কলাম পড়লে ১,১১৯ MiB. Polars-এর lazy query ২৫২ MiB আর DuckDB ১০৮ MiB, কারণ দুটিই প্রয়োজনীয় কলাম টুকরো টুকরো পড়ে ছেঁকে সঙ্গে সঙ্গে গুটিয়ে ফেলে; পুরো টেবিল কখনো RAM-এ আসে না। সময়: pandas-ও এক সেকেন্ডের কম, Polars ও DuckDB সব core-এ কয়েক শতাংশ সেকেন্ডে। এটি একটি ল্যাপটপের একটি run, ফাইল সম্ভবত operating system-এর cache-এ ছিল; সাধারণ benchmark নয়। উত্তর চারটিতেই এক, মাপার আগে এটিই প্রথম শর্ত।

RAM-এর চেয়েও বড় ডেটায় দুটিরই উত্তর আছে। DuckDB-র অনেক operator (group by, join, sort) memory-র সীমা ছুঁলে অস্থায়ী ফল ডিস্কে লেখে (spill to disk) আর কাজ চালিয়ে যায়; সীমা SET memory_limit = '4GB'. Polars-এর streaming engine lazy query টুকরো টুকরো চালায়। সব operator সমানভাবে পারে না, তাই নিজের version-এর docs দেখুন। ML-এর সাধারণ ধারা: কাঁচা টেবিল SQL বা Polars দিয়ে ক্রেতা বা দিন প্রতি ছোট feature টেবিলে নামান, Parquet-এ রাখুন, তারপর pandas আর scikit-learn. SQL-এর window function আর cohort পর্ব ৬১-এ।

৯ · সব core, তারপর cluster — আর টুল বাছাইয়ের সিদ্ধান্ত

এক যন্ত্রের ভেতরে প্রথম ধাপ সব CPU core কাজে লাগানো। scikit-learn-এর অনেক estimator আর cross_val_score, GridSearchCV-এর মতো টুল n_jobs নেয়, আর ভেতরে joblib কাজ ভাগ করে: random forest-এর আলাদা গাছ, বা cross-validation-এর আলাদা fold আলাদা core-এ (data parallelism). পাতার Python-এ একটিই thread, তাই এটি ডেস্কটপের সত্যিকারের run (scikit-learn 1.9.1, ১ লাখ সারি, ২০টি feature, ১০০টি গাছ):

~/bazarghar-scale$ uv run python njobs.py
sklearn 1.9.1  n_jobs=1:  35.2 s, train acc 1.000
sklearn 1.9.1  n_jobs=4:   9.3 s, train acc 1.000

চারটি core-এ প্রায় ৩.৮ গুণ দ্রুত, আর একই random_state-এ গাছগুলো একই (train accuracy শুধু মিল দেখাতে, মূল্যায়ন নয়)। তবে n_jobs=-1 চোখ বন্ধ করে নয়: প্রতিটি worker process-এর নিজের memory লাগে, আর বড় ডেটা প্রতিটিতে copy হলে RAM শেষ। GridSearchCV(n_jobs=8)-এর ভেতরে model-ও n_jobs=8 হলে ৬৪টি কাজ আটটি core-এর জন্য লড়ে। একটি স্তরেই সমান্তরাল করুন।

এক যন্ত্র ছাড়িয়ে গেলে Dask আর Apache Spark-এর মতো টুল কাজ অনেক যন্ত্রে ভাগ করে (ডেটা পাইপলাইনের নকশা System Design কোর্সের batch আর stream processing পর্ব-এ)। Dask Python-এর ভেতরে pandas-এর মতো API দেয়, আর একই কোড ল্যাপটপে বা cluster-এ চালাতে পারে; Spark JVM-ভিত্তিক, বড় প্রতিষ্ঠানের ডেটা পাইপলাইনে বহুল প্রচলিত, Python থেকে PySpark দিয়ে ব্যবহার হয়। দুটিই শক্তিশালী, কিন্তু নতুন জটিলতা আনে: নেটওয়ার্কে ডেটা সরানোর খরচ (shuffle), যন্ত্র বিকল হওয়া, খরচ আর debug-এর কষ্ট; আর সব ML algorithm ভাগ করা সহজ নয়। অনেক দলের (সবার নয়) পরামর্শ: cluster-এ ডেটা ছোট feature টেবিলে নামান, শেখান এক যন্ত্রে। Cluster বানানো আর চালানো একটি পরের MLOps আর ডেটা ইঞ্জিনিয়ারিং কোর্সের বিষয়।

নিচের stepper তিন আকারের ডেটায় সিদ্ধান্তটি দেখায়। ধরুন যন্ত্রটি ১৬ GB RAM-এর একটি ল্যাপটপ।

যন্ত্র: ১৬ GB RAM-এর ল্যাপটপসবচেয়ে সরল যা চলে

            
  1. প্রশ্নের ক্রম। আগে হিসাব, পরে টুল: অংশ ২-এর সূত্র প্রথম ছাঁকনি, তারপর গুটিয়ে আনা যায় কি না, আর শেখাতে আসলে কতটুকু লাগে।
  2. ২০০ MB. ঠিক dtype-এ হয়তো ৮০ MB; দশ গুণেও ৮০০ MB, ১৬ GB-তে কিছুই না। এখানে Spark মানে মশা মারতে কামান। pandas, scikit-learn, আর ফাইলটি Parquet-এ।
  3. ৩০ GB. পুরো টেবিল RAM-এ আসবে না, তবু ল্যাপটপই যথেষ্ট। DuckDB বা Polars lazy ফাইল থেকে টুকরো টুকরো পড়ে ক্রেতা প্রতি একটি সারিতে গুটিয়ে আনে (মোট কেনা, শেষ কেনার দিন)। ফল ছোট, তাই শেখানো চেনা পথে; অর্ডার-স্তরে শেখাতে হলে partial_fit বা স্তরভিত্তিক নমুনা।
  4. ৫ TB. ল্যাপটপের ডিস্ক থেকে শুধু পড়তেই কয়েক ঘণ্টা, আর ডেটা রোজ বাড়ে। ভাগ করা ব্যবস্থা (Spark, Dask বা ডেটা warehouse) এখানে যুক্তিসঙ্গত, মূলত গুটিয়ে আনার জন্য; শেখানো আবার এক যন্ত্রে। ভাগ করে শেখানো (distributed training) শেষ উপায়।
  5. নিয়ম। সবচেয়ে সরল যে পথ চলে, সেটিই। প্রতিটি ধাপে তিনটি মাপ: উত্তর মেলে কি না, সর্বোচ্চ memory আর সময়। পরের জটিল ধাপ তখনই, যখন মাপ বলে আগেরটি কুলায় না।

আরও গভীরে: Chan, Golub আর LeVeque-এর জোড়া লাগানোর সূত্র দিয়ে variance একটি “monoid”-এর মতো আচরণ করে: যেকোনো ক্রমে, যেকোনো ভাগে জোড়া দিলে একই ফল, তাই cluster-এ গাছের মতো করে সমান্তরালে জোড়া যায়; median-এর জন্য এমন সঠিক সারাংশ নেই, তাই t-digest (Ted Dunning) বা KLL-এর মতো আনুমানিক sketch, আর আলাদা মান গুনতে HyperLogLog (Flajolet ও সহকর্মী, ২০০৭)। Parquet-এর ভেতরের বিন্যাস Google-এর Dremel প্রবন্ধের (২০১০) ধারণা থেকে এসেছে, আর ফাইলের ভেতরে প্রতিটি কলামের টুকরো আবার page-এ ভাগ, যেখানে dictionary আর run-length encoding মান চাপে। SGD-এর তত্ত্বে Bottou আর Bousquet (২০০৮) দেখান, বিশাল ডেটায় সীমিত সময়ে অনেক ডেটার ওপর মোটামুটি optimisation, অল্প ডেটার ওপর নিখুঁত optimisation-এর চেয়ে প্রায়ই ভালো generalisation দেয়।

পরীক্ষা ও ইন্টারভিউতে: (১) ৫ কোটি সারি, কলাম int64, category (৮ মান), float32, uint8, bool: memory কত? (৮ + ১ + ৪ + ১ + ১) × ৫ × ১০৭ = ৭৫০ MB. (২) Chunk-এ গড় কীভাবে? প্রতি chunk-এ sum আর count, শেষে মোট sum ÷ মোট count. (৩) Welford: mean = ৫, k = ৪, M2 = ২০, নতুন x = ১০ — নতুন mean আর M2? d = ৫, mean = ৫ + ৫/৫ = ৬, M2 = ২০ + ৫ × ৪ = ৪০, variance = ৪০/৪ = ১০. (৪) TfidfVectorizer-এর বদলে HashingVectorizer কেন? শব্দভান্ডার শিখতে হয় না, তাই chunk-এ চলে; দাম collision আর ব্যাখ্যার অভাব। (৫) Parquet কেন দ্রুত? শুধু দরকারি কলাম আর footer-এর min-max দেখে দরকারি row group পড়ে, binary আর চাপা।

১০ · AI-এর যুগে — বড় ডেটার ML কোড যাচাই

AI assistant বড় ডেটার কোড দ্রুত দেয়, যা প্রায়ই ছোট ডেটায় নিখুঁত চলে; ভুল ধরা পড়ে বড় ফাইলে, বা থেকে যায় চুপচাপ ভুল সংখ্যায়।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
Chunk-এর গড়ের গড়, বা chunk-এর median-এর গড়চুপচাপ ভুল সংখ্যাছোট ফাইলে পুরো-পড়ার উত্তরের সঙ্গে মেলান
TfidfVectorizer.partial_fit বা RandomForestClassifier.partial_fit লেখেএমন method নেই; বানানো APIনিজের version-এর API reference; hasattr(model, "partial_fit")
Polars-এর পুরোনো নাম (groupby, with_column), pandas-এর appendনতুন version-এ নেই বা বদলেছেDocs আর changelog; কোড চালিয়ে দেখা
Σx² দিয়ে streaming varianceবড় মানে শূন্য বা ঋণাত্মক variance১০৯ যোগ করা ডেটায় পরীক্ষা; Welford
তারিখে সাজানো chunk এলোমেলো না করেই partial_fit, আর শেখানো chunk-এ accuracy রিপোর্টপক্ষপাতদুষ্ট model, ফোলানো সংখ্যাআগে আলাদা স্থির test set; chunk-এর ক্রম যাচাই
সবখানে n_jobs=-1, nested-ওRAM শেষ, core-এর জন্য লড়াইএক স্তরে সমান্তরাল; memory মাপুন
২ GB ডেটায় Spark বা Dask cluster প্রস্তাবঅকারণ জটিলতা আর খরচসারি × byte হিসাব; আগে DuckDB/Polars
টাকার যোগফল float16/float32-এগোলের ভুল জমেপয়সায় integer, বা যোগফল float64-এ

AI-কে কোডের সঙ্গে হিসাব আর যাচাইও চাইতে বলুন:

আমার কাছে ১ বছরের অর্ডারের Parquet ফাইল, প্রায় ৩০ GB, ল্যাপটপে ১৬ GB RAM. কলাম: order_id, customer_id, day, district, category, price, qty, returned. ১. আগে memory-র হিসাব দেখাও: প্রতিটি কলামের উপযুক্ত dtype আর প্রতি সারির byte. ২. DuckDB বা Polars lazy দিয়ে ক্রেতাভিত্তিক feature টেবিল বানানোর কোড দাও; কোন version ধরে লিখছ বলো। ৩. কোনো গড় বা variance chunk-এ হলে কীভাবে জোড়া লাগাবে, তা লেখো; গড়ের গড় নয়। ৪. যদি অর্ডার-স্তরে শেখাতে হয়, partial_fit-সহ কোন estimator, প্রথম ডাকে classes, chunk-এর ক্রম এলোমেলো করা, আর আলাদা স্থির test set রাখো। ৫. ছোট একটি নমুনায় তোমার কোডের উত্তর pandas-এর পুরো-পড়ার উত্তরের সঙ্গে মেলানোর একটি পরীক্ষা লেখো। ৬. Cluster প্রস্তাব করার আগে কারণ দেখাও কেন এক যন্ত্রে কুলাবে না।

AI-এর দেওয়া বড়-ডেটার কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১২০০০-এর দশকে “বড় ডেটা” মানেই ছিল Hadoop cluster. আজ অনেক দল আবার এক যন্ত্রে ফিরছে। কেন? ইতিহাস কী শেখায়?

২০০৪ সালে Google-এর Jeffrey Dean আর Sanjay Ghemawat MapReduce প্রবন্ধ প্রকাশ করেন: হাজার হাজার সস্তা যন্ত্রে ডেটা ভাগ করে map আর reduce ধাপে হিসাব, যন্ত্র বিকল হলেও কাজ চলবে। ২০০৬ সালের দিকে Doug Cutting আর সহকর্মীরা এর খোলা উৎসের রূপ Hadoop দাঁড় করান, যা Yahoo-তে বড় আকারে চলে। বছর কয়েক পর UC Berkeley-র AMPLab থেকে Matei Zaharia-র দলের Spark আসে (প্রবন্ধ ২০১২), যা মধ্যবর্তী ফল memory-তে রেখে বারবার ঘোরা ML-এর কাজ অনেক দ্রুত করে। তখন RAM দামি, এক যন্ত্রে কয়েক GB, ডিস্ক ধীর; বড় ডেটায় cluster প্রায় অনিবার্য ছিল।

২০১৫ সালে Frank McSherry, Michael Isard আর Derek Murray একটি ছোট প্রবন্ধ লেখেন, “Scalability! But at what COST?”. তাঁরা দেখান, কিছু প্রকাশিত graph-processing system-এর বহু-যন্ত্রের ফল একটি ল্যাপটপের এক thread-এর ভালো লেখা কোড ছাড়িয়ে যেতে অনেক core লাগে, কোনোটির ক্ষেত্রে কখনোই ছাড়ায় না। যুক্তিটি সরল: ভাগ করার খরচ (নেটওয়ার্কে ডেটা, সমন্বয়, বিকলতা সামলানো) বিনা মূল্যে আসে না।

এর পর পরিবেশ বদলেছে। ল্যাপটপে ১৬–৬৪ GB RAM আর দ্রুত SSD সাধারণ, server-এ শত শত GB. Parquet আর in-process engine (DuckDB, Polars) এক যন্ত্রে এমন প্রশ্ন সেকেন্ডে চালায়, যার জন্য আগে cluster লাগত। তাই অনেকে “এক যন্ত্র আগে” দর্শনের কথা বলেন। তবু সব জায়গায় নয়: রোজ কয়েক TB ঢোকা log, অনেক দলের ভাগ করা ডেটা, বা খুব বড় model-এর training-এ ভাগ করা ব্যবস্থা এখনো দরকার। বিতর্কটি তাই “cluster ভালো না খারাপ” নয়, বরং সীমারেখা কোথায়।

মূল উপলব্ধি: Scalability নিজে লক্ষ্য নয়; আগে এক যন্ত্রের সবচেয়ে ভালো পথ মাপুন, তারপর দেখুন ভাগ করার খরচ পোষায় কি না।

প্র ০২SGD প্রতিটি সারি একবার দেখে, তবু বিশাল ডেটায় অনেকে একে নিখুঁত batch optimisation-এর চেয়ে ভালো মনে করেন। এর তাত্ত্বিক ভিত্তি কী, আর কোথায় এটি খাটে না?

ধারণাটির শিকড় ১৯৫১ সালে, যখন Herbert Robbins আর Sutton Monro “stochastic approximation” প্রকাশ করেন: কোলাহলপূর্ণ মাপ থেকেও ধাপের আকার ঠিকমতো কমালে সঠিক উত্তরে পৌঁছানো যায়। Perceptron-এর শেখার নিয়মও একটি একটি উদাহরণ দেখে হালনাগাদ। কিন্তু অনেক বছর optimisation-এর বইয়ে SGD ছিল ধীর, অমার্জিত পদ্ধতি, কারণ নির্দিষ্ট একটি loss-কে নিখুঁতভাবে কমাতে Newton বা quasi-Newton অনেক কম ধাপে পৌঁছায়।

২০০৭–২০০৮ সালে Léon Bottou আর Olivier Bousquet “The Tradeoffs of Large Scale Learning” প্রবন্ধে প্রশ্নটি বদলে দেন। ML-এ আসল লক্ষ্য training loss নয়, নতুন ডেটায় ভুল। মোট ভুলকে তাঁরা তিন ভাগে দেখান: model-এর সীমাবদ্ধতা, সীমিত ডেটার কারণে ভুল (estimation), আর optimisation অসম্পূর্ণ থাকার ভুল। সময় যখন সীমা, তখন একই সময়ে অনেক বেশি ডেটা একবার দেখা SGD estimation-এর ভুল বেশি কমায়, আর optimisation-এর অল্প ভুল তার তুলনায় নগণ্য হতে পারে। তাই ডেটা অসীমের মতো বড় হলে “অনেক ডেটা, মোটামুটি optimisation” প্রায়ই জেতে।

সীমাও আছে। এই পর্বের পাতায় ডেটা ছোট (১০ হাজার), তাই এক পাসের SGD ০.৮৯২-এ থেমেছে, অনেক epoch-এর model ০.৯৩৮: এখানে সময় নয়, ডেটাই সীমা, আর পুনরায় ঘোরা লাভজনক। SGD learning rate আর feature scaling-এর প্রতি সংবেদনশীল, আর ডেটার ক্রম সাজানো থাকলে পক্ষপাত আসে। Tree-ভিত্তিক model (gradient boosting), যা ট্যাবুলার ডেটায় প্রায়ই সেরা (পর্ব ৩৮), এভাবে টুকরো টুকরো শেখে না। তাই বাস্তবে অনেক দল ডেটা গুটিয়ে বা নমুনা নিয়ে এক যন্ত্রে boosting চালায়, আর linear model ও partial_fit রাখে খুব বড়, পাতলা (sparse) feature-এর জন্য, যেমন লেখা বা click.

মূল উপলব্ধি: বড় ডেটায় প্রশ্ন “কোন optimiser নিখুঁত” নয়, “একই সময়ে কোনটি নতুন ডেটায় কম ভুল দেয়”; উত্তর নির্ভর করে ডেটা না সময়, কোনটি সীমা।

প্র ০৩Hashing trick একটি “ইচ্ছাকৃত ভুল” মেনে নেয়: কিছু শব্দ একই কলামে মিশে যায়। তবু বড় ব্যবস্থায় এটি জনপ্রিয় কেন, আর কখন এড়াবেন?

বড় লেখা-ডেটায় শব্দভান্ডার অসীমের দিকে যায়: নতুন brand, বানান-ভুল, ইমোজি, শব্দজোড়া। অভিধান রাখতে হলে প্রথমে পুরো ডেটা দেখতে হয়, অভিধানটি memory-তে রাখতে হয়, আর serving-এর সময় একই অভিধান সঙ্গে নিতে হয়। Hashing trick এই তিনটিই মুছে দেয়: শব্দ থেকে কলাম নম্বর আসে একটি নির্দিষ্ট hash function থেকে, যেকোনো যন্ত্রে, যেকোনো সময়ে একই।

John Langford-এর দলের Vowpal Wabbit (Yahoo Research-এ ২০০৭ সালের দিকে শুরু) online learning আর hashing একসঙ্গে ব্যবহার করে খুব বড় ডেটায় দ্রুত linear model শেখানোর জন্য পরিচিত হয়। ২০০৯ সালে Kilian Weinberger ও সহকর্মীরা “Feature Hashing for Large Scale Multitask Learning” প্রবন্ধে বিশ্লেষণ করেন, কেন collision-এর ক্ষতি সাধারণত ছোট: অনেক কলাম থাকলে দুটি গুরুত্বপূর্ণ শব্দ এক কলামে পড়ার সম্ভাবনা কম, আর একটি দ্বিতীয় hash দিয়ে চিহ্ন (+/−) দিলে মিশে যাওয়া মানগুলো গড়ে কাটাকাটি হয়; scikit-learn-এর alternate_sign ঠিক এটি। প্রবন্ধটি spam ফিল্টারে ব্যবহারকারী-ভিত্তিক ব্যক্তিকরণের উদাহরণ দেয়।

দাম তিনটি। ব্যাখ্যা: কলাম ১,৭৩৫ কোন শব্দ, তা আর জানা যায় না, তাই model-এর গুরুত্বপূর্ণ শব্দ দেখাতে আলাদা করে hash-এর হিসাব রাখতে হয়। কলাম কম হলে collision বাড়ে; এই পর্বের ব্লকে ৪,০৯৬টি কলাম, যা ছোট উদাহরণের জন্য যথেষ্ট, কিন্তু লাখো শব্দে ২২০ (scikit-learn-এর ডিফল্ট) বা বেশি লাগে। আর IDF-এর মতো পুরো-ডেটার পরিসংখ্যান সরাসরি আসে না। তাই ছোট, স্থির শব্দভান্ডারে (যেমন কয়েক হাজার পণ্যের নাম) সাধারণ vectorizer সহজ আর ব্যাখ্যাযোগ্য; hashing আসে যখন ডেটা স্রোতের মতো, শব্দভান্ডার খোলা, বা memory আসল সীমা।

মূল উপলব্ধি: Hashing অভিধানের memory আর সমন্বয়ের খরচ দিয়ে কেনে সরলতা আর অসীম শব্দভান্ডার; collision আর ব্যাখ্যার অভাব সেই কেনার দাম।

প্র ০৪Database কয়েক দশক ধরে সারি ধরে ডেটা রেখেছে। বিশ্লেষণের জগৎ কেন কলামে সরে গেল, আর কলাম-ভিত্তিক বিন্যাস কোথায় খারাপ?

প্রচলিত relational database (দোকানের অর্ডার নেওয়া, ব্যাংকের লেনদেন) সারি ধরে রাখে, কারণ সেখানে প্রতিটি কাজ একটি বা কয়েকটি পুরো সারি পড়ে বা লেখে: “অর্ডার ৫০১-এর সব তথ্য দাও”, “এই অর্ডারের অবস্থা বদলাও”। এই ধরনের কাজকে OLTP বলে (DBMS & SQL কোর্সের storage-এর ভেতরের পর্ব)। বিশ্লেষণ উল্টো: “গত বছর জেলাভিত্তিক electronics-এর আয়” লাখো সারি ছোঁয়, কিন্তু মাত্র তিন-চারটি কলাম। সারি-ভিত্তিক বিন্যাসে বাকি সব কলামও ডিস্ক থেকে আসে।

কলাম-ভিত্তিক database-এর ধারণা পুরোনো, কিন্তু ২০০৫ সালে Michael Stonebraker ও সহকর্মীদের C-Store প্রবন্ধ আর তা থেকে আসা বাণিজ্যিক ব্যবস্থা এটিকে মূলধারায় আনে বলে ধরা হয়। CWI-এর MonetDB গবেষণাও এই ধারার ভিত্তি; DuckDB-ও CWI থেকেই, Mark Raasveldt আর Hannes Mühleisen-এর হাতে, ২০১৯ সালের দিকে। ২০১০ সালে Google-এর Dremel প্রবন্ধ নেস্টেড ডেটা কলামে ভাঙার পদ্ধতি দেখায়; ২০১৩ সালে Twitter আর Cloudera সেই ধারণায় খোলা ফাইল-বিন্যাস Parquet প্রকাশ করে। ২০১৬ সালে Apache Arrow memory-তে একই রকম কলাম-বিন্যাসের মান দেয়, যাতে pandas, Polars, DuckDB আর Spark copy ছাড়া ডেটা ভাগ করতে পারে।

কলামের সুবিধা তিনটি: দরকারি কলামই পড়া, একই ধরনের মান পাশাপাশি থাকায় ভালো চাপা, আর CPU-র vector নির্দেশে একসঙ্গে অনেক মানের হিসাব। দুর্বলতাও স্পষ্ট: একটি সারি বদলাতে অনেক কলামের টুকরো ছুঁতে হয়, তাই ঘন ঘন ছোট হালনাগাদে (একটি অর্ডারের অবস্থা বদলানো) এটি অদক্ষ; Parquet ফাইল কার্যত একবার লেখা, বহুবার পড়ার জন্য। তাই বাস্তব ব্যবস্থায় দুটিই থাকে: লেনদেন সারি-ভিত্তিক database-এ, আর রাতে বা স্রোতে কপি হয়ে বিশ্লেষণের জন্য কলামে।

মূল উপলব্ধি: সারি না কলাম, তা ঠিক করে প্রশ্নের ধরন: কয়েকটি পুরো সারি লেখা-পড়া হলে সারি, অনেক সারির কয়েকটি কলাম গুটালে কলাম।

প্র ০৫“pandas ছেড়ে Polars-এ চলে যান” — এমন পরামর্শ এখন প্রায়ই শোনা যায়। অন্যদিকে অনেকে বলেন “SQL-ই যথেষ্ট, DuckDB নিন”। এই বিতর্কে আপনি কীভাবে সিদ্ধান্ত নেবেন?

pandas ২০০৮ সালে Wes McKinney একটি আর্থিক প্রতিষ্ঠানে কাজের সময় শুরু করেন, আর পরের দশকে Python-এ ডেটা বিশ্লেষণের প্রায় সাধারণ ভাষা হয়ে ওঠে। ২০১৭ সালের একটি লেখায় McKinney নিজেই pandas-এর কিছু নকশাগত দুর্বলতা তালিকা করেন: memory-র অপচয়, এক core-এ কাজ, লেখার কলামের অদক্ষতা। সেই উপলব্ধি থেকে তিনি Apache Arrow-এর কাজে যুক্ত হন। Polars (Ritchie Vink, ২০২০ সালের দিকে শুরু) Arrow-বিন্যাস, সব core আর lazy query optimizer নিয়ে আসে; DuckDB একই সুবিধা SQL-এর ভাষায় দেয়। pandas-ও বদলেছে: pandas 3-এ copy-on-write আর Arrow-ভিত্তিক লেখার কলাম ডিফল্ট, যা এই পর্বের ডেস্কটপ মাপেও দেখা গেছে।

Polars-এর পক্ষে যুক্তি: দ্রুত, কম memory, পরিষ্কার expression-এর API, lazy পথে স্বয়ংক্রিয় optimisation. DuckDB-র পক্ষে: SQL প্রায় সব বিশ্লেষক জানেন, Parquet আর CSV সরাসরি পড়ে, RAM ছাড়ালে ডিস্কে ঢালে, আর একই প্রশ্ন পরে ডেটা warehouse-এ নিয়ে যাওয়া সহজ। pandas-এর পক্ষে: বিশাল ecosystem, scikit-learn, statsmodels আর chart-এর library সরাসরি DataFrame নেয়, অসংখ্য উদাহরণ আর পুরোনো কোড, আর ছোট-মাঝারি ডেটায় গতির পার্থক্য প্রায়ই অপ্রাসঙ্গিক।

ব্যবহারিক পদ্ধতি: আগে মাপুন, আসল কাজে সময় আর memory কোথায় যায় (উত্তর এক কি না যাচাইসহ)। ডেটা RAM-এ আরামে ধরলে আর কাজ এক সেকেন্ডে শেষ হলে টুল বদলানোর লাভ কম, শেখার খরচ বেশি। ভারী গুটানোর ধাপ (কাঁচা টেবিল থেকে feature) DuckDB বা Polars-এ আর model-এর ধাপ pandas ও scikit-learn-এ, এই মিশ্র পথ অনেক দলে প্রচলিত, আর Arrow-এর কারণে এদের মধ্যে ডেটা আনা-নেওয়া সস্তা। দলের দক্ষতাও বিবেচ্য: পাঁচজনের দলে সবাই SQL জানলে DuckDB স্বাভাবিক পছন্দ। কোনো “সব ক্ষেত্রে সেরা” উত্তর নেই, আর নতুন version-এর সঙ্গে তুলনাও বদলায়।

মূল উপলব্ধি: টুলের বিতর্ক মীমাংসা হয় নিজের ডেটায় মাপ দিয়ে; একাধিক টুল পাশাপাশি রাখা প্রায়ই একটি বেছে নেওয়ার চেয়ে ভালো।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। চাইলে পাতার ব্লকগুলোতে সংখ্যা বদলে যাচাই করুন।

  1. পূর্ণ numerical: (ক) একটি টেবিলে ৫ কোটি সারি; কলাম order_id (int64), district (category, ৮টি মান), price (float32), qty (uint8), returned (bool). memory কত MB? কাজের সময় ৫–১০ গুণ ধরলে ১৬ GB-এর যন্ত্রে চলবে? (খ) মান ১০, ১২, ১৭-এ Welford হাতে চালান: প্রতি ধাপে k, d, mean, M2, আর শেষে sample variance. (গ) দুটি chunk: A-তে n = ৩, mean = ১৩, M2 = ২৬; B-তে n = ২, mean = ৮, M2 = ২. Chan-এর সূত্রে জোড়া লাগানো mean আর variance বের করুন।

    (ক) প্রতি সারি ৮ + ১ + ৪ + ১ + ১ = ১৫ byte; ১৫ × ৫ × ১০৭ = ৭৫০ MB (প্রায় ৭১৫ MiB). ৫–১০ গুণে ৩.৭৫–৭.৫ GB, তাই ১৬ GB-এ চলবে, তবে অন্য কাজ বন্ধ রাখা ভালো।

    (খ) x = ১০: k = ১, d = ১০, mean = ১০, M2 = ০. x = ১২: k = ২, d = ২, mean = ১১, M2 = ০ + ২ × (১২ − ১১) = ২. x = ১৭: k = ৩, d = ৬, mean = ১৩, M2 = ২ + ৬ × (১৭ − ১৩) = ২৬. Variance = ২৬ ÷ ২ = ১৩. যাচাই: দূরত্ব −৩, −১, ৪; বর্গের যোগ ৯ + ১ + ১৬ = ২৬.

    (গ) n = ৫, δ = ৮ − ১৩ = −৫, mean = (৩ × ১৩ + ২ × ৮) ÷ ৫ = ৫৫ ÷ ৫ = ১১. M2 = ২৬ + ২ + (−৫)² × ৩ × ২ ÷ ৫ = ২৮ + ৩০ = ৫৮; variance = ৫৮ ÷ ৪ = ১৪.৫. যাচাই করতে চাইলে A = {১০, ১২, ১৭} আর B = {৭, ৯} নিন: পাঁচটির গড় ১১, দূরত্বের বর্গ ১ + ১ + ৩৬ + ১৬ + ৪ = ৫৮.

  2. সমালোচনা ও নকশা: একজন সহকর্মী ৪০ GB-এর তারিখে সাজানো CSV থেকে churn model বানাতে নিচের কোড লিখেছেন। অন্তত পাঁচটি সমস্যা খুঁজুন আর প্রতিটির প্রতিকার লিখুন।
    import pandas as pd
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.linear_model import SGDClassifier
    clf, vec, means = SGDClassifier(), TfidfVectorizer(), []
    for chunk in pd.read_csv("orders_2026.csv", chunksize=500_000):
        means.append(chunk["price"].mean())
        X = vec.fit_transform(chunk["title"])
        clf.partial_fit(X, chunk["churned"])
        print("accuracy:", clf.score(X, chunk["churned"]))
    print("avg price:", sum(means) / len(means))

    (১) vec.fit_transform প্রতি chunk-এ নতুন শব্দভান্ডার শেখে, তাই একই কলাম প্রতি chunk-এ আলাদা শব্দ বোঝায়। প্রতিকার HashingVectorizer, বা আগে এক পাসে স্থির শব্দভান্ডার। (২) প্রথম partial_fit-এ classes=[0, 1] নেই; প্রথম chunk-এ শুধু এক শ্রেণি থাকলে error বা ভুল। (৩) ফাইল তারিখে সাজানো, তাই model শেষের মাসগুলোর দিকে হেলে পড়ে; chunk-এর ক্রম এলোমেলো করুন। (৪) শেখানো chunk-এই accuracy: ফোলানো সংখ্যা। শুরুতে আলাদা, স্থির, পরের সময়ের test set. (৫) গড়ের গড়: শেষ chunk ছোট হলে ভুল; sum আর count জমান। (৬) প্রতিটি chunk-এ সব কলাম পড়া হচ্ছে; usecols আর dtype দিন, সম্ভব হলে একবার Parquet-এ রূপান্তর করুন। (৭) Churn ক্রেতা-স্তরের প্রশ্ন; অর্ডার-স্তরে শেখালে একই ক্রেতা বারবার আসে আর leakage হয়। আগে DuckDB-তে ক্রেতাভিত্তিক feature টেবিল ভালো নকশা (পর্ব ৭২, ক্যাপস্টোন — বাজারঘরের churn আর lifetime value প্রকল্প)। (৮) random_state নেই।

  3. আউটপুট অনুমান করুন: চালানোর আগে দুটি লাইন লিখুন।
    import pandas as pd
    df = pd.DataFrame({"x": [2, 4, 6, 8, 10]})
    means, total, count = [], 0, 0
    for i in range(0, len(df), 2):
        chunk = df.x[i:i + 2]
        means.append(float(chunk.mean()))
        total, count = total + chunk.sum(), count + len(chunk)
    print(means)
    print(round(sum(means) / len(means), 2), total / count, df.x.mean())
    [3.0, 7.0, 10.0]
    6.67 6.0 6.0

    Chunk তিনটি: [২, ৪], [৬, ৮], [১০]; শেষটিতে একটি মাত্র মান। তাদের গড় ৩, ৭, ১০, আর গড়ের গড় ২০ ÷ ৩ ≈ ৬.৬৭. কিন্তু মোট sum ৩০ আর count ৫, তাই আসল গড় ৬.০, যা পুরো কলামের mean()-এর সমান। ছোট শেষ chunk-এর একটি মান গড়ের গড়ে পুরো এক-তৃতীয়াংশ ওজন পেয়েছে। (ডেস্কটপে pandas 3.1.1-এ চালিয়ে মেলানো।)

  4. কোড লিখুন: একটি function reservoir(stream, k, rng) লিখুন, যা একটি স্রোত (যেকোনো iterable) একবার পড়ে ঠিক k-টি মানের সমান-সম্ভাবনার নমুনা ফেরত দেয় (Algorithm R). random.Random(70) দিয়ে ১ থেকে ১,০০০ পর্যন্ত স্রোতে k = ৫ চালান, আর ২০,০০০ বার ছোট স্রোতে (০–৯, k = ৩) চালিয়ে দেখান যে প্রতিটি মান প্রায় ০.৩ ভাগ সময় নমুনায় আসে।
    import random
    from collections import Counter
    
    def reservoir(stream, k, rng):
        sample = []
        for i, x in enumerate(stream):          # i = 0, 1, 2, ...
            if i < k:
                sample.append(x)                # প্রথম k-টি সরাসরি
            else:
                j = rng.randrange(i + 1)        # 0 থেকে i, সমান সম্ভাবনায়
                if j < k:
                    sample[j] = x               # সম্ভাবনা k / (i + 1)
        return sample
    
    print(reservoir(range(1, 1001), 5, random.Random(70)))
    hits = Counter()
    for s in range(20_000):                     # প্রতিটি মান কতবার নমুনায় আসে?
        hits.update(reservoir(range(10), 3, random.Random(s)))
    print([round(hits[v] / 20_000, 3) for v in range(10)])   # প্রত্যাশা 3/10 = 0.3

    সত্যিকারের run (ডেস্কটপ, Python 3.14):

    [6, 908, 655, 987, 857]
    [0.297, 0.3, 0.3, 0.304, 0.301, 0.293, 0.304, 0.303, 0.296, 0.302]

    কেন সমান: i-তম মান ঢোকে k ÷ (i + ১) সম্ভাবনায়; পরে প্রতিটি নতুন মান j-তম ধাপে তাকে সরায় ১ ÷ (j + ১) সম্ভাবনায়। গুণ করলে প্রতিটি মানের শেষ পর্যন্ত টিকে থাকার সম্ভাবনা k ÷ n. দশটি মানের হার ০.২৯৩ থেকে ০.৩০৪, ২০,০০০ বারের এলোমেলো ওঠানামার মধ্যে। Memory-তে সবসময় k-টি মান, স্রোত যত লম্বাই হোক।

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

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

ক্রমRAM-এ না ধরা অর্ডার-ডেটা নিয়ে একটি model-এর কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সারি × প্রতি সারির byte দিয়ে memory-র হিসাব আর dtype ঠিক করা
  2. ডেটাকে কলাম-ভিত্তিক ফাইলে (Parquet) রূপান্তর, শুধু দরকারি কলাম রেখে
  3. শুরুতেই আলাদা আর স্থির একটি test set সরিয়ে রাখা
  4. DuckDB বা Polars দিয়ে ছোট feature টেবিলে গুটিয়ে আনা, আর ছোট অংশে pandas-এর উত্তরের সঙ্গে মেলানো
  5. এক যন্ত্রে শেখানো, দরকার হলে partial_fit দিয়ে chunk-এ chunk-এ
  6. Test set-এ মূল্যায়ন, আর memory ও সময় মেপে পরের ধাপ (cluster) লাগবে কি না ঠিক করা

একটি টেবিলে ২ কোটি সারি আর চারটি কলাম: int64, float64, category (১০টি মান, প্রতি সারিতে ১ byte কোড) আর bool. মোটামুটি কত memory লাগবে?

  • ১৬০ MB
  • ৬৪০ MB
  • ৩৬০ MB
  • ৩.৬ GB

একটি বড় CSV তিনটি chunk-এ পড়া হলো। Chunk-গুলোতে ঢাকার সারি ১০০, ১০০ আর ১০, আর ঢাকার গড় দাম ৫০০, ৫০০ আর ৮০০ টাকা। পুরো ফাইলে ঢাকার আসল গড় দাম কত?

  • ৬০০ টাকা
  • প্রায় ৫১৪.৩ টাকা
  • ৫০০ টাকা
  • জানা সম্ভব নয়, পুরো ফাইল আবার পড়তে হবে

Welford-এর নিয়মে এ পর্যন্ত k = ৪, mean = ৫, M2 = ২০. নতুন মান x = ১০ এল। নতুন mean আর M2 কত?

  • mean = ৬.২৫, M2 = ৩৮.৭৫
  • mean = ৬, M2 = ৪৫
  • mean = ৫, M2 = ৪৫
  • mean = ৬, M2 = ৪০

পণ্যের শিরোনাম chunk-এ chunk-এ এসে SGDClassifier.partial_fit-এ যাচ্ছে। লেখাকে সংখ্যায় রূপ দিতে কোনটি সবচেয়ে উপযুক্ত?

  • HashingVectorizer
  • প্রতি chunk-এ TfidfVectorizer.fit_transform
  • শুধু প্রথম chunk-এ CountVectorizer.fit, তারপর transform
  • TfidfVectorizer.partial_fit

একটি Parquet ফাইলে ছয়টি row group, প্রতিটির day কলামের min–max: ০–৬০, ৬০–১২১, ১২১–১৮১, ১৮১–২৪২, ২৪২–৩০৩, ৩০৩–৩৬৪. day ≥ ৩৩০ filter-এ কয়টি row group ডিস্ক থেকে পড়তে হবে?

  • ছয়টিই
  • দুটি
  • একটি
  • একটিও না

১৬ GB RAM-এর একটি ল্যাপটপে ৩০ GB-এর Parquet ফাইল থেকে ক্রেতাভিত্তিক feature টেবিল বানাতে হবে, যার ফল প্রায় ২০ লাখ সারি। প্রথম পছন্দ কোনটি যুক্তিসঙ্গত?

  • pd.read_parquet দিয়ে পুরো ফাইল পড়ে groupby
  • DuckDB SQL বা Polars lazy query দিয়ে সরাসরি ফাইল থেকে গুটিয়ে আনা
  • প্রথমেই একটি Spark cluster বানানো
  • ১% সরল নমুনা নিয়ে feature বানানো

শব্দকোষ

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

মনে রাখুন

  1. আগে হিসাব: memory ≈ সারি × প্রতি সারির byte, আর কাজের সময় তার ৫–১০ গুণ; category, downcast আর bool প্রায়ই টুল বদলানোর দরকারই মেটায়।
  2. জোড়া লাগানো যায় এমন সারাংশ: chunk-এ count আর sum, শেষে ভাগ; variance-এ (n, mean, M2) আর Welford; median-এ আনুমানিক sketch.
  3. Incremental learning: partial_fit-এ প্রথম ডাকে classes, লেখায় HashingVectorizer, chunk-এর ক্রম এলোমেলো, আর স্থির test set; এক পাসের দাম কিছুটা নির্ভুলতা।
  4. Parquet: কলাম ধরে, row group আর footer-এর min-max; filter-এর কলামে সাজিয়ে লিখলে অপ্রয়োজনীয় টুকরো পড়াই হয় না।
  5. সবচেয়ে সরল যা চলে: pandas → DuckDB/Polars দিয়ে গুটিয়ে আনা → partial_fit বা নমুনা → cluster; প্রতিটি ধাপে উত্তর, memory আর সময় মাপুন।

পরের পর্বে: এখন পর্যন্ত আলাদা আলাদা টুকরো: serving, test, নজরদারি আর বড় ডেটা। পর্ব ৭১ (ML system design — একটি পুরো নকশা, শুরু থেকে নজরদারি পর্যন্ত) এগুলো জোড়া লাগিয়ে বাজারঘরের একটি ব্যবস্থা লক্ষ্য থেকে নজরদারি পর্যন্ত নকশা করবে, সংখ্যার হিসাবসহ।

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

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