19. Polars#

علاوه بر آنچه در Anaconda موجود است، این درس به کتابخانه‌های زیر نیاز دارد:

!pip install --upgrade polars yfinance

Hide code cell output

Collecting polars
  Downloading polars-1.43.2-py3-none-any.whl.metadata (11 kB)
Requirement already satisfied: yfinance in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (1.5.2)
Collecting polars-runtime-32==1.43.2 (from polars)
  Downloading polars_runtime_32-1.43.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.5 kB)
Requirement already satisfied: pandas>=1.3.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (3.0.3)
Requirement already satisfied: numpy>=1.16.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (2.4.6)
Requirement already satisfied: requests>=2.31 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (2.34.2)
Requirement already satisfied: multitasking>=0.0.7 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (0.0.13)
Requirement already satisfied: platformdirs>=2.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (4.9.4)
Requirement already satisfied: pytz>=2022.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (2026.3.post1)
Requirement already satisfied: peewee>=3.16.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (4.3.0)
Requirement already satisfied: beautifulsoup4>=4.11.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (4.15.0)
Requirement already satisfied: curl_cffi>=0.15 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (0.16.0)
Requirement already satisfied: protobuf>=3.19.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (6.33.5)
Requirement already satisfied: websockets>=13.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (16.0)
Requirement already satisfied: soupsieve>=1.6.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from beautifulsoup4>=4.11.1->yfinance) (2.8.4)
Requirement already satisfied: typing-extensions>=4.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from beautifulsoup4>=4.11.1->yfinance) (4.16.0)
Requirement already satisfied: cffi>=2.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from curl_cffi>=0.15->yfinance) (2.1.0)
Requirement already satisfied: certifi>=2024.2.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from curl_cffi>=0.15->yfinance) (2026.6.17)
Requirement already satisfied: pycparser in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from cffi>=2.0.0->curl_cffi>=0.15->yfinance) (3.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas>=1.3.0->yfinance) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from python-dateutil>=2.8.2->pandas>=1.3.0->yfinance) (1.17.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.31->yfinance) (3.4.7)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.31->yfinance) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.31->yfinance) (2.7.0)
Downloading polars-1.43.2-py3-none-any.whl (847 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/847.1 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 847.1/847.1 kB 23.4 MB/s  0:00:00
?25h
Downloading polars_runtime_32-1.43.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (57.3 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/57.3 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━ 30.9/57.3 MB 155.1 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 MB 128.3 MB/s  0:00:00
?25h
Installing collected packages: polars-runtime-32, polars
?25l
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0/2 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0/2 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0/2 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0/2 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━ 1/2 [polars]
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2/2 [polars]
Successfully installed polars-1.43.2 polars-runtime-32-1.43.2

19.1. مروری کلی#

Polars یک کتابخانه سریع دستکاری داده برای Python است که به زبان Rust نوشته شده است.

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

Polars با در نظر گرفتن عملکرد و کارایی حافظه طراحی شده و از موارد زیر بهره می‌برد:

  • قالب ستونی Apache Arrow برای دسترسی سریع به داده

  • ارزیابی تنبل برای بهینه‌سازی اجرای پرس‌وجو

  • پردازش موازی برای استفاده از تمام هسته‌های پردازنده در دسترس

  • یک رابط برنامه‌نویسی گویا که حول عبارات ستونی ساخته شده است

Tip

چرا Polars را به‌جای pandas در نظر بگیریم؟

  • حافظه: pandas معمولاً به ۵ تا ۱۰ برابر حجم مجموعه داده شما نیاز به RAM دارد؛ Polars تنها به ۲ تا ۴ برابر نیاز دارد

  • سرعت: Polars برای بسیاری از عملیات رایج ۱۰ تا ۱۰۰ برابر سریع‌تر است

  • مشاهده کنید: معیارهای TPC-H در Polars برای مقایسه‌های عملکردی به‌روز

در طول این درس، فرض می‌کنیم که واردسازی‌های زیر انجام شده است

import polars as pl
import numpy as np
import matplotlib.pyplot as plt

مانند Pandas، Polars دو نوع داده مهم تعریف می‌کند: Series و DataFrame.

می‌توانید Series را به عنوان یک ستون داده در نظر بگیرید، مانند مجموعه‌ای از مشاهدات یک متغیر واحد.

DataFrame یک شیء دوبعدی برای ذخیره ستون‌های مرتبط داده است.

19.2. Series#

بیایید با Series شروع کنیم.

ابتدا سری‌ای از چهار مشاهده تصادفی می‌سازیم

s = pl.Series(name='daily returns', values=np.random.randn(4))
s
shape: (4,)
daily returns
f64
1.486557
1.118068
0.38464
0.156635

Note

برخلاف Series در pandas، Series در Polars هیچ نمایه ردیفی ندارند. Polars ستون‌محور است — دسترسی به داده از طریق عبارات ستونی و ماسک‌های بولی مدیریت می‌شود، نه برچسب‌های ردیف. برای جزئیات بیشتر راهنمای مهاجرت Polars برای کاربران pandas را ببینید.

Series در Polars بر پایه آرایه‌های Apache Arrow ساخته شده‌اند و از بسیاری از عملیات آشنا پشتیبانی می‌کنند

s * 100
shape: (4,)
daily returns
f64
148.655749
111.806849
38.464045
15.663515

مقادیر مطلق به عنوان یک متد در دسترس هستند

s.abs()
shape: (4,)
daily returns
f64
1.486557
1.118068
0.38464
0.156635

همچنین می‌توانیم آمار خلاصه سریع دریافت کنیم

s.describe()
shape: (9, 2)
statisticvalue
strf64
"count"4.0
"null_count"0.0
"mean"0.786475
"std"0.621353
"min"0.156635
"25%"0.38464
"50%"1.118068
"75%"1.118068
"max"1.486557

از آنجا که Polars هیچ نمایه ردیفی ندارد، داده‌های برچسب‌دار به DataFrame نیاز دارند.

برای مثال، برای مرتبط کردن نمادهای معاملاتی با بازده‌ها:

df = pl.DataFrame({
    'company': ['AMZN', 'AAPL', 'MSFT', 'GOOG'],
    'daily returns': np.random.randn(4)
})
df
shape: (4, 2)
companydaily returns
strf64
"AMZN"-0.582233
"AAPL"-0.133717
"MSFT"0.044996
"GOOG"-0.87905

با فیلتر کردن بر روی یک عبارت ستونی به یک مقدار دسترسی پیدا می‌کنیم

df.filter(
    pl.col('company') == 'AMZN'
).select('daily returns').item()
-0.5822325458777632

به‌روزرسانی‌ها نیز از عبارات به‌جای تخصیص نمایه استفاده می‌کنند

df = df.with_columns(
    pl.when(pl.col('company') == 'AMZN')
    .then(0)
    .otherwise(pl.col('daily returns'))
    .alias('daily returns')
)
df
shape: (4, 2)
companydaily returns
strf64
"AMZN"0.0
"AAPL"-0.133717
"MSFT"0.044996
"GOOG"-0.87905

می‌توانیم عضویت را نیز بررسی کنیم

'AAPL' in df['company']
True

19.3. DataFrameها#

در حالی که Series یک ستون منفرد از داده است، DataFrame چندین ستون است، یکی برای هر متغیر.

مانند Pandas، بیایید با داده‌های Penn World Tables کار کنیم.

این را با pl.read_csv می‌خوانیم

url = ('https://raw.githubusercontent.com/QuantEcon/'
       'lecture-python-programming/main/lectures/_static/'
       'lecture_specific/pandas/data/test_pwt.csv')
df = pl.read_csv(url)
df
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.6530.9995295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.8942364.43645110.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.93983227242.3694972.718715.726546
"United States""USA"2000282171.9571.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

19.3.1. انتخاب داده#

می‌توانیم ردیف‌ها را با برش‌دهی و ستون‌ها را با نام انتخاب کنیم

df[2:5]
shape: (3, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.8942364.43645110.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954

برای انتخاب ستون‌های خاص، فهرستی از نام‌ها را به select بدهید

df.select(['country', 'tcgdp'])
shape: (8, 2)
countrytcgdp
strf64
"Argentina"295072.21869
"Australia"541804.6521
"India"1.7281e6
"Israel"129253.89423
"Malawi"5026.221784
"South Africa"227242.36949
"United States"9.8987e6
"Uruguay"25255.961693

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

df[2:5].select(['country', 'tcgdp'])
shape: (3, 2)
countrytcgdp
strf64
"India"1.7281e6
"Israel"129253.89423
"Malawi"5026.221784

19.3.2. فیلتر کردن بر اساس شرایط#

متد filter عبارات بولی ساخته‌شده از pl.col را می‌پذیرد

df.filter(pl.col('POP') >= 20000)
shape: (4, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.6530.9995295072.2186975.7168055.578804
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"South Africa""ZAF"200045064.0986.93983227242.3694972.718715.726546
"United States""USA"2000282171.9571.09.8987e672.3470546.032454

چندین شرط می‌توانند با & (و) و | (یا) ترکیب شوند

df.filter(
    (pl.col('country').is_in(['Argentina', 'India', 'South Africa'])) &
    (pl.col('POP') > 40000)
)
shape: (2, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"South Africa""ZAF"200045064.0986.93983227242.3694972.718715.726546

عبارات می‌توانند شامل عملیات حسابی بین ستون‌ها باشند

df.filter(
    (pl.col('cc') + pl.col('cg') >= 80) & (pl.col('POP') <= 20000)
)
shape: (2, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

کشوری با بزرگ‌ترین سهم مصرف خانوار را انتخاب کنید

df.filter(pl.col('cc') == pl.col('cc').max())
shape: (1, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

19.3.3. عبارات ستونی#

یک تفاوت کلیدی با pandas این است که Polars از عبارات ستونی برای تبدیل‌ها استفاده می‌کند، نه فراخوانی‌های عنصر به عنصر apply.

در اینجا مثالی برای محاسبه بیشینه هر ستون عددی آورده شده است

df.select(
    pl.col(['year', 'POP', 'XRAT', 'tcgdp', 'cc', 'cg'])
    .max()
    .name.suffix('_max')
)
shape: (1, 6)
year_maxPOP_maxXRAT_maxtcgdp_maxcc_maxcg_max
i64f64f64f64f64f64
20001.0063e659.5438089.8987e678.9787414.072206

عبارات می‌توانند در داخل with_columns برای افزودن یا تغییر ستون‌ها استفاده شوند

df.with_columns(
    (pl.col('XRAT') / 10).alias('XRAT_scaled'),
    pl.col(pl.Float64).round(2)
)
shape: (8, 9)
countrycountry isocodeyearPOPXRATtcgdpcccgXRAT_scaled
strstri64f64f64f64f64f64f64
"Argentina""ARG"200037335.651.0295072.2275.725.580.09995
"Australia""AUS"200019053.191.72541804.6567.766.720.172483
"India""IND"20001006300.344.941.7281e664.5814.074.49416
"Israel""ISR"20006114.574.08129253.8964.4410.270.407733
"Malawi""MWI"200011801.559.545026.2274.7111.665.954381
"South Africa""ZAF"200045064.16.94227242.3772.725.730.693983
"United States""USA"2000282171.961.09.8987e672.356.030.1
"Uruguay""URY"20003219.7912.125255.9678.985.111.209959

منطق شرطی از pl.when(...).then(...).otherwise(...) استفاده می‌کند

df.with_columns(
    pl.when(pl.col('POP') >= 20000)
    .then(pl.col('POP'))
    .otherwise(None)
    .alias('POP_filtered')
).select(['country', 'POP', 'POP_filtered'])
shape: (8, 3)
countryPOPPOP_filtered
strf64f64
"Argentina"37335.65337335.653
"Australia"19053.186null
"India"1.0063e61.0063e6
"Israel"6114.57null
"Malawi"11801.505null
"South Africa"45064.09845064.098
"United States"282171.957282171.957
"Uruguay"3219.793null

Note

Polars map_elements را به عنوان راه فراری برای اعمال توابع دلخواه Python به‌صورت ردیف‌به‌ردیف ارائه می‌دهد، اما این کار موتور بهینه‌شده عبارات را دور می‌زند و در صورت وجود عبارت بومی باید از آن اجتناب شود.

19.3.4. مقادیر گم‌شده#

بیایید برخی مقادیر تهی را برای نمایش تکنیک‌های جایگذاری وارد کنیم

df_nulls = df.with_row_index().with_columns(
    pl.when(pl.col('index') == 0)
    .then(None).otherwise(pl.col('XRAT')).alias('XRAT'),
    pl.when(pl.col('index') == 3)
    .then(None).otherwise(pl.col('cc')).alias('cc'),
    pl.when(pl.col('index') == 5)
    .then(None).otherwise(pl.col('tcgdp')).alias('tcgdp'),
    pl.when(pl.col('index') == 6)
    .then(None).otherwise(pl.col('POP')).alias('POP'),
).drop('index')
df_nulls
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.653null295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.89423null10.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.93983null72.718715.726546
"United States""USA"2000null1.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

تمام مقادیر تهی را با صفر پر کنید

df_nulls.fill_null(0)
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.6530.0295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.894230.010.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.939830.072.718715.726546
"United States""USA"20000.01.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

یا با میانگین‌های ستونی پر کنید

cols = ['cc', 'tcgdp', 'POP', 'XRAT']
df_nulls.with_columns(
    pl.col(cols).fill_null(pl.col(cols).mean())
)
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.65318.618141295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.8942372.40050210.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.939831.8033e672.718715.726546
"United States""USA"2000161269.8717141.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

Polars همچنین از پر کردن رو به جلو (fill_null(strategy='forward')) و درون‌یابی پشتیبانی می‌کند.

ابزارهای جایگذاری پیشرفته‌تری در scikit-learn در دسترس هستند.

19.3.5. تجسم‌سازی#

بیایید یک ستون تولید ناخالص داخلی سرانه بسازیم و آن را رسم کنیم

df = (df
    .select(['country', 'POP', 'tcgdp'])
    .rename({'POP': 'population', 'tcgdp': 'total GDP'})
    .with_columns(
        (pl.col('population') * 1e3).alias('population')
    )
    .with_columns(
        (pl.col('total GDP') * 1e6 / pl.col('population'))
        .alias('GDP percap')
    )
    .sort('GDP percap', descending=True)
)
df
shape: (8, 4)
countrypopulationtotal GDPGDP percap
strf64f64f64
"United States"2.82171957e89.8987e635080.381854
"Australia"1.9053186e7541804.652128436.433261
"Israel"6.11457e6129253.8942321138.672749
"Argentina"3.7335653e7295072.218697903.229085
"Uruguay"3.219793e625255.9616937843.97062
"South Africa"4.5064098e7227242.369495042.647686
"India"1.0063e91.7281e61717.324719
"Malawi"1.1801505e75026.221784425.896679

می‌توانیم ستون‌ها را مستقیماً برای matplotlib استخراج کنیم

Note

Polars همچنین یک رابط برنامه‌نویسی رسم داخلی مبتنی بر Altair ارائه می‌دهد (به عنوان مثال، df.plot.bar(x=..., y=...)). ما در اینجا از matplotlib برای هماهنگی با بقیه سری درس‌ها استفاده می‌کنیم.

fig, ax = plt.subplots()
ax.bar(df['country'].to_list(), df['GDP percap'].to_list())
ax.set_xlabel('country', fontsize=12)
ax.set_ylabel('GDP per capita', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
_images/1086660a9c88f1b6fbcf4ee7a74cff8d6f85f83902ebd67b49e16bc20f052ab0.png

19.4. ارزیابی تنبل#

یکی از قدرتمندترین ویژگی‌های Polars ارزیابی تنبل است.

به‌جای اجرای فوری هر عملیات، حالت تنبل کل برنامه پرس‌وجو را جمع‌آوری کرده و پیش از اجرا آن را بهینه می‌کند.

19.4.1. مشتاق در برابر تنبل#

# Reload the dataset
url = ('https://raw.githubusercontent.com/QuantEcon/'
       'lecture-python-programming/main/lectures/_static/'
       'lecture_specific/pandas/data/test_pwt.csv')
df_full = pl.read_csv(url)

رابط برنامه‌نویسی مشتاق (eager) بلافاصله اجرا می‌شود (مانند pandas)

result_eager = (df_full
    .filter(pl.col('tcgdp') > 1000)
    .select(['country', 'year', 'tcgdp'])
    .sort('tcgdp', descending=True)
)
result_eager.head()
shape: (5, 3)
countryyeartcgdp
stri64f64
"United States"20009.8987e6
"India"20001.7281e6
"Australia"2000541804.6521
"Argentina"2000295072.21869
"South Africa"2000227242.36949

رابط برنامه‌نویسی تنبل (lazy) در عوض یک برنامه پرس‌وجو می‌سازد

lazy_query = (df_full.lazy()
    .filter(pl.col('tcgdp') > 1000)
    .select(['country', 'year', 'tcgdp'])
    .sort('tcgdp', descending=True)
)
print(lazy_query.explain())
SORT BY [descending: [true]] [col("tcgdp")]
  FILTER col("tcgdp") > 1000.0
  FROM
    DF ["country", "country isocode", "year", "POP", ...]; PROJECT["country", "year", "tcgdp"] 3/8 COLUMNS

برای اجرای برنامه، collect را فراخوانی کنید

result_lazy = lazy_query.collect()
result_lazy.head()
shape: (5, 3)
countryyeartcgdp
stri64f64
"United States"20009.8987e6
"India"20001.7281e6
"Australia"2000541804.6521
"Argentina"2000295072.21869
"South Africa"2000227242.36949

19.4.2. بهینه‌سازی پرس‌وجو#

موتور تنبل به‌طور خودکار چندین بهینه‌سازی را اعمال می‌کند:

  • فشار محمول (predicate pushdown) — فیلترها در اسرع وقت اعمال می‌شوند

  • فشار تصویر (projection pushdown) — فقط ستون‌های مورد نیاز از منبع خوانده می‌شوند

  • حذف زیرعبارت مشترک — محاسبات تکراری ادغام می‌شوند

بیایید ببینیم Polars چگونه یک پرس‌وجوی چندمرحله‌ای را بازنویسی می‌کند

optimized = (df_full.lazy()
    .select(['country', 'year', 'tcgdp', 'POP'])
    .filter(pl.col('tcgdp') > 500)
    .with_columns(
        (pl.col('tcgdp') / pl.col('POP')).alias('gdp_per_capita')
    )
    .filter(pl.col('gdp_per_capita') > 10)
    .select(['country', 'year', 'gdp_per_capita'])
)

print("Optimized plan:")
print(optimized.explain())
Optimized plan:
FILTER col("gdp_per_capita") > 10.0
FROM
  simple π 3/3 ["country", "year", ... 1 other column]
     WITH_COLUMNS:
     [(col("tcgdp") / col("POP")).alias("gdp_per_capita")] 
      FILTER col("tcgdp") > 500.0
      FROM
        DF ["country", "country isocode", "year", "POP", ...]; PROJECT["country", "year", "tcgdp", "POP"] 4/8 COLUMNS

اجرای برنامه نتیجه نهایی را به ما می‌دهد

optimized.collect()
shape: (3, 3)
countryyeargdp_per_capita
stri64f64
"Australia"200028.436433
"Israel"200021.138673
"United States"200035.080382

19.4.3. مقایسه عملکرد#

بیایید pandas، Polars مشتاق، و Polars تنبل را روی همان کار مقایسه کنیم.

ابتدا با یک مجموعه داده کوچک (همان Penn World Tables که در بالا استفاده کردیم) شروع می‌کنیم تا نشان دهیم که برای داده‌های کوچک تفاوت‌ها ناچیز هستند

import pandas as pd
import time

# Small dataset -- Penn World Tables (~8 rows)
url = ('https://raw.githubusercontent.com/QuantEcon/'
       'lecture-python-programming/main/lectures/_static/'
       'lecture_specific/pandas/data/test_pwt.csv')
small_pd = pd.read_csv(url)
small_pl = pl.read_csv(url)

اکنون همان عملیات فیلتر-انتخاب-مرتب‌سازی را در هر کتابخانه زمان‌بندی می‌کنیم

# pandas
start = time.perf_counter()
_ = (small_pd
     .query('tcgdp > 500')
     [['country', 'year', 'tcgdp', 'POP']]
     .assign(gdp_pc=lambda d: d['tcgdp'] / d['POP'])
     .sort_values('gdp_pc', ascending=False))
pd_small = time.perf_counter() - start

# Polars eager
start = time.perf_counter()
_ = (small_pl
     .filter(pl.col('tcgdp') > 500)
     .select(['country', 'year', 'tcgdp', 'POP'])
     .with_columns((pl.col('tcgdp') / pl.col('POP')).alias('gdp_pc'))
     .sort('gdp_pc', descending=True))
pl_small = time.perf_counter() - start

print(f"Small data  --  pandas: {pd_small:.4f}s | Polars eager: {pl_small:.4f}s")
Small data  --  pandas: 0.0055s | Polars eager: 0.0010s

روی تعداد اندکی ردیف، تفاوت سرعت ناچیز است — از هر رابط برنامه‌نویسی که راحت‌تر می‌یابید استفاده کنید.

اکنون بیایید مقیاس را به ۵ میلیون ردیف بزرگ کنیم که تفاوت واضح می‌شود.

کار این است: فیلتر کردن ردیف‌هایی که value > 0، محاسبه یک حاصل‌ضرب وزنی value * weight، سپس گرفتن میانگین آن حاصل‌ضرب در هر گروه — یک میانگین وزنی گروه‌بندی‌شده.

n = 5_000_000
np.random.seed(42)

groups = np.random.choice(['A', 'B', 'C', 'D'], n)
values = np.random.randn(n)
weights = np.random.rand(n)
extra1 = np.random.randn(n)
extra2 = np.random.randn(n)

big_pd = pd.DataFrame({
    'group': groups, 'value': values,
    'weight': weights, 'extra1': extra1, 'extra2': extra2
})
big_pl = pl.DataFrame({
    'group': groups, 'value': values,
    'weight': weights, 'extra1': extra1, 'extra2': extra2
})

ابتدا، خط پایه pandas

start = time.perf_counter()
tmp = big_pd[big_pd['value'] > 0][['group', 'value', 'weight']].copy()
tmp['weighted'] = tmp['value'] * tmp['weight']
_ = tmp.groupby('group')['weighted'].mean()
pd_time = time.perf_counter() - start
print(f"pandas:       {pd_time:.4f}s")
pandas:       0.1233s

سپس، Polars در حالت مشتاق

start = time.perf_counter()
_ = (big_pl
    .filter(pl.col('value') > 0)
    .select(['group', 'value', 'weight'])
    .with_columns(
        (pl.col('value') * pl.col('weight')).alias('weighted'))
    .group_by('group')
    .agg(pl.col('weighted').mean()))
eager_time = time.perf_counter() - start
print(f"Polars eager: {eager_time:.4f}s")
Polars eager: 0.0350s

و در نهایت، Polars در حالت تنبل

start = time.perf_counter()
_ = (big_pl.lazy()
    .filter(pl.col('value') > 0)
    .select(['group', 'value', 'weight'])
    .with_columns(
        (pl.col('value') * pl.col('weight')).alias('weighted'))
    .group_by('group')
    .agg(pl.col('weighted').mean())
    .collect())
lazy_time = time.perf_counter() - start
print(f"Polars lazy:  {lazy_time:.4f}s")
Polars lazy:  0.0292s

نکته کلیدی:

  • برای داده‌های کوچک (هزاران ردیف)، pandas و Polars به‌طور مشابهی عمل می‌کنند — بر اساس ترجیح رابط برنامه‌نویسی و تناسب اکوسیستم انتخاب کنید.

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

رابط برنامه‌نویسی تنبل هنگام خواندن از دیسک به‌ویژه قدرتمند است — scan_csv مستقیماً یک LazyFrame برمی‌گرداند، بنابراین فیلترها و تصویرها به خواننده فایل فشار داده می‌شوند.

Tip

از pl.scan_csv(path) به‌جای pl.read_csv(path) هنگام کار با فایل‌های CSV بزرگ استفاده کنید. فقط ستون‌ها و ردیف‌هایی که واقعاً به آن‌ها نیاز دارید از دیسک خوانده می‌شوند. مستندات ورودی/خروجی Polars را ببینید.

19.5. منابع داده آنلاین#

مانند Pandas، Python پرس‌وجوی پایگاه‌های داده آنلاین را ساده می‌کند.

یک پایگاه داده مهم برای اقتصاددانان FRED است — مجموعه‌ای گسترده از داده‌های سری زمانی که توسط فدرال رزرو سنت‌لوئیس نگهداری می‌شود.

متد read_csv در Polars می‌تواند داده را مستقیماً از یک URL دریافت کند.

از try_parse_dates=True برای تجزیه خودکار ستون تاریخ استفاده می‌کنیم

fred_url = ('https://fred.stlouisfed.org/graph/fredgraph.csv?'
            'bgcolor=%23e1e9f0&chart_type=line&drp=0&'
            'fo=open%20sans&graph_bgcolor=%23ffffff&'
            'height=450&mode=fred&recession_bars=on&'
            'txtcolor=%23444444&ts=12&tts=12&width=1318&'
            'nt=0&thu=0&trc=0&show_legend=yes&'
            'show_axis_titles=yes&show_tooltip=yes&'
            'id=UNRATE&scale=left&cosd=1948-01-01&'
            'coed=2024-06-01&line_color=%234572a7&'
            'link_values=false&line_style=solid&'
            'mark_type=none&mw=3&lw=2&ost=-99999&'
            'oet=99999&mma=0&fml=a&fq=Monthly&fam=avg&'
            'fgst=lin&fgsnd=2020-02-01&line_index=1&'
            'transformation=lin&vintage_date=2024-07-29&'
            'revision_date=2024-07-29&nd=1948-01-01')
data = pl.read_csv(fred_url, try_parse_dates=True)

بیایید چند ردیف اول را بررسی کنیم

data.head()
shape: (5, 2)
observation_dateUNRATE
datef64
1948-01-013.4
1948-02-013.8
1948-03-014.0
1948-04-013.9
1948-05-013.5

و آمار خلاصه را دریافت کنیم

data.describe()
shape: (9, 3)
statisticobservation_dateUNRATE
strstrf64
"count""918"918.0
"null_count""0"0.0
"mean""1986-03-17 06:30:35.294117"5.693246
"std"null1.710248
"min""1948-01-01"2.5
"25%""1967-02-01"4.4
"50%""1986-04-01"5.5
"75%""2005-05-01"6.7
"max""2024-06-01"14.8

نرخ بیکاری از ۲۰۰۶ تا ۲۰۱۲ را رسم کنید

filtered = data.filter(
    (pl.col('observation_date') >= pl.date(2006, 1, 1)) &
    (pl.col('observation_date') <= pl.date(2012, 12, 31))
)

fig, ax = plt.subplots()
ax.plot(filtered['observation_date'].to_list(),
        filtered['UNRATE'].to_list())
ax.set_title('US Unemployment Rate')
ax.set_xlabel('year', fontsize=12)
ax.set_ylabel('%', fontsize=12)
plt.show()
_images/a61de207bcb9903916c9e95a2444a28fad81a433fb0183bbe235b91ff888599c.png

Polars از فرمت‌های فایل بسیاری از جمله Excel، JSON، Parquet، و اتصالات مستقیم پایگاه داده پشتیبانی می‌کند.

19.6. تمرین‌ها#

Exercise 19.1

با این واردسازی‌ها:

import datetime as dt
import yfinance as yf

برنامه‌ای بنویسید که تغییر درصدی قیمت را در طول سال ۲۰۲۱ برای سهام‌های زیر محاسبه کند:

ticker_list = {'INTC': 'Intel',
               'MSFT': 'Microsoft',
               'IBM': 'IBM',
               'BHP': 'BHP',
               'TM': 'Toyota',
               'AAPL': 'Apple',
               'AMZN': 'Amazon',
               'C': 'Citigroup',
               'QCOM': 'Qualcomm',
               'KO': 'Coca-Cola',
               'GOOG': 'Google'}

در اینجا تابعی است که قیمت‌های بسته شدن را در یک DataFrame Polars می‌خواند:

def read_data_polars(ticker_list,
                     start=dt.datetime(2021, 1, 1),
                     end=dt.datetime(2021, 12, 31)):
    """
    Read closing price data from Yahoo Finance
    and return a Polars DataFrame.
    """
    dataframes = []

    for tick in ticker_list:
        stock = yf.Ticker(tick)
        prices = stock.history(start=start, end=end)
        df = pl.DataFrame({
            'Date': list(prices.index.date),
            tick: prices['Close'].values
        }).with_columns(pl.col('Date').cast(pl.Date))
        dataframes.append(df)

    result = dataframes[0]
    for df in dataframes[1:]:
        result = result.join(
            df, on='Date', how='full', coalesce=True
        )
    return result.sort('Date')

ticker = read_data_polars(ticker_list)

Note

اتصال‌های Polars ترتیب ردیف‌های خروجی را تضمین نمی‌کنند — کلیدهایی که فقط در یک طرف مطابقت دارند به‌جای قرار گرفتن در جای خود، اضافه می‌شوند. این همان تم “بدون نمایه، بدون تراز خودکار” از بالاست: بدون برچسب‌های ردیف برای تراز کردن، ترتیب چیزی است که به‌طور صریح درخواست می‌کنیم. به همین دلیل sort('Date') قبل از بازگشت قرار دارد، که هر محاسبه بعدی first()/last() به آن متکی است.

برنامه را برای رسم نتیجه به عنوان یک نمودار میله‌ای کامل کنید.

Exercise 19.2

با استفاده از read_data_polars از Exercise 19.1، تغییر درصدی سال به سال را برای این شاخص‌ها بدست آورید:

indices_list = {'^GSPC': 'S&P 500',
               '^IXIC': 'NASDAQ',
               '^DJI': 'Dow Jones',
               '^N225': 'Nikkei'}

نتیجه را به عنوان یک نمودار سری زمانی رسم کنید.