programing

판다의 루프를 위한 것이 정말 나쁜가요?언제가 좋을까요?

bestprogram 2023. 7. 16. 13:43

판다의 루프를 위한 것이 정말 나쁜가요?언제가 좋을까요?

for루프가 정말 "나쁘다"?그렇지 않다면 어떤 상황에서 기존의 "벡터화된"1 접근 방식을 사용하는 것보다 더 나을까요?

저는 "벡터화"의 개념과 판다가 어떻게 벡터화된 기술을 사용하여 계산 속도를 높이는지 잘 알고 있습니다.벡터화된 기능은 전체 시리즈 또는 DataFrame에 걸쳐 작업을 브로드캐스트하여 기존의 데이터 반복보다 훨씬 빠른 속도를 달성합니다.

(Stack Overflow에 ▁using▁however▁(스▁that▁solutions)를 사용하여 데이터를 순환하는 문제에 대한 을 제공하는 놀랐습니다.for루프 및 목록 이해.설명서와 API에 따르면 루프는 "나쁘다"며 어레이, 시리즈 또는 DataFrames를 "절대" 반복해서는 안 됩니다.그렇다면 루프 기반 솔루션을 제안하는 사용자를 가끔 볼 수 있는 이유는 무엇입니까?


1 - 질문이 다소 광범위하게 들리는 것은 사실이지만, 진실은 다음과 같은 매우 구체적인 상황이 있다는 것입니다.for루프는 일반적으로 데이터를 반복하는 것보다 낫습니다.이 게시물은 후세를 위해 이를 담아내는 것을 목표로 합니다.

TLDR; 아니요,for루프가 포괄적으로 "나쁜" 것은 아닙니다. 적어도 항상 그런 것은 아닙니다.일부 벡터화된 연산은 반복하는 것보다 느리다고 말하는 것이 더 정확할 것입니다. 반면에 일부 벡터화된 연산은 반복하는 것보다 빠르다고 말하는 것이 더 정확할 것입니다.코드의 성능을 최대화하기 위해서는 시기와 이유를 파악하는 것이 중요합니다.간단히 말해서, 벡터화된 판다 기능의 대안을 고려할 가치가 있는 상황은 다음과 같습니다.

  1. 데이터가 작을 때(...작업 내용에 따라 다름)
  2. 를 때object/ dtype/mixed dtype
  3. str functions/regex 파일

이러한 상황을 개별적으로 살펴보겠습니다.


작은 데이터에 대한 v/s 벡터화 반복

Pandas는 API 설계에서 "Convention Over Configuration" 방식을 따릅니다.이는 동일한 API가 광범위한 데이터 및 사용 사례에 맞게 적용되었음을 의미합니다.

판다 기능이 호출되면 다음 사항(특히)이 기능에 의해 내부적으로 처리되어야 작동합니다.

  1. 인덱스/축 정렬
  2. 혼합 데이터 유형 처리
  3. 결측 데이터 처리

거의 모든 기능이 다양한 범위에서 이러한 문제를 처리해야 하며, 이는 오버헤드를 나타냅니다.숫자 함수(예: )의 경우 오버헤드가 적은 반면 문자열 함수(예: )의 경우 오버헤드가 더 뚜렷합니다.

for 반면에 루프는 당신이 생각하는 것보다 빠릅니다.더 좋은 것은 목록 이해입니다(목록을 만드는 방법).for루프)는 목록 작성에 최적화된 반복 메커니즘이기 때문에 훨씬 더 빠릅니다.

목록 이해는 패턴을 따릅니다.

[f(x) for x in seq]

에▁where디seq는 팬더 시리즈 또는 DataFrame 열입니다.또는 여러 열에 걸쳐 작동할 때

[f(x, y) for x, y in zip(seq1, seq2)]

에▁where디seq1그리고.seq2열입니다.

비교
간단한 부울 인덱싱 작업을 고려합니다.목록 이해 방법은 다음에 대해 시간이 지정되었습니다.!=) 및 . 다음은 기능입니다.

# 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

간단히 말해, 저는 이 게시물에서 테스트하는 모든 시간 동안 패키지를 실행하는 데 사용했습니다.위의 작업 시간은 다음과 같습니다.

enter image description here

목록 이해력이 성능을 능가합니다.query중간 크기의 N의 경우, 작은 N의 경우 벡터화된 비교보다 성능이 우수합니다.불행히도 목록 이해는 선형적으로 확장되므로 더 큰 N에 대해 성능 향상을 제공하지 않습니다.

사항
목록 이해의 이점의 대부분은 인덱스 정렬에 대해 걱정할 필요가 없는 데서 나온다는 점을 언급할 가치가 있지만, 이것은 코드가 인덱싱 정렬에 종속되어 있으면 이 기능이 손상된다는 것을 의미합니다.경우에 따라 기본 NumPy 배열에 대한 벡터화된 연산은 판다 기능의 불필요한 오버헤드 없이 벡터화를 가능하게 하는 "양쪽의 장점"을 가져오는 것으로 간주될 수 있습니다.즉, 위의 작업을 다음과 같이 다시 작성할 수 있습니다.

df[df.A.values != df.B.values]

이것은 판다와 목록 이해 동등한 것들을 모두 능가합니다.

NumPy 벡터화는 이 게시물의 범위에서 벗어나지만 성능이 중요하다면 고려할 가치가 있습니다.

수 »
다른 예를 들어, 이번에는 for 루프보다 빠른 또 다른 바닐라 파이썬 구조를 사용합니다. 일반적인 요구 사항은 값 카운트를 계산하고 결과를 사전으로 반환하는 것입니다.이 작업은 , , 및 를 사용하여 수행됩니다.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

enter image description here

가 더요, 결는더뚜다니합렷.Counter더 큰 범위의 작은 N(~3500)에 대해 두 벡터화된 방법을 모두 이깁니다.

사항
더 많은 사소한 일들 (courty @user2357112).CounterC 가속기를 사용하여 구현되므로 기본 C 데이터 유형 대신 Python 개체와 함께 작동해야 하지만 여전히 a보다 빠릅니다.for루프. 파이썬 파워!

물론 여기서 얻을 수 있는 이점은 성능이 데이터와 사용 사례에 따라 달라진다는 것입니다.이러한 예제의 요점은 이러한 솔루션을 합법적인 옵션으로 배제하지 않도록 설득하는 것입니다.그래도 필요한 성능을 제공하지 못하는 경우에는 항상 사이톤누바가 있습니다.이 테스트를 믹스에 추가해 보겠습니다.

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

enter image description here

Numba는 루프 파이썬 코드의 JIT 컴파일을 매우 강력한 벡터화된 코드로 제공합니다.numba를 작동시키는 방법을 이해하는 것은 학습 곡선을 포함합니다.


혼합혼합된 objectdtypesd

기반
첫 번째 섹션에서 필터링 예제를 다시 살펴봅니다. 비교할 열이 문자열이면 어떻게 됩니까?위의 3개 함수와 동일하지만 입력 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

enter image description here

그래서, 무엇이 변했을까요?여기서 주목할 점은 문자열 연산은 본질적으로 벡터화하기 어렵다는 것입니다.판다는 문자열을 객체로 취급하고 객체에 대한 모든 작업은 느리고 반복적인 구현으로 돌아갑니다.

이제, 이 엉터리 구현은 위에서 언급한 모든 오버헤드에 둘러싸여 있기 때문에, 동일한 규모로 확장되더라도 이러한 솔루션 간에는 일정한 차이가 있습니다.

가변/복잡한 개체에 대한 작업은 비교할 수 없습니다.목록 이해는 딕트 및 목록과 관련된 모든 작업을 능가합니다.

를 값
하는 두 입니다: 다은사열전에값추두는작시가다니간입업의지음출하을.map그리고 목록 이해력.설정은 부록의 "코드 스니펫" 제목 아래에 있습니다.

# Dictionary value extraction.
ser.map(operator.itemgetter('value'))     # map
pd.Series([x.get('value') for x in ser])  # list comprehension

enter image description here

목록
열 목록(처리 예외), , 접근법 및 목록 이해도에서 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)

영상 시리즈를 재구성할 때.

enter image description here

평탄화
마지막 예는 목록을 평평하게 만드는 것입니다.이것은 또 다른 일반적인 문제이며, 순수한 파이썬이 얼마나 강력한지를 보여줍니다.

# 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

enter image description here

와 중첩된 목록 이해는 모두 순수한 파이썬 구조이며, 보다 훨씬 더 잘 확장됩니다.stack해결책

이러한 타이밍은 판다가 혼합된 d형을 사용할 수 없다는 사실을 강력하게 보여주며, 이를 위해 사용하는 것을 자제해야 할 것입니다.가능한 경우 데이터는 별도의 열에 스칼라 값(ints/floats/string)으로 표시되어야 합니다.

마지막으로, 이러한 솔루션의 적용 가능성은 데이터에 크게 좌우됩니다.따라서 무엇을 사용할지 결정하기 전에 데이터에서 이러한 작업을 테스트하는 것이 가장 좋습니다.그래프가 왜곡될 수 있기 때문에 이러한 솔루션에 시간을 두지 않았습니다(네, 그렇게 느립니다).


및 Regex 파일.str

판다는 , , , 와 같은 정규식 연산뿐만 아니라 다른 "벡터화된" 문자열 연산(예:str.split,str.find,str.translate등)을(를) 문자열 열에 표시합니다.이러한 기능은 목록 이해보다 느리며, 다른 어떤 기능보다 편리한 기능입니다.

일반적으로 정규식 패턴을 사전 컴파일하고 데이터를 통해 반복하는 것이 훨씬 빠릅니다(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기능은 매우 확장 가능합니다.필요에 따라 각 캡처 그룹의 목록을 반환하도록 적합할 수 있습니다.쿼리를 추출합니다.group또는groups일치 개체의 특성입니다.

위해서str.extractall, 변경p.searchp.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

enter image description here

예제
전체 공개 - 저는 아래 나열된 게시물의 작성자입니다(일부 또는 전체).


결론

위의 예에서 볼 수 있듯이 데이터 프레임의 작은 행, 혼합 데이터 유형 및 정규식을 사용할 때 반복이 빛을 발합니다.

속도 향상은 데이터와 문제에 따라 달라지므로 주행 거리가 달라질 수 있습니다.가장 좋은 방법은 신중하게 테스트를 실행하고 지불액이 노력할 가치가 있는지 확인하는 것입니다.

"벡터화된" 기능은 단순성과 가독성에서 빛을 발하므로 성능이 중요하지 않다면 이러한 기능을 선호해야 합니다.

또 다른 참고로, 특정 문자열 작업은 NumPy의 사용을 선호하는 제약 조건을 다룹니다.다음은 신중한 NumPy 벡터화가 파이썬을 능가하는 두 가지 예입니다.

또한 때로는 다음을 통해 기본 어레이에서 운영됩니다..valuesSeries 또는 DataFrames와는 대조적으로 대부분의 일반적인 시나리오에 대해 충분한 속도 향상을 제공할 수 있습니다(위 수치 비교 섹션의 참고 참조).예를 들면,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
)

요컨대

  • + 루 + 용의 iterrows매우 느립니다.오버헤드는 최대 1k개 행에서는 크지 않지만 10k개 이상의 행에서는 두드러집니다.
  • + 루 + 용의 itertuples 훨씬빠다보다 훨씬 .iterrows또는apply.
  • 훨씬 빠릅니다.itertuples

벤치마크

언급URL : https://stackoverflow.com/questions/54028199/are-for-loops-in-pandas-really-bad-when-should-i-care