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

seaborn — পরিসংখ্যানের chart আর DataFrame থেকে সরাসরি ছবি

seaborn — Statistical Charts Straight from a DataFrame: hue, Facets, KDE, Bootstrap Error Bars, pairplot and heatmap

Taught in Bangla, this lesson uses seaborn 0.13 on tidy DataFrames: semantic mapping with hue and col, axes-level versus figure-level functions, histplot and KDE bandwidth risks, box, strip and violin plots, what bootstrap confidence intervals and SD error bars really mean with hand calculations, pairplot on Iris, and correlation heatmaps with a confounder example.

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

  • seaborn matplotlib-এর ওপরে কী যোগ করে, আর long DataFrame-এর কলাম থেকে hue, size, style, col কীভাবে ছবিতে যায়
  • Axes-level আর figure-level function-এর পার্থক্য, FacetGrid, আর কখন কোনটি বেছে নেবেন
  • Distribution: histplot-এর multiple, KDE আর bandwidth-এর বিপদ, ecdfplot
  • Category: box, violin, strip, swarm, আর barplot/pointplot-এর error bar: bootstrap CI বনাম SD, হাতে হিসাবসহ
  • সম্পর্ক আর matrix: regplot/lmplot, Iris-এ pairplot, jointplot, correlation heatmap, theme, palette আর seaborn.objects

আগের পর্বে matplotlib দিয়ে Figure আর Axes বানিয়ে line, bar, scatter আর histogram আঁকা দেখেছেন (পর্ব ২৮), আর পর্ব ২৯-এ দেখেছেন কোন প্রশ্নে কোন chart. সেখানে একটি জিনিস বারবার হাতে করতে হয়: “COD-এর অর্ডারগুলো আলাদা করো, তাদের histogram নীল রঙে, bKash-এরগুলো কমলায়, তারপর legend”. বাজারঘরের বিশ্লেষক প্রতিদিন এমন প্রশ্নই করেন: কোন পেমেন্ট পদ্ধতিতে অর্ডারের দাম কেমন ছড়ানো, কোন জেলায় ডেলিভারি কত দিন লাগে, দেরি হলে রেটিং কমে কি না। এই প্রশ্নগুলো পরিসংখ্যানের প্রশ্ন, আর এদের chart-এর জন্য বানানো library হলো seaborn. এই পর্বে আমরা pandas-এর DataFrame (পর্ব ২০ থেকে ২৭) থেকে সরাসরি ছবি বানাব, আর প্রতিটি ছবির পেছনে seaborn আসলে কী হিসাব করছে, সংখ্যা মিলিয়ে দেখব।

১ · seaborn কী যোগ করে — matplotlib-এর ওপরে পরিসংখ্যানের স্তর

রান্নার কথা ভাবুন। matplotlib হলো পুরো রান্নাঘর: চুলা, হাঁড়ি, মশলা সব আছে, আর আপনি চাইলে যেকোনো পদ রাঁধতে পারেন, কিন্তু প্রতিটি ধাপ নিজে করতে হয়। seaborn হলো একজন অভিজ্ঞ বাবুর্চি, যাঁকে বললেই হয় “জেলা ধরে ডেলিভারির দিনের box plot, পেমেন্ট অনুযায়ী রং”; তিনি একই রান্নাঘরেই রাঁধেন, শুধু কাটা-বাছা আর মাপজোখ নিজে করে নেন। seaborn নিজে কোনো pixel আঁকে না; সব আঁকা হয় matplotlib দিয়ে। seaborn যা যোগ করে, তা চার ভাগে বলা যায়:

  • DataFrame বোঝে: data=orders, x="district", y="delivery_days". কলামের নামই axis-এর লেবেল হয়।
  • দলে ভাগ করে রং বসায়: hue="payment" লিখলে প্রতিটি পেমেন্ট পদ্ধতির জন্য আলাদা রং আর legend, কোনো লুপ ছাড়াই।
  • পরিসংখ্যান হিসাব করে: histogram-এর bin গোনা, KDE-র মসৃণ বক্ররেখা, গড় আর তার confidence interval, regression line. এ জন্যই নাম “statistical data visualization”.
  • ভালো default: theme, রঙের palette, পড়ার মতো font আকার।

seaborn-এর লেখক Michael Waskom. তিনি ২০১২ সালের দিকে, তখন স্নায়ুবিজ্ঞানের গবেষণার ছাত্র অবস্থায়, নিজের কাজের জন্য এটি লেখা শুরু করেন বলে সাধারণত বলা হয়; পরে এটি Python-এর সবচেয়ে ব্যবহৃত plotting library-গুলোর একটি হয়। এর নকশা নিয়ে তাঁর প্রবন্ধ “seaborn: statistical data visualization” প্রকাশিত হয় ২০২১ সালে Journal of Open Source Software (JOSS)-এ; গবেষণায় seaborn উল্লেখ করতে এই প্রবন্ধটিই উদ্ধৃত করা হয়। প্রবন্ধের মূল কথা: বিশ্লেষক ভাববেন ডেটার অর্থ নিয়ে (“কোন variable কোন দলে”), আর library সেই ভাবনাকে ছবির বৈশিষ্ট্যে অনুবাদ করবে।

লেখার সময়ে seaborn-এর সর্বশেষ স্থির সংস্করণ 0.13.2. এই পাতার চালানো যায় এমন কোড চলে আপনার ব্রাউজারে (Pyodide), সেখানে matplotlib 3.10.8, pandas 3.0.2 আর seaborn 0.13.2. seaborn Pyodide-এর সঙ্গে আসে না, তাই কোনো block প্রথমবার import seaborn করলে পাতাটি সেটি নামিয়ে বসায়; এতে প্রথমবার ১০ থেকে ২০ সেকেন্ডের মতো লাগতে পারে, পরের run দ্রুত। লেখকের ল্যাপটপের run-গুলো (Python 3.14.4, seaborn 0.13.2, matplotlib 3.11.2, pandas 3.1.1) আলাদা করে চিহ্নিত।

একটি পুরোনো ধারণা ঝালাই করুন। পুরোনো টিউটোরিয়ালে লেখা থাকে “seaborn import করলেই সব chart-এর চেহারা বদলে যায়”. বহু বছর আগে তা-ই হতো, কিন্তু এখন import নিজে কোনো style বদলায় না। seaborn-এর চেহারা পেতে sns.set_theme() ডাকতে হয় (অংশ ৯). লেখকের ল্যাপটপে যাচাই: import-এর পর default রং matplotlib-এর #1f77b4, আর set_theme()-এর পর seaborn-এর “deep” palette-এর #4c72b0, পেছনের রং #EAEAF2.

২ · Long ডেটা আর semantic mapping — কলাম থেকে ছবির বৈশিষ্ট্য

seaborn সবচেয়ে ভালো কাজ করে long বা tidy টেবিলে: প্রতিটি সারি একটি পর্যবেক্ষণ (একটি অর্ডার), প্রতিটি কলাম একটি variable (জেলা, পেমেন্ট, দাম). পর্ব ২৫-এ দেখেছেন wide টেবিল কীভাবে melt দিয়ে long হয়; seaborn-এর জন্য সেটিই প্রায় সবসময় প্রথম ধাপ। কারণ সহজ: seaborn-কে আপনি বলবেন “এই কলামটি রঙে যাক”, আর তা বলা যায় কেবল তখনই, যখন সেই তথ্য একটি কলামের মান হিসেবে আছে, কলামের নামে লুকানো নেই।

কোন কলাম ছবির কোন বৈশিষ্ট্যে যাবে, তা বলাকে seaborn-এর নথিতে বলা হয় semantic mapping.

Parameterছবিতে কী বদলায়কোন ধরনের কলামবাজারঘরে উদাহরণ
x, yঅবস্থানসংখ্যা বা categoryx="district", y="delivery_days"
hueরংcategory (আলাদা রং) বা সংখ্যা (রঙের ধারা)hue="payment"
sizeবিন্দু বা রেখার আকারসাধারণত সংখ্যাsize="qty"
styleবিন্দুর আকৃতি বা রেখার ধরনঅল্প কয়েকটি categorystyle="eid"
col, rowআলাদা ছোট panelcategorycol="payment" (শুধু figure-level-এ)

একই তথ্য একাধিক বৈশিষ্ট্যে দেওয়া যায় (hue="payment", style="payment"), আর সেটি প্রায়ই ভালো অভ্যাস: যে পাঠক রং আলাদা করতে পারেন না, তিনি আকৃতি দেখে পারবেন। নিচের stepper-এ ছয়টি অর্ডারের একটি tidy টেবিল থেকে ছবি তৈরির ধাপ দেখুন।

sns.scatterplot(data=t, x="district", y="taka", hue="payment")লাল = এই ধাপে যা ব্যবহার হলো

            
mapping
ব্যবহৃত কলাম
ছবির বৈশিষ্ট্য
স্তর বা পরিসর
  1. Tidy টেবিল। ছয়টি অর্ডার, প্রতিটি একটি সারি। জেলা, পেমেন্ট আর দাম তিনটিই আলাদা কলাম, তাই যেকোনোটিকে যেকোনো বৈশিষ্ট্যে পাঠানো যায়।
  2. x="district". কলামটি লেখা (string), তাই seaborn একে category ধরে। আলাদা মানগুলো টেবিলে প্রথম যে ক্রমে এসেছে, সেই ক্রমে x-অবস্থান পায়: Dhaka 0, Khulna 1, Sylhet 2. অন্য ক্রম চাইলে order=[...].
  3. y="taka". সংখ্যার কলাম, তাই একটি সাধারণ সংখ্যার axis. মানগুলো ৪০০ থেকে ১৫০০ টাকা, axis এই পরিসর ঘিরে থাকে। লেবেলে বসে কলামের নাম।
  4. hue="payment". তিনটি আলাদা মান, প্রথম দেখার ক্রমে COD, bKash, card; প্রতিটি palette-এর পরের রং পায় (এখানে colorblind palette-এর নীল, কমলা, সবুজ). অন্য ক্রম চাইলে hue_order. legend নিজে থেকেই তৈরি।
  5. আঁকা। প্রতিটি সারি থেকে একটি বিন্দু: অর্ডার ৪ বসে (1, 1500)-এ, সবুজ রঙে, কারণ তার পেমেন্ট card. কাজ শেষে scatterplot যে Axes-এ এঁকেছে, সেটি ফেরত দেয়।
  6. Panel-এ ভাগ। একই টেবিলে figure-level relplot-এ col="payment" দিলে প্রতিটি পদ্ধতির জন্য আলাদা ছোট chart, পাশাপাশি। ফেরত আসে একটি FacetGrid. কেন এই পার্থক্য, তা পরের অংশে।

সংখ্যার কলামকে hue-তে দিলে seaborn আলাদা রং নয়, একটি ধারাবাহিক রঙের ধারা (sequential colormap) ব্যবহার করে। কোনো সংখ্যা আসলে category হলে (যেমন রেটিং ১ থেকে ৫, বা ০/১ পতাকা), আর আপনি আলাদা রং চান, তবে আগে কলামটিকে string বা pandas-এর category বানিয়ে নিন।

৩ · Axes-level বনাম figure-level — কে Figure বানায়

seaborn-এর function দুই পরিবারের। Axes-level function একটি matplotlib Axes-এ আঁকে: আপনি ax= দিয়ে নির্দিষ্ট Axes দিতে পারেন, না দিলে বর্তমান Axes নেয়, আর শেষে সেই Axes ফেরত দেয়। Figure-level function নিজেই একটি নতুন Figure বানায়, তার ভেতরে এক বা একাধিক Axes সাজায় (col/row দিয়ে), আর ফেরত দেয় একটি FacetGrid, মানে Figure আর তার Axes-গুলোর একটি মোড়ক। প্রতিটি figure-level function একটি পরিবারের “দরজা”, যার kind= ঠিক করে ভেতরে কোন axes-level function চলবে।

Figure-level (ফেরত FacetGrid)ভেতরের axes-level (ফেরত Axes)কোন প্রশ্নে
displothistplot, kdeplot, ecdfplotএকটি variable কীভাবে ছড়ানো
relplotscatterplot, lineplotদুটি সংখ্যার সম্পর্ক, সময়ের ধারা
catplotstripplot, swarmplot, boxplot, violinplot, barplot, pointplot …category ধরে তুলনা
lmplotregplotসম্পর্ক আর fitted line
pairplot, jointplot(নিজস্ব PairGrid, JointGrid)অনেক জোড়া বা দুই variable আর তাদের distribution

কেন figure-level function ax= মানে না? কারণ সে নিজেই ঠিক করে কতগুলো Axes লাগবে: col="payment"-এ তিনটি পদ্ধতি থাকলে তিনটি। আপনার দেওয়া একটি Axes-এ তিনটি panel আঁটে না। seaborn তাই ax পেলে একটি সতর্কবার্তা দেয় আর নতুন Figure-এই আঁকে। লেখকের ল্যাপটপে সত্যিকারের run (seaborn 0.13.2, matplotlib 3.11.2; অংশবিশেষ):

~/bazarghar-ai$ python facts.py
python 3.14.4 seaborn 0.13.2 matplotlib 3.11.2 pandas 3.1.1
scatterplot returns Axes True
relplot returns FacetGrid axes shape (1, 3)
displot returns FacetGrid
catplot returns FacetGrid
pairplot returns PairGrid
jointplot returns JointGrid
lmplot returns FacetGrid
relplot(ax=...) -> UserWarning relplot is a figure-level function and does not accept the `ax` parameter. You may wish to try scatterplot
displot(ax=...) -> UserWarning `displot` is a figure-level function and does not accept the ax= parameter. You may wish to try histplot.
catplot(ax=...) -> UserWarning catplot is a figure-level function and does not accept target axes. You may wish to try stripplot

দ্বিতীয় লাইনের True মানে scatterplot ঠিক যে Axes দেওয়া হয়েছিল, সেটিই ফেরত দিয়েছে। relplot-এর FacetGrid-এ g.axes একটি NumPy array, shape (1, 3): এক সারি, তিন কলাম panel. সতর্কবার্তাগুলো নিজেই সমাধান বলে দিচ্ছে: নিজের সাজানো Figure-এ আঁকতে চাইলে axes-level function নিন।

Python-এ চালানো লাইনলাল = এই ধাপের লাইন

            
অবস্থা
খোলা Figure
ফেরত পাওয়া জিনিস
আপনার Figure 1-এ আঁকা
  1. নিজের Figure বানানো। matplotlib দিয়ে একটি Figure আর পাশাপাশি দুটি খালি Axes (পর্ব ২৮-এর subplots). এখন বিন্যাস আপনার হাতে।
  2. Axes-level: histplot(..., ax=axs[0]). বাঁয়ের Axes-এ histogram আঁকা হলো, আর ফেরত এল সেই একই Axes; a is axs[0] সত্য।
  3. আরেকটি axes-level. ডানের Axes-এ box plot. একটি Figure-এ ভিন্ন ধরনের দুটি seaborn chart: এটি কেবল axes-level function দিয়েই সম্ভব।
  4. Figure-level: displot(..., col="payment"). seaborn নতুন একটি Figure 2 বানাল, তিনটি panel সহ, আর ফেরত দিল FacetGrid. আপনার Figure 1 ছোঁয়াও হয়নি।
  5. Figure-level-এ ax= দিলে। seaborn সতর্ক করে (ওপরের সত্যিকারের বার্তাটিই), ax উপেক্ষা করে, আর আরও একটি নতুন Figure 3 বানায়। axs[0]-এ কিছুই বদলায় না।
  6. সাজানো। Axes-level-এ সরাসরি matplotlib-এর ax.set_title; figure-level-এ FacetGrid-এর method (set_axis_labels, set_titles), পুরো Figure g.figure, আর প্রতিটি panel g.axes-এর একটি ঘর।
মূল ধারণা

একটি প্রশ্নের অনেক দলের একই রকম ছবি চাইলে figure-level; অন্য chart-এর পাশে নিজের বিন্যাসে বসাতে চাইলে axes-level. Figure-level-এ আকার দেন height (প্রতিটি panel-এর উচ্চতা, ইঞ্চিতে) আর aspect (প্রস্থ ÷ উচ্চতা) দিয়ে, matplotlib-এর figsize দিয়ে নয়। পর্ব ২৯-এ যে small multiples-এর কথা এসেছে, col/row ঠিক সেটিই এক লাইনে বানায়।

৪ · Distribution — histplot, KDE-র bandwidth আর ecdfplot

“অর্ডারের দাম কেমন ছড়ানো?” এই প্রশ্নের প্রথম উত্তর histogram: দামের পরিসরকে সমান মাপের ঘরে (bin) ভাগ করে প্রতিটি ঘরে কতটি অর্ডার পড়ল, তা গোনা। histplot এটি করে, আর hue দিলে প্রতিটি দলের জন্য আলাদা। দল একাধিক হলে বারগুলো কীভাবে বসবে, ঠিক করে multiple:

  • "layer" (default): দলগুলো একটির ওপর আরেকটি, স্বচ্ছ রঙে। দুটি দলে ভালো, তিনটির বেশি হলে জট।
  • "stack": একটির মাথায় আরেকটি। বারের মোট উচ্চতা = সব দলের মোট, তাই “মোট অর্ডার কোন দামে বেশি” পড়া সহজ; কিন্তু ওপরের দলের আকৃতি পড়া কঠিন।
  • "dodge": প্রতিটি bin-এ দলগুলো পাশাপাশি সরু বার।
  • "fill": প্রতিটি bin-এর উচ্চতা ১, ভেতরে দলের ভাগ। “এই দামে কোন পদ্ধতির ভাগ কত” প্রশ্নের জন্য।

আরেকটি জরুরি parameter stat: default "count", মানে কাঁচা গোনা। দলগুলোর আকার আলাদা হলে (COD-এর অর্ডার card-এর দ্বিগুণের বেশি) কাঁচা গোনায় আকৃতির তুলনা ঠকায়; তখন stat="density" বা "percent" আর common_norm=False দিন, যাতে প্রতিটি দল নিজের মোট দিয়ে ভাগ হয়।

KDE — প্রতিটি বিন্দুতে একটি ছোট ঢিবি

Histogram-এর ছবি bin-এর মাপ আর শুরুর বিন্দুর ওপর নির্ভর করে; একই ডেটা দুই রকম bin-এ দুই রকম দেখায়। এর একটি মসৃণ বিকল্প kernel density estimation (KDE). ধারণাটি সহজ: প্রতিটি অর্ডারের দামের জায়গায় একটি ছোট ঘণ্টার আকারের ঢিবি (Gaussian “kernel”) বসান, সবগুলোর উচ্চতা যোগ করুন, আর মোট ক্ষেত্রফল ১ রাখতে ভাগ করুন। যেখানে অনেক অর্ডার কাছাকাছি, সেখানে ঢিবিগুলো জমে উঁচু হয়। গণিতের ভাষায়, n টি বিন্দু x1 … xn থেকে কোনো দাম x-এ ঘনত্বের অনুমান:

f̂(x) = (1 / n) · ∑ Kh(x − xi)

এখানে Kh হলো h চওড়া একটি ঘণ্টা-আকারের বক্ররেখা, যার ক্ষেত্রফল ১. সবচেয়ে জরুরি সংখ্যা এই h, যার নাম bandwidth: ঢিবি কত চওড়া। নিচের ছবিতে একই চারটি দাম (৪, ৬, ৭ আর ১২ শত টাকা) থেকে দুটি bandwidth-এ KDE.

একই চারটি বিন্দু, দুটি bandwidth

ধূসর = প্রতিটি বিন্দুর ঢিবি (÷ ৪), নীল = যোগফল = KDE; দুই panel-এ একই y-স্কেল

Bandwidth বদলালে KDE কীভাবে বদলায় সরু: h = 0.7 → তিনটি আলাদা চূড়া 0481216 চওড়া: h = 2.5 → একটি নিচু ঢেউ 0481216 অর্ডারের দাম (শত টাকা); লাল = আসল অর্ডার

সরু bandwidth-এ (ওপরে) বক্ররেখা প্রায় প্রতিটি বিন্দুকে আলাদা চূড়া বানায়: ৪-এ একটি, ৬ আর ৭ মিলে একটি উঁচু, ১২-তে একটি। চারটি মাত্র অর্ডার থেকে “তিন ধরনের ক্রেতা” দেখার লোভ হয়, যা আসলে কাকতাল। চওড়া bandwidth-এ (নিচে) সব মিলে একটি নিচু ঢেউ, যার চূড়া ৬-এ আর উচ্চতা অর্ধেকেরও কম; ১২-র আলাদা অর্ডারটি হারিয়ে গেছে। আর বাঁ প্রান্তে লক্ষ করুন: শূন্যে রেখাটি মাটিতে নামেনি। এই চারটি ঢিবির মোট ক্ষেত্রফলের প্রায় ১.৬% শূন্যের নিচে পড়ে (লেখকের হিসাব), মানে KDE দাবি করছে কিছু অর্ডারের দাম ঋণাত্মক। এটিই KDE-র মূল বিপদ: মসৃণ করতে গিয়ে সে এমন জায়গাতেও ঘনত্ব বানায়, যেখানে ডেটা নেই বা থাকা অসম্ভব।

seaborn-এর kdeplot-এ h নিজে থেকে বাছা হয় Scott-এর নিয়মে (bw_method="scott", যা নমুনার ছড়ানো আর আকার থেকে একটি সূত্রে h দেয়)। আপনি সাধারণত সেটি গুণ করে বদলান bw_adjust দিয়ে: ০.৫ মানে অর্ধেক চওড়া, ২ মানে দ্বিগুণ। আর cut=3 (default) মানে রেখাটি সবচেয়ে ছোট আর বড় বিন্দুর বাইরে আরও ৩টি bandwidth পর্যন্ত আঁকা হয়। দাম, দিন বা বয়সের মতো যেসব মান শূন্যের নিচে যেতে পারে না, সেখানে cut=0 বা clip=(0, None) দিন, অথবা histogram-ই রাখুন। নিচের কোডে বাজারঘরের ৩০০টি অর্ডার: প্রথম ছবিতে পেমেন্ট অনুযায়ী stack করা histogram আর KDE, দ্বিতীয়টিতে শুধু card-এর অর্ডারে দুটি চরম bandwidth. প্রথমবার চালালে seaborn নামাতে ১০ থেকে ২০ সেকেন্ড লাগতে পারে।

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

rng = np.random.default_rng(42)
n = 300
pay = rng.choice(["COD", "bKash", "card"], n, p=[0.45, 0.35, 0.20])
boost = np.where(pay == "card", 1.6, 1.0)       # card orders bigger
paisa = (rng.lognormal(np.log(80_000), 0.5, n) * boost).round()
orders = pd.DataFrame({"payment": pay, "price_paisa": paisa.astype(np.int64)})
orders["taka"] = orders["price_paisa"] / 100
print("seaborn", sns.__version__, "| rows:", len(orders))
print(orders.groupby("payment")["taka"].agg(["count", "median"]).round(0))

sns.set_theme(style="whitegrid", palette="colorblind")
order = ["COD", "bKash", "card"]
fig, (a1, a2) = plt.subplots(1, 2, figsize=(7, 3.2), dpi=100)
sns.histplot(data=orders, x="taka", hue="payment", hue_order=order,
             multiple="stack", bins=20, ax=a1)
a1.set(title="histplot, multiple='stack'", xlabel="Order value (taka)")
sns.kdeplot(data=orders, x="taka", hue="payment", hue_order=order,
            common_norm=False, ax=a2)
a2.set(title="kdeplot by payment", xlabel="Order value (taka)")
fig.tight_layout(); plt.show()

card = orders.loc[orders["payment"] == "card", "taka"]
fig, ax = plt.subplots(figsize=(6, 3), dpi=100)
for bw, ls in [(0.25, ":"), (3, "--")]:
    sns.kdeplot(x=card, bw_adjust=bw, ls=ls, label=f"bw_adjust={bw}", ax=ax)
sns.rugplot(x=card, height=0.05, ax=ax)
ax.set(title="Card orders: two bandwidths", xlabel="Order value (taka)")
ax.legend(); fig.tight_layout(); plt.show()
print("smallest card order:", card.min(), "taka | wide KDE starts at:",
      round(ax.lines[1].get_xdata()[0]), "taka")

ব্রাউজারে (Pyodide, seaborn 0.13.2, matplotlib 3.10.8) একটি run-এর আসল আউটপুট (ছবি দুটি কোডের নিচে):

seaborn 0.13.2 | rows: 300
         count  median
payment
COD        139   841.0
bKash      104   851.0
card        57  1182.0
smallest card order: 354.7 taka | wide KDE starts at: -2460 taka

৩০০টি অর্ডারের ১৩৯টি COD, ১০৪টি bKash, ৫৭টি card; card-এর median (মাঝের মান) প্রায় ১১৮২ টাকা, বাকি দুটির ৮৫০-এর কাছাকাছি, কারণ ডেটা বানানোর সময় আমরা card-এর অর্ডার ১.৬ গুণ বড় ধরেছি। বাঁয়ের stack histogram-এ বারের মোট উচ্চতা সব অর্ডারের গোনা, আর ডান দিকে একটি লম্বা লেজ: কয়েকটি অর্ডার তিন-চার হাজার টাকার। ডানের KDE-তে common_norm=False, তাই প্রতিটি বক্ররেখার ক্ষেত্রফল আলাদাভাবে ১; card-এর বক্ররেখা নিচু আর ডানে সরানো, অথচ কাঁচা গোনায় সেটি ছোট দেখাত শুধু কম অর্ডারের কারণে।

দ্বিতীয় ছবিটি বিপদটি দেখায়। bw_adjust=0.25-এ বক্ররেখা দাঁতাল, প্রায় প্রতিটি ছোট গুচ্ছই একটি চূড়া। bw_adjust=3-এ একটি মসৃণ ঘণ্টা, কিন্তু শেষ লাইন বলছে সেটি শুরু হয়েছে −২৪৬০ টাকা থেকে, যখন সবচেয়ে ছোট card অর্ডার ৩৫৪.৭ টাকার। নিয়ম: default, অর্ধেক আর দ্বিগুণ bandwidth-এ দেখুন; যে আকৃতি তিনটিতেই টিকে থাকে, শুধু তাকেই বিশ্বাস করুন।

ECDF — কোনো মসৃণ করা নেই

তৃতীয় পথ ECDF (empirical cumulative distribution function): প্রতিটি মান x-এর জন্য “কত ভাগ অর্ডার x বা তার কম”. ecdfplot একটি সিঁড়ির মতো রেখা আঁকে, যা ০ থেকে ১-এ ওঠে, আর প্রতিটি অর্ডারে এক ধাপ। এতে কোনো bin বা bandwidth বাছতে হয় না, তাই এটি ডেটার সবচেয়ে সৎ ছবি; আর “কত ভাগ অর্ডার ৩ দিনের মধ্যে পৌঁছেছে?” ধরনের প্রশ্নের উত্তর সরাসরি y-axis থেকে পড়া যায়। দুর্বলতা হলো আকৃতি (চূড়া কোথায়) চোখে কম পড়ে।

৫ · Category ধরে তুলনা — box, violin, strip, swarm আর bar

“কোন জেলায় ডেলিভারিতে কত দিন লাগে?” এখানে x একটি category (জেলা), y একটি সংখ্যা। seaborn এর জন্য তিন ধরনের chart দেয়: প্রতিটি অর্ডার দেখানো, distribution-এর সারাংশ দেখানো, আর একটি গড় ও তার অনিশ্চয়তা দেখানো।

Functionকী দেখায়কখন ভালো, কখন সাবধান
stripplotপ্রতিটি পর্যবেক্ষণ একটি বিন্দু, একটু এলোমেলো সরানো (jitter)কয়েকশো পর্যন্ত বিন্দুতে; বেশি হলে কালো দলা
swarmplotবিন্দু না মিশিয়ে পাশে পাশে সাজানোআকৃতি স্পষ্ট, কিন্তু কয়েকশোর বেশি বিন্দু আঁটে না, তখন সতর্কবার্তা দেয়
boxplotmedian, quartile, whisker আর বাইরের বিন্দুঅনেক দল দ্রুত তুলনায়; দুই-চূড়ার distribution লুকিয়ে ফেলে
violinplotদুই পাশে আয়নার মতো KDE, ভেতরে ছোট boxআকৃতি দেখায়; কিন্তু KDE-র সব বিপদ এখানেও (cut=2 default)
barplot, pointplotপ্রতিটি দলের একটি সংখ্যা (default গড়) আর error barগড়ের তুলনায়; ছড়ানো দেখায় না, আর error bar-এর অর্থ জানা জরুরি

Box plot পড়া। John Tukey-র ১৯৭০-এর দশকের এই chart-এ বাক্সের মাঝের দাগ median; বাক্সের নিচ আর ওপর প্রথম আর তৃতীয় quartile (Q1, Q3), মানে ২৫% আর ৭৫% অবস্থান; বাক্সের উচ্চতা IQR = Q3 − Q1. Whisker যায় বাক্স থেকে ১.৫ × IQR দূরত্বের মধ্যে সবচেয়ে দূরের আসল বিন্দু পর্যন্ত (seaborn-এ whis=1.5), আর তার বাইরের বিন্দুগুলো আলাদা করে আঁকা হয়। হাতে একটি উদাহরণ: সিলেটের ছয়টি অর্ডারের ডেলিভারি ৩, ৪, ৪, ৫, ৬, ৯ দিন। median = (৪ + ৫) / ২ = ৪.৫; NumPy-র default নিয়মে Q1 = ৪, Q3 = ৫.৭৫; IQR = ১.৭৫; ওপরের সীমা ৫.৭৫ + ১.৫ × ১.৭৫ = ৮.৩৭৫. তাই ওপরের whisker থামে ৬-এ, আর ৯ আলাদা একটি বিন্দু (outlier-এর প্রার্থী)। লেখকের ল্যাপটপে matplotlib-এর boxplot_stats ঠিক এই সংখ্যাগুলোই দিয়েছে: q1 4.0, med 4.5, q3 5.75, whishi 6, fliers [9].

Box plot-এর ভালো অভ্যাস হলো তার ওপরে আসল বিন্দু বসানো: পাঁচটি সংখ্যার সারাংশ আর প্রতিটি অর্ডার, একসঙ্গে। তখন box-এর নিজের বাইরের বিন্দু বন্ধ রাখুন (fliersize=0), না হলে সেগুলো দুবার আঁকা হবে। নিচের কোডে ১২০টি অর্ডারের ডেলিভারি-দিন, পাঁচ জেলায়। দ্বিতীয় ছবিটি পরের অংশের বিষয়: একই ডেটায় দুই রকম error bar.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

rng = np.random.default_rng(7)
dist = ["Dhaka", "Chattogram", "Khulna", "Rajshahi", "Sylhet"]
lam = {"Dhaka": 1.5, "Chattogram": 2.5, "Khulna": 3, "Rajshahi": 3.5, "Sylhet": 4}
d = rng.choice(dist, 120, p=[0.35, 0.25, 0.15, 0.13, 0.12])
days = [1 + rng.poisson(lam[x]) for x in d]
df = pd.DataFrame({"district": d, "delivery_days": days})
print(df.groupby("district")["delivery_days"]
        .agg(["count", "mean", "std"]).loc[dist].round(2))

sns.set_theme(style="whitegrid", palette="colorblind")
fig, ax = plt.subplots(figsize=(6, 3.2), dpi=100)
sns.boxplot(data=df, x="district", y="delivery_days", order=dist,
            color="0.85", fliersize=0, ax=ax)
sns.stripplot(data=df, x="district", y="delivery_days", order=dist,
              jitter=0.25, size=3, alpha=0.6, ax=ax)
ax.set(title="Box + strip: every order is a dot", ylabel="Delivery days")
fig.tight_layout(); plt.show()

fig, axs = plt.subplots(1, 2, figsize=(7, 3), dpi=100, sharey=True)
for ax, eb in zip(axs, [("ci", 95), "sd"]):
    sns.barplot(data=df, x="district", y="delivery_days", order=dist,
                errorbar=eb, seed=1, ax=ax)
    ax.set(title=f"errorbar={eb}", xlabel="", ylabel="Mean delivery days")
    ax.tick_params(axis="x", rotation=45)
    lo, hi = ax.lines[0].get_ydata()      # first bar = Dhaka
    print(f"Dhaka {eb}: {lo:.2f} to {hi:.2f}")
fig.tight_layout(); plt.show()

ব্রাউজারে (Pyodide, seaborn 0.13.2) একটি run-এর আসল আউটপুট:

            count  mean   std
district
Dhaka          40  2.40  1.10
Chattogram     33  3.55  1.39
Khulna         15  4.60  2.03
Rajshahi       18  4.50  2.12
Sylhet         14  5.36  2.41
Dhaka ('ci', 95): 2.07 to 2.75
Dhaka sd: 1.30 to 3.50

প্রথম ছবিতে ঢাকার বাক্স নিচে আর সরু, সিলেটের ওপরে আর চওড়া; আর বিন্দুগুলো দেখায় প্রতিটি জেলায় আসলে কতটি অর্ডার: ঢাকায় ৪০টি, সিলেটে মাত্র ১৪টি। শুধু বাক্স দেখলে এই পার্থক্য চোখে পড়ত না।

দ্বিতীয় ছবিতে বারের উচ্চতা দুটিতেই এক (গড়, যেমন ঢাকা ২.৪০), কিন্তু দাগের দৈর্ঘ্য খুব আলাদা। ঢাকার জন্য ৯৫% CI ২.০৭ থেকে ২.৭৫, আর SD-র দাগ ১.৩০ থেকে ৩.৫০, যা ঠিক ২.৪০ ± ১.১০. প্রথমটি বলে “ঢাকার গড় ডেলিভারি-সময় আমরা কতটা নিশ্চিতভাবে জানি”, দ্বিতীয়টি বলে “ঢাকার একটি অর্ডার সাধারণত গড় থেকে কত দূরে”. পরের অংশ এই পার্থক্যের।

৬ · Error bar আসলে কী বলে — bootstrap CI, SD আর seed

বারের মাথার কালো দাগটিকে বলে error bar. seaborn 0.13-এ barplot, pointplot আর lineplot-এর default ঠিক কী, তা function-এর signature থেকেই দেখা যায়। লেখকের ল্যাপটপে সত্যিকারের run (seaborn 0.13.2, inspect.signature দিয়ে):

barplot {'estimator': 'mean', 'errorbar': ('ci', 95), 'n_boot': 1000, 'seed': None}
pointplot {'estimator': 'mean', 'errorbar': ('ci', 95), 'n_boot': 1000, 'seed': None}
lineplot {'estimator': 'mean', 'errorbar': ('ci', 95), 'n_boot': 1000, 'seed': None}

অর্থাৎ বারের উচ্চতা গড় (estimator="mean"), আর দাগটি গড়ের ৯৫% confidence interval (CI), যা হিসাব হয় ১০০০ বার bootstrap করে, আর seed=None মানে প্রতিবার আলাদা এলোমেলো সংখ্যা। seaborn-এর নথির “Statistical estimation and error bars” অধ্যায়ে চার ধরনের error bar-এর কথা আছে, যা দুই প্রশ্নে ভাগ হয়:

errorbar=প্রশ্ননমুনা বাড়লেবাজারঘরে পড়া
("ci", 95) (default)গড়টি কতটা অনিশ্চিতসরু হয় (মোটামুটি 1/√n হারে)“ঢাকার গড় ডেলিভারি সম্ভবত ২.০৭ থেকে ২.৭৫ দিনের মধ্যে”
("se", 1) বা "se"গড়ের standard errorসরু হয়গড় ± SD/√n
"sd"একটি পর্যবেক্ষণ কতটা ছড়ানোপ্রায় একই থাকে“ঢাকার একটি অর্ডার সাধারণত ১.৩ থেকে ৩.৫ দিনে”
("pi", 95)মাঝের ৯৫% পর্যবেক্ষণ কোথায়প্রায় একই থাকে২.৫ আর ৯৭.৫ percentile

পার্থক্যটি আবহাওয়ার পূর্বাভাসের মতো। “আগামী সপ্তাহের গড় তাপমাত্রা ৩১ থেকে ৩২ ডিগ্রির মধ্যে” আর “আগামী সপ্তাহের যেকোনো একদিন তাপমাত্রা ২৭ থেকে ৩৬ ডিগ্রি” দুটিই সত্য হতে পারে, কিন্তু দ্বিতীয়টি অনেক চওড়া। ক্রেতাকে “কত দিনে পাবেন” বলতে হলে লাগে SD বা percentile interval; দুই জেলার গড় আসলেই আলাদা কি না ভাবতে লাগে CI.

হাতে হিসাব: SD থেকে CI

Standard deviation (SD, প্রতীক s) মাপে প্রতিটি পর্যবেক্ষণ গড় থেকে কত দূরে; standard error SE = s / √n মাপে নমুনার গড় নিজে কতটা নড়ে। নমুনা যথেষ্ট বড় আর গড়ের distribution মোটামুটি normal হলে ৯৫% CI ≈ গড় ± ১.৯৬ × SE (কেন, তা আসবে পর্ব ৩৭-এ, central limit theorem দিয়ে)। ওপরের ঢাকার সংখ্যা দিয়ে: n = ৪০, গড় = ২.৪০, s = ১.১০; √৪০ ≈ ৬.৩২; SE = ১.১০ / ৬.৩২ ≈ ০.১৭৪; ১.৯৬ × ০.১৭৪ ≈ ০.৩৪. তাই CI ≈ ২.০৬ থেকে ২.৭৪, আর seaborn-এর bootstrap দিয়েছিল ২.০৭ থেকে ২.৭৫: প্রায় হুবহু। সিলেটে n = ১৪, s = ২.৪১: SE = ২.৪১ / ৩.৭৪ ≈ ০.৬৪, CI ≈ ৫.৩৬ ± ১.২৬. কম অর্ডার আর বেশি ছড়ানো, তাই দাগ প্রায় চার গুণ লম্বা; ছবিতে ঠিক তা-ই দেখেছেন।

Bootstrap — নিজের নমুনা থেকে আবার নমুনা

সূত্রটি normal ধরে নেয়। Bradley Efron ১৯৭৯ সালে যে bootstrap পদ্ধতি প্রকাশ করেন, তা এই অনুমান ছাড়াই কাজ করে: “নতুন ডেটা আনতে পারছি না, তাই আমার নমুনাকেই জনসংখ্যা ধরে তার থেকে বারবার নমুনা নিই”. লটারির বাক্সে ছয়টি টোকেন রেখে একটি তুলে লিখে আবার ফেরত রাখা, এভাবে ছয়বার (sampling with replacement). প্রতিবার ছয়টি সংখ্যার গড় লিখুন; হাজারবার করলে গড়গুলো যতটা ছড়ায়, সেটিই গড়ের অনিশ্চয়তার অনুমান। ধাপে ধাপে দেখুন, সিলেটের সেই ছয়টি অর্ডার দিয়ে।

নমুনা: সিলেটের ৬টি অর্ডার, ডেলিভারি-দিনলাল = এই ধাপের হিসাব

            
হিসাব
resample-এর সংখ্যা
শেষ গড়
অনুমান
  1. মূল নমুনা। ছয়টি অর্ডার, গড় ৫.১৭ দিন। প্রশ্ন: আরেক মাসের ছয়টি অর্ডার নিলে গড় কত নড়ত? নতুন মাস আনা যায় না, তাই bootstrap.
  2. প্রথম resample. ছয়টি থেকে ফেরত রেখে ছয়বার তোলা (rng.integers(0, 6, 6) দিয়ে index). ৫ দুবার এসেছে, ৬ আসেনি; গড় ৫.০০. পড়ার সুবিধায় সংখ্যাগুলো সাজিয়ে দেখানো।
  3. দ্বিতীয় resample. এবার ৩ দুবার; গড় আবার ৫.০০.
  4. তৃতীয় resample. ৫ চারবার, তাই গড় ৫.৮৩. প্রতিটি resample একটি সম্ভাব্য “অন্য মাস”.
  5. ১০০০ বার। গড়গুলো ৩.১৭ থেকে ৭.৮৩-এর মধ্যে, তাদের SD ০.৭৫. সূত্রের SE = ২.১৪ / √৬ ≈ ০.৮৭; এত ছোট নমুনায় bootstrap অনিশ্চয়তা কিছুটা কম দেখায়, এটি তার জানা দুর্বলতা।
  6. Percentile. সাজানো ১০০০টি গড়ের নিচের ২.৫% আর ওপরের ২.৫% বাদ: ২৫তম মান ৩.৮৩, ৯৭৫তম ৬.৬৭. এটিই ৯৫% CI. seaborn-এর নিজের bootstrap seed=0-এ একই সংখ্যা দিয়েছে, seed=1-এ ওপরের প্রান্ত ৬.৮৩: এলোমেলোতা বদলালে CI-ও একটু নড়ে।

শেষ ধাপের পার্থক্যই seed-এর কারণ। seed না দিলে একই notebook দুবার চালালে error bar-এর প্রান্ত একটু আলাদা আসবে; রিপোর্টে বা পরীক্ষায় সংখ্যা মিলবে না। barplot(..., seed=1) দিলে bootstrap প্রতিবার একই এলোমেলো ক্রম নেয় (পর্ব ১৬-এর random seed-এর মতোই)। আর মনে রাখুন, seaborn-এর SD হলো নমুনার SD, ddof=1 দিয়ে: লেখকের ল্যাপটপে ওই ছয়টি সংখ্যায় errorbar="sd" দিয়েছে ৩.০৩ থেকে ৭.৩০, মানে ৫.১৭ ± ২.১৪.

দুটি CI একে অপরকে ছুঁলেই “পার্থক্য নেই” নয়, আর না ছুঁলেই “নিশ্চিত পার্থক্য” নয়। চোখে CI-র ওভারল্যাপ দেখে দুই দলের তুলনা করা একটি প্রচলিত ভুল; দুটি গড়ের পার্থক্যের নিজস্ব CI আলাদা করে হিসাব করতে হয় (পর্ব ৩৭). আর error bar-ওয়ালা যেকোনো chart-এর caption-এ লিখুন দাগটি কী: “৯৫% CI (bootstrap)”, “±১ SD”, নাকি “±১ SE”.

pointplot একই হিসাব দেখায় বারের বদলে একটি বিন্দু আর দাগ দিয়ে, আর দলগুলোর বিন্দু রেখায় জোড়ে। বারের উচ্চতা শূন্য থেকে শুরু হয়, তাই গড় যখন শূন্য থেকে অনেক দূরে বা দলগুলোর ক্রম আছে (যেমন সপ্তাহের দিন), তখন pointplot প্রায়ই পরিষ্কার।

৭ · সম্পর্ক — scatterplot, regplot/lmplot, pairplot আর jointplot

দুটি সংখ্যার সম্পর্ক দেখার মূল chart scatterplot: প্রতিটি সারি একটি বিন্দু, আর অংশ ২-এর মতো hue, size, style দিয়ে আরও দুই-তিনটি variable যোগ করা যায়। তবে সাবধান: চারটির বেশি variable এক ছবিতে দিলে পাঠক কিছুই পড়তে পারেন না। তখন relplot(col=...) দিয়ে panel-এ ভাগ করুন।

regplot আর lmplot. regplot scatter-এর ওপর একটি সরল রেখা বসায়, যা least squares দিয়ে fit করা (রেখা থেকে বিন্দুগুলোর উল্লম্ব দূরত্বের বর্গের যোগফল সবচেয়ে কম)। রেখার চারপাশের হালকা ছায়া রেখার ৯৫% CI, যা আবার bootstrap দিয়ে হিসাব (ci=95, n_boot=1000; এখানে parameter-এর নাম এখনো ci, errorbar নয়)। ছায়া প্রান্তে চওড়া হয়, কারণ সেখানে বিন্দু কম আর রেখার ঢাল একটু বদলালেই প্রান্তে বড় পার্থক্য হয়। lmplot একই জিনিসের figure-level রূপ, যেখানে hue আর col দিয়ে প্রতিটি দলের আলাদা রেখা। order=2 দিলে বাঁকা (দ্বিঘাত) রেখা, logistic=True দিলে ০/১ ফলের জন্য S-আকার। একটি জরুরি কথা: এটি একটি ছবি, মডেল নয়। seaborn রেখার ঢাল বা intercept ফেরত দেয় না, আর এর CI নিয়ে কোনো পরিসংখ্যানিক পরীক্ষা করে না। Linear regression-এর loss, gradient descent আর closed form আসবে পর্ব ৫৮-এ; সেখানে সংখ্যাগুলো নিজে হিসাব করবেন।

pairplot. অনেকগুলো সংখ্যার কলাম থাকলে প্রতিটি জোড়ার scatter একটি ছকে: সারি i, কলাম j-এ variable i বনাম j, আর কর্ণে (diagonal) প্রতিটির নিজের distribution. নিচের কোড scikit-learn-এর সঙ্গে আসা Iris ডেটাসেট ব্যবহার করে (load_iris; Ronald Fisher ১৯৩৬ সালের প্রবন্ধে এটি জনপ্রিয় করেন, মাপগুলো নিয়েছিলেন উদ্ভিদবিদ Edgar Anderson): তিন প্রজাতির ১৫০টি ফুলের পাপড়ি (petal) আর বৃতি (sepal)-র মাপ, সেন্টিমিটারে। দ্রুত রাখতে তিনটি কলাম আর corner=True (ছকের ওপরের অর্ধেক বাদ, কারণ সেটি নিচের আয়না)। প্রথমবার scikit-learn নামাতে আরও প্রায় ১০ সেকেন্ড লাগতে পারে।

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris

iris = load_iris(as_frame=True)          # bundled with scikit-learn
df = iris.frame.rename(columns=lambda c: c.replace(" (cm)", ""))
df["species"] = iris.target_names[iris.target]
cols = ["petal length", "petal width", "sepal length"]
print(df.shape, "| per species:", df["species"].value_counts().tolist())
print(df.groupby("species")[cols].mean().round(2))

sns.set_theme(style="ticks", palette="colorblind")
g = sns.pairplot(df, vars=cols, hue="species", corner=True,
                 height=1.7, plot_kws={"s": 12})
print(type(g).__name__, "| axes grid:", g.axes.shape)
g.figure.set_dpi(100)
plt.show()
print(df[cols].corr().round(2))

ব্রাউজারে (Pyodide, seaborn 0.13.2, scikit-learn 1.8.0) একটি run-এর আসল আউটপুট:

(150, 6) | per species: [50, 50, 50]
            petal length  petal width  sepal length
species
setosa              1.46         0.25          5.01
versicolor          4.26         1.33          5.94
virginica           5.55         2.03          6.59
PairGrid | axes grid: (3, 3)
              petal length  petal width  sepal length
petal length          1.00         0.96          0.87
petal width           0.96         1.00          0.82
sepal length          0.87         0.82          1.00

প্রতিটি প্রজাতির ৫০টি ফুল। ছবিতে setosa (নীল) বাকি দুটি থেকে পুরো আলাদা: পাপড়ির দৈর্ঘ্য গড়ে ১.৪৬ সেমি, অন্যদের ৪.২৬ আর ৫.৫৫. versicolor আর virginica কিছুটা মিশে আছে। pairplot ফেরত দিয়েছে একটি PairGrid, ৩ × ৩ Axes-এর ছক; corner=True থাকায় ওপরের তিনটি ঘর খালি। hue দেওয়ায় কর্ণে নিজে থেকেই KDE (hue না দিলে histogram). শেষ টেবিলে petal length আর petal width-এর correlation ০.৯৬: প্রায় একটি সরল রেখা, ছবির মাঝের বাঁ panel-এ যা দেখছেন। তবে লক্ষ করুন, এই ০.৯৬ অনেকটাই তিন প্রজাতির আলাদা গুচ্ছ থেকে আসে; একটি প্রজাতির ভেতরে সম্পর্ক অনেক দুর্বল হতে পারে।

jointplot. দুটি variable-এর জন্য একটি বড় scatter, আর তার ওপরে ও ডানে প্রতিটির নিজের distribution (marginal). kind দিয়ে মাঝের ছবি বদলানো যায়: "scatter" (default), "kde", "hist", "hex" (হাজারো বিন্দুতে ষড়ভুজ ঘরে গোনা, যখন scatter কালো দলা হয়ে যায়) বা "reg". ফেরত আসে একটি JointGrid; মাঝের Axes g.ax_joint, পাশের দুটি g.ax_marg_x আর g.ax_marg_y.

৮ · Matrix — correlation heatmap, আর correlation মানে কারণ নয়

অনেক কলামের জোড়ায় জোড়ায় সম্পর্ক এক নজরে দেখতে correlation matrix-এর heatmap. Pearson correlation r হলো −১ থেকে +১-এর একটি সংখ্যা: +১ মানে দুটি variable একটি ঊর্ধ্বমুখী সরল রেখায়, −১ মানে নিম্নমুখী রেখায়, ০ মানে কোনো সরল রৈখিক সম্পর্ক নেই (বাঁকা সম্পর্ক থাকতেই পারে)। সূত্র আর তার ব্যাখ্যা আসবে পর্ব ৩৭-এ; এখানে ছবিটি ঠিকভাবে আঁকার তিনটি নিয়ম:

  • vmin=-1, vmax=1, center=0: রঙের স্কেল পুরো পরিসর জুড়ে, আর ০ ঠিক মাঝের নিরপেক্ষ রঙে। না দিলে seaborn ডেটার সবচেয়ে কম আর বেশি মান দিয়ে স্কেল বানায়, আর ০.০৩-এর মতো দুর্বল সম্পর্কও গাঢ় দেখাতে পারে।
  • Diverging palette: দুই দিকে দুটি আলাদা রং, মাঝে হালকা; যেমন "vlag" (নীল থেকে লাল) বা "coolwarm". লাল-সবুজ জোড়া এড়িয়ে চলুন, কারণ লাল-সবুজ বর্ণান্ধতা পুরুষদের মধ্যে তুলনামূলক সাধারণ। একমুখী মানের (গোনা, দাম) জন্য sequential colormap, যেমন "viridis".
  • annot=True, fmt=".2f": প্রতিটি ঘরে সংখ্যা লেখা, কারণ রঙের সূক্ষ্ম পার্থক্য চোখ ঠিকভাবে পড়ে না। ম্যাট্রিক্সটি প্রতিসম, তাই চাইলে mask=np.triu(np.ones_like(corr, dtype=bool)) দিয়ে ওপরের অর্ধেক ঢাকা যায়।

নিচের কোডে বাজারঘরের ৪০০টি অর্ডার, যেখানে আমরা নিজেরাই একটি লুকানো কারণ রেখেছি: ঈদের আগের সপ্তাহের অর্ডার (eid = 1). সেই সপ্তাহে মানুষ বেশি কেনেন, আর কুরিয়ারের চাপে ডেলিভারি দেরি হয়। পরিমাণ (qty) আর ডেলিভারি-দিনের মধ্যে আমরা কোনো সরাসরি যোগ রাখিনি।

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

rng = np.random.default_rng(42)
n = 400
eid = rng.random(n) < 0.25                  # order placed in Eid-rush week
qty = rng.integers(1, 6, n) + 2 * eid       # people buy more before Eid
unit = rng.lognormal(np.log(30_000), 0.4, n).round()      # paisa
days = 1 + rng.poisson(2.5 + 2 * eid)       # couriers are overloaded
rating = np.clip(np.round(5.2 - 0.35 * days + rng.normal(0, 0.8, n)), 1, 5)
df = pd.DataFrame({"qty": qty, "taka": qty * unit / 100, "days": days,
                   "rating": rating, "discount": rng.integers(0, 100, n),
                   "eid": eid.astype(int)})

corr = df.corr(method="pearson")
print(corr.round(2))
for e, g in df.groupby("eid"):
    print("eid =", e, "| n =", len(g), "| r(qty, days) =",
          round(g["qty"].corr(g["days"]), 2))

fig, ax = plt.subplots(figsize=(5.5, 4.4), dpi=100)
sns.heatmap(corr, annot=True, fmt=".2f", vmin=-1, vmax=1, center=0,
            cmap="vlag", square=True, linewidths=0.5,
            cbar_kws={"shrink": 0.8}, ax=ax)
ax.set_title("Pearson r, synthetic orders")
fig.tight_layout(); plt.show()

ব্রাউজারে (Pyodide, seaborn 0.13.2) একটি run-এর আসল আউটপুট:

           qty  taka  days  rating  discount   eid
qty       1.00  0.73  0.20   -0.10     -0.06  0.54
taka      0.73  1.00  0.22   -0.12     -0.07  0.42
days      0.20  0.22  1.00   -0.60     -0.07  0.49
rating   -0.10 -0.12 -0.60    1.00     -0.02 -0.30
discount -0.06 -0.07 -0.07   -0.02      1.00 -0.05
eid       0.54  0.42  0.49   -0.30     -0.05  1.00
eid = 0 | n = 297 | r(qty, days) = -0.07
eid = 1 | n = 103 | r(qty, days) = -0.12

কিছু সম্পর্ক আমরা ইচ্ছা করে বানিয়েছি, আর heatmap সেগুলো ধরেছে: qty আর taka ০.৭৩ (বেশি পণ্য, বেশি দাম), days আর rating −০.৬০ (দেরি হলে রেটিং কমে, যা আমরা সূত্রে রেখেছি)। কিন্তু qty আর days-এর মধ্যেও ০.২০, অথচ আমরা এদের মধ্যে কোনো যোগ রাখিনি। শেষ দুই লাইন রহস্যটি খোলে: ঈদের সপ্তাহের ভেতরে r = −০.১২, বাকি সময়ে −০.০৭, দুটিই প্রায় শূন্য। সম্পর্কটি এসেছে পুরোপুরি eid থেকে, যা দুটিকেই একসঙ্গে বাড়ায়। একইভাবে qty আর rating-এর −০.১০: বেশি কিনলে ক্রেতা অখুশি হন না; ঈদের ভিড়ে দেরি হয় বলে রেটিং কমে। discount-এর সারির সব মান প্রায় ০, কারণ সেটি সম্পূর্ণ এলোমেলো; −০.০৭-এর মতো ছোট মান ৪০০টি নমুনায় কাকতালীয়ভাবেই আসে।

এটি পর্ব ২৪-এর Simpson's paradox-এর আত্মীয়। সেখানে দুই কুরিয়ার “দ্রুতি” আর “পথিক”-এর সব মিলিয়ে হার উল্টো কথা বলছিল, কারণ জেলা ঠিক করছিল কে কোন অর্ডার পাবে আর অর্ডারটি কতটা কঠিন। এখানে ঈদ একটি confounding variable, আর qty-days-এর ০.২০ একটি spurious correlation. বিশ্লেষক যদি heatmap দেখে লেখেন “বড় অর্ডার প্যাক করতে দেরি হয়, তাই প্যাকিং দল বাড়ান”, সিদ্ধান্তটি ভুল কারণের ওপর দাঁড়াবে। Heatmap প্রশ্ন খোঁজার যন্ত্র, উত্তর নয়: প্রতিটি উল্লেখযোগ্য ঘরের জন্য scatter দেখুন, সম্ভাব্য তৃতীয় কারণ ধরে দলে ভাগ করে আবার দেখুন (hue="eid" বা col="eid"), আর কারণ দাবি করার আগে পরীক্ষা বা A/B test ভাবুন।

৯ · চেহারা আর নিয়ন্ত্রণ — theme, context, palette, Axes আর seaborn.objects

Theme আর context. sns.set_theme() একবার ডাকলে পরের সব chart-এ seaborn-এর চেহারা। এর ভেতরে তিনটি আলাদা জিনিস: style (পেছনের রং আর grid: "darkgrid" default, "whitegrid", "white", "ticks"), palette (রঙের তালিকা, default "deep") আর context (লেখা আর রেখার আকার)। Context ঠিক করে ছবিটি কোথায় দেখা হবে: কাগজে ছোট, notebook-এ মাঝারি, প্রজেক্টরে বড়। লেখকের ল্যাপটপে সত্যিকারের run (sns.plotting_context(name); font.size, axes.labelsize আর lines.linewidth):

paper 9.600000000000001 9.600000000000001 1.2000000000000002
notebook 12 12 1.5
talk 18.0 18.0 2.25
poster 24 24 3.0

অর্থাৎ "notebook"-কে ১ ধরে paper ০.৮ গুণ, talk ১.৫ গুণ, poster ২ গুণ। স্লাইডের আগে sns.set_context("talk") দিন। একটি chart-এর জন্য সাময়িকভাবে বদলাতে with sns.plotting_context("talk"): ব্লক ব্যবহার করুন।

Palette. sns.color_palette(name, n) রঙের একটি তালিকা দেয়। তিন ধরনের palette, তিন ধরনের ডেটার জন্য: qualitative (আলাদা category, যেমন "colorblind", "deep", "tab10"), sequential (একমুখী সংখ্যা, যেমন "viridis", "rocket", "mako") আর diverging (মাঝে একটি অর্থপূর্ণ শূন্য, যেমন "vlag"). এই পর্বের সব কোডে colour-blind-safe palette "colorblind"; লেখকের যন্ত্রে এর প্রথম চারটি রং #0173b2 (নীল), #de8f05 (কমলা), #029e73 (সবুজ), #d55e00 (লাল-কমলা). তবু শুধু রঙের ওপর নির্ভর করবেন না: দল তিনটির বেশি হলে style, সরাসরি লেবেল বা আলাদা panel দিন। সৎ আর সবার পড়ার মতো chart-এর পুরো আলোচনা আসবে পর্ব ৩২-এ।

ফেরত পাওয়া Axes দিয়ে সাজানো। seaborn যেহেতু matplotlib-এর ওপরে, তাই যা seaborn-এর parameter-এ নেই, তা matplotlib দিয়ে করুন। Axes-level function-এর ফল একটি সাধারণ Axes: ax.set(xlabel=..., ylim=(0, None)), ax.axhline(3, ls="--"), ax.tick_params(axis="x", rotation=45), ax.figure.savefig("chart.png", dpi=200, bbox_inches="tight"). Legend সরাতে seaborn-এর নিজস্ব sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1)). Figure-level-এ g.set(...) সব panel-এ একসঙ্গে, আর for ax in g.axes.flat: দিয়ে আলাদা আলাদা।

seaborn.objects — নতুন, ঘোষণামূলক interface

seaborn 0.12 (২০২২ সাল) থেকে একটি দ্বিতীয় interface আছে: seaborn.objects. এটি “grammar of graphics” ধারণার দিকে যায় (Leland Wilkinson-এর বই, আর R-এর ggplot2 যে ধারণায় গড়া): একটি so.Plot-এ ডেটা আর mapping দেন, তারপর .add() দিয়ে স্তর যোগ করেন, যেখানে প্রতিটি স্তর একটি mark (কী আঁকা হবে: so.Bar, so.Dot, so.Line, so.Range) আর একটি stat (কী হিসাব হবে: so.Agg, so.Est, so.Hist). নথিতে একে এখনো পরীক্ষামূলক বলা হয়, আর API বদলাতে পারে। অংশ ৫-এর একই ১২০টি অর্ডারে গড় আর ±১ SD, লেখকের ল্যাপটপে সত্যিকারের run (seaborn 0.13.2):

~/bazarghar-ai$ cat objects_demo.py     # (মূল অংশ)
p = (
    so.Plot(df, x="district", y="delivery_days")
    .add(so.Bar(), so.Agg("mean"))
    .add(so.Range(), so.Est(errorbar="sd"))
    .scale(x=so.Nominal(order=dist))
    .label(title="Mean delivery days, ±1 SD", y="Days")
)
print(type(p).__name__)
p.save("objects_bar.png", dpi=100)
fig = plt.figure(figsize=(6, 3.2))
p.on(fig).plot()             # draw onto our own matplotlib Figure
ax = fig.axes[0]
heights = [round(float(b.get_height()), 2) for b in ax.patches]
print("bars:", len(ax.patches), heights)
print("title:", ax.get_title())

~/bazarghar-ai$ python objects_demo.py
seaborn 0.13.2
Plot
bars: 5 [2.4, 3.55, 4.6, 4.5, 5.36]
title: Mean delivery days, ±1 SD

বারের উচ্চতা ২.৪, ৩.৫৫, ৪.৬, ৪.৫, ৫.৩৬: অংশ ৫-এর টেবিলের গড়ের সঙ্গে হুবহু মেলে, কারণ ডেটা একই seed-এ বানানো। so.Plot(...) নিজে কিছু আঁকে না, শুধু একটি বর্ণনা (Plot object); আঁকা হয় .save(), .plot() বা notebook-এ দেখানোর সময়। .on(fig) দিয়ে নিজের matplotlib Figure বা Axes-এ আঁকানো যায়, তাই পুরোনো আর নতুন interface একসঙ্গে চলে। এই কোর্সে মূল হিসেবে function-ভিত্তিক interface-ই থাকবে।

আরও গভীরে। KDE-র ধারণা Murray Rosenblatt (১৯৫৬) আর Emanuel Parzen (১৯৬২)-এর প্রবন্ধ থেকে, তাই একে কখনো Parzen–Rosenblatt window বলা হয়; bandwidth বাছার Scott-এর নিয়ম David Scott-এর, আর Silverman-এর নিয়ম Bernard Silverman-এর ১৯৮৬ সালের বই থেকে। seaborn-এর KDE ভেতরে একটি Gaussian kernel আর একটি নির্দিষ্ট grid-এ মূল্যায়ন ব্যবহার করে (gridsize=200 default), আর bootstrap চলে তার নিজস্ব algorithms.bootstrap function-এ, NumPy-র seeded random generator দিয়ে। Bootstrap-এর মূল প্রবন্ধ Efron-এর ১৯৭৯ সালের “Bootstrap methods: another look at the jackknife” (Annals of Statistics); percentile পদ্ধতির চেয়ে নির্ভুল BCa-র মতো উন্নত রূপ আছে, যা seaborn ব্যবহার করে না।

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):

  • seaborn আর matplotlib-এর সম্পর্ক? seaborn matplotlib-এর ওপরে লেখা; DataFrame, semantic mapping আর পরিসংখ্যান যোগ করে, আঁকে matplotlib.
  • histplot আর displot-এর পার্থক্য? প্রথমটি axes-level (Axes ফেরত, ax= মানে); দ্বিতীয়টি figure-level (FacetGrid ফেরত, col/row, ax= উপেক্ষা)।
  • seaborn-এর barplot-এর কালো দাগ কী? 0.13-এ default গড়ের ৯৫% CI, ১০০০টি bootstrap resample থেকে; SD চাইলে errorbar="sd".
  • KDE-র bandwidth বাড়ালে কী হয়? বক্ররেখা মসৃণ হয়, খুঁটিনাটি হারায়, আর ডেটার পরিসরের বাইরে (যেমন ঋণাত্মক দামে) ঘনত্ব দেখাতে পারে; কমালে প্রতিটি বিন্দু চূড়া।
  • correlation heatmap-এ center=0 কেন? diverging palette-এর নিরপেক্ষ মাঝের রং যেন ঠিক ০-তে বসে, তাই ধনাত্মক আর ঋণাত্মক সম্পর্ক ন্যায্যভাবে আলাদা দেখায়।

১০ · AI-এর যুগে — AI সহকারীর seaborn কোড আর chart যাচাই

“এই CSV থেকে জেলাভিত্তিক সুন্দর chart বানাও” লিখলে AI সহকারী কয়েক সেকেন্ডে seaborn কোড দেবে, আর chart দেখতে ভালোই হবে। সমস্যা হলো, chart-এর ভুল error দেয় না; শুধু ভুল গল্প বলে। seaborn-এর বিষয়ে AI সহকারীরা প্রায়ই যা করে:

  • পুরোনো API লেখে: distplot, barplot(ci="sd"), hue ছাড়া palette, বা sns.load_dataset (যা ইন্টারনেট থেকে নামায়)। লেখকের ল্যাপটপে seaborn 0.13.2-এর সত্যিকারের সতর্কবার্তা নিচে।
  • বানানো parameter লেখে: যেমন histplot(..., smooth=True) বা figure-level function-এ ax=, যা চুপচাপ উপেক্ষিত হয়।
  • error bar-এর অর্থ ভুল বলে: default CI-কে “SD” বা “পরিসর” লেখে, অথবা caption-এ কিছুই লেখে না; আর CI-র ওভারল্যাপ দেখে “পার্থক্য পরিসংখ্যানগতভাবে উল্লেখযোগ্য নয়” ঘোষণা করে।
  • correlation থেকে কারণ বানায়: heatmap দেখে “দেরিতে ডেলিভারির কারণ বড় অর্ডার” লেখে, confounder না খুঁজে।
  • সতর্কবার্তা লুকায়: warnings.filterwarnings("ignore") দিয়ে deprecation আর “points cannot be placed” (swarmplot) বার্তা চাপা দেয়; আর ক্রেতার আসল ডেটা দিয়ে উদাহরণ চায়।
~/bazarghar-ai$ python old_api.py
barplot(ci='sd'): FutureWarning: The `ci` parameter is deprecated. Use `errorbar='sd'` for the same effect.
distplot: UserWarning: `distplot` is a deprecated function and will be removed in seaborn v0.14.0. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-lev
boxplot(palette=...) no hue: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.
boxplot(palette=...) no hue: MatplotlibDeprecationWarning: vert: bool was deprecated in Matplotlib 3.11 and will be removed in 3.13. Use orientation: {'vertical', 'horizontal'} instead.
boxplot(palette=...) no hue: MatplotlibDeprecationWarning: vert: bool was deprecated in Matplotlib 3.11 and will be removed in 3.13. Use orientation: {'vertical', 'horizontal'} instead.

প্রথম তিনটি বার্তা পুরোনো কোডের দোষ, আর প্রতিটিই ঠিক পথ বলে দিচ্ছে। শেষ দুটি আলাদা ধরনের: কোড ঠিক, কিন্তু seaborn 0.13.2 নিজে ভেতরে matplotlib-এর একটি parameter ব্যবহার করে, যা matplotlib 3.11-এ পুরোনো ঘোষিত। Pyodide-এর matplotlib 3.10.8-এ এই বার্তা আসে না। শিক্ষা: সতর্কবার্তা পড়ুন, কার দোষ বুঝুন, আর library-গুলোর version একসঙ্গে lockfile-এ রাখুন (পর্ব ১২)।

প্রম্পটে version, ডেটার গঠন, error bar-এর অর্থ আর যাচাইয়ের শর্ত নিজেই দিন। আসল গ্রাহকের ডেটা নয়, বানানো নমুনা দিন।

আমি Python 3.14, pandas 3.0 আর seaborn 0.13.2 ব্যবহার করছি। একটি long DataFrame আছে: orders(order_id, district, payment, delivery_days, rating), প্রতিটি সারি একটি অর্ডার। কাজ: জেলা অনুযায়ী গড় ডেলিভারি-দিনের barplot, পেমেন্ট অনুযায়ী hue, আর পাশে box + strip plot. শর্ত: ১. শুধু seaborn 0.13-এর বর্তমান API; distplot, ci= বা hue ছাড়া palette ব্যবহার করবে না। ২. barplot-এ errorbar আর seed স্পষ্ট লেখো, আর caption-এ লেখো দাগটি CI না SD. ৩. প্রতিটি জেলায় অর্ডারের সংখ্যা আর গড় আলাদা করে print করো, যাতে chart-এর সঙ্গে মেলানো যায়। ৪. colorblind palette, আর figure-এর সব লেখা ইংরেজিতে। ৫. কোনো warning চাপা দেবে না; কোনো parameter নিয়ে নিশ্চিত না হলে বলো। ৬. chart থেকে কোনো কারণ-সম্পর্কের দাবি করবে না।

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

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

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

প্র ০১গড়, SD আর correlation-এর মতো সারাংশ সংখ্যা থাকলে chart আঁকার দরকার কী? সংখ্যাই কি যথেষ্ট নয়?

এই প্রশ্নের সবচেয়ে বিখ্যাত উত্তর দেন পরিসংখ্যানবিদ Francis Anscombe, ১৯৭৩ সালে The American Statistician-এ “Graphs in Statistical Analysis” প্রবন্ধে। তিনি চারটি ছোট ডেটাসেট বানান, প্রতিটিতে ১১টি (x, y) জোড়া, যাদের x-এর গড়, y-এর গড়, variance, correlation (প্রায় ০.৮২) আর regression line প্রায় হুবহু এক। কিন্তু ছবিতে চারটি সম্পূর্ণ আলাদা: একটি সাধারণ ছড়ানো সরল সম্পর্ক, একটি নিখুঁত বাঁকা রেখা, একটি নিখুঁত সরল রেখা যাকে একটি মাত্র বিন্দু টেনে বাঁকিয়েছে, আর একটি যেখানে সব x এক জায়গায়, শুধু একটি বিন্দু দূরে, আর সেটিই পুরো correlation বানাচ্ছে। এটি “Anscombe's quartet” নামে পরিচিত।

২০১৭ সালে Justin Matejka আর George Fitzmaurice ACM CHI সম্মেলনে “Same Stats, Different Graphs” প্রবন্ধে ধারণাটিকে চরমে নেন: একটি অপ্টিমাইজেশন পদ্ধতি দিয়ে বিন্দুগুলোকে একটু একটু সরিয়ে তাঁরা একই গড়, SD আর correlation (দুই দশমিক পর্যন্ত) রেখে ডাইনোসর, তারা, বৃত্ত আর সমান্তরাল রেখার মতো এক ডজন আকৃতি বানান: “Datasaurus Dozen”. শিক্ষা একই: সারাংশ সংখ্যা ডেটাকে কয়েকটি সংখ্যায় চাপে, আর চাপার সময় আকৃতির তথ্য হারায়।

বাজারঘরে এর মানে কী? দুই জেলার গড় ডেলিভারি-সময় সমান হতে পারে, অথচ একটিতে সব অর্ডার ৩ দিনে আর অন্যটিতে অর্ধেক ১ দিনে, অর্ধেক ৫ দিনে (হয়তো দুটি আলাদা কুরিয়ার)। গড় বলবে “সব ঠিক”, strip plot বা ECDF বলবে “দুটি আলাদা গল্প”. এ জন্যই এই পর্বে box plot-এর ওপর বিন্দু বসানো হয়েছে। আবার উল্টো দিকও আছে: ছবি দেখে চোখ এমন নকশাও দেখে, যা কাকতাল (অংশ ৪-এর সরু KDE). তাই ভালো অভ্যাস হলো দুটি একসঙ্গে: ছবি দেখে প্রশ্ন, সংখ্যা দিয়ে যাচাই, আর মডেল বানানোর আগে EDA-তে (পর্ব ৬২) দুটিই।

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

প্র ০২বারের ওপরের error bar নিয়ে এত বিভ্রান্তি কেন? গবেষকেরাও কি CI আর SD গুলিয়ে ফেলেন?

হ্যাঁ, আর এটি মাপাও হয়েছে। ২০০৫ সালে Sarah Belia, Fiona Fidler, Jennifer Williams আর Geoff Cumming Psychological Methods জার্নালে “Researchers misunderstand confidence intervals and standard error bars” প্রবন্ধ প্রকাশ করেন। তাঁরা মনোবিজ্ঞান, স্নায়ুবিজ্ঞান আর চিকিৎসাবিজ্ঞানের জার্নালের লেখকদের একটি অনলাইন পরীক্ষায় দুটি দলের গড় আর error bar দেখিয়ে বলেন, একটি দলকে এমনভাবে সরান যাতে পার্থক্যটি ঠিক “পরিসংখ্যানগতভাবে উল্লেখযোগ্য” হয়। অনেক উত্তরদাতা CI আর SE bar-কে একইভাবে ব্যবহার করেন, আর অনেকে ধরে নেন দুটি ৯৫% CI ঠিক ছুঁয়ে গেলেই সীমানা, যা ভুল। বিস্তারিত ফলাফল প্রবন্ধে আছে; মোটা কথা, অভিজ্ঞ গবেষকদের বড় অংশও error bar ভুল পড়েন। Cumming আর Finch একই বছর “Inference by eye” প্রবন্ধে কিছু মোটামুটি নিয়ম দেন, যেমন স্বাধীন দুটি দলের ৯৫% CI মোটামুটি অর্ধেক বাহুর মতো ওভারল্যাপ করলেও পার্থক্য প্রায় উল্লেখযোগ্য হতে পারে।

বিভ্রান্তির একটি কারণ chart নিজে: তিন ধরনের দাগ (SD, SE, CI) দেখতে হুবহু এক, অথচ অর্থ সম্পূর্ণ আলাদা, আর অনেক chart-এ লেখাই থাকে না কোনটি। আগে barplot-এ parameter-এর নাম ছিল ci, যেখানে ci="sd" লিখতে হতো, মানে নামটিই বলছিল “confidence interval” অথচ আঁকছিল SD. seaborn 0.12-এ নতুন errorbar parameter আসে, আর নথিতে একটি পুরো অধ্যায়ে ব্যাখ্যা করা হয় কোন দাগ কোন প্রশ্নের উত্তর: ছড়ানো (SD, PI) বনাম অনুমানের অনিশ্চয়তা (SE, CI).

ব্যবহারিক পরামর্শ তিনটি। প্রথমত, প্রতিটি chart-এ লিখুন দাগটি কী আর n কত। দ্বিতীয়ত, শ্রোতা ঠিক করুন: ক্রেতা বা ডেলিভারি পরিকল্পনার জন্য ছড়ানো (SD বা percentile), গড়ের তুলনার জন্য CI. তৃতীয়ত, ছোট নমুনায় গড়ের বার না দেখিয়ে আসল বিন্দু দেখান; ছয়টি অর্ডারের বার আর দাগের চেয়ে ছয়টি বিন্দু বেশি সৎ।

মূল উপলব্ধি: একই দেখতে দাগ তিনটি আলাদা প্রশ্নের উত্তর দেয়; দাগটি কী, তা না লিখলে পাঠক প্রায় নিশ্চিতভাবেই ভুল পড়বেন।

প্র ০৩Bootstrap এত সহজ হলে সবসময় কেন এটিই ব্যবহার হয় না? কখন এটি ভুল উত্তর দেয়?

Bradley Efron ১৯৭৯ সালে Annals of Statistics-এ bootstrap প্রকাশ করেন, আর নামটি এসেছে ইংরেজি প্রবাদ “pulling yourself up by your bootstraps” থেকে: নিজের জুতার ফিতা ধরে নিজেকে টেনে তোলা, মানে বাইরের সাহায্য ছাড়া। পদ্ধতিটি বিপ্লবী ছিল, কারণ জটিল অনুমানের (median, অনুপাত, correlation) standard error-এর জন্য আর কঠিন সূত্র লাগল না; কম্পিউটারের বারবার হিসাবই যথেষ্ট।

কিন্তু bootstrap-এর মূল অনুমান হলো আপনার নমুনা জনসংখ্যার একটি ভালো ছোট প্রতিচ্ছবি, আর পর্যবেক্ষণগুলো একে অপরের থেকে স্বাধীন। এই অনুমান ভাঙলে উত্তর ভুল। ছোট নমুনা: ছয়টি অর্ডারে নমুনায় যা নেই (যেমন ১২ দিনের একটি বিরল দেরি), resample-এও কখনো আসবে না; তাই CI সাধারণত খুব সরু হয়। এই পর্বের stepper-এ bootstrap SE ০.৭৫, সূত্রের ০.৮৭: ঠিক সেই প্রবণতা। ভারী লেজ: কয়েকটি বিশাল অর্ডার থাকলে গড়ের bootstrap distribution অস্থির, আর CI প্রতি seed-এ উল্লেখযোগ্যভাবে বদলায়। নির্ভরশীল ডেটা: time series-এ (পর্ব ২৬) আজকের বিক্রি গতকালের সঙ্গে সম্পর্কিত; সারিগুলো এলোমেলো করে resample করলে সেই সম্পর্ক ভাঙে আর অনিশ্চয়তা কম দেখায়। এর জন্য block bootstrap-এর মতো রূপ আছে। একইভাবে একই ক্রেতার দশটি অর্ডার স্বাধীন নয়; তখন অর্ডার নয়, ক্রেতা ধরে resample করতে হয়।

তাই seaborn-এর default CI একটি দ্রুত, যুক্তিসংগত প্রথম অনুমান, কোনো চূড়ান্ত পরিসংখ্যানিক সিদ্ধান্ত নয়। গুরুত্বপূর্ণ সিদ্ধান্তে ডেটার গঠন (কে কার ওপর নির্ভর করে) ভাবুন, প্রয়োজনে scipy.stats-এর bootstrap function-এর মতো যন্ত্রে BCa পদ্ধতি নিন, আর পর্ব ৩৫ থেকে ৩৭-এর সম্ভাবনা ও পরিসংখ্যান দিয়ে যাচাই করুন।

মূল উপলব্ধি: Bootstrap সূত্রের বদলে হিসাব দেয়, কিন্তু নমুনা প্রতিনিধিত্বমূলক আর পর্যবেক্ষণ স্বাধীন হওয়ার অনুমান সরায় না; ছোট, ভারী-লেজের বা নির্ভরশীল ডেটায় এর CI অতি-আত্মবিশ্বাসী হয়।

প্র ০৪Histogram, KDE আর ECDF — তিনটির মধ্যে কোনটি “সবচেয়ে সৎ”? Bandwidth বাছার কি কোনো সঠিক নিয়ম আছে?

তিনটিই একই প্রশ্নের তিনটি উত্তর: “ডেটা কীভাবে ছড়ানো?” আর প্রতিটির একটি লুকানো সিদ্ধান্ত আছে। Histogram-এ সিদ্ধান্ত bin-এর মাপ আর শুরুর বিন্দু। KDE-তে kernel-এর আকৃতি আর bandwidth. ECDF-এ কোনো সিদ্ধান্ত নেই: প্রতিটি পর্যবেক্ষণ ঠিক এক ধাপ, তাই একই ডেটা থেকে সবাই একই ECDF পান। এই অর্থে ECDF সবচেয়ে “সৎ”, কিন্তু চূড়া বা দুই-চূড়ার আকৃতি এতে চোখে কম পড়ে, তাই অনেক পাঠকের কাছে কম সহজ।

KDE-র ইতিহাস ১৯৫০-৬০-এর দশকের। Murray Rosenblatt ১৯৫৬ সালে আর Emanuel Parzen ১৯৬২ সালে প্রায় একই ধারণা প্রকাশ করেন। পরের কয়েক দশক পরিসংখ্যানবিদেরা “সেরা” bandwidth নিয়ে গবেষণা করেন। Scott-এর নিয়ম (seaborn-এর default) আর Silverman-এর নিয়ম দুটিই ধরে নেয় যে আসল distribution মোটামুটি normal-এর মতো; তখন তারা একটি নির্দিষ্ট ত্রুটির মাপকে মোটামুটি সর্বনিম্ন করে। কিন্তু ডেটা দুই-চূড়ার হলে (যেমন ঈদের সপ্তাহের বড় অর্ডার আর সাধারণ সপ্তাহের ছোট অর্ডার মিশে) এই নিয়মগুলো সাধারণত বেশি চওড়া bandwidth দেয় আর দুটি চূড়া একটিতে মিশিয়ে ফেলে।

আরেকটি সমস্যা সীমানা। দাম, দিন, বয়স শূন্যের নিচে যায় না, আর রেটিং ১ থেকে ৫-এর মধ্যে আটকানো। সাধারণ Gaussian KDE এই সীমা জানে না, তাই সীমানার কাছে ঘনত্ব কম দেখায় আর সীমার বাইরে ফাঁস করে; এই পর্বের −২৪৬০ টাকা তার উদাহরণ। পূর্ণসংখ্যার ডেটায় (ডেলিভারি-দিন) KDE ২.৫ দিনকেও সম্ভব দেখায়; সেখানে histplot(..., discrete=True) বেশি সৎ। ব্যবহারিক নিয়ম: আকৃতি বোঝাতে histogram দিয়ে শুরু করুন, তুলনা আর ভিড় এড়াতে KDE, “কত ভাগ X-এর কম” প্রশ্নে ECDF; আর যেকোনো আকৃতিকে বিশ্বাস করার আগে দু-তিনটি bin বা bandwidth-এ দেখুন।

মূল উপলব্ধি: প্রতিটি distribution chart-এ একটি মসৃণ করার সিদ্ধান্ত লুকানো, শুধু ECDF ছাড়া; কোনো সার্বজনীন সঠিক bandwidth নেই, তাই আকৃতির দাবি একাধিক মসৃণতায় টিকলেই বিশ্বাসযোগ্য।

প্র ০৫matplotlib, seaborn, seaborn.objects, pandas-এর .plot, Plotly — এত plotting library কেন? উঁচু-স্তরের library-র দাম কী?

matplotlib-এর জন্ম ২০০৩ সালের দিকে, John Hunter-এর হাতে, MATLAB-এর মতো plotting Python-এ আনার জন্য; তাই এর pyplot interface MATLAB-এর ধাঁচে। এটি নিম্ন-স্তরের: প্রতিটি রেখা, লেখা আর রং আপনি নিয়ন্ত্রণ করেন, আর প্রায় যেকোনো ছবি বানানো যায়। কিন্তু “জেলা ধরে, পেমেন্ট অনুযায়ী রং” ধরনের সাধারণ পরিসংখ্যানিক কাজেও অনেক লাইন লাগে। এই ফাঁক পূরণে উঁচু-স্তরের library এল। Leland Wilkinson-এর ১৯৯৯ সালের বই “The Grammar of Graphics” একটি তত্ত্ব দেয়: chart মানে ডেটা, mapping (কোন variable কোন বৈশিষ্ট্যে), geometry (বিন্দু, বার), stat আর scale-এর সমন্বয়। Hadley Wickham ২০০৫ সালের দিকে R-এ ggplot2 বানান এই ধারণায়, আর সেটি পরিসংখ্যানবিদদের মধ্যে বিপুল জনপ্রিয় হয়।

seaborn-এর function-ভিত্তিক interface একটি মাঝামাঝি পথ: প্রতিটি পরিচিত chart-এর জন্য একটি function, ভেতরে grammar-এর ধারণা (semantic mapping), কিন্তু নিচে সম্পূর্ণ matplotlib. seaborn.objects আরও এক ধাপ grammar-এর দিকে। pandas-এর df.plot() দ্রুত এক নজরের জন্য, আর Plotly ব্রাউজারে interactive chart দেয় (পর্ব ৩১)।

উঁচু-স্তরের library-র দাম তিনটি। প্রথমত, লুকানো হিসাব: seaborn নিঃশব্দে গড়, bootstrap আর KDE করে; আপনি না জানলে ভুল পড়বেন, এই পর্বের মূল সতর্কতা। দ্বিতীয়ত, শেষ খুঁটিনাটির জন্য নিচের স্তরে নামতে হয়: প্রকাশনার মানের chart-এ প্রায় সবসময় কিছু matplotlib লাগে, তাই দুটিই জানা দরকার। তৃতীয়ত, version-এর পরিবর্তন: API-র নাম বদলায় (ci থেকে errorbar, distplot থেকে histplot/displot), আর দুই library-র version-এর মিল না থাকলে সতর্কবার্তা আসে, যেমন লেখকের যন্ত্রে seaborn 0.13.2 আর matplotlib 3.11-এর vert বার্তা। সম্প্রদায়ে কেউ ggplot-ধাঁচের ঘোষণামূলক পথ পছন্দ করেন, কেউ matplotlib-এর সরাসরি নিয়ন্ত্রণ; কোনোটিই একমাত্র ঠিক নয়।

মূল উপলব্ধি: উঁচু-স্তরের library সময় আর ভুল বাঁচায়, কিন্তু হিসাব লুকায় আর নিচের স্তরের ওপর নির্ভর করে; তাই seaborn দিয়ে শুরু, matplotlib দিয়ে শেষ, আর ভেতরে কী হিসাব হচ্ছে তা সবসময় জানা।

অনুশীলন

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

  1. হিসাব: খুলনার ২৫টি অর্ডারের গড় ডেলিভারি-সময় ৩.২ দিন, নমুনার SD ১.৫ দিন। (ক) SE কত? (খ) normal অনুমানে গড়ের ৯৫% CI মোটামুটি কত? (গ) errorbar="sd"-এর দাগ কোথা থেকে কোথায়? (ঘ) অর্ডার ১০০টি হলে (গড় আর SD একই ধরে) CI আর SD-র দাগ কী হবে?

    (ক) SE = s / √n = ১.৫ / √২৫ = ১.৫ / ৫ = ০.৩০. (খ) ১.৯৬ × ০.৩০ = ০.৫৯, তাই CI ≈ ৩.২ ± ০.৫৯ = ২.৬১ থেকে ৩.৭৯; seaborn-এর bootstrap CI এর কাছাকাছি হবে, হুবহু নয়। (গ) ৩.২ ± ১.৫ = ১.৭ থেকে ৪.৭. (ঘ) √১০০ = ১০, SE = ০.১৫, CI ≈ ৩.২ ± ০.২৯ = ২.৯১ থেকে ৩.৪৯: অর্ধেক চওড়া, কারণ CI 1/√n হারে সরু হয় (চার গুণ ডেটা, অর্ধেক চওড়া)। SD-র দাগ প্রায় একই থাকে (১.৭ থেকে ৪.৭), কারণ বেশি অর্ডার আনলেও প্রতিটি অর্ডারের ছড়ানো কমে না।

  2. আউটপুট অনুমান করুন: চালানোর আগে আউটপুটের দুটি লাইন লিখুন।
    import matplotlib.pyplot as plt
    import pandas as pd
    import seaborn as sns
    df = pd.DataFrame({"d": ["A", "A", "B", "B", "C", "C"],
                       "p": ["x", "y", "x", "y", "x", "y"],
                       "v": [1, 2, 3, 4, 5, 6]})
    fig, ax = plt.subplots()
    out = sns.scatterplot(data=df, x="d", y="v", hue="p", ax=ax)
    g = sns.relplot(data=df, x="d", y="v", col="p", row="d", height=1.5)
    print(type(out).__name__, out is ax, type(g).__name__, g.axes.shape)
    print(len(plt.get_fignums()), [t.get_text() for t in ax.get_legend().texts])

    লেখকের যন্ত্রে সত্যিকারের run (seaborn 0.13.2):

    Axes True FacetGrid (3, 2)
    2 ['x', 'y']

    scatterplot axes-level, তাই দেওয়া Axes-টিই ফেরত দেয় (True). relplot figure-level: row="d"-এর তিনটি মান (A, B, C) সারি, col="p"-এর দুটি মান (x, y) কলাম, তাই shape (3, 2). খোলা Figure ২টি: আপনার নিজের, আর relplot-এর বানানো নতুনটি। Legend-এ hue-র দুটি স্তর x আর y, প্রথম দেখার ক্রমে। লক্ষ করুন, relplot-এর প্রতিটি panel-এ মাত্র একটি বিন্দু; ছয়টি সারি ৩ × ২ ঘরে ভাগ হওয়ায় প্রতিটি ঘরে একটি সারিই পড়েছে, তাই এখানে facet তুলনার কিছু দেখায় না।

  3. নকশা ও সমালোচনা: একটি AI সহকারী বাজারঘরের মাসিক রিপোর্টের জন্য নিচের chart বানিয়েছে, শিরোনাম “Sylhet customers are unhappy because deliveries are slow”. অন্তত পাঁচটি সমস্যা চিহ্নিত করুন আর ভালো সংস্করণের রূপরেখা দিন।
    import warnings; warnings.filterwarnings("ignore")
    sns.barplot(data=orders, x="district", y="rating", ci="sd",
                palette=["green", "red", "green", "red", "red"])
    plt.ylim(3.5, 4.5)
    plt.title("Sylhet customers are unhappy because deliveries are slow")
    • পুরোনো API আর চাপা সতর্কবার্তা: ci="sd" 0.12 থেকে পুরোনো (errorbar="sd"), hue ছাড়া palette-ও পুরোনো; আর filterwarnings("ignore") ঠিক এই দুটি বার্তাই লুকিয়েছে।
    • লাল-সবুজ রং: বর্ণান্ধ পাঠক জেলাগুলো আলাদা করতে পারবেন না; তার ওপর রং কোনো variable-কে বোঝায় না, শুধু “ভালো-খারাপ” রায় চাপায়। একটি রং, অথবা colorblind palette.
    • কাটা y-axis: বার শূন্য থেকে না শুরু হলে উচ্চতার অনুপাত মিথ্যা বলে; ৪.১ আর ৪.৩-এর প্রায় ৫%-এর পার্থক্য বারে এক-তৃতীয়াংশ দেখায়, কারণ বারের দৈর্ঘ্য দাঁড়ায় ০.৬ আর ০.৮ একক। গড় তুলনায় pointplot বা বিন্দু, অথবা পূর্ণ axis (পর্ব ৩২-এ বিস্তারিত)।
    • রেটিং-এর জন্য বার আর গড়: ১ থেকে ৫-এর রেটিং-এ গড় আর SD-র দাগ ছড়ানো ঠিক দেখায় না; প্রতিটি জেলায় কত ভাগ ১-২ তারা, সেটি (histplot(..., multiple="fill") বা stacked অনুপাত) বেশি অর্থপূর্ণ। আর n কত, তা লেখা নেই।
    • কারণের দাবি: chart-এ ডেলিভারি-সময়ই নেই, অথচ শিরোনাম কারণ বলছে। দরকার days বনাম rating-এর scatter বা জেলা ধরে hue, আর ঈদের মতো confounder বিবেচনা; শিরোনাম হওয়া উচিত একটি পর্যবেক্ষণ, যেমন “Mean rating by district, ±95% CI, n shown”.

    ভালো সংস্করণ: pointplot(errorbar=("ci", 95), seed=1), প্রতিটি জেলার n লেবেলে, colorblind palette, caption-এ দাগের অর্থ, আর পাশে days বনাম rating-এর ছবি, যেখানে কারণ নয়, শুধু সম্পর্ক বলা।

  4. কোড লিখুন: অংশ ৫-এর মতো একই seed-এ (default_rng(7), ১২০টি অর্ডার, একই জেলা আর λ) ডেটা বানান। (ক) ecdfplot দিয়ে জেলা অনুযায়ী ডেলিভারি-দিনের ECDF আঁকুন, আর x = ৩-এ একটি ড্যাশ দেওয়া উল্লম্ব রেখা দিন। (খ) pandas দিয়ে প্রতিটি জেলায় ৩ দিন বা তার কমে পৌঁছানো অর্ডারের ভাগ ছাপুন, আর ECDF-এর সঙ্গে মেলান।
    import numpy as np, pandas as pd
    import matplotlib.pyplot as plt, seaborn as sns
    rng = np.random.default_rng(7)
    dist = ["Dhaka", "Chattogram", "Khulna", "Rajshahi", "Sylhet"]
    lam = {"Dhaka": 1.5, "Chattogram": 2.5, "Khulna": 3, "Rajshahi": 3.5, "Sylhet": 4}
    d = rng.choice(dist, 120, p=[0.35, 0.25, 0.15, 0.13, 0.12])
    days = [1 + rng.poisson(lam[x]) for x in d]
    df = pd.DataFrame({"district": d, "delivery_days": days})
    ax = sns.ecdfplot(data=df, x="delivery_days", hue="district", hue_order=dist)
    ax.axvline(3, ls="--", color="0.4")
    ax.set(xlabel="Delivery days", ylabel="Share of orders")
    share = (df.assign(fast=df["delivery_days"] <= 3)
               .groupby("district")["fast"].mean().loc[dist].round(2))
    print(share)

    লেখকের যন্ত্রে সত্যিকারের run (seaborn 0.13.2, pandas 3.1.1):

    district
    Dhaka         0.82
    Chattogram    0.52
    Khulna        0.20
    Rajshahi      0.33
    Sylhet        0.21
    Name: fast, dtype: float64

    ড্যাশ রেখা যেখানে প্রতিটি ECDF-কে কাটে, সেই উচ্চতাই এই ভাগ: ঢাকার ৮২% অর্ডার ৩ দিনের মধ্যে, খুলনা আর সিলেটে মাত্র ২০-২১%. ECDF সিঁড়ির মতো, কারণ দিন পূর্ণসংখ্যা; x = ৩-এর ধাপে রেখা লাফ দেয়, আর ঠিক ৩-এ মান হলো “৩ বা তার কম”.

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

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

ক্রমএকটি দলের গড়ের ৯৫% bootstrap confidence interval বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🎲 পুনরুৎপাদনের জন্য একটি seed ঠিক করা
  2. 🔁 মূল নমুনা থেকে ফেরত রেখে একই আকারের একটি resample নেওয়া
  3. ➗ সেই resample-এর গড় হিসাব করে লিখে রাখা
  4. 🔂 আগের দুই ধাপ ১০০০ বার করা
  5. 📊 ১০০০টি গড় সাজিয়ে ২.৫ আর ৯৭.৫ percentile নেওয়া
  6. 🏷️ chart-এর caption-এ লেখা: “95% CI, bootstrap, n = …”

seaborn 0.13-এ sns.barplot(data=df, x="district", y="days")-এ কোনো errorbar না দিলে বারের ওপরের দাগটি কী দেখায়?

  • প্রতিটি দলের standard deviation
  • গড়ের ৯৫% confidence interval, bootstrap দিয়ে হিসাব
  • দলের সবচেয়ে কম থেকে সবচেয়ে বেশি মান
  • গড়ের ±১ standard error

একটি দলে ৩৬টি পর্যবেক্ষণ, গড় ১০, নমুনার SD ৬. normal অনুমানে গড়ের ৯৫% CI মোটামুটি কত?

  • ৪ থেকে ১৬
  • ৯ থেকে ১১
  • প্রায় ৮.০৪ থেকে ১১.৯৬
  • ৯.৬৭ থেকে ১০.৩৩

নিজের বানানো fig, axs = plt.subplots(1, 2)-এর axs[0]-এ seaborn দিয়ে একটি histogram বসাতে চান। কোনটি সঠিক?

  • sns.displot(df, x="taka", ax=axs[0])
  • sns.relplot(df, x="taka", ax=axs[0])
  • sns.catplot(df, x="taka", kind="hist", ax=axs[0])
  • sns.histplot(df, x="taka", ax=axs[0])

শুধু ধনাত্মক মানের (অর্ডারের দাম) ডেটায় kdeplot(..., bw_adjust=3) দিলে কোন সমস্যাটি সবচেয়ে বেশি দেখা যায়?

  • বক্ররেখা অতি মসৃণ হয়, আর শূন্যের নিচেও ঘনত্ব দেখায়
  • প্রতিটি অর্ডারে একটি আলাদা চূড়া হয়
  • মোট ক্ষেত্রফল ৩ হয়ে যায়
  • seaborn error দেয়, কারণ bw_adjust ১-এর বেশি হতে পারে না

৪০০টি অর্ডারের heatmap-এ qty আর delivery_days-এর correlation ০.২০. ঈদের সপ্তাহ আর বাকি সময় আলাদা করলে প্রতিটি দলে correlation প্রায় ০. সবচেয়ে যুক্তিসংগত ব্যাখ্যা কী?

  • বড় অর্ডার প্যাক করতে বেশি সময় লাগে
  • ঈদের সপ্তাহ দুটিকেই বাড়ায়, তাই এটি একটি confounder-এর ফল
  • heatmap-এ center=0 না দেওয়ায় ভুল সংখ্যা এসেছে
  • দলে ভাগ করলে নমুনা ছোট হয়, তাই দলের correlation অর্থহীন

একটি tidy DataFrame-এ rating কলামের মান ১ থেকে ৫ পূর্ণসংখ্যা (int). sns.scatterplot(data=df, x="days", y="taka", hue="rating")-এ seaborn রং কীভাবে বসাবে?

  • পাঁচটি সম্পূর্ণ আলাদা qualitative রং
  • error দেবে, কারণ hue-তে শুধু লেখা চলে
  • একটি ধারাবাহিক রঙের ধারা, ছোট থেকে বড় মানে
  • রং নয়, বিন্দুর আকার বদলাবে

g = sns.relplot(data=df, x="days", y="taka", col="payment"), যেখানে payment-এর তিনটি মান। g কী, আর প্রথম panel-এর Axes কীভাবে পাবেন?

  • একটি FacetGrid; g.axes[0, 0]
  • একটি matplotlib Axes; সরাসরি g
  • একটি PairGrid; g.diag_axes[0]
  • একটি matplotlib Figure; g.axes[0]

শব্দকোষ

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

মনে রাখুন

  1. seaborn = matplotlib + DataFrame + পরিসংখ্যান: long টেবিল দিন, x, y, hue, size, style, col/row দিয়ে কলামকে ছবির বৈশিষ্ট্যে পাঠান।
  2. Axes-level বনাম figure-level: histplot/scatterplot/boxplot Axes ফেরত দেয় আর ax= মানে; displot/relplot/catplot নতুন Figure আর FacetGrid দেয়, ax= উপেক্ষা করে।
  3. Distribution: histogram-এ bin, KDE-তে bandwidth একটি লুকানো সিদ্ধান্ত; KDE অসম্ভব মানেও ঘনত্ব বানাতে পারে (cut=0, clip); ECDF-এ কোনো মসৃণতা নেই।
  4. Error bar: default গড়ের ৯৫% bootstrap CI (১০০০ resample); SD ছড়ানো বলে, CI গড়ের অনিশ্চয়তা; seed দিন আর caption-এ লিখুন দাগটি কী।
  5. সম্পর্ক আর matrix: regplot-এর ছায়া রেখার CI, মডেল নয়; heatmap-এ vmin=-1, vmax=1, center=0 আর diverging palette; correlation কারণ নয়, confounder খুঁজুন।

পরের পর্বে (পর্ব ৩১): Interactive chart, Plotly দিয়ে: hover করে মান দেখা, zoom আর ফিল্টার, আর কখন interactive chart সত্যিই দরকার, কখন একটি সাধারণ ছবিই ভালো।

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

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