python - मुझे अपने कोड में कभी भी पांडा के उपयोग() का उपयोग कब करना चाहिए?




pandas performance (3)

सभी apply जैसे नहीं हैं

नीचे दिए गए चार्ट से पता चलता है कि कब apply 1 पर विचार apply । हरे रंग का मतलब संभवतः कुशल है; लाल बचना।

इसमें से कुछ सहज है: pd.Series.apply एक पायथन-स्तरीय पंक्ति-वार लूप, ditto pd.DataFrame.apply पंक्ति-वार ( axis=1 ) है। इनका दुरुपयोग कई और व्यापक हैं। दूसरी पोस्ट उनके साथ और अधिक गहराई से पेश आती है। लोकप्रिय समाधान वेक्टर किए गए तरीकों, सूची समझ (स्वच्छ डेटा को मानता है), या pd.DataFrame निर्माणकर्ता (जैसे apply(pd.Series) करने से बचने के apply(pd.Series) जैसे कुशल उपकरण हैं।

यदि आप pd.DataFrame.apply पंक्ति-वार का उपयोग कर रहे हैं, तो raw=True (जहाँ संभव हो) निर्दिष्ट करना अक्सर फायदेमंद होता है। इस स्तर पर, numba आमतौर पर एक बेहतर विकल्प है।

GroupBy.apply : आम तौर पर इष्ट

apply करने से बचने के apply संचालन को दोहराने से प्रदर्शन को नुकसान होगा। GroupBy.apply आमतौर पर यहाँ ठीक है, बशर्ते आपके कस्टम फ़ंक्शन में आपके द्वारा उपयोग की जाने वाली विधियाँ स्वयं वेक्टरकृत हों। कभी-कभी आपके द्वारा लागू किए जाने वाले समूहबद्ध एकत्रीकरण के लिए कोई देशी पांडास विधि नहीं होती है। इस मामले में, कस्टम फ़ंक्शन के साथ कम संख्या में समूह apply हैं, फिर भी उचित प्रदर्शन की पेशकश कर सकते हैं।

pd.DataFrame.apply कॉलम-वार: एक मिश्रित बैग

pd.DataFrame.apply कॉलम-वार ( axis=0 ) एक दिलचस्प मामला है। बड़ी संख्या में स्तंभों की एक छोटी संख्या के लिए, यह लगभग हमेशा महंगा होता है। स्तंभों के सापेक्ष बड़ी संख्या में, अधिक सामान्य मामला, आप कभी-कभी apply महत्वपूर्ण प्रदर्शन सुधार देख apply :

# Python 3.7, Pandas 0.23.4
np.random.seed(0)
df = pd.DataFrame(np.random.random((10**7, 3)))     # Scenario_1, many rows
df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns

                                               # Scenario_1  | Scenario_2
%timeit df.sum()                               # 800 ms      | 109 ms
%timeit df.apply(pd.Series.sum)                # 568 ms      | 325 ms

%timeit df.max() - df.min()                    # 1.63 s      | 314 ms
%timeit df.apply(lambda x: x.max() - x.min())  # 838 ms      | 473 ms

%timeit df.mean()                              # 108 ms      | 94.4 ms
%timeit df.apply(pd.Series.mean)               # 276 ms      | 233 ms

1 अपवाद हैं, लेकिन ये आमतौर पर सीमांत या असामान्य हैं। कुछ उदाहरण:

  1. df['col'].apply(str) थोड़ा outperform df['col'].astype(str)
  2. df.apply(pd.to_datetime) स्ट्रिंग्स पर काम for लूप के for नियमित रूप से पंक्तियों के साथ अच्छी तरह से पैमाने पर नहीं होता है।

यह एक स्व-उत्तरित QnA है जिसका अर्थ उपयोगकर्ताओं को उपयोग करने के नुकसान और लाभों के बारे में निर्देश देना है।

मैंने स्टैक ओवरफ्लो पर प्रश्नों के कई उत्तर पोस्ट किए हैं, जिसमें आवेदन का उपयोग शामिल है। मैंने उपयोगकर्ताओं को यह कहते हुए टिप्पणी करते देखा है कि " apply धीमा है", और इससे बचना चाहिए "।

मैंने प्रदर्शन के विषय पर कई लेख पढ़े हैं जो बताते हैं कि apply करना धीमा है। मैंने डॉक्स में एक अस्वीकरण भी देखा है कि कैसे यूडीएफ को पारित apply लिए बस एक सुविधा फ़ंक्शन है (अब यह पता नहीं लग सकता है)। इसलिए, आम सहमति यह है कि यदि संभव हो तो apply को टाला जाना चाहिए। हालाँकि, यह निम्नलिखित प्रश्न उठाता है:

  1. यदि apply इतना बुरा है, तो यह एपीआई में क्यों है?
  2. मुझे अपना कोड कैसे और कब apply करना चाहिए?
  3. क्या कभी भी ऐसी परिस्थितियाँ हैं जहाँ apply होना अच्छा है (अन्य संभावित समाधानों से बेहतर)?

apply , सुविधा फ़ंक्शन जिसे आपको कभी ज़रूरत नहीं थी

हम ओपी में प्रश्नों को एक-एक करके संबोधित करते हुए शुरू करते हैं।

" यदि आवेदन इतना बुरा है, तो यह एपीआई में क्यों है? "

DataFrame.apply और Series.apply क्रमशः DataFrame और Series ऑब्जेक्ट पर परिभाषित सुविधा फ़ंक्शन हैं। किसी भी उपयोगकर्ता परिभाषित फ़ंक्शन को लागू करता है जो किसी DataFrame पर परिवर्तन / एकत्रीकरण को apply करता है। 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) पसंद करना चाहिए df['date'].apply(pd.to_datetime)

docs पर अधिक पढ़ें।

एक सामान्य ख़तरा: सूचियों के स्तंभों का विस्फोट

s = pd.Series([[1, 2]] * 3)
s

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object

लोग apply(pd.Series) का उपयोग करने के लिए 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 : 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

एप्लिकेशन का उपयोग करके, आप इसे एक सिंगल कॉल पर छोटा कर सकते हैं।

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 के लिए तय किया गया था, अधिक जानकारी के लिए यहां देखें ।)


axis=1 (यानी पंक्ति-वार फ़ंक्शंस) तो आप केवल apply बदले में निम्न फ़ंक्शन का उपयोग कर सकते हैं। मुझे आश्चर्य है कि यह pandas व्यवहार क्यों नहीं है। (यौगिक अनुक्रमित के साथ अप्रकाशित, लेकिन यह apply तुलना में बहुत तेज़ प्रतीत apply )

def faster_df_apply(df, func):
    cols = list(df.columns)
    data, index = [], []
    for row in df.itertuples(index=True):
        row_dict = {f:v for f,v in zip(cols, row[1:])}
        data.append(func(row_dict))
        index.append(row[0])
    return pd.Series(data, index=index)





apply