Метод pandas apply для столбцов: эффективная обработка данных
Для кого эта статья:
- Аналитики данных, желающие улучшить свои навыки в pandas
- Студенты и профессионалы, обучающиеся в сфере анализа данных и науки о данных
Специалисты, работающие с большими объёмами данных и стремящиеся оптимизировать свои рабочие процессы
Обработка данных в pandas может превратиться из утомительного процесса в элегантное решение, если владеть правильными инструментами. Метод
apply()— один из таких инструментов, настоящий швейцарский нож аналитика данных. Хотя многие знают о существовании этого метода, далеко не все используют его потенциал на 100%, особенно при работе с отдельными столбцами DataFrame. Сегодня разберём, какapply()может трансформировать ваш код, делая его чище, эффективнее и производительнее. 🚀
Хотите освоить не только
apply(), но и весь арсенал инструментов для анализа данных? Курс Профессия аналитик данных от Skypro предлагает глубокое погружение в pandas и другие библиотеки Python для обработки информации. Вы научитесь не просто применять отдельные функции, а создавать полноценные аналитические пайплайны, решая реальные бизнес-задачи. Стройте карьеру в одной из самых востребованных областей IT!
Основы метода
Метод apply() в pandas предназначен для применения функции к каждому элементу Series (столбца) или к каждой строке/столбцу DataFrame. Это мощный инструмент, позволяющий выполнять преобразования данных без явных циклов, что делает код более читаемым и, зачастую, более эффективным.
При работе с отдельным столбцом DataFrame, метод apply() принимает функцию в качестве аргумента и применяет её к каждому элементу выбранного столбца. Эта функция может быть:
- Встроенной функцией Python (например,
len,str.upper) - Функцией из библиотек (например,
numpy.sqrt) - Пользовательской функцией (определённой через
def) - Анонимной lambda-функцией
Рассмотрим простой пример. Допустим, у нас есть DataFrame с информацией о товарах:
import pandas as pd
# Создаём простой DataFrame
data = {
'product': ['Ноутбук', 'Смартфон', 'Наушники', 'Монитор'],
'price': [75000, 45000, 8000, 25000]
}
df = pd.DataFrame(data)
Теперь применим функцию к столбцу 'price', чтобы рассчитать цену со скидкой 10%:
# Применение apply() к столбцу 'price'
df['discounted_price'] = df['price'].apply(lambda x: x * 0.9)
print(df)
Результат будет следующим:
| product | price | discounted_price |
|---|---|---|
| Ноутбук | 75000 | 67500.0 |
| Смартфон | 45000 | 40500.0 |
| Наушники | 8000 | 7200.0 |
| Монитор | 25000 | 22500.0 |
В этом примере мы использовали lambda-функцию, которая умножает каждое значение на 0.9. Метод apply() последовательно применил эту функцию к каждому элементу столбца 'price'.

Синтаксис и принципы работы
Основной синтаксис применения apply() к одному столбцу выглядит следующим образом:
df['column_name'].apply(function, args=(), **kwargs)
Где:
- df['column_name'] — столбец DataFrame, к которому применяется функция
- function — функция, которая будет применена к каждому элементу
- args — дополнительные позиционные аргументы для передачи в функцию
- kwargs — дополнительные именованные аргументы для функции
Александр Петров, Data Science Lead
Когда я только начинал работать с pandas, я часто использовал циклы для обработки данных. Помню свой первый крупный проект: нужно было проанализировать логи сервера с миллионами записей. Каждая строка содержала временную метку, которую требовалось преобразовать в удобный формат.
Мой первоначальный код выглядел примерно так:
PythonСкопировать кодfor i in range(len(df)): df.loc[i, 'formatted_time'] = convert_timestamp(df.loc[i, 'timestamp'])На датасете из миллиона строк этот код работал почти 40 минут! Когда я заменил его на:
PythonСкопировать кодdf['formatted_time'] = df['timestamp'].apply(convert_timestamp)Время выполнения сократилось до 2 минут. Это был момент прозрения — я понял, насколько мощными могут быть встроенные методы pandas по сравнению с обычными Python-циклами.
Принципы работы apply() с одним столбцом:
- Последовательная обработка:
apply()последовательно обрабатывает каждый элемент столбца. - Создание нового Series: результатом выполнения
apply()является новый объект Series, который можно присвоить новому столбцу или использовать отдельно. - Сохранение индексов: полученный Series сохраняет индексы исходного столбца.
- Возможность изменения типа данных: функция может возвращать значения другого типа, чем исходные элементы столбца.
Рассмотрим несколько примеров с различными типами функций:
# Создадим DataFrame с текстовыми данными
text_df = pd.DataFrame({
'text': ['Python', 'Pandas', 'Data Analysis', 'Machine Learning']
})
# 1. Встроенная функция
text_df['length'] = text_df['text'].apply(len)
# 2. Метод строки
text_df['uppercase'] = text_df['text'].apply(str.upper)
# 3. Пользовательская функция
def count_vowels(text):
vowels = 'aeiou'
return sum(1 for char in text.lower() if char in vowels)
text_df['vowels'] = text_df['text'].apply(count_vowels)
# 4. Lambda-функция
text_df['starts_with_p'] = text_df['text'].apply(lambda x: x.startswith('P'))
print(text_df)
Результат:
| text | length | uppercase | vowels | startswithp |
|---|---|---|---|---|
| Python | 6 | PYTHON | 1 | True |
| Pandas | 6 | PANDAS | 2 | True |
| Data Analysis | 13 | DATA ANALYSIS | 5 | False |
| Machine Learning | 16 | MACHINE LEARNING | 6 | False |
Практические примеры
Метод apply() универсален и может применяться к столбцам с различными типами данных. Рассмотрим несколько практических примеров для числовых, текстовых, временных данных и более сложных структур. 📊
1. Числовые данные
import pandas as pd
import numpy as np
# Создаём DataFrame с числовыми данными
numeric_df = pd.DataFrame({
'value': [10, 25, -3, 42, 0, -15],
'other_value': [2, 5, 1, 0, 8, 3]
})
# Применяем математические функции
numeric_df['squared'] = numeric_df['value'].apply(np.square)
numeric_df['sqrt'] = numeric_df['value'].apply(lambda x: np.sqrt(x) if x >= 0 else np.nan)
# Применяем функцию с условиями
numeric_df['category'] = numeric_df['value'].apply(
lambda x: 'high' if x > 20 else 'medium' if x > 0 else 'low'
)
# Функция, использующая несколько столбцов через .loc
def safe_divide(row_idx, df):
a = df.loc[row_idx, 'value']
b = df.loc[row_idx, 'other_value']
return a / b if b != 0 else np.inf
numeric_df['division'] = numeric_df.index.map(lambda idx: safe_divide(idx, numeric_df))
print(numeric_df)
2. Текстовые данные
# DataFrame с текстовыми данными
text_df = pd.DataFrame({
'name': ['John Smith', 'Mary Johnson', 'Robert Williams', 'Susan Brown'],
'email': ['john@example.com', 'mary.j@mail.co.uk', 'rob_w@domain.org', 'susan.brown@company.net']
})
# Извлечение имени и фамилии
text_df['first_name'] = text_df['name'].apply(lambda x: x.split()[0])
text_df['last_name'] = text_df['name'].apply(lambda x: x.split()[1])
# Извлечение домена из email
text_df['email_domain'] = text_df['email'].apply(lambda x: x.split('@')[1])
# Создание приветственного сообщения с помощью пользовательской функции
def create_greeting(name):
first_name = name.split()[0]
return f"Hello, {first_name}! Welcome to our platform."
text_df['greeting'] = text_df['name'].apply(create_greeting)
print(text_df)
3. Временные данные
# DataFrame с временными данными
from datetime import datetime
date_df = pd.DataFrame({
'date': pd.date_range(start='2023-01-01', periods=5),
'event': ['Meeting', 'Conference', 'Workshop', 'Training', 'Presentation']
})
# Извлечение компонентов даты
date_df['year'] = date_df['date'].apply(lambda x: x.year)
date_df['month'] = date_df['date'].apply(lambda x: x.month)
date_df['day_of_week'] = date_df['date'].apply(lambda x: x.day_name())
# Форматирование даты
date_df['formatted_date'] = date_df['date'].apply(lambda x: x.strftime('%d/%m/%Y'))
# Определение, является ли дата выходным
date_df['is_weekend'] = date_df['date'].apply(lambda x: x.day_name() in ['Saturday', 'Sunday'])
print(date_df)
4. Сложные структуры
# DataFrame с JSON-подобными данными
json_df = pd.DataFrame({
'user_id': [1, 2, 3, 4],
'preferences': [
{'theme': 'dark', 'notifications': True, 'language': 'en'},
{'theme': 'light', 'notifications': False, 'language': 'fr'},
{'theme': 'auto', 'notifications': True, 'language': 'de'},
{'theme': 'dark', 'notifications': False, 'language': 'es'}
]
})
# Извлечение значений из словаря
json_df['theme'] = json_df['preferences'].apply(lambda x: x.get('theme'))
json_df['language'] = json_df['preferences'].apply(lambda x: x.get('language'))
# Более сложное преобразование
def format_preferences(prefs):
theme = prefs.get('theme', 'default')
notif = 'enabled' if prefs.get('notifications', False) else 'disabled'
lang = prefs.get('language', 'en').upper()
return f"Theme: {theme}, Notifications: {notif}, Language: {lang}"
json_df['formatted_preferences'] = json_df['preferences'].apply(format_preferences)
print(json_df)
Мария Иванова, Data Engineer
В нашей команде мы получили задание обработать датасет с отзывами пользователей. Нужно было классифицировать отзывы по тональности, извлечь упоминаемые продукты и нормализовать текст.
Первоначально мой коллега предложил последовательно создавать новые столбцы:
PythonСкопировать код# Очистка текста df['clean_text'] = df['text'].apply(clean_text_function) # Классификация тональности df['sentiment'] = df['clean_text'].apply(classify_sentiment) # Извлечение продуктов df['products'] = df['clean_text'].apply(extract_products)Каждый вызов
apply()проходил по всему датасету, что было неэффективно. Я предложила альтернативный подход:PythonСкопировать кодdef process_review(text): clean = clean_text_function(text) sentiment = classify_sentiment(clean) products = extract_products(clean) return pd.Series([clean, sentiment, products]) df[['clean_text', 'sentiment', 'products']] = df['text'].apply(process_review)Это решение сократило время обработки на 40%, так как мы проходили по данным только один раз, а не три.
Сравнение метода
Метод apply() — не единственный способ трансформации данных в pandas. Давайте сравним его с альтернативными подходами и определим, когда какой метод использовать эффективнее. 🔄
| Метод | Преимущества | Недостатки | Лучше использовать, когда: |
|---|---|---|---|
apply() | – Гибкость (можно использовать сложные функции)<br>- Читаемость кода<br>- Сохранение индексов | – Медленнее векторизованных операций<br>- Последовательное выполнение | – Нужна сложная логика преобразования<br>- Используются пользовательские функции<br>- Требуется доступ к нескольким столбцам |
map() | – Проще синтаксис<br>- Немного быстрее apply() для простых операций<br>- Работает со словарями для замены | – Только для Series (не DataFrame)<br>- Менее гибкий, чем apply() | – Простые преобразования одного столбца<br>- Замена значений по словарю<br>- Небольшие наборы данных |
| Векторизация | – Наивысшая производительность<br>- Параллельное выполнение<br>- Оптимизированный код | – Ограниченные возможности сложных преобразований<br>- Менее читаемый код для сложной логики | – Математические операции<br>- Большие наборы данных<br>- Простые трансформации |
| Циклы Python | – Максимальная гибкость<br>- Понятная логика для новичков | – Очень низкая производительность<br>- Громоздкий код | – Почти никогда (исключение: прототипирование)<br>- Уникальные случаи, где нужен полный контроль |
Давайте рассмотрим конкретный пример для сравнения производительности различных подходов:
import pandas as pd
import numpy as np
import time
# Создадим большой DataFrame для теста
n = 1_000_000
df = pd.DataFrame({
'value': np.random.randn(n)
})
# Функция для тестирования времени выполнения
def time_execution(func, name):
start = time.time()
result = func()
end = time.time()
print(f"{name}: {end – start:.4f} секунд")
return result
# 1. Используем apply() с lambda-функцией
time_execution(
lambda: df['value'].apply(lambda x: x**2 if x > 0 else x),
"apply() с lambda"
)
# 2. Используем map() с lambda-функцией
time_execution(
lambda: df['value'].map(lambda x: x**2 if x > 0 else x),
"map() с lambda"
)
# 3. Используем векторизацию с numpy
time_execution(
lambda: np.where(df['value'] > 0, df['value']**2, df['value']),
"Векторизация с numpy"
)
# 4. Используем цикл Python (на небольшом подмножестве для скорости)
def using_loop():
result = df['value'].copy()
for i in range(10000): # Используем только первые 10,000 строк
result.iloc[i] = result.iloc[i]**2 if result.iloc[i] > 0 else result.iloc[i]
return result
time_execution(using_loop, "Python-цикл (10k строк)")
Результаты сравнения показывают, что векторизованные операции с numpy значительно быстрее, чем apply() или map(). Однако стоит учитывать не только производительность, но и читаемость кода, а также сложность требуемых преобразований.
Когда лучше использовать apply() для одного столбца:
- Когда требуется сложная логика, которую сложно или невозможно выразить векторизованно
- Когда нужно применить пользовательскую функцию
- Когда читаемость кода важнее незначительных улучшений производительности
- Когда функция должна обращаться к другим столбцам или внешним данным
Оптимизация pandas
Метод apply() очень удобен, но при работе с большими объемами данных его использование может привести к проблемам с производительностью. Рассмотрим несколько стратегий оптимизации для повышения эффективности. 🔧
1. Использование встроенных функций вместо apply()
Многие операции можно выполнить с помощью встроенных методов pandas без использования apply():
# Вместо
df['value_squared'] = df['value'].apply(lambda x: x**2)
# Лучше использовать
df['value_squared'] = df['value'] ** 2
# Вместо
df['is_positive'] = df['value'].apply(lambda x: x > 0)
# Лучше использовать
df['is_positive'] = df['value'] > 0
2. Предварительная компиляция функций с numba
Библиотека numba позволяет компилировать Python-функции в оптимизированный машинный код:
from numba import jit
# Компилируем функцию с помощью numba
@jit(nopython=True)
def complex_calculation(x):
result = 0
for i in range(100):
result += x * i / (i + 1)
return result
# Применяем скомпилированную функцию
df['result'] = df['value'].apply(complex_calculation)
3. Использование pandas.eval() для сложных выражений
# Вместо
df['complex_result'] = df['a'] * df['b'] + df['c'] * df['d']
# Можно использовать
df['complex_result'] = pd.eval('a * b + c * d', engine='numexpr', target=df)
4. Разделение на части (chunking)
При работе с очень большими данными можно обрабатывать их частями:
def process_in_chunks(dataframe, chunk_size=100000):
result_chunks = []
for start_idx in range(0, len(dataframe), chunk_size):
end_idx = min(start_idx + chunk_size, len(dataframe))
chunk = dataframe.iloc[start_idx:end_idx].copy()
# Обработка чанка
chunk['result'] = chunk['value'].apply(complex_function)
result_chunks.append(chunk)
return pd.concat(result_chunks)
processed_df = process_in_chunks(df)
5. Параллельная обработка с pandarallel
Библиотека pandarallel позволяет распараллелить выполнение apply():
from pandarallel import pandarallel
# Инициализация pandarallel
pandarallel.initialize()
# Использование parallel_apply вместо apply
df['result'] = df['value'].parallel_apply(complex_function)
6. Сравнение производительности различных подходов
Оптимизация должна основываться на конкретных требованиях к производительности. Ниже приведено сравнение различных методов для распространенной задачи – преобразования строк:
- Векторизация: Используйте встроенные функции pandas и numpy, когда это возможно
- Компиляция: Для сложных функций используйте
numbaдля предварительной компиляции - Параллелизация: Для независимых операций используйте параллельную обработку
- Группировка операций: Объединяйте несколько операций в одну функцию для уменьшения проходов по данным
Пример оптимизации функции для обработки текстовых данных:
# Неоптимизированная версия с несколькими вызовами apply()
df['text_lower'] = df['text'].apply(str.lower)
df['text_clean'] = df['text_lower'].apply(lambda x: x.replace('[^\w\s]', ''))
df['word_count'] = df['text_clean'].apply(lambda x: len(x.split()))
df['char_count'] = df['text_clean'].apply(len)
# Оптимизированная версия с одним вызовом apply()
def process_text(text):
text_lower = text.lower()
text_clean = ''.join(c for c in text_lower if c.isalnum() or c.isspace())
word_count = len(text_clean.split())
char_count = len(text_clean)
return pd.Series([text_lower, text_clean, word_count, char_count])
df[['text_lower', 'text_clean', 'word_count', 'char_count']] = df['text'].apply(process_text)
Выбор стратегии оптимизации зависит от конкретной задачи, размера данных и требований к производительности. Иногда читаемость кода и удобство поддержки могут быть важнее небольшого выигрыша в производительности.
Метод
apply()— универсальный и мощный инструмент для трансформации данных в pandas. Мы изучили его синтаксис, принципы работы с одним столбцом, рассмотрели множество практических примеров для различных типов данных и сравнили с альтернативными подходами. Ключ к эффективному использованию pandas — баланс между элегантностью кода, гибкостью и производительностью. Правильно применяяapply()в сочетании с векторизацией и другими оптимизациями, вы сможете создавать эффективные и поддерживаемые решения для анализа данных, которые масштабируются от прототипов до производственных систем. 💪