6.11. צמצומים

צמצום מקפל מערך לאורך ציר אחד או יותר באמצעות סכימה, מיצוע, לקיחת מינימום וכן הלאה. כל צמצום הוא קריאת ספרייה אחת על המערך כולו, מהירה הרבה יותר מלולאת Python שקולה. numpy מכסה את הצמצומים הנפוצים:

  • sum() – סך כל האיברים

  • mean() – ממוצע חשבוני (הסכום חלקי מספר האיברים)

  • std() – סטיית תקן, ddof= מתאים את המכנה (N - ddof)

  • min() / max() – האיבר הקטן והגדול ביותר

  • median() – הערך האמצעי כשהאיברים ממוינים (האחוזון ה-50)

  • argmin() / argmax()האינדקס של האיבר המינימלי או המקסימלי

  • all() / any() – צמצומים של ערכי אמת על מערכים בוליאניים

6.11.1. ללא מילת המפתח axis

כשהיא נקראת ללא axis=, צמצום מחזיר סקלר המכסה את המערך כולו:

a = np.array([1, 2, 3, 4], dtype=np.float)
np.sum(a)           # 10.0
np.mean(a)          # 2.5
np.std(a)           # 1.118...
np.median(a)        # 2.5

b = np.array([40, 10, 30, 20], dtype=np.float)
np.max(b)           # 40.0
np.argmax(b)        # 0  (index of the maximum)

6.11.2. עם מילת המפתח axis

axis= מכווץ ציר אחד נקוב ומשאיר את האחרים שלמים. התוצאה היא מערך בעל דרגה נמוכה באחת מזו של הקלט:

m = np.arange(12, dtype=np.float).reshape((3, 4))

np.sum(m)               # 66.0          - scalar
np.sum(m, axis=0)       # length-4      - column sums
np.sum(m, axis=1)       # length-3      - row sums

אותו כלל צורה חל על כל צמצום: axis=0 מקפל את הציר הראשון, axis=1 מקפל את הציר השני וכן הלאה. ממוצע / סטיית תקן לאורך שורה, למשל, נכתבים np.mean(m, axis=1) ו-np.std(m, axis=1). לתוצאה יש האורך של הציר האחר.

מילת המפתח keepdims=True משאירה את הציר המכווץ במקומו באורך 1 במקום להשמיט אותו. ההבחנה חשובה כאשר התוצאה המצומצמת צריכה לחזור ולהתפשט (broadcast) כנגד המקור: keepdims משמרת את הדרגה, מה ששומר על כללי ההתפשטות מיושרים ציר מול ציר.

חיסור הממוצע של כל שורה מאותה שורה הוא השימוש המובהק:

m = np.arange(12, dtype=np.float).reshape((3, 4))
row_means = np.mean(m, axis=1, keepdims=True)
# row_means has shape (3, 1)
centred = m - row_means
# (3, 4) - (3, 1) -> (3, 4), each row centred on its own mean

ללא keepdims, np.mean(m, axis=1) מחזיר תוצאה חד-ממדית בצורה (3,). התפשטות (3, 4) - (3,) מיישרת את (3,) בתור (1, 3) לאחר הוספת הדרגה, מה שאינו תואם ל-(3, 4): הצירים האחרונים אינם מסכימים (4 מול 3) ואף אחד מהם אינו 1, ולכן numpy מעלה ValueError. keepdims=True הוא מה ששומר על תקפות החיסור.

6.11.3. הפריסה חשובה

בשילוב עם פריסת השורה-תחילה (row-major) שמתוארת ב-צורה וצעדים (strides), צמצום לאורך הציר האחרון הוא המקרה הזול ביותר. הצמצום עובר על בלוק הנתונים בכיוון שבו הוא מאוחסן, ללא קפיצות משורה לשורה:

m = np.arange(2000, dtype=np.float).reshape((2, 1000))
np.sum(m, axis=1)       # cheap - long axis is the inner one
np.sum(m, axis=0)       # has to jump rows on every step

כשליישום יש בחירה כיצד לפרוס חוצץ (buffer), שים את הציר הארוך אחרון כך שצמצומים לאורכו ירוצו בכיוון המהיר.

6.11.4. אובייקטים ניתנים-לאיטרציה כקלט

רוב הצמצומים מקבלים אובייקט Python ניתן-לאיטרציה (list, range, tuple) במקום ndarray. הנוחות הזו עולה כמה מיקרו-שניות עבור ההמרה המשתמעת – שמצטברות במהירות בתוך לולאה. כשאותם נתונים מצומצמים מספר פעמים, בנה את ה-ndarray פעם אחת והעבר אותו הלאה.