Как перебирать строки DataFrame (и что делать?

Android

Один из самых популярных (и обсуждаемых) вопросов о пандах — как итерировать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Аргументы можно сгруппировать в следующие категории.

  1. Эффективность (скорость и память
  2. Проблемы, вызванные смешанными типами в одной строке
  3. Читабельность и ремонтопригодность

скорость и память

В общем, если вы хотите, чтобы в pandas (или Numpy, или любой другой фреймворк обеспечивает векторное вычисление) все было быстро, вам не нужно перебирать элементы, а выбрать векторизованное решение. Однако, даже если решение _могло_ быть векторизовано, это может быть утомительно для программистов, особенно начинающих. Другие ответы на этот вопрос в Stack Overflow предлагают множество других решений. Большинство из них попадают в следующие категории в порядке приоритета скорости.

  1. векторизация
  2. Подпрограммы Cython
  3. Понимание списка (ваниль для цикла).
  4. DataFrame.apply()
  5. DataFrame.itertuples() и iteritems()
  6. 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, чтобы выбрать любую строку. Конечно, это ничем не отличается от других итераций, а также нестандартно, поэтому другим может быть сложно читать и понимать при чтении вашего кода. Я построил наивную версию в приведенном ниже коде сравнения производительности, если хотите (с ужасной производительностью).

хорошо выбирай

Выбор правильного решения в основном зависит от двух факторов.

  1. Насколько велик ваш набор данных?
  2. Что вы можете легко написать (и поддерживать)?

На изображении ниже вы можете увидеть время выполнения рассмотренного нами решения (Код для создания этого находится здесь).Как видите, только векторизованное решение хорошо работает с большими данными. Если ваш набор данных большой, векторизованное решение может быть вашим единственным разумным вариантом.

Сравнение времени выполнения различных методов в нашем DataFrame.

Однако, в зависимости от того, сколько раз вам нужно выполнить код, сколько времени вам потребуется, чтобы сделать его правильно, и вашей способности поддерживать код, вы можете выбрать любое из других решений, и это будет хорошо. На самом деле все эти решения линейно растут с увеличением объема данных.

Возможно, один из способов думать об этом - это не просто нотация с большой буквой O, а нотация с большой буквой U. Другими словами, сколько времени вам потребуется, чтобы написать правильное решение? Если это меньше, чем время выполнения вашего кода, итеративное решение может быть идеальным. Однако, если вы пишете производственный код, найдите время, чтобы научиться векторизовать.

Еще один момент: иногда легко написать итеративное решение для меньшего набора, и вы можете сначала сделать это, а затем написать векторизованную версию. Проверьте свои результаты с помощью итеративного решения, чтобы убедиться, что вы все делаете правильно, а затем используйте векторизованную версию для большего полного набора данных.

Я надеюсь, что вы чувствуете себя хорошо на этот разDataFrameИтеративные глубокие погружения интересны. Я знаю, что узнал что-то полезное на этом пути.

The postHow to iterate over DataFrame rows (and should you? )appeared first onwrighters.io.