apply
आप कभी जरूरत नहीं है, सुविधा समारोह
हम ओपी में प्रश्नों को एक-एक करके संबोधित करते हुए शुरू करते हैं।
" अगर आवेदन इतना बुरा है, तो यह एपीआई में क्यों है? "
DataFrame.apply
और Series.apply
कर रहे हैं सुविधा कार्यों DataFrame और सीरीज पर परिभाषित क्रमश आपत्ति है। apply
किसी भी उपयोगकर्ता परिभाषित फ़ंक्शन को स्वीकार करता है जो किसी DataFrame पर परिवर्तन / एकत्रीकरण लागू करता है। apply
प्रभावी रूप से एक चांदी की गोली है जो किसी भी मौजूदा पांडा कार्य नहीं कर सकती है।
कुछ चीजें apply
कर सकते हैं:
- DataFrame या Series पर कोई भी उपयोगकर्ता-परिभाषित फ़ंक्शन चलाएँ
- डेटाफ़्रेम पर किसी फ़ंक्शन को पंक्ति-वार (
axis=1
) या स्तंभ-वार ( axis=0
) लागू करें
- फ़ंक्शन को लागू करते समय सूचकांक संरेखण करें
- उपयोगकर्ता-परिभाषित कार्यों के साथ एकत्रीकरण करें (हालांकि, हम आमतौर पर पसंद करते हैं
agg
या transform
इन मामलों में)
- तत्व-वार रूपांतरण करें
- मूल पंक्तियों को समेकित परिणाम प्रसारित करें (
result_type
तर्क देखें )।
- उपयोगकर्ता द्वारा परिभाषित कार्यों को पारित करने के लिए स्थितीय / खोजशब्द तर्क स्वीकार करें।
...दूसरों के बीच में। अधिक जानकारी के लिए, दस्तावेज़ में पंक्ति या स्तंभ-वार फ़ंक्शन अनुप्रयोग देखें ।
तो, इन सभी विशेषताओं के साथ, apply
बुरा क्यों है ? यह है क्योंकि apply
है धीमी गति से । पंडों आपके फ़ंक्शन की प्रकृति के बारे में कोई धारणा नहीं बनाते हैं, और इसलिए पुनरावृत्ति आवश्यक रूप से प्रत्येक पंक्ति / स्तंभ पर आपके फ़ंक्शन को लागू करती है। इसके अतिरिक्त, उपरोक्त सभी स्थितियों को संभालने का अर्थ है कि apply
प्रत्येक पुनरावृत्ति में कुछ प्रमुख ओवरहेड हो। इसके अलावा, apply
बहुत अधिक मेमोरी का उपभोग करता है, जो मेमोरी बाउंड एप्लिकेशन के लिए एक चुनौती है।
ऐसी बहुत कम परिस्थितियाँ हैं, जिनका apply
उपयोग करना उचित है (उस नीचे अधिक)। यदि आप सुनिश्चित नहीं हैं कि आप का उपयोग किया जाना चाहिए apply
, तो आपको शायद नहीं करना चाहिए।
आइए अगले प्रश्न पर ध्यान दें।
" मुझे अपना कोड कब और कैसे लागू करना चाहिए ? "
रीफ़्रेज़ करने के लिए, यहाँ कुछ सामान्य स्थितियाँ हैं जहाँ आप किसी भी कॉल से छुटकारा पाना चाहेंगे apply
।
संख्यात्मक डेटा
यदि आप संख्यात्मक डेटा के साथ काम कर रहे हैं, तो संभवत: पहले से ही एक वेक्टराइंट साइथन फ़ंक्शन है, जो वास्तव में आप क्या करने की कोशिश कर रहे हैं (यदि नहीं, तो कृपया स्टैक ओवरफ्लो पर एक प्रश्न पूछें या गिटहब पर एक फीचर अनुरोध खोलें)।
apply
एक साधारण जोड़ ऑपरेशन के लिए प्रदर्शन का विरोध करें ।
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df
A B
0 9 12
1 4 7
2 2 5
3 1 4
df.apply(np.sum)
A 16
B 28
dtype: int64
df.sum()
A 16
B 28
dtype: int64
प्रदर्शन के लिहाज से, इसकी कोई तुलना नहीं है, साइंटोनाइज्ड समकक्ष बहुत तेज है। एक ग्राफ की जरूरत नहीं है, क्योंकि अंतर खिलौना डेटा के लिए भी स्पष्ट है।
%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
यहां तक कि अगर आप raw
तर्क के साथ कच्चे सरणियों को पारित करने में सक्षम करते हैं, तो यह अभी भी दो बार धीमा है।
%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
एक और उदाहरण:
df.apply(lambda x: x.max() - x.min())
A 8
B 8
dtype: int64
df.max() - df.min()
A 8
B 8
dtype: int64
%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()
2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
सामान्य तौर पर, यदि संभव हो तो सदिश विकल्पों की तलाश करें।
स्ट्रिंग / Regex
पंडों ज्यादातर स्थितियों में "वेक्टराइज्ड" स्ट्रिंग कार्य प्रदान करते हैं, लेकिन ऐसे दुर्लभ मामले हैं जहां उन कार्यों को नहीं ... "लागू करें", इसलिए बोलने के लिए।
एक सामान्य समस्या यह जांचना है कि क्या स्तंभ में एक मान उसी पंक्ति के किसी अन्य स्तंभ में मौजूद है।
df = pd.DataFrame({
'Name': ['mickey', 'donald', 'minnie'],
'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
'Value': [20, 10, 86]})
df
Name Value Title
0 mickey 20 wonderland
1 donald 10 welcome to donald's castle
2 minnie 86 Minnie mouse clubhouse
यह पंक्ति दूसरी और तीसरी पंक्ति को वापस कर देना चाहिए, क्योंकि "डोनाल्ड" और "मिन्नी" अपने संबंधित "शीर्षक" कॉलम में मौजूद हैं।
प्रयोग का उपयोग करते हुए, यह प्रयोग किया जाएगा
df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)
0 False
1 True
2 True
dtype: bool
df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
हालाँकि, सूची बोध का उपयोग करके एक बेहतर समाधान मौजूद है।
df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
यहां ध्यान देने वाली बात यह है कि apply
ओवरहेड रूटीन लोअरहेड की वजह से तेज गति से होता है । यदि आपको NaNs और अमान्य dtypes को संभालने की आवश्यकता है, तो आप इस पर एक कस्टम फ़ंक्शन का उपयोग करके बना सकते हैं जिसे आप सूची समझ के अंदर तर्कों के साथ कॉल कर सकते हैं।
सूची बोध को कब एक अच्छा विकल्प माना जाना चाहिए, इस बारे में अधिक जानकारी के लिए, मेरा राइटअप देखें: फॉर लूप्स विद पांडा - आपको कब ध्यान देना चाहिए? ।
नोट
दिनांक और डेटाटाइम संचालन के भी सदिश संस्करण हैं। इसलिए, उदाहरण के लिए, आपको पसंद करना चाहिए pd.to_datetime(df['date'])
, ओवर, कहना df['date'].apply(pd.to_datetime)
,।
डॉक्स पर अधिक पढ़ें
।
एक आम ख़तरा: सूचियों के स्तंभों का विस्फोट
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
लोग उपयोग करने के लिए ललचाते हैं apply(pd.Series)
। प्रदर्शन के मामले में यह भयानक है।
s.apply(pd.Series)
0 1
0 1 2
1 1 2
2 1 2
एक बेहतर विकल्प कॉलम को सूचीबद्ध करना और इसे pd.DataFrame को पास करना है।
pd.DataFrame(s.tolist())
0 1
0 1 2
1 1 2
2 1 2
%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())
2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
अंततः,
" क्या कोई ऐसी स्थिति है जहाँ apply
अच्छा है? "
लागू करना एक सुविधा कार्य है, इसलिए ऐसी परिस्थितियां हैं जहां ओवरहेड को माफ करने के लिए नगण्य है। यह वास्तव में इस बात पर निर्भर करता है कि फ़ंक्शन को कितनी बार कहा जाता है।
श्रृंखला के लिए वेक्टर किए गए फ़ंक्शंस, लेकिन डेटाफ़्रेम नहीं
यदि आप कई स्तंभों पर एक स्ट्रिंग ऑपरेशन लागू करना चाहते हैं तो क्या होगा? यदि आप कई कॉलमों को डेटाइम में बदलना चाहते हैं तो क्या होगा? इन कार्यों को केवल श्रृंखला के लिए सदिश किया गया है, इसलिए उन्हें प्रत्येक कॉलम पर लागू किया जाना चाहिए जिसे आप कनवर्ट / संचालित करना चाहते हैं।
df = pd.DataFrame(
pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2),
columns=['date1', 'date2'])
df
date1 date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30
df.dtypes
date1 object
date2 object
dtype: object
यह एक स्वीकार्य मामला है apply
:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
ध्यान दें कि यह भी समझ में आता है stack
, या बस एक स्पष्ट लूप का उपयोग करेगा। इन सभी विकल्पों का उपयोग करने की तुलना में थोड़ा तेज है apply
, लेकिन अंतर माफ करने के लिए काफी छोटा है।
%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
आप अन्य ऑपरेशन जैसे स्ट्रिंग ऑपरेशन या श्रेणी में रूपांतरण के लिए एक समान मामला बना सकते हैं।
u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))
v / s
u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
v[c] = df[c].astype(category)
और इसी तरह...
सीरीज़िंग को str
: astype
बनामapply
ऐसा लगता है जैसे एपीआई का एक idiosyncrasy। apply
किसी श्रृंखला में पूर्णांकों को स्ट्रिंग में बदलने के लिए उपयोग करने की तुलना में (और कभी-कभी तेज) तुलनीय है astype
।
perfplot
पुस्तकालय
का उपयोग करके ग्राफ तैयार किया गया था ।
import perfplot
perfplot.show(
setup=lambda n: pd.Series(np.random.randint(0, n, n)),
kernels=[
lambda s: s.astype(str),
lambda s: s.apply(str)
],
labels=['astype', 'apply'],
n_range=[2**k for k in range(1, 20)],
xlabel='N',
logx=True,
logy=True,
equality_check=lambda x, y: (x == y).all())
झांकियों के साथ, मैं देख रहा हूं कि astype
लगातार उतना ही तेज है, या इससे थोड़ा तेज है apply
। तो यह इस तथ्य के साथ करना है कि परीक्षण में डेटा पूर्णांक प्रकार है।
GroupBy
जंजीर परिवर्तन के साथ संचालन
GroupBy.apply
अब तक चर्चा नहीं की गई है, लेकिन GroupBy.apply
मौजूदा GroupBy
कार्यों के लिए कुछ भी संभालने के लिए एक पुनरावृत्ति सुविधा समारोह भी है ।
एक सामान्य आवश्यकता है एक GroupBy और फिर दो प्रमुख ऑपरेशन जैसे "लैग्ड कम्सम" करना:
df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df
A B
0 a 12
1 a 7
2 b 5
3 c 4
4 c 5
5 c 4
6 d 3
7 d 2
8 e 1
9 e 10
आपको यहाँ दो क्रमिक कॉल की आवश्यकता होगी:
df.groupby('A').B.cumsum().groupby(df.A).shift()
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
का उपयोग करते हुए apply
, आप इसे एक एकल कॉल के लिए छोटा कर सकते हैं।
df.groupby('A').B.apply(lambda x: x.cumsum().shift())
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
प्रदर्शन को निर्धारित करना बहुत कठिन है क्योंकि यह डेटा पर निर्भर करता है। लेकिन सामान्य तौर पर, apply
एक स्वीकार्य समाधान है यदि लक्ष्य एक groupby
कॉल को कम करना है (क्योंकि groupby
यह काफी महंगा भी है)।
अन्य कैवियट
ऊपर उल्लिखित केवतों के अलावा, यह भी ध्यान देने योग्य है कि apply
पहली पंक्ति (या स्तंभ) पर दो बार संचालित होता है। यह निर्धारित करने के लिए किया जाता है कि फ़ंक्शन का कोई दुष्प्रभाव है या नहीं। यदि नहीं, apply
तो परिणाम के मूल्यांकन के लिए फास्ट-पथ का उपयोग करने में सक्षम हो सकता है, अन्यथा यह धीमी गति से कार्यान्वयन के लिए वापस आता है।
df = pd.DataFrame({
'A': [1, 2],
'B': ['x', 'y']
})
def func(x):
print(x['A'])
return x
df.apply(func, axis=1)
# 1
# 1
# 2
A B
0 1 x
1 2 y
यह व्यवहार GroupBy.apply
पांडा के संस्करणों पर भी देखा जाता है <0.25 (यह 0.25 के लिए तय किया गया था, अधिक जानकारी के लिए यहां देखें )
returns.add(1).apply(np.log)
बनामnp.log(returns.add(1)
एक ऐसा मामला है जहांapply
आम तौर पर थोड़ी तेजी होगी, जो कि नीचे jpp के आरेख में नीचे दाईं ओर हरे रंग का बॉक्स है।