5 методов работы с двумя столбцами в Pandas: сравнение и оптимизация
Для кого эта статья:
- Аналитики данных и разработчики, работающие с Pandas в Python
- Студенты и специалисты, желающие повысить свои навыки в обработке данных
Профессионалы, стремящиеся оптимизировать производительность кодов для обработки больших объемов информации
Работа с данными в Python — это искусство, в котором библиотека Pandas играет роль основного холста. Одна из частых задач, с которой сталкиваются аналитики и разработчики — применение функций к двум столбцам DataFrame. Как рассчитать новый столбец на основе значений из двух других? Как это сделать максимально эффективно, не погрязнув в циклах и неоптимальном коде? В этой статье я раскрою 5 проверенных методов, которые позволят вам элегантно решать подобные задачи в Pandas, значительно ускоряя обработку данных. 📊🐼
Хотите освоить Python и научиться профессионально работать с данными? Обучение Python-разработке от Skypro — это практический курс, где вы изучите не только основы языка, но и профессиональную работу с библиотеками для анализа данных, включая Pandas. Вы научитесь писать оптимальный код для обработки больших объемов информации и реализовывать сложные алгоритмы анализа, которые пригодятся в реальных проектах.
Сравнение 5 методов работы с двумя столбцами в Pandas
Pandas предоставляет несколько способов применения функций к двум столбцам DataFrame. Каждый метод имеет свои преимущества и недостатки, которые важно понимать для выбора оптимального решения в конкретной ситуации.
Давайте представим, что у нас есть таблица продаж с двумя столбцами: quantity (количество проданных единиц) и price (цена за единицу). Нам нужно вычислить общую стоимость для каждой строки, умножив количество на цену.
Вот пять основных методов, которые мы подробно рассмотрим:
- Векторизованные операции — прямые арифметические действия между столбцами
- Метод
apply()с лямбда-функциями - Использование
zip()в сочетании сmap() - Метод
numpy.vectorize()для создания векторизованных функций - Использование
eval()для выражений
Давайте начнем с создания тестового DataFrame для наших примеров:
import pandas as pd
import numpy as np
# Создаем тестовый DataFrame
df = pd.DataFrame({
'quantity': [5, 10, 15, 20, 25],
'price': [1\.5, 2.5, 3.5, 4.5, 5.5]
})
Теперь сравним эти методы по нескольким ключевым параметрам:
| Метод | Скорость выполнения | Читаемость кода | Гибкость | Сложность использования |
|---|---|---|---|---|
| Векторизованные операции | Очень высокая | Отличная | Низкая | Низкая |
| apply() + lambda | Средняя | Хорошая | Высокая | Средняя |
| zip() + map() | Средняя | Средняя | Высокая | Средняя |
| numpy.vectorize() | Высокая | Средняя | Высокая | Высокая |
| eval() | Высокая | Хорошая | Средняя | Низкая |
Александр Петров, Senior Data Analyst Когда я только начинал работать с Pandas, я попытался обработать набор данных из 5 миллионов строк, используя циклы Python для вычисления значений между столбцами. Скрипт работал несколько часов, и это казалось нормальным, пока коллега не показал мне, как решить ту же задачу с помощью векторизованных операций. Разница была ошеломляющей — вместо часов код выполнялся за секунды. Это стало моим первым важным уроком: в Pandas почти всегда есть более эффективный способ, чем тот, что приходит на ум изначально.

Векторизованные операции в DataFrame: максимальная скорость
Векторизованные операции — самый быстрый и читаемый способ работы с двумя столбцами в Pandas. Они позволяют выполнять арифметические операции непосредственно между столбцами DataFrame без использования явных циклов.
Основное преимущество этого подхода — высокая производительность, поскольку операции выполняются на низком уровне с использованием оптимизированного кода NumPy.
# Умножаем количество на цену, используя векторизованную операцию
df['total'] = df['quantity'] * df['price']
print(df)
Этот код выполняется очень быстро и интуитивно понятен даже для начинающих. Pandas автоматически согласовывает индексы и выполняет умножение поэлементно.
Важно отметить, что векторизованные операции поддерживают все основные арифметические действия:
- Сложение:
df['col1'] + df['col2'] - Вычитание:
df['col1'] – df['col2'] - Умножение:
df['col1'] * df['col2'] - Деление:
df['col1'] / df['col2'] - Возведение в степень:
df['col1'] ** df['col2'] - Целочисленное деление:
df['col1'] // df['col2'] - Остаток от деления:
df['col1'] % df['col2']
Для более сложных вычислений можно комбинировать операции:
# Вычисляем скидку 10% от общей стоимости
df['discount'] = df['quantity'] * df['price'] * 0.1
# Вычисляем итоговую сумму с учетом скидки
df['final_price'] = df['quantity'] * df['price'] – df['discount']
Также векторизованные операции прекрасно работают с математическими функциями из библиотеки NumPy:
# Логарифм отношения цены к количеству
df['log_ratio'] = np.log(df['price'] / df['quantity'])
# Округляем до ближайшего целого
df['rounded_total'] = np.round(df['quantity'] * df['price'])
Несмотря на все преимущества, у векторизованных операций есть ограничения. Они хорошо работают с простыми арифметическими операциями, но могут стать неудобными, когда требуется реализовать сложную логику. В таких случаях стоит обратиться к другим методам.
Метод apply() и lambda-функции для обработки данных
Метод apply() — это гибкий инструмент для обработки данных в Pandas, который позволяет применять пользовательские функции к строкам или столбцам DataFrame. В сочетании с lambda-функциями он становится мощным средством для обработки нескольких столбцов одновременно.
Существует два основных способа использования apply() для работы с двумя столбцами:
- Применение
apply()к отдельной строке (axis=1) - Использование
apply()с серией, полученной из двух столбцов
Рассмотрим первый подход:
# Использование apply() с axis=1 для доступа к строкам
df['total'] = df.apply(lambda row: row['quantity'] * row['price'], axis=1)
В этом примере apply() вызывается для всего DataFrame, параметр axis=1 указывает, что функция должна применяться к каждой строке, а не к столбцам. Lambda-функция принимает строку DataFrame как аргумент и возвращает произведение значений из столбцов quantity и price.
Для более сложных вычислений можно использовать именованную функцию:
# Определяем функцию для расчета итоговой стоимости с учетом скидки
def calculate_total_with_discount(row):
base_price = row['quantity'] * row['price']
discount_percent = 5 if row['quantity'] > 10 else 0
discount = base_price * discount_percent / 100
return base_price – discount
# Применяем функцию к каждой строке
df['total_with_discount'] = df.apply(calculate_total_with_discount, axis=1)
Второй подход с использованием серии:
# Создаем серию из двух столбцов
quantity_price = pd.Series(zip(df['quantity'], df['price']))
# Применяем функцию к серии
df['total'] = quantity_price.apply(lambda x: x[0] * x[1])
Этот метод может быть немного эффективнее, чем применение apply() к всему DataFrame, особенно если DataFrame содержит много столбцов.
Марина Соколова, Data Scientist Несколько лет назад я работала над проектом анализа финансовых транзакций, где нужно было классифицировать каждую транзацию по сложным правилам, зависящим от нескольких столбцов. Первоначально я использовала векторизованные операции, но код становился все более запутанным и трудным для поддержки. Переход на метод apply() с четко определенной функцией классификации решил проблему. Хотя производительность немного снизилась, код стал намного понятнее и легче в поддержке. Баланс между читаемостью и производительностью всегда был для меня ключевым фактором при выборе подхода к обработке данных.
Вот сравнение производительности разных вариантов использования apply():
| Метод | Производительность | Читаемость | Удобство отладки |
|---|---|---|---|
| apply() с axis=1 и лямбда | Средняя | Хорошая | Средняя |
| apply() с именованной функцией | Средняя | Отличная | Отличное |
| apply() с серией | Выше средней | Средняя | Средняя |
Когда следует использовать apply() вместо векторизованных операций? Основные случаи:
- Логика вычислений включает условные выражения
- Необходимо вызвать внешние функции или методы
- Требуется доступ к нескольким столбцам с разными типами данных
- Необходимо выполнить операции, которые сложно выразить через векторизацию
Важно помнить, что метод apply() всегда медленнее векторизованных операций, поэтому его стоит использовать только когда вам нужна дополнительная гибкость. 🔄
Использование функции zip и map в Pandas DataFrame
Функции zip() и map() предоставляют еще один элегантный способ обработки двух столбцов в Pandas. Этот подход особенно полезен, когда вам нужно попарно применить функцию к элементам столбцов, сохраняя при этом достаточную производительность.
Основная идея состоит в использовании zip() для объединения значений из двух столбцов в пары, а затем применении функции к этим парам с помощью map().
Рассмотрим базовый пример:
# Умножаем quantity на price с использованием zip и map
df['total'] = list(map(lambda x: x[0] * x[1], zip(df['quantity'], df['price'])))
Что происходит в этом коде:
zip(df['quantity'], df['price'])создает итератор, генерирующий пары значений из обоих столбцов- Функция
lambda x: x[0] * x[1]умножает первый элемент пары на второй map()применяет эту функцию к каждой паре из итератораlist()преобразует результат map в список, который затем присваивается новому столбцу
Для более сложных операций можно использовать именованные функции:
def calculate_discount(qty, price):
total = qty * price
# Скидка 10% для покупок на сумму более 50
return total * 0.9 if total > 50 else total
df['discounted_total'] = list(map(calculate_discount, df['quantity'], df['price']))
Заметьте, что в этом примере мы напрямую передаем столбцы в map(), что упрощает код.
Комбинация zip() и map() особенно эффективна, когда нужно работать с более чем двумя столбцами:
# Добавим столбец с налогом
df['tax_rate'] = [0\.05, 0.07, 0.06, 0.08, 0.05]
# Рассчитаем итоговую сумму с учетом налога
def calculate_total_with_tax(qty, price, tax_rate):
return qty * price * (1 + tax_rate)
df['final_total'] = list(map(calculate_total_with_tax,
df['quantity'],
df['price'],
df['tax_rate']))
Этот метод имеет несколько преимуществ:
- Более высокая производительность по сравнению с
apply()для многих задач - Позволяет легко работать с произвольным количеством столбцов
- Дает доступ к стандартным функциям Python (не ограничен возможностями Pandas)
- Требует минимум дополнительного кода для реализации
Однако у него есть и недостатки:
- Может быть менее читабельным, особенно для сложных операций
- Требует явного преобразования результата в список
- Не позволяет напрямую обращаться к индексам строк
При выборе между zip()+map() и другими методами, руководствуйтесь следующими принципами:
- Используйте этот подход, когда вам нужна производительность выше, чем у
apply(), но требуется гибкость, недоступная в векторизованных операциях - Особенно эффективен для случаев, когда логика обработки зависит от значений в нескольких столбцах
- Хорошо подходит для работы с небольшими и средними DataFrame
Оптимизация производительности при вычислениях в Pandas
Когда речь идет о работе с большими объемами данных, производительность становится критически важным фактором. Выбор правильного метода обработки двух столбцов может значительно повлиять на время выполнения вашего кода.
Давайте рассмотрим несколько стратегий оптимизации производительности при работе с двумя столбцами в Pandas:
1. Выбор правильного метода для конкретной задачи
Проведем сравнительный анализ производительности различных методов на DataFrame с миллионом строк:
import pandas as pd
import numpy as np
import time
# Создаем большой DataFrame для тестирования
n = 1000000
df_large = pd.DataFrame({
'a': np.random.rand(n),
'b': np.random.rand(n)
})
# Тестирование векторизованных операций
start = time.time()
df_large['result1'] = df_large['a'] * df_large['b']
vector_time = time.time() – start
# Тестирование apply + lambda
start = time.time()
df_large['result2'] = df_large.apply(lambda x: x['a'] * x['b'], axis=1)
apply_time = time.time() – start
# Тестирование zip + map
start = time.time()
df_large['result3'] = list(map(lambda x: x[0] * x[1], zip(df_large['a'], df_large['b'])))
zip_map_time = time.time() – start
# Тестирование eval
start = time.time()
df_large['result4'] = df_large.eval('a * b')
eval_time = time.time() – start
print(f"Векторизация: {vector_time:.4f} сек")
print(f"Apply + lambda: {apply_time:.4f} сек")
print(f"Zip + map: {zip_map_time:.4f} сек")
print(f"Eval: {eval_time:.4f} сек")
Результаты такого бенчмарка обычно показывают, что векторизованные операции и eval() значительно быстрее, чем apply() или zip()+map(). Но это справедливо только для простых операций.
Основные рекомендации по выбору метода:
- Для простых арифметических операций: всегда используйте векторизованные операции
- Для сложной логики с условиями:
apply()илиnp.where() - Для обработки нескольких столбцов с одинаковой функцией:
zip()+map() - Для длинных цепочек операций:
eval()
2. Использование метода eval() для сложных выражений
Метод eval() в Pandas позволяет эффективно выполнять строковые выражения без создания промежуточных объектов DataFrame:
# Использование eval() для цепочки операций
df['complex_result'] = df.eval('quantity * price * (1 – discount_rate) + tax')
Преимущества eval():
- Высокая производительность для сложных выражений
- Уменьшение использования памяти за счет отсутствия промежуточных объектов
- Читаемый синтаксис для арифметических операций
3. Использование numpy.where() как альтернатива условным конструкциям
Для условной логики между столбцами, np.where() часто работает быстрее, чем apply():
# Используем np.where для условной логики
df['discount'] = np.where(
df['quantity'] > 10,
df['quantity'] * df['price'] * 0.1, # если quantity > 10
0 # иначе
)
Для более сложной логики можно комбинировать несколько условий:
# Многоуровневая скидка
df['tier_discount'] = np.where(
df['quantity'] > 20, df['price'] * 0.2, # скидка 20% для quantity > 20
np.where(
df['quantity'] > 10, df['price'] * 0.1, # скидка 10% для quantity > 10
np.where(
df['quantity'] > 5, df['price'] * 0.05, # скидка 5% для quantity > 5
0 # нет скидки
)
)
)
4. Предварительная фильтрация данных
Если операция применяется только к части данных, сначала отфильтруйте DataFrame:
# Применяем функцию только к строкам, соответствующим условию
mask = df['quantity'] > 10
df.loc[mask, 'bonus'] = df.loc[mask, 'quantity'] * df.loc[mask, 'price'] * 0.05
5. Использование numba для ускорения пользовательских функций
Библиотека numba позволяет компилировать Python-функции в эффективный машинный код:
from numba import jit
@jit(nopython=True)
def fast_calculation(quantity, price):
return quantity * price * (1 – 0.05 if quantity > 10 else 0)
# Применение numba-оптимизированной функции
result = np.empty(len(df))
for i in range(len(df)):
result[i] = fast_calculation(df.iloc[i]['quantity'], df.iloc[i]['price'])
df['fast_result'] = result
Однако стоит помнить, что накладные расходы на компиляцию могут превышать выигрыш для небольших DataFrame.
При работе с двумя столбцами в Pandas, выбор метода напрямую влияет на производительность, читаемость и масштабируемость вашего кода. Векторизованные операции должны быть вашим первым выбором для простых вычислений благодаря непревзойденной скорости. Для сложной логики используйте apply() с четко структурированными функциями или комбинацию zip() и map(). Постоянно тестируйте производительность на реальных данных и не бойтесь экспериментировать с разными подходами. Помните, что оптимальный метод сегодня может стать узким местом завтра, когда ваши данные вырастут на порядок.