راهنمای جامع پنداز ۳.۰: ویژگی‌های جدید، تغییرات و نحوه مهاجرت

پنداز ۳.۰ با تغییرات بنیادین مثل Copy-on-Write، نوع رشته‌ای PyArrow و pd.col() منتشر شد. در این راهنما همه ویژگی‌های جدید و نحوه مهاجرت رو با مثال‌های عملی بررسی می‌کنیم.

مقدمه: چرا پنداز ۳.۰ اهمیت دارد؟

اگر با پایتون و تحلیل داده سر و کار دارید، احتمالاً پنداز (Pandas) رو خوب می‌شناسید. این کتابخانه سال‌هاست که ستون فقرات تحلیل داده در پایتون به حساب میاد و میلیون‌ها توسعه‌دهنده و دانشمند داده در سراسر دنیا ازش استفاده می‌کنند. خب، در تاریخ ۲۱ ژانویه ۲۰۲۶ (اول بهمن ۱۴۰۴)، نسخه ۳.۰ پنداز رسماً منتشر شد و باید بگم تغییراتش واقعاً بنیادین هستند.

پنداز ۳.۰ یک آپدیت معمولی نیست. این نسخه حاصل سال‌ها برنامه‌ریزی و توسعه‌ست و معماری داخلی کتابخانه رو به شکل اساسی تغییر داده. سه تغییر کلیدی این نسخه:

  • Copy-on-Write (CoW) به عنوان رفتار پیش‌فرض و تنها حالت کاری
  • نوع داده رشته‌ای مبتنی بر PyArrow به جای نوع object قدیمی
  • عبارات ستونی pd.col() برای زنجیره‌سازی روان‌تر متدها

علاوه بر اینا، تغییرات مهمی توی دقت پیش‌فرض تاریخ و زمان، بهینه‌سازی‌های عملکردی چشمگیر و حذف کلی قابلیت‌های منسوخ‌شده (deprecated) صورت گرفته. توی این مقاله، همه این تغییرات رو عملی و با مثال‌های کد بررسی می‌کنیم و یه راهنمای کامل برای مهاجرت هم ارائه می‌دیم.

خب، بریم سراغش.

Copy-on-Write (CoW): تحولی بنیادین در مدیریت حافظه

CoW چیست و چرا اهمیت دارد؟

صادقانه بگم، یکی از آزاردهنده‌ترین مشکلات تاریخی پنداز، رفتار غیرقابل پیش‌بینی هنگام کار با نماها (views) و کپی‌های دیتافریم بود. توی نسخه‌های قبلی، وقتی یه زیرمجموعه از دیتافریم رو انتخاب می‌کردید، گاهی یک نما (view) و گاهی یک کپی (copy) دریافت می‌کردید. این رفتار نامشخص باعث خطای معروف SettingWithCopyWarning می‌شد — خطایی که احتمالاً هر کاربر پنداز حداقل یه بار (و شاید صد بار!) باهاش مواجه شده.

Copy-on-Write یا «کپی هنگام نوشتن» این مشکل رو کاملاً حل می‌کنه. توی این مکانیزم، هر عملیات ایندکس‌گذاری همیشه یه نمای سبک‌وزن از داده‌ها برمی‌گردونه. اما به محض اینکه بخواید روی اون نما تغییری اعمال کنید، پنداز خودکار یه کپی ایجاد می‌کنه. یعنی:

  • تغییر یک دیتافریم هرگز بر دیتافریم دیگه‌ای تأثیر نمی‌ذاره
  • رفتار کد همیشه قابل پیش‌بینی و مشخصه
  • خطای SettingWithCopyWarning به طور کامل حذف شده
  • مصرف حافظه در خیلی از موارد بهینه‌تر شده

توی پنداز ۳.۰، حالت CoW دیگه یه گزینه اختیاری نیست — تنها حالت کاری کتابخانه‌ست. دیگه نمی‌تونید غیرفعالش کنید.

الگوهای قدیمی که دیگر کار نمی‌کنند

مهم‌ترین تغییر رفتاری مربوط به انتساب زنجیره‌ای (chained assignment) هست. توی نسخه‌های قبلی، خیلی از توسعه‌دهنده‌ها از الگوهایی مثل این استفاده می‌کردن:

# ❌ الگوی قدیمی - در پنداز ۳.۰ دیگر کار نمی‌کند
import pandas as pd

df = pd.DataFrame({"name": ["Alice", "Bob", "Charlie"], "age": [25, 30, 35]})

# انتساب زنجیره‌ای: ابتدا فیلتر، سپس تغییر
df[df["age"] > 28]["name"] = "Updated"
# در پنداز ۳.۰ این خط هیچ تأثیری روی df اصلی ندارد!

# یک مثال رایج دیگر
df["name"][0] = "New Name"
# این هم دیگر df اصلی را تغییر نمی‌دهد!

توی نسخه‌های قبلی، این کد گاهی کار می‌کرد (وقتی پنداز تصادفاً یه نما برمی‌گردوند) و گاهی نه. ولی توی پنداز ۳.۰، این الگو هرگز کار نمی‌کنه، چون هر عملیات ایندکس‌گذاری میانی یه شیء موقت ایجاد می‌کنه و تغییرات روی اون شیء موقت اعمال میشه، نه روی دیتافریم اصلی.

الگوهای صحیح با .loc

راه‌حل درست استفاده از .loc برای انجام فیلتر و انتساب در یک مرحله واحده:

# ✅ الگوی صحیح در پنداز ۳.۰
import pandas as pd

df = pd.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 30, 35],
    "city": ["Tehran", "Isfahan", "Shiraz"]
})

# تغییر مقادیر با استفاده از .loc (فیلتر و انتساب در یک عملیات)
df.loc[df["age"] > 28, "name"] = "Updated"
print(df)
#       name  age      city
# 0    Alice   25    Tehran
# 1  Updated   30  Isfahan
# 2  Updated   35    Shiraz

# تغییر یک سلول خاص
df.loc[0, "name"] = "New Name"
print(df)
#       name  age      city
# 0  New Name   25    Tehran
# 1  Updated   30  Isfahan
# 2  Updated   35    Shiraz

# تغییر چندین ستون همزمان
df.loc[df["age"] > 28, ["name", "city"]] = ["Unknown", "N/A"]
print(df)

اگر هم می‌خواید یه کپی مستقل از دیتافریم داشته باشید، باید صراحتاً از متد .copy() استفاده کنید:

# ✅ ایجاد کپی مستقل
import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})

# ایجاد یک کپی مستقل
df2 = df.copy()
df2["A"] = [10, 20, 30]

print(df)   # بدون تغییر: A=[1,2,3]
print(df2)  # تغییر یافته: A=[10,20,30]

# بدون .copy() — نما ایجاد می‌شود ولی تغییر روی آن df اصلی را تغییر نمی‌دهد
df3 = df[["A"]]
df3["A"] = [100, 200, 300]  # فقط df3 تغییر می‌کند، df بدون تغییر می‌ماند

مزایای عملکردی CoW

مکانیزم Copy-on-Write فقط کد رو قابل پیش‌بینی‌تر نمی‌کنه — مزایای عملکردی واقعاً قابل توجهی هم داره:

  • کاهش کپی‌های غیرضروری: توی نسخه‌های قبلی، خیلی از عملیات‌ها محافظه‌کارانه داده‌ها رو کپی می‌کردن. حالا فقط وقتی کپی انجام میشه که واقعاً لازمه.
  • بهینه‌سازی حافظه: چندین دیتافریم می‌تونن داده‌های زیربنایی یکسانی رو به اشتراک بذارن تا وقتی یکیشون تغییر کنه.
  • سرعت بالاتر در عملیات خواندن: عملیات‌هایی مثل فیلتر کردن، مرتب‌سازی و گروه‌بندی که داده‌ها رو تغییر نمیدن، سریع‌تر اجرا میشن.

مثلاً توی سناریویی که یه دیتافریم بزرگ رو چندین بار فیلتر می‌کنید بدون تغییر داده‌ها، مصرف حافظه به طور چشمگیری کاهش پیدا می‌کنه — چون همه نماها به داده‌های یکسانی اشاره می‌کنن.

نوع داده رشته‌ای مبتنی بر PyArrow

نوع str جدید به جای object

یکی از قدیمی‌ترین دردسرهای پنداز، نحوه ذخیره‌سازی رشته‌ها بود. توی نسخه‌های قبلی، ستون‌های رشته‌ای با نوع داده object ذخیره می‌شدن — که در واقع یه آرایه از اشاره‌گرهای پایتون بود و هر کدوم به یه شیء رشته‌ای جداگانه توی حافظه اشاره می‌کرد. خب، این روش واقعاً ناکارآمد بود.

توی پنداز ۳.۰، نوع داده پیش‌فرض برای رشته‌ها str هست که در پس‌زمینه از PyArrow استفاده می‌کنه. Apache Arrow یه فرمت حافظه ستونی‌ه که برای پردازش کارآمد داده‌ها طراحی شده و رشته‌ها رو فشرده و پیوسته توی حافظه ذخیره می‌کنه.

# مقایسه رفتار قدیم و جدید
import pandas as pd

# در پنداز ۳.۰ — رشته‌ها به صورت پیش‌فرض نوع str دارند
df = pd.DataFrame({"name": ["علی", "مریم", "حسین", "زهرا"]})
print(df.dtypes)
# name    str
# dtype: object

# در نسخه‌های قبلی (۲.x) خروجی این بود:
# name    object
# dtype: object

# بررسی نوع داده دقیق‌تر
print(df["name"].dtype)
# str  (مبتنی بر PyArrow در پس‌زمینه)

بهبود عملکرد: ۵ تا ۱۰ برابر سریع‌تر، ۵۰ درصد حافظه کمتر

تفاوت عملکردی بین نوع object قدیمی و نوع str جدید واقعاً چشمگیره. بر اساس آزمایش‌های انجام شده:

  • عملیات رشته‌ای (جستجو، جایگزینی، تبدیل حروف) ۵ تا ۱۰ برابر سریع‌تر اجرا میشن
  • مصرف حافظه تا ۵۰ درصد کاهش پیدا می‌کنه
  • خواندن فایل‌های CSV با ستون‌های رشته‌ای فراوان محسوساً سریع‌تره
  • مقادیر گمشده (missing values) به صورت بومی پشتیبانی میشن بدون نیاز به NaN عددی

بذارید با یه مثال نشونتون بدم:

import pandas as pd
import numpy as np

# ایجاد یک دیتافریم بزرگ با داده‌های رشته‌ای
n = 1_000_000
data = {
    "city": np.random.choice(["Tehran", "Isfahan", "Shiraz", "Tabriz", "Mashhad"], n),
    "district": [f"District-{i % 100}" for i in range(n)],
    "postal_code": [f"{np.random.randint(10000, 99999)}" for _ in range(n)]
}

df = pd.DataFrame(data)

# بررسی نوع داده‌ها
print(df.dtypes)
# city           str
# district       str
# postal_code    str

# بررسی مصرف حافظه
print(f"Memory usage: {df.memory_usage(deep=True).sum() / 1e6:.1f} MB")

# عملیات رشته‌ای — در پنداز ۳.۰ بسیار سریع‌تر
result = df["city"].str.upper()
result = df["district"].str.contains("District-5")
result = df["postal_code"].str[:3]

مدیریت مقادیر گمشده در نوع رشته‌ای جدید

یکی از مزایای خوب نوع str جدید، پشتیبانی بومی از مقادیر گمشده با pd.NA به جای np.nan هست. توی نوع قدیمی object، مقادیر گمشده به صورت NaN (یه مقدار عددی اعشاری) ذخیره می‌شدن — که خب، از نظر معنایی اصلاً درست نبود. یه رشته که نیست، چرا باید «عدد نیست» باشه؟

import pandas as pd

# در پنداز ۳.۰
s = pd.Series(["hello", None, "world"])
print(s)
# 0    hello
# 1     
# 2    world
# dtype: str

print(s.isna())
# 0    False
# 1     True
# 2    False
# dtype: bool

# مقایسه با رفتار قدیمی (پنداز ۲.x):
# 0    hello
# 1      NaN      ← NaN عددی برای رشته‌ها معنایی نداشت
# 2    world
# dtype: object

نصب PyArrow

برای استفاده از قابلیت‌های رشته‌ای جدید، باید PyArrow نصب باشه. خبر خوب اینه که با نصب پنداز ۳.۰ معمولاً خودکار نصب میشه:

# نصب پنداز ۳.۰ (PyArrow به صورت خودکار نصب می‌شود)
pip install pandas>=3.0

# یا به‌روزرسانی از نسخه قبلی
pip install --upgrade pandas

# بررسی نسخه‌ها
python -c "import pandas as pd; print(pd.__version__)"
python -c "import pyarrow as pa; print(pa.__version__)"

اگر از conda استفاده می‌کنید:

conda install pandas>=3.0

عبارات ستونی pd.col(): زنجیره‌سازی روان‌تر متدها

معرفی نحو جدید

خب، بذارید درباره یکی از هیجان‌انگیزترین ویژگی‌های پنداز ۳.۰ صحبت کنیم: تابع pd.col(). این تابع یه عبارت ستونی (column expression) ایجاد می‌کنه که بهتون اجازه میده بدون لامبدا یا ارجاع مستقیم به دیتافریم، عملیات روی ستون‌ها تعریف کنید. اگه با زنجیره‌سازی متدها کار کرده باشید، می‌دونید چقدر این مهمه.

ایده پشت pd.col() ساده‌ست: بتونید به یه ستون «اشاره» کنید بدون اینکه نام دیتافریم رو تکرار کنید. اگه با Polars کار کرده باشید، این مفهوم براتون آشناست — pl.col() همین کار رو می‌کنه و حالا پنداز هم اقتباسش کرده.

مقایسه با روش‌های قدیمی

بیاید تفاوت بین روش‌های قدیمی و نحو جدید pd.col() رو عملی ببینیم:

import pandas as pd

df = pd.DataFrame({
    "product": ["Laptop", "Phone", "Tablet", "Monitor"],
    "price": [1200, 800, 400, 350],
    "quantity": [10, 25, 15, 8],
    "discount": [0.1, 0.05, 0.15, 0.0]
})

# --- روش قدیمی ۱: استفاده از براکت و نام دیتافریم ---
df["total"] = df["price"] * df["quantity"]
df["final_price"] = df["price"] * (1 - df["discount"])

# --- روش قدیمی ۲: استفاده از lambda در assign ---
result = (
    df
    .assign(
        total=lambda x: x["price"] * x["quantity"],
        final_price=lambda x: x["price"] * (1 - x["discount"]),
        revenue=lambda x: x["price"] * x["quantity"] * (1 - x["discount"])
    )
)

# --- روش جدید پنداز ۳.۰: استفاده از pd.col() ---
result = (
    df
    .assign(
        total=pd.col("price") * pd.col("quantity"),
        final_price=pd.col("price") * (1 - pd.col("discount")),
        revenue=pd.col("price") * pd.col("quantity") * (1 - pd.col("discount"))
    )
)

print(result)

می‌بینید؟ نحو جدید pd.col() خیلی خواناتر و تمیزتر از لامبداهاست. دیگه نیازی به lambda x: توی ابتدای هر عبارت نیست و کد بیشتر شبیه یه فرمول ریاضی ساده به نظر میرسه.

مثال‌های عملی پیشرفته‌تر

pd.col() فقط به عملیات ریاضی ساده محدود نمیشه. توی سناریوهای پیچیده‌تر هم عالی کار می‌کنه:

import pandas as pd

df = pd.DataFrame({
    "employee": ["Ali", "Maryam", "Hossein", "Zahra", "Reza"],
    "department": ["Engineering", "Marketing", "Engineering", "HR", "Marketing"],
    "salary": [5000, 4500, 5500, 4000, 4800],
    "bonus_rate": [0.15, 0.10, 0.20, 0.08, 0.12],
    "years": [5, 3, 8, 2, 6]
})

# استفاده از pd.col() در زنجیره‌سازی متدها
result = (
    df
    .assign(
        total_compensation=pd.col("salary") * (1 + pd.col("bonus_rate")),
        seniority_bonus=pd.col("salary") * pd.col("years") * 0.01,
        tax=pd.col("salary") * 0.2
    )
    .query("department == 'Engineering'")
    .sort_values("total_compensation", ascending=False)
)

print(result)

یه مزیت بزرگ pd.col() اینه که عبارات ستونی می‌تونن قبل از اجرا بهینه‌سازی بشن. پنداز می‌تونه این عبارات رو تحلیل کنه و عملیات‌ها رو کارآمدتر اجرا کنه، در حالی که لامبداها برای پنداز «جعبه سیاه» هستن و امکان بهینه‌سازی نداره.

البته باید گفت که pd.col() توی نسخه ۳.۰ هنوز در مراحل اولیه‌ست و احتمالاً توی نسخه‌های بعدی قابلیت‌های بیشتری بهش اضافه میشه. ولی همین الانش هم برای خیلی از کاربردها فوق‌العاده مفیده.

دقت پیش‌فرض تاریخ و زمان: میکروثانیه به جای نانوثانیه

چرا این تغییر مهم است؟

توی نسخه‌های قبلی پنداز، تمام مقادیر تاریخ و زمان با دقت نانوثانیه ذخیره می‌شدن. هر مقدار تاریخ‌-زمان توی یه عدد صحیح ۶۴ بیتی ذخیره می‌شد و دقت نانوثانیه یعنی بازه زمانی قابل نمایش فقط حدود ۵۸۴ سال بود:

  • قدیمی‌ترین تاریخ قابل نمایش: حدود سال ۱۶۷۸ میلادی
  • جدیدترین تاریخ قابل نمایش: حدود سال ۲۲۶۲ میلادی

این محدودیت برای خیلی از کاربردها دردسرساز بود. مثلاً اگه با داده‌های تاریخی قبل از ۱۶۷۸ کار می‌کردید (اسناد تاریخی، باستان‌شناسی و...)، پنداز خطا می‌داد یا مقادیر رو به NaT تبدیل می‌کرد.

توی پنداز ۳.۰، دقت پیش‌فرض به میکروثانیه تغییر کرده. بازه زمانی قابل نمایش حالا حدود ۵۸۴,۰۰۰ سال شده — از حدود ۲۹۰,۰۰۰ سال قبل از میلاد تا ۲۹۰,۰۰۰ بعد از میلاد. فکر کنم این دیگه برای همه کافیه!

import pandas as pd

# در پنداز ۳.۰ — دقت پیش‌فرض میکروثانیه
ts = pd.Timestamp("2026-01-21 10:30:00")
print(ts)
# 2026-01-21 10:30:00
print(ts.unit)
# 'us' (microsecond)

# تاریخ‌های بسیار قدیمی حالا بدون مشکل کار می‌کنند
ancient_date = pd.Timestamp("0500-03-15")
print(ancient_date)
# 0500-03-15 00:00:00

# در پنداز ۲.x این خطا می‌داد:
# OutOfBoundsDatetime: Out of bounds nanosecond timestamp

# ایجاد بازه زمانی گسترده
dates = pd.date_range("1000-01-01", periods=5, freq="400YS")
print(dates)

# اگر واقعاً به دقت نانوثانیه نیاز دارید:
ts_ns = pd.Timestamp("2026-01-21 10:30:00.123456789", unit="ns")
print(ts_ns.unit)
# 'ns'

این تغییر سازگاری بهتری هم با کتابخانه‌های دیگه مثل NumPy، Python datetime و Apache Arrow ایجاد می‌کنه — که همگی از میکروثانیه به عنوان پیش‌فرض استفاده می‌کنن.

تأثیر بر کد موجود

اگه کد شما به دقت نانوثانیه وابسته‌ست (مثلاً توی سیستم‌های معاملات مالی فرکانس بالا)، باید صراحتاً دقت نانوثانیه رو مشخص کنید. ولی برای اکثر کاربردها، این تغییر شفافه و نیازی به تغییر کد ندارید. فقط باید بدونید که مقایسه دقیق مُهرهای زمانی ممکنه نتایج متفاوتی بده اگه قبلاً از نانوثانیه استفاده می‌کردید.

راهنمای مهاجرت: گام به گام از نسخه‌های قبلی به پنداز ۳.۰

گام ۱: ابتدا به نسخه ۲.۳ ارتقا دهید

توصیه اکید تیم توسعه پنداز اینه که مستقیماً از نسخه‌های قدیمی به ۳.۰ مهاجرت نکنید. در عوض، اول به آخرین نسخه پایدار سری ۲.x (یعنی نسخه ۲.۳) ارتقا بدید. چرا؟ چون نسخه ۲.۳ همه هشدارهای لازم برای تغییرات ناسازگار نسخه ۳.۰ رو نشون میده:

# گام ۱: ارتقا به نسخه ۲.۳
pip install pandas==2.3.*

# اجرای تست‌ها و بررسی هشدارها
python -W all -m pytest your_test_suite/

# یا اجرای اسکریپت با نمایش همه هشدارها
python -W all your_script.py

توی نسخه ۲.۳، هر جایی که کد شما از الگوهایی استفاده می‌کنه که توی ۳.۰ تغییر خواهند کرد، یه هشدار FutureWarning یا DeprecationWarning نمایش داده میشه. همه این هشدارها رو رفع کنید و بعد برید سراغ گام بعدی.

گام ۲: فعال‌سازی CoW در نسخه ۲.۳

توی نسخه ۲.۳ می‌تونید حالت Copy-on-Write رو آزمایشی فعال کنید تا کدتون رو قبل از مهاجرت تست کنید:

# فعال‌سازی CoW در نسخه ۲.x
import pandas as pd
pd.set_option("mode.copy_on_write", True)

# حالا کد خود را اجرا کنید و ببینید آیا خطایی رخ می‌دهد
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})

# اگر از انتساب زنجیره‌ای استفاده کرده‌اید، اینجا مشکل ظاهر می‌شود
# df[df["A"] > 1]["B"] = 99  # ❌ این دیگر کار نمی‌کند

# به جای آن:
df.loc[df["A"] > 1, "B"] = 99  # ✅ این درست است

گام ۳: مهاجرت نوع داده رشته‌ای

برای آماده‌سازی کد برای نوع رشته‌ای جدید:

# فعال‌سازی نوع رشته‌ای جدید در نسخه ۲.x
import pandas as pd
pd.set_option("future.infer_string", True)

# حالا رشته‌ها با نوع str ذخیره می‌شوند
df = pd.DataFrame({"name": ["Ali", "Maryam"]})
print(df.dtypes)
# name    str
# dtype: object

# بررسی سازگاری کد
# اگر جایی از df["name"].dtype == "object" استفاده کرده‌اید،
# باید آن را به‌روزرسانی کنید:

# ❌ قدیمی
if df["name"].dtype == "object":
    print("This is a string column")

# ✅ جدید — سازگار با هر دو نسخه
if pd.api.types.is_string_dtype(df["name"]):
    print("This is a string column")

گام ۴: ارتقا به پنداز ۳.۰

بعد از اطمینان از سازگاری کد، وقتشه:

# ارتقای نهایی
pip install pandas>=3.0

# بررسی نسخه
python -c "import pandas as pd; print(pd.__version__)"

دام‌ها و مشکلات رایج

توی فرآیند مهاجرت، حواستون به این موارد باشه:

  1. بررسی نوع داده با مقایسه مستقیم:

    اگه توی کدتون از dtype == "object" برای شناسایی ستون‌های رشته‌ای استفاده می‌کنید، این بررسی توی پنداز ۳.۰ دیگه درست کار نمی‌کنه. از pd.api.types.is_string_dtype() استفاده کنید.

  2. فرض بر اینکه ایندکس‌گذاری تغییرپذیر است:

    هر کدی که فرض می‌کنه نتیجه ایندکس‌گذاری یه نمای تغییرپذیر (mutable view) به دیتافریم اصلیه، باید بازنویسی بشه.

  3. مقادیر گمشده:

    توی نوع رشته‌ای جدید، مقادیر گمشده pd.NA هستن نه np.nan. اگه کدی دارید که صراحتاً np.nan رو برای رشته‌ها چک می‌کنه، باید تغییرش بدید.

  4. سازگاری با کتابخانه‌های ثالث:

    بعضی کتابخانه‌ها ممکنه هنوز با پنداز ۳.۰ سازگار نباشن. قبل از ارتقا حتماً بررسی کنید. کتابخانه‌هایی مثل scikit-learn، matplotlib و seaborn معمولاً سریع آپدیت میشن.

  5. تغییر دقت زمانی:

    اگه داده‌های سریالی‌شده (مثلاً فایل‌های Parquet یا Pickle) با دقت نانوثانیه دارید، ممکنه هنگام خواندنشون تفاوت‌هایی ببینید.

توصیه من اینه که حتماً اول مهاجرت رو توی یه محیط آزمایشی انجام بدید و تست‌های کاملتون رو اجرا کنید.

معیارهای عملکردی و مقایسه‌ها

مقایسه سرعت عملیات رشته‌ای

یکی از ملموس‌ترین بهبودهای پنداز ۳.۰ توی حوزه عملیات رشته‌ایه. این اعداد رو ببینید — مقایسه بین پنداز ۲.۲ (نوع object) و پنداز ۳.۰ (نوع str مبتنی بر PyArrow) برای یک میلیون رکورد:

  • str.contains(): پنداز ۲.۲ → ۴۵۰ms | پنداز ۳.۰ → ۶۵ms (حدود ۷ برابر سریع‌تر)
  • str.upper(): پنداز ۲.۲ → ۳۸۰ms | پنداز ۳.۰ → ۵۵ms (حدود ۷ برابر سریع‌تر)
  • str.replace(): پنداز ۲.۲ → ۵۲۰ms | پنداز ۳.۰ → ۹۰ms (حدود ۶ برابر سریع‌تر)
  • str.len(): پنداز ۲.۲ → ۲۸۰ms | پنداز ۳.۰ → ۳۰ms (حدود ۹ برابر سریع‌تر)
  • مصرف حافظه: پنداز ۲.۲ → ۱۲۸MB | پنداز ۳.۰ → ۶۲MB (۵۲ درصد کمتر)

این اعداد واقعاً قابل توجهن. مخصوصاً اگه با دیتاست‌های بزرگ رشته‌ای کار می‌کنید.

مقایسه عملکرد Copy-on-Write

CoW هم تأثیر مثبتی روی عملکرد داره، به خصوص وقتی کپی‌های زیادی از دیتافریم دارید:

import pandas as pd
import time

# ایجاد دیتافریم بزرگ
df = pd.DataFrame({
    f"col_{i}": range(1_000_000) for i in range(50)
})

# سناریو: ایجاد چندین "نما" بدون تغییر
start = time.time()
views = []
for i in range(100):
    subset = df[df.columns[:25]]  # انتخاب ۲۵ ستون
    views.append(subset)
elapsed = time.time() - start

print(f"Time for 100 subset operations: {elapsed:.3f} seconds")
# در پنداز ۳.۰ (با CoW): بسیار سریع‌تر چون کپی انجام نمی‌شود
# در پنداز ۲.x (بدون CoW): کندتر چون هر بار کپی ایجاد می‌شد

مقایسه با Polars

سؤالی که خیلی‌ها می‌پرسن: «حالا فاصله پنداز با Polars چقدره؟» خب، باید بگم با تغییرات ۳.۰ فاصله کم شده ولی هنوز توی خیلی از سناریوها Polars سریع‌تره. با این حال، پنداز مزایای خاص خودش رو داره:

  • اکوسیستم گسترده‌تر: پنداز با هزاران کتابخانه دیگه سازگاره
  • جامعه کاربری بزرگ‌تر: پیدا کردن راه‌حل و کمک آسون‌تره
  • مستندات جامع‌تر: سال‌ها تجربه و مستندسازی پشتشه
  • یادگیری آسان‌تر: منابع آموزشی فراوان‌تری وجود داره
  • عملکرد بهبود یافته: با PyArrow و CoW، شکاف عملکردی خیلی کمتر شده

انتخاب بین پنداز و Polars واقعاً به پروژه‌تون بستگی داره. برای خیلی از پروژه‌ها، پنداز ۳.۰ کاملاً کافی و عالیه.

آزمایش عملکرد خواندن فایل CSV

خواندن CSV هم سریع‌تر شده، مخصوصاً وقتی فایل پر از ستون‌های رشته‌ایه:

import pandas as pd
import time

# خواندن یک فایل CSV بزرگ با ستون‌های رشته‌ای
start = time.time()
df = pd.read_csv("large_dataset.csv")
elapsed = time.time() - start
print(f"Read time: {elapsed:.2f} seconds")
print(f"Memory: {df.memory_usage(deep=True).sum() / 1e6:.1f} MB")
print(f"Dtypes:\n{df.dtypes}")

# در پنداز ۳.۰ ستون‌های رشته‌ای به صورت خودکار
# با نوع str (مبتنی بر PyArrow) خوانده می‌شوند
# که هم سریع‌تر و هم کم‌حافظه‌تر است

تغییرات دیگر و قابلیت‌های حذف‌شده

قابلیت‌های منسوخ‌شده و حذف‌شده

پنداز ۳.۰ یه خونه‌تکانی حسابی کرده و کلی قابلیت منسوخ رو حذف کرده. مهم‌ترینا:

  • DataFrame.swaplevel و DataFrame.reorder_levels بدون آرگومان axis: باید از پارامتر axis استفاده کنید
  • پارامتر inplace در برخی متدها: روند حذف تدریجی ادامه داره
  • DataFrame.append: کاملاً حذف شده — از pd.concat() استفاده کنید
  • Series.dt.to_pydatetime(): رفتارش به خاطر تغییر دقت زمانی عوض شده

بهبود در GroupBy و عملیات تجمیعی

عملیات groupby هم بهبودهای عملکردی خوبی داشته. با بک‌اند PyArrow، عملیات تجمیعی روی ستون‌های رشته‌ای سریع‌تر اجرا میشن:

import pandas as pd

df = pd.DataFrame({
    "category": ["A", "B", "A", "C", "B", "A", "C", "B"],
    "subcategory": ["x", "y", "x", "z", "y", "z", "x", "y"],
    "value": [100, 200, 150, 300, 250, 180, 350, 220]
})

# عملیات گروه‌بندی — در پنداز ۳.۰ بهینه‌تر
result = (
    df
    .groupby(["category", "subcategory"])
    .agg(
        total_value=("value", "sum"),
        avg_value=("value", "mean"),
        count=("value", "count")
    )
    .reset_index()
    .sort_values("total_value", ascending=False)
)

print(result)

بهترین شیوه‌ها برای کار با پنداز ۳.۰

با توجه به تغییرات بنیادین، این نکات رو رعایت کنید:

  1. همیشه از .loc و .iloc استفاده کنید:

    برای دسترسی و تغییر داده‌ها، از ایندکسرهای صریح .loc و .iloc استفاده کنید. از انتساب زنجیره‌ای کاملاً پرهیز کنید.

  2. از pd.col() برای زنجیره‌سازی استفاده کنید:

    هر جایی که قبلاً لامبدا توی .assign() می‌نوشتید، به pd.col() مهاجرت کنید. کدتون خواناتر و قابل نگهداری‌تر میشه.

  3. از pd.api.types برای بررسی نوع داده استفاده کنید:

    به جای مقایسه مستقیم dtype با رشته‌ها، از توابع کمکی pd.api.types استفاده کنید.

  4. مصرف حافظه رو نظارت کنید:

    با وجود بهبودها، همیشه با df.memory_usage(deep=True) مصرف حافظه رو چک کنید — مخصوصاً برای دیتاست‌های بزرگ.

  5. تست‌ها رو به‌روز نگه دارید:

    مطمئن بشید تست‌هاتون سناریوهای CoW، نوع رشته‌ای جدید و دقت زمانی رو پوشش میدن.

مثال‌های عملی جامع

مثال ۱: پایپ‌لاین تحلیل داده با ویژگی‌های جدید

بیاید یه پایپ‌لاین کامل تحلیل داده رو با ویژگی‌های جدید پنداز ۳.۰ بنویسیم:

import pandas as pd

# ایجاد داده نمونه
sales_data = pd.DataFrame({
    "date": pd.date_range("2025-01-01", periods=1000, freq="D"),
    "product": ["Laptop", "Phone", "Tablet", "Monitor", "Keyboard"] * 200,
    "region": ["Tehran", "Isfahan", "Shiraz", "Tabriz", "Mashhad"] * 200,
    "revenue": [1200, 800, 400, 350, 50] * 200,
    "units_sold": [1, 3, 2, 1, 10] * 200,
    "customer_name": [f"Customer_{i}" for i in range(1000)]
})

# بررسی نوع داده‌ها — رشته‌ها با نوع str هستند
print(sales_data.dtypes)

# پایپ‌لاین تحلیلی با pd.col() و زنجیره‌سازی متدها
result = (
    sales_data
    .assign(
        avg_price=pd.col("revenue") / pd.col("units_sold"),
        year=pd.col("date").dt.year,
        month=pd.col("date").dt.month,
        is_high_value=pd.col("revenue") > 500
    )
    .query("is_high_value == True")
    .groupby(["product", "region"])
    .agg(
        total_revenue=("revenue", "sum"),
        total_units=("units_sold", "sum"),
        avg_unit_price=("avg_price", "mean"),
        num_transactions=("revenue", "count")
    )
    .reset_index()
    .sort_values("total_revenue", ascending=False)
)

print(result.head(10))

مثال ۲: تمیزسازی داده با الگوهای جدید

import pandas as pd

# داده‌های خام با مشکلات رایج
raw_data = pd.DataFrame({
    "name": ["  Ali  ", "MARYAM", "hossein", None, "Zahra "],
    "email": ["[email protected]", "[email protected]", None, "invalid", "[email protected]"],
    "salary": ["5000", "4500", "5500", "N/A", "4000"],
    "join_date": ["2020-01-15", "2019-06-20", "2018-03-10", "2021-11-05", "2017-08-25"]
})

# تمیزسازی با ویژگی‌های پنداز ۳.۰
cleaned = (
    raw_data
    .assign(
        # تمیزسازی نام — عملیات رشته‌ای روی نوع str سریع‌تر است
        name=lambda x: x["name"].str.strip().str.title(),
        # تبدیل ایمیل به حروف کوچک
        email=lambda x: x["email"].str.lower(),
        # تبدیل حقوق به عددی
        salary=lambda x: pd.to_numeric(x["salary"], errors="coerce"),
        # تبدیل تاریخ — با دقت میکروثانیه پیش‌فرض
        join_date=lambda x: pd.to_datetime(x["join_date"])
    )
    .dropna(subset=["name", "email"])  # حذف ردیف‌های با نام یا ایمیل خالی
)

print(cleaned)
print(cleaned.dtypes)
# name          str          ← نوع جدید
# email         str          ← نوع جدید
# salary      float64
# join_date   datetime64[us] ← دقت میکروثانیه

سؤالات متداول

آیا کد قدیمی من در پنداز ۳.۰ کار می‌کند؟

بستگی داره. اگه کدتون از انتساب زنجیره‌ای استفاده نمی‌کنه، نوع داده رو مستقیماً مقایسه نمی‌کنه و به دقت نانوثانیه وابسته نیست، احتمالاً بدون تغییر کار خواهد کرد. ولی توصیه اکید اینه که اول روی نسخه ۲.۳ تست کنید و هشدارها رو رفع کنید.

آیا می‌توانم CoW را غیرفعال کنم؟

نه. توی پنداز ۳.۰، Copy-on-Write تنها حالت کاریه و غیرفعال نمیشه. اگه کدتون به رفتار قدیمی وابسته‌ست، باید بازنویسیش کنید.

آیا PyArrow ضروری است؟

به شدت توصیه شده و برای نوع رشته‌ای جدید و خیلی از بهینه‌سازی‌های داخلی لازمه. خبر خوب اینه که موقع نصب پنداز ۳.۰ با pip، معمولاً خودکار نصب میشه.

آیا pd.col() جایگزین توابع لامبدا می‌شود؟

نه کاملاً. pd.col() برای عملیات ساده روی ستون‌ها عالیه، ولی برای منطق پیچیده‌تر (مثل فراخوانی توابع سفارشی یا شرط‌های پیچیده) هنوز ممکنه به لامبدا نیاز داشته باشید. البته با توسعه این قابلیت توی نسخه‌های بعدی، انتظار میره پوشش گسترده‌تری داشته باشه.

نتیجه‌گیری

پنداز ۳.۰ واقعاً یه نقطه عطف مهم توی تاریخ این کتابخانه محبوبه. Copy-on-Write به عنوان رفتار پیش‌فرض، خیلی از مشکلات تاریخی مربوط به نماها و کپی‌ها رو حل کرده و کد نوشته‌شده با پنداز قابل اطمینان‌تر شده.

نوع داده رشته‌ای مبتنی بر PyArrow هم یه جهش بزرگ در عملکرده. سرعت ۵ تا ۱۰ برابری و ۵۰ درصد حافظه کمتر — دیگه نیازی نیست برای عملیات رشته‌ای ساده به ابزارهای خارجی مراجعه کنید.

pd.col() هم گام بلندی توی بهبود تجربه توسعه‌دهنده و خوانایی کده. صادقانه بگم، خوشحالم که تیم پنداز از Polars یاد گرفته و بهترین ایده‌ها رو اقتباس کرده.

تغییر دقت زمانی به میکروثانیه هم مشکل قدیمی محدودیت بازه زمانی رو حل کرده.

برای مهاجرت موفق، فراموش نکنید:

  1. اول به نسخه ۲.۳ ارتقا بدید و تمام هشدارها رو رفع کنید
  2. حالت CoW رو آزمایشی فعال کنید و کدتون رو بررسی کنید
  3. نوع رشته‌ای جدید رو تست کنید و بررسی‌های نوع داده رو آپدیت کنید
  4. تست‌های جامع اجرا کنید و بعد به ۳.۰ ارتقا بدید

اگه هنوز از نسخه‌های قدیمی‌تر استفاده می‌کنید، الان بهترین زمان برای برنامه‌ریزی مهاجرته. با یه سرمایه‌گذاری کوچیک توی آپدیت کد، از عملکرد بهتر، کد خواناتر و رفتار قابل پیش‌بینی‌تر بهره‌مند میشید.