Как перебирать строки в pandas и почему вам не следует этого делать
Оглавление
- Как перебирать строки фрейма данных в pandas
- Почему вам обычно следует избегать перебора строк в pandas
- Используйте Векторизованные Методы на протяжении всей Итерации
- Используйте Промежуточные Столбцы, Чтобы Вы Могли Использовать Векторизованные Методы
- Заключение
Один из самых распространенных вопросов, который может возникнуть у вас при знакомстве с миром pandas - это как перебирать строки в pandas Фрейм данных. Если вы освоились с использованием циклов в core Python, то этот вопрос вполне естественен.
Хотя итерация по строкам относительно проста с помощью .itertuples() или .iterrows(),, это не обязательно означает, что итерация - лучший способ работы с фреймами данных. На самом деле, хотя итерация может быть быстрым способом достижения прогресса, использование итераций может стать серьезным препятствием на пути к повышению эффективности работы с pandas.
В этом руководстве вы узнаете, как выполнять итерацию по строкам во фрейме данных pandas, но вы также узнаете, почему вам, вероятно, этого не хочется. Как правило, вам лучше избегать повторений, поскольку это приводит к снижению производительности и противоречит принципам panda.
Чтобы ознакомиться с этим руководством, вы можете загрузить наборы данных и примеры кода по следующей ссылке:
Бесплатный пример кода: Нажмите здесь, чтобы загрузить бесплатный пример кода и наборы данных, которые вы будете использовать для изучения итераций по строкам в pandas DataFrame против использования векторизованных методов.
Последним этапом подготовительной работы является создание виртуальной среды и установка нескольких пакетов:
PS> python -m venv venv
PS> venv\Scripts\activate
(venv) PS> python -m pip install pandas httpx codetiming
$ python -m venv venv $ source venv/bin/activate (venv) $ python -m pip install pandas httpx codetiming
Установка pandas не станет для вас сюрпризом, но вы можете задаться вопросом о других. В одном из примеров вы будете использовать пакет httpx для выполнения некоторых HTTP-запросов, а пакет codetiming - для быстрого сравнения производительности.
Теперь вы готовы приступить к работе и узнать, как выполнять итерацию по строкам, почему вы, вероятно, этого не хотите и какие еще варианты следует исключить, прежде чем прибегать к итерации.
Как выполнить итерацию по строкам фрейма данных в pandas
Хотя это и редкость, но в некоторых ситуациях вам может сойти с рук итерация по фреймворку данных. Как правило, в таких ситуациях вы:
- Необходимо последовательно передавать информацию из фрейма данных pandas в другой API
- Требуется, чтобы операция над каждой строкой вызывала побочный эффект, такой как HTTP-запрос
- Должны выполняться сложные операции с использованием различных столбцов во фрейме данных
- Не обращайте внимания на снижение производительности итераций, возможно, потому, что работа с данными не является узким местом, набор данных очень мал или это просто личный проект
Чаще всего циклы используются в pandas, когда вы в интерактивном режиме изучаете данные и экспериментируете с ними. В таких случаях производительность обычно не вызывает беспокойства. Перебирая строки данных, вы можете отобразить отдельные строки и ознакомиться с ними. Основываясь на этом опыте, вы сможете позже внедрить более эффективные подходы.
В качестве примера более постоянного использования представьте, что у вас есть список URL-адресов во фрейме данных, и вы хотите проверить, какие URL-адреса находятся в сети. В загружаемых материалах вы найдете CSV-файл с некоторыми данными о наиболее популярных веб-сайтах, которые вы можете загрузить во фрейм данных:
>>> import pandas as pd
>>> websites = pd.read_csv("resources/popular_websites.csv", index_col=0)
>>> websites
name url total_views
0 Google https://www.google.com 5.207268e+11
1 YouTube https://www.youtube.com 2.358132e+11
2 Facebook https://www.facebook.com 2.230157e+11
3 Yahoo https://www.yahoo.com 1.256544e+11
4 Wikipedia https://www.wikipedia.org 4.467364e+10
5 Baidu https://www.baidu.com 4.409759e+10
6 Twitter https://twitter.com 3.098676e+10
7 Yandex https://yandex.com 2.857980e+10
8 Instagram https://www.instagram.com 2.621520e+10
9 AOL https://www.aol.com 2.321232e+10
10 Netscape https://www.netscape.com 5.750000e+06
11 Nope https://alwaysfails.example.com 0.000000e+00
Эти данные содержат название веб-сайта, его URL-адрес и общее количество просмотров за неопределенный период времени. В примере pandas показывает количество просмотров в научной записи. У вас также есть фиктивный веб-сайт для тестирования.
Вы хотите написать средство проверки подключения для проверки URL-адресов и предоставления удобочитаемого сообщения, указывающего, подключен ли веб-сайт к сети или он перенаправляется на другой URL-адрес:
>>> import httpx
>>> def check_connection(name, url):
... try:
... response = httpx.get(url)
... location = response.headers.get("location")
... if location is None or location.startswith(url):
... print(f"{name} is online!")
... else:
... print(f"{name} is online! But redirects to {location}")
... return True
... except httpx.ConnectError:
... print(f"Failed to establish a connection with {url}")
... return False
...
Здесь вы определили функцию check_connection(), которая выполняет запрос и распечатывает сообщения для заданного имени и URL.
С помощью этой функции вы будете использовать как столбцы url, так и столбцы name. Вы не очень заботитесь о производительности чтения значений из фрейма данных по двум причинам — отчасти потому, что данные очень малы, но главным образом потому, что приемник в реальном времени выполняет HTTP-запросов, а не чтение из фрейма данных.
Кроме того, вы хотите проверить, не работает ли какой-либо из веб-сайтов. То есть вас интересует побочный эффект, а не добавление информации во фрейм данных.
По этим причинам вам может сойти с рук использование .itertuples():
>>> for website in websites.itertuples():
... check_connection(website.name, website.url)
...
Google is online!
YouTube is online!
Facebook is online!
Yahoo is online!
Wikipedia is online!
Baidu is online!
Twitter is online!
Yandex is online!
Instagram is online!
AOL is online!
Netscape is online! But redirects to https://www.aol.com/
Failed to establish a connection with https://alwaysfails.example.com
Здесь вы используете for цикл в итераторе, который вы получаете из .itertuples(). Итератор выдает namedtuple для каждой строки. Используя точечную запись, вы выбираете два столбца для ввода в функцию check_connection().
Примечание: Если по какой-либо причине вы хотите использовать динамические значения для выбора столбцов из каждой строки, вы можете использовать .iterrows(), хотя это немного медленнее. Метод .iterrows() возвращает кортеж из двух элементов, содержащий индексный номер и объект Series для каждой строки. Та же итерация, что и выше, будет выглядеть следующим образом с .iterrows():
for _, website in websites.iterrows():
check_connection(website["name"], website["url"])
В этом коде вы отбрасываете индексный номер из каждого кортежа, созданного с помощью .iterrows(). Затем с помощью объекта Series вы можете использовать индексацию в квадратных скобках ([]), чтобы выбрать нужные вам столбцы из каждой строки. Индексация в квадратных скобках позволяет использовать любое выражение, например переменную, в квадратных скобках.
В этом разделе вы рассмотрели, как выполнять итерацию по строкам фрейма данных pandas. Хотя итерация имеет смысл для продемонстрированного здесь варианта использования, вы должны быть осторожны при применении этих знаний в других местах. Может возникнуть соблазн использовать итерацию для выполнения многих других типов задач в pandas, но это не в стиле pandas. Далее вы узнаете основную причину этого.
Почему вам обычно следует избегать перебора строк в pandas
Библиотека pandas использует программирование массивов или векторизацию, что значительно повышает ее производительность. Векторизация - это поиск способов применить операцию к набору значений сразу, а не по одному.
Например, если у вас есть два списка чисел и вы хотите добавить каждый элемент в другой, то вы можете создать цикл for, чтобы просмотреть и добавить каждый элемент в свой аналог:
>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> for a_int, b_int in zip(a, b):
... print(a_int + b_int)
...
5
7
9
Хотя циклирование является вполне приемлемым подходом, pandas и некоторые библиотеки, от которых оно зависит — например, NumPy — используют программирование массивов для иметь возможность работать со всем списком гораздо более эффективным образом.
Векторизованные функции создают впечатление, что вы оперируете всем списком за одну операцию. Такой подход позволяет библиотекам использовать параллелизм, специальное аппаратное обеспечение процессора и памяти, а также низкоуровневые компилируемые языки, такие как C.
Все эти и другие методы позволяют выполнять векторизованные операции значительно быстрее, чем явные циклы, когда одна операция должна быть применена к последовательности элементов. Например, pandas рекомендует вам рассматривать операции как то, что вы применяете к сразу ко всем столбцам, а не к одной строке за раз.
Использование векторизованных операций над табличными данными - это то, что делает pandas "пандами". Вам всегда следует сначала искать векторизованные операции. Существует множество DataFrame и Series методов на выбор, поэтому держите превосходную документацию по pandas под рукой.
Поскольку векторизация является неотъемлемой частью pandas, вы часто будете слышать, как люди говорят если вы используете цикл в pandas, то вы делаете это неправильно. Или, возможно, даже что-то более экстремальное, из замечательной статьи @ryxcommar:
Циклы в pandas - это грех. (Источник)
Хотя эти утверждения могут быть преувеличены для пущего эффекта, они являются хорошим практическим правилом, если вы новичок в pandas. Практически все, что вам нужно сделать с вашими данными, возможно с помощью векторизованных методов. Если для вашей работы существует определенный метод, то, как правило, лучше всего использовать этот метод — для скорости, надежности и удобства чтения.
Аналогично, в фантастических канонических материалах StackOverflow для pandas, собранных Coldsp33d, вы найдете еще одно взвешенное предупреждение о недопустимости повторения:
Итерация в Pandas - это анти-шаблон, и это то, что вы должны делать только тогда, когда исчерпали все остальные варианты. (Источник)
Ознакомьтесь с материалами canonic для получения дополнительных показателей производительности и информации о том, какие другие опции доступны.
В принципе, когда вы используете pandas для того, для чего он предназначен — анализа данных и других операций с данными, - вы почти всегда можете положиться на векторизованные операции. Но иногда вам нужно писать код на окраине территории pandas, и тогда вам может сойти с рук повторение. Это тот случай, когда вы взаимодействуете с другими API, например, для выполнения HTTP-запросов, как вы делали в предыдущем примере.
Принятие векторизованного мышления на первый взгляд может показаться немного странным. Большая часть обучения программированию включает в себя изучение итераций, и теперь вам говорят, что вам нужно придумать операцию, выполняемую над последовательностью элементов одновременно ? Что это за колдовство? Но если вы собираетесь использовать pandas, то воспользуйтесь векторизацией и получите в награду высокопроизводительные, чистые и идиоматичные pandas.
В следующем разделе вы познакомитесь с парой примеров, в которых итерация сравнивается с векторизацией, и сравните их производительность.
Используйте векторизованные Методы на протяжении всей Итерации
В этом и следующем разделах вы рассмотрите примеры случаев, когда у вас может возникнуть соблазн использовать итеративный подход, но когда векторизованные методы значительно быстрее.
Допустим, вы хотели получить сумму всех просмотров в наборе данных веб-сайта, с которым вы работали ранее в этом руководстве.
Чтобы использовать итеративный подход, вы могли бы использовать .itertuples():
>>> import pandas as pd
>>> websites = pd.read_csv("resources/popular_websites.csv", index_col=0)
>>> total = 0
>>> for website in websites.itertuples():
... total += website.total_views
...
>>> total
1302975468008.0
Это будет представлять собой итеративный подход к вычислению суммы. У вас есть цикл for, который выполняется строка за строкой, принимая значение и увеличивая переменную total. Теперь вы, возможно, узнаете более логичный подход к получению суммы:
>>> sum(website.total_views for website in websites.itertuples())
1302975468008.0
Здесь вы используете sum() встроенный метод вместе с генераторным выражением, чтобы получить сумму.
Хотя эти подходы могут показаться достойными — и они, безусловно, работают, — они не являются идиоматическими пандами, особенно когда у вас есть .sum() доступный векторизованный метод:
>>> websites["total_views"].sum()
1302975468008.0
Здесь вы выбираете столбец total_views с индексацией в квадратных скобках во фрейме данных. Эта индексация возвращает объект Series, представляющий столбец total_views. Затем вы используете метод .sum() для всей серии.
Наиболее очевидным преимуществом этого метода является то, что он, пожалуй, самый читаемый из трех. Но его читабельность, хотя и чрезвычайно важна, не является самым существенным преимуществом.
Ознакомьтесь с приведенным ниже сценарием, в котором вы используете пакет codetiming для сравнения трех методов:
# take_sum_codetiming.py
import pandas as pd
from codetiming import Timer
def loop_sum(websites):
total = 0
for website in websites.itertuples():
total += website.total_views
return total
def python_sum(websites):
return sum(website.total_views for website in websites.itertuples())
def pandas_sum(websites):
return websites["total_views"].sum()
for func in [loop_sum, python_sum, pandas_sum]:
websites = pd.read_csv("resources/popular_websites.csv", index_col=0)
with Timer(name=func.__name__, text="{name:20}: {milliseconds:.2f} ms"):
func(websites)
В этом скрипте вы определяете три функции, каждая из которых принимает сумму в столбце total_views. Все функции принимают фрейм данных и возвращают сумму, но они используют следующие три подхода, соответственно:
- Цикл
forи.itertuples() - Функция Python
sum()и понимание с помощью.itertuples() - Панды
.sum()векторизованный метод
Это три подхода, которые вы рассмотрели выше, но теперь вы используете codetiming.Timer, чтобы узнать, насколько быстро выполняется каждая функция.
Ваши точные результаты могут отличаться, но пропорция должна быть примерно такой, как вы можете видеть ниже:
$ python take_sum_codetiming.py
loop_sum : 0.24 ms
python_sum : 0.19 ms
pandas_sum : 0.14 ms
Даже для такого крошечного набора данных, как этот, разница в производительности довольно существенна: .sum() в pandas работает почти в два раза быстрее, чем в цикле. Встроенный в Python sum() является улучшением по сравнению с loop, но он по-прежнему не подходит для pandas.
Примечание: codetiming разработан для удобства мониторинга времени выполнения вашего производственного кода. При использовании библиотеки для сравнительного анализа, как вы делаете здесь, вам следует запустить свой код несколько раз, чтобы проверить стабильность ваших таймингов.
Тем не менее, при таком маленьком наборе данных это не совсем соответствует масштабу оптимизации, которого может достичь векторизация. Чтобы перейти на следующий уровень, вы можете искусственно увеличить набор данных, продублировав строки тысячу раз, например:
# python take_sum_codetiming.py
# ...
for func in [pandas_sum, loop_sum, python_sum]:
websites = pd.read_csv("resources/popular_websites.csv", index_col=0)
+ websites = pd.concat([websites for _ in range(1000)])
with Timer(name=func.__name__, text="{name:20}: {milliseconds:.2f} ms"):
func(websites)
В этой модификации используется функция concat() для объединения тысячи экземпляров websites друг с другом. Теперь у вас есть набор данных из нескольких тысяч строк. Повторный запуск сценария синхронизации даст результаты, аналогичные приведенным ниже:
$ python take_sum_codetiming.py
loop_sum : 3.55 ms
python_sum : 3.67 ms
pandas_sum : 0.15 ms
Похоже, что метод pandas .sum() по-прежнему занимает примерно столько же времени, в то время как loop и метод Python sum() значительно увеличились. Обратите внимание, что .sum() в pandas примерно в двадцать раз быстрее, чем в обычных циклах Python!
В следующем разделе вы увидите пример работы с векторизацией, даже если pandas не предлагает конкретного векторизованного метода для вашей задачи.
Используйте Промежуточные Столбцы, Чтобы Вы Могли Использовать Векторизованные Методы
Возможно, вы слышали, что итерацию можно использовать, когда вам нужно использовать несколько столбцов для получения нужного результата. Возьмем, к примеру, набор данных, представляющий продажи продукта за месяц:
>>> import pandas as pd
>>> products = pd.read_csv("resources/products.csv")
>>> products
month sales unit_price
0 january 3 0.50
1 february 2 0.53
2 march 5 0.55
3 april 10 0.71
4 may 8 0.66
Эти данные содержат столбцы с количеством продаж и средней ценой за единицу товара за данный месяц. Но что вам нужно, так это совокупная сумма от общего дохода за несколько месяцев.
Возможно, вы знаете, что у pandas есть .cumsum() метод получения суммарной суммы. Но в этом случае вам придется сначала умножить столбец sales на столбец unit_price, чтобы получить общий объем продаж за каждый месяц.
Такая ситуация может подтолкнуть вас к повторению, но есть способ обойти эти ограничения. Вы можете использовать промежуточные столбцы, даже если это означает выполнение двух векторизованных операций. В этом случае вы бы сначала умножили sales и unit_price, чтобы получить новый столбец, а затем использовали .cumsum() для нового столбца.
Рассмотрим этот сценарий, в котором вы сравниваете производительность этих двух подходов, генерируя фрейм данных с дополнительным cumulative_sum столбцом:
# cumulative_sum_codetiming.py
import pandas as pd
from codetiming import Timer
def loop_cumsum(products):
cumulative_sum = []
for product in products.itertuples():
income = product.sales * product.unit_price
if cumulative_sum:
cumulative_sum.append(cumulative_sum[-1] + income)
else:
cumulative_sum.append(income)
return products.assign(cumulative_income=cumulative_sum)
def pandas_cumsum(products):
return products.assign(
income=lambda df: df["sales"] * df["unit_price"],
cumulative_income=lambda df: df["income"].cumsum(),
).drop(columns="income")
for func in [loop_cumsum, pandas_cumsum]:
products = pd.read_csv("resources/products.csv")
with Timer(name=func.__name__, text="{name:20}: {milliseconds:.2f} ms"):
func(products)
В этом сценарии вы хотите добавить столбец во фрейм данных, и поэтому каждая функция принимает фрейм данных с значением products и будет использовать метод .assign() для возврата фрейма данных с новым столбцом с именем cumulative_sum.
Метод .assign() принимает аргументы из ключевых слов, которые будут именами столбцов. Это могут быть имена, которых еще нет во фрейме данных, или те, которые уже существуют. Если столбцы уже существуют, то pandas обновит их.
Значением каждого аргумента ключевого слова может быть функция обратного вызова, которая принимает фрейм данных и возвращает последовательность. В приведенном выше примере в функции pandas_cumsum() в качестве обратных вызовов используются лямбда-функции. Каждый обратный вызов возвращает новую последовательность.
В pandas_cumsum() первый обратный вызов создает столбец income путем умножения столбцов sales и unit_price вместе. Второй обратный вызов вызывает .cumsum() в новом столбце income. После выполнения этих операций вы используете метод .drop(), чтобы удалить промежуточный столбец income.
Запуск этого скрипта приведет к результатам, аналогичным следующим:
$ python cumulative_sum_codetiming.py
loop_cumsum : 0.43 ms
pandas_cumsum : 1.04 ms
Подождите, цикл на самом деле быстрее? Разве векторизованный метод не должен был быть быстрее?
Как оказалось, для абсолютно крошечных наборов данных, подобных этим, затраты на выполнение двух векторизованных операций — умножение двух столбцов и последующее использование метода .cumsum() — медленнее, чем на итерацию. Но продолжайте и увеличьте цифры так же, как вы делали это в предыдущем тесте:
for f in [loop_cumsum, pandas_cumsum]:
products = pd.read_csv("resources/products.csv")
+ products = pd.concat(products for _ in range(1000))
with Timer(name=f.__name__, text="{name:20}: {milliseconds:.2f} ms"):
Работа с набором данных, который в тысячу раз больше, покажет почти ту же историю, что и с .sum():
$ python cumulative_sum_codetiming.py
loop_cumsum : 2.80 ms
pandas_cumsum : 1.21 ms
pandas снова продвигается вперед и будет продвигаться все более стремительно по мере увеличения вашего набора данных. Несмотря на то, что ему приходится выполнять две векторизованные операции, как только ваш набор данных становится больше нескольких сотен строк, pandas прекращает итерацию.
И не только это, но и то, что в итоге вы получаете красивый идиоматический код pandas, который другие специалисты pandas распознают и смогут быстро прочитать. Хотя может потребоваться некоторое время, чтобы привыкнуть к такому способу написания кода, вам никогда не захочется возвращаться к нему!
Заключение
В этом руководстве вы узнали, как выполнять итерацию по строкам фрейма данных и в каких случаях такой подход может иметь смысл. Но вы также узнали о том, почему вам, вероятно, не захочется делать это в большинстве случаев. Вы узнали о векторизации и о том, как искать способы использования векторизованных методов вместо итераций, и в итоге у вас получились красивые, невероятно быстрые, идиоматичные pandas.
Бесплатный пример кода: Нажмите здесь, чтобы загрузить бесплатный пример кода и наборы данных, которые вы будете использовать для изучения итераций по строкам в pandas DataFrame против использования векторизованных методов.
Ознакомьтесь с загружаемыми материалами, где вы найдете еще один пример, сравнивающий производительность векторизованных методов с другими альтернативами, включая некоторые варианты составления списков, которые на самом деле превосходят векторизованные операции.
Back to Top