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

সৎ chart — ভুল axis, বিভ্রান্তিকর রং আর গল্প বলা

Honest and Accessible Charts — Lie Factor, Misleading Axes, Colour, Colour-Vision Deficiency and Data Storytelling

Taught in Bangla, this lesson computes Tufte's lie factor for truncated bar charts and shows how cherry-picked windows, dual axes, cumulative charts, areas, averages and small samples mislead. In-page matplotlib runs compare jet, viridis and cividis lightness, simulate deuteranopia, and turn a cluttered default chart into a clean one with an action title, plus accessibility and AI-chart checks.

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

  • Lie factor হিসাব করা, আর truncated axis, dual axis, বাছাই করা সময়, ক্ষেত্রফল ও 3-D প্রভাব চিনে ফেলা
  • Cumulative chart, গড়, মোট বনাম per-capita আর ছোট নমুনা কীভাবে ভুল ধারণা দেয়, আর error bar দিয়ে অনিশ্চয়তা দেখানো
  • ডেটার ধরন দেখে palette বাছাই, jet-এর সমস্যা, viridis/cividis, আর NumPy-তে রঙকানা দৃষ্টির simulation
  • Accessibility: alt text, লেখায় সারাংশ, contrast আর লেখার আকার
  • একটি জঞ্জালভরা default chart থেকে action title-ওয়ালা পরিষ্কার chart, আর সৎ chart-এর চেকলিস্ট

এই মডিউলে এ পর্যন্ত chart বানানোর হাতিয়ার দেখেছেন: পর্ব ২৮-এ matplotlib-এর figure আর axes, পর্ব ২৯-এ কোন প্রশ্নে কোন chart, পর্ব ৩০-এ seaborn আর পর্ব ৩১-এ Plotly. এই শেষ পর্বের প্রশ্ন আলাদা: chart-টি কি সত্যি বলছে, আর সবাই কি সেটা পড়তে পারছেন? উদাহরণ আবার বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান)। এখানে যত সংখ্যা আছে, সবই কাল্পনিক বা seeded কোডে বানানো, কোনো আসল কোম্পানির নয়। চালানো যায় এমন ব্লকগুলো পাতাতেই চলে (ব্রাউজারের Python, matplotlib 3.10.8); প্রথমবার matplotlib নামতে কয়েক সেকেন্ড লাগতে পারে। ছবির ভেতরের লেখা ইংরেজিতে রাখা হয়েছে, কারণ ব্রাউজারের matplotlib-এ বাংলা অক্ষর ভেঙে যায় (পর্ব ২৮-এ এর কারণ দেখেছেন)।

১ · সংখ্যা ঠিক, ছবি ভুল — Huff, Tufte আর lie factor

একটি দোকানের বিজ্ঞাপনের কথা ভাবুন: “আমাদের বিক্রি প্রতিদ্বন্দ্বীর তিন গুণ!” পাশে দুটি bar, একটি অন্যটির তিন গুণ লম্বা। পরে দেখা গেল আসল সংখ্যা ৯১ আর ৮৮, আর bar দুটি আঁকা শুরু হয়েছে ৮৬ থেকে। কেউ কোনো সংখ্যা বানায়নি, তবু ছবিটি মিথ্যা। Chart-এর ভুল প্রায়ই এভাবেই হয়: হিসাব ঠিক, কিন্তু চোখ যা মাপে (দৈর্ঘ্য, ক্ষেত্রফল, ঢাল, রঙের উজ্জ্বলতা) তা ডেটার সঙ্গে মেলে না।

১৯৫৪ সালে সাংবাদিক Darrell Huff লেখেন How to Lie with Statistics (ছবি এঁকেছিলেন Irving Geis). ছোট, মজার বইটিতে “gee-whiz graph” (কেটে ফেলা axis), এক মাত্রায় বাড়িয়ে দুই মাত্রায় আঁকা ছবি আর বাছাই করা গড়ের কৌশল দেখানো হয়। বহু দশক ধরে এটি পরিসংখ্যানের সবচেয়ে বেশি পড়া জনপ্রিয় বইগুলোর একটি বলে ধরা হয়। পরে ১৯৮৩ সালে Edward Tufte তাঁর The Visual Display of Quantitative Information বইয়ে এই সমস্যাকে মাপার একটি সংখ্যা দেন: lie factor. একই বইয়ে আছে data-ink ratio আর chartjunk-এর ধারণা, যা অংশ ৯-এ দেখবেন।

Tufte-এর সংজ্ঞা সহজ:

lie factor = ছবিতে দেখানো প্রভাবের আকার ÷ ডেটায় প্রভাবের আকার

এখানে “প্রভাবের আকার” মানে আপেক্ষিক পরিবর্তন: প্রথম মান a থেকে দ্বিতীয় মান b-তে গেলে পরিবর্তন (b − a) / a. ডেটায় এটি সংখ্যার পরিবর্তন; ছবিতে এটি দাগের দৈর্ঘ্য বা ক্ষেত্রফলের পরিবর্তন। lie factor ১ মানে ছবি ডেটার সঙ্গে মিলে আছে; ১-এর চেয়ে বড় মানে ছবি পার্থক্য বাড়িয়ে দেখাচ্ছে, ছোট মানে কমিয়ে। অনেক পাঠ্যসূত্রে বলা হয়, মান ০.৯৫ থেকে ১.০৫-এর বাইরে গেলে বিকৃতি উল্লেখযোগ্য; এই সীমাটি বই থেকে সরাসরি যাচাই করতে পারিনি, তাই একটি প্রচলিত নিয়ম হিসেবেই নিন। Tufte-এর নিজের ওয়েবসাইটের আলোচনায় পাঠকেরা দেখিয়েছেন যে শতাংশ দিয়ে ভাগ করার এই সূত্রের কিছু বিকল্পও আছে; মূল ভাবনা অবশ্য একই: চোখ যা দেখে, তা ডেটার অনুপাতে হওয়া উচিত।

বাজারের দাঁড়িপাল্লার কথা ভাবুন। এক পাল্লায় ৮৮ গ্রাম, অন্যটিতে ৯১ গ্রাম; কাঁটা সামান্য সরে। এখন কেউ যদি কাঁটার শুরু ৮৬ গ্রামে বেঁধে দেয়, তাহলে ২ আর ৫-এর তুলনায় কাঁটা যেন আড়াই গুণ বেশি ঘুরছে। ওজন একই আছে, শুধু মাপার দাগ সরানো হয়েছে। Truncated axis ঠিক এই কাজটি করে।

একই দুটি সংখ্যা, দুটি ভিন্ন y-axis y-axis 0 থেকে 092 8891 y-axis 86 থেকে 8692 8891 25
  1. ডেটা। বাজারঘরের দুই জেলায় সময়মতো ডেলিভারির হার: চট্টগ্রাম ৮৮%, খুলনা ৯১% (কাল্পনিক)। বাঁ দিকের chart শূন্য থেকে আঁকা; দুটি bar প্রায় সমান, যা সত্যি।
  2. ডেটায় প্রভাব। (91 − 88) / 88 = 3 / 88, প্রায় ৩.৪%. অর্থাৎ খুলনার হার চট্টগ্রামের চেয়ে আপেক্ষিকভাবে মাত্র ৩.৪% বেশি।
  3. Axis কাটা হলো। ডান দিকের chart-এ y-axis শুরু ৮৬ থেকে। এখন bar-এর দৃশ্যমান দৈর্ঘ্য 88 − 86 = 2 আর 91 − 86 = 5 একক। চোখ দৈর্ঘ্য মাপে, তাই খুলনার bar আড়াই গুণ লম্বা দেখায়।
  4. ছবিতে প্রভাব। (5 − 2) / 2 = 1.5, অর্থাৎ ১৫০%. ডেটায় যা ৩.৪%, ছবিতে তা ১৫০%.
  5. Lie factor. 150% ÷ 3.4% ≈ 44. ঠিক হিসাবে 1.5 ÷ (3/88) = 88 / 2 = 44. সাধারণভাবে এই সংজ্ঞায় bar chart-এর lie factor হয় a / (a − baseline); baseline শূন্য হলে মান ঠিক ১, আর baseline মানের যত কাছে, lie factor তত বড়।

২ · Bar chart-এর axis শূন্য থেকে — আর কখন না হলেও চলে

Bar chart-এ মান বোঝায় bar-এর দৈর্ঘ্য, তাই দৈর্ঘ্যের শুরু অর্থাৎ শূন্য বাদ দিলে অনুপাত ভেঙে যায়। এমন axis-কে বলে truncated axis. matplotlib নিজে থেকে bar chart-এ শূন্য রাখে, কিন্তু কেউ set_ylim(86, 92) লিখলে বা AI “পার্থক্য স্পষ্ট করতে” এটি যোগ করলে ছবি বদলে যায়। নিচের ব্লকে একই দুটি সংখ্যা পাশাপাশি দুইভাবে আঁকা হয়েছে, আর কোড নিজেই lie factor ছাপে।

import matplotlib.pyplot as plt

# বাজারঘর: সময়মতো ডেলিভারির হার (%), কাল্পনিক সংখ্যা
names = ["Chattogram", "Khulna"]
vals = [88.0, 91.0]

def lie_factor(a, b, base):
    data_effect = (b - a) / a                       # ডেটায় পরিবর্তন
    shown_effect = ((b - base) - (a - base)) / (a - base)  # bar-এর দৈর্ঘ্যে
    return data_effect, shown_effect, shown_effect / data_effect

fig, axs = plt.subplots(1, 2, figsize=(6.4, 3), dpi=100)
for ax, base in zip(axs, [0, 86]):
    ax.bar(names, vals, color="#1f77b4", width=0.6)
    ax.set_ylim(base, 92)
    ax.set_ylabel("On-time delivery (%)")
    ax.set_title(f"y-axis starts at {base}")
    for x, v in enumerate(vals):
        ax.text(x, v, f"{v:.0f}%", ha="center", va="bottom")
    d, s, lf = lie_factor(*vals, base)
    print(f"baseline {base:>2}: ডেটায় {d:.1%}, ছবিতে {s:.1%}, lie factor {lf:.2f}")
fig.tight_layout()
plt.show()

আসল আউটপুট (ব্রাউজারের Python, matplotlib 3.10.8), সঙ্গে দুই প্যানেলের একটি ছবি:

baseline  0: ডেটায় 3.4%, ছবিতে 3.4%, lie factor 1.00
baseline 86: ডেটায় 3.4%, ছবিতে 150.0%, lie factor 44.00

বাঁ প্যানেলে দুটি bar প্রায় সমান, ডান প্যানেলে একটি অন্যটির আড়াই গুণ। লক্ষ করুন, দুই প্যানেলের মাথায় লেখা মান (৮৮% আর ৯১%) একই; শুধু দাগের দৈর্ঘ্য বদলেছে। এ কারণেই সংখ্যা লিখে দিলেও truncated bar বিভ্রান্তিকর থাকে: মানুষ আগে আকার দেখে, লেখা পড়ে পরে। lie_factor function-এর সূত্র ঠিক stepper-এর ধাপ ২ থেকে ৪, তাই হিসাব কোড দিয়েও মিলল।

কখন শূন্য ছাড়া axis ঠিক

নিয়মটি bar আর area chart-এর জন্য, যেখানে দৈর্ঘ্য বা ক্ষেত্রফল নিজেই মান। Line chart বা dot plot-এ চোখ মাপে অবস্থান আর ঢাল, দৈর্ঘ্য নয়। শরীরের তাপমাত্রা ৩৬.৫ থেকে ৩৯ °C-এর মধ্যে ওঠানামা করে; শূন্য থেকে আঁকলে জ্বরের রেখা একটি সোজা দাগ হয়ে যাবে, যা নিজেই এক রকম অসততা, কারণ গুরুত্বপূর্ণ পরিবর্তন অদৃশ্য হয়ে যায়। তেমনি বাজারঘরের গড় ডেলিভারি সময় ২.১ থেকে ২.৪ দিনে যাওয়া line chart-এ ২ থেকে শুরু হওয়া axis-এ দেখানো যুক্তিসঙ্গত। শর্ত তিনটি:

  • Axis-এর সংখ্যাগুলো স্পষ্ট রাখুন, আর দরকার হলে লিখে দিন “y-axis 2 থেকে শুরু”.
  • পরিবর্তনটি বাস্তবে গুরুত্বপূর্ণ কি না, তা লেখায় বলুন; শুধু ছবির ঢাল দিয়ে নাটক তৈরি করবেন না।
  • Bar chart-এ জায়গা বাঁচাতে axis ভাঙতে হলে (axis break) ভাঙার চিহ্ন আঁকুন, অথবা বরং dot plot ব্যবহার করুন।
মূল ধারণা

দৈর্ঘ্য বা ক্ষেত্রফল যেখানে মান, সেখানে শূন্য বাধ্যতামূলক। অবস্থান বা ঢাল যেখানে মান (line, dot), সেখানে axis ডেটার পরিসর অনুযায়ী হতে পারে, তবে স্পষ্ট লেবেলসহ। প্রশ্নটা “শূন্য আছে কি না” নয়, বরং “চোখ যা মাপছে তা কি ডেটার অনুপাতে?”

৩ · সময়ের জানালা, দুই axis আর cumulative chart

সময়ের ডেটায় তিনটি সাধারণ ফাঁদ আছে। প্রথমটি cherry-picking: পুরো ইতিহাস থেকে শুধু সেই অংশ কেটে দেখানো যা নিজের গল্পের পক্ষে যায়। দ্বিতীয়টি dual axis: একই ছবিতে দুটি আলাদা y-axis, যার প্রতিটির scale আঁকিয়ে নিজের ইচ্ছামতো ঠিক করতে পারেন, ফলে যেকোনো দুটি রেখাকে একসঙ্গে ওঠানামা করতে দেখানো যায়। তৃতীয়টি cumulative chart: চলমান যোগফল (cumulative sum) সবসময় বাড়ে বা সমান থাকে, তাই মাসিক বিক্রি কমলেও রেখাটি ওপরেই ওঠে।

নিচের ব্লকে বাজারঘরের ২৪ মাসের একটি কাল্পনিক বিক্রির series (seed 42) আছে। কোড প্রতিটি সম্ভাব্য ৬ মাসের জানালা খুঁজে সবচেয়ে ভালো আর সবচেয়ে খারাপটি বের করে, আর একটি সম্পর্কহীন series-কে dual axis-এ পাশে বসায়।

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
m = np.arange(24)                                # ২৪ মাস
sales = 300 + 4 * m + rng.normal(0, 18, 24)      # হাজার টাকা, কাল্পনিক
other = 50 + 6 * m + rng.normal(0, 25, 24)       # সম্পর্কহীন আরেকটি series

def change(a, b):
    return (sales[b] - sales[a]) / sales[a]

up = max(range(18), key=lambda a: change(a, a + 5))   # সবচেয়ে ভালো ৬ মাস
dn = min(range(18), key=lambda a: change(a, a + 5))   # সবচেয়ে খারাপ ৬ মাস
wins = [("All 24 months", 0, 23), ("Best 6 months", up, up + 5),
        ("Worst 6 months", dn, dn + 5)]
for t, a, b in wins:
    print(f"{t:15} ({a:>2}→{b:>2}): {change(a, b):+.1%}")
print("level-এর corr:", np.corrcoef(sales, other)[0, 1].round(2))
print("মাসিক পরিবর্তনের corr:", np.corrcoef(np.diff(sales), np.diff(other))[0, 1].round(2))

fig, axs = plt.subplots(1, 3, figsize=(7.5, 2.6), dpi=100)
for ax, (t, a, b) in zip(axs, wins):
    ax.plot(m[a:b + 1], sales[a:b + 1], marker="o", ms=3)
    ax.set_title(t, fontsize=10); ax.set_xlabel("Month")
axs[0].set_ylabel("Sales (thousand Tk)")
fig.tight_layout()

fig2, ax = plt.subplots(figsize=(5, 2.6), dpi=100)
ax.plot(m, sales, color="tab:blue"); ax.set_ylabel("Sales (thousand Tk)", color="tab:blue")
ax2 = ax.twinx()
ax2.plot(m, other, color="tab:orange", ls="--"); ax2.set_ylabel("Unrelated series", color="tab:orange")
ax.set_title("Dual axis: each scale chosen freely")
fig2.tight_layout()
plt.show()

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4.6), সঙ্গে দুটি ছবি:

All 24 months   ( 0→23): +27.4%
Best 6 months   ( 5→10): +20.0%
Worst 6 months  (10→15): -3.2%
level-এর corr: 0.83
মাসিক পরিবর্তনের corr: 0.19

একই ডেটা থেকে তিনটি শিরোনাম লেখা যায়: “দুই বছরে বিক্রি ২৭% বেড়েছে”, “ছয় মাসে ২০% লাফ” আর “বিক্রি কমছে”. প্রতিটি সংখ্যা সত্যি, কিন্তু শেষ দুটি জানালা বাছাই করা। ছোট প্যানেলগুলোর y-axis নিজে থেকে নিজের পরিসরে zoom করে, তাই ৩.২%-এর পতনও খাড়া দেখায়। প্রতিকার: পুরো পরিসর দেখান, বা অন্তত জানান কেন এই জানালা বেছেছেন, আর তুলনার জন্য প্যানেলগুলোর axis এক রাখুন (sharey=True).

Dual axis-এর ছবিতে দুটি রেখা প্রায় হাত ধরাধরি করে চলে, আর level-এর correlation ০.৮৩. অথচ দ্বিতীয় series-টি সম্পর্কহীন random সংখ্যা, শুধু দুটোরই সময়ের সঙ্গে ঊর্ধ্বমুখী প্রবণতা আছে। মাসে মাসে পরিবর্তনের correlation মাত্র ০.১৯; একসঙ্গে ওঠার ছাপটি এসেছে প্রবণতা আর scale বাছাই থেকে। এটি spurious correlation-এর ক্লাসিক রূপ।

Dual axis কেন বিপজ্জনক: দুটি y-axis-এর শূন্য আর scale দুটোই স্বাধীন, তাই আঁকিয়ে রেখা দুটিকে যেখানে খুশি মেলাতে বা আলাদা করতে পারেন; কাটাকাটির বিন্দুরও কোনো অর্থ নেই। Data visualization লেখক Stephen Few-সহ অনেকে তাই এর বদলে দুটি আলাদা প্যানেল (একই x-axis, ওপর-নিচে), অথবা দুই series-কে একটি শুরুর মান ১০০ ধরে index করে এক axis-এ আঁকার পরামর্শ দেন। ব্যতিক্রম শুধু একই রাশির দুই একক, যেমন বাঁয়ে °C আর ডানে °F.

Cumulative chart বনাম মাসিক chart

ধরুন বাজারঘরের একটি নতুন পণ্যের চার মাসের বিক্রি (কাল্পনিক) ৫০, ৪৮, ৪৫ আর ৪১ হাজার টাকা। প্রতি মাসে বিক্রি কমছে। কিন্তু cumulative যোগফল ৫০, ৯৮, ১৪৩, ১৮৪: রেখাটি সুন্দরভাবে ওপরে উঠছে। চলমান যোগফলের ঢাল আসলে মাসিক বিক্রি; ঢাল কমছে কি না চোখে ধরা কঠিন। তাই কোনো উপস্থাপনায় শুধু “মোট গ্রাহক”, “মোট ডাউনলোড” বা “এ পর্যন্ত বিক্রি” দেখলে প্রশ্ন করুন: প্রতি মাসের নতুন সংখ্যা কত? মাসিক বা সাপ্তাহিক মান আলাদা আঁকলে পতন সঙ্গে সঙ্গে চোখে পড়ে।

৪ · আকার, ক্ষেত্রফল আর 3-D-এর ফাঁকি

Huff-এর বইয়ের একটি বিখ্যাত উদাহরণ: আয় দ্বিগুণ হয়েছে বোঝাতে টাকার থলির ছবি দ্বিগুণ লম্বা আর দ্বিগুণ চওড়া করে আঁকা। থলির উচ্চতা দ্বিগুণ হলেও ক্ষেত্রফল চার গুণ, আর ছবিটি যদি ত্রিমাত্রিক বস্তু বোঝায়, তাহলে চোখে আয়তন আট গুণ মনে হতে পারে। Bubble chart, মানচিত্রের বৃত্ত বা icon দিয়ে মান দেখালে এই ভুল খুব সহজে ঢোকে।

মান দ্বিগুণ হলে বৃত্ত কত বড় হবে

Doubling a value: radius versus area

Radius দ্বিগুণ করলে ক্ষেত্রফল চার গুণ মান 100 মান 200 (ভুল) মান 200 (ঠিক) r = 20 r = 40 r = 28.3 ক্ষেত্রফল ×1 ক্ষেত্রফল ×4 ক্ষেত্রফল ×2
ছবি ৩২.১ — মান দ্বিগুণ হলে ব্যাসার্ধ বাড়াতে হয় √2 ≈ 1.41 গুণে, যাতে ক্ষেত্রফল দ্বিগুণ হয়। মাঝের বৃত্তে lie factor ৩।

হিসাবটি করে দেখি। ডেটায় প্রভাব: ১০০ থেকে ২০০, অর্থাৎ ১০০%. মাঝের বৃত্তে ব্যাসার্ধ দ্বিগুণ, তাই ক্ষেত্রফল π × 40² ÷ (π × 20²) = 4 গুণ, অর্থাৎ প্রভাব ৩০০%. lie factor 300 ÷ 100 = 3. ঠিক নিয়ম হলো ক্ষেত্রফলকে মানের সমানুপাতিক রাখা: r ∝ √মান. matplotlib-এর scatter-এ s parameter আসলে marker-এর ক্ষেত্রফল (points²), ব্যাসার্ধ নয়; তাই s-এ সরাসরি মানটি দিলেই ক্ষেত্রফল সমানুপাতিক হয়। ভুল হয় যখন কেউ “বড় দেখাতে” s=value**2 লেখেন।

তবে মনে রাখুন, ক্ষেত্রফল ঠিক রাখলেও মানুষ ক্ষেত্রফলের তুলনা দৈর্ঘ্যের মতো নির্ভুলভাবে করতে পারে না। ১৯৮৪ সালে পরিসংখ্যানবিদ William Cleveland আর Robert McGill মানুষের ওপর পরীক্ষা করে দেখান যে একই axis-এ অবস্থান আর দৈর্ঘ্যের বিচার সবচেয়ে নির্ভুল, কোণ আর ক্ষেত্রফলের বিচার তুলনায় দুর্বল, রঙের ঘনত্ব আরও দুর্বল। এ কারণেই সঠিক তুলনার দরকার হলে bar বা dot plot বেশি নিরাপদ।

3-D chart: ঘোরানো 3-D pie chart-এ সামনের টুকরো perspective-এর কারণে বড় দেখায়, পেছনেরটি ছোট; 3-D bar-এর মাথা কোথায়, পেছনের grid-এ মেলাতে গিয়ে চোখ ভুল করে। তৃতীয় মাত্রাটি কোনো ডেটা বহন করে না, শুধু বিকৃতি যোগ করে। ডেটা সত্যিই তিন মাত্রার না হলে 3-D প্রভাব বাদ দিন।

৫ · গড়ের আড়ালে, আর মোট বনাম প্রতি-ক্রেতা

একটি bar যখন একটি গড় দেখায়, তখন সেই bar-এর পেছনে শত শত আলাদা মান লুকিয়ে থাকে। দুই জেলার গড় ডেলিভারি সময় যদি দুটোই ২.৫ দিন হয়, তবু একটিতে সবাই ২ থেকে ৩ দিনে পেতে পারেন, অন্যটিতে অর্ধেক মানুষ ১ দিনে আর বাকিরা ৪ দিনে। Aggregation (অনেক মানকে একটিতে নামানো) সারাংশ দেয়, কিন্তু ছড়ানো লুকায়। সৎ বিকল্প হলো distribution দেখানো: box plot, violin, strip plot বা histogram (পর্ব ২৯ আর ৩০)। অন্তত গড়ের পাশে median আর ছড়ানোর একটি মাপ দিন।

দলে ভাগ করা ডেটায় আরও বড় ফাঁদ আছে: পর্ব ২৪-এর Simpson's paradox, যেখানে সব মিলিয়ে একটি কুরিয়ার এগিয়ে, অথচ প্রতিটি জেলায় আলাদাভাবে অন্যটি। একটি মোট chart সেখানে উল্টো গল্প বলে। তাই গুরুত্বপূর্ণ তুলনায় দলভিত্তিক chart (small multiples, অর্থাৎ একই ধাঁচের ছোট ছোট প্যানেল) পাশে রাখুন।

মোট নাকি প্রতি জনে

মানচিত্রে বা bar chart-এ জেলাভিত্তিক মোট বিক্রি দেখালে প্রায় সবসময় বড় জেলা এগিয়ে থাকে, কারণ সেখানে ক্রেতা বেশি। প্রশ্ন যদি হয় “কোথায় ক্রেতারা বেশি সক্রিয়”, তাহলে দরকার per-capita বা প্রতি জনে হিসাব। নিচের টেবিলের সব সংখ্যা কাল্পনিক, শুধু ধারণা বোঝানোর জন্য বানানো; কোনো জেলার আসল জনসংখ্যা বা আসল ক্রেতার সংখ্যা নয়।

জেলামাসের অর্ডারনিবন্ধিত ক্রেতা (কাল্পনিক)প্রতি ক্রেতায় অর্ডার
ঢাকা৪,২০০২,৮০০১.৫০
চট্টগ্রাম২,১০০১,৫০০১.৪০
সিলেট৯০০৪৫০২.০০

মোট অর্ডারে ক্রম ঢাকা, চট্টগ্রাম, সিলেট; প্রতি ক্রেতায় ক্রম সিলেট (২.০০), ঢাকা (১.৫০), চট্টগ্রাম (১.৪০)। কোনোটিই “ভুল” নয়; দুটি ভিন্ন প্রশ্নের উত্তর। ভুল হয় যখন প্রশ্ন একটা, chart অন্যটার। মোট দেখালে শিরোনামে “মোট” লিখুন; হার দেখালে হরটি কী (প্রতি ক্রেতা, প্রতি অর্ডার, প্রতি হাজার মানুষ) তা axis-এ লিখুন। আর ছোট হরের হার সাবধানে পড়ুন: ৪৫০ জনের জেলায় কয়েকজন বড় ক্রেতা যোগ হলেই হার অনেক নড়ে যায়, যা পরের অংশের বিষয়।

৬ · অনিশ্চয়তা দেখানো — error bar, ছোট নমুনা আর শব্দমুখর প্রবণতা

বাজারঘরের অ্যাপে সিলেটের গড় rating ৪.৬, ঢাকার ৪.১. একটি bar chart দেখে মনে হবে সিলেটের ক্রেতারা বেশি খুশি। কিন্তু যদি সিলেটের গড় মাত্র ৫টি rating থেকে আর ঢাকার ৪০০টি থেকে আসে (কাল্পনিক), তাহলে এই দুটি সংখ্যার নির্ভরযোগ্যতা একেবারে আলাদা। ক্রিকেটে এক ম্যাচে ১০০ রান করা নতুন ব্যাটারের গড় আর ১০০ ম্যাচ খেলা ব্যাটারের গড় যেমন সমান ভরসার নয়, এখানেও তাই।

নমুনার গড় কতটা নড়তে পারে, তার একটি মাপ standard error: SE = s / √n, যেখানে s মানে rating-গুলোর standard deviation আর n নমুনার আকার। ধরুন দুই জায়গাতেই s ≈ 0.9 (কাল্পনিক অনুমান)।

  • সিলেট: SE = 0.9 / √5 ≈ 0.40. মোটামুটি ৯৫% confidence interval-এর জন্য প্রচলিত হাতের হিসাব “গড় ± ২ SE”, অর্থাৎ 4.6 ± 0.80, প্রায় ৩.৮ থেকে ৫ (rating ৫-এর বেশি হয় না)। এত ছোট নমুনায় t-distribution ব্যবহার করা উচিত (৪ degrees of freedom-এ গুণক প্রায় 2.78), তাতে পরিসর আরও চওড়া: ±1.12.
  • ঢাকা: SE = 0.9 / √400 = 0.045, পরিসর 4.1 ± 0.09, প্রায় ৪.০১ থেকে ৪.১৯.

সিলেটের পরিসর ঢাকার পুরো পরিসরকে ঢেকে ফেলে; এই ডেটা দিয়ে “সিলেট বেশি খুশি” বলা যায় না। Chart-এ এটি দেখানোর উপায় error bar (প্রতিটি bar বা বিন্দুর ওপর-নিচে একটি দাগ), আর সঙ্গে প্রতিটি দলের n লিখে দেওয়া। matplotlib-এ ax.errorbar(x, y, yerr=...) বা ax.bar(..., yerr=...); seaborn-এর অনেক chart নিজেই interval আঁকে (পর্ব ৩০)। দুটি সাবধানতা: error bar কী বোঝাচ্ছে (standard deviation, standard error নাকি confidence interval) তা caption-এ লিখুন, কারণ তিনটির দৈর্ঘ্য খুব আলাদা; আর দুটি interval সামান্য ছুঁয়ে গেলেই “কোনো পার্থক্য নেই” সিদ্ধান্ত টানবেন না, এর জন্য ঠিক পরীক্ষা লাগে। এর তত্ত্ব আসবে পর্ব ৩৫–৩৭-এ।

শব্দমুখর প্রবণতা: দৈনিক বিক্রির chart-এ দুই দিনের ওঠানামা প্রায়ই নিছক এলোমেলো শব্দ (noise). শেষ তিনটি বিন্দু নিচে গেছে বলেই “পতন শুরু” লিখবেন না। পর্ব ২৬-এর moving average দিয়ে মসৃণ রেখা পাশে আঁকুন, আর আগের বছরগুলোতে এমন ওঠানামা স্বাভাবিক ছিল কি না দেখুন। আবহাওয়ার পূর্বাভাসে যেমন “৭০% সম্ভাবনা” বলা হয়, “কাল বৃষ্টি হবেই” নয়; সৎ chart-ও অনিশ্চয়তা লুকায় না।

৭ · রং — ডেটার ধরন, jet-এর সমস্যা আর রঙকানা দৃষ্টি

রং chart-এ তিন রকম কাজ করে, আর প্রতিটির জন্য আলাদা ধরনের palette লাগে। Colormap হলো সংখ্যা থেকে রঙে যাওয়ার নিয়ম; matplotlib-এ cmap parameter দিয়ে বাছাই করেন।

Qualitative

নাম-ধরনের দল (জেলা, পেমেন্টের ধরন)। রংগুলো আলাদা, কিন্তু কোনো ক্রম বোঝায় না। দল সাত-আটটির বেশি হলে রং মনে রাখা কঠিন; তখন দল কমান বা সরাসরি লেবেল দিন।

tab10, Okabe–Ito

Sequential

কম থেকে বেশি (বিক্রির ঘনত্ব, ডেলিভারির দিন)। উজ্জ্বলতা একদিকে বাড়ে, তাই গাঢ় মানে বেশি বা কম, এক নজরে বোঝা যায়।

viridis, cividis

Diverging

অর্থপূর্ণ একটি মাঝের মান ঘিরে দুই দিকে (গত মাসের তুলনায় পরিবর্তন, ০ মাঝখানে)। দুই প্রান্তে দুটি ভিন্ন রং, মাঝে হালকা।

RdBu, PuOr
  1. ডেটার ধরননাম না সংখ্যা?প্রথম প্রশ্ন
  2. দলtab10qualitative
  3. কম→বেশিviridissequential
  4. মাঝ ঘিরেRdBudiverging
  5. যাচাইCVD + grayscaleসবাই পড়তে পারেন?
প্রশ্নরং কোন তথ্য বহন করবে
district → নাম, ক্রম নেই
sales    → 0 থেকে বেশি
change   → 0 ঘিরে − ও +
Qualitativeপ্রতিটি দলে একটি আলাদা রং
ax.bar(names, vals,
       color=plt.cm.tab10(range(5)))
# ৫টি দল; রঙে ক্রম নেই
Sequentialউজ্জ্বলতা একদিকে বাড়ে
ax.imshow(density,
          cmap="viridis")
# L*: 15 → 26 → … → 91
Divergingমাঝের মান ঠিক মাঝের রঙে
from matplotlib.colors import TwoSlopeNorm
norm = TwoSlopeNorm(vcenter=0)
ax.imshow(change, cmap="RdBu",
          norm=norm)
যাচাইরং ছাড়াও কি বার্তা টেকে?
১. CVD simulation
২. grayscale-এ দেখা
৩. লাল-সবুজ একা নয়
৪. সরাসরি label
  1. প্রশ্ন আগে। রং কোন তথ্য বহন করবে? নাম-ধরনের দল, কম থেকে বেশি সংখ্যা, নাকি একটি মাঝের মান ঘিরে দুই দিকের সংখ্যা? এই উত্তরই palette ঠিক করে, রুচি নয়।
  2. নাম-ধরনের দল। জেলা বা পেমেন্টের ধরন: qualitative palette, যেমন matplotlib-এর default tab10. রংগুলোর উজ্জ্বলতা কাছাকাছি, যাতে কোনো দল “বেশি গুরুত্বপূর্ণ” না দেখায়।
  3. কম থেকে বেশি। বিক্রির ঘনত্ব বা দিনের সংখ্যা: sequential palette. viridis-এ উজ্জ্বলতা (L*) ১৫ থেকে ৯১ পর্যন্ত একদিকে বাড়ে, তাই গাঢ় থেকে উজ্জ্বল মানেই কম থেকে বেশি।
  4. মাঝ ঘিরে দুই দিক। গত মাসের তুলনায় পরিবর্তন: diverging palette, আর TwoSlopeNorm(vcenter=0) দিয়ে শূন্যকে ঠিক মাঝের হালকা রঙে বসান; নইলে পরিসর অসমান হলে শূন্য একদিকের রঙে চলে যায়।
  5. যাচাই। রঙকানা দৃষ্টির simulation আর grayscale-এ দেখে নিন বার্তা টিকে আছে কি না; শুধু লাল বনাম সবুজে অর্থ রাখবেন না; সম্ভব হলে legend-এর বদলে সরাসরি label দিন।

Jet কেন সমস্যা, viridis কেন ভালো

“রামধনু” palette jet একসময় MATLAB আর matplotlib দুটোরই default ছিল। সমস্যা হলো এর উজ্জ্বলতা একদিকে বাড়ে না: নীল থেকে cyan আর হলুদে উজ্জ্বলতা হঠাৎ বেড়ে আবার লালে কমে যায়। ফলে মসৃণ ডেটায়ও চোখে নকল সীমারেখা (band) দেখা যায়, আর মাঝের হলুদ অংশকে সবচেয়ে বড় মান মনে হয়। ২০০৭ সালে David Borland আর Russell Taylor-এর “Rainbow Color Map (Still) Considered Harmful” প্রবন্ধটি এই সমালোচনার একটি পরিচিত সূত্র। এর জবাবে Stéfan van der Walt আর Nathaniel Smith viridis বানান, যা ২০১৭ সালে matplotlib 2.0-তে default হয়। এটি perceptually uniform: মানের সমান ধাপে চোখে প্রায় সমান রঙের পার্থক্য। ২০১৮ সালে Jamie Nuñez ও সহকর্মীরা cividis প্রকাশ করেন, যা বিশেষভাবে রঙকানা দৃষ্টির মানুষের কাছে প্রায় একইরকম দেখানোর জন্য নকশা করা।

Color-vision deficiency (রঙকানা দৃষ্টি) সম্পর্কে প্রচলিত হিসাব হলো, উত্তর ইউরোপীয় বংশোদ্ভূত পুরুষদের প্রায় ৮% আর নারীদের প্রায় ০.৫%-এর কোনো না কোনো ধরনের লাল-সবুজ রঙকানা দৃষ্টি আছে; অন্য জনগোষ্ঠীতে হার কিছুটা ভিন্ন হতে পারে, আর বাংলাদেশের জন্য নির্ভরযোগ্য সংখ্যা আমার জানা নেই। যেকোনো বড় পাঠকগোষ্ঠীতে এমন মানুষ থাকবেনই। সবচেয়ে সাধারণ ধরন deuteranopia/deuteranomaly, যেখানে সবুজ শনাক্তকারী cone কাজ করে না বা দুর্বল।

নিচের ব্লক দুটি কাজ করে। প্রথমে jet, viridis আর cividis-এর ৯টি বিন্দুতে CIE L* (চোখে অনুভূত উজ্জ্বলতা, ০ কালো, ১০০ সাদা) হিসাব করে দেখায় কোনটি একমুখী। তারপর একটি লাল-সবুজ bar chart আঁকে, সেটিকে NumPy array-তে নেয় (পর্ব ১৮-এ দেখেছেন ছবি মানেই array), আর প্রতিটি pixel-এ একটি ৩×৩ matrix গুণ করে deuteranopia simulate করে। Matrix-টি Gustavo Machado, Manuel Oliveira আর Leandro Fernandes-এর ২০০৯ সালের প্রবন্ধ “A Physiologically-based Model for Simulation of Color Vision Deficiency” (IEEE TVCG) থেকে, severity 1.0; সংখ্যাগুলো colour-science library-র dataset-এর সঙ্গে মিলিয়ে নেওয়া। গুণটি sRGB থেকে linear RGB-তে নিয়ে করা হয়েছে। এটি একটি আনুমানিক simulation: আলাদা মানুষের দৃষ্টি আলাদা, আর পর্দার রং-ও এক নয়।

import io
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import colormaps
from matplotlib.colors import to_rgb
def lin(c):  # sRGB (0..1) থেকে linear RGB
    return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)
def enc(c):  # linear RGB থেকে আবার sRGB
    c = np.clip(c, 0, 1)
    return np.where(c <= 0.0031308, 12.92 * c, 1.055 * c ** (1 / 2.4) - 0.055)
def lstar(rgb):  # CIE L*: চোখে কতটা উজ্জ্বল (0 কালো, 100 সাদা)
    Y = lin(rgb) @ np.array([0.2126, 0.7152, 0.0722])
    return np.where(Y > 0.008856, 116 * np.cbrt(Y) - 16, 903.3 * Y)

for name in ["jet", "viridis", "cividis"]:
    L = lstar(colormaps[name](np.linspace(0, 1, 9))[:, :3])
    print(f"{name:8}", L.round().astype(int), "একমুখী:", bool(np.all(np.diff(L) > 0)))
# Machado et al. (2009), deuteranopia, severity 1.0; linear RGB-তে প্রয়োগ
DEUT = np.array([[0.367322, 0.860646, -0.227968],
                 [0.280085, 0.672501, 0.047413],
                 [-0.011820, 0.042940, 0.968881]])
rg = np.array([to_rgb("tab:red"), to_rgb("tab:green")])
print("L* আগে:", lstar(rg).round(1), "পরে:", lstar(enc(lin(rg) @ DEUT.T)).round(1))
f, a = plt.subplots(figsize=(3, 2.4), dpi=100)
a.bar(range(3), [70, 64, 58], color=["tab:red", "tab:green", "tab:red"])
a.axis("off")                       # লেখা ছাড়া শুধু bar; শিরোনাম পরের ছবিতে
buf = io.BytesIO(); f.savefig(buf, format="png"); buf.seek(0); plt.close(f)
chart = plt.imread(buf)[..., :3]                    # chart থেকে pixel array, 0..1
y, x = np.mgrid[0:1:60j, 0:1:60j]
z = np.exp(-((x - .3)**2 + (y - .6)**2) / .04) + .6 * np.exp(-((x - .7)**2 + (y - .3)**2) / .02)
fig, axs = plt.subplots(2, 2, figsize=(6, 4.6), dpi=100)
for ax, img, t in [(axs[0, 0], z, "jet"), (axs[0, 1], z, "viridis"),
                   (axs[1, 0], chart, "Red = late, green = on time"), (axs[1, 1], enc(lin(chart) @ DEUT.T), "Deuteranopia (sim.)")]:
    ax.imshow(img, cmap=t if img is z else None); ax.set_title(t, fontsize=10); ax.axis("off")
fig.tight_layout(); plt.show()

আসল আউটপুট (ব্রাউজারের Python, matplotlib 3.10.8), সঙ্গে চার প্যানেলের একটি ছবি:

jet      [13 32 55 90 90 95 71 54 25] একমুখী: False
viridis  [15 26 36 45 54 64 73 82 91] একমুখী: True
cividis  [14 24 34 43 52 61 71 81 91] একমুখী: True
L* আগে: [46.8 57.9] পরে: [52.  56.4]

jet-এর উজ্জ্বলতা ১৩ থেকে ৯৫-এ উঠে আবার ২৫-এ নামে; তাই jet-এর ছবিতে ডান দিকের ছোট চূড়াটির চারপাশে উজ্জ্বল হলুদ-cyan বলয় দেখা যায়, যেন সেখানে আলাদা কোনো সীমা আছে। viridis-এ উজ্জ্বলতা একমুখী, তাই উজ্জ্বল মানেই বড় মান। লাল আর সবুজের উজ্জ্বলতা simulation-এর পরে প্রায় সমান (৫২ আর ৫৬.৪), রং দুটিও প্রায় একই জলপাই-বাদামি: নিচের ডান প্যানেলে “দেরি” আর “সময়মতো”-র পার্থক্য প্রায় হারিয়ে যায়। ছোট লাল-সবুজ chart-টিতে ইচ্ছা করে কোনো লেখা রাখা হয়নি: লেখকের পরীক্ষায় ব্রাউজারের matplotlib-এ নিজের কোড থেকে লেখাওয়ালা figure savefig করলে একটি deprecation warning আসছিল, তাই নাম আর শিরোনাম বসানো হয়েছে চার প্যানেলের ছবিতে। এই কোড NumPy-র broadcasting আর matrix গুণ (@) ছাড়া কিছুই ব্যবহার করেনি; matrix multiplication-এর ধারণা পর্ব ১৭-এ দেখেছেন।

কখনো শুধু লাল বনাম সবুজ নয়। “লাল মানে খারাপ, সবুজ মানে ভালো” ঐতিহ্যটি রঙকানা দৃষ্টির মানুষের জন্য প্রায়ই অর্থহীন। বিকল্প: নীল বনাম কমলা (tab10-এর প্রথম দুটি রং), উজ্জ্বলতায় স্পষ্ট পার্থক্য, আকৃতি বা নকশা (marker, দাগের ধরন), আর সবচেয়ে ভালো হলো সরাসরি লেখা: bar-এর পাশে “দেরি” বা “সময়মতো”.

৮ · সবার জন্য chart — alt text, লেখায় সারাংশ, contrast আর লেখার আকার

একটি chart যিনি দেখতে পান না (screen reader ব্যবহারকারী), যিনি কম আলোয় ফোনে দেখছেন, বা যিনি সাদা-কালো প্রিন্ট পেয়েছেন, তাঁদের কাছেও বার্তাটি পৌঁছানো উচিত। ওয়েবের accessibility-র আন্তর্জাতিক নির্দেশিকা WCAG (W3C-র Web Content Accessibility Guidelines) chart-এর জন্য সরাসরি কয়েকটি কাজের নিয়ম দেয়।

  • Alt text: ছবির বিকল্প লেখা, যা screen reader পড়ে শোনায়। “Bar chart” যথেষ্ট নয়; লিখুন chart-এর ধরন, কী মাপা হয়েছে, আর মূল বার্তা: “জেলাভিত্তিক মোট বিক্রির bar chart; ঢাকা মোট বিক্রির ৩৯% আনে, বাকি চার জেলা ৯১ থেকে ২৩৮ হাজার টাকা।” HTML-এ <img alt="...">; জটিল chart-এ alt ছোট রেখে নিচে বিস্তারিত লেখা দিন।
  • লেখায় সারাংশ আর টেবিল: chart-এর নিচে এক-দুই বাক্যে মূল কথা, আর আসল সংখ্যার একটি টেবিল বা ডাউনলোড করা যায় এমন CSV. এতে যাচাইও সহজ হয়।
  • Contrast ratio: WCAG 2.1 অনুযায়ী সাধারণ লেখায় অন্তত ৪.৫:১, বড় লেখায় ৩:১, আর chart-এর দাগের মতো জরুরি গ্রাফিক অংশে অন্তত ৩:১. সাদা পটভূমিতে হালকা ধূসর #999999 মাত্র প্রায় ২.৮৫:১ (ব্যর্থ), #767676 প্রায় ৪.৫৪:১ (সাধারণ লেখায় সবে পাস)। টিক লেবেল প্রায়ই এই ভুলে পড়ে।
  • লেখার আকার: ফোনে chart সংকুচিত হয়। matplotlib-এ ছোট figsize আর যথেষ্ট fontsize (অন্তত ৯–১০ pt) রাখুন, বিশাল figure ছোট করে দেখাবেন না।
  • Direct labeling: রেখা বা bar-এর পাশেই নাম লিখুন। legend থাকলে চোখকে রং মিলিয়ে বারবার আসা-যাওয়া করতে হয়, আর রঙকানা দৃষ্টিতে মেলানো আরও কঠিন।

ডাক্তার যখন রোগীকে রিপোর্ট বোঝান, শুধু X-ray তুলে ধরেন না; বলেন “ডান ফুসফুসের নিচে এই ছায়াটা দেখুন, এটাই সংক্রমণ।” Alt text আর লেখায় সারাংশ সেই কথাটুকু, যা ছবি না দেখেও রোগীর জানা দরকার।

৯ · গল্প বলা — এক বার্তা, action title আর বাড়তি সাজ বাদ

বিশ্লেষণের সময় আপনি নিজের জন্য chart আঁকেন, শত শত; উপস্থাপনার সময় আঁকেন অন্যের জন্য, অল্প কয়েকটি। দ্বিতীয় ধরনের chart-এর কাজ একটি বার্তা পৌঁছানো। এর কয়েকটি পরিচিত কৌশল:

  • এক chart, এক বার্তা। পাঁচটি কথা বলতে চাইলে পাঁচটি chart, বা small multiples.
  • Action title. শিরোনাম বিষয় নয়, সিদ্ধান্ত: “Sales by district”-এর বদলে “ঢাকা থেকে আসে মোট বিক্রির ৩৯%”. শিরোনামের দাবি অবশ্যই ডেটা থেকে হিসাব করা হতে হবে।
  • Annotation. মূল বিন্দুতে তীর বা ছোট লেখা: “এই মাসে ঈদের ছুটি”, “নতুন কুরিয়ার শুরু”.
  • ক্রম পাঠকের জন্য। দলের নাম বর্ণানুক্রমে নয়, মান অনুযায়ী সাজান; সময় থাকলে সময়ের ক্রমে।
  • Chartjunk বাদ। ভারী grid, ছায়া, 3-D, অপ্রয়োজনীয় রং।

শেষেরটি Tufte-এর data-ink ratio ভাবনা থেকে: chart-এর কালির যত অংশ ডেটা দেখায়, ততই ভালো; ডেটা না দেখানো কালি যতটা সম্ভব মুছুন। তবে এখানে বিতর্ক আছে (ভাবনার প্রশ্ন ২): কিছু গবেষণা দেখিয়েছে সাজানো chart মনে বেশি থাকে, তাই নীতিটিকে অন্ধ নিয়ম নয়, পরিমিতির নির্দেশনা হিসেবে নিন। নিচের stepper-এ একটি default chart ধাপে ধাপে পরিষ্কার করা হয়েছে।

Chart পরিষ্কার করা চট্টগ্রাম ঢাকা খুলনা রাজশাহী সিলেট 37623813112491 ঢাকাচট্টগ্রামখুলনারাজশাহীসিলেট চট্টগ্রাম ঢাকা খুলনা রাজশাহী সিলেট বাকি চার জেলা মিলে 61% মান: হাজার টাকা, 600টি কাল্পনিক অর্ডার
  1. Default chart. পাঁচ জেলা, পাঁচটি রং, একটি legend, ভারী grid, বাক্স, বাঁকা লেবেল আর বর্ণানুক্রম। পাঠককে রং মিলিয়ে legend থেকে নাম খুঁজতে হয়; শিরোনাম শুধু বিষয় বলে, বার্তা নয়।
  2. Grid আর বাক্স বাদ। এগুলো ডেটা দেখায় না (data-ink নয়)। শুধু নিচের baseline রাখা হলো, কারণ bar শূন্য থেকে শুরু হচ্ছে তা বোঝাতে ওটা দরকার।
  3. ক্রম বদল। বর্ণানুক্রমের বদলে বড় থেকে ছোট। এখন ঢাকা, চট্টগ্রাম, খুলনা, রাজশাহী, সিলেট; তুলনা এক নজরে।
  4. এক highlight আর সরাসরি মান। বার্তা ঢাকাকে নিয়ে, তাই শুধু ঢাকা নীল, বাকিরা ধূসর। প্রতিটি bar-এর ওপর মান (৩৭৬, ২৩৮, ১৩১, ১২৪, ৯১ হাজার টাকা) লেখা, তাই legend আর y-axis-এর সংখ্যার দরকার নেই। নামগুলো সোজা করে লেখা।
  5. Action title আর annotation. শিরোনাম এখন সিদ্ধান্ত: ঢাকা থেকে মোট বিক্রির ৩৯%. হিসাব: 376 ÷ (376 + 238 + 131 + 124 + 91) = 376 ÷ 960 ≈ 0.39. Annotation-এ বাকি অংশ (৬১%) আর একক ও উৎস লেখা। পাঠক তিন সেকেন্ডে মূল কথা পান।

এবার একই কাজ matplotlib-এ। বাঁ প্যানেল ইচ্ছা করে জঞ্জালভরা, ডান প্যানেল পরিষ্কার। খেয়াল করুন, action title-এর সংখ্যাটি হাতে লেখা নয়, ডেটা থেকে f-string দিয়ে বসানো; ডেটা বদলালে শিরোনামও নিজে থেকে ঠিক থাকবে।

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

rng = np.random.default_rng(42)                     # বাজারঘরের কাল্পনিক অর্ডার
dist = ["Dhaka", "Chattogram", "Khulna", "Rajshahi", "Sylhet"]
df = pd.DataFrame({"district": rng.choice(dist, 600, p=[.4, .25, .13, .12, .1]),
                   "price_paisa": rng.integers(20_000, 300_000, 600)})
tk = (df.groupby("district")["price_paisa"].sum() / 100_000).round(1)  # হাজার টাকা
print(tk.to_dict())

fig, (a, b) = plt.subplots(1, 2, figsize=(7.6, 3), dpi=100)
a.bar(tk.index, tk.values, color=plt.cm.tab10(range(5)), label=list(tk.index))
a.legend(fontsize=6); a.grid(True, lw=1); a.set_title("Sales by district")
a.tick_params(axis="x", rotation=45)

s = tk.sort_values()                                 # ক্রম: পাঠকের সুবিধায়
top = s.index[-1]
share = s[top] / s.sum()
cols = ["#1f77b4" if d == top else "#b8c2cc" for d in s.index]
b.barh(s.index, s.values, color=cols)
for y, v in enumerate(s.values):
    b.text(v + 5, y, f"{v:.0f}", va="center", fontsize=9)   # সরাসরি label
for side in ["top", "right", "bottom"]:
    b.spines[side].set_visible(False)
b.set_xticks([]); b.set_xlim(0, s.max() * 1.2)
title = f"{top} brings {share:.0%} of sales"
b.set_title(title, loc="left", fontsize=11, fontweight="bold")
b.set_xlabel("Sales, thousand Tk (600 invented orders)")
print("action title:", title)
fig.tight_layout()
plt.show()

আসল আউটপুট (ব্রাউজারের Python, pandas 3.0.2), সঙ্গে দুই প্যানেলের একটি ছবি:

{'Chattogram': 238.3, 'Dhaka': 375.9, 'Khulna': 131.3, 'Rajshahi': 123.6, 'Sylhet': 91.1}
action title: Dhaka brings 39% of sales

groupby (পর্ব ২৪) জেলাগুলো বর্ণানুক্রমে দেয়, আর বাঁ প্যানেল সেই ক্রমেই আঁকা। ডান প্যানেলে sort_values দিয়ে ক্রম বদলেছে, barh-এ নামগুলো সোজা পড়া যায়, শুধু ঢাকা নীল, মান সরাসরি লেখা, আর অপ্রয়োজনীয় তিনটি spine (chart-এর চারপাশের রেখা) বাদ। শিরোনাম ৩৯% হিসাব করা 375.9 ÷ 960.2 থেকে, যা stepper-এর পূর্ণসংখ্যার হিসাবের সঙ্গে মেলে। ধূসর #b8c2cc-এর contrast সাদায় মাত্র প্রায় ১.৮:১; তাই এই ধূসর শুধু bar ভরাট করতে ব্যবহার করুন, লেখার রং হিসেবে নয়।

সৎ chart-এর চেকলিস্ট

১০ · AI-এর যুগে — AI-এর বানানো chart আর সারাংশ যাচাই

এখন অনেকেই CSV ফাইল AI assistant-এ দিয়ে বলেন “একটা chart বানিয়ে সারাংশ লেখো”. ফলাফল প্রায়ই সুন্দর আর আত্মবিশ্বাসী, কিন্তু সুন্দর মানেই সঠিক নয়। AI-এর লেখা সারাংশে ভুল সংখ্যা, না-থাকা প্রবণতা বা উল্টো তুলনা আসতে পারে (hallucination), আর সুন্দর ছবি দেখে আমরা যাচাই না করেই মেনে নিতে চাই (automation bias). এই পর্বের প্রতিটি ফাঁদ AI-এর বানানো chart-এও থাকতে পারে।

AI যা প্রায়ই করেকীভাবে ধরবেন
“পার্থক্য স্পষ্ট করতে” set_ylim দিয়ে bar-এর axis কেটে দেয়কোডে ylim খুঁজুন; lie factor হিসাব করুন
সারাংশে “বিক্রি তিন গুণ” লেখে, অথচ ডেটায় ৩০% বৃদ্ধিনিজে groupby বা দুটি সংখ্যা ভাগ করে মিলিয়ে দেখুন
দুটি series-এর জন্য twinx দিয়ে dual axis বানায়, তারপর “সম্পর্ক” দাবি করেআলাদা প্যানেলে আঁকুন; পরিবর্তনের correlation দেখুন
পুরোনো API দেয়, যেমন plt.cm.get_cmap (matplotlib 3.9-এ সরানো) বা seaborn-whitegrid style-এর পুরোনো নামনিজের version-এর অফিসিয়াল docs দেখুন: matplotlib.colormaps[...]
jet বা লাল-সবুজ palette বেছে নেয়ডেটার ধরন দেখে palette; simulation চালান
ভুল কলাম বা ভুল একক (পয়সাকে টাকা) plot করেকয়েকটি সারি head() দিয়ে দেখুন; axis label-এর একক মেলান
warning লুকিয়ে ফেলে বা ছবিতে বাংলা ভাঙা বাক্স রেখে দেয়পুরো আউটপুট পড়ুন; ছবি নিজে খুলে দেখুন

AI-কে chart বানাতে দিলে সততার শর্তগুলো আগেই বলে দিন, আর তাকে দিয়ে নিজের দাবির হিসাব ছাপিয়ে নিন, যাতে আপনি মেলাতে পারেন। আসল গ্রাহকের নাম, ফোন নম্বর বা গোপন ডেটা কখনো পেস্ট করবেন না; কয়েকটি কাল্পনিক সারি বা শুধু কলামের নাম ও dtype দিন।

আমার কাছে orders.csv আছে: কলাম district, date, price_paisa (পূর্ণসংখ্যা, পয়সায়)। matplotlib 3.10 দিয়ে জেলাভিত্তিক মোট বিক্রির একটি bar chart-এর Python কোড লেখো। শর্ত: ১. y-axis শূন্য থেকে; set_ylim দিয়ে কাটবে না। টাকায় দেখাবে (÷100) আর axis-এ একক লিখবে। ২. বড় থেকে ছোট ক্রমে সাজাবে, একটি মাত্র রং, শুধু সবচেয়ে বড় জেলাটি highlight, সরাসরি মান লেখা, legend থাকবে না। ৩. শিরোনাম হবে action title, যার সংখ্যা কোড ডেটা থেকে হিসাব করবে; হাতে লিখবে না। ৪. শিরোনামের দাবির পেছনের সংখ্যাগুলো print করবে, যাতে আমি মিলিয়ে দেখতে পারি। ৫. ছবির সব লেখা ইংরেজিতে। কোনো deprecated API ব্যবহার করবে না, আর কোন অংশ কেন, এক লাইনে বলবে।

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

আরও গভীরে: রঙকানা দৃষ্টির simulation-এর কয়েকটি পদ্ধতি আছে: Brettel, Viénot ও Mollon (১৯৯৭), Viénot, Brettel ও Mollon (১৯৯৯) আর Machado ও সহকর্মীরা (২০০৯)। DaltonLens প্রকল্পের একটি তুলনামূলক পর্যালোচনা বলে, সম্পূর্ণ dichromacy-র জন্য Viénot বা Brettel বেশি নির্ভরযোগ্য হতে পারে, আর Machado আংশিক (anomalous) ধরনের জন্য বেশি নীতিসম্মত; তাই এখানের ছবিকে আনুমানিক ধরুন। viridis নকশা করা হয়েছিল CAM02-UCS নামের একটি উপলব্ধি-সমান রঙের জগতে, যেখানে দুটি রঙের দূরত্ব মোটামুটি চোখের অনুভূত পার্থক্যের সমান। Python-এ colorspacious আর daltonlens package দিয়ে এগুলো নিজে পরীক্ষা করা যায়।

পরীক্ষা ও ইন্টারভিউতে:

  • Lie factor কী? ছবিতে দেখানো আপেক্ষিক পরিবর্তন ÷ ডেটার আপেক্ষিক পরিবর্তন; ১ মানে সৎ।
  • Bar chart-এর axis শূন্য থেকে কেন, line chart-এ কেন বাধ্যতামূলক নয়? Bar-এ দৈর্ঘ্যই মান; line-এ চোখ অবস্থান ও ঢাল মাপে।
  • jet-এর বদলে viridis কেন? viridis-এর উজ্জ্বলতা একমুখী ও perceptually uniform; jet নকল band তৈরি করে।
  • Dual axis-এর সমস্যা? দুই scale স্বাধীন, তাই যেকোনো দুই রেখাকে মেলানো যায়; আলাদা প্যানেল বা index করা ভালো।
  • Error bar দেখলে প্রথম প্রশ্ন? এটি SD, SE নাকি confidence interval, আর প্রতিটি দলের n কত।

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

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

প্র ০১Huff-এর ১৯৫৪ সালের বই আজও পড়ানো হয়। কিন্তু লেখকের নিজের ইতিহাস থেকে কী শেখার আছে?

How to Lie with Statistics টিকে আছে কারণ এর কৌশলগুলো প্রযুক্তি বদলালেও বদলায়নি। কাটা axis, এক মাত্রার মান দুই মাত্রার ছবিতে আঁকা, “গড়” শব্দের আড়ালে mean আর median গুলিয়ে ফেলা, ছোট নমুনা থেকে বড় দাবি; ১৯৫০-এর দশকের পত্রিকার chart আর আজকের সামাজিক মাধ্যমের infographic-এ একই ভুল দেখা যায়। Huff পাঠককে শেখান পাঁচটি প্রশ্ন করতে: কে বলছে, কীভাবে জানল, কী বাদ পড়েছে, কেউ কি বিষয় বদলে দিয়েছে, আর কথাটি কি যুক্তিসঙ্গত। এই প্রশ্নগুলো আজ AI-এর বানানো সারাংশের ক্ষেত্রেও খাটে।

লেখকের নিজের গল্পটি আরও শিক্ষণীয়। পরিসংখ্যানবিদ Alex Reinhart-সহ কয়েকজন গবেষক পরে (২০১০-এর দশকে) জানান যে Huff ১৯৬০-এর দশকে তামাক শিল্পের অর্থায়নে মার্কিন সিনেটে সাক্ষ্য দিয়েছিলেন, আর ধূমপান ও ক্যানসারের সম্পর্কের পরিসংখ্যানকে সন্দেহজনক দেখাতে How to Lie with Smoking Statistics নামে একটি পাণ্ডুলিপি লিখেছিলেন, যা প্রকাশিত হয়নি বলে জানা যায়। অর্থাৎ যে মানুষটি পরিসংখ্যানের অপব্যবহার চেনাতে বই লিখেছিলেন, তিনিই সেই দক্ষতা ব্যবহার করেছেন সত্য প্রমাণকে সন্দেহে ফেলতে।

এখান থেকে দুটি শিক্ষা। প্রথমত, সন্দেহ করার দক্ষতা নিরপেক্ষ নয়: যিনি “সব পরিসংখ্যানই মিথ্যা” বলে সব প্রমাণ উড়িয়ে দেন, তিনিও এক রকম প্রতারণা করছেন। সঠিক অবস্থান হলো নির্দিষ্ট প্রশ্ন করা (নমুনা কত বড়, কোন তুলনা, কোন axis), আর উত্তর পেলে প্রমাণ মেনে নেওয়া। দ্বিতীয়ত, কে অর্থ দিচ্ছে আর কার স্বার্থ জড়িত, তা জানা chart পড়ার অংশ। একটি সুন্দর chart-এর উৎস আর অর্থায়ন জানা ডেটার উৎস জানার মতোই জরুরি।

মূল উপলব্ধি: chart-এর কৌশল চেনা দরকার বিভ্রান্তি এড়াতে, সব প্রমাণ অবিশ্বাস করতে নয়; সৎ সন্দেহ নির্দিষ্ট প্রশ্ন করে আর উত্তর পেলে থামে।

প্র ০২Tufte বলেন chartjunk মুছে ফেলুন। কিন্তু সাজানো chart কি সবসময় খারাপ?

Tufte-এর data-ink ratio ভাবনা (১৯৮৩) প্রভাবশালী: ছায়া, ভারী grid, অলংকার আর 3-D যতটা সম্ভব বাদ দিয়ে কালি রাখুন শুধু ডেটার জন্য। বিশ্লেষণের chart আর বৈজ্ঞানিক প্রবন্ধে এই নীতি প্রায় সবসময় কাজের, কারণ পাঠক নির্ভুলভাবে তুলনা করতে চান।

তবে এটি অভিজ্ঞতামূলক প্রমাণ থেকে আসা নিয়ম নয়, মূলত নকশার একটি দর্শন। ২০১০ সালে Scott Bateman ও সহকর্মীরা CHI সম্মেলনে “Useful Junk?” নামে একটি গবেষণা প্রকাশ করেন: তাঁরা Nigel Holmes-এর আঁকা ছবিসহ সাজানো chart আর সাদামাটা chart তুলনা করে দেখেন, তাৎক্ষণিক পড়ার নির্ভুলতায় বড় পার্থক্য নেই, কিন্তু কয়েক সপ্তাহ পরে সাজানো chart-এর বিষয় অংশগ্রহণকারীরা বেশি মনে রাখতে পেরেছিলেন। গবেষণাটি ছোট ছিল আর পরে এ নিয়ে আরও আলোচনা হয়েছে, তাই একে চূড়ান্ত রায় ধরবেন না। Michelle Borkin ও সহকর্মীদের ২০১৩ সালের একটি গবেষণাও মনে রাখার যোগ্যতা নিয়ে কাজ করে, যেখানে মানুষের মতো দেখতে বস্তু বা রঙিন ছবি মনে থাকার সঙ্গে যুক্ত পাওয়া যায়।

এর মানে, প্রসঙ্গ গুরুত্বপূর্ণ। একটি অফিস রিপোর্টে বা model-এর মূল্যায়নে, যেখানে পাঠক সংখ্যা মেলাবেন, সেখানে পরিচ্ছন্নতা জরুরি। একটি জনসচেতনতামূলক পোস্টারে বা সংবাদপত্রের infographic-এ, যেখানে মানুষ মাত্র কয়েক সেকেন্ড দেখবেন আর মনে রাখতে হবে, সেখানে একটি প্রাসঙ্গিক ছবি বা icon কাজে দিতে পারে, যদি তা ডেটাকে বিকৃত না করে। সীমারেখাটি পরিষ্কার: সাজ যদি আকার, ক্ষেত্রফল বা ক্রম বদলে দেয় (যেমন 3-D pie, বড় করা icon), তখন তা আর নিছক সাজ নয়, বিকৃতি।

মূল উপলব্ধি: data-ink নীতি একটি ভালো শুরু, অন্ধ নিয়ম নয়; প্রশ্ন হলো সাজটি পাঠককে সাহায্য করছে, না ডেটার অনুপাত বদলে দিচ্ছে।

প্র ০৩Rainbow colormap এত সমালোচনার পরও এত দিন default ছিল কেন, আর এখনো কেন দেখা যায়?

Rainbow (jet) palette আকর্ষণীয়: অনেক রং, উজ্জ্বল, আর “বৈজ্ঞানিক” দেখায়। ১৯৯০ ও ২০০০-এর দশকে MATLAB-এর default ছিল jet, আর matplotlib শুরুতে MATLAB-এর আচরণ অনুসরণ করত, তাই সেখানেও এটি default ছিল। Default-এর শক্তি অনেক: বেশিরভাগ মানুষ কখনো cmap বদলান না। ফলে আবহাওয়ার মানচিত্র থেকে চিকিৎসার ছবি, অনেক জায়গায় jet ছড়িয়ে পড়ে।

সমস্যাগুলো আগেই জানা ছিল। Borland আর Taylor (২০০৭) দেখান rainbow-এর উজ্জ্বলতা একমুখী নয়, তাই নকল সীমারেখা দেখা দেয় আর ক্রম বোঝা কঠিন হয় (হলুদ কি সবুজের চেয়ে বড়?)। ২০১১ সালে Michelle Borkin ও সহকর্মীরা হৃদ্‌রোগ নির্ণয়ে ধমনীর ছবি নিয়ে একটি গবেষণায় দেখান, rainbow-এর বদলে diverging palette ব্যবহার করলে চিকিৎসকেরা সমস্যার জায়গা বেশি নির্ভুলভাবে চিহ্নিত করেছেন; বিস্তারিত সংখ্যা মূল প্রবন্ধে দেখে নিন। অর্থাৎ ভুল palette শুধু সৌন্দর্যের প্রশ্ন নয়, সিদ্ধান্তের মান কমাতে পারে।

পরিবর্তন এসেছে ধীরে। MATLAB ২০১৪ সালে (R2014b) default বদলে parula করে, আর matplotlib ২০১৭ সালে version 2.0-তে viridis করে; viridis নির্বাচনের আলোচনা SciPy 2015 সম্মেলনে উপস্থাপিত হয়েছিল। তবু jet এখনো দেখা যায়: পুরোনো কোড, পুরোনো টিউটোরিয়াল, আর সেগুলো থেকে শেখা AI মডেল প্রায়ই cmap="jet" লেখে। কিছু ক্ষেত্রে, যেমন নির্দিষ্ট মান খুঁজে বের করা (মানচিত্রে নির্দিষ্ট উচ্চতা), বহু-রঙের palette-এর পক্ষে যুক্তিও আছে; সেখানে perceptually উন্নত rainbow বিকল্প যেমন turbo (Google, ২০১৯) প্রস্তাব করা হয়েছে, যদিও তারও একমুখী উজ্জ্বলতা নেই।

মূল উপলব্ধি: default-ই সবচেয়ে প্রভাবশালী নকশার সিদ্ধান্ত; ভালো default লক্ষ মানুষের chart ঠিক করে, তাই নিজের কোড, template বা টিমের style guide-এ default বাছাই দায়িত্ব নিয়ে করুন।

প্র ০৪Log scale কি সৎ নাকি বিভ্রান্তিকর? দ্রুত বাড়তে থাকা সংখ্যা কীভাবে দেখাবেন?

Log scale-এ axis-এর সমান দূরত্ব মানে সমান গুণ, সমান যোগ নয়: ১ থেকে ১০, ১০ থেকে ১০০, ১০০ থেকে ১০০০ সমান দূরত্বে। দ্রুত বাড়তে থাকা বা অনেক মাত্রায় ছড়ানো ডেটার (যেমন পণ্যের দাম ২০ টাকা থেকে ২ লাখ টাকা) জন্য এটি খুব কাজের। Exponential বৃদ্ধি log scale-এ সোজা রেখা হয়, আর রেখার ঢাল থেকে বৃদ্ধির হার পড়া যায়: ঢাল কমা মানে বৃদ্ধি ধীর হচ্ছে, যদিও সংখ্যা বাড়ছে।

সমস্যা হলো সাধারণ পাঠক। ২০২০ সালে COVID-19 মহামারির সময় সংবাদমাধ্যমে আক্রান্তের সংখ্যা প্রায়ই log scale-এ দেখানো হয়েছিল। Alessandro Romano ও সহকর্মীদের ২০২০ সালের একটি গবেষণায় (Health Economics জার্নাল) দেখা যায়, log scale-এর chart দেখা অংশগ্রহণকারীরা linear chart দেখা অংশগ্রহণকারীদের তুলনায় প্রবণতা বুঝতে ও ভবিষ্যৎ অনুমানে বেশি ভুল করেছেন। log scale-এ বাঁকা হয়ে আসা রেখা অনেকের কাছে “সমস্যা থেমে যাচ্ছে” মনে হয়েছে, অথচ সংখ্যা তখনও দ্রুত বাড়ছিল।

তাই log scale নিজে অসৎ নয়, কিন্তু পাঠক না জানলে বিভ্রান্তিকর। সৎ ব্যবহারের নিয়ম: axis-এ স্পষ্ট লিখুন “log scale”, টিক লেবেলে আসল মান দিন (১০, ১০০, ১০০০; 102 নয়), সম্ভব হলে linear আর log দুটো পাশাপাশি দিন, আর প্রশ্নটা কী তা ভাবুন: “কত গুণ বাড়ছে” প্রশ্নে log, “আজ কতজন” প্রশ্নে linear. matplotlib-এ ax.set_yscale("log"); শূন্য বা ঋণাত্মক মান থাকলে log চলে না, তখন "symlog" বা ডেটা রূপান্তর লাগে, আর সেটাও chart-এ লিখতে হয়।

মূল উপলব্ধি: scale নির্বাচন প্রশ্ন আর পাঠক দুটোর ওপর নির্ভর করে; বিশেষজ্ঞের জন্য যা সবচেয়ে তথ্যপূর্ণ, সাধারণ পাঠকের জন্য তা ভুল বার্তা দিতে পারে, তাই লেবেল আর ব্যাখ্যা chart-এর অংশ।

প্র ০৫AI চোখের পলকে chart আর সারাংশ বানায়। তাহলে মানুষের দক্ষতা হিসেবে visualization-এর কী থাকে?

AI assistant এখন CSV পড়ে কোড লেখে, chart আঁকে আর সারাংশ লেখে। এটি সত্যিই সময় বাঁচায়; বিশেষ করে বিশ্লেষণের শুরুতে দশটি দ্রুত chart দেখে নেওয়ায়। কিন্তু চার জায়গায় মানুষের বিচার এখনো দরকার।

  • প্রশ্ন ঠিক করা: মোট নাকি প্রতি ক্রেতা, গড় নাকি distribution, কোন সময়ের জানালা; এগুলো ব্যবসার প্রশ্ন থেকে আসে, ডেটা থেকে নয়। AI যুক্তিসঙ্গত একটি পছন্দ করবে, কিন্তু আপনার প্রশ্নের সঙ্গে মিলবে কি না, তা আপনাকেই দেখতে হবে।
  • সংখ্যা যাচাই: ভাষা মডেল লেখা তৈরি করে, হিসাব নয়; কোড না চালিয়ে লেখা সারাংশে ভুল সংখ্যা আসতে পারে। কোড চালালেও ভুল কলাম, ভুল একক বা ভুল filter থাকতে পারে। প্রতিটি দাবি ডেটা থেকে মেলানোর অভ্যাস তাই মৌলিক।
  • সততার সিদ্ধান্ত: “পার্থক্যটা নাটকীয় দেখাও” বললে AI প্রায়ই axis কেটে দেবে। কোনটা গ্রহণযোগ্য জোর আর কোনটা বিকৃতি, এই নৈতিক সিদ্ধান্ত ও তার দায় মানুষের।
  • পাঠককে চেনা: রঙকানা দৃষ্টির সহকর্মী, ফোনে দেখা ব্যবস্থাপক, screen reader ব্যবহারকারী; তাঁদের প্রয়োজন জানেন আপনি।

উল্টো দিকেও একটি সুযোগ আছে: AI-কে সমালোচক হিসেবে ব্যবহার করা। একটি chart-এর কোড দিয়ে জিজ্ঞেস করুন “এই chart-এ কোথায় বিভ্রান্তি থাকতে পারে, চেকলিস্ট ধরে বলো”; এটি নিজের ভুল খোঁজার দ্রুত দ্বিতীয় চোখ হতে পারে, তবে শেষ বিচার এখানেও আপনার। এই কোর্সের পরের অংশে model-এর মূল্যায়নের chart (confusion matrix, learning curve) আসবে, আর সেখানে একই নীতি খাটে: test set-এর ফল, সঠিক baseline আর অনিশ্চয়তা না দেখালে সবচেয়ে সুন্দর chart-ও ভুল সিদ্ধান্তে নিয়ে যায়।

মূল উপলব্ধি: AI chart আঁকার খরচ প্রায় শূন্যে নামিয়েছে, কিন্তু ঠিক প্রশ্ন, সঠিক সংখ্যা আর সৎ উপস্থাপনার দায় নামায়নি; সেটাই এখন মূল দক্ষতা।

অনুশীলন

হিসাব, কোড, সমালোচনা আর আউটপুট অনুমান; প্রতিটির উত্তর নিচে লুকানো আছে।

  1. বৃত্তের lie factor (হিসাব): একটি poster-এ বাজারঘরের দুই বছরের অর্ডার দেখানো হয়েছে বৃত্ত দিয়ে: প্রথম বছর ৫০ হাজার অর্ডার, ব্যাসার্ধ ২ সেমি; দ্বিতীয় বছর ১ লাখ অর্ডার, ব্যাসার্ধ ৪ সেমি (কাল্পনিক)। Lie factor কত? সৎ হতে দ্বিতীয় বৃত্তের ব্যাসার্ধ কত হওয়া উচিত ছিল?

    ডেটায় প্রভাব: (100000 − 50000) / 50000 = 1, অর্থাৎ ১০০%. ক্ষেত্রফল ব্যাসার্ধের বর্গের অনুপাতে বাড়ে: (4 / 2)² = 4 গুণ, তাই ছবিতে প্রভাব (4 − 1) / 1 = 3, অর্থাৎ ৩০০%. Lie factor 3 ÷ 1 = 3.

    সৎ ব্যাসার্ধে ক্ষেত্রফল দ্বিগুণ হবে: r = 2 × √2 ≈ 2.83 সেমি। আর মনে রাখুন, ঠিক ক্ষেত্রফলেও মানুষ তুলনা কম নির্ভুলভাবে করে; দুটি সংখ্যার জন্য পাশাপাশি দুটি bar আরও ভালো।

  2. কোন baseline-এ lie factor ২ (কোড): চট্টগ্রাম ৮৮% আর খুলনা ৯১%-এর bar chart-এর y-axis কোথা থেকে শুরু করলে lie factor ঠিক ২ হয়? আগে সূত্রে উত্তর বের করুন, তারপর truncated_bars.py ব্লকের তালিকায় [0, 86]-এর বদলে সেই baseline বসিয়ে যাচাই করুন।

    এই সংজ্ঞায় bar chart-এর lie factor a / (a − base), যেখানে a = 88. 88 / (88 − base) = 2 থেকে 88 − base = 44, তাই base = 44. অর্থাৎ axis অর্ধেক পথ কাটলেই পার্থক্য দ্বিগুণ দেখায়। কোডে for ax, base in zip(axs, [0, 44]): লিখে চালালে দ্বিতীয় লাইনে lie factor 2.00 আসার কথা (ডেটায় 3.4%, ছবিতে 6.8%). লক্ষ করুন, lie factor ৪৪ পেতে মাত্র ৮৬ পর্যন্ত কাটতে হয়েছিল; baseline মানের কাছে যত যায়, বিকৃতি তত দ্রুত বাড়ে।

  3. সমালোচনা ও নতুন নকশা: একটি রিপোর্টে আছে ঘোরানো 3-D pie chart: বাজারঘরের পেমেন্টের ভাগ, COD লাল, bKash সবুজ, card হলুদ, ডান পাশে legend, শিরোনাম “Payment methods”, কোথাও মোট অর্ডারের সংখ্যা নেই। কী কী সমস্যা, আর আপনি কীভাবে নতুন করে আঁকবেন?
    • 3-D আর ঘোরানো: সামনের টুকরো বড় দেখায়; কোণ ও ক্ষেত্রফলের বিচার এমনিতেই দুর্বল।
    • লাল বনাম সবুজ: রঙকানা দৃষ্টিতে COD আর bKash প্রায় এক রঙের দেখাতে পারে।
    • Legend: রং মিলিয়ে নাম খুঁজতে হয়।
    • শিরোনাম: বিষয় বলে, বার্তা নয়; মোট n নেই, তাই ১০০টি নাকি ১ লাখ অর্ডার, বোঝা যায় না।

    নতুন নকশা: বড় থেকে ছোট ক্রমে একটি horizontal bar chart (শতাংশ, শূন্য থেকে), প্রতিটি bar-এর পাশে শতাংশ ও সংখ্যা, একটি রং আর মূল দলটি highlight, action title যেমন “অর্ধেকের বেশি অর্ডার এখনো COD-তে” (যদি ডেটা তা-ই বলে), নিচে “n = মোট অর্ডার, সময়কাল” আর alt text. দুই-তিনটি ভাগ হলে pie চলতে পারে, তবে 2-D, সরাসরি লেবেলসহ।

  4. আউটপুট অনুমান: নিচের কোড কী ছাপবে, আর এই দুই লাইন থেকে কোন chart-এর ফাঁদ বোঝা যায়?
    import numpy as np
    monthly = np.array([50, 48, 45, 41])
    print(np.cumsum(monthly))
    print(np.diff(monthly))
    [ 50  98 143 184]
    [-2 -3 -4]

    cumsum চলমান যোগফল: ৫০, ৫০ + ৪৮ = ৯৮, ৯৮ + ৪৫ = ১৪৩, ১৪৩ + ৪১ = ১৮৪. NumPy পুরো array-র জন্য একই প্রস্থে সংখ্যা ছাপে, তাই ৫০-এর আগে ফাঁকা জায়গা আসে। diff পাশাপাশি দুটি মানের পার্থক্য: −২, −৩, −৪; প্রতি মাসে বিক্রি কমছে, আর কমার গতিও বাড়ছে। Cumulative chart শুধু ওপরে ওঠা রেখা দেখাবে, পতন লুকিয়ে রাখবে; মাসিক মান বা পরিবর্তন আলাদা আঁকলে সমস্যাটি স্পষ্ট।

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

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

ক্রমএকটি সৎ, সবার পড়ার মতো chart বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. একটি প্রশ্ন আর একটি বার্তা ঠিক করা
  2. প্রশ্ন অনুযায়ী মাপ বাছাই (মোট, হার বা distribution) আর chart-এর ধরন ঠিক করা
  3. সৎ axis, পুরো সময়ের জানালা আর অনিশ্চয়তা (n, error bar) বসানো
  4. ডেটার ধরন মেনে palette, তারপর রঙকানা দৃষ্টিতে যাচাই
  5. Action title, annotation আর সরাসরি label যোগ, বাড়তি সাজ বাদ
  6. সংখ্যা মিলিয়ে দেখা, তারপর alt text আর লেখায় সারাংশ লেখা

দুটি bar-এর মান ৪০ আর ৫০, আর chart-এর y-axis শুরু হয়েছে ৩০ থেকে। Tufte-এর সংজ্ঞায় (আপেক্ষিক পরিবর্তনের অনুপাত) lie factor কত?

  • ১
  • ২
  • ৪
  • ০.২৫

কোন chart-এ y-axis শূন্য থেকে শুরু না হলেও সাধারণত গ্রহণযোগ্য, যদি axis স্পষ্ট লেবেল করা থাকে?

  • পাঁচ জেলার মোট বিক্রির bar chart
  • এক সপ্তাহে রোগীর শরীরের তাপমাত্রার line chart
  • তিনটি পণ্যের বিক্রির stacked area chart
  • বৃত্তের আকার দিয়ে জেলার বিক্রি দেখানো bubble chart

একটি মানচিত্রে প্রতিটি এলাকার অর্ডারের ঘনত্ব (০ থেকে বেশি) রং দিয়ে দেখাতে হবে। কোন palette সবচেয়ে উপযুক্ত?

  • viridis-এর মতো sequential palette
  • jet (rainbow)
  • tab10-এর মতো qualitative palette
  • মাঝখানে সাদা রেখে RdBu diverging palette

একটি chart-এ বাঁ দিকের y-axis-এ মাসিক বিক্রি আর ডান দিকের y-axis-এ বিজ্ঞাপনের খরচ, আর দুটি রেখা প্রায় একসঙ্গে ওঠানামা করছে। সবচেয়ে সঠিক মন্তব্য কোনটি?

  • বিজ্ঞাপনই বিক্রি বাড়াচ্ছে, ছবিতে স্পষ্ট
  • যেখানে দুটি রেখা কাটে, সেখানে খরচ আর বিক্রি সমান
  • দুই axis-এ সংখ্যা লেখা থাকলে কোনো সমস্যা নেই
  • দুই scale ইচ্ছামতো বাছাই করা যায়; আলাদা প্যানেলে বা একই index-এ এঁকে আর পরিবর্তনের সম্পর্ক মেপে যাচাই করা উচিত

একটি জেলার গড় rating ৪.৮, এসেছে ৬টি rating থেকে; আরেক জেলার ৪.২, এসেছে ৫০০টি থেকে। একটি bar chart-এ এই দুটি দেখাতে হলে সবচেয়ে সৎ উপায় কোনটি?

  • দুটি গড় বড় করে দেখানো, যাতে পার্থক্য স্পষ্ট হয়
  • প্রতিটি bar-এ error bar দেওয়া, পাশে n লেখা, আর error bar কী বোঝায় তা caption-এ জানানো
  • y-axis ৪ থেকে শুরু করা, যাতে পার্থক্য চোখে পড়ে
  • ৬টি rating-এর জেলাটি chart থেকে বাদ দেওয়া, কিছু না জানিয়ে

একটি AI assistant বিক্রির CSV থেকে chart বানিয়ে লিখেছে: “তিন মাসে বিক্রি তিন গুণ হয়েছে।” Chart-এর y-axis শুরু ৯০০ থেকে। প্রথম কাজ কী?

  • Chart দেখে মিলিয়ে নেওয়া; bar-গুলো সত্যিই তিন গুণ লম্বা
  • AI-কে জিজ্ঞেস করা, “উত্তরটা কি নিশ্চিত?”
  • ডেটা থেকে নিজে মাসিক মোট হিসাব করে অনুপাত মেলানো, আর axis শূন্য থেকে এঁকে দেখা
  • Palette বদলে viridis করা

শব্দকোষ

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

মনে রাখুন

  1. Lie factor = ছবিতে প্রভাব ÷ ডেটায় প্রভাব; bar chart-এ a / (a − baseline), তাই bar আর area শূন্য থেকে; line chart-এ শূন্য ছাড়া axis চলে, স্পষ্ট লেবেলসহ।
  2. বাছাই করা সময়, dual axis, cumulative chart, ক্ষেত্রফল আর 3-D সঠিক সংখ্যা দিয়েও ভুল গল্প বলে; পুরো পরিসর, আলাদা প্যানেল আর মাসিক মান দেখান।
  3. গড়ের সঙ্গে distribution, মোটের সঙ্গে হার, আর প্রতিটি দলের n ও error bar; ছোট নমুনা আর শব্দমুখর প্রবণতা বেশি পড়বেন না।
  4. Palette ডেটার ধরন মেনে (qualitative, sequential, diverging); jet নয়, viridis/cividis; শুধু লাল-সবুজে অর্থ নয়; simulation, alt text আর contrast দিয়ে যাচাই।
  5. এক chart, এক বার্তা: ডেটা থেকে হিসাব করা action title, annotation, সরাসরি label; AI-এর বানানো chart-এর সংখ্যা ও axis সবসময় নিজে মেলান।

পরের পর্বে: মডিউল ৬ শুরু: AI-এর গণিতের অন্তর্দৃষ্টি। প্রথমে vector আর similarity: dot product, cosine similarity, আর কেন embedding মানে অর্থকে সংখ্যার তালিকায় রাখা।

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

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