TLDR; नहीं, for
लूप कंबल "खराब" नहीं हैं, कम से कम, हमेशा नहीं। यह कहना शायद अधिक सटीक है कि कुछ सदिश किए गए ऑपरेशन पुनरावृति की तुलना में धीमे होते हैं , या यह कहते हुए कि पुनरावृत्ति कुछ सदिश परिचालनों की तुलना में तेज़ है। यह जानना कि आपके कोड से सबसे अधिक प्रदर्शन कब और क्यों महत्वपूर्ण है। संक्षेप में, ये वे परिस्थितियाँ हैं जहाँ यह वेक्टरकृत पांडा कार्यों के विकल्प पर विचार करने के लायक है:
- जब आपका डेटा छोटा हो (... जो आप कर रहे हैं उसके आधार पर),
- जब
object
मिश्रित dtypes के साथ काम कर रहे हो
str
/ Regex accessor फ़ंक्शन का उपयोग करते समय
आइए इन स्थितियों की व्यक्तिगत रूप से जांच करें।
लघु डेटा पर विचलन v / s वेक्टरकरण
पंडों ने अपने एपीआई डिजाइन में "कन्वेंशन ओवर कॉन्फ़िगरेशन" दृष्टिकोण का अनुसरण किया । इसका मतलब है कि डेटा और उपयोग के मामलों की एक विस्तृत श्रृंखला को पूरा करने के लिए एक ही एपीआई को फिट किया गया है।
जब एक पंडों के समारोह को बुलाया जाता है, तो काम को सुनिश्चित करने के लिए निम्नलिखित चीजों (दूसरों के बीच) को आंतरिक रूप से फ़ंक्शन द्वारा नियंत्रित किया जाना चाहिए
- सूचकांक / अक्ष संरेखण
- मिश्रित डेटाटिप्स को संभालना
- गुम डाटा को संभालना
लगभग हर फंक्शन के लिए अलग-अलग तरीके से इनसे निपटना होगा, और यह एक उपरि प्रस्तुत करता है । ओवरहेड संख्यात्मक कार्यों (उदाहरण के लिए Series.add
) के लिए कम है , जबकि यह स्ट्रिंग फ़ंक्शन (उदाहरण के लिए Series.str.replace
) के लिए अधिक स्पष्ट है ।
for
दूसरी ओर, लूप तेजी से होते हैं, फिर आप सोचते हैं। क्या बेहतर है सूची समझ (जो for
छोरों के माध्यम से सूची बनाते हैं) और भी तेज हैं क्योंकि वे सूची निर्माण के लिए अनुकूलित तंत्र हैं।
सूची समझ पैटर्न का पालन करें
[f(x) for x in seq]
जहां seq
एक पांडा श्रृंखला या डेटाफ़्रेम कॉलम है। या, जब कई स्तंभों पर काम चल रहा हो,
[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
इस पोस्ट में सभी समय परीक्षण को चलाने के लिए पैकेज का उपयोग किया है । ऊपर के संचालन के लिए समय नीचे हैं:
query
मध्यम आकार के एन के लिए सूची समझ से बाहर प्रदर्शन , और यहां तक कि बेहतर प्रदर्शन वेक्टर बेहतर छोटे एन के लिए तुलना नहीं करता है। दुर्भाग्य से, सूची व्यापक रूप से स्केल समझती है, इसलिए यह बड़े एन के लिए अधिक प्रदर्शन लाभ प्रदान नहीं करता है।
नोट
यह ध्यान देने योग्य है कि सूची समझ का बहुत अधिक लाभ सूचकांक संरेखण के बारे में चिंता न करने से आता है, लेकिन इसका मतलब है कि यदि आपका कोड अनुक्रमण संरेखण पर निर्भर है, तो यह टूट जाएगा। कुछ मामलों में, अंतर्निहित NumPy सरणियों पर वेक्टर किए गए संचालन को "दोनों दुनिया के सर्वश्रेष्ठ" में लाने के रूप में माना जा सकता है, पांडा के सभी अनावश्यक कार्यों के बिना वेक्टरकरण की अनुमति देता है । इसका मतलब है कि आप ऊपर दिए गए ऑपरेशन को फिर से लिख सकते हैं
df[df.A.values != df.B.values]
पंडों और सूची समझ दोनों समकक्षों में से कौन सा बेहतर है:
NumPy वैश्वीकरण इस पद के दायरे से बाहर है, लेकिन यह निश्चित रूप से विचार करने योग्य है, यदि प्रदर्शन मायने रखता है।
मान मायने
रखता है एक और उदाहरण लेना - इस बार, एक और वेनिला अजगर निर्माण के साथ जो लूप के लिए तेजी से है - 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
छोटे एन (~ 3500) की एक बड़ी रेंज के लिए दोनों सदिश तरीकों पर जीतता है।
नोट
अधिक सामान्य ज्ञान (सौजन्य @ user2357112) Counter
एक साथ लागू किया गया है सी त्वरक , यह अभी भी काम करना पड़ता है इसलिए जब अजगर अंतर्निहित सी डेटाटाइप्स के बजाय वस्तुओं के साथ, यह अभी भी तेजी से एक से है 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
नंबा बहुत शक्तिशाली वेक्टर कोड के लिए लूप पीथॉन कोड का जेआईटी संकलन प्रदान करता है। सुंबा काम बनाने के तरीके को समझना एक सीखने की अवस्था है।
मिश्रित / object
dtypes के साथ संचालन
स्ट्रिंग-आधारित तुलना
पहले खंड से फ़िल्टरिंग उदाहरण को फिर से देखना, क्या होगा यदि स्तंभों की तुलना की जा रही है तार? ऊपर दिए गए 3 कार्यों पर विचार करें, लेकिन इनपुट के साथ डेटाफ्रेम को स्ट्रिंग में डाला जाए।
# 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
तो, क्या बदला? यहां ध्यान देने वाली बात यह है कि स्ट्रिंग ऑपरेशन को वेक्टर करना मुश्किल है। पंडाल वस्तुओं के रूप में तारों का व्यवहार करते हैं, और वस्तुओं पर सभी संचालन एक धीमी, ढलान पर लागू होते हैं।
अब, क्योंकि यह ढलान कार्यान्वयन उपर्युक्त सभी ओवरहेड से घिरा हुआ है, इन समाधानों के बीच एक निरंतर परिमाण अंतर है, भले ही वे समान पैमाने पर हों।
जब यह परिवर्तनशील / जटिल वस्तुओं के संचालन की बात आती है, तो कोई तुलना नहीं है। सूची समझ से बाहर सभी कार्यों dicts और सूचियों को शामिल।
कुंजी द्वारा डिक्शनरी वैल्यू एक्सेस करना
यहाँ दो ऑपरेशंस के लिए टाइमिंग है जो शब्दकोशों के एक कॉलम से एक वैल्यू निकालते हैं: map
और लिस्ट कॉम्प्रिहेंशन। सेटअप "कोड स्निपेट्स" शीर्षक के तहत परिशिष्ट में है।
# Dictionary value extraction.
ser.map(operator.itemgetter('value')) # map
pd.Series([x.get('value') for x in ser]) # list comprehension
3 संचालन के लिए स्थितीय सूची अनुक्रमण टाइमिंग जो स्तंभों की सूची (हैंडलिंग अपवाद) map
, str.get
एक्सेसर विधि और सूची समझ से 0 वें तत्व को निकालती है :
# 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
सॉल्यूशन की तुलना में बहुत बेहतर हैं।
ये समय इस तथ्य का एक मजबूत संकेत है कि पांडा मिश्रित dtypes के साथ काम करने के लिए सुसज्जित नहीं है, और आपको ऐसा करने के लिए उपयोग करने से शायद बचना चाहिए। जहाँ भी संभव हो, डेटा को अलग-अलग कॉलम में अदिश मान (इन्टस / फ्लोट्स / स्ट्रिंग्स) के रूप में मौजूद होना चाहिए।
अंत में, इन समाधानों की प्रयोज्यता आपके डेटा पर व्यापक रूप से निर्भर करती है। तो, सबसे अच्छी बात यह है कि इन आंकड़ों का परीक्षण करने से पहले यह तय कर लें कि क्या करना है। ध्यान दें कि मैंने apply
इन समाधानों पर कैसे समय नहीं दिया है , क्योंकि यह ग्राफ को तिरछा करेगा (हाँ, यह धीमा है)।
रेगेक्स ऑपरेशंस, और एक्सेसर .str
मेथड्स
पांडा जैसे regex संचालन आवेदन कर सकते हैं str.contains
, str.extract
और str.extractall
साथ ही अन्य "vectorized" स्ट्रिंग आपरेशन (जैसे, str.split
, str.find ,
str.translate`, और इसी तरह) स्ट्रिंग स्तंभों पर। ये कार्य सूची समझ की तुलना में धीमी हैं, और किसी भी चीज़ की तुलना में अधिक सुविधा वाले कार्य हैं।
यह आमतौर पर एक रेगाक्स पैटर्न को प्री-कंपाइल करने और आपके डेटा पर पुनरावृति करने के लिए बहुत तेज़ है re.compile
(यह भी देखें कि क्या यह पायथन के 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]]
यदि आपको NaNs को संभालने की आवश्यकता है, तो आप कुछ ऐसा कर सकते हैं
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.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
अधिक उदाहरण
पूर्ण प्रकटीकरण - मैं नीचे सूचीबद्ध इन पदों का लेखक (आंशिक या संपूर्ण) हूं।
निष्कर्ष
जैसा कि ऊपर दिए गए उदाहरणों से दिखाया गया है, डेटाफ़्रेम, मिश्रित डेटाैटिप्स और नियमित अभिव्यक्तियों की छोटी पंक्तियों के साथ काम करते समय चलना चमकता है।
आपको मिलने वाला स्पीडअप आपके डेटा और आपकी समस्या पर निर्भर करता है, इसलिए आपका माइलेज अलग-अलग हो सकता है। सबसे अच्छी बात यह है कि सावधानीपूर्वक परीक्षण चलाएं और देखें कि क्या पेआउट प्रयास के लायक है।
"वेक्टराइज्ड" फ़ंक्शन उनकी सादगी और पठनीयता में चमकते हैं, इसलिए यदि प्रदर्शन महत्वपूर्ण नहीं है, तो आपको निश्चित रूप से उन लोगों को पसंद करना चाहिए।
एक अन्य पक्ष ध्यान दें, कुछ स्ट्रिंग ऑपरेशन, उन बाधाओं से निपटते हैं, जो न्यूमपी के उपयोग के पक्ष में हैं। यहाँ दो उदाहरण हैं जहाँ सावधान NumPy वैश्वीकरण outperforms 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
)
pd.Series
औरpd.DataFrame
अब पुनरावृत्तियों से निर्माण का समर्थन करें। इसका मतलब है कि कोई व्यक्ति पहले सूची का निर्माण करने की आवश्यकता के बजाय निर्माण कार्यों के लिए एक पायथन जनरेटर को पास कर सकता है (सूची समझ का उपयोग करके), जो कई मामलों में धीमा हो सकता है। हालाँकि, जनरेटर आउटपुट का आकार पहले से निर्धारित नहीं किया जा सकता है। मुझे यकीन नहीं है कि कितना समय / मेमोरी ओवरहेड का कारण होगा।