งูต้นฉบับจัดเมล็ดสีที่อยู่ติดกันลงถาด แต่มีเมล็ดสีเดียวกันอีกกองถูกคั่นอยู่ไกลออกไป

Python itertools.groupby: รวมข้อมูลที่ติดกัน ไม่ใช่ SQL GROUP BY

พี่โชว์Engineering

ชื่อ itertools.groupby ชวนให้นึกถึง SQL แต่พฤติกรรมจริงคือแบ่ง ช่วงที่ key เดียวกันติดกัน ถ้าข้อมูล A, B, A จะได้กลุ่ม A สองก้อน ไม่ถูกรวมเป็นก้อนเดียวครับ

ถ้าต้องการยอดรวมต่อ key ทั้งชุด ให้ sort ด้วย key เดียวกับที่ส่งเข้า groupby ก่อน อีกกับดักคือ iterator ของแต่ละกลุ่มแชร์ source กับ outer iterator ต้อง consume หรือแปลงเป็น list ก่อนขยับกลุ่มถัดไป

from __future__ import annotations

import unittest
from itertools import groupby


def adjacent_runs(rows: list[tuple[str, int]]) -> list[tuple[str, int]]:
    return [(key, sum(value for _, value in group))
            for key, group in groupby(rows, key=lambda row: row[0])]


def totals_by_key(rows: list[tuple[str, int]]) -> list[tuple[str, int]]:
    ordered = sorted(rows, key=lambda row: row[0])
    return adjacent_runs(ordered)


class GroupByTests(unittest.TestCase):
    def test_adjacent_runs_remain_separate(self):
        rows = [("api", 2), ("web", 3), ("api", 5)]
        self.assertEqual(adjacent_runs(rows), [("api", 2), ("web", 3), ("api", 5)])

    def test_sort_then_group_combines_all_keys(self):
        rows = [("api", 2), ("web", 3), ("api", 5)]
        self.assertEqual(totals_by_key(rows), [("api", 7), ("web", 3)])


if __name__ == "__main__":
    unittest.main()

เมื่อ input เรียงอยู่แล้ว groupby ทำงานแบบ streaming และใช้ memory ต่ำ แต่การ sort ทำให้ต้องรับต้นทุน O(n log n) หากข้อมูลใหญ่มากและไม่ต้องรักษาลำดับ ใช้ dictionary accumulator อาจตรงกว่า

Recap

  • ถามก่อนว่าต้องการ adjacent runs หรือ global groups
  • sort ด้วย key เดียวกันเมื่อจะรวมทุกแถว
  • consume group iterator ก่อน outer loop เดินต่อ
  • ใช้ dictionary เมื่อ streaming order ไม่สำคัญ