Python itertools.groupby: รวมข้อมูลที่ติดกัน ไม่ใช่ SQL GROUP BY
ชื่อ 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 ไม่สำคัญ