21 січня 2026 року команда pandas офіційно випустила pandas 3.0 — і це, чесно кажучи, найбільше мажорне оновлення за останні роки. Але це не просто «ще одна версія з новими функціями». Pandas 3.0 фундаментально змінює внутрішню поведінку бібліотеки, на яку покладаються мільйони дата-інженерів, аналітиків та науковців з даних по всьому світу.
Якщо ви працюєте з Python-екосистемою обробки даних — ця версія безпосередньо вплине на ваш код, ваші пайплайни та, можливо, навіть ваші звички.
У цьому посібнику ми детально розглянемо всі ключові зміни: новий рядковий тип даних за замовчуванням, обов'язковий режим Copy-on-Write, зміну роздільної здатності datetime, новий синтаксис виразів, інтеграцію з Apache Arrow та покрокову стратегію міграції. Кожен розділ містить практичні приклади коду, пояснення причин змін і конкретні поради. Що ж, давайте розбиратися.
Новий рядковий тип даних за замовчуванням
Одна з найпомітніших змін у pandas 3.0 — те, як бібліотека тепер обробляє текстові дані. Раніше рядки зберігалися з типом object, що було, м'яко кажучи, неефективно з точки зору пам'яті та продуктивності. Тепер pandas 3.0 використовує спеціалізований тип str (він же StringDtype) як тип за замовчуванням для текстових стовпців.
Ця зміна — прямий наслідок глибшої інтеграції з бекендом PyArrow, який забезпечує набагато ефективніше зберігання рядків у пам'яті.
Як було раніше (pandas 2.x)
У попередніх версіях створення DataFrame з текстовими даними автоматично призначало тип object. Кожен рядок зберігався як окремий Python-об'єкт, що спричиняло значне споживання пам'яті та уповільнювало операції:
# pandas 2.x поведінка
import pandas as pd
df = pd.DataFrame({"name": ["Олена", "Максим", "Ірина"],
"city": ["Київ", "Львів", "Одеса"]})
print(df.dtypes)
# name object
# city object
# dtype: object
Як стало тепер (pandas 3.0)
У pandas 3.0 ті самі дані автоматично отримують тип str, побудований на базі PyArrow:
# pandas 3.0 поведінка
import pandas as pd
df = pd.DataFrame({"name": ["Олена", "Максим", "Ірина"],
"city": ["Київ", "Львів", "Одеса"]})
print(df.dtypes)
# name str
# city str
# dtype: object
# Перевірка внутрішнього типу
print(type(df["name"].dtype))
# <class 'pandas.core.arrays.string_.StringDtype'>
Вплив на існуючий код
Ця зміна може зламати код, який явно перевіряє тип object. І повірте, таких місць у типовому проекті буває більше, ніж здається. Ось що потрібно оновити:
# Старий код (pandas 2.x) — ЗЛАМАЄТЬСЯ
if df["name"].dtype == "object":
print("Це текстовий стовпець")
# Новий код (pandas 3.0) — ПРАВИЛЬНО
if pd.api.types.is_string_dtype(df["name"]):
print("Це текстовий стовпець")
# Або альтернативний варіант
if df["name"].dtype == "str" or df["name"].dtype == pd.StringDtype():
print("Це текстовий стовпець")
Поведінка з пропущеними значеннями
Важлива перевага нового типу str — коректна обробка пропущених значень. Замість NaN (який технічно є числом з плаваючою комою — і це завжди виглядало дивно для текстових даних) тепер використовується pd.NA:
import pandas as pd
import numpy as np
df = pd.DataFrame({"name": ["Олена", None, "Ірина"]})
# pandas 2.x: значення None перетворювалось на NaN (float)
# pandas 3.0: значення None стає pd.NA
print(df["name"].isna())
# 0 False
# 1 True
# 2 False
print(df["name"][1])
# <NA> (замість NaN)
Поради з міграції рядкових типів
- Замініть усі перевірки
dtype == "object"наpd.api.types.is_string_dtype(). - Якщо ваш код залежить від
NaNу рядкових стовпцях, оновіть його для роботи зpd.NA. - Використовуйте
df.convert_dtypes()для автоматичного перетворення типів під час міграції. - Зверніть увагу на серіалізацію: Parquet та Arrow працюватимуть без змін, але деякі CSV-пайплайни можуть потребувати оновлення.
Copy-on-Write (CoW) як обов'язковий режим
Copy-on-Write (CoW) — механізм управління пам'яттю, який кардинально змінює спосіб роботи pandas з даними. У pandas 3.0 він став єдиним доступним режимом. Що це означає на практиці? Будь-яка операція індексування тепер повертає представлення (view) даних, а фактичне копіювання відбувається лише тоді, коли ви намагаєтесь ці дані модифікувати.
Цей підхід значно зменшує споживання пам'яті та підвищує продуктивність. Але є нюанси.
Що таке Copy-on-Write і чому це важливо
У попередніх версіях pandas поведінка копіювання була, скажімо чесно, непередбачуваною. Іноді операція повертала копію, іноді — представлення, і це залежало від внутрішньої реалізації (яку мало хто розумів до кінця). Результат — численні помилки з ненавмисною модифікацією оригінальних даних та горезвісне попередження SettingWithCopyWarning.
CoW усуває цю невизначеність раз і назавжди: тепер кожна операція індексування повертає представлення, і лише при спробі запису створюється копія.
Ланцюгове присвоювання більше не працює
Це, мабуть, найважливіший наслідок CoW — ланцюгове присвоювання (chained assignment) більше не модифікує оригінальний DataFrame. Якщо ви хоч раз натрапляли на баг через ланцюгове присвоювання, то зрозумієте, чому це хороша новина:
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# СТАРИЙ КОД — працював у pandas 2.x (з попередженням)
# Тепер НЕ МОДИФІКУЄ оригінальний df!
df["A"][0] = 100
print(df["A"][0])
# 1 (значення НЕ змінилось!)
# Також не працює:
df[df["A"] > 1]["B"] = 99
print(df)
# A B
# 0 1 4
# 1 2 5
# 2 3 6
# (нічого не змінилось)
Використання .loc замість ланцюгового присвоювання
Правильний спосіб модифікації даних у pandas 3.0 — використання .loc, .iloc або інших явних методів:
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# ПРАВИЛЬНО — використовуємо .loc
df.loc[0, "A"] = 100
print(df["A"][0])
# 100 (значення змінилось!)
# ПРАВИЛЬНО — умовне присвоювання через .loc
df.loc[df["A"] > 1, "B"] = 99
print(df)
# A B
# 0 100 4
# 1 2 99
# 2 3 99
Переваги продуктивності
CoW приносить відчутні переваги у продуктивності, бо зменшує кількість непотрібних копій. Ось наочний приклад:
import pandas as pd
import numpy as np
# Створюємо великий DataFrame
df = pd.DataFrame(np.random.randn(1_000_000, 50))
# Зрізи тепер завжди повертають представлення (view)
# Копія створюється лише при модифікації
subset = df[["A", "B", "C"]] # Без копіювання!
filtered = df[df[0] > 0] # Без копіювання!
# Копія створюється лише коли ви змінюєте subset
subset.loc[0, "A"] = 999 # Тут відбувається копіювання
SettingWithCopyWarning видалено
Нарешті! Оскільки CoW усуває невизначеність у поведінці копіювання, попередження SettingWithCopyWarning було повністю видалено з pandas 3.0. Якщо ваш код придушував це попередження (через pd.options.mode.chained_assignment = None), можете сміливо видалити ці рядки.
# Цей код більше не потрібен у pandas 3.0
# pd.options.mode.chained_assignment = None # видаліть це
# pd.set_option("mode.chained_assignment", None) # і це також
# Також видалено опцію copy_on_write, оскільки CoW тепер єдиний режим
# pd.options.mode.copy_on_write = True # більше не існує
Поради з міграції CoW
- Знайдіть усі випадки ланцюгового присвоювання та замініть на
.loc/.iloc. - Видаліть усі налаштування
pd.options.mode.chained_assignment. - Видаліть явні виклики
.copy(), які раніше використовувались для уникнення попереджень — тепер вони зайві. - Протестуйте код на pandas 2.3 з
pd.options.mode.copy_on_write = Trueперед переходом на 3.0.
Нова роздільна здатність дати та часу
Ще одна фундаментальна зміна стосується обробки часових міток. Pandas 3.0 змінює роздільну здатність за замовчуванням з наносекунд на мікросекунди.
Чому? Причина досить прагматична: наносекундна точність обмежувала діапазон дат приблизно від 1677 до 2262 року. Для більшості задач це не проблема, але якщо ви коли-небудь працювали з історичними даними або фінансовим моделюванням на довгий горизонт — ви точно натрапляли на це обмеження.
Зміна з наносекунд на мікросекунди
Мікросекундна роздільна здатність розширює діапазон дат до приблизно 290 000 років в обидва боки від нашої ери — цього вистачить навіть найамбітнішим проектам:
import pandas as pd
# pandas 2.x
# ts = pd.Timestamp("2026-01-21")
# ts.unit → 'ns'
# pandas 3.0
ts = pd.Timestamp("2026-01-21")
print(ts.unit)
# 'us' (мікросекунди)
# DatetimeIndex також за замовчуванням у мікросекундах
dti = pd.to_datetime(["2026-01-21", "2026-06-15", "2026-12-31"])
print(dti.dtype)
# datetime64[us] (замість datetime64[ns])
Вплив на операції з часовими мітками
Ця зміна впливає на арифметику з часовими мітками та порівняння, особливо якщо ваш код явно працював з наносекундами:
import pandas as pd
import numpy as np
# Створення DatetimeIndex
dates = pd.date_range("2026-01-01", periods=5, freq="D")
print(dates.dtype)
# datetime64[us]
# Timedelta також у мікросекундах
td = pd.Timedelta("1 day")
print(td.unit)
# 'us'
# Перетворення з numpy — зверніть увагу на роздільну здатність
np_date = np.datetime64("2026-01-21", "ns") # наносекунди
pd_ts = pd.Timestamp(np_date)
print(pd_ts.unit)
# 'ns' (зберігає оригінальну роздільну здатність при явному вказанні)
Використання as_unit() для контролю роздільної здатності
Метод as_unit() дозволяє явно конвертувати між різними роздільними здатностями. Це особливо корисно під час міграції:
import pandas as pd
ts = pd.Timestamp("2026-01-21 14:30:00.123456789")
# Конвертація до різних роздільних здатностей
print(ts.as_unit("ns")) # 2026-01-21 14:30:00.123456789
print(ts.as_unit("us")) # 2026-01-21 14:30:00.123456
print(ts.as_unit("ms")) # 2026-01-21 14:30:00.123
print(ts.as_unit("s")) # 2026-01-21 14:30:00
# Для Series та DataFrame
df = pd.DataFrame({
"timestamp": pd.to_datetime(["2026-01-21", "2026-06-15"])
})
# Конвертація стовпця до наносекунд (якщо потрібно для сумісності)
df["timestamp_ns"] = df["timestamp"].dt.as_unit("ns")
print(df.dtypes)
# timestamp datetime64[us]
# timestamp_ns datetime64[ns]
Поради з міграції часових міток
- Перевірте код, який покладається на наносекундну точність — у більшості випадків мікросекунди цілком достатні.
- Якщо ваш код обчислює значення у наносекундах (наприклад, через
ts.value), оновіть його для врахування нової роздільної здатності. - Використовуйте
as_unit("ns")там, де наносекундна точність є критичною. - Оновіть тести, що перевіряють
dtype == "datetime64[ns]"наdatetime64[us].
Новий синтаксис pd.col()
А ось це, на мою думку, одна з найприємніших новинок. Pandas 3.0 вводить функцію pd.col() — відкладений вираз (deferred expression), який дозволяє створювати лаконічні ланцюжки операцій без необхідності повторювати ім'я DataFrame або писати лямбда-функції.
Якщо ви працювали з Polars, цей підхід буде вам знайомий. І це чудово, бо синтаксис дійсно стає набагато чистішим.
Спрощене посилання на стовпці
Функція pd.col() створює об'єкт-вираз, який обчислюється лише у контексті конкретного DataFrame:
import pandas as pd
df = pd.DataFrame({
"name": ["Олена", "Максим", "Ірина"],
"salary": [45000, 52000, 48000],
"bonus_pct": [0.10, 0.15, 0.12]
})
# Старий спосіб (pandas 2.x) — з лямбда-функцією
df_old = df.assign(total=lambda x: x["salary"] * (1 + x["bonus_pct"]))
# Новий спосіб (pandas 3.0) — з pd.col()
df_new = df.assign(total=pd.col("salary") * (1 + pd.col("bonus_pct")))
print(df_new)
# name salary bonus_pct total
# 0 Олена 45000 0.10 49500.0
# 1 Максим 52000 0.15 59800.0
# 2 Ірина 48000 0.12 53760.0
Приклади використання з assign()
Функція pd.col() особливо зручна при множинних трансформаціях через assign(). Порівняйте з тим, як це виглядало раніше з лямбдами — різниця разюча:
import pandas as pd
df = pd.DataFrame({
"product": ["Ноутбук", "Телефон", "Планшет"],
"price": [25000, 15000, 12000],
"quantity": [10, 25, 15],
"discount": [0.05, 0.10, 0.08]
})
# Ланцюжок трансформацій з pd.col()
result = df.assign(
revenue=pd.col("price") * pd.col("quantity"),
discounted_price=pd.col("price") * (1 - pd.col("discount")),
total_after_discount=(
pd.col("price") * (1 - pd.col("discount")) * pd.col("quantity")
)
)
print(result)
# product price quantity discount revenue discounted_price total_after_discount
# 0 Ноутбук 25000 10 0.05 250000 23750.0 237500.0
# 1 Телефон 15000 25 0.10 375000 13500.0 337500.0
# 2 Планшет 12000 15 0.08 180000 11040.0 165600.0
Ланцюжкові операції
Вирази pd.col() підтримують широкий набір операцій — рядкові, числові, агрегатні:
import pandas as pd
df = pd.DataFrame({
"city": ["київ", "львів", "одеса", "харків"],
"population": [2962000, 717000, 993000, 1419000],
"area_km2": [847, 182, 236, 350]
})
# Рядкові операції
result = df.assign(
city_upper=pd.col("city").str.upper(),
city_len=pd.col("city").str.len(),
density=pd.col("population") / pd.col("area_km2")
)
print(result[["city", "city_upper", "density"]])
# city city_upper density
# 0 київ КИЇВ 3497.639905
# 1 львів ЛЬВІВ 3939.560440
# 2 одеса ОДЕСА 4207.627119
# 3 харків ХАРКІВ 4054.285714
Інтеграція з Apache Arrow (Arrow PyCapsule Interface)
Pandas 3.0 значно покращує інтеграцію з екосистемою Apache Arrow через підтримку Arrow PyCapsule Interface. Це стандартизований протокол обміну даними між бібліотеками без копіювання пам'яті (так званий zero-copy). Нові методи DataFrame.from_arrow() та Series.from_arrow() дозволяють безшовно конвертувати дані між pandas та будь-якою бібліотекою, що підтримує цей інтерфейс.
Методи from_arrow()
Нові методи приймають будь-який об'єкт, що реалізує Arrow PyCapsule Interface — таблиці PyArrow, DuckDB, Polars тощо:
import pandas as pd
import pyarrow as pa
# Створення таблиці PyArrow
arrow_table = pa.table({
"id": [1, 2, 3, 4],
"name": ["Олена", "Максим", "Ірина", "Дмитро"],
"score": [95.5, 87.3, 92.1, 88.7]
})
# Конвертація з Arrow до pandas (zero-copy де можливо)
df = pd.DataFrame.from_arrow(arrow_table)
print(df)
# id name score
# 0 1 Олена 95.5
# 1 2 Максим 87.3
# 2 3 Ірина 92.1
# 3 4 Дмитро 88.7
print(df.dtypes)
# id int64
# name str
# score float64
# Конвертація Series з Arrow
arrow_array = pa.array([10, 20, 30, 40])
series = pd.Series.from_arrow(arrow_array, name="values")
print(series)
# 0 10
# 1 20
# 2 30
# 3 40
# Name: values, dtype: int64
Zero-copy інтероперабельність
Головна перевага PyCapsule Interface — обмін даними без копіювання пам'яті. Для великих датасетів це може бути справжнім порятунком:
import pandas as pd
import pyarrow as pa
# Великий набір даних — 10 мільйонів рядків
large_arrow_table = pa.table({
"id": pa.array(range(10_000_000), type=pa.int64()),
"value": pa.array(range(10_000_000), type=pa.float64())
})
# Zero-copy конвертація — миттєво, без додаткової пам'яті
df = pd.DataFrame.from_arrow(large_arrow_table)
# Зворотна конвертація — pandas до Arrow
arrow_back = pa.RecordBatch.from_pandas(df)
# Інтеграція з DuckDB через Arrow
import duckdb
conn = duckdb.connect()
result = conn.execute("SELECT * FROM df WHERE value > 5000000").arrow()
df_filtered = pd.DataFrame.from_arrow(result)
print(f"Відфільтровано рядків: {len(df_filtered)}")
# Відфільтровано рядків: 4999999
Переваги нової інтеграції
- Уніфікований інтерфейс — один метод для конвертації з будь-якої Arrow-сумісної бібліотеки.
- Zero-copy — мінімальне споживання додаткової пам'яті.
- Стандартизація — PyCapsule Interface є офіційним стандартом Apache Arrow.
- Продуктивність — значне прискорення порівняно з попередніми методами конвертації.
Нові можливості merge() — анти-з'єднання
Якщо ви коли-небудь писали конструкцію з merge(), indicator=True та фільтрацією _merge == "left_only" — то ви точно оціните цю зміну. Pandas 3.0 додає довгоочікувані анти-з'єднання (anti-joins) прямо у функцію merge().
Тепер замість п'яти рядків коду потрібен лише один.
left_anti та right_anti
Два нові значення параметра how:
import pandas as pd
# Два DataFrame: клієнти та замовлення
customers = pd.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"name": ["Олена", "Максим", "Ірина", "Дмитро", "Наталія"]
})
orders = pd.DataFrame({
"order_id": [101, 102, 103],
"customer_id": [1, 3, 3],
"amount": [500, 300, 750]
})
# LEFT ANTI JOIN — клієнти, які НЕ мають замовлень
no_orders = pd.merge(customers, orders, on="customer_id", how="left_anti")
print(no_orders)
# customer_id name
# 0 2 Максим
# 1 4 Дмитро
# 2 5 Наталія
# RIGHT ANTI JOIN — замовлення від клієнтів, яких немає у таблиці
unknown_orders_data = pd.DataFrame({
"order_id": [201, 202, 203],
"customer_id": [3, 7, 8],
"amount": [100, 200, 150]
})
unknown = pd.merge(customers, unknown_orders_data, on="customer_id", how="right_anti")
print(unknown)
# order_id customer_id amount
# 0 202 7 200
# 1 203 8 150
Порівняння зі старим підходом
Просто подивіться, наскільки простішим став код:
import pandas as pd
customers = pd.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"name": ["Олена", "Максим", "Ірина", "Дмитро", "Наталія"]
})
orders = pd.DataFrame({
"order_id": [101, 102, 103],
"customer_id": [1, 3, 3],
"amount": [500, 300, 750]
})
# Старий спосіб (pandas 2.x) — громіздкий
merged = pd.merge(customers, orders, on="customer_id", how="left", indicator=True)
no_orders_old = merged[merged["_merge"] == "left_only"][customers.columns]
# Новий спосіб (pandas 3.0) — один рядок
no_orders_new = pd.merge(customers, orders, on="customer_id", how="left_anti")
# Результат однаковий, але різниця у читабельності — величезна
Зміни в API та застарілі функції
Pandas 3.0 містить чимало змін в API. Деякі з них анонсувались раніше через попередження, але тепер стали обов'язковими. Давайте пройдемося по найважливіших.
Зміна аліасів зміщень (Offset Aliases)
Аліаси для частот дат були оновлені. Старі однолітерні аліаси замінені на більш описові варіанти — і це, чесно кажучи, логічний крок:
import pandas as pd
# СТАРІ аліаси (pandas 2.x) — більше НЕ працюють
# pd.date_range("2026-01-01", periods=3, freq="M") # ПОМИЛКА!
# pd.date_range("2026-01-01", periods=4, freq="Q") # ПОМИЛКА!
# pd.date_range("2026-01-01", periods=3, freq="Y") # ПОМИЛКА!
# НОВІ аліаси (pandas 3.0)
monthly = pd.date_range("2026-01-01", periods=3, freq="ME") # Month End
print(monthly)
# DatetimeIndex(['2026-01-31', '2026-02-28', '2026-03-31'], dtype='datetime64[us]')
quarterly = pd.date_range("2026-01-01", periods=4, freq="QE") # Quarter End
print(quarterly)
# DatetimeIndex(['2026-03-31', '2026-06-30', '2026-09-30', '2026-12-31'], dtype='datetime64[us]')
yearly = pd.date_range("2026-01-01", periods=3, freq="YE") # Year End
print(yearly)
# DatetimeIndex(['2026-12-31', '2027-12-31', '2028-12-31'], dtype='datetime64[us]')
# Аналогічно для початку періоду:
# MS → без змін (Month Start)
# QS → QS (Quarter Start) — без змін
# YS → YS (Year Start) — без змін
Видалення параметра copy
Параметр copy видалений з багатьох функцій — CoW робить його просто непотрібним:
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3]})
# Старий код (pandas 2.x)
# df2 = df.rename(columns={"A": "B"}, copy=True) # ПОМИЛКА в 3.0!
# df3 = df.astype(float, copy=False) # ПОМИЛКА в 3.0!
# Новий код (pandas 3.0) — просто видаліть параметр copy
df2 = df.rename(columns={"A": "B"})
df3 = df.astype(float)
# CoW автоматично оптимізує копіювання
inplace=True тепер повертає self
Цікава зміна: тепер методи з inplace=True повертають посилання на той самий об'єкт замість None. Це дозволяє використовувати inplace у ланцюжках (хоча, на мою думку, краще все ж уникати inplace у сучасному коді):
import pandas as pd
df = pd.DataFrame({"A": [3, 1, 2], "B": ["x", "y", "z"]})
# pandas 2.x: inplace=True повертав None
# result = df.sort_values("A", inplace=True)
# print(result) # None
# pandas 3.0: inplace=True повертає self
result = df.sort_values("A", inplace=True)
print(result is df) # True
print(df)
# A B
# 1 1 y
# 2 2 z
# 0 3 x
Мінімальна версія Python та інші зміни
- Python 3.11 — мінімальна підтримувана версія. Python 3.9 та 3.10 більше не підтримуються.
- pytz → zoneinfo — бібліотека
pytzзамінена на стандартнуzoneinfoз Python.
import pandas as pd
from zoneinfo import ZoneInfo
# Старий спосіб (pandas 2.x з pytz)
# import pytz
# ts = pd.Timestamp("2026-01-21", tz=pytz.timezone("Europe/Kyiv"))
# Новий спосіб (pandas 3.0 з zoneinfo)
ts = pd.Timestamp("2026-01-21", tz=ZoneInfo("Europe/Kyiv"))
print(ts)
# 2026-01-21 00:00:00+02:00
# Або просто рядок — працює як і раніше
ts2 = pd.Timestamp("2026-01-21", tz="Europe/Kyiv")
print(type(ts2.tz))
# <class 'zoneinfo.ZoneInfo'> (замість pytz.timezone)
Покращення введення/виведення
Pandas 3.0 приносить чимало покращень у сфері I/O — нативна підтримка Apache Iceberg, покращення Parquet та нові параметри для CSV і SQL.
read_iceberg() та to_iceberg()
Apache Iceberg — відкритий табличний формат для масштабних аналітичних даних. Pandas 3.0 додає нативну підтримку:
import pandas as pd
# Читання Iceberg-таблиці
df = pd.read_iceberg(
"s3://my-bucket/warehouse/db/sales_table",
snapshot_id=123456789 # опціонально: конкретний знімок
)
# Запис до Iceberg-таблиці
df.to_iceberg(
"s3://my-bucket/warehouse/db/output_table",
mode="append" # або "overwrite"
)
# Читання з фільтрацією (predicate pushdown)
df_filtered = pd.read_iceberg(
"s3://my-bucket/warehouse/db/sales_table",
filters=[("year", "=", 2026), ("region", "=", "UA")]
)
Покращення read_parquet
Функція read_parquet() отримала покращення продуктивності та нові можливості:
import pandas as pd
# Покращена підтримка фільтрації при читанні
df = pd.read_parquet(
"data/large_dataset.parquet",
filters=[("date", ">=", "2026-01-01")],
columns=["date", "value", "category"]
)
# Підтримка glob-патернів для читання декількох файлів
df_all = pd.read_parquet("data/partitioned_data/**/*.parquet")
Нові можливості to_csv та to_sql
Метод to_csv() тепер підтримує f-рядки у float_format, а to_sql() отримав новий режим if_exists="delete_rows" — дуже зручно, коли потрібно очистити таблицю перед вставкою, не руйнуючи її структуру:
import pandas as pd
df = pd.DataFrame({
"item": ["Кава", "Чай", "Сік"],
"price": [45.678, 32.123, 28.956]
})
# Новий float_format з f-рядками
df.to_csv("prices.csv", float_format="{:.2f}")
# Результат у CSV:
# item,price
# Кава,45.68
# Чай,32.12
# Сік,28.96
# Також підтримується f-рядок з відсотками, вирівнюванням тощо
df.to_csv("prices_formatted.csv", float_format="{:>10.1f}")
# to_sql з delete_rows — видаляє всі рядки перед вставкою,
# але зберігає структуру таблиці
from sqlalchemy import create_engine
engine = create_engine("sqlite:///mydb.sqlite")
df.to_sql("prices", engine, if_exists="delete_rows", index=False)
# Відрізняється від "replace" тим, що не видаляє/створює таблицю заново,
# а лише очищує дані. Це зберігає індекси, тригери та обмеження.
Покращення GroupBy
Операції групування — одна з найпотужніших функцій pandas. У версії 3.0 вони стали ще зручнішими.
Параметр skipna
Новий параметр skipna у методах агрегації GroupBy дає більше контролю над обробкою пропущених значень:
import pandas as pd
import numpy as np
df = pd.DataFrame({
"group": ["A", "A", "A", "B", "B", "B"],
"value": [10, np.nan, 30, 40, 50, np.nan]
})
# За замовчуванням — пропускає NaN (як і раніше)
result_skip = df.groupby("group")["value"].sum()
print(result_skip)
# group
# A 40.0
# B 90.0
# Новий параметр skipna=False — NaN поширюється на результат
result_no_skip = df.groupby("group")["value"].sum(skipna=False)
print(result_no_skip)
# group
# A NaN
# B NaN
# skipna працює з різними агрегатними функціями
result_mean = df.groupby("group")["value"].mean(skipna=False)
print(result_mean)
# group
# A NaN
# B NaN
Покращення NamedAgg
Іменоване агрегування стало ще зручнішим:
import pandas as pd
import numpy as np
df = pd.DataFrame({
"department": ["IT", "IT", "HR", "HR", "IT", "HR"],
"employee": ["Олена", "Максим", "Ірина", "Дмитро", "Наталія", "Андрій"],
"salary": [55000, 62000, 48000, 51000, 58000, 45000],
"experience_years": [5, 8, 3, 6, 4, 2]
})
# Покращене іменоване агрегування
summary = df.groupby("department").agg(
avg_salary=pd.NamedAgg(column="salary", aggfunc="mean"),
max_salary=pd.NamedAgg(column="salary", aggfunc="max"),
min_salary=pd.NamedAgg(column="salary", aggfunc="min"),
total_experience=pd.NamedAgg(column="experience_years", aggfunc="sum"),
team_size=pd.NamedAgg(column="employee", aggfunc="count")
)
print(summary)
# avg_salary max_salary min_salary total_experience team_size
# department
# HR 48000.000 51000 45000 11 3
# IT 58333.333 62000 55000 17 3
Нові методи Rolling
Ковзні обчислення отримали нові статистичні методи. Наприклад, ковзний rank:
import pandas as pd
import numpy as np
# Часовий ряд з денними даними
dates = pd.date_range("2026-01-01", periods=30, freq="D")
df = pd.DataFrame({
"date": dates,
"price": np.random.uniform(100, 200, 30).round(2)
})
df = df.set_index("date")
# Стандартні ковзні обчислення
df["rolling_mean_7d"] = df["price"].rolling(7).mean()
df["rolling_std_7d"] = df["price"].rolling(7).std()
# Нові можливості Rolling у pandas 3.0
df["rolling_rank"] = df["price"].rolling(7).rank()
print(df.head(10))
# price rolling_mean_7d rolling_std_7d rolling_rank
# date
# 2026-01-01 156.23 NaN NaN NaN
# 2026-01-02 142.87 NaN NaN NaN
# 2026-01-03 178.45 NaN NaN NaN
# ...
Покрокова міграція з pandas 2.x
Отже, ви вирішили мігрувати. Правильне рішення! Але поспішати не варто — міграція потребує систематичного підходу. Ось детальний план, який допоможе зробити перехід максимально безболісним.
Крок 1: Оновіть до pandas 2.3
Перед переходом на pandas 3.0 обов'язково оновіть до останньої версії 2.x. Pandas 2.3 містить усі попередження про майбутні зміни — це ваша «канарка у шахті»:
# Оновіть pandas до останньої версії 2.x
pip install "pandas>=2.3,<3.0"
# Увімкніть режим CoW для тестування
import pandas as pd
pd.options.mode.copy_on_write = True
# Увімкніть майбутню поведінку рядків
pd.options.future.infer_string = True
Крок 2: Виправте всі попередження
Запустіть тести з увімкненими попередженнями та виправте кожне:
# Запустіть тести з відображенням попереджень
python -W all -m pytest tests/
# Або додайте у конфігурацію pytest (pyproject.toml)
# [tool.pytest.ini_options]
# filterwarnings = ["error::FutureWarning"]
Типові попередження, на які варто звернути увагу:
FutureWarning: Setting an item of incompatible dtype— оновіть типи даних.FutureWarning: ChainedAssignmentError— замініть ланцюгове присвоювання на.loc.FutureWarning: 'M' is deprecated, use 'ME'— оновіть аліаси частот.DeprecationWarning: the copy keyword is deprecated— видаліть параметрcopy.
Крок 3: Оновіть перевірки рядкових типів
Знайдіть та оновіть усі місця, де код перевіряє тип object для рядків:
import pandas as pd
# Знайдіть у вашому коді всі такі паттерни:
# ЗАМІНІТЬ:
# if col.dtype == object:
# if col.dtype == "object":
# if str(col.dtype) == "object":
# НА:
if pd.api.types.is_string_dtype(col):
pass # обробка рядкових стовпців
# Також оновіть порівняння з NaN для рядків:
# ЗАМІНІТЬ:
# if pd.isna(value): # працює, але поведінка змінилась
# НА:
# value is pd.NA (для рядкових стовпців)
Крок 4: Видаліть ланцюгове присвоювання
Систематично знайдіть та замініть усі випадки:
import pandas as pd
df = pd.DataFrame({
"category": ["A", "B", "A", "C"],
"value": [10, 20, 30, 40]
})
# ПАТТЕРН 1: Індексування через [][]
# СТАРИЙ КОД:
# df["value"][df["category"] == "A"] = 100
# НОВИЙ КОД:
df.loc[df["category"] == "A", "value"] = 100
# ПАТТЕРН 2: Фільтрація + присвоювання
# СТАРИЙ КОД:
# df[df["value"] > 20]["category"] = "X"
# НОВИЙ КОД:
df.loc[df["value"] > 20, "category"] = "X"
# ПАТТЕРН 3: Присвоювання через зріз
# СТАРИЙ КОД:
# df[:2]["value"] = 0
# НОВИЙ КОД:
df.iloc[:2, df.columns.get_loc("value")] = 0
Крок 5: Оновіть аліаси зміщень
Ось повна таблиця замін (збережіть її — точно знадобиться):
import pandas as pd
# Таблиця замін:
# Старий → Новий
# M → ME (Month End)
# Q → QE (Quarter End)
# Y → YE (Year End)
# A → YE (Annual/Year End — A також замінено)
# BM → BME (Business Month End)
# BQ → BQE (Business Quarter End)
# BY → BYE (Business Year End)
# BA → BYE (Business Annual/Year End)
# H → h (Hour — зміна регістру)
# T → min (Minute)
# S → s (Second)
# L → ms (Millisecond)
# U → us (Microsecond)
# N → ns (Nanosecond)
# Приклад оновлення:
# СТАРИЙ КОД:
# monthly = pd.date_range("2026-01-01", periods=12, freq="M")
# quarterly = df.resample("Q").sum()
# НОВИЙ КОД:
monthly = pd.date_range("2026-01-01", periods=12, freq="ME")
df_ts = pd.DataFrame(
{"value": range(365)},
index=pd.date_range("2026-01-01", periods=365, freq="D")
)
quarterly = df_ts.resample("QE").sum()
Крок 6: Протестуйте операції з датами
Переконайтесь, що зміна роздільної здатності не зламала ваші обчислення:
import pandas as pd
# Перевірте dtype ваших часових стовпців
df = pd.DataFrame({
"timestamp": pd.to_datetime(["2026-01-21 10:30:00", "2026-01-22 14:45:00"])
})
assert df["timestamp"].dtype == "datetime64[us]", "Очікується мікросекундна роздільна здатність"
# Перевірте обчислення різниці часу
diff = df["timestamp"].diff()
print(diff.dtype)
# timedelta64[us]
# Якщо ваш код покладався на .value (повертає ціле число)
# пам'ятайте, що тепер значення в мікросекундах, а не наносекундах
ts = pd.Timestamp("2026-01-21")
print(ts.value) # Тепер у мікросекундах від epoch
# Для зворотної сумісності використовуйте as_unit()
ts_ns = ts.as_unit("ns")
print(ts_ns.value) # У наносекундах від epoch
Крок 7: Оновіть до pandas 3.0
Після виправлення всіх попереджень — фінальний крок:
# Встановіть pandas 3.0
pip install "pandas>=3.0"
# Перевірте версію
import pandas as pd
print(pd.__version__)
# 3.0.0
# Переконайтесь, що Python >= 3.11
import sys
print(sys.version)
# 3.11.x або новіший
# Запустіть повний набір тестів
python -m pytest tests/ -v --tb=long
Скрипт автоматичної перевірки сумісності
Я підготував корисний скрипт, який допоможе виявити потенційні проблеми у вашому коді ще до міграції:
"""
Скрипт перевірки сумісності коду з pandas 3.0
Запустіть у кореневій директорії проекту:
python check_pandas3_compat.py
"""
import ast
import sys
from pathlib import Path
PATTERNS = {
'dtype == "object"': "Замініть на pd.api.types.is_string_dtype()",
"dtype == object": "Замініть на pd.api.types.is_string_dtype()",
'freq="M"': 'Замініть на freq="ME"',
'freq="Q"': 'Замініть на freq="QE"',
'freq="Y"': 'Замініть на freq="YE"',
'freq="A"': 'Замініть на freq="YE"',
"copy=True": "Видаліть параметр copy (CoW обробляє автоматично)",
"copy=False": "Видаліть параметр copy (CoW обробляє автоматично)",
"import pytz": "Замініть на from zoneinfo import ZoneInfo",
"pytz.timezone": "Замініть на ZoneInfo()",
"chained_assignment": "Видаліть — більше не потрібно в pandas 3.0",
}
def check_file(filepath):
issues = []
with open(filepath, "r", encoding="utf-8") as f:
for i, line in enumerate(f, 1):
for pattern, suggestion in PATTERNS.items():
if pattern in line:
issues.append((i, pattern, suggestion))
return issues
def main():
project_dir = Path(".")
total_issues = 0
for py_file in project_dir.rglob("*.py"):
issues = check_file(py_file)
if issues:
print(f"\n{py_file}:")
for line_num, pattern, suggestion in issues:
print(f" Рядок {line_num}: знайдено '{pattern}'")
print(f" → {suggestion}")
total_issues += 1
print(f"\nЗнайдено {total_issues} потенційних проблем.")
return total_issues
if __name__ == "__main__":
sys.exit(main())
Додаткові зміни та покращення
Окрім основних змін, pandas 3.0 містить безліч менших, але важливих покращень.
Покращення продуктивності
Завдяки глибшій інтеграції з PyArrow та обов'язковому CoW, загальна продуктивність помітно зросла:
- Рядкові операції — до 3-5 разів швидші завдяки бекенду PyArrow.
- Споживання пам'яті — зменшено на 30-50% для типових робочих навантажень.
- read_csv() — прискорено завдяки оптимізованому парсингу рядків.
- GroupBy — покращена продуктивність агрегатних операцій.
Зміни у поведінці порівнянь
Порівняння з pd.NA тепер поводяться більш консистентно (і, чесно кажучи, більш логічно):
import pandas as pd
s = pd.Series(["hello", pd.NA, "world"])
# Порівняння з NA повертає NA (не True/False)
result = s == "hello"
print(result)
# 0 True
# 1 <NA>
# 2 False
# dtype: boolean
# Для фільтрації використовуйте .fillna(False) або .eq()
filtered = s[result.fillna(False)]
print(filtered)
# 0 hello
# dtype: str
Нові утиліти для перетворення типів
import pandas as pd
df = pd.DataFrame({
"id": ["1", "2", "3"],
"value": ["10.5", "20.3", "30.1"],
"flag": ["true", "false", "true"]
})
# convert_dtypes() тепер працює ще краще
df_converted = df.convert_dtypes()
print(df_converted.dtypes)
# id str
# value str
# flag str
# Для числових конвертацій використовуйте to_numeric
df_converted["value"] = pd.to_numeric(df_converted["value"])
df_converted["id"] = pd.to_numeric(df_converted["id"]).astype("Int64")
print(df_converted.dtypes)
# id Int64
# value float64
# flag str
Висновки
Pandas 3.0 — це, без перебільшення, найважливіше оновлення бібліотеки за останні роки. Три фундаментальні зміни — новий рядковий тип за замовчуванням, обов'язковий Copy-on-Write та мікросекундна роздільна здатність часових міток — разом формують нову парадигму роботи з даними у Python.
Звісно, міграція потребує зусиль. Але результат того вартий: кращу продуктивність, менше споживання пам'яті та більш передбачуваний код.
Нові можливості — pd.col() для виразних трансформацій, анти-з'єднання у merge(), нативна підтримка Apache Iceberg та покращена інтеграція з Apache Arrow — значно розширюють арсенал інструментів і наближають pandas до сучасних стандартів обробки даних.
Ключові рекомендації для успішної міграції:
- Не поспішайте — спочатку оновіть до pandas 2.3 та виправте всі попередження.
- Увімкніть CoW у pandas 2.3 (
pd.options.mode.copy_on_write = True) та протестуйте код. - Оновіть перевірки типів — замініть
dtype == "object"наpd.api.types.is_string_dtype(). - Видаліть ланцюгове присвоювання — використовуйте
.locта.iloc. - Оновіть аліаси частот —
M→ME,Q→QE,Y→YEта інші. - Перевірте часові мітки — зверніть увагу на зміну з наносекунд на мікросекунди.
- Оновіть залежності — Python >= 3.11,
pytzзамініть наzoneinfo. - Запустіть тести після кожного кроку міграції.
Pandas залишається незамінним інструментом для аналізу даних у Python, і версія 3.0 це лише підтверджує. Починайте міграцію вже сьогодні — крок за кроком, слідуючи рекомендаціям цього посібника, і перехід буде максимально безболісним. Успіхів!