Коротко
- Сколько занимает таблица —
memory_usage(deep=True). Безdeepу столбцовobjectсчитаются только ссылки на строки; у типаstrрезультат одинаковый. - Повторяющиеся строки →
category: память падает в разы. - Маленькие числа → меньший тип:
pd.to_numeric(s, downcast="integer"). - Главное ускорение — векторизация: операции над столбцами вместо
apply(axis=1)и циклов. Разница — на порядки. - Читайте только нужное:
read_csv(usecols=…, dtype=…); для промежуточных данных используйте Parquet. - Типы Apache Arrow (
dtype_backend="pyarrow") есть для всех столбцов. Строки типаstrпри установленном pyarrow и так хранятся в Arrow. evalиquery— выражения строкой; на больших таблицах иногда быстрее, но главное — короче.
Синтаксис
df.memory_usage(deep=True)
df["col"].astype("category")
pd.to_numeric(s, downcast="integer") # или "float"
pd.read_csv(path, usecols=[...], dtype={...}, dtype_backend="pyarrow")
df.convert_dtypes(dtype_backend="pyarrow")
df.eval("c = a * b"), df.query("a > 1")Примеры
rng = np.random.default_rng(0)
n = 100_000
orders = pd.DataFrame({
"city": rng.choice(["Москва", "Санкт-Петербург", "Казань", "Омск"], size=n),
"qty": rng.integers(1, 20, size=n),
"price": rng.uniform(100, 500, size=n).round(2),
})usage = orders.memory_usage(deep=True, index=False) # байты по столбцам
(usage / 2**20).round(1) # в мегабайтахВывод
city 2.2 qty 0.8 price 0.8 dtype: float64
text = orders["city"] # тип str
obj = text.astype(object) # те же строки как объекты Python
print(text.memory_usage(index=False) == text.memory_usage(deep=True, index=False))
obj.memory_usage(index=False) < obj.memory_usage(deep=True, index=False) # без deep — только ссылкиВывод
True True
before = orders["city"].memory_usage(deep=True, index=False)
after = orders["city"].astype("category").memory_usage(deep=True, index=False)
before // after # во сколько раз меньше: категории хранятся один разВывод
23
small = pd.to_numeric(orders["qty"], downcast="integer")
print(small.dtype) # хватит int8: значения до 20
orders["qty"].nbytes // small.nbytes # 8 байт на число → 1 байтВывод
int8 8
import timeit
t_apply = min(timeit.repeat(lambda: orders.apply(lambda r: r["qty"] * r["price"], axis=1), number=1, repeat=3))
t_vector = min(timeit.repeat(lambda: orders["qty"] * orders["price"], number=1, repeat=3))
print(f"apply: {t_apply * 1000:.0f} мс, столбцы: {t_vector * 1000:.2f} мс")
print(f"ускорение: ×{t_apply / t_vector:.0f}") # разница — на порядкиВыводзамер на Apple M4 · macOS · Python 3.14, 29.09.2026
apply: 167 мс, столбцы: 0.07 мс ускорение: ×2431
Время снято один раз и показано вместе с машиной и датой замера. На другом компьютере числа будут другими, но порядок разницы сохранится.
orders.to_csv("orders.csv", index=False)
pd.read_csv("orders.csv", usecols=["city", "qty"], dtype={"city": "category"}).dtypesВывод
city category qty int64 dtype: object
orders.head(3).convert_dtypes(dtype_backend="pyarrow").dtypes # типы на основе Apache ArrowВывод
city string[pyarrow] qty int64[pyarrow] price double[pyarrow] dtype: object
orders.eval("revenue = qty * price").head(3) # выражение строкойВывод
city qty price revenue 0 Омск 9 342.80 3085.20 1 Казань 11 185.02 2035.22 2 Казань 3 322.50 967.50
Подводные камни
Оптимизация до измерения
Сначала найдите, что действительно медленно или занимает память: memory_usage(deep=True), %timeit в Jupyter. Чаще всего проблема — apply, iterrows или цикл с concat, а не типы.
downcast и переполнение
После уменьшения типа (int8 до 127) дальнейшая арифметика может выйти за пределы и молча переполниться, как в NumPy. Уменьшайте типы для хранения, а считайте в обычных.
small = pd.Series([100, 120], dtype="int8")
print(small + 100) # 200 и 220 не помещаются в int8
small.astype("int64") + 100Вывод
0 -56 1 -36 dtype: int8 0 200 1 220 dtype: int64