Один из самых популярных (и обсуждаемых) вопросов о пандах — как итерироватьDataFrame
линия в . Для тех, кто загрузил некоторые данные вDataFrame
, эта проблема сразу возникает у новых пользователей, которые теперь хотят сделать с ней что-то полезное. Для большинства программистов естественной мыслью о том, что делать дальше, является создание цикла. Они могут не знать, как использоватьDataFrames
«правильный» способ, но даже опытные разработчики pandas и NumPy рассмотрятDataFrame
Перебирайте строки, чтобы решить проблему. Вместо того, чтобы пытаться найти единственно правильный ответ об итерации, имеет больше смысла понимать связанные с этим проблемы и знать, когда выбрать лучшее решение.
до сих пор,Самый популярный вопрос на Stack Overflow с тегом «панды»:о том, какDataFrame
строка для повторения.оказывается,Эта проблематакжеэто весь сайтСамый копируемый ответ с включенными кодовыми блоками. Разработчики Stack Overflow говорят, что каждую неделю тысячи людей просматривают этот ответ и копируют его, чтобы решить свои проблемы. Очевидно, люди хотятDataFrame
Итерация по строкам!
Используйте лучшие решения вDataFrame
Повторение строк имеет серьезные последствия. Другие ответы на этот вопрос (особенно второй по рейтингу) довольно хорошо предлагают другие варианты, но весь список из 26 (и их количество!) ответов довольно сбивает с толку. Вместо того, чтобы спрашивать _как_ вDataFrame
Имеет больше смысла повторять на линии, чем разбираться, какие варианты доступны, каковы их плюсы и минусы, а затем выбирать тот, который имеет смысл для вас. В некоторых случаях повторный ответ с наибольшим количеством голосов может быть лучшим выбором!
Но я слышал, что итерация неправильная, так ли это?
Сначала выберитеDataFrame
Итерация по строкам не является автоматически неправильным способом решения проблемы. Однако в большинстве случаев то, что новички делают с итерацией, лучше сделать другим способом. Однако никто не должен расстраиваться из-за написания первого решения, использующего итерацию вместо других (возможно, лучших) методов. Часто это лучший способ обучения, и вы можете думать о первом решении как о первом черновике своей статьи, который вы можете улучшить, отредактировав его.
Теперь то, что мы делаем, этоDataFrame
?
Начнем с основного вопроса. Если мы посмотрим на исходный вопрос о переполнении стека, вопрос и ответ просто напечатаны.DataFrame
Содержание. Во-первых, давайте все согласимся, что это не взгляд наDataFrame
хороший способ содержания.DataFrame
стандартный рендеринг, либо сprint
Рендеринг или просмотр с помощью Jupyter Notebookdisplay
, или в виде вывода в ячейке, будет намного лучше, чем то, что будет напечатано в пользовательском формате.
еслиDataFrame
имеет большой размер и по умолчанию может отображать только некоторые столбцы и строки. использоватьhead
иtail
чтобы получить представление о данных. Если вы хотите увидеть только одинDataFrame
подмножество, вместо использования цикла для отображения только этих строк используйтеМощная индексная функция Pandas. Немного потренировавшись, вы сможете выбрать любую комбинацию строк или столбцов для отображения. Начало здесь.
Теперь вместо тривиального примера печати давайте посмотрим, как это сделать в примере, который содержит какую-то логикуDataFrame
, который фактически использует строку данных.
пример
Создадим рабочий примерDataFrame
. Я сделаю это, создав некоторые поддельные данные (используяФакер)Что нужно сделать.Обратите внимание, что эти столбцы представляют собой разные типы данных (у нас есть несколько строк, целое число и дата).
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
from faker import Faker
fake = Faker()
today = datetime.now()
next_month = today + timedelta(days=30)
df = pd.DataFrame([[fake.first_name(), fake.last_name(),
fake.date_this_decade(), fake.date_between_dates(today, next_month),
fake.city(), fake.state(), fake.zipcode(), fake.random_int(-100,1000)]
for r in range(100)],
columns=['first_name', 'last_name', 'start_date',
'end_date', 'city', 'state', 'zipcode', 'balance'])
df['start_date'] = pd.to_datetime(df['start_date']) # convert to datetimes
df['end_date'] = pd.to_datetime(df['end_date'])
df.dtypes
first_name object
last_name object
start_date datetime64[ns]
end_date datetime64[ns]
city object
state object
zipcode object
balance int64
dtype: object
df.head()
first_name last_name start_date end_date city state \
0 Katherine Moody 2020-02-04 2021-06-28 Longberg Maryland
1 Sarah Merritt 2021-03-02 2021-05-30 South Maryborough Tennessee
2 Karen Hensley 2020-02-29 2021-06-23 Brentside Missouri
3 David Ferguson 2020-02-02 2021-06-14 Judithport Virginia
4 Phillip Davis 2020-07-17 2021-06-04 Louisberg Minnesota
zipcode balance
0 20496 493
1 18495 680
2 63702 427
3 66787 587
4 98616 211
первая попытка
Предположим, нашDataFrame
Содержа данные о клиентах, у нас есть функция оценки клиентов, которая использует несколько атрибутов клиентов, чтобы дать им оценку между «A» и «F». Любой клиент с отрицательным балансом отмечен буквой «F», все, что выше 500, — буквой «A», а дальнейшая логика зависит от того, является ли клиент «традиционным» клиентом и в каком штате он живет.
Обратите внимание, что я провел тесты для этой функции, для получения дополнительной информации о том, как проводить модульное тестирование в Jupyter, см.Сообщения о моих модульных тестах Jupyter.
from dataclasses import dataclass
@dataclass
class Customer:
first_name: str
last_name: str
start_date: datetime
end_date: datetime
city: str
state: str
zipcode: str
balance: int
def score_customer(customer:Customer) -> str:
"""Give a customer a credit score.
>>> score_customer(Customer("Joe", "Smith", datetime(2020, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, -5))
'F'
>>> score_customer(Customer("Joe", "Smith", datetime(2020, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 50))
'C'
>>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 50))
'D'
>>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 150))
'C'
>>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 250))
'B'
>>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 350))
'B'
>>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Santa Fe", "California", 88888, 350))
'A'
>>> score_customer(Customer("Joe", "Smith", datetime(2020, 1, 1), datetime(2023,1,1), "Santa Fe", "California", 88888, 50))
'C'
"""
if customer.balance < 0:
return 'F'
if customer.balance > 500:
return 'A'
# legacy vs. non-legacy
if customer.start_date > datetime(2020, 1, 1):
if customer.balance < 100:
return 'D'
elif customer.balance < 200:
return 'C'
elif customer.balance < 300:
return 'B'
else:
if customer.state in ['Illinois', 'Indiana']:
return 'B'
else:
return 'A'
else:
if customer.balance < 100:
return 'C'
else:
return 'A'
import doctest
doctest.testmod()
TestResults(failed=0, attempted=8)
Оцените наших клиентов
Хорошо, теперь, когда у нас есть конкретный пример, как нам получить баллы для всех клиентов? Давайте перейдем прямо к главному ответу на вопрос о переполнении стека,DataFrame.iterrows
. Это генератор, который возвращает индекс строки вместе с этой строкой, какSeries
. Если вы не знакомы с тем, чтоСтроитель,Вы можете думать об этом как об итерируемой функции. Так назови этоnext
Получит первый элемент.
next(df.iterrows())
(0,
first_name Katherine
last_name Moody
start_date 2020-02-04 00:00:00
end_date 2021-06-28 00:00:00
city Longberg
state Maryland
zipcode 20496
balance 493
Name: 0, dtype: object)
Выглядит многообещающе! Это кортеж, содержащий индекс первой строки и сами данные строки. Может быть, мы можем передать его прямо в нашу функцию. Давайте попробуем и посмотрим, что произойдет. Хотя линия являетсяSeries
, но его столбцы такие же, как у нашегоCustomer
Свойства класса такие же, поэтому мы могли бы передать его непосредственно в нашу функцию оценки.
score_customer(next(df.iterrows())[1])
'A'
Ничего себе, это, кажется, работает. Можем ли мы напрямую оценить всю таблицу?
df['score'] = [score_customer(c[1]) for c in df.iterrows()]
Это наш лучший вариант?
Вау, это кажется таким простым. Вы можете понять, почему этот ответ получил наибольшее количество голосов, поскольку кажется, что это именно то, что мы хотим. Почему этот ответ спорный?
Как обычно в случае с пандами (и действительно с любой проблемой программного обеспечения), выбирая идеальное решение зависит от ввода. Давайте суммируем возможные проблемы с различным выбором дизайна. Если заданный вопрос не подходит для вашего конкретного случая использования, то используйтеiterrows
Итерация может быть полностью приемлемым решением! Я не буду судить вас. Я использую это много, и принять решение о возможных решениях в финальном резюме.
За и против использованияiterrows
Аргументы можно сгруппировать в следующие категории.
- Эффективность (скорость и память
- Проблемы, вызванные смешанными типами в одной строке
- Читабельность и ремонтопригодность
скорость и память
В общем, если вы хотите, чтобы в pandas (или Numpy, или любой другой фреймворк обеспечивает векторное вычисление) все было быстро, вам не нужно перебирать элементы, а выбрать векторизованное решение. Однако, даже если решение _могло_ быть векторизовано, это может быть утомительно для программистов, особенно начинающих. Другие ответы на этот вопрос в Stack Overflow предлагают множество других решений. Большинство из них попадают в следующие категории в порядке приоритета скорости.
- векторизация
- Подпрограммы Cython
- Понимание списка (ваниль для цикла).
- DataFrame.apply()
- DataFrame.itertuples() и iteritems()
- DataFrame.iterrows()
векторизация
Основная проблема, связанная с постоянным советом людям о векторизации всего, заключается в том, что иногда векторизованное решение может быть очень громоздким для написания, отладки и поддержки. Чтобы продемонстрировать, что векторизация предпочтительнее, в приведенных примерах обычно демонстрируются тривиальные операции, такие как простое умножение. Но поскольку пример, который я начал в этом посте, — это не просто одно вычисление, я решил написать возможное векторизованное решение этой проблемы.
def vectorized_score(df):
return np.select([df['balance'] < 0,
df['balance'] > 500, # technically not needed, would fall through
((df['start_date'] > datetime(2020,1,1)) &
(df['balance'] < 100)),
((df['start_date'] > datetime(2020,1,1)) &
(df['balance'] >= 100) &
(df['balance'] < 200)),
((df['start_date'] > datetime(2020,1,1)) &
(df['balance'] >= 200) &
(df['balance'] < 300)),
((df['start_date'] > datetime(2020,1,1)) &
(df['balance'] >= 300) &
df['state'].isin(['Illinois', 'Indiana'])),
((df['start_date'] >= datetime(2020,1,1)) &
(df['balance'] < 100)),
], # conditions
['F',
'A',
'D',
'C',
'B',
'B',
'C'], # choices
'A') # default score
assert (df['score'] == vectorized_score(df)).all()
Конечно, есть несколько способов сделать это. я решил использоватьnp.select
(ты сможешья используюwhere
иmask
статьяПодробнее об этом и других обновлениях читайте вDataFrame
метод с). Когда у вас есть несколько подобных условий, мне нравится использоватьnp.select
, хотя это не очень читабельно. Мы также могли бы сделать это с помощью большего количества кода, с обновлением вектора на каждом этапе, что сделало бы его более читабельным. Наверное примерно так же по скорости.
Лично мне это трудно читать, но, возможно, с некоторыми хорошими комментариями это можно будет ясно объяснить будущим сопровождающим (или самому себе в будущем). Но причина, по которой мы делаем векторный код, состоит в том, чтобы сделать его быстрее. для нашего образцаDataFrame
, как производительность?
%timeit vectorized_score(df)
2.75 ms ± 489 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Давайте также замерим время нашего исходного решения.
%timeit [score_customer(c[1]) for c in df.iterrows()]
13.5 ms ± 911 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Итак, мы почти в 5 раз быстрее, только с нашим небольшим набором данных. Эта скорость не имеет значения для небольших наборов данных, но для больших наборов данных простая перезапись может дать вам такую скорость. Я считаю, что, немного подумав и проанализировав, можно написать более быструю векторную версию. Но, пожалуйста, дочитайте до конца и посмотрите, как это работает с большими наборами данных.
Cython
Cython— это проект, который упрощает написание расширений C для Python с использованием (в основном) синтаксиса Python. Признаюсь, я далеко не эксперт по Cython, но я обнаружил, что даже небольшое усилие в Cython может ускорить работу с кодом Python. В этом случае мы показали, что можем сделать векторизованное решение, поэтому использование Cython в невекторизованном решении, возможно, не стоит рассматривать в качестве первого выбора. Тем не менее, я делаюэто здесьНаписал простую версию Cython, которая является самой быстрой из невекторизованных решений на входных данных меньшего размера, даже с небольшими усилиями. Особенно для тех случаев, когда на строку приходится много вычислений, которые нельзя векторизовать, использование Cython может быть хорошим вариантом, но требует определенных затрат времени.
понимание списка
Теперь следующий вариант немного отличается. Признаюсь, я не думаю, что использую эту технику очень часто. Идея здесь состоит в том, чтобы использовать понимание списка с вашимDataFrame
Каждый элемент вызывает вашу функцию. Обратите внимание, что в нашем первом решении я использовал понимание списка, но это то же самое, что иiterrows
. В этот раз не используетсяiterrows
, а прямо изDataFrame
Извлеките данные из каждого столбца , затем выполните итерацию. В этом случае нет созданияSeries
. Если ваша функция имеет несколько параметров, вы можете использоватьzip
чтобы сделать параметрические примитивы в вашемDataFrame
Столбцы передаются в соответствии с порядком аргументов. Теперь, чтобы сделать это, мне нужна модифицированная функция подсчета очков, потому что мойDataFrame
еще не встроенныйCustomer
объектов, а создание их только для вызова этой функции добавляет еще один уровень. Я использую только три свойства клиента, так что вот простое переписывание.
def score_customer_attributes(balance:int, start_date:datetime, state:str) -> str:
if balance < 0:
return 'F'
if balance > 500:
return 'A'
# legacy vs. non-legacy
if start_date > datetime(2020, 1, 1):
if balance < 100:
return 'D'
elif balance < 200:
return 'C'
elif balance < 300:
return 'B'
else:
if state in ['Illinois', 'Indiana']:
return 'B'
else:
return 'A'
else:
if balance < 100:
return 'C'
else:
return 'A'
Вот как выглядит первый цикл обработки списка при вызове функции.
next(zip(df['balance'], df['start_date'], df['state']))
(493, Timestamp('2020-02-04 00:00:00'), 'Maryland')
мы сейчасDataFrame
, строит список всех оценок.
df['score3'] = [score_customer_attributes(*a) for a in zip(df['balance'], df['start_date'], df['state'])]
assert (df['score'] == df['score3']).all()
Как быстро это сейчас?
%timeit [score_customer_attributes(*a) for a in zip(df['balance'], df['start_date'], df['state'])]
171 µs ± 11.2 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Ничего себе, это намного быстрее, более чем в 70 раз быстрее, чем первоначальная обработка этих данных. Оценки быстро вычисляются в пространстве Python, просто беря необработанные данные и вызывая простую функцию Python. Нет необходимости преобразовывать строку вSeries
.
Обратите внимание, что мы также можем вызвать нашу исходную функцию, нам просто нужно сделатьCustomer
передается объект. Это немного уродливо, но все еще довольно быстро.
%timeit [score_customer(Customer(first_name='', last_name='', end_date=None, city=None, zipcode=None, balance=a[0], start_date=a[1], state=a[2])) for a in zip(df['balance'], df['start_date'], df['state'])]
254 µs ± 2.59 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
DataFrame.apply
Мы также можем использоватьDataFrame.apply
. Обратите внимание: чтобы применить это к строкам, вам нужно передать правильную ось, так как по умолчанию она применяется к каждому столбцу. Параметр оси здесь указывает, какой индекс вы хотите иметь в объекте, переданном вашей функции. Мы хотим, чтобы каждый объект был строкой клиента со столбцом в качестве индекса.
assert (df.apply(score_customer, axis=1) == df['score']).all()
%timeit df.apply(score_customer, axis=1)
3.57 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Производительность здесь лучше, чем у нашего оригинала, более чем в 3 раза быстрее. Это также очень удобочитаемо и позволяет нам использовать наши примитивные функции, которые легко читать и поддерживать. Но это все еще медленнее, чем понимание списка, потому что оно создаетSeries
объект.
DataFrame.iteritems и DataFrame.itertuples
Теперь мы более подробно рассмотрим обычный итеративный подход. заDataFrame
с, есть триiter
функция:iteritems
,itertuples
, иiterrows
.DataFrames
Существует также прямая поддержка итерации, но не все эти функции повторяют одно и то же. Поскольку понять, что делают эти методы, просто взглянув на их имена, может быть очень сложно, давайте рассмотрим их здесь.
-
iter(df)
(перечислитьDataFrame.__iter__
метод). Пересеките информационную ось, дляDataFrames
, которое является именем столбца, а не значением.
next(iter(df)) # 'first_name'
'first_name'
-
iteritems
. выполняет итерацию по столбцам, возвращая кортеж имени столбца и столбца, какSeries
.
next(df.iteritems())
next(df.items()) # these two are equivalent
('first_name',
0 Katherine
1 Sarah
2 Karen
3 David
4 Phillip
...
95 Robert
96 Christopher
97 Kristen
98 Nicholas
99 Caroline
Name: first_name, Length: 100, dtype: object)
-
items
. Это то же самое, что и выше.iteritems
на самом деле просто звонитitems
.
next(df.iterrows())
(0,
first_name Katherine
last_name Moody
start_date 2020-02-04 00:00:00
end_date 2021-06-28 00:00:00
city Longberg
state Maryland
zipcode 20496
balance 493
score A
score3 A
Name: 0, dtype: object)
-
iterrows
. Мы видели это, он перебирает строки, но возвращает их как кортеж индекса и строки, какSeries
. -
itertuples
, перебирает строки, возвращая по одному для каждой строкиnamedtuple
. При желании вы можете изменить имя кортежа и отключить возвращаемый индекс.
next(df.itertuples())
Pandas(Index=0, first_name='Katherine', last_name='Moody', start_date=Timestamp('2020-02-04 00:00:00'), end_date=Timestamp('2021-06-28 00:00:00'), city='Longberg', state='Maryland', zipcode='20496', balance=493, score='A', score3='A')
использовать itertuples
Поскольку мы виделиiterrows
, мы просто должны посмотреть наitertuples
. Как видите, возвращаемое значение,namedtuple
, который можно использовать в нашей исходной функции.
assert ([score_customer(t) for t in df.itertuples()] == df['score']).all()
%timeit [score_customer(t) for t in df.itertuples()]
858 µs ± 5.23 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Производительность здесь довольно хорошая, более чем в 12 раз быстрее. построить по одному для каждой строкиnamedtuple
, чем строитьSeries
, намного быстрее.
Смешанные типы в одной строке
Сейчас предлагаетсяiterrows
иitertuples
Хорошее время для еще одного отличия. Одинnamedtuple
, который может соответствующим образом представить любой тип в одной строке. В нашем случае у нас есть строки, типы дат и целые числа. Тем не менее, пандыSeries
,На протяженииSeries
, должен быть только один тип данных. Поскольку наши типы данных достаточно разнообразны, все они представлены в видеobject
, которые в итоге сохранили свой тип и не имели для нас функциональных проблем. Тем не менее, это не всегда так!
Например, если ваши столбцы имеют разные числовые типы, они в конечном итоге будут представлять все типы. Это может привести к вашемуitertuples
иiterrows
, данные, возвращаемые этими двумя методами, немного отличаются, так что будьте внимательны.
dfmixed = pd.DataFrame({'integer_column': [1,2,3], 'float_column': [1.1, 2.2, 3.3]})
dfmixed.dtypes
integer_column int64
float_column float64
dtype: object
next(dfmixed.itertuples())
Pandas(Index=0, integer_column=1, float_column=1.1)
next(dfmixed.iterrows())
(0,
integer_column 1.0
float_column 1.1
Name: 0, dtype: float64)
имя столбца
Еще одно предупреждение. если вашDataFrame
Есть столбцы, которые не могут быть представлены именами переменных Python, и вы не сможете получить к ним доступ с помощью точечного синтаксиса. Итак, если у вас есть файл с именем2b
илиMy Column
столбцы, то вам придется использовать имя позиции для доступа к ним (например, первый столбец будет называться_1
). заiterrows
, линия будетSeries
, поэтому вы должны использовать["2b"]
или["My Column"]
для доступа к колонке.
Другие варианты
Конечно, есть и другие итерационные варианты. Например, вы можете увеличить целочисленное смещение и использоватьiloc
индексатор вDataFrame
, чтобы выбрать любую строку. Конечно, это ничем не отличается от других итераций, а также нестандартно, поэтому другим может быть сложно читать и понимать при чтении вашего кода. Я построил наивную версию в приведенном ниже коде сравнения производительности, если хотите (с ужасной производительностью).
хорошо выбирай
Выбор правильного решения в основном зависит от двух факторов.
- Насколько велик ваш набор данных?
- Что вы можете легко написать (и поддерживать)?
На изображении ниже вы можете увидеть время выполнения рассмотренного нами решения (Код для создания этого находится здесь).Как видите, только векторизованное решение хорошо работает с большими данными. Если ваш набор данных большой, векторизованное решение может быть вашим единственным разумным вариантом.
Сравнение времени выполнения различных методов в нашем DataFrame.
Однако, в зависимости от того, сколько раз вам нужно выполнить код, сколько времени вам потребуется, чтобы сделать его правильно, и вашей способности поддерживать код, вы можете выбрать любое из других решений, и это будет хорошо. На самом деле все эти решения линейно растут с увеличением объема данных.
Возможно, один из способов думать об этом - это не просто нотация с большой буквой O, а нотация с большой буквой U. Другими словами, сколько времени вам потребуется, чтобы написать правильное решение? Если это меньше, чем время выполнения вашего кода, итеративное решение может быть идеальным. Однако, если вы пишете производственный код, найдите время, чтобы научиться векторизовать.
Еще один момент: иногда легко написать итеративное решение для меньшего набора, и вы можете сначала сделать это, а затем написать векторизованную версию. Проверьте свои результаты с помощью итеративного решения, чтобы убедиться, что вы все делаете правильно, а затем используйте векторизованную версию для большего полного набора данных.
Я надеюсь, что вы чувствуете себя хорошо на этот разDataFrame
Итеративные глубокие погружения интересны. Я знаю, что узнал что-то полезное на этом пути.
The postHow to iterate over DataFrame rows (and should you? )appeared first onwrighters.io.