인가 for
루프는 정말 “나쁜”? 그렇지 않다면 기존의 “벡터화 된”접근 방식을 사용하는 것보다 어떤 상황에서 더 나을까요? 1
저는 “벡터화”의 개념과 팬더가 계산 속도를 높이기 위해 벡터화 기술을 사용하는 방법에 대해 잘 알고 있습니다. 벡터화 된 함수는 전체 시리즈 또는 DataFrame에 대한 작업을 브로드 캐스트하여 기존의 데이터 반복보다 훨씬 더 빠른 속도를 달성합니다.
그러나 for
루프 및 목록 이해를 사용하여 데이터를 반복하는 문제에 대한 솔루션을 제공하는 많은 코드 (Stack Overflow의 답변 포함)를보고 놀랐습니다 . 문서와 API는 루프가 “나쁘다”고, 배열, 시리즈 또는 데이터 프레임을 “절대”반복해서는 안된다고 말합니다. 그렇다면 때때로 사용자가 루프 기반 솔루션을 제안하는 것을 보는 이유는 무엇입니까?
1-질문이 다소 광범위하게 들리는 것은 사실이지만 for
루프가 일반적으로 데이터를 반복하는 것보다 일반적으로 더 나은 매우 특정한 상황이 있다는 것 입니다. 이 포스트는 이것을 후세를 위해 포착하는 것을 목표로합니다.
답변
TLDR; 아니요, for
루프는 적어도 항상 그런 것은 아닙니다. 일부 벡터화 된 작업이 반복보다 느리다고 말하는 것이, 일부 벡터화 된 작업보다 반복이 더 빠르다고 말하는 것이 더 정확할 것입니다 . 코드에서 최고의 성능을 얻으려면 언제, 왜 그런지 아는 것이 중요합니다. 요컨대, 다음은 벡터화 된 팬더 함수의 대안을 고려할 가치가있는 상황입니다.
- 데이터가 작을 때 (…하고있는 작업에 따라 다름),
object
/ mixed dtypes를 다룰 때str
/ regex 접근 자 함수를 사용하는 경우
이러한 상황을 개별적으로 살펴 보겠습니다.
작은 데이터에 대한 반복 대 벡터화
Pandas 는 API 디자인에서 “컨벤션 오버 컨벤션” 접근 방식을 따릅니다 . 이는 동일한 API가 광범위한 데이터 및 사용 사례에 적합하다는 것을 의미합니다.
pandas 함수가 호출 될 때, 작동을 보장하기 위해 함수가 다음과 같은 사항을 내부적으로 처리해야합니다.
- 색인 / 축 정렬
- 혼합 데이터 유형 처리
- 누락 된 데이터 처리
거의 모든 함수는 이러한 것들을 다양한 범위로 처리해야하며 이는 오버 헤드를 제공합니다 . 숫자 함수 (예 :)의 경우 오버 헤드가 적고 Series.add
문자열 함수 (예 :)의 경우 더 두드러 Series.str.replace
집니다.
for
반면에 루프는 생각보다 빠릅니다. 더 좋은 점은 목록 생성을위한 최적화 된 반복 메커니즘이기 때문에 목록 이해 ( for
루프를 통해 목록을 생성하는 )가 훨씬 빠르다는 것입니다.
목록 이해는 패턴을 따릅니다.
[f(x) for x in seq]
seq
Pandas 시리즈 또는 DataFrame 열은 어디에 있습니까 ? 또는 여러 열에 대해 작업 할 때
[f(x, y) for x, y in zip(seq1, seq2)]
어디 seq1
및 seq2
열입니다.
숫자 비교
간단한 부울 인덱싱 작업을 고려하십시오. 목록 이해 방법은 Series.ne
( !=
) 및 query
. 기능은 다음과 같습니다.
# Boolean indexing with Numeric value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
단순화 perfplot
를 위해이 게시물의 모든 timeit 테스트를 실행 하기 위해 패키지를 사용 했습니다. 위의 작업 시간은 다음과 같습니다.
목록 이해력 query
은 중간 크기의 N보다 성능이 뛰어나고 벡터화 된 같지 않음 작은 N에 대한 비교보다 성능이 뛰어납니다. 불행히도 목록 이해력은 선형 적으로 확장되므로 큰 N에 대한 성능 향상을 많이 제공하지 않습니다.
참고
목록 이해의 이점 중 상당 부분은 인덱스 정렬에 대해 걱정할 필요가 없다는 점에서 비롯되지만 이는 코드가 인덱싱 정렬에 의존하는 경우 중단된다는 것을 의미합니다. 어떤 경우에는 기본 NumPy 배열에 대한 벡터화 된 작업이 “양쪽 세계의 최고”를 가져 오는 것으로 간주 될 수 있으므로 팬더 함수의 불필요한 오버 헤드 없이 벡터화가 가능 합니다. 이것은 위의 작업을 다음과 같이 다시 작성할 수 있음을 의미합니다.df[df.A.values != df.B.values]
pandas와 list comprehension 등가물을 능가하는 것 :
NumPy 벡터화는이 게시물의 범위를 벗어 났지만 성능이 중요하다면 확실히 고려할 가치가 있습니다.
값 카운트
다른 예를 들면, 이번에 는 for 루프보다 빠른 또 다른 바닐라 파이썬 구조를 사용합니다 collections.Counter
. 일반적인 요구 사항은 값 개수를 계산하고 결과를 사전으로 반환하는 것입니다. 이 작업은 이루어집니다 value_counts
, np.unique
그리고 Counter
:
# Value Counts comparison.
ser.value_counts(sort=False).to_dict() # value_counts
dict(zip(*np.unique(ser, return_counts=True))) # np.unique
Counter(ser) # Counter
결과는 더 Counter
뚜렷하고 더 큰 범위의 작은 N (~ 3500)에 대해 두 벡터화 방법을 능가합니다.
더 많은 퀴즈를 참고하십시오 (예의 @ user2357112). 는Counter
로 구현 C 촉진제 가 빠르게보다는 여전히 파이썬 대신 기본 C 데이터 유형 객체 여전히 작동을 가져서 동시에for
루프. 파이썬 파워!
물론 여기서 빼면 성능은 데이터와 사용 사례에 따라 달라집니다. 이 예의 요점은 이러한 솔루션을 합법적 인 옵션으로 배제하지 않도록 설득하는 것입니다. 그래도 필요한 성능을 제공하지 못하는 경우 항상 cython 및 numba가 있습니다. 이 테스트를 믹스에 추가해 보겠습니다.
from numba import njit, prange
@njit(parallel=True)
def get_mask(x, y):
result = [False] * len(x)
for i in prange(len(x)):
result[i] = x[i] != y[i]
return np.array(result)
df[get_mask(df.A.values, df.B.values)] # numba
Numba는 루피 파이썬 코드의 JIT 컴파일을 매우 강력한 벡터 코드로 제공합니다. numba를 작동시키는 방법을 이해하려면 학습 곡선이 필요합니다.
혼합 / object
dtypes 작업
문자열 기반 비교
첫 번째 섹션의 필터링 예제를 다시 살펴보면 비교 중인 열이 문자열이면 어떻게됩니까? 위의 동일한 세 가지 함수를 고려하지만 입력 DataFrame은 문자열로 캐스트됩니다.
# Boolean indexing with string value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
그래서 무엇이 바뀌 었습니까? 여기서 주목할 점은 문자열 연산이 본질적으로 벡터화하기 어렵다는 것입니다. Pandas는 문자열을 객체로 취급하고 객체에 대한 모든 작업은 느리고 반복적 인 구현으로 돌아갑니다.
이제이 반복적 인 구현은 위에서 언급 한 모든 오버 헤드로 둘러싸여 있기 때문에 확장이 동일하더라도 이러한 솔루션 간에는 일정한 크기 차이가 있습니다.
변경 가능 / 복잡한 개체에 대한 작업에 관해서는 비교가 없습니다. 목록 이해는 사전 및 목록과 관련된 모든 작업을 능가합니다.
키로 사전 값 액세스
다음은 사전 열에서 값을 추출하는 두 가지 작업 map
, 즉 목록 이해에 대한 타이밍입니다 . 설정은 “코드 조각”제목 아래의 부록에 있습니다.
# Dictionary value extraction.
ser.map(operator.itemgetter('value')) # map
pd.Series([x.get('value') for x in ser]) # list comprehension
열 목록 (예외 처리) map
, str.get
접근 자 메서드 및 목록 이해력 에서 0 번째 요소를 추출하는 3 개의 작업에 대한 위치 목록 인덱싱 타이밍 :
# List positional indexing.
def get_0th(lst):
try:
return lst[0]
# Handle empty lists and NaNs gracefully.
except (IndexError, TypeError):
return np.nan
ser.map(get_0th) # map
ser.str[0] # str accessor
pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]) # list comp
pd.Series([get_0th(x) for x in ser]) # list comp safe
참고
색인이 중요한 경우 다음을 수행 할 수 있습니다.pd.Series([...], index=ser.index)
시리즈를 재구성 할 때.
목록 병합
마지막 예는 목록 병합 입니다. 이것은 또 다른 일반적인 문제이며 순수한 파이썬이 여기에 얼마나 강력한 지 보여줍니다.
# Nested list flattening.
pd.DataFrame(ser.tolist()).stack().reset_index(drop=True) # stack
pd.Series(list(chain.from_iterable(ser.tolist()))) # itertools.chain
pd.Series([y for x in ser for y in x]) # nested list comp
모두 itertools.chain.from_iterable
와 중첩 된 목록의 이해는 훨씬 더보다는 순수 파이썬 구조, 규모입니다 stack
솔루션입니다.
이러한 타이밍은 pandas가 혼합 dtypes와 함께 작동하도록 준비되어 있지 않다는 사실을 보여주는 강력한 표시이며 그렇게하는 데 사용하지 않아야 할 것입니다. 가능한 경우 데이터는 별도의 열에 스칼라 값 (ints / floats / strings)으로 표시되어야합니다.
마지막으로 이러한 솔루션의 적용 가능성은 데이터에 따라 크게 달라집니다. 따라서 가장 좋은 방법은 무엇을 사용할지 결정하기 전에 데이터에서 이러한 작업을 테스트하는 것입니다. apply
그래프를 왜곡 할 수 있기 때문에 이러한 솔루션에 대해 시간을 설정 하지 않았는지 확인 하십시오 (예, 그렇게 느립니다).
정규식 작업 및 접근 자 .str
방법
팬더는 다음과 같은 정규식 작업을 적용 할 수 있습니다 str.contains
, str.extract
그리고 str.extractall
뿐만 아니라 (같은 다른 “벡터화”문자열 연산 str.split
str.find, ,
문자열 컬럼에 등 str.translate`). 이러한 함수는 목록 이해보다 느리며 다른 어떤 것보다 더 편리한 함수입니다.
일반적으로 정규식 패턴을 사전 컴파일하고 데이터를 반복하는 것이 훨씬 빠릅니다 re.compile
(또한 Python의 re.compile을 사용할 가치가 있습니까? 참조 ). 에 해당하는 목록 구성 str.contains
은 다음과 같습니다.
p = re.compile(...)
ser2 = pd.Series([x for x in ser if p.search(x)])
또는,
ser2 = ser[[bool(p.search(x)) for x in ser]]
NaN을 처리해야하는 경우 다음과 같이 할 수 있습니다.
ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
str.extract
(그룹 없음)에 해당하는 목록 구성 은 다음과 같습니다.
df['col2'] = [p.search(x).group(0) for x in df['col']]
불일치 및 NaN을 처리해야하는 경우 사용자 지정 함수를 사용할 수 있습니다 (여전히 더 빠릅니다!).
def matcher(x):
m = p.search(str(x))
if m:
return m.group(0)
return np.nan
df['col2'] = [matcher(x) for x in df['col']]
이 matcher
기능은 매우 확장 가능합니다. 필요에 따라 각 캡처 그룹에 대한 목록을 반환하도록 조정할 수 있습니다. matcher 개체 의 group
또는 groups
특성 쿼리를 추출하기 만하면 됩니다.
의 경우 str.extractall
로 변경 p.search
합니다 p.findall
.
문자열 추출
간단한 필터링 작업을 고려하십시오. 아이디어는 대문자가 앞에 오는 경우 4 자리 숫자를 추출하는 것입니다.
# Extracting strings.
p = re.compile(r'(?<=[A-Z])(\d{4})')
def matcher(x):
m = p.search(x)
if m:
return m.group(0)
return np.nan
ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False) # str.extract
pd.Series([matcher(x) for x in ser]) # list comprehension
추가 예
전체 공개-본인은 아래 나열된 게시물의 일부 또는 전체의 작성자입니다.
결론
위의 예에서 볼 수 있듯이 반복은 작은 행의 DataFrame, 혼합 데이터 유형 및 정규식으로 작업 할 때 빛납니다.
속도 향상은 데이터와 문제에 따라 다르므로 마일리지가 다를 수 있습니다. 가장 좋은 방법은 신중하게 테스트를 실행하고 지불금이 노력할만한 가치가 있는지 확인하는 것입니다.
“벡터화 된”함수는 단순성과 가독성에서 빛을 발합니다. 따라서 성능이 중요하지 않다면 확실히 선호해야합니다.
또 다른 참고 사항은 특정 문자열 작업이 NumPy 사용을 선호하는 제약 조건을 처리한다는 것입니다. 다음은 신중한 NumPy 벡터화가 Python을 능가하는 두 가지 예입니다.
또한 때로는 .values
시리즈 또는 데이터 프레임과 달리 기본 어레이에서 작동하는 것만으로도 대부분의 일반적인 시나리오에 대해 충분한 속도 향상을 제공 할 수 있습니다 (위 의 숫자 비교 섹션 의 참고 사항 참조 ). 따라서, 예를 들어 이상 인스턴트 성능 부스트를 보여줄 것입니다 . 모든 상황에서 사용 이 적절하지는 않지만 알아두면 유용한 해킹입니다.df[df.A.values != df.B.values]
df[df.A != df.B]
.values
위에서 언급했듯이 이러한 솔루션을 구현할 가치가 있는지 여부를 결정하는 것은 귀하에게 달려 있습니다.
부록 : 코드 조각
import perfplot
import operator
import pandas as pd
import numpy as np
import re
from collections import Counter
from itertools import chain
# Boolean indexing with Numeric value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B']),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
lambda df: df[get_mask(df.A.values, df.B.values)]
],
labels=['vectorized !=', 'query (numexpr)', 'list comp', 'numba'],
n_range=[2**k for k in range(0, 15)],
xlabel='N'
)
# Value Counts comparison.
perfplot.show(
setup=lambda n: pd.Series(np.random.choice(1000, n)),
kernels=[
lambda ser: ser.value_counts(sort=False).to_dict(),
lambda ser: dict(zip(*np.unique(ser, return_counts=True))),
lambda ser: Counter(ser),
],
labels=['value_counts', 'np.unique', 'Counter'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=lambda x, y: dict(x) == dict(y)
)
# Boolean indexing with string value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B'], dtype=str),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
],
labels=['vectorized !=', 'query (numexpr)', 'list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Dictionary value extraction.
ser1 = pd.Series([{'key': 'abc', 'value': 123}, {'key': 'xyz', 'value': 456}])
perfplot.show(
setup=lambda n: pd.concat([ser1] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(operator.itemgetter('value')),
lambda ser: pd.Series([x.get('value') for x in ser]),
],
labels=['map', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# List positional indexing.
ser2 = pd.Series([['a', 'b', 'c'], [1, 2], []])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(get_0th),
lambda ser: ser.str[0],
lambda ser: pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]),
lambda ser: pd.Series([get_0th(x) for x in ser]),
],
labels=['map', 'str accessor', 'list comprehension', 'list comp safe'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Nested list flattening.
ser3 = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: pd.DataFrame(ser.tolist()).stack().reset_index(drop=True),
lambda ser: pd.Series(list(chain.from_iterable(ser.tolist()))),
lambda ser: pd.Series([y for x in ser for y in x]),
],
labels=['stack', 'itertools.chain', 'nested list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Extracting strings.
ser4 = pd.Series(['foo xyz', 'test A1234', 'D3345 xtz'])
perfplot.show(
setup=lambda n: pd.concat([ser4] * n, ignore_index=True),
kernels=[
lambda ser: ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False),
lambda ser: pd.Series([matcher(x) for x in ser])
],
labels=['str.extract', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)