Tại sao cuối cùng tôi lại chuyển từ Pandas sang Polars
Tôi đã từng thử tải một tệp CSV 10GB vào Pandas DataFrame trên một chiếc laptop có 16GB RAM. Chỉ trong vài giây, quạt tản nhiệt kêu to như động cơ phản lực. Trước khi tôi kịp chạy một lệnh head() đơn giản, terminal đã báo lỗi MemoryError và dừng hoạt động. Nếu bạn làm việc với dữ liệu trong Python, có lẽ bạn đã từng gặp giới hạn này. Pandas là tiêu chuẩn của ngành vì có lý do của nó, nhưng nó chưa bao giờ được xây dựng để đáp ứng quy mô của kỹ thuật dữ liệu hiện đại.
Polars thay đổi điều đó. Được viết bằng Rust và xây dựng trên định dạng bộ nhớ Apache Arrow, nó xử lý được những tập dữ liệu mà Pandas phải “đầu hàng”. Theo kinh nghiệm của tôi, chuyển sang Polars là cách hiệu quả nhất để xây dựng các pipeline có khả năng mở rộng mà không cần đến sự phức tạp của một cụm Spark. Đây không chỉ là một bản nâng cấp nhỏ; đó là một sự thay đổi căn bản trong cách Python xử lý dữ liệu.
Khởi đầu nhanh: Sẵn sàng sử dụng trong vài phút
Chuyển đổi dễ dàng hơn bạn nghĩ. Mặc dù cú pháp khắt khe hơn Pandas, nó tuân theo một cấu trúc logic, dễ đọc và ưu tiên việc nối chuỗi phương thức (method chaining).
Cài đặt
pip install polars
DataFrame đầu tiên của bạn
Hãy cùng xem một thao tác lọc và gom nhóm (filter-and-aggregate) cơ bản. Lưu ý cách chúng ta sử dụng các biểu thức (expressions) thay vì thao tác trực tiếp trên chỉ mục (index).
import polars as pl
# Tạo một DataFrame
df = pl.DataFrame({
"id": [1, 2, 3, 4, 5],
"category": ["A", "B", "A", "C", "B"],
"value": [10.5, 20.0, 15.2, 7.8, 12.1]
})
# Lọc, Gom nhóm và Tính tổng sử dụng nối chuỗi phương thức
result = (df.filter(pl.col("value") > 10)
.group_by("category")
.agg(pl.col("value").sum())
.sort("value", descending=True))
print(result)
Hàm pl.col() là linh hồn của Polars. Những “Biểu thức” (Expressions) này cho phép engine tối ưu hóa truy vấn của bạn trước khi bất kỳ dữ liệu nào thực sự được di chuyển, giúp mã nguồn của bạn hiệu quả hơn đáng kể.
Bộ máy vận hành: Tại sao Polars vượt trội hơn Pandas
Polars không chỉ là một lớp vỏ bọc; nó là một sự tái thiết kế toàn diện về kiến trúc. Ba tính năng cụ thể sau đây mang lại lợi thế khổng lồ cho nó so với các thư viện truyền thống.
1. Rust và Apache Arrow
Polars được viết bằng Rust, mang lại hiệu năng ngang tầm ngôn ngữ C với tính an toàn bộ nhớ nghiêm ngặt. Nó sử dụng Apache Arrow cho bố cục bộ nhớ nội bộ. Vì Arrow có cấu trúc dạng cột (columnar), CPU không lãng phí chu kỳ để đọc dữ liệu không cần thiết từ các cột lân cận. Bố cục này cho phép thực hiện các hoạt động vector hóa (vectorized operations) với tốc độ cực nhanh.
2. Song song hóa mặc định
Pandas hầu hết là đơn luồng (single-threaded). Nếu CPU của bạn có 12 nhân, Pandas thường để trống 11 nhân trong số đó. Polars thì khác. Nó tự động phân phối khối lượng công việc trên mọi nhân có sẵn. Bạn có được hiệu năng đa luồng mà không cần viết một dòng mã multiprocessing phức tạp nào.
3. Bộ tối ưu hóa truy vấn
Khi sử dụng Lazy API, Polars không thực thi mã theo từng dòng. Nó phân tích toàn bộ script của bạn trước. Nếu bạn lọc một tập dữ liệu 50 triệu hàng ở cuối script, Polars sẽ đẩy bộ lọc đó lên ngay từ đầu. Kỹ thuật “predicate pushdown” này đảm bảo engine chỉ đọc các hàng và cột cụ thể mà nó cần từ đĩa cứng.
Xử lý tệp 100GB với Lazy Evaluation
Nếu tập dữ liệu của bạn lớn hơn dung lượng RAM hiện có, hãy ngừng sử dụng read_csv. Thay vào đó, hãy dùng scan_csv để kích hoạt Lazy API.
# Lệnh này tạo ra một kế hoạch truy vấn mà không cần tải tệp vào bộ nhớ
lazy_query = (pl.scan_csv("massive_dataset.csv")
.filter(pl.col("status") == "active")
.select([
pl.col("user_id"),
(pl.col("revenue") * 0.8).alias("net_revenue")
]))
# Engine tối ưu hóa kế hoạch và chỉ thực thi khi bạn gọi collect()
df_final = lazy_query.collect()
Việc gọi .collect() sẽ ra lệnh cho Polars thực thi kế hoạch. Nếu bạn chỉ cần hai cột trong số một trăm cột, engine sẽ chỉ lấy đúng hai cột đó từ đĩa. Tôi đã thấy điều này giúp giảm mức sử dụng bộ nhớ từ 40GB xuống còn 2GB trong các môi trường thực tế.
Đơn giản hóa các phép gom nhóm phức tạp
Các hàm cửa sổ (Window functions) trong Polars cực kỳ gọn gàng. Ví dụ, lấy ba giao dịch cuối cùng cho mỗi người dùng chỉ với một dòng mã:
df.group_by("user_id").agg([
pl.col("transaction_amount").tail(3).alias("last_3_tx")
])
Những bài học kinh nghiệm từ thực tế
Sau một năm sử dụng Polars trong các pipeline dữ liệu, tôi đã tìm ra một vài mẹo nhỏ nhưng quan trọng để tránh các nút thắt cổ chai phổ biến.
1. Bẫy hiệu năng của .apply()
Trong Pandas, .apply(lambda x: ...) là một công cụ tiêu chuẩn. Trong Polars, nó là “kẻ sát nhân” đối với hiệu năng. Việc sử dụng lambda của Python buộc dữ liệu phải thoát ra khỏi lõi Rust tốc độ cao để quay lại trình thông dịch Python chậm chạp. Hãy luôn tìm kiếm các biểu thức gốc như pl.when().then().otherwise() trước khi phải nhờ đến apply.
2. Tôn trọng Schema
Polars rất khắt khe về kiểu dữ liệu. Nếu bạn cố gắng join một cột Int32 với một cột Int64, Polars sẽ báo lỗi thay vì tự ý dự đoán. Điều này có thể gây khó chịu lúc đầu, nhưng nó ngăn chặn các lỗi làm sai lệch dữ liệu âm thầm thường gặp trong các dự án Pandas. Hãy tập thói quen sử dụng .cast(pl.Int64) sớm trong quá trình làm sạch dữ liệu.
3. Ưu tiên Parquet thay vì CSV
Bất cứ khi nào có thể, hãy lưu trữ dữ liệu của bạn ở định dạng Parquet. Polars có thể đọc metadata của Parquet để bỏ qua toàn bộ các khối dữ liệu mà nó không cần. Sự kết hợp giữa scan_parquet và định dạng tệp phù hợp cho phép bạn xử lý hàng trăm gigabyte trên một máy trạm tiêu chuẩn.
4. Tích hợp liền mạch
Bạn không cần phải viết lại toàn bộ mã nguồn của mình. Nếu bạn có một thư viện cụ thể yêu cầu Pandas, chỉ cần sử dụng df.to_pandas(). Vì cả hai thư viện đều tương thích với Arrow, việc chuyển đổi này thường diễn ra gần như tức thì và rất tiết kiệm bộ nhớ.
Chuyển sang Polars đòi hỏi một sự thay đổi trong tư duy. Bạn chuyển từ việc nói với máy tính “làm thế nào” sang nói với nó “bạn muốn gì”. Kết quả sẽ tự chứng minh. Tôi đã thấy thời gian xử lý giảm từ 20 phút xuống còn dưới 40 giây chỉ bằng cách để bộ tối ưu hóa của Polars dẫn dắt.

