6.11. การลดค่า (Reductions)

การลดค่า (reduction) คือการยุบอาร์เรย์ตามแกนหนึ่งหรือหลายแกนด้วยการหาผลรวม ค่าเฉลี่ย ค่าต่ำสุด และอื่น ๆ การลดค่าแต่ละครั้งเป็นการเรียกไลบรารีเพียงครั้งเดียวต่ออาร์เรย์ทั้งหมด ซึ่งเร็วกว่าลูป 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 แทนที่จะตัดออก ความแตกต่างนี้มีความสำคัญเมื่อผลลัพธ์ที่ลดค่าแล้วต้องการบรอดแคสต์กลับไปยังต้นฉบับ: 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) จะคืนผลลัพธ์แบบ 1-D ที่มีรูปร่าง (3,) การบรอดแคสต์ (3, 4) - (3,) จะจัด (3,) เป็น (1, 3) หลังจากเติมอันดับ ซึ่งไม่เข้ากันกับ (3, 4): แกนสุดท้ายไม่ตรงกัน (4 กับ 3) และไม่มีแกนใดเป็น 1 ดังนั้น numpy จะเกิด ValueError keepdims=True คือสิ่งที่ทำให้การลบยังคงถูกต้อง

6.11.3. เรื่องของการจัดวางหน่วยความจำ

เมื่อใช้ร่วมกับการจัดวางแบบ row-major ที่อธิบายไว้ใน Shape และ stride การลดค่าตามแกน สุดท้าย เป็นกรณีที่ประหยัดที่สุด การลดค่าจะวนซ้ำบล็อกข้อมูลในทิศทางที่จัดเก็บโดยไม่มีการข้ามจากแถวหนึ่งไปยังอีกแถว:

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

เมื่อแอปพลิเคชันมีทางเลือกในการจัดวางบัฟเฟอร์ ให้วางแกนยาวไว้สุดท้ายเพื่อให้การลดค่าตามแกนนั้นทำงานในทิศทางที่รวดเร็ว

6.11.4. อิเทอเรเบิลเป็นอินพุต

การลดค่าส่วนใหญ่รับอิเทอเรเบิลของ Python (list, range, tuple) แทน ndarray ได้ ความสะดวกนี้มีค่าใช้จ่ายเพียงไม่กี่ไมโครวินาทีสำหรับการแปลงโดยนัย ซึ่งสะสมได้รวดเร็วในลูป เมื่อข้อมูลเดียวกันถูกลดค่าหลายครั้ง ให้สร้าง ndarray ครั้งเดียวแล้วส่งต่อไปใช้งาน