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

Time series — তারিখ, resample, rolling আর Asia/Dhaka

Pandas Time Series — Parsing Dates, Time Zones, Resampling, Rolling Windows and Leakage-free Lag Features

Taught in Bangla, this lesson reads messy dates with to_datetime, format and dayfirst, uses the .dt accessor and DatetimeIndex slicing, converts between Asia/Dhaka and UTC, builds Friday-Saturday business days, fills missing days, and covers resample, rolling, expanding, shift and pct_change, ending with a real run where a leaked shift(-1) feature gives a perfect MAE of 0.0.

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

  • মেশানো তারিখের লেখা to_datetime দিয়ে পড়া: format, dayfirst, errors="coerce", আর 03/04-এর দ্ব্যর্থতা কীভাবে নিঃশব্দে ভুল দেয়
  • .dt দিয়ে বছর, মাস, বার, ঘণ্টা, ISO সপ্তাহ আর ত্রৈমাসিক বের করা, আর DatetimeIndex-এ তারিখ লিখে বাছাই
  • Naive আর aware সময়, Asia/Dhaka থেকে UTC-তে রূপান্তর, বাংলাদেশের UTC+6 আর ২০০৯ সালের DST-এর ঘটনা
  • date_range, frequency-র নাম, শুক্র-শনি ছুটি মেনে কাজের দিন, ফাঁকা দিন ভরা, আর resample দিয়ে সাপ্তাহিক-মাসিক যোগফল
  • rolling, expanding, shift, diff, pct_change দিয়ে forecasting-এর feature বানানো, আর ভবিষ্যতের তথ্য ঢুকে পড়ার ফাঁদ চেনা

এ পর্যন্ত মডিউল ৪-এ বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) অর্ডারের টেবিল নিয়ে কাজ করেছেন: Series, DataFrame আর index (পর্ব ২০), বাছাই (পর্ব ২২), পরিষ্কার করা (পর্ব ২৩), groupby (পর্ব ২৪) আর merge (পর্ব ২৫)। এই পর্বে টেবিলের একটি বিশেষ কলামকে কেন্দ্রে আনা হবে: তারিখ আর সময়। “গত সপ্তাহের বিক্রি কত”, “ঈদের আগে কত বাড়ল”, “আগামীকাল কত অর্ডার আসতে পারে” — এই সব প্রশ্নের উত্তরের পথ যায় সময়ের কলাম দিয়ে। পর্ব ১০-এ দেখেছেন ফাইলে তারিখ কীভাবে ISO 8601 আর time zone-সহ রাখতে হয়; এখানে দেখবেন pandas-এর ভেতরে সেই তারিখ দিয়ে কী কী করা যায়। সব runnable কোড পাতাতেই চলবে, Pyodide দিয়ে (pandas 3.0.2); প্রথমবার চালালে pandas নামতে কয়েক সেকেন্ড লাগতে পারে। কিছু অংশ ডেস্কটপে pandas 3.1.1 দিয়ে চালানো, সেখানে তা বলা আছে।

১ · সময়ের ডেটা আলাদা কেন — Timestamp, Timedelta আর NaT

চায়ের দোকানের হিসাব খাতার কথা ভাবুন। প্রতিটি পাতায় একটি তারিখ, আর সেদিনের বিক্রি। পাতাগুলো এলোমেলো করে দিলে প্রতিটি সংখ্যা ঠিক থাকে, কিন্তু খাতাটি আর কোনো গল্প বলে না: কোন সপ্তাহে বিক্রি বেড়েছে, বৃষ্টির দিনে কমেছে কি না, কিছুই বোঝা যায় না। এমন ডেটাকে বলে time series: সময়ের ক্রমে মাপা একটি মানের ধারা। এখানে ক্রম নিজেই তথ্য। আগের পর্বগুলোর টেবিলে সারির ক্রম বদলালে ফল বদলাত না; time series-এ বদলায়।

pandas সময়কে লেখা হিসেবে রাখে না, রাখে একটি সংখ্যা হিসেবে: একটি নির্দিষ্ট শুরুর মুহূর্ত (১৯৭০ সালের ১ জানুয়ারি, UTC) থেকে কত microsecond পেরিয়েছে। এই সংখ্যার ওপর একটি মোড়ক হলো Timestamp. দুটি Timestamp-এর ব্যবধান হলো Timedelta, আর অনুপস্থিত তারিখের চিহ্ন NaT (Not a Time), সংখ্যার NaN-এর মতো। ডেস্কটপে একটি সত্যিকারের run (pandas 3.1.1):

>>> t = pd.Timestamp("2026-04-14 09:30")
>>> t
Timestamp('2026-04-14 09:30:00')
>>> t + pd.Timedelta(days=30)
Timestamp('2026-05-14 09:30:00')
>>> pd.Timestamp("2026-04-30") - pd.Timestamp("2026-03-01")
Timedelta('60 days 00:00:00')
>>> pd.to_datetime(pd.Series(["2026-04-14", None])).tolist()
[Timestamp('2026-04-14 00:00:00'), NaT]
>>> pd.to_datetime(pd.Series(["2026-04-14 09:30"])).dtype
dtype('<M8[us]')

শেষ লাইনটি খেয়াল করুন। <M8[us] হলো NumPy-র সংক্ষিপ্ত লেখায় datetime64[us], মানে প্রতিটি ঘর একটি 64-bit পূর্ণসংখ্যা, এককে microsecond (us). pandas 2-এ লেখা থেকে পড়া তারিখ সাধারণত nanosecond (ns) এককে আসত; pandas 3.0 থেকে লেখা পড়লে microsecond আসে, যাতে বহু শতাব্দীর তারিখ ধরা যায়। পুরোনো টিউটোরিয়ালে datetime64[ns] দেখলে অবাক হবেন না, আর দুই এককের কলাম মেশানোর সময় dtype ছেপে দেখুন। একটি সংখ্যা হিসেবে রাখার লাভ হলো বিয়োগ, তুলনা, সাজানো সব vectorized, NumPy-র গতিতে।

জিনিসকী বোঝায়উদাহরণ
Timestampএকটি মুহূর্ত১৪ এপ্রিল ২০২৬, সকাল ৯:৩০
Timedeltaদুই মুহূর্তের ব্যবধান৬০ দিন, ৩ ঘণ্টা
DatetimeIndexTimestamp-এর indexমার্চ-এপ্রিলের ৬১টি দিন
NaTঅনুপস্থিত বা অপঠিত তারিখ“অজানা” লেখা থেকে

২ · তারিখ পড়া — to_datetime, format, dayfirst আর 03/04-এর ফাঁদ

বাংলাদেশে আমরা সাধারণত দিন আগে লিখি: ০৩/০৪/২০২৬ মানে ৩ এপ্রিল। যুক্তরাষ্ট্রের অনেক সফটওয়্যার মাস আগে লেখে: 03/04/2026 মানে March 4. দুটি লেখা অক্ষরে অক্ষরে এক। মানুষ প্রসঙ্গ দেখে বোঝে; যন্ত্র বোঝে না, শুধু একটি নিয়ম মানে। বাজারঘরে তারিখ আসে তিন জায়গা থেকে: পুরোনো Excel ফাইল, নতুন ওয়েবসাইটের ISO তারিখ, আর হাতে লেখা নোট। নিচের কোড একই মেশানো কলাম চারভাবে পড়ে।

import pandas as pd
pd.set_option("display.width", 60)

# বাজারঘরের তিন উৎসের তারিখ, এক কলামে মেশানো
raw = pd.Series(["03/04/2026", "13/04/2026", "2026-04-14",
                 "14 Apr 2026", "অজানা", None])

# ১. কিছু না বলে: প্রথম লেখা দেখে format আন্দাজ
guess = pd.to_datetime(raw, errors="coerce")
# ২. প্রতিটি লেখা আলাদা করে পড়া, তবে মাস আগে ধরে
mixed = pd.to_datetime(raw, errors="coerce", format="mixed")
# ৩. একই, কিন্তু দিন আগে (বাংলাদেশের অভ্যাস)
day1 = pd.to_datetime(raw, errors="coerce", format="mixed",
                      dayfirst=True)
print(pd.DataFrame({"raw": raw, "guess": guess,
                    "mixed": mixed, "dayfirst": day1}))
print("dtype:", day1.dtype, "| NaT:", day1.isna().sum())

# ৪. সবচেয়ে নিরাপদ: format ঠিক করে দেওয়া
strict = pd.to_datetime(pd.Series(["03/04/2026", "13/04/2026"]),
                        format="%d/%m/%Y")
print(strict.dt.strftime("%d %b %Y").tolist())
try:
    pd.to_datetime(pd.Series(["03/04/2026", "13/04/2026"]))
except ValueError as e:
    print("ValueError:", str(e).splitlines()[0])

আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):

           raw      guess      mixed   dayfirst
0   03/04/2026 2026-03-04 2026-03-04 2026-04-03
1   13/04/2026        NaT 2026-04-13 2026-04-13
2   2026-04-14        NaT 2026-04-14 2026-04-14
3  14 Apr 2026        NaT 2026-04-14 2026-04-14
4        অজানা        NaT        NaT        NaT
5          NaN        NaT        NaT        NaT
dtype: datetime64[us] | NaT: 2
['03 Apr 2026', '13 Apr 2026']
ValueError: time data "13/04/2026" doesn't match format "%m/%d/%Y". You might want to try:

চারটি কলাম চারটি আলাদা গল্প বলে। guess: pandas প্রথম লেখা "03/04/2026" দেখে নিয়ম ধরেছে %m/%d/%Y (মাস আগে), তাই প্রথম সারি ৪ মার্চ। তারপর একই নিয়ম সব সারিতে চাপিয়েছে; "13/04/2026"-এ ১৩ নম্বর মাস হয় না, আর ISO লেখাও এই নিয়মে মেলে না। errors="coerce" থাকায় কোনো error নেই, শুধু NaT. ছয়টির পাঁচটি তারিখ চুপচাপ হারিয়ে গেছে। mixed: প্রতিটি লেখা আলাদা করে পড়েছে, তাই ISO আর "14 Apr 2026" ঠিক এসেছে। কিন্তু প্রথম সারি এখনো ৪ মার্চ, অথচ দ্বিতীয় সারি ১৩ এপ্রিল, কারণ ১৩ মাস হতে পারে না বলে pandas সেখানে নিজে থেকে দিন আগে ধরেছে। একই কলামে দুই নিয়ম, কোনো সতর্কবার্তা ছাড়া। dayfirst: এবার প্রথম সারি ৩ এপ্রিল, যা বাজারঘরের Excel-এর আসল অর্থ। "অজানা" আর None দুটিই NaT, যা isna() দিয়ে গোনা যায়। শেষ অংশে format="%d/%m/%Y" দুটি তারিখই ঠিক পড়েছে, আর format ছাড়া errors-এর default ("raise") দিলে pandas থেমে গিয়ে স্পষ্ট বলে দিয়েছে কোন লেখা মেলেনি।

errors="coerce" ভুল লুকায়। এটি ভাঙা লেখাকে NaT বানিয়ে কাজ চালিয়ে যায়, যা পরিষ্কারের সময় সুবিধার (পর্ব ২৩), কিন্তু ভুল format-এর পুরো কলামও একইভাবে NaT হয়ে যায়। তাই coerce দিলে সবসময় সাথে সাথে isna().sum() ছেপে দেখুন, আর আগে-পরে কয়টি সারি অপঠিত হলো তা লিখে রাখুন। ৬টির মধ্যে ৫টি NaT হলে সেটি “নোংরা ডেটা” নয়, ভুল নিয়ম।

নিয়মটি নিজে লিখতে format-এর সংকেতগুলো জানা দরকার। এগুলো Python-এর strftime-এর সংকেত, যা Python কোর্সের standard library পর্বে datetime-এর সঙ্গে দেখেছেন:

সংকেতমানেউদাহরণ
%d / %m / %Yদিন / মাস / চার অঙ্কের বছর14/04/2026 → %d/%m/%Y
%H:%M২৪ ঘণ্টার ঘড়ি, মিনিট21:05
%I %p১২ ঘণ্টার ঘড়ি, AM/PM09 PM
%b / %Bমাসের ছোট / পুরো ইংরেজি নামApr / April
format="ISO8601"যেকোনো বৈধ ISO 8601 লেখা2026-04-14T09:30+06:00

CSV পড়ার সময় একই কাজ read_csv-এর ভেতরেই করা যায় (পর্ব ২১), নিয়ম একই। নিজের সিস্টেম থেকে ডেটা বের করার সময় ISO 8601 (2026-04-03) লিখুন: দ্ব্যর্থতা নেই, আর লেখা হিসেবে সাজালেও সময়ের ক্রম ঠিক থাকে। বাংলা অঙ্কে লেখা তারিখ (“৩ এপ্রিল”) pandas সরাসরি পড়ে না; আগে অঙ্ক আর মাসের নাম বদলাতে হয়, যা পর্ব ২৩-এর বাংলা লেখা পরিষ্কারের অংশ।

৩ · .dt — বছর, মাস, বার, ঘণ্টা, সপ্তাহ আর ত্রৈমাসিক

তারিখের কলাম হাতে পেলে প্রথম প্রশ্ন সাধারণত এর টুকরো নিয়ে: কোন মাসে বেশি অর্ডার, কোন বারে, দিনের কোন ঘণ্টায়। s.dt হলো তারিখের Series-এর একটি দরজা, যাকে বলে .dt accessor; এর ভেতর দিয়ে প্রতিটি সারির টুকরো একসঙ্গে বের হয়, কোনো loop ছাড়া। লেখার কলামের জন্য যেমন .str, তারিখের জন্য তেমন .dt. বাজারঘরের চারটি অর্ডার দিয়ে একটি সত্যিকারের run (ডেস্কটপ, pandas 3.1.1):

o = pd.DataFrame({"order_id": [101, 102, 103, 104],
    "ts": pd.to_datetime(["2026-03-20 19:45", "2026-04-03 08:10",
                          "2026-04-14 11:00", "2026-12-31 23:59"])})
d = o["ts"].dt
pd.DataFrame({"y": d.year, "m": d.month, "day": d.day_name().str[:3],
    "dow": d.day_of_week, "h": d.hour,
    "wk": d.isocalendar().week, "q": d.quarter})

      y   m  day  dow   h  wk  q
0  2026   3  Fri    4  19  12  1
1  2026   4  Fri    4   8  14  2
2  2026   4  Tue    1  11  16  2
3  2026  12  Thu    3  23  53  4
  • day_of_week সোমবার থেকে গোনে: সোমবার 0, শুক্রবার 4, শনিবার 5, রবিবার 6. বাংলাদেশের সাপ্তাহিক ছুটি শুক্র-শনি, তাই “ছুটির দিন” মানে d.day_of_week.isin([4, 5]). পুরোনো নাম dayofweek pandas 3.1.1-এ চালালে আমাদের run-এ একটি Pandas4Warning এসেছে (ভবিষ্যতে সরানো হবে), তাই নতুন কোডে day_of_week লিখুন।
  • day_name() ইংরেজি নাম দেয়. বাংলা নাম চাইলে একটি dict দিয়ে map করুন: d.day_of_week.map({4: "শুক্র", 5: "শনি"}). pandas-এর locale argument যন্ত্রের ভাষা-সেটিংয়ের ওপর নির্ভর করে, তাই সব যন্ত্রে একই ফল দেয় না।
  • সপ্তাহ: .dt.week আর নেই. pandas 2.0-তে সরানো হয়েছে; আমাদের run-এ AttributeError: 'DatetimeProperties' object has no attribute 'week'. এখন d.isocalendar().week, যা ISO 8601-এর সপ্তাহ: সোমবারে শুরু, আর বছরের প্রথম বৃহস্পতিবার যে সপ্তাহে, সেটি সপ্তাহ ১।
  • ISO বছরের চমক: ২০২৬-এর ৩১ ডিসেম্বর সপ্তাহ 53. আমাদের run-এ pd.Timestamp("2027-01-01").isocalendar() দিয়েছে year=2026, week=53: ২০২৭-এর প্রথম দিনটি ISO হিসাবে ২০২৬-এর শেষ সপ্তাহে। সপ্তাহ ধরে গ্রুপ করলে তাই isocalendar().year-ও সঙ্গে নিন, নইলে বছরের শেষে দুই বছরের ডেটা মিশে যাবে।
  • ত্রৈমাসিক (quarter) ক্যালেন্ডার বছর ধরে: জানুয়ারি-মার্চ 1, এপ্রিল-জুন 2. বাংলাদেশের সরকারি অর্থবছর জুলাই থেকে জুন, তাই অর্থবছরের হিসাবে এটি সরাসরি চলে না।

৪ · DatetimeIndex — তারিখ লিখেই বাছাই

তারিখের কলামকে index বানালে (df.set_index("date")) pandas একটি DatetimeIndex পায়, আর তখন হিসাব খাতার পাতা ওল্টানোর মতো সহজে বাছাই করা যায়: “মার্চের ১০ থেকে ১২”, “পুরো এপ্রিল”, “১৪ এপ্রিল”। এই পর্বের বাকি অংশে বাজারঘরের মার্চ-এপ্রিল ২০২৬-এর ৬১ দিনের দৈনিক বিক্রি ব্যবহার হবে, seed 42 দিয়ে বানানো, টাকায় নয়, পয়সায় পূর্ণসংখ্যা হিসেবে (অংশ ৮-এর কোডে তৈরির নিয়ম দেখবেন)। নিচে k হলো একই Series টাকায় ((sales // 100).rename("taka")). ডেস্কটপে সত্যিকারের run (pandas 3.1.1, NumPy 2.5.4):

>>> type(k.index).__name__, k.index.dtype
('DatetimeIndex', dtype('<M8[us]'))
>>> k["2026-03-10":"2026-03-12"]           # দুই প্রান্তই ধরা
2026-03-10    44881
2026-03-11    55276
2026-03-12    54666
Freq: D, Name: taka, dtype: int64
>>> len(k["2026-04"]), int(k["2026-04"].sum())   # পুরো মাস
(30, 1554513)
>>> k.loc["2026-04-14"]                     # পহেলা বৈশাখ
np.int64(75700)
>>> k[k.index.day_name() == "Friday"].head(3)
2026-03-06    50186
2026-03-13    58396
2026-03-20    57700
Name: taka, dtype: int64

একটি তারিখ দিলে ফল একটি একক সংখ্যা, যা NumPy 2 np.int64(75700) হিসেবে দেখায়; এটি সাধারণ 75700-ই। তিনটি জিনিস লক্ষ করুন। প্রথমত, তারিখের লেখা দিয়ে slice করলে দুই প্রান্তই ধরা হয়: ১০ থেকে ১২ মানে তিন দিন। এটি .loc-এর লেবেল-ভিত্তিক নিয়ম (পর্ব ২২), সংখ্যার অবস্থান দিয়ে slice-এর মতো শেষটি বাদ যায় না। দ্বিতীয়ত, "2026-04"-এর মতো আংশিক লেখা পুরো মাস দেয়; একে বলে partial string indexing. তৃতীয়ত, index-এর নিজেরও .day_name(), .month আছে, .dt ছাড়াই, কারণ .dt লাগে শুধু সাধারণ কলামে।

একটি শর্ত আছে: index সাজানো না থাকলে তারিখের পরিসর দিয়ে বাছাই ঝামেলা করে। আমাদের run-এ তিনটি এলোমেলো দিনের index-এ s["2026-02-27":"2026-03-02"] দিয়েছে KeyError: 'Value based partial slicing on non-monotonic DatetimeIndexes with non-existing keys is not allowed.', আর s.sort_index() করার পর একই লেখা ঠিক দুটি সারি দিয়েছে। অনেক উৎসের ডেটা জোড়া লাগানোর পর (পর্ব ২৫) তাই প্রথম কাজ sort_index(), আর index.is_monotonic_increasing দিয়ে যাচাই।

৫ · Time zone — naive, aware, Asia/Dhaka আর UTC

ঢাকা থেকে লন্ডনে আত্মীয়কে ফোন করার আগে আমরা হিসাব করি: “এখানে রাত ১০টা, ওখানে বিকেল ৪টা”। মুহূর্ত একটিই, ঘড়ির লেখা দুটি। “রাত ১০টা” কথাটি একা কোনো মুহূর্ত নয়; সঙ্গে “ঢাকায়” না বললে অর্থ অসম্পূর্ণ। pandas-এ time zone ছাড়া সময়কে বলে naive, আর time zone-সহ সময়কে aware. Naive সময় শুধু ঘড়ির কাঁটার অবস্থান; aware সময় পৃথিবীর একটি নির্দিষ্ট মুহূর্ত।

বাংলাদেশের ঘড়ি UTC (পৃথিবীর মান সময়) থেকে ৬ ঘণ্টা এগিয়ে, UTC+6, আর লেখার সময় এখানে কোনো ঘড়ি এগোনো-পেছানো নেই। কিন্তু একটি জিনিস বিপজ্জনক: ঢাকার মধ্যরাত UTC-তে সন্ধ্যা ৬টা। তাই রাত ১২টা থেকে সকাল ৬টার মধ্যের প্রতিটি অর্ডার ঢাকার হিসাবে এক তারিখের, UTC-র হিসাবে আগের তারিখের। নিচের ছবি বাজারঘরের তিনটি অর্ডার দিয়ে সেটি দেখায়।

একই মুহূর্ত, দুই ঘড়ি — ঢাকা (UTC+6) আর UTC

Three orders on a Dhaka clock and on a UTC clock; between Dhaka midnight and UTC midnight the two clocks disagree on the date

ঢাকা আর UTC-র ঘড়িতে একই তিনটি অর্ডার দুই ঘড়ির তারিখ আলাদা ঢাকায় ১৪, UTC-তে ১৩ ঢাকা 18:0000:0006:0012:00 ১৩ এপ্রিল ১৪ এপ্রিল UTC 12:0018:0000:0006:00 ১৩ এপ্রিল ১৪ এপ্রিল ক খ গ ক · ঢাকা ১৩/৪ 23:50 = UTC ১৩/৪ 17:50 খ · ঢাকা ১৪/৪ 02:10 = UTC ১৩/৪ 20:10 গ · ঢাকা ১৪/৪ 09:30 = UTC ১৪/৪ 03:30 খ কোন দিনের বিক্রি? ঘড়ির ওপর নির্ভর।
ছবি ২৬.১ — ওপরের নীল রেখা ঢাকার ঘড়ি, নিচের সবুজ রেখা UTC; প্রতি ঘর ৬ ঘণ্টা, আর খাড়া লাল দাগ মানে একই মুহূর্ত। হলুদ অংশ ঢাকার মধ্যরাত (UTC সন্ধ্যা ৬টা) থেকে UTC-র মধ্যরাত (ঢাকার সকাল ৬টা) পর্যন্ত, দিনে ৬ ঘণ্টা। এই সময়ের অর্ডার খ ঢাকার হিসাবে ১৪ এপ্রিলের, পহেলা বৈশাখের বিক্রি; UTC-র হিসাবে ১৩ এপ্রিলের। দৈনিক যোগফল UTC-তে করলে পহেলা বৈশাখের প্রথম ৬ ঘণ্টার বিক্রি আগের দিনে চলে যায়।

pandas-এ দুটি আলাদা কাজ আছে, আর তাদের পার্থক্যই এই অংশের মূল কথা। tz_localize("Asia/Dhaka") একটি naive সময়ের গায়ে লিখে দেয় “এটি ঢাকার ঘড়ির সময়”: ঘড়ির লেখা বদলায় না, শুধু পরিচয় যোগ হয়। tz_convert("UTC") একটি aware সময়কে অন্য ঘড়িতে দেখায়: মুহূর্ত বদলায় না, লেখা বদলায়। নিচের ধাপে অর্ডার খ-কে দুই কাজের মধ্য দিয়ে নিয়ে যান।

1t = pd.Timestamp("2026-04-14 02:10")2print(t.tz)3bd = t.tz_localize("Asia/Dhaka")4u = bd.tz_convert("UTC")5print(bd == u)6print(u.date(), bd.date())
মান (Watch)
t
bd
u
  1. দোকানের খাতার লেখা। t হলো "2026-04-14 02:10", কোনো time zone ছাড়া। এটি শুধু ঘড়ির কাঁটার অবস্থান; ঢাকার না লন্ডনের, তা জানা নেই।
  2. Naive প্রমাণ। t.tz হলো None. এমন সময়কে অন্য কোনো aware সময়ের সঙ্গে বিয়োগ বা তুলনা করলে pandas TypeError দেয়, কারণ উত্তরটি অনির্দিষ্ট।
  3. Localize: পরিচয় যোগ। tz_localize("Asia/Dhaka") ঘড়ির লেখা একই রাখে, 02:10, শুধু শেষে +06:00 জোড়ে। এখন এটি পৃথিবীর একটি নির্দিষ্ট মুহূর্ত।
  4. Convert: অন্য ঘড়িতে দেখা। tz_convert("UTC") ৬ ঘণ্টা পিছিয়ে লেখে: 02:10 − 6 ঘণ্টা = আগের দিন 20:10, +00:00. তারিখটিও বদলে ১৩ এপ্রিল হয়েছে।
  5. একই মুহূর্ত। bd == u হলো True: দুটি aware Timestamp-এর তুলনা হয় মুহূর্ত ধরে, ঘড়ির লেখা ধরে নয়। ভেতরে দুটির সংখ্যাও একই।
  6. তারিখ ঘড়ির ওপর নির্ভরশীল। UTC-তে তারিখ ১৩ এপ্রিল, ঢাকায় ১৪ এপ্রিল। “দৈনিক বিক্রি” হিসাবের আগে তাই ঠিক করতে হয় কোন ঘড়ির দিন গুনবেন; বাজারঘরের ব্যবসার দিন ঢাকার দিন।

এবার একই কাজ পুরো কলামে, সঙ্গে .dt দিয়ে তারিখ আর ঘণ্টা দুই ঘড়িতে। ব্রাউজারের Python-এ time zone-এর নিয়মগুলো আসে tzdata package থেকে, তাই প্রথম লাইনে import tzdata; ডেস্কটপে সাধারণত এটি লাগে না, তবে Windows-এ zoneinfo-র জন্য tzdata install করা থাকলে ভালো।

import tzdata                     # ব্রাউজারে time zone-এর তালিকা
import pandas as pd
pd.set_option("display.width", 60)

# দোকানের ঘড়িতে লেখা সময় (time zone ছাড়া = naive)
local = pd.Series(pd.to_datetime([
    "2026-04-13 23:50", "2026-04-14 02:10", "2026-04-14 09:30"]))
print("naive tz:", local.dt.tz)

dhaka = local.dt.tz_localize("Asia/Dhaka")   # "এটি ঢাকার সময়"
utc = dhaka.dt.tz_convert("UTC")              # একই মুহূর্ত, UTC-তে
df = pd.DataFrame({"dhaka": dhaka, "utc": utc})
print(df)
print("একই মুহূর্ত?", (df["dhaka"] == df["utc"]).all())

# .dt accessor: কোন ঘড়িতে দেখছেন, তার ওপর উত্তর নির্ভর করে
print(pd.DataFrame({
    "date_bd": df["dhaka"].dt.date, "hour_bd": df["dhaka"].dt.hour,
    "date_utc": df["utc"].dt.date, "hour_utc": df["utc"].dt.hour}))
d = df["dhaka"].dt
print(d.day_name().tolist(), d.quarter.tolist(),
      d.isocalendar().week.tolist())

try:
    local[0] - dhaka[0]                        # naive − aware
except TypeError as e:
    print("TypeError:", e)

# ২০০৯ সালের গ্রীষ্ম: tz database অনুযায়ী তখন UTC+7
for t in ["2009-07-01 12:00", "2010-07-01 12:00"]:
    print(t, "->", pd.Timestamp(t).tz_localize("Asia/Dhaka")
          .strftime("%z"))

আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):

naive tz: None
                      dhaka                       utc
0 2026-04-13 23:50:00+06:00 2026-04-13 17:50:00+00:00
1 2026-04-14 02:10:00+06:00 2026-04-13 20:10:00+00:00
2 2026-04-14 09:30:00+06:00 2026-04-14 03:30:00+00:00
একই মুহূর্ত? True
      date_bd  hour_bd    date_utc  hour_utc
0  2026-04-13       23  2026-04-13        17
1  2026-04-14        2  2026-04-13        20
2  2026-04-14        9  2026-04-14         3
['Monday', 'Tuesday', 'Tuesday'] [2, 2, 2] [16, 16, 16]
TypeError: Cannot subtract tz-naive and tz-aware datetime-like objects.
2009-07-01 12:00 -> +0700
2010-07-01 12:00 -> +0600

প্রথম লাইন নিশ্চিত করে কলামটি naive (None). Localize-এর পর তিনটি ঘড়ির লেখা একই থেকে +06:00 পেয়েছে, আর convert-এর পর ৬ ঘণ্টা পিছিয়েছে। == তুলনা True, কারণ মুহূর্ত একই। দ্বিতীয় টেবিলটি এই অংশের সবচেয়ে কাজের শিক্ষা: দ্বিতীয় অর্ডার ঢাকায় ১৪ তারিখ রাত ২টা, UTC-তে ১৩ তারিখ রাত ৮টা। “কোন ঘণ্টায় সবচেয়ে বেশি অর্ডার” প্রশ্নের উত্তর UTC-র ঘণ্টা দিয়ে দিলে বাজারঘরের বিপণন দল সকাল ৯টার ভিড়কে রাত ৩টার ভিড় ভাববে। Naive আর aware বিয়োগে TypeError: pandas অনুমান করে না যে naive সময়টি ঢাকার। শেষ দুই লাইনে IANA tz database-এর ইতিহাস: ২০০৯ সালের জুলাইয়ে ঢাকার offset ছিল +0700, ২০১০-এ আবার +0600.

ইতিহাস: বাংলাদেশের এক বছরের DST. বিদ্যুৎ সাশ্রয়ের লক্ষ্যে বাংলাদেশ ২০০৯ সালে একবার ঘড়ি এক ঘণ্টা এগিয়েছিল, যাকে বলে daylight saving time (DST). IANA time zone database-এর রেকর্ড অনুযায়ী ১৯ জুন ২০০৯ রাত ১১টায় ঘড়ি সরাসরি রাত ১২টায় চলে যায়, আর ২০০৯-এর ৩১ ডিসেম্বর রাতের শেষে আবার এক ঘণ্টা পেছায়; এরপর আর DST চালু হয়নি। সরকারি সিদ্ধান্তের খুঁটিনাটি আর মানুষ কতটা মেনেছিলেন, তা নিয়ে বিবরণ ভিন্ন হতে পারে; এখানে শুধু tz database যা রেখেছে, সেটি বলা হলো। আমাদের ডেস্কটপ run-এ (pandas 3.1.1, tzdata 2026.6) pd.Timestamp("2009-06-19 23:30").tz_localize("Asia/Dhaka") দিয়েছে ValueError: 2009-06-19 23:30:00 is a nonexistent time due to daylight savings time.: সেই রাতে ঢাকার ঘড়িতে ১১:৩০ বলে কোনো সময় আসেইনি। পুরোনো ডেটা নিয়ে কাজ করলে এমন সময়ের জন্য nonexistent আর ambiguous argument আছে।

নিয়ম: রাখুন UTC-তে, দেখান ঢাকায়

বড় সিস্টেম প্রায় সবসময় এই নিয়ম মানে, কারণ UTC-তে কোনো DST নেই, প্রতিটি মুহূর্তের একটিই লেখা, আর ভিন্ন দেশের সার্ভারের log মেলানো যায়। বিশ্লেষণের সময় একবার tz_convert("Asia/Dhaka") করে তারপর দিন, বার বা ঘণ্টা ধরে হিসাব করুন, আর কোন কলাম কোন ঘড়ির তা নামেই লিখুন, যেমন created_at_utc বা order_time_bd. ব্যতিক্রম ভবিষ্যতের নির্ধারিত ঘটনা (“প্রতিদিন ঢাকার সকাল ৯টায় অফার”): এগুলো ঢাকার ঘড়ি আর zone-এর নাম দিয়ে রাখা ভালো, কারণ নিয়ম বদলালে আগে থেকে হিসাব করা UTC সময় ভুল হয়ে যায়। SQL-এর দিক দেখতে পারেন SQL ফাংশন পর্বের টাইমজোন অংশে।

৬ · date_range, frequency, শুক্র-শনি ছুটি আর ফাঁকা দিন

পুরো একটি ক্যালেন্ডার বানাতে pd.date_range(start, end, freq=...); freq বলে ধাপ কত বড়। ধাপের নামগুলোকে বলে frequency alias, আর pandas 2.2 থেকে এদের কয়েকটির নাম বদলেছে, যা পুরোনো টিউটোরিয়াল আর AI সহকারীর কোডে প্রায়ই ভুল আকারে আসে। ডেস্কটপে সত্যিকারের run (pandas 3.1.1):

>>> pd.date_range("2026-04-01", periods=4, freq="W-SAT")
DatetimeIndex(['2026-04-04', '2026-04-11', '2026-04-18',
               '2026-04-25'],
              dtype='datetime64[us]', freq='W-SAT')
>>> pd.date_range("2026-01-01", periods=3, freq="ME")
DatetimeIndex(['2026-01-31', '2026-02-28', '2026-03-31'],
              dtype='datetime64[us]', freq='ME')
>>> pd.date_range("2026-01-01", periods=3, freq="MS")
DatetimeIndex(['2026-01-01', '2026-02-01', '2026-03-01'],
              dtype='datetime64[us]', freq='MS')
>>> pd.date_range("2026-01-01", periods=2, freq="M")
ValueError: Invalid frequency: M. Failed to parse with error message: ValueError("'M' is no longer supported for offsets. Please use 'ME' instead.")

Error-এর শুধু শেষ লাইন দেখানো হয়েছে। একই run-এ freq="Y" আর freq="H"-ও ValueError দিয়েছে; দ্বিতীয়টির বার্তায় pandas নিজেই বলেছে Did you mean h?.

নামধাপমন্তব্য
Dদিনক্যালেন্ডারের প্রতিটি দিন
W = W-SUNসপ্তাহ, রবিবারে শেষW-SAT মানে রবি…শনি সপ্তাহ
ME / MSমাসের শেষ / প্রথম দিনপুরোনো M pandas 3-এ error
QE / YEত্রৈমাসিক / বছরের শেষপুরোনো Q, Y বা A-র বদলে
h / minঘণ্টা / মিনিটবড় হাতের H pandas 3-এ error
B / Cকাজের দিন / নিজের নিয়মের কাজের দিনB ধরে সোম–শুক্র

শেষ সারিটি বাংলাদেশের জন্য গুরুত্বপূর্ণ। B বা bdate_range-এর default business day হলো সোম থেকে শুক্র, পশ্চিমের অফিসের সপ্তাহ। বাংলাদেশে সরকারি অফিস আর ব্যাংকের সাপ্তাহিক ছুটি সাধারণত শুক্র-শনি, কাজ রবি থেকে বৃহস্পতি (প্রতিষ্ঠানভেদে ভিন্ন হতে পারে)। বাজারঘরের অফিস কোন দিন পার্সেল পাঠায়, তা হিসাব করতে freq="C" আর weekmask দিন, সঙ্গে ঘোষিত ছুটির তালিকা holidays. সত্যিকারের run (pandas 3.1.1):

>>> pd.bdate_range("2026-04-09", "2026-04-16").strftime("%d %a").tolist()
['09 Thu', '10 Fri', '13 Mon', '14 Tue', '15 Wed', '16 Thu']
>>> bd = pd.bdate_range("2026-04-09", "2026-04-16", freq="C",
...                     weekmask="Sun Mon Tue Wed Thu",
...                     holidays=["2026-04-14"])   # পহেলা বৈশাখ
>>> bd.strftime("%d %a").tolist()
['09 Thu', '12 Sun', '13 Mon', '15 Wed', '16 Thu']

Default নিয়মে শুক্রবার ১০ তারিখ কাজের দিন, আর রবিবার ১২ তারিখ ছুটি, যা বাংলাদেশের জন্য উল্টো। নিজের weekmask দিলে শুক্র-শনি বাদ যায়, রবিবার আসে, আর ১৪ এপ্রিল পহেলা বৈশাখের সরকারি ছুটি হিসেবে বাদ পড়ে। বাংলাদেশে পহেলা বৈশাখ এখন প্রতি বছর ১৪ এপ্রিল, কিন্তু ঈদের ছুটি চাঁদ দেখার ওপর নির্ভর করে, আর ছুটি কয়দিন হবে তা প্রতি বছর ঘোষণা হয়। তাই holidays-এর তালিকা কোডে আন্দাজে না লিখে প্রতি বছরের সরকারি ছুটির তালিকা থেকে একটি আলাদা ফাইলে রাখুন।

ফাঁকা দিন — asfreq, reindex আর ভরার তিন পথ

বাস্তবের ডেটায় দিন হারায়: সার্ভার বন্ধ ছিল, বা সেদিন কোনো অর্ডারই আসেনি বলে কোনো সারি তৈরি হয়নি। একটি দিন না থাকা আর একটি দিনে শূন্য বিক্রি দুটি আলাদা কথা, কিন্তু টেবিলে দুটিই “সারি নেই”। ধরুন মার্চের প্রথম সপ্তাহ থেকে ৩ আর ৪ তারিখ হারিয়েছে। asfreq("D") পুরো দৈনিক ক্যালেন্ডার বানিয়ে ফাঁকা দিনে NaN বসায়, যাতে ফাঁকগুলো চোখে পড়ে। ডেস্কটপে সত্যিকারের run (pandas 3.1.1), মান টাকায়:

>>> small.index.freq, len(small)          # ৭ দিনের মধ্যে ৫টি সারি
(None, 5)
>>> full = small.asfreq("D")
>>> full
2026-03-01    51828.0
2026-03-02    43760.0
2026-03-03        NaN
2026-03-04        NaN
2026-03-05    38293.0
2026-03-06    50186.0
2026-03-07    50767.0
Freq: D, Name: taka, dtype: float64
>>> full.fillna(0).astype("int64").tolist()   # বন্ধ ছিল
[51828, 43760, 0, 0, 38293, 50186, 50767]
>>> full.ffill().tolist()                     # আগের মান টেনে
[51828.0, 43760.0, 43760.0, 43760.0, 38293.0, 50186.0, 50767.0]
>>> full.interpolate().round().tolist()       # সরলরেখায় অনুমান
[51828.0, 43760.0, 41938.0, 40115.0, 38293.0, 50186.0, 50767.0]
>>> small.reindex(pd.date_range("2026-03-01", "2026-03-07"),
...               fill_value=0).tolist()       # একই কাজ এক ধাপে
[51828, 43760, 0, 0, 38293, 50186, 50767]

কোনটি ঠিক, তা নির্ভর করে কেন দিনটি ফাঁকা তার ওপর, pandas-এর ওপর নয়। দোকান বন্ধ থাকলে বিক্রি সত্যিই 0, তাই fillna(0). তাপমাত্রা, দাম বা গুদামের মজুদের মতো “অবস্থা”-র মাপে forward fill (ffill) যুক্তিসংগত, কারণ মাপ না নিলেও অবস্থা থেকে যায়। কিন্তু বিক্রির মতো গোনা সংখ্যায় ffill মানে একই দিনের বিক্রি দুবার গোনা: ওপরের উদাহরণে দুই দিনে বাড়তি ৮৭ হাজার টাকার বানানো বিক্রি। Interpolation মসৃণ অনুমান দেয়, কিন্তু অনুমিত বিক্রি আসল বিক্রি নয়। কয়টি দিন কীভাবে ভরলেন, তা একটি আলাদা কলামে (was_missing) রাখুন। Rolling-এর একটি সূক্ষ্মতাও এখানে: ফাঁকা দিনসহ ডেটায় rolling(3) শেষ তিনটি সারি নেয়, যা পাঁচ দিনের পরিসর হতে পারে; rolling("3D") শেষ তিন ক্যালেন্ডার দিন নেয়। আমাদের run-এ ৫ মার্চে rolling(3).sum() দিয়েছে 133881 (১, ২, ৫ তারিখ) আর rolling("3D").sum() দিয়েছে 38293 (শুধু ৫ তারিখ, কারণ ৩ আর ৪ ফাঁকা)।

৭ · resample — দিন থেকে সপ্তাহ আর মাস

মাস শেষে দোকানদার প্রতিদিনের পাতা উল্টে উল্টে সপ্তাহের যোগফল লেখেন। resample ঠিক এই কাজ: সময়ের ধারাকে নতুন মাপের বাক্সে ভাগ করে প্রতিটি বাক্সে একটি সারাংশ। এটি আসলে পর্ব ২৪-এর groupby, শুধু গ্রুপের চাবি একটি কলাম নয়, সময়ের বাক্স: sales.resample("W-SAT").sum() মানে “শনিবারে শেষ হওয়া প্রতিটি সপ্তাহ ধরে ভাগ, তারপর যোগ”। দুটি প্রশ্ন সব সময় থাকে: বাক্সের সীমা কোথায়, আর বাক্সের নাম (label) কোন দিনের। নিচের ধাপে ১০ দিনের একটি ছোট উদাহরণ (হাজার টাকায়) দুই সপ্তাহে ভাগ হচ্ছে।

১২ বৃহঃ
52
১৩ শুক্র
47
১৪ শনি
60
১৫ রবি
58
১৬ সোম
91
১৭ মঙ্গল
95
১৮ বুধ
54
১৯ বৃহঃ
49
২০ শুক্র
51
২১ শনি
63
  1. দশটি দিন। ১২ মার্চ বৃহস্পতিবার থেকে ২১ মার্চ শনিবার, প্রতিদিনের বিক্রি হাজার টাকায়। লক্ষ্য resample("W-SAT").sum(): শনিবারে শেষ হওয়া সপ্তাহ।
  2. প্রথম বাক্স। সাপ্তাহিক বাক্স ডান দিকে বন্ধ: শনিবার ৭ মার্চের পরের দিন থেকে শনিবার ১৪ মার্চ পর্যন্ত। আমাদের ডেটা শুরু ১২ তারিখে, তাই এই বাক্সে মাত্র তিন দিন: 52 + 47 + 60 = 159. এটি একটি অসম্পূর্ণ সপ্তাহ।
  3. দ্বিতীয় বাক্স। রবিবার ১৫ থেকে শনিবার ২১, পুরো সাত দিন: 58 + 91 + 95 + 54 + 49 + 51 + 63 = 461. বাংলাদেশের কাজের সপ্তাহ রবিবারে শুরু, তাই W-SAT এখানে স্বাভাবিক বাছাই।
  4. ফল ও নাম। প্রতিটি বাক্সের নাম তার শেষ দিন: 2026-03-14 আর 2026-03-21. সাপ্তাহিক আর মাসিক বাক্সে এটিই default (label="right", closed="right"); দিনের বা ঘণ্টার বাক্সে উল্টো, নাম শুরুর দিন।
  5. Default W দিলে। W মানে W-SUN, রবিবারে শেষ: প্রথম বাক্স ১২–১৫ (চার দিন, 217), দ্বিতীয় ১৬–২১ (ছয় দিন, 403, কারণ ২২ তারিখ ডেটায় নেই)। একই ডেটা, একেবারে ভিন্ন সাপ্তাহিক সংখ্যা।

শেষ ধাপের শিক্ষা হলো, সাপ্তাহিক চার্টে প্রথম আর শেষ সপ্তাহ প্রায়ই অসম্পূর্ণ, আর ছোট দেখায় শুধু কম দিনের কারণে। বাজারঘরের পুরো ৬১ দিনের ডেটায় resample("W-SAT").count() আমাদের run-এ দিয়েছে [7, 7, 7, 7, 7, 7, 7, 7, 5]: শেষ সপ্তাহ (নাম ২ মে) মাত্র ৫ দিনের, তাই এর যোগফল বাকিগুলোর চেয়ে প্রায় এক-তৃতীয়াংশ কম। সমাধান: agg(["sum", "count"]) দিয়ে দিনের সংখ্যাও পাশে রাখুন, আর তুলনার সময় অসম্পূর্ণ বাক্স বাদ দিন বা দৈনিক গড়ে তুলনা করুন। মাসের বেলায় একই কথা: মার্চ ৩১ দিনের, এপ্রিল ৩০ দিনের, ফেব্রুয়ারি ২৮ দিনের; মাসিক যোগফলের তুলনায় এই এক-দুই দিনের পার্থক্যও কয়েক শতাংশ বদলায়।

৮ · rolling আর expanding — চলমান গড় আর উৎসবের ঢেউ

ক্রিকেটে একজন ব্যাটসম্যানের “শেষ পাঁচ ইনিংসের গড়” তাঁর এখনকার ফর্ম বোঝায়, আর “ক্যারিয়ারের গড়” পুরো ইতিহাস। প্রথমটি rolling(5).mean(), দ্বিতীয়টি expanding().mean(). Rolling window একটি নির্দিষ্ট মাপের জানালা, যা প্রতিদিন এক ঘর সরে; আর expanding window শুরুতে বাঁধা থাকে, প্রতিদিন বড় হয়। দৈনিক বিক্রির এলোমেলো ওঠানামা মসৃণ করতে ৭ দিনের moving average সবচেয়ে প্রচলিত, কারণ ৭ দিনে প্রতিটি বার ঠিক একবার পড়ে, তাই সপ্তাহের ভেতরের ছন্দ (শুক্রবারের ভিড়) গড়ে মিশে যায়। নিচের ধাপে একটি ৩ দিনের জানালা সাত দিনের ওপর দিয়ে সরছে।

বৃহঃ
শুক্র
শনি
রবি
সোম
মঙ্গল
বুধ
52
47
60
58
91
95
54
  1. সাত দিনের বিক্রি (হাজার টাকা, ছোট উদাহরণ)। সোম আর মঙ্গলবারে 91 আর 95: একটি কাল্পনিক উৎসবের আগের ভিড়। জানালার মাপ 3, ফল দাঁড়াবে প্রতিটি জানালার শেষ দিনের ঘরে।
  2. প্রথম পূর্ণ জানালা বৃহঃ, শুক্র, শনি: (52 + 47 + 60) / 3 = 53.0, বসে শনিবারের ঘরে। বৃহঃ আর শুক্রবারে তিনটি দিন জোটেনি, তাই সেখানে NaN. min_periods=1 দিলে সেখানে 52.0 আর 49.5 আসত।
  3. এক ঘর সরা। পুরোনো বৃহঃ বাদ, নতুন রবি যোগ: (47 + 60 + 58) / 3 = 55.0. প্রতিটি ধাপে একটি মান ঢোকে, একটি বের হয়।
  4. ভিড় ঢুকছে। শনি, রবি, সোম: (60 + 58 + 91) / 3 ≈ 69.7. একদিনের লাফ 58 থেকে 91, কিন্তু গড় উঠেছে মাত্র 55.0 থেকে 69.7: জানালা ঢেউকে নরম করে।
  5. চূড়া। রবি, সোম, মঙ্গল: (58 + 91 + 95) / 3 ≈ 81.3. ভিড়ের দুই দিন একসঙ্গে জানালায়, তাই সবচেয়ে বড় গড়।
  6. ঢেউ নামছে, কিন্তু দেরিতে। সোম, মঙ্গল, বুধ: (91 + 95 + 54) / 3 = 80.0. বুধবারের আসল বিক্রি 54, স্বাভাবিকে ফিরেছে, কিন্তু গড় এখনো 80: পেছনের দিকে তাকানো জানালা সবসময় একটু দেরিতে সাড়া দেয়। এটিই এর দাম।

এখন পুরো বাজারঘরের ডেটা। নিচের কোড মার্চ-এপ্রিলের ৬১ দিনের দৈনিক বিক্রি বানায়: একটি সাধারণ স্তর, শুক্রবারে একটু বেশি (সাপ্তাহিক seasonality), ১৫–১৯ মার্চ একটি উৎসবের ভিড়, ১৪ এপ্রিল পহেলা বৈশাখের লাফ, আর seed দেওয়া এলোমেলো ওঠানামা। সতর্কতা: ঈদের তারিখ চাঁদ দেখার ওপর নির্ভর করে, আর হিজরি বছর সৌর বছরের চেয়ে প্রায় ১০–১১ দিন ছোট বলে প্রতি বছর ঈদ ইংরেজি ক্যালেন্ডারে আগে সরে আসে। এখানে ১৫–১৯ মার্চ শুধু একটি কাল্পনিক “ঈদের আগের ভিড়”, কোনো বছরের আসল ঈদের তারিখের দাবি নয়। তারপর সাপ্তাহিক ও মাসিক যোগফল, ৭ দিনের চলমান গড়, expanding গড় আর একটি chart.

import numpy as np, pandas as pd
import matplotlib.pyplot as plt
pd.set_option("display.width", 60)

rng = np.random.default_rng(42)
days = pd.date_range("2026-03-01", "2026-04-30", freq="D")
base = 50_000 + 8_000 * (days.day_of_week == 4)   # শুক্রবার বেশি
spike = np.where((days >= "2026-03-15") & (days <= "2026-03-19"),
                 40_000, 0)                       # কাল্পনিক ঈদের ভিড়
spike += np.where(days == "2026-04-14", 25_000, 0)  # পহেলা বৈশাখ
taka = base + spike + rng.normal(0, 6_000, len(days))
sales = pd.Series((taka * 100).round().astype("int64"), index=days,
                  name="paisa")                  # টাকা নয়, পয়সা

k = lambda s: (s / 100_000).round(1)             # পয়সা → হাজার টাকা
print("প্রথম তিন দিন:", k(sales.head(3)).tolist())
week = sales.resample("W-SAT").sum()             # রবি…শনি এক সপ্তাহ
print(len(week), "সপ্তাহ; প্রথম তিনটি:", k(week.head(3)).tolist())
print("মাস:", k(sales.resample("ME").sum()).tolist())

roll7 = sales.rolling(7).mean()                  # ৭ দিনের চলমান গড়
grow = sales.expanding().mean()                  # শুরু থেকে আজ পর্যন্ত
print("NaN:", roll7.isna().sum(), "| ১৯ মার্চ:", k(roll7["2026-03-19"]),
      "| শেষ গড়:", k(grow.iloc[-1]))

fig, ax = plt.subplots(figsize=(7, 3.2))
ax.plot(days, sales / 100_000, lw=0.8, color="#94a3b8", label="Daily")
ax.plot(days, roll7 / 100_000, lw=2, color="#2563eb",
        label="7-day mean")
ax.axvspan(pd.Timestamp("2026-03-15"), pd.Timestamp("2026-03-19"),
           color="#fde68a", alpha=0.5, label="Festival week (fictional)")
ax.set(title="Bazarghar daily sales", ylabel="Sales ('000 taka)")
ax.legend(fontsize=8); fig.autofmt_xdate(); plt.tight_layout()
plt.show()

আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2; chart কোডের নিচে আঁকা হয়):

প্রথম তিন দিন: [51.8, 43.8, 54.5]
9 সপ্তাহ; প্রথম তিনটি: [345.0, 368.0, 556.0]
মাস: [1797.9, 1554.5]
NaN: 6 | ১৯ মার্চ: 80.6 | শেষ গড়: 55.0

প্রথম তিন দিন ৫১.৮, ৪৩.৮ আর ৫৪.৫ হাজার টাকা। ৬১ দিন রবি-শনি সপ্তাহে ৯টি বাক্স হয়েছে; প্রথম সপ্তাহ পুরো (১ মার্চ ২০২৬ রবিবার), আর তৃতীয় সপ্তাহে (১৫–২১ মার্চ) উৎসবের ভিড় থাকায় যোগফল প্রায় দেড় গুণ, ৫৫৬ হাজার। মার্চের মোট এপ্রিলের চেয়ে বেশি, তার এক কারণ ভিড়, আরেক কারণ মার্চে একদিন বেশি। rolling(7)-এর প্রথম ৬টি ঘর NaN, কারণ সাত দিন না জমলে গড় হয় না। ১৯ মার্চের চলমান গড় প্রায় ৮০.৬ হাজার, যেখানে সাধারণ স্তর ৫০–৫৮ হাজার: জানালায় ভিড়ের পাঁচ দিনই আছে। Expanding গড় শেষে ৫৫.০ হাজার, পুরো দুই মাসের গড়। Chart-এ ধূসর রেখা দৈনিক ঝাঁকুনি, নীল রেখা চলমান গড়, আর হলুদ অংশ কাল্পনিক উৎসবের সপ্তাহ; নীল রেখা হলুদ অংশের পরে চূড়ায় ওঠে, ঠিক ধাপে দেখা দেরির মতো। পহেলা বৈশাখের একদিনের লাফ ধূসর রেখায় স্পষ্ট, কিন্তু নীল রেখায় প্রায় মিলিয়ে গেছে: একদিনের ঘটনা ৭ দিনের গড়ে সাত ভাগের এক ভাগ ওজন পায়।

দুটি সতর্কতা। প্রথমত, rolling(7, center=True) জানালাকে মাঝখানে বসায়, মানে আজকের গড়ে আগামী তিন দিনের বিক্রিও ঢোকে। পেছনের দিকে তাকিয়ে chart আঁকার জন্য এটি সুন্দর, কিন্তু ভবিষ্যদ্বাণীর feature-এ এটি ভবিষ্যৎ চুরি (অংশ ৯)। দ্বিতীয়ত, ঈদ প্রতি বছর তারিখ বদলায়, তাই “মার্চের তৃতীয় সপ্তাহ” feature কাজ করবে না; কাজ করবে “ঈদের কত দিন আগে”, যাচাই করা ছুটির তালিকা থেকে বানানো। সাম্প্রতিক দিনে বেশি ওজন দিতে চাইলে ewm (exponentially weighted) আছে। SQL-এ একই হিসাব window function দিয়ে হয়, যা DBMS কোর্সের window function পর্বে আছে।

৯ · shift, diff, pct_change আর lag feature — ভবিষ্যৎ যেন না ঢোকে

আবহাওয়ার পূর্বাভাস দেওয়ার সময় আবহাওয়াবিদ শুধু আজ পর্যন্ত মাপা তথ্য ব্যবহার করতে পারেন; আগামীকালের মাপ তাঁর হাতে নেই। Forecasting-এর feature-এও একই শর্ত। “গতকালের বিক্রি”, “গত সপ্তাহের একই বারের বিক্রি”, “গতকাল থেকে কত বাড়ল” — এগুলোকে বলে lag feature, আর এদের যন্ত্র তিনটি:

  • shift(1) প্রতিটি মান এক ঘর নিচে নামায়: আজকের সারিতে গতকালের মান। প্রথম ঘর NaN. shift(7) গত সপ্তাহের একই বার। shift(-1) উল্টো দিকে: আজকের সারিতে আগামীকালের মান।
  • diff() = আজ − গতকাল, মানে s - s.shift(1). diff(-1) = আজ − আগামীকাল।
  • pct_change() = (আজ − গতকাল) / গতকাল, শতাংশের পরিবর্তন; 0.25 মানে ২৫% বৃদ্ধি। গতকাল 0 হলে ফল inf, তাই বিক্রি শূন্য হতে পারে এমন ডেটায় সাবধান।

Data leakage-এর অর্থ (data leakage) আপনি পর্ব ৫-এ প্রথম দেখেছেন: model-কে এমন তথ্য দেওয়া, যা ভবিষ্যদ্বাণীর মুহূর্তে বাস্তবে পাওয়া যেত না। Time series-এ এর সবচেয়ে সাধারণ রূপ একটি চিহ্নের ভুল: shift(1)-এর জায়গায় shift(-1). নিচের কোড আগামীকালের বিক্রি আন্দাজ করার একটি ছোট linear model বানায় (NumPy-র lstsq দিয়ে, পর্ব ১৭-এর least squares), দুই সেট feature দিয়ে: একটি সৎ, একটিতে ভবিষ্যৎ লুকানো। Train আর test ভাগ হয়েছে তারিখ ধরে, এলোমেলো নয়।

import numpy as np, pandas as pd
pd.set_option("display.width", 60)
rng = np.random.default_rng(42)
days = pd.date_range("2026-03-01", "2026-04-30", freq="D")
fri = 8 * (days.day_of_week == 4)            # শুক্রবার বেশি
s = pd.Series((55 + fri + rng.normal(0, 6, len(days))).round(1),
              index=days)   # দৈনিক বিক্রি, হাজার টাকায় (কাল্পনিক)

df = pd.DataFrame({"sales": s,
                   "lag1": s.shift(1),        # গতকাল
                   "diff1": s.diff(),         # আজ − গতকাল
                   "pct": s.pct_change().round(3)})
print(df.head(4))

df["y"] = s.shift(-1)              # লক্ষ্য: আগামীকালের বিক্রি
df["leak"] = s.diff(-1)            # ভুল: আজ − আগামীকাল!
df["lag7"] = s.shift(7)            # গত সপ্তাহের একই বার
df = df.dropna()

train = df[:"2026-04-15"]          # সময় ধরে ভাগ, এলোমেলো নয়
test = df["2026-04-16":]

def mae(cols):
    X = np.c_[np.ones(len(train)), train[cols]]
    w, *_ = np.linalg.lstsq(X, train["y"], rcond=None)
    Xt = np.c_[np.ones(len(test)), test[cols]]
    return np.abs(Xt @ w - test["y"]).mean().round(2)

print("rows:", len(train), len(test))
print("baseline (আজকের মান):",
      (test["sales"] - test["y"]).abs().mean().round(2))
print("সৎ feature:", mae(["sales", "lag1", "lag7", "diff1"]))
print("leak সহ:", mae(["sales", "lag1", "leak"]))

আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):

            sales  lag1  diff1    pct
2026-03-01   56.8   NaN    NaN    NaN
2026-03-02   48.8  56.8   -8.0 -0.141
2026-03-03   59.5  48.8   10.7  0.219
2026-03-04   60.6  59.5    1.1  0.018
rows: 39 14
baseline (আজকের মান): 7.55
সৎ feature: 4.97
leak সহ: 0.0

প্রথম টেবিলে তিন যন্ত্রের কাজ: ২ মার্চের সারিতে lag1 = 56.8 (১ মার্চের মান), diff1 = 48.8 − 56.8 = −8.0, আর pct = −8.0 / 56.8 ≈ −0.141, মানে ১৪% কমেছে। প্রথম সারিতে তিনটিই NaN, কারণ আগের দিন নেই। dropna()-এর পর (lag7-এর জন্য প্রথম ৭ দিন, আর লক্ষ্যের জন্য শেষ দিন বাদ) ৩৯টি দিন train, ১৪টি test. Baseline “আগামীকাল = আজ” ধরে গড়ে প্রায় ৭.৫৫ হাজার টাকা ভুল করে (MAE); সৎ feature দিয়ে ভুল কমে ৪.৯৭-এর কাছাকাছি, মূলত শুক্রবারের ছন্দ ধরতে পারায়। আর leak-সহ model-এর ভুল 0.0: নিখুঁত। কারণ সরল বীজগণিত: leak = আজ − আগামীকাল, তাই আগামীকাল = sales − leak, আর linear model ঠিক এই সূত্রটি শিখে নিয়েছে। বাস্তবে আজ রাতে আগামীকালের বিক্রি জানা নেই, তাই এই feature কখনো পাওয়া যাবে না; কাগজে নিখুঁত model বাস্তবে চলবেই না।

মূল ধারণা

প্রতিটি feature-এর পাশে প্রশ্ন করুন: “ভবিষ্যদ্বাণীর মুহূর্তে এটি কি হাতে থাকত?” shift(-k), diff(-k), center=True, পুরো ডেটার গড় দিয়ে scaling, বা এলোমেলো train-test ভাগ — সবই ভবিষ্যৎ ঢোকানোর পথ। লক্ষ্য (y) বানাতে shift(-1) ঠিক, feature-এ কখনো নয়। আর মেট্রিক হঠাৎ অবিশ্বাস্য রকম ভালো হলে প্রথমে leakage খুঁজুন, উদযাপন পরে।

ভাগের নিয়মটিও সমান গুরুত্বপূর্ণ। এলোমেলোভাবে সারি বেছে test বানালে (train-test split-এর default shuffle=True) ১৫ এপ্রিলের সারি test-এ আর ১৪ ও ১৬ এপ্রিল train-এ পড়তে পারে, আর model দুই পাশের দিন দেখে মাঝের দিন আন্দাজ করে, যা ভবিষ্যদ্বাণী নয়, interpolation. Time series-এ তাই time-based split: একটি তারিখের আগে train, পরে test, যেমন ওপরের কোডে। Validation-এর আরও নিয়ম, যেমন বারবার সামনে সরানো জানালা দিয়ে যাচাই, আসবে পর্ব ৫৬ · Train, validation আর test — data leakage-এর ফাঁদ-এ, আর feature বানানোর পুরো কৌশল পর্ব ৬৩ · Feature engineering-এ।

আরও গভীরে। Time series বিশ্লেষণের ক্লাসিক কাঠামো George Box আর Gwilym Jenkins-এর ১৯৭০ সালের বই “Time Series Analysis: Forecasting and Control”, যেখান থেকে ARIMA model-এর পদ্ধতি জনপ্রিয় হয়; Rob Hyndman আর George Athanasopoulos-এর বিনামূল্যের অনলাইন বই “Forecasting: Principles and Practice” আধুনিক পরিচিতি। Python-এ statsmodels (যা Pyodide-এও আছে) seasonal decomposition আর ARIMA দেয়। ভেতরের দিকে, pandas-এর resample আসলে একটি সময়ের বাক্স-নির্ধারক (Resampler) দিয়ে groupby, আর সময়ভিত্তিক rolling ("3D") ভেতরে জানালার শুরু-শেষ index হিসাব করে, তাই ফাঁকা দিনেও ক্যালেন্ডার মানে। M-competition নামের ধারাবাহিক forecasting প্রতিযোগিতার ফল বারবার দেখিয়েছে, সরল পদ্ধতি অনেক সময় জটিল model-এর সমান বা কাছাকাছি করে; তাই baseline সবসময় আগে।

পরীক্ষা ও ইন্টারভিউতে। বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এমন প্রশ্ন প্রায়ই আসে:
● tz_localize আর tz_convert-এর পার্থক্য? — localize naive সময়ে time zone-এর পরিচয় যোগ করে (লেখা একই থাকে); convert একই মুহূর্ত অন্য ঘড়িতে দেখায় (লেখা বদলায়)।
● [40, 50, 45]-এ rolling(2).mean()? — [NaN, 45.0, 47.5].
● resample আর groupby-এর সম্পর্ক? — resample হলো সময়ের বাক্স ধরে groupby; বাক্সের সীমা আর label-এর নিয়ম freq অনুযায়ী।
● Time series-এ এলোমেলো train-test split কেন ভুল? — model ভবিষ্যতের পাশের দিন দেখে ফেলে; তারিখ ধরে ভাগ করতে হয়।
● shift(1) আর shift(-1)? — আগের দিনের মান আর পরের দিনের মান; দ্বিতীয়টি feature-এ leakage.

১০ · AI-এর যুগে — সময়ের কোডে AI সহকারীর ভুল ধরা

“দৈনিক বিক্রি থেকে মাসিক যোগফল আর আগামী সপ্তাহের forecast-এর feature বানাও” লিখলে AI সহকারী কয়েক সেকেন্ডে কোড দেবে, আর সেই কোড প্রায়ই চলবে। সময়ের কোডে ভুলের ধরনগুলো চেনা, আর বেশিরভাগই নিঃশব্দ:

  • পুরোনো API: resample("M"), freq="H", .dt.week, dayofweek; pandas 3-এ প্রথম তিনটি error দেয়, শেষটি সতর্কবার্তা। আবার df.append-এর মতো আরও পুরোনো জিনিসও আসে, যা pandas 2.0-তে সরানো হয়েছে।
  • তারিখ আন্দাজে পড়া: format বা dayfirst ছাড়া to_datetime, সঙ্গে errors="coerce"; ফলে বাংলাদেশের দিন-আগে লেখা তারিখ মাস-আগে পড়া হয়, বা অর্ধেক কলাম NaT হয়, কেউ টের পায় না।
  • Time zone উপেক্ষা: UTC-তে রাখা সময়ে সরাসরি .dt.hour বা .dt.date, ফলে ঢাকার সকাল আর রাত গুলিয়ে যায়; বা tz_localize দিয়ে convert-এর কাজ করে ৬ ঘণ্টার ভুল।
  • Leakage: shift(-1) বা center=True feature-এ, পুরো ডেটা দিয়ে scaling, এলোমেলো split, আর train সেটেই মেট্রিক দেখিয়ে “৯৯% accurate” দাবি।
  • বানানো parameter আর লুকানো সতর্কবার্তা: অস্তিত্বহীন argument (যেমন resample(..., tz=...)), বা warnings.filterwarnings("ignore") দিয়ে সব চাপা দেওয়া; আর ফাঁকা দিন ffill করে বিক্রি দুবার গোনা।

প্রম্পটে version, তারিখের format, time zone, সপ্তাহের নিয়ম আর leakage-এর শর্ত স্পষ্ট বলে দিন। আসল ক্রেতার ডেটা নয়, বানানো ছোট নমুনা দিন।

আমি Python 3.14 আর pandas 3.0-এ কাজ করছি। আমার CSV-তে order_time কলাম আছে, UTC-তে ISO 8601 লেখা (যেমন 2026-04-13T20:10:00Z), আর amount_paisa কলাম (int64). বানানো উদাহরণ সারি: 2026-04-13T20:10:00Z, 152000. ১. order_time-কে aware UTC হিসেবে পড়ো, তারপর Asia/Dhaka-তে convert করো; ঢাকার তারিখ ধরে দৈনিক মোট বানাও। ব্রাউজারে চালাব, তাই শুরুতে import tzdata রাখো। ২. ফাঁকা দিন asfreq("D") দিয়ে দেখাও আর 0 দিয়ে ভরো (দোকান বন্ধ মানে বিক্রি 0); কয়টি দিন ভরলে তা ছাপো। ৩. সাপ্তাহিক যোগফল W-SAT দিয়ে (রবি থেকে শনি), সঙ্গে প্রতিটি সপ্তাহে কয়দিনের ডেটা আছে সেটিও; মাসিক হলে ME, পুরোনো "M" নয়। ৪. আগামীকালের বিক্রির জন্য feature: lag1, lag7, rolling 7 দিনের গড় — শুধু shift(1)-এর পরের ডেটা দিয়ে, যাতে আজকের লক্ষ্যের কোনো তথ্য না ঢোকে। shift(-1) শুধু লক্ষ্য y-এর জন্য। ৫. train আর test তারিখ ধরে ভাগ করো, shuffle নয়; "আগামীকাল = আজ" baseline-এর MAE-ও দাও। কোনো warning চাপা দেবে না। কোনো function বা parameter নিয়ে নিশ্চিত না হলে বলো; বানিয়ে লিখবে না।

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

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

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

প্র ০১সময়কে “১৯৭০ থেকে কত সেকেন্ড” হিসেবে রাখার ধারণা কোথা থেকে এল, আর pandas কেন nanosecond থেকে microsecond-এ সরে এল?

কম্পিউটারে সময় রাখার সবচেয়ে প্রচলিত উপায় এসেছে Unix অপারেটিং সিস্টেম থেকে। ১৯৭০-এর দশকের শুরুতে Bell Labs-এ Unix-এর নকশাকারেরা ঠিক করেন, সময় হবে একটি নির্দিষ্ট শুরুর মুহূর্ত থেকে পেরোনো সেকেন্ডের সংখ্যা; শেষ পর্যন্ত সেই মুহূর্ত দাঁড়ায় ১ জানুয়ারি ১৯৭০, UTC. এই “Unix epoch” বেছে নেওয়ার পেছনে গভীর কোনো জ্যোতির্বিদ্যা নেই, ছিল সুবিধা: একটি গোল তারিখ, যা তখনকার কাছাকাছি। একটি সংখ্যা হিসেবে সময় রাখলে তুলনা, বিয়োগ আর সাজানো হয় সাধারণ পূর্ণসংখ্যার গতিতে, আর time zone বা ক্যালেন্ডারের জটিলতা শুধু দেখানোর সময় আসে।

সংখ্যার আকারের একটি দাম আছে। পুরোনো সিস্টেমে সেকেন্ড রাখা হতো 32-bit signed integer-এ, যা ২০৩৮ সালের জানুয়ারিতে ফুরিয়ে যায় (“Year 2038 problem”). NumPy-র datetime64 64-bit সংখ্যা নেয় আর একক বেছে নিতে দেয়; একক যত সূক্ষ্ম, পরিসর তত ছোট। Nanosecond এককে পরিসর মোটামুটি ১৬৭৭ থেকে ২২৬২ সাল, microsecond-এ হাজার গুণ বড়।

pandas বহু বছর শুধু nanosecond ব্যবহার করত, তাই ঐতিহাসিক নথি বা “৯৯৯৯-১২-৩১” ধরনের প্রহরী তারিখ পড়তে গেলে OutOfBoundsDatetime error দিত। pandas 2.0 থেকে অন্য একক রাখার সুযোগ আসে, আর pandas 3.0-তে লেখা থেকে পড়া তারিখের default একক microsecond হয়েছে, যা এই পর্বের run-এ datetime64[us] হিসেবে দেখেছেন। এর একটি ব্যবহারিক প্রভাব: পুরোনো কোড যদি ধরে নেয় ভেতরের সংখ্যা nanosecond (যেমন astype("int64") // 10**9 দিয়ে সেকেন্ড বের করা), সেটি এখন হাজার গুণ ভুল দিতে পারে। তাই ভেতরের সংখ্যা নিয়ে কাজ করলে আগে .dtype বা .unit দেখুন, অথবা as_unit("s")-এর মতো স্পষ্ট রূপান্তর ব্যবহার করুন।

মূল উপলব্ধি: সময় ভেতরে একটি সংখ্যা মাত্র; কোন একক আর কোন শুরুর মুহূর্ত, সেটিই তার অর্থ ঠিক করে, আর library-র version বদলালে সেই অনুমান বদলাতে পারে।

প্র ০২পৃথিবীর time zone-এর নিয়ম কে রাখে, আর বাংলাদেশের ২০০৯ সালের DST-এর মতো ঘটনা সফটওয়্যারের জন্য কেন কঠিন?

“Asia/Dhaka” নামটি এসেছে IANA time zone database থেকে, যাকে অনেকে tz database বা Olson database বলেন, এর দীর্ঘদিনের রক্ষণাবেক্ষণকারী Arthur David Olson-এর নামে। ১৯৮০-এর দশকে স্বেচ্ছাসেবকদের একটি প্রকল্প হিসেবে শুরু হওয়া এই তালিকা পৃথিবীর প্রতিটি অঞ্চলের ঘড়ির নিয়ম রাখে, শুধু আজকের নয়, ইতিহাসসহ: কোন বছর কোন offset, কবে DST শুরু-শেষ। ২০১১ সাল থেকে এটি IANA-র তত্ত্বাবধানে, আর Paul Eggert-এর মতো সম্পাদকেরা সরকারি ঘোষণা দেখে বছরে কয়েকবার নতুন সংস্করণ বের করেন। Linux, macOS, Android, Java, আর Python-এর zoneinfo ও tzdata package সবাই এই তালিকা ব্যবহার করে; এই পর্বের ডেস্কটপ run-এ tzdata-র সংস্করণ ছিল 2026.6.

Database-এর রেকর্ড অনুযায়ী বাংলাদেশ ২০০৯ সালের ১৯ জুন রাত ১১টায় ঘড়ি এক ঘণ্টা এগিয়ে দেয়, আর বছরের শেষে আবার পিছিয়ে আনে। সিদ্ধান্তটি এসেছিল বিদ্যুৎ সাশ্রয়ের যুক্তিতে, আর প্রচলিত বিবরণ অনুযায়ী ঘোষণা থেকে কার্যকর হওয়ার সময় ছিল কম। সফটওয়্যারের জন্য এটিই সবচেয়ে কঠিন পরিস্থিতি: প্রতিটি ফোন আর সার্ভারের ভেতরের tz database-এর copy হালনাগাদ হতে সময় লাগে, তাই কিছু যন্ত্র পুরোনো UTC+6-এ চলে, কিছু নতুন UTC+7-এ। একই শহরে দুই ঘড়ি। অন্য দেশেও এমন হঠাৎ বদল বহুবার হয়েছে, আর tz database-এর রক্ষণাবেক্ষণকারীরা বারবার আগেভাগে ঘোষণার অনুরোধ করেছেন।

ডেটা বিশ্লেষণে এর প্রভাব দীর্ঘস্থায়ী। ২০০৯ সালের জুন থেকে ডিসেম্বরের naive সময়ের log পেলে প্রশ্ন ওঠে: এটি নতুন ঘড়ির সময়, না পুরোনো? যে যন্ত্র লিখেছে, তার ঘড়ি কি হালনাগাদ ছিল? ১৯ জুন রাত ১১:৩০ বলে কোনো সময় ঢাকার আনুষ্ঠানিক ঘড়িতে আসেনি, আর বছরের শেষ রাতে একটি ঘণ্টা দুবার এসেছে; এই পর্বের run-এ pandas সেজন্যই nonexistent আর ambiguous নিয়ে error দিয়েছে। এই কারণেই অভিজ্ঞ প্রকৌশলীরা সময় UTC-তে রাখেন: UTC-তে কোনো সময় হারায় না, কোনো সময় দুবার আসে না। Python কোর্সের standard library পর্বে zoneinfo-র দিক থেকে একই কথা দেখেছেন।

মূল উপলব্ধি: time zone হলো রাজনীতির সিদ্ধান্ত, প্রকৃতির নিয়ম নয়; তাই এর নিয়ম বদলায়, আর সফটওয়্যারকে একটি হালনাগাদ তালিকার ওপর নির্ভর করতে হয়। নিরাপদ অভ্যাস: রাখুন UTC-তে, দেখানোর সময় convert করুন।

প্র ০৩ভবিষ্যৎ ঢুকে পড়া feature কেন এত সহজে ঘটে, আর এটি কি শুধু ছাত্রদের ভুল, নাকি বড় প্রকল্পেও হয়?

Leakage সহজে ঘটে কারণ টেবিলে ভবিষ্যৎ আর অতীত পাশাপাশি বসে থাকে। বিশ্লেষক যখন ঐতিহাসিক ডেটা নিয়ে কাজ করেন, পুরো দুই মাস তাঁর সামনে; shift(-1) আর shift(1) একটি চিহ্নের পার্থক্য, আর দুটিই একটি সংখ্যার কলাম দেয়। কোড চলে, error নেই, মেট্রিক ভালো। ভুলটি ধরা পড়ে শুধু যখন model বাস্তবে চালু হয়, আর আগামীকালের মান আজ হাতে থাকে না। ক্রিকেটের ভাষায়, ম্যাচ শেষের স্কোরকার্ড হাতে নিয়ে “টস জেতার পর কে জিতবে” অনুমান করা।

এটি শুধু শিক্ষার্থীদের ভুল নয়। Shachar Kaufman আর সহকর্মীরা ২০১১-১২ সালের একটি প্রভাবশালী প্রবন্ধে (“Leakage in Data Mining: Formulation, Detection, and Avoidance”) দেখিয়েছিলেন, KDD Cup-এর মতো নামী ডেটা-মাইনিং প্রতিযোগিতাতেও leakage ঘটেছে, কখনো আয়োজকদের ডেটা প্রস্তুতির ভুলে। পরে Sayash Kapoor আর Arvind Narayanan (Princeton) বিভিন্ন বৈজ্ঞানিক ক্ষেত্রের ML-ভিত্তিক গবেষণা পর্যালোচনা করে বহু প্রবন্ধে leakage-এর প্রমাণ পাওয়ার কথা জানিয়েছেন; নির্দিষ্ট সংখ্যা তাঁদের মূল প্রতিবেদনে দেখে নিন। Time series-এ এর একটি বিশেষ রূপ হলো পরে সংশোধিত পরিসংখ্যান (যেমন সংশোধিত GDP) দিয়ে অতীতের ভবিষ্যদ্বাণী পরীক্ষা করা, যখন সেই সংশোধন তখন প্রকাশিতই হয়নি।

প্রতিকারের নীতি সরল, কিন্তু শৃঙ্খলা লাগে। প্রতিটি সারির জন্য একটি “ভবিষ্যদ্বাণীর মুহূর্ত” ঠিক করুন, আর প্রতিটি feature সেই মুহূর্তের আগের তথ্য দিয়ে বানান। ভাগ করুন তারিখ ধরে, আর পারলে পুরো pipeline (scaling-সহ) শুধু train অংশে fit করুন, যা পর্ব ৬৪-এর scikit-learn pipeline-এর বিষয়। আর সন্দেহজনক ভালো ফলকে একটি সংকেত ধরুন: এই পর্বের কোডে MAE 0.0 আসা আনন্দের নয়, অ্যালার্মের।

মূল উপলব্ধি: leakage-এর বিপদ হলো এটি ভুলকে সাফল্যের মতো দেখায়; সময়ের ডেটায় প্রতিটি feature-এর জন্ম-তারিখ জানা থাকা চাই, আর সেটি ভবিষ্যদ্বাণীর মুহূর্তের আগে হতে হবে।

প্র ০৪চলমান গড় কি নিরীহ মসৃণকারী, নাকি এটি ডেটায় এমন ঢেউ তৈরি করতে পারে যা আসলে নেই?

চলমান গড়ের ধারণা পুরোনো; জ্যোতির্বিদ আর অর্থনীতিবিদেরা উনিশ শতক থেকেই ঝাঁকুনি কমাতে এটি ব্যবহার করেছেন, আর শেয়ারবাজারের chart-এ “৫০ দিনের গড়” আজও প্রচলিত। এর আকর্ষণ সরলতা, কিন্তু এর দুটি পার্শ্বপ্রতিক্রিয়া আছে।

প্রথমটি দেরি। পেছনে তাকানো k দিনের জানালার কেন্দ্র আজ থেকে প্রায় (k − 1) / 2 দিন পেছনে। ৭ দিনের গড়ে প্রায় ৩ দিন। এই পর্বের ধাপে দেখেছেন, ভিড় শেষ হয়ে বুধবার বিক্রি স্বাভাবিকে ফিরলেও গড় তখনো ৮০; বাজারঘরের chart-এ নীল রেখা উৎসবের সপ্তাহের পরে চূড়ায় ওঠে। মহামারির সময় বিভিন্ন দেশের দৈনিক সংক্রমণের ৭ দিনের গড় নিয়ে এই আলোচনা অনেক হয়েছিল: গড় ঝাঁকুনি কমায়, কিন্তু প্রবণতার মোড় কয়েক দিন দেরিতে দেখায়। সিদ্ধান্তের গতি যেখানে জরুরি, সেখানে এই দেরি একটি খরচ।

দ্বিতীয়টি সূক্ষ্মতর। ১৯২০-এর দশকে রুশ পরিসংখ্যানবিদ Eugen Slutsky দেখান, সম্পূর্ণ এলোমেলো সংখ্যার ওপর বারবার চলমান গড় বা যোগফল প্রয়োগ করলে এমন নিয়মিত-দেখতে ঢেউ তৈরি হতে পারে, যা অর্থনীতির “ব্যবসা চক্রের” মতো লাগে। প্রচলিতভাবে একে Slutsky–Yule effect বলা হয় (George Udny Yule-ও কাছাকাছি সময়ে সম্পর্কিত কাজ করেছিলেন)। অর্থাৎ মসৃণ করা ডেটায় একটি চক্র দেখলে আগে জিজ্ঞেস করতে হয়, চক্রটি কি ডেটায় ছিল, নাকি আমাদের ছাঁকনি বানিয়েছে। তাই মসৃণ রেখার পাশে আসল দৈনিক রেখাও রাখুন, যেমন এই পর্বের chart-এ ধূসর রেখা, আর জানালার মাপ বাছাইয়ের কারণ লিখে রাখুন।

মূল উপলব্ধি: চলমান গড় একটি ছাঁকনি, আর প্রতিটি ছাঁকনি কিছু সরায়, কিছু দেরি করায়, কখনো কিছু বানায়; তাই ছাঁকা রেখা আর আসল রেখা দুটিই দেখুন।

প্র ০৫“এক সপ্তাহ” বা “এক মাস” এত সরল শোনায়; তাহলে ক্যালেন্ডার নিয়ে সফটওয়্যারে এত ভুল কেন হয়, আর বাংলাদেশের প্রেক্ষাপটে বাড়তি কী জটিলতা আছে?

মানুষের ক্যালেন্ডার বহু শতাব্দীর আপসের ফল। মাস ২৮ থেকে ৩১ দিনের, বছর ৩৬৫ বা ৩৬৬ দিনের, আর সপ্তাহ কোন দিনে শুরু তা দেশভেদে আলাদা: ISO 8601 বলে সোমবার, যুক্তরাষ্ট্রের অনেক ক্যালেন্ডার রবিবার, আর বাংলাদেশে কাজের সপ্তাহ রবিবারে শুরু। প্রোগ্রামারদের মধ্যে “Falsehoods programmers believe about time” নামে বহুল প্রচারিত তালিকায় “প্রতিটি দিন ২৪ ঘণ্টার” বা “সপ্তাহ ১ সবসময় জানুয়ারিতে” ধরনের ভুল ধারণা সংকলিত; এই পর্বেই দেখেছেন, ২০২৭-এর ১ জানুয়ারি ISO হিসাবে ২০২৬-এর সপ্তাহ 53.

pandas-এর frequency-র নামেও এই ইতিহাসের ছাপ। আগে "M" মানে ছিল মাসের শেষ, কিন্তু একই অক্ষর অন্য জায়গায় মাসের period বোঝাত; এই দ্ব্যর্থতা দূর করতে pandas 2.2 "ME" (month end) আর "MS" (month start) আলাদা করে, পুরোনো নামে সতর্কবার্তা দেয়, আর pandas 3-এ পুরোনো নাম error. এই পর্বের run-এ ঠিক সেটিই দেখেছেন। Library যখন অস্পষ্টতা দূর করে, পুরোনো কোড ভাঙে; এটি সেই দামের একটি উদাহরণ।

বাংলাদেশের ডেটায় এর ওপর বাড়তি স্তর আছে। ব্যবসায়িক হিসাব চলে গ্রেগরিয়ান ক্যালেন্ডারে, সরকারি অর্থবছর জুলাই থেকে জুন, পহেলা বৈশাখ ১৪ এপ্রিল নির্দিষ্ট, কিন্তু দুই ঈদ, শবে বরাত বা শবে কদরের মতো দিন হিজরি চান্দ্র ক্যালেন্ডারে, যা প্রতি বছর প্রায় ১১ দিন সরে, আর চূড়ান্ত তারিখ চাঁদ দেখা কমিটির ঘোষণার ওপর নির্ভর করে। তাই কোনো library-র ভেতরে বাংলাদেশের “সঠিক” ছুটির তালিকা আগে থেকে বসানো থাকে না, আর থাকলেও তা যাচাই করা দরকার। একটি forecasting model যদি শিখে “মার্চের তৃতীয় সপ্তাহে বিক্রি বাড়ে”, তা কয়েক বছরে ভুল হয়ে যাবে, কারণ ঈদ সরে যাবে; “ঈদের কত দিন আগে” feature বছর বদলালেও কাজ করে।

মূল উপলব্ধি: ক্যালেন্ডার হলো সংস্কৃতি আর রাজনীতির নিয়ম; সফটওয়্যারে প্রতিটি “সপ্তাহ”, “মাস” আর “ছুটি”-র অর্থ স্পষ্ট করে লিখুন, আর স্থানীয় ঘটনার তারিখ আন্দাজে নয়, যাচাই করা উৎস থেকে নিন।

অনুশীলন

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

  1. হিসাব — হাতে rolling, diff আর pct_change: ১২ থেকে ১৫ এপ্রিলের দৈনিক বিক্রি (হাজার টাকা) a = [40, 50, 45, 60]. হাতে বের করুন: (ক) a.shift(1), (খ) a.diff(), (গ) a.pct_change(), (ঘ) a.rolling(3).mean(), (ঙ) a.expanding().max().

    (ক) [NaN, 40, 50, 45]: প্রতিটি মান এক ঘর নিচে। (খ) [NaN, 10, −5, 15]: 50 − 40, 45 − 50, 60 − 45. (গ) [NaN, 0.25, −0.1, 0.3333]: 10 / 40, −5 / 50, 15 / 45. (ঘ) [NaN, NaN, 45.0, 51.67]: (40 + 50 + 45) / 3 = 45, (50 + 45 + 60) / 3 ≈ 51.67. (ঙ) [40, 50, 50, 60]: শুরু থেকে এ পর্যন্ত সর্বোচ্চ।

    আমাদের ডেস্কটপ run-এ (pandas 3.1.1) পাঁচটিই ঠিক এই মান দিয়েছে। লক্ষ করুন, (গ)-তে শতাংশের পরিবর্তন প্রতিসম নয়: 40 থেকে 50 মানে +২৫%, কিন্তু 50 থেকে 40 মানে −২০%. তাই “এক সপ্তাহে ২০% কমে পরের সপ্তাহে ২০% বাড়লে আগের জায়গায় ফেরে” কথাটি ভুল: 100 → 80 → 96.

  2. আউটপুট অনুমান করুন: চালানোর আগে লিখুন তিনটি সংখ্যা কী আসবে, তারপর import tzdata আর import pandas as pd দিয়ে চালিয়ে মিলিয়ে দেখুন।
    t = pd.to_datetime("05/06/2026 01:30", dayfirst=True)
    u = t.tz_localize("Asia/Dhaka").tz_convert("UTC")
    print(t.month, u.day, u.hour)
    6 4 19

    dayfirst=True, তাই "05/06" মানে ৫ জুন: মাস 6. ঢাকার ৫ জুন রাত ১:৩০ থেকে ৬ ঘণ্টা পেছালে UTC-তে ৪ জুন সন্ধ্যা ৭:৩০, তাই দিন 4, ঘণ্টা 19. আমাদের ডেস্কটপ run-এ ঠিক এটি এসেছে। dayfirst না দিলে একই লেখা ৬ মে হতো (আমাদের run-এ .month দিয়েছে 5), আর তিনটি সংখ্যাই বদলে যেত। ঢাকার মধ্যরাত থেকে সকাল ৬টার যেকোনো সময় UTC-তে আগের তারিখে পড়ে, ঠিক ছবি ২৬.১-এর হলুদ অংশের মতো।

  3. নকশা ও সমালোচনা: একটি AI সহকারী “আগামীকালের বিক্রির forecast-এর feature” হিসেবে নিচের কোড দিয়েছে, আর বলেছে test-এ ৯৯% নির্ভুল। অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো সংস্করণের রূপরেখা দিন।
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df = df.set_index("date").resample("M").sum()
    df["roll"] = df["sales"].rolling(7, center=True).mean()
    df["next"] = df["sales"].shift(-1)
    df["temp"] = df["temp"].fillna(0)
    X_tr, X_te, y_tr, y_te = train_test_split(
        df[["roll", "next", "temp"]], df["sales"])
    • তারিখ আন্দাজে: format বা dayfirst নেই, সঙ্গে errors="coerce"; দিন-আগে লেখা তারিখ ভুল পড়া বা NaT হয়ে হারাতে পারে, আর কোনো গণনা ছাপা হয়নি।
    • ভুল ধাপ আর পুরোনো নাম: লক্ষ্য দৈনিক forecast, অথচ resample("M") মাসিক যোগফল বানায়; তার ওপর pandas 3-এ "M" error দেয় ("ME" লাগবে)। আর মাসিক ডেটায় ৭ ঘরের rolling মানে ৭ মাস।
    • center=True: জানালায় ভবিষ্যতের তিন ঘর ঢোকে; feature-এ leakage. লাগবে shift(1).rolling(7).mean().
    • shift(-1) feature-এ: “next” মানে পরের মান, যা ভবিষ্যদ্বাণীর মুহূর্তে জানা নেই; আর লক্ষ্য রাখা হয়েছে আজকের sales, আগামীকালের নয়।
    • fillna(0) তাপমাত্রায়: হারানো তাপমাত্রা 0° নয়; অবস্থার মাপে ffill বা interpolation বেশি যুক্তিসংগত, আর কয়টি ভরা হলো তা চিহ্নিত রাখা দরকার।
    • এলোমেলো split আর কোনো baseline নেই: train_test_split-এর default shuffle সময় মেশায়; তারিখ ধরে ভাগ লাগবে, আর “আগামীকাল = আজ” baseline-এর পাশে MAE দেখাতে হবে। ৯৯% দাবি নিজেই সতর্কসংকেত।

    ভালো রূপরেখা: format দিয়ে তারিখ পড়া আর NaT গোনা; প্রয়োজনে Asia/Dhaka-তে convert; দৈনিক asfreq("D") আর কারণ অনুযায়ী ভরা; y = sales.shift(-1); feature sales, shift(1), shift(7), shift(1).rolling(7).mean(), আর গতকালের তাপমাত্রা; একটি তারিখের আগে train, পরে test; baseline-এর সঙ্গে test MAE.

  4. কোড লিখুন — ছুটির দিন বনাম কাজের দিন: অংশ ৮-এর কোডের বাজারঘরের sales (পয়সায়, seed 42) থেকে বের করুন: (ক) শুক্র-শনির আর রবি-বৃহস্পতির দিনের সংখ্যা ও গড় বিক্রি (টাকায়), (খ) W-SAT সপ্তাহের মধ্যে সবচেয়ে বেশি বিক্রির সপ্তাহ আর তার যোগফল, (গ) প্রতিটি সপ্তাহে কয়দিনের ডেটা আছে।
    wk = sales.index.day_of_week.isin([4, 5])     # শুক্র, শনি
    print(wk.sum(), (~wk).sum())
    print(round(sales[wk].mean() / 100), round(sales[~wk].mean() / 100))
    w = sales.resample("W-SAT").sum()
    print(w.idxmax().date(), w.max() // 100)
    print(sales.resample("W-SAT").count().tolist())

    আমাদের ডেস্কটপ run-এর আউটপুট (pandas 3.1.1):

    16 45
    54586 55089
    2026-03-21 555970
    [7, 7, 7, 7, 7, 7, 7, 7, 5]

    ৬১ দিনের ১৬টি শুক্র-শনি, ৪৫টি কাজের দিন। গড় প্রায় সমান (৫৪,৫৮৬ বনাম ৫৫,০৮৯ টাকা), যদিও শুক্রবার বেশি বিক্রির নিয়ম আছে: শনিবার সাধারণ, আর উৎসবের ভিড় পড়েছে রবি থেকে বৃহস্পতির মধ্যে, যা কাজের দিনের গড় তুলেছে। এটি একটি ভালো শিক্ষা: একটি কারণ (উৎসব) আরেকটি কারণকে (বার) ঢেকে দিতে পারে, তাই তুলনার আগে উৎসবের দিন আলাদা করে দেখা উচিত। সবচেয়ে বড় সপ্তাহ ১৫–২১ মার্চ (নাম ২১ মার্চ), ৫,৫৫,৯৭০ টাকা। শেষ সপ্তাহে মাত্র ৫ দিন, তাই তাকে পুরো সপ্তাহের সঙ্গে সরাসরি তুলনা করা যাবে না।

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

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

ক্রমUTC-তে রাখা অর্ডারের সময় থেকে আগামীকালের বিক্রির forecast-এর ডেটা বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📅 to_datetime-এ format আর UTC দিয়ে সময় পড়া, NaT গোনা
  2. 🕕 tz_convert("Asia/Dhaka") করে ঢাকার দিন ধরা
  3. 📦 resample("D").sum() দিয়ে দৈনিক মোট, ফাঁকা দিন কারণ বুঝে ভরা
  4. ⏪ shift(1), shift(7) আর rolling দিয়ে শুধু অতীতের feature, shift(-1) দিয়ে লক্ষ্য
  5. ✂️ একটি তারিখ ধরে train আর test ভাগ
  6. 📏 test অংশে baseline আর model-এর MAE তুলনা

pandas 3-এ pd.to_datetime(pd.Series(["03/04/2026", "13/04/2026"]), errors="coerce")-এর ফল কী?

  • ৩ এপ্রিল আর ১৩ এপ্রিল
  • ValueError
  • ৪ মার্চ আর NaT
  • দুটিই NaT

একটি naive Timestamp "2026-04-14 02:10"-এ প্রথমে tz_localize("Asia/Dhaka"), তারপর tz_convert("UTC") করলে ফল কী?

  • 2026-04-14 08:10:00+00:00
  • 2026-04-13 20:10:00+00:00
  • 2026-04-14 02:10:00+00:00
  • TypeError

pandas 3-এ দৈনিক বিক্রির একটি Series থেকে মাসের শেষ তারিখে নাম দেওয়া মাসিক যোগফল পেতে কোনটি সঠিক?

  • s.resample("M").sum()
  • s.resample("MS").sum()
  • s.rolling(30).sum()
  • s.resample("ME").sum()

s = pd.Series([10, 20, 30, 40]). s.rolling(3).mean()-এর ফল কী?

  • [NaN, NaN, 20.0, 30.0]
  • [10.0, 15.0, 20.0, 30.0]
  • [NaN, 20.0, 30.0, NaN]
  • [10.0, 15.0, 20.0, 25.0]

আগামীকালের বিক্রি অনুমানের একটি model-এ কোন feature-টি data leakage ঘটায়?

  • sales.shift(1)
  • sales.shift(7)
  • sales.rolling(7, center=True).mean()
  • sales.shift(1).rolling(7).mean()

বাংলাদেশের শুক্র-শনি সাপ্তাহিক ছুটি মেনে ৯ থেকে ১৬ এপ্রিল ২০২৬-এর কাজের দিন পেতে কোনটি উপযুক্ত?

  • pd.bdate_range("2026-04-09", "2026-04-16")
  • pd.bdate_range("2026-04-09", "2026-04-16", freq="C", weekmask="Sun Mon Tue Wed Thu")
  • pd.date_range("2026-04-09", "2026-04-16", freq="D")
  • pd.date_range("2026-04-09", "2026-04-16", freq="W-THU")

শব্দকোষ

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

মনে রাখুন

  1. তারিখ পড়তে format বলুন: format="%d/%m/%Y" বা dayfirst=True; errors="coerce" দিলে সাথে সাথে NaT গুনুন; নিজের ডেটায় ISO 8601.
  2. localize পরিচয় দেয়, convert ঘড়ি বদলায়; রাখুন UTC-তে, দিন-ঘণ্টার হিসাব করুন Asia/Dhaka-তে; ঢাকার রাত ১২টা থেকে সকাল ৬টা UTC-তে আগের তারিখ।
  3. freq-এর নতুন নাম: ME, MS, QE, YE, h; বাংলাদেশের সপ্তাহে W-SAT, কাজের দিনে freq="C" আর weekmask; ফাঁকা দিন কারণ বুঝে ভরুন।
  4. resample = সময়ের বাক্সে groupby; বাক্সে কয়দিন আছে গুনুন। rolling ঢেউ মসৃণ করে কিন্তু দেরিতে সাড়া দেয়; expanding শুরু থেকে আজ।
  5. shift(1) অতীত, shift(-1) ভবিষ্যৎ: ভবিষ্যৎ শুধু লক্ষ্যে, feature-এ কখনো নয়; ভাগ তারিখ ধরে, আর অবিশ্বাস্য ভালো মেট্রিক মানে আগে leakage খোঁজা।

পরের পর্বে: বাজারঘরের অর্ডার যখন লাখ লাখ সারি। কোন dtype কত memory নেয়, category আর chunk দিয়ে কীভাবে বড় ফাইল সামলাবেন, আর Polars কখন বিকল্প হতে পারে।

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

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