Каков наиболее эффективный способ перебрать фреймы данных с помощью панд?

Я хочу последовательно выполнять свои собственные сложные операции с финансовыми данными в фреймах данных.

Например, я использую следующий CSV-файл MSFT, взятый из Yahoo Finance:

Date,Open,High,Low,Close,Volume,Adj Close
2011-10-19,27.37,27.47,27.01,27.13,42880000,27.13
2011-10-18,26.94,27.40,26.80,27.31,52487900,27.31
2011-10-17,27.11,27.42,26.85,26.98,39433400,26.98
2011-10-14,27.31,27.50,27.02,27.27,50947700,27.27

....

Затем я делаю следующее:

#!/usr/bin/env python
from pandas import *

df = read_csv('table.csv')

for i, row in enumerate(df.values):
    date = df.index[i]
    open, high, low, close, adjclose = row
    #now perform analysis on open/close based on date, etc..

Это самый эффективный способ? Учитывая акцент на скорости в пандах, я бы предположил, что должна быть какая-то специальная функция для перебора значений таким образом, чтобы можно было получить индекс (возможно, через генератор, чтобы эффективно использовать память)? df.iteritems, к сожалению, выполняет итерацию только столбец за столбцом.


person Muppet    schedule 20.10.2011    source источник
comment
вы пробовали написать функцию и передать ее df.apply()?   -  person naught101    schedule 16.04.2015
comment
Если вам нужна эффективность памяти, вам следует рассмотреть возможность использования векторизованных операций (с использованием матриц и векторов). Но я не знаю панд, поэтому не могу сказать, возможны ли там такие операции.   -  person mike    schedule 10.08.2015
comment
Ссылаясь на unutbu, NumPy, похоже, поддерживает векторизованные операции (The key to speed with NumPy arrays is to perform your operations on the whole array at once).   -  person mike    schedule 10.08.2015
comment
Вопрос касался последовательной итерации, что очень часто встречается в финансах, где векторизация не всегда возможна. И принятый ответ Ника Кроуфорда отвечает на этот вопрос и дополнительно предостерегает использовать векторизацию, где это возможно.   -  person Muppet    schedule 31.05.2019


Ответы (12)


Новейшие версии pandas теперь включают встроенную функцию для перебора строк.

for index, row in df.iterrows():

    # do some logic here

Или, если хотите быстрее, используйте itertuples()

Но предложение unutbu использовать функции numpy, чтобы избежать итерации по строкам, даст самый быстрый код.

person Nick Crawford    schedule 23.07.2012
comment
Обратите внимание, что iterrows работает очень медленно (он преобразует каждую строку в серию, потенциально влияя на ваши типы данных). Когда вам нужен итератор, лучше использовать itertuples - person joris; 29.07.2015
comment
BTW itertuples возвращает именованные кортежи (docs.python.org/3/library/), поэтому вы можете получить доступ к каждому столбцу по имени с помощью row.high или getattr (row, 'high') - person seanv507; 17.04.2016
comment
Имейте в виду, согласно текущим документам: Вам не следует изменять то, что вы повторяете. Это не гарантируется во всех случаях. В зависимости от типов данных итератор возвращает копию, а не представление, и запись в него не будет иметь никакого эффекта. - person viddik13; 07.12.2016
comment
Эй, как использовать решение unutbu, если я хочу эффективно выполнять итерацию по серии Pandas? - person datavinci; 21.05.2017
comment
@joris. Не могу с вами согласиться, itertuples примерно в 100 раз старше, чем iterrows. - person GoingMyWay; 07.11.2017
comment
fjsj: поскольку вопрос был задан о «цикле» и «эффективности», я дал ответ, который ответил на обе части вопроса. Никто не должен перебирать фрейм данных без крайней необходимости. - person Nick Crawford; 09.01.2019
comment
itertuples (name = None) работает еще быстрее, потому что он дает обычные кортежи вместо именованных кортежей. См. Эту интересную статью: medium.com/swlh/ - person Ismael EL ATIFI; 23.12.2020

Pandas основан на массивах NumPy. Ключ к ускорению работы с массивами NumPy - это выполнение операций со всем массивом сразу, а не построчно или построчно.

Например, если close - это одномерный массив, и вы хотите, чтобы процентное изменение дня было изменено,

pct_change = close[1:]/close[:-1]

Это вычисляет весь массив процентных изменений как один оператор, а не

pct_change = []
for row in close:
    pct_change.append(...)

Поэтому постарайтесь полностью избежать цикла Python for i, row in enumerate(...) и подумайте о том, как выполнять свои вычисления с операциями над всем массивом (или фреймом данных) в целом, а не построчно.

person unutbu    schedule 20.10.2011
comment
Я согласен с тем, что это лучший способ, и это то, что я обычно делаю для простых операций. Однако в данном случае это невозможно, поскольку результирующие операции могут быть очень сложными. В частности, я пытаюсь протестировать торговые стратегии на исторических данных. Например. если цена находится на новом минимуме в течение 30 дней, тогда мы можем захотеть купить акцию и выйти из нее всякий раз, когда будет выполнено определенное условие, и это необходимо смоделировать на месте. Этот простой пример все еще может быть выполнен с помощью векторизации, однако, чем сложнее становится торговая стратегия, тем меньше вероятность использования векторизации. - person Muppet; 20.10.2011
comment
Вам нужно будет более подробно объяснить точный расчет, который вы пытаетесь выполнить. Это помогает сначала написать код любым удобным для вас способом, а затем профилировать и оптимизировать его. - person unutbu; 20.10.2011
comment
Кстати, для некоторых вычислений (особенно тех, которые не могут быть выражены как операции над целыми массивами) код с использованием списков Python может быть быстрее, чем эквивалентный код с использованием массивов numpy. - person unutbu; 20.10.2011
comment
Я согласен, что векторизация - это правильное решение, где это возможно, но иногда итерационный алгоритм - единственный способ. - person Wes McKinney; 21.10.2011
comment
поздний комментарий, но я обнаружил, что попытки выполнить полный расчет для столбца иногда трудно писать и отлаживать. Рассмотрение промежуточных столбцов расчета, упрощает отладку и понимание расчетов. обнаружили, что даже самая сложная логика может быть реализована таким образом, избегая при этом зацикливания. - person Joop; 22.09.2014

Как уже упоминалось ранее, объект pandas наиболее эффективен при одновременной обработке всего массива. Однако для тех, кому действительно нужно перебрать pandas DataFrame для выполнения чего-то, я нашел как минимум три способа сделать это. Я провел небольшой тест, чтобы увидеть, какой из трех занимает меньше всего времени.

t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
B = []
C = []
A = time.time()
for i,r in t.iterrows():
    C.append((r['a'], r['b']))
B.append(time.time()-A)

C = []
A = time.time()
for ir in t.itertuples():
    C.append((ir[1], ir[2]))    
B.append(time.time()-A)

C = []
A = time.time()
for r in zip(t['a'], t['b']):
    C.append((r[0], r[1]))
B.append(time.time()-A)

print B

Результат:

[0.5639059543609619, 0.017839908599853516, 0.005645036697387695]

Вероятно, это не лучший способ измерить потребление времени, но для меня он быстрый.

Вот некоторые плюсы и минусы ИМХО:

  • .iterrows (): возвращать индекс и элементы строки в отдельных переменных, но значительно медленнее
  • .itertuples (): быстрее, чем .iterrows (), но возвращает индекс вместе с элементами строки, ir [0] - это индекс
  • zip: самый быстрый, но без доступа к индексу строки

РЕДАКТИРОВАТЬ 2020/11/10

Вот обновленный тест с некоторыми другими альтернативами (производительность с MacBookPro 2,4 ГГц Intel Core i9 8 ядер 32 Go 2667 МГц DDR4)

import sys
import tqdm
import time
import pandas as pd

B = []
t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
for _ in tqdm.tqdm(range(10)):
    C = []
    A = time.time()
    for i,r in t.iterrows():
        C.append((r['a'], r['b']))
    B.append({"method": "iterrows", "time": time.time()-A})

    C = []
    A = time.time()
    for ir in t.itertuples():
        C.append((ir[1], ir[2]))
    B.append({"method": "itertuples", "time": time.time()-A})

    C = []
    A = time.time()
    for r in zip(t['a'], t['b']):
        C.append((r[0], r[1]))
    B.append({"method": "zip", "time": time.time()-A})

    C = []
    A = time.time()
    for r in zip(*t.to_dict("list").values()):
        C.append((r[0], r[1]))
    B.append({"method": "zip + to_dict('list')", "time": time.time()-A})

    C = []
    A = time.time()
    for r in t.to_dict("records"):
        C.append((r["a"], r["b"]))
    B.append({"method": "to_dict('records')", "time": time.time()-A})

    A = time.time()
    t.agg(tuple, axis=1).tolist()
    B.append({"method": "agg", "time": time.time()-A})

    A = time.time()
    t.apply(tuple, axis=1).tolist()
    B.append({"method": "apply", "time": time.time()-A})

print(f'Python {sys.version} on {sys.platform}')
print(f"Pandas version {pd.__version__}")
print(
    pd.DataFrame(B).groupby("method").agg(["mean", "std"]).xs("time", axis=1).sort_values("mean")
)

## Output

Python 3.7.9 (default, Oct 13 2020, 10:58:24) 
[Clang 12.0.0 (clang-1200.0.32.2)] on darwin
Pandas version 1.1.4
                           mean       std
method                                   
zip + to_dict('list')  0.002353  0.000168
zip                    0.003381  0.000250
itertuples             0.007659  0.000728
to_dict('records')     0.025838  0.001458
agg                    0.066391  0.007044
apply                  0.067753  0.006997
iterrows               0.647215  0.019600
person Richard Wong    schedule 16.12.2015
comment
NB в Python 3 zip() возвращает итератор, поэтому используйте list(zip()) - person Louis Maddox; 12.10.2016
comment
Не могли бы вы использовать t.index для просмотра индекса? - person elPastor; 22.12.2016
comment
Отлично; спасибо Ричард. Это по-прежнему актуально для Python 3.7+. С 286 секунд с интервалом до 3,62 с застежкой-молнией. Спасибо - person pacta_sunt_servanda; 16.05.2019
comment
Я повторно запустил этот тест с pandas .__ version__ == 1.1.4, Python 3.7.9 и новым MacBookPro 2,4 ГГц Intel Core i9 8 ядер 32 Go 2667 МГц DDR4, и результаты еще хуже для iterrows(): [0.6970570087432861, 0.008062124252319336, 0.0036787986755371094] - person ClementWalter; 10.11.2020
comment
@ClementWalter, здорово! - person Richard Wong; 12.11.2020

Вы можете прокручивать строки, транспонируя и затем вызывая iteritems:

for date, row in df.T.iteritems():
   # do some logic here

Я не уверен в эффективности в этом случае. Чтобы добиться максимальной производительности итеративного алгоритма, вы можете попробовать написать его в Cython, чтобы сделать что-то вроде:

def my_algo(ndarray[object] dates, ndarray[float64_t] open,
            ndarray[float64_t] low, ndarray[float64_t] high,
            ndarray[float64_t] close, ndarray[float64_t] volume):
    cdef:
        Py_ssize_t i, n
        float64_t foo
    n = len(dates)

    for i from 0 <= i < n:
        foo = close[i] - open[i] # will be extremely fast

Я бы порекомендовал сначала написать алгоритм на чистом Python, убедитесь, что он работает, и посмотрите, насколько он быстр - если он недостаточно быстр, конвертируйте такие вещи в Cython, как этот, с минимальными усилиями, чтобы получить что-то такое же быстрое, как написанное вручную C / C ++.

person Wes McKinney    schedule 21.10.2011
comment
Я также рекомендую Cython; Я работал над аналогичной проблемой при создании своего движка для тестирования на истории, и я получил ускорение в 1000 раз. Затем я объединил это с библиотекой многопроцессорной обработки, что является очень хорошей комбинацией. - person vgoklani; 07.10.2012
comment
Этот ответ требует обновления, чтобы включить новый df.iterrows() в соответствии с ответом @ NickCrawford. - person LondonRob; 06.06.2014
comment
df.T.iteritems() - отличное решение вместо использования df.iterrows(), если вы хотите перебирать определенный столбец +1 - person Alireza; 25.10.2015
comment
Выдает ошибку: def my_algo(ndarray[object] dates, ndarray[float64_t] opn, ^ SyntaxError: invalid syntax - person Bhishan Poudel; 01.04.2019

У вас есть три варианта:

По index (простейший ):

>>> for index in df.index:
...     print ("df[" + str(index) + "]['B']=" + str(df['B'][index]))

С iterrows (большинство использовал):

>>> for index, row in df.iterrows():
...     print ("df[" + str(index) + "]['B']=" + str(row['B']))

С помощью itertuples (самый быстрый ):

>>> for row in df.itertuples():
...     print ("df[" + str(row.Index) + "]['B']=" + str(row.B))

Три варианта отображают что-то вроде:

df[0]['B']=125
df[1]['B']=415
df[2]['B']=23
df[3]['B']=456
df[4]['B']=189
df[5]['B']=456
df[6]['B']=12

Источник: alphons .io

person Fifi    schedule 04.02.2018

Я проверил iterrows после того, как заметил ответ Ника Кроуфорда, но обнаружил, что он дает кортежи (index, Series). Не уверен, что лучше всего подойдет вам, но в итоге я использовал метод itertuples для своей проблемы, который дает кортежи (index, row_value1 ...).

Также есть iterkv, который выполняет итерацию по кортежам (столбцам, сериям).

person beardc    schedule 29.07.2012
comment
вы можете сделать что-то вроде dict (row), чтобы создать набор из строки с доступными для поиска столбцами - person Carst; 17.10.2013
comment
Я также обнаружил, что itertuples работает намного быстрее (в 10 раз) в моем случае использования, поскольку объекты Series не создаются. - person Kamil Sindi; 11.06.2014
comment
К сведению: iterkv устарело с версии 0.13.1 - person JS.; 10.09.2015

В качестве небольшого дополнения вы также можете применить приложение, если у вас есть сложная функция, которую вы применяете к одному столбцу:

http://pandas.pydata.org/pandas-docs/dev/generated/pandas.DataFrame.apply.html

df[b] = df[a].apply(lambda col: do stuff with col here)
person Carst    schedule 16.10.2013
comment
вероятно, x - непонятное имя для имени столбца и переменной строки, хотя я согласен, что apply - самый простой способ сделать это :) - person Andy Hayden; 17.10.2013
comment
просто чтобы добавить, apply можно также применить к нескольким столбцам: df['c'] = df[['a','b']].apply(lambda x: do stuff with x[0] and x[1] here, axis=1) - person fantabolous; 16.08.2014
comment
Можно ли применить дубль к функции, определенной в другом месте кода? это сделано для того, чтобы мы могли ввести более сложную функцию - person user308827; 09.11.2014
comment
Да, лямбда-функция может использовать любые пользовательские функции. Имейте в виду: если у вас большой фрейм данных, вы можете вместо этого вернуться к cython (у Python есть немного накладных расходов, когда дело доходит до вызова функций) - person Carst; 18.11.2014
comment
Я переименовал x в ›col. Лучшее имя - person smci; 05.02.2015
comment
DataFrame.apply не ограничивается одним столбцом; попробуйте df.apply(lambda x: print(x.a + x,b), axis = 1) - person Tom; 21.09.2016
comment
@ user308827 Функция, которую вы даете apply (), не обязательно должна быть лямбда, может быть любой вызываемой - например, функция, определенная в другом месте, или класс, который определяет call и т. д. - person Egor Kraev; 09.10.2019

Как отметил @joris, iterrows намного медленнее, чем itertuples, а itertuples примерно в 100 раз длиннее, чем iterrows, и я тестировал скорость обоих методов в DataFrame с 5027505 записями результат для iterrows, это 1200 бит / с, а itertuples - 120000 бит / с.

Если вы используете itertuples, обратите внимание, что каждый элемент в цикле for является именованным кортежем, поэтому, чтобы получить значение в каждом столбце, вы можете обратиться к следующему примеру кода

>>> df = pd.DataFrame({'col1': [1, 2], 'col2': [0.1, 0.2]},
                      index=['a', 'b'])
>>> df
   col1  col2
a     1   0.1
b     2   0.2
>>> for row in df.itertuples():
...     print(row.col1, row.col2)
...
1, 0.1
2, 0.2
person GoingMyWay    schedule 07.11.2017

Конечно, самый быстрый способ перебрать фрейм данных - это получить доступ к базовому numpy ndarray либо через df.values (как вы), либо через доступ к каждому столбцу отдельно df.column_name.values. Поскольку вы тоже хотите иметь доступ к индексу, вы можете использовать для этого df.index.values.

index = df.index.values
column_of_interest1 = df.column_name1.values
...
column_of_interestk = df.column_namek.values

for i in range(df.shape[0]):
   index_value = index[i]
   ...
   column_value_k = column_of_interest_k[i]

Не питонический? Конечно. Но быстро.

Если вы хотите выжать больше из цикла, вам следует изучить cython. Cython позволит вам получить огромное ускорение (подумайте, в 10-100 раз). Для максимальной производительности проверьте просмотры памяти для cython.

person Vlad    schedule 23.03.2018

Другое предложение - объединить groupby с векторизованными вычислениями, если подмножества строк имеют общие характеристики, которые позволяют вам это делать.

person JoeCondron    schedule 14.11.2014

посмотри на последний

t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
B = []
C = []
A = time.time()
for i,r in t.iterrows():
    C.append((r['a'], r['b']))
B.append(round(time.time()-A,5))

C = []
A = time.time()
for ir in t.itertuples():
    C.append((ir[1], ir[2]))    
B.append(round(time.time()-A,5))

C = []
A = time.time()
for r in zip(t['a'], t['b']):
    C.append((r[0], r[1]))
B.append(round(time.time()-A,5))

C = []
A = time.time()
for r in range(len(t)):
    C.append((t.loc[r, 'a'], t.loc[r, 'b']))
B.append(round(time.time()-A,5))

C = []
A = time.time()
[C.append((x,y)) for x,y in zip(t['a'], t['b'])]
B.append(round(time.time()-A,5))
B

0.46424
0.00505
0.00245
0.09879
0.00209
person Леонид Невежин    schedule 14.01.2021

Я считаю, что самый простой и эффективный способ перебрать DataFrames - использовать numpy и numba. В этом случае во многих случаях цикл может быть примерно таким же быстрым, как и векторизованные операции. Если numba не подходит, то, вероятно, будет следующим лучшим вариантом. Как уже много раз отмечалось, по умолчанию должна быть векторизация, но этот ответ просто рассматривает эффективный цикл, учитывая решение о цикле по какой-либо причине.

В качестве тестового примера давайте воспользуемся примером из ответа @ DSM о вычислении процентного изменения. Это очень простая ситуация, и с практической точки зрения вы не стали бы писать цикл для его вычисления, но как таковой он обеспечивает разумную основу для синхронизации векторизованных подходов по сравнению с циклами.

Давайте настроим 4 подхода с небольшим DataFrame, и мы приурочим их к большему набору данных ниже.

import pandas as pd
import numpy as np
import numba as nb

df = pd.DataFrame( { 'close':[100,105,95,105] } )

pandas_vectorized = df.close.pct_change()[1:]

x = df.close.to_numpy()
numpy_vectorized = ( x[1:] - x[:-1] ) / x[:-1]
        
def test_numpy(x):
    pct_chng = np.zeros(len(x))
    for i in range(1,len(x)):
        pct_chng[i] = ( x[i] - x[i-1] ) / x[i-1]
    return pct_chng

numpy_loop = test_numpy(df.close.to_numpy())[1:]

@nb.jit(nopython=True)
def test_numba(x):
    pct_chng = np.zeros(len(x))
    for i in range(1,len(x)):
        pct_chng[i] = ( x[i] - x[i-1] ) / x[i-1]
    return pct_chng
    
numba_loop = test_numba(df.close.to_numpy())[1:]

И вот тайминги в DataFrame со 100000 строками (тайминги, выполненные с помощью функции Jupyter %timeit, свернуты в сводную таблицу для удобства чтения):

pandas/vectorized   1,130 micro-seconds
numpy/vectorized      382 micro-seconds
numpy/looped       72,800 micro-seconds
numba/looped          455 micro-seconds

Резюме: для простых случаев, подобных этому, вы бы использовали (векторизованные) панды для простоты и удобочитаемости и (векторизованные) numpy для скорости. Если вам действительно нужно использовать цикл, сделайте это в numpy. Если доступно numba, объедините его с numpy для дополнительной скорости. В этом случае numpy + numba почти так же быстро, как векторизованный код numpy.

Другие детали:

  • Не показаны различные параметры, такие как iterrows, iteruples и т. Д., Которые работают на несколько порядков медленнее и действительно никогда не должны использоваться.
  • Тайминги здесь довольно типичны: numpy быстрее, чем pandas, а векторизация быстрее, чем циклы, но добавление numba к numpy часто значительно ускоряет numpy.
  • Все, кроме опции pandas, требует преобразования столбца DataFrame в массив numpy. Это преобразование включено в тайминги.
  • Время для определения / компиляции функций numpy / numba не было включено в тайминги, но, как правило, было бы незначительным компонентом времени для любого большого фрейма данных.
person JohnE    schedule 22.12.2020