Matplotlib: בחירת גרף וקריאת נתונים#
במחברת זו נשתמש ב־Matplotlib כדי להפוך נתונים מספריים לגרפים שמספרים סיפור מדעי. המטרה אינה להכיר את כל סוגי הגרפים בספרייה, אלא לתרגל כמה בחירות נפוצות בכימיה:
גרף קווי כאשר לציר ה־\(x\) יש סדר טבעי, כמו אורך גל או זמן.
גרף פיזור כאשר הנקודות הן מדידות נפרדות.
פסי שגיאה כאשר כל נקודה מסכמת כמה מדידות.
צבעים כאשר רוצים להבדיל בין קבוצות או להראות שינוי הדרגתי.
בכל דוגמה נשאל קודם מה אנחנו רוצים להבין מהנתונים, ורק אחר כך נבחר איך לצייר אותם.
import numpy as np
import matplotlib.pyplot as plt
התבנית הבסיסית: plt.subplots ופקודות ציור#
ברוב המחברת נשתמש באותה תבנית פשוטה:
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_xlabel(...)
ax.set_ylabel(...)
plt.show()
plt.subplots() מחזירה שני אובייקטים:
figהוא האיור השלם - המסגרת שיכולה להכיל גרף אחד או כמה גרפים. משתמשים בו לפעולות ששייכות לאיור כולו, למשל שמירה לקובץ או הוספת סרגל צבעים.axהוא אזור הציור עם צירי ה־\(x\) וה־\(y\). עליו מציירים את הנתונים ומגדירים תוויות, כותרת, גבולות ומקרא באמצעות פקודות כמוax.plot,ax.scatter,ax.set_xlabelו־ax.set_title.
במחברת זו רוב האיורים מכילים אזור ציור אחד, ולכן נקבל אובייקט ax יחיד. בסוף, plt.show() מציגה את האיור.
גרף קווי: ספקטרום בליעה#
גרף קווי מתאים כאשר לציר ה־\(x\) יש סדר טבעי וכאשר החיבור בין נקודות סמוכות משמעותי. ספקטרום בליעה הוא דוגמה טובה: אורכי הגל מסודרים, ואנחנו מצפים שהבליעה תשתנה בצורה חלקה יחסית.
כמו שראינו בדוגמת הכימיה הקודמת, np.linspace יוצר מערך של נקודות שווי־מרחק. כאן נשתמש בו כדי ליצור ציר אורכי גל מסודר. הנוסחה עם np.exp רק מייצרת ספקטרום מדומה עם פס בליעה חלק; המטרה כאן היא לתרגל את הגרף, לא את מקור הנוסחה.
wavelength = np.linspace(400, 700, 301) # nm
peak_center = 520 # nm
peak_width = 28 # nm
absorbance = np.exp(-((wavelength - peak_center) ** 2) / (2 * peak_width ** 2))
fig, ax = plt.subplots()
ax.plot(wavelength, absorbance)
ax.set_xlabel("wavelength (nm)")
ax.set_ylabel("absorbance")
ax.set_title("Simulated absorption spectrum")
plt.show()
הדבר החשוב כאן אינו רק הקוד, אלא הקריאה המדעית: הפס נמצא סביב \(520\ \mathrm{nm}\), והרוחב שלו מתאר את תחום אורכי הגל שבהם החומר בולע.
שימו לב גם להרגלים הטובים: שמות צירים, יחידות, וכותרת קצרה שמסבירה מה מוצג.
גרף פיזור: נקודות מדידה#
גרף פיזור מתאים כאשר הנקודות הן מדידות נפרדות. לדוגמה, בעקומת כיול אפשר למדוד בליעה עבור כמה ריכוזים ידועים. גם אם אנחנו חושדים שיש קשר לינארי, לא כדאי לחבר את נקודות המדידה בקו כאילו מדדנו את כל הערכים שביניהן.
כדי לדמות נתונים ניסיוניים נוסיף רעש מדידה קטן. np.random.default_rng(7) יוצר מחולל מספרים אקראיים; המספר 7 גורם לכך שנקבל את אותה דוגמה בכל הרצה. הפקודה rng.normal(0.0, 0.035, size=concentration.size) יוצרת רעש מהתפלגות נורמלית עם ממוצע 0 וסטיית תקן 0.035. concentration.size הוא מספר ערכי הריכוז, ולכן נקבל ערך רעש אחד לכל נקודת מדידה.
rng = np.random.default_rng(7)
concentration = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2]) # mmol/L
true_absorbance = 0.65 * concentration + 0.03
measured_absorbance = true_absorbance + rng.normal(0.0, 0.035, size=concentration.size)
fig, ax = plt.subplots()
ax.scatter(concentration, measured_absorbance)
ax.set_xlabel("concentration (mmol/L)")
ax.set_ylabel("absorbance")
ax.set_title("Calibration measurements")
plt.show()
כאן כל נקודה היא מדידה. גרף הפיזור מדגיש את העובדה שיש רעש מדידה, ומכין אותנו לשאלה שנפגוש בהמשך: איך מתאימים מודל לנקודות כאלה?
פסי שגיאה: מדידות חוזרות#
לפעמים כל נקודה בגרף אינה מדידה אחת אלא סיכום של כמה מדידות חוזרות. במקרה כזה אפשר להציג ממוצע ופסי שגיאה, למשל סטיית תקן. פסי השגיאה עונים על השאלה: עד כמה המדידות באותה נקודה יציבות?
בקוד הבא כל שורה במערך replicates מייצגת ריכוז אחד, וכל עמודה מייצגת מדידה חוזרת. הביטוי replicate_true_absorbance[:, None] הופך מערך חד־ממדי לעמודה, כדי ש־NumPy יוכל להוסיף כמה ערכי רעש לכל ריכוז בעזרת broadcasting. לאחר מכן np.mean(..., axis=1) ו־np.std(..., axis=1) מחשבות ממוצע וסטיית תקן לאורך המדידות החוזרות בכל שורה.
replicate_concentration = np.array([0.2, 0.5, 0.8, 1.1])
replicate_true_absorbance = 0.65 * replicate_concentration + 0.03
replicates = replicate_true_absorbance[:, None] + rng.normal(0.0, 0.04, size=(replicate_concentration.size, 5))
mean_absorbance = np.mean(replicates, axis=1)
std_absorbance = np.std(replicates, axis=1)
fig, ax = plt.subplots()
ax.errorbar(replicate_concentration, mean_absorbance, yerr=std_absorbance, fmt="o", capsize=4)
ax.set_xlabel("concentration (mmol/L)")
ax.set_ylabel("absorbance")
ax.set_title("Mean absorbance with repeated measurements")
plt.show()
ב־ax.errorbar, הארגומנט yerr קובע את גובה פסי השגיאה בציר ה־\(y\). הארגומנט fmt="o" אומר לצייר את הממוצעים כנקודות, ו־capsize=4 מוסיף קווים קטנים בקצות פסי השגיאה כדי שיהיה קל לראות אותם.
צבעים: להתאים את הצבע למשמעות הנתונים#
צבעים הם לא רק קישוט. כדאי לבחור אותם לפי סוג המידע שהם מייצגים.
צבעים איכותיים (qualitative) מתאימים לקבוצות שאין ביניהן סדר טבעי, למשל חומרים A, B ו־C.
צבעים הדרגתיים (sequential/progressive) מתאימים לערך מספרי מסודר, למשל זמן, טמפרטורה או ריכוז.
צבעים מתבדרים (diverging) מתאימים לערכים סביב מרכז משמעותי, למשל שאריות סביב אפס.
צבעים איכותיים: קבוצות שונות#
בדוגמה הבאה שלושת הצבעים מסמנים שלושה חומרים שונים. אין משמעות לכך שחומר A “קטן” או “גדול” מחומר B; הצבעים רק מפרידים בין קטגוריות.
כאשר מציירים כמה עקומות על אותה מערכת צירים, label נותן שם לכל עקומה, ו־ax.legend() מציג את השמות האלה כמקרא.
spectrum_A = np.exp(-((wavelength - 470) ** 2) / (2 * 22 ** 2))
spectrum_B = 0.9 * np.exp(-((wavelength - 540) ** 2) / (2 * 28 ** 2))
spectrum_C = 0.8 * np.exp(-((wavelength - 625) ** 2) / (2 * 24 ** 2))
fig, ax = plt.subplots()
ax.plot(wavelength, spectrum_A, label="material A")
ax.plot(wavelength, spectrum_B, label="material B")
ax.plot(wavelength, spectrum_C, label="material C")
ax.set_xlabel("wavelength (nm)")
ax.set_ylabel("absorbance")
ax.set_title("Spectra of three materials")
ax.legend()
plt.show()
צבעים הדרגתיים: שינוי בזמן#
כאשר יש סדר טבעי, עדיף להשתמש בסכמת צבעים הדרגתית. כאן הצבע מציין זמן: ככל שהזמן מתקדם, הצבע מתקדם לאורך אותה מפת צבעים.
מפת צבעים כמו plt.cm.viridis מקבלת מספרים בין 0 ל־1 ומחזירה צבעים. לכן ניצור קודם מיקומים שווי־מרחק בין 0 ל־1, ואז נהפוך אותם לצבעים.
times = np.array([0, 2, 4, 6, 8]) # min
color_positions = np.linspace(0, 1, len(times))
colors = plt.cm.viridis(color_positions)
fig, ax = plt.subplots()
for i in range(len(times)):
time = times[i]
color = colors[i]
spectrum = np.exp(-time / 8) * spectrum_A
ax.plot(wavelength, spectrum, color=color, label=f"{time} min")
ax.set_xlabel("wavelength (nm)")
ax.set_ylabel("absorbance")
ax.set_title("Sequential colors for time series")
ax.legend(title="time")
plt.show()
צבעים מתבדרים: סטייה סביב אפס#
צבעים מתבדרים שימושיים כאשר יש מרכז משמעותי. למשל, בשאריות של מדידה אפס מייצג התאמה מושלמת: ערכים חיוביים ושליליים נמצאים משני צדדים של המרכז.
ב־ax.scatter, הארגומנט c=residuals אומר שהצבע של כל נקודה ייקבע לפי ערך השארית שלה. cmap="coolwarm" בוחר מפת צבעים מתבדרת, ו־vmin ו־vmax קובעים את תחום הערכים של הצבעים. כאן בחרנו ערכים סימטריים סביב אפס, כדי שאפס יהיה המרכז הוויזואלי. הפקודה ax.axhline(0) מוסיפה קו אופקי באפס, ו־fig.colorbar מוסיפה סרגל שמראה איזה צבע מתאים לאיזה ערך.
residuals = measured_absorbance - true_absorbance
fig, ax = plt.subplots()
scatter = ax.scatter(concentration, residuals, c=residuals, cmap="coolwarm", vmin=-0.08, vmax=0.08)
ax.axhline(0, color="black", linewidth=1)
ax.set_xlabel("concentration (mmol/L)")
ax.set_ylabel("measurement residual")
ax.set_title("Diverging colors around zero")
fig.colorbar(scatter, label="residual")
plt.show()
שמירת איור לקובץ#
כאשר רוצים להכניס איור למצגת, דוח או מאמר, אפשר לשמור את האובייקט fig בעזרת fig.savefig לפני שמפעילים plt.show.
fig.savefig("spectrum.png", dpi=300, bbox_inches="tight")
fig.savefig("spectrum.pdf", bbox_inches="tight")
plt.show()
png מתאים למצגות ואתרים. pdf או svg מתאימים לאיורים וקטוריים. dpi=300 הוא ערך נפוץ לאיכות הדפסה בקובץ תמונה, ו־bbox_inches="tight" מצמצם שוליים מיותרים.
רשימת בדיקה קצרה#
לפני שמסיימים איור מדעי, כדאי לבדוק:
האם ברור איזו שאלה הגרף עונה עליה?
האם הצירים מסומנים עם יחידות?
האם סוג הגרף מתאים לנתונים?
האם הצבעים מייצגים קטגוריות, סדר מספרי או סטייה סביב מרכז?
האם המקרא נחוץ וברור?
האם אפשר להבין את המסר המרכזי בלי לקרוא את כל הקוד?