python पंडों के साथ तेजी से विराम चिह्न हटाने




regex string (3)

सुन्न का उपयोग करके हम अब तक पोस्ट किए गए सर्वोत्तम तरीकों से अधिक स्वस्थ गति प्राप्त कर सकते हैं। मूल रणनीति समान है --- एक बड़ी सुपर स्ट्रिंग बनाएं। लेकिन प्रसंस्करण बहुत तेज़ी से लगता है, शायद इसलिए क्योंकि हम बिना कुछ बदले प्रतिस्थापन की सादगी का पूरी तरह से फायदा उठाते हैं।

छोटी ( 0x110000 वर्णों से कम कुल) समस्याओं के लिए हम स्वचालित रूप से एक विभाजक पाते हैं, बड़ी समस्याओं के लिए हम एक धीमी विधि का उपयोग करते हैं जो str.split पर निर्भर नहीं करता है।

ध्यान दें कि मैंने सभी प्रीकंप्यूटेबल्स को फंक्शन्स से बाहर कर दिया है। यह भी ध्यान दें, कि translate और pd_translate को तीन सबसे बड़ी समस्याओं के लिए एकमात्र संभव विभाजक मुफ्त में पता np_multi_strat है जबकि np_multi_strat को इसकी गणना np_multi_strat है या विभाजक-कम रणनीति पर वापस आना पड़ता है। और अंत में, ध्यान दें कि पिछले तीन डेटा बिंदुओं के लिए मैं अधिक "दिलचस्प" समस्या पर स्विच करता हूं; pd_replace और re_sub क्योंकि वे अन्य तरीकों के बराबर नहीं हैं, इसके लिए उन्हें बाहर करना पड़ा।

एल्गोरिथ्म पर:

मूल रणनीति वास्तव में काफी सरल है। केवल 0x110000 विभिन्न यूनिकोड वर्ण हैं। जैसे ही ओपी विशाल डेटा सेट के संदर्भ में चुनौती देता है, यह पूरी तरह से एक लुकअप टेबल बनाता है जिसमें कैरेक्टर आईडी पर True होता है जिसे हम रखना चाहते हैं और उन पर False है जो हमारे उदाहरण में --- विराम चिह्न हैं।

इस तरह के लुकअप टेबल को बल्क लूकअप के लिए इस्तेमाल किया जा सकता है। जैसा कि लुकअप पूरी तरह से वेक्टरकृत है और अनिवार्य रूप से एक व्यूह की मात्रा को परिभाषित करने के लिए यह उदाहरण शब्दकोश लुकअप की तुलना में बहुत तेज़ है। यहां हम सुस्पष्ट दृश्य कास्टिंग का उपयोग करते हैं जो यूनिकोड वर्णों को पूर्ण रूप से मुफ्त में पूर्णांकों के रूप में फिर से व्याख्या करने की अनुमति देता है।

डेटा सरणी का उपयोग करना, जिसमें बूलियन मास्क में लुकअप टेबल परिणामों में अनुक्रमित करने के लिए संख्याओं के अनुक्रम के रूप में केवल एक मॉन्स्टर स्ट्रिंग को फिर से व्याख्या किया गया है। यह मुखौटा तब अवांछित पात्रों को छानने के लिए इस्तेमाल किया जा सकता है। बूलियन इंडेक्सिंग का उपयोग करते हुए, यह भी, कोड की एक एकल पंक्ति है।

अब तक बहुत सरल है। मुश्किल बिट अपने भागों में राक्षस स्ट्रिंग को काट रहा है। यदि हमारे पास एक विभाजक है, अर्थात एक वर्ण जो डेटा या विराम चिह्न सूची में नहीं होता है, तो यह अभी भी आसान है। शामिल होने और फिर से शुरू करने के लिए इस चरित्र का उपयोग करें। हालांकि, स्वचालित रूप से एक विभाजक ढूंढना चुनौतीपूर्ण है और वास्तव में नीचे कार्यान्वयन में आधे स्थान के लिए जिम्मेदार है।

वैकल्पिक रूप से, हम विभाजित बिंदुओं को एक अलग डेटा संरचना में रख सकते हैं, ट्रैक कर सकते हैं कि वे अवांछित वर्णों को हटाने के परिणामस्वरूप कैसे आगे बढ़ते हैं और फिर उन्हें संसाधित राक्षस स्ट्रिंग को टुकड़ा करने के लिए उपयोग करते हैं। चूंकि असमान लंबाई के कुछ हिस्सों में काट-छाँट करना सुपीरियर का सबसे मजबूत सूट नहीं है, इसलिए यह विधि str.split से धीमी है और केवल एक str.split रूप में उपयोग की जाती है जब एक विभाजक यह गणना करने के लिए बहुत महंगा होगा कि क्या यह पहले स्थान पर मौजूद है।

कोड (@ COLDSPEED की पोस्ट के आधार पर समय पर भारी साजिश रचना):

import numpy as np
import pandas as pd
import string
import re


spct = np.array([string.punctuation]).view(np.int32)
lookup = np.zeros((0x110000,), dtype=bool)
lookup[spct] = True
invlookup = ~lookup
OSEP = spct[0]
SEP = chr(OSEP)
while SEP in string.punctuation:
    OSEP = np.random.randint(0, 0x110000)
    SEP = chr(OSEP)


def find_sep_2(letters):
    letters = np.array([letters]).view(np.int32)
    msk = invlookup.copy()
    msk[letters] = False
    sep = msk.argmax()
    if not msk[sep]:
        return None
    return sep

def find_sep(letters, sep=0x88000):
    letters = np.array([letters]).view(np.int32)
    cmp = np.sign(sep-letters)
    cmpf = np.sign(sep-spct)
    if cmp.sum() + cmpf.sum() >= 1:
        left, right, gs = sep+1, 0x110000, -1
    else:
        left, right, gs = 0, sep, 1
    idx, = np.where(cmp == gs)
    idxf, = np.where(cmpf == gs)
    sep = (left + right) // 2
    while True:
        cmp = np.sign(sep-letters[idx])
        cmpf = np.sign(sep-spct[idxf])
        if cmp.all() and cmpf.all():
            return sep
        if cmp.sum() + cmpf.sum() >= (left & 1 == right & 1):
            left, sep, gs = sep+1, (right + sep) // 2, -1
        else:
            right, sep, gs = sep, (left + sep) // 2, 1
        idx = idx[cmp == gs]
        idxf = idxf[cmpf == gs]

def np_multi_strat(df):
    L = df['text'].tolist()
    all_ = ''.join(L)
    sep = 0x088000
    if chr(sep) in all_: # very unlikely ...
        if len(all_) >= 0x110000: # fall back to separator-less method
                                  # (finding separator too expensive)
            LL = np.array((0, *map(len, L)))
            LLL = LL.cumsum()
            all_ = np.array([all_]).view(np.int32)
            pnct = invlookup[all_]
            NL = np.add.reduceat(pnct, LLL[:-1])
            NLL = np.concatenate([[0], NL.cumsum()]).tolist()
            all_ = all_[pnct]
            all_ = all_.view(f'U{all_.size}').item(0)
            return df.assign(text=[all_[NLL[i]:NLL[i+1]]
                                   for i in range(len(NLL)-1)])
        elif len(all_) >= 0x22000: # use mask
            sep = find_sep_2(all_)
        else: # use bisection
            sep = find_sep(all_)
    all_ = np.array([chr(sep).join(L)]).view(np.int32)
    pnct = invlookup[all_]
    all_ = all_[pnct]
    all_ = all_.view(f'U{all_.size}').item(0)
    return df.assign(text=all_.split(chr(sep)))

def pd_replace(df):
    return df.assign(text=df['text'].str.replace(r'[^\w\s]+', ''))


p = re.compile(r'[^\w\s]+')

def re_sub(df):
    return df.assign(text=[p.sub('', x) for x in df['text'].tolist()])

punct = string.punctuation.replace(SEP, '')
transtab = str.maketrans(dict.fromkeys(punct, ''))

def translate(df):
    return df.assign(
        text=SEP.join(df['text'].tolist()).translate(transtab).split(SEP)
    )

# MaxU's version (https://stackoverflow.com/a/50444659/4909087)
def pd_translate(df):
    return df.assign(text=df['text'].str.translate(transtab))

from timeit import timeit

import pandas as pd
import matplotlib.pyplot as plt

res = pd.DataFrame(
       index=['translate', 'pd_replace', 're_sub', 'pd_translate', 'np_multi_strat'],
       columns=[10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000,
                1000000],
       dtype=float
)

for c in res.columns:
    if c >= 100000: # stress test the separator finder
        all_ = np.r_[:OSEP, OSEP+1:0x110000].repeat(c//10000)
        np.random.shuffle(all_)
        split = np.arange(c-1) + \
                np.sort(np.random.randint(0, len(all_) - c + 2, (c-1,))) 
        l = [x.view(f'U{x.size}').item(0) for x in np.split(all_, split)]
    else:
        l = ['a..b?!??', '%hgh&12','abc123!!!', '$$$1234'] * c
    df = pd.DataFrame({'text' : l})
    for f in res.index: 
        if f == res.index[0]:
            ref = globals()[f](df).text
        elif not (ref == globals()[f](df).text).all():
            res.at[f, c] = np.nan
            print(f, 'disagrees at', c)
            continue
        stmt = '{}(df)'.format(f)
        setp = 'from __main__ import df, {}'.format(f)
        res.at[f, c] = timeit(stmt, setp, number=16)

ax = res.div(res.min()).T.plot(loglog=True) 
ax.set_xlabel("N"); 
ax.set_ylabel("time (relative)");

plt.show()

यह एक स्व-उत्तर वाली पोस्ट है। नीचे मैं एनएलपी डोमेन में एक आम समस्या को रेखांकित करता हूं और इसे हल करने के लिए कुछ प्रदर्शन करने वाले तरीकों का प्रस्ताव करता हूं।

पाठ की सफाई और पूर्व प्रसंस्करण के दौरान विराम चिह्न को हटाने के लिए अक्सर आवश्यकता उत्पन्न होती है। विराम चिह्न को string.punctuation में किसी भी वर्ण के रूप में परिभाषित किया गया है।

>>> import string
string.punctuation
'!"#$%&\'()*+,-./:;<=>[email protected][\\]^_`{|}~'

यह एक सामान्य पर्याप्त समस्या है और इसे विज्ञापन से पहले पूछा गया है। सबसे मुहावरेदार समाधान पांडा str.replace का उपयोग करता है। हालांकि, ऐसी स्थितियों के लिए जिनमें बहुत अधिक पाठ शामिल हैं, एक अधिक निष्पादन समाधान पर विचार करने की आवश्यकता हो सकती है।

सैकड़ों रिकॉर्ड के साथ काम करते समय str.replace लिए कुछ अच्छे, प्रदर्शन करने वाले विकल्प क्या हैं?


वेनिला पायथन str.translate() :

def pd_translate(df):
    return df.assign(text=df['text'].str.translate(transtab))


सेट अप

प्रदर्शन के उद्देश्य से, आइए इस DataFrame पर विचार करें।

df = pd.DataFrame({'text':['a..b?!??', '%hgh&12','abc123!!!', '$$$1234']})
df
        text
0   a..b?!??
1    %hgh&12
2  abc123!!!
3    $$$1234

नीचे, मैं विकल्पों को सूचीबद्ध करता हूं, एक-एक करके, प्रदर्शन के बढ़ते क्रम में

str.replace

इस विकल्प को अन्य, अधिक निष्पादन समाधानों की तुलना करने के लिए मानदंड के रूप में डिफ़ॉल्ट विधि स्थापित करने के लिए शामिल किया गया है।

यह पांडा में निर्मित str.replace फ़ंक्शन का उपयोग करता है जो रेगेक्स-आधारित प्रतिस्थापन करता है।

df['text'] = df['text'].str.replace(r'[^\w\s]+', '')

df
     text
0      ab
1   hgh12
2  abc123
3    1234

यह कोड करना बहुत आसान है, और काफी पठनीय है, लेकिन धीमा है।

regex.sub

इसमें re लाइब्रेरी से sub फ़ंक्शन का उपयोग करना शामिल है। प्रदर्शन के लिए एक रेगेक्स पैटर्न को पूर्व-संकलित करें, और सूची regex.sub अंदर regex.sub को कॉल करें। df['text'] को पहले से सूची में परिवर्तित करें यदि आप कुछ मेमोरी को छोड़ सकते हैं, तो आपको इसमें से थोड़ा अच्छा प्रदर्शन बूस्ट मिलेगा।

import re
p = re.compile(r'[^\w\s]+')
df['text'] = [p.sub('', x) for x in df['text'].tolist()]

df
     text
0      ab
1   hgh12
2  abc123
3    1234

नोट: यदि आपके डेटा में NaN मान हैं, तो यह (और साथ ही नीचे दी गई अगली विधि) काम नहीं करेगा। " अन्य विचार " पर अनुभाग देखें।

str.translate

अजगर की str.translate फ़ंक्शन सी में कार्यान्वित की जाती है, और इसलिए बहुत तेज़ है

यह कैसे काम करता है:

  1. सबसे पहले, अपने सभी स्ट्रिंग्स को एक साथ एक एकल (या अधिक) वर्ण विभाजक का उपयोग करके एक विशाल स्ट्रिंग बनाने के लिए शामिल करें जिसे आप चुनते हैं। आपको एक ऐसे चरित्र / विकल्प का उपयोग करना होगा जिसकी गारंटी आप अपने डेटा के अंदर नहीं दे सकते।
  2. बड़ी स्ट्रिंग पर str.translate प्रदर्शन करें, विराम चिह्न (चरण 1 से विभाजक को हटा) को हटा दें।
  3. विभाजक पर स्ट्रिंग को विभाजित करें जिसका उपयोग चरण 1 में शामिल होने के लिए किया गया था। परिणामी सूची में आपके प्रारंभिक कॉलम की लंबाई समान होनी चाहिए

यहाँ, इस उदाहरण में, हम पाइप विभाजक पर विचार करते हैं । यदि आपके डेटा में पाइप है, तो आपको एक और विभाजक चुनना होगा।

import string

punct = '!"#$%&\'()*+,-./:;<=>[email protected][\\]^_`{}~'   # `|` is not present here
transtab = str.maketrans(dict.fromkeys(punct, ''))

df['text'] = '|'.join(df['text'].tolist()).translate(transtab).split('|')

df
     text
0      ab
1   hgh12
2  abc123
3    1234

प्रदर्शन

str.translate अब तक का सर्वश्रेष्ठ प्रदर्शन करता है। ध्यान दें कि नीचे दिए गए ग्राफ़ में MaxU के उत्तर से एक और प्रकार Series.str.translate शामिल है।

(दिलचस्प बात यह है कि मैं इसे दूसरी बार फिर से चलाता हूं, और परिणाम पहले से थोड़ा अलग हैं। दूसरे रन के दौरान, ऐसा लगता है कि re.sub वास्तव में कम मात्रा में डेटा के लिए str.translate पर जीत रहा था।)

translate का उपयोग करने के साथ एक अंतर्निहित जोखिम होता है (विशेषकर, यह तय करने की प्रक्रिया को स्वचालित करने की समस्या कि कौन सा विभाजक उपयोग करने के लिए गैर-तुच्छ है), लेकिन व्यापार-बंद जोखिम के लायक हैं।

अन्य बातें

सूची समझने के तरीकों के साथ NaN को संभालना; ध्यान दें कि यह विधि (और अगले) केवल तब तक काम करेगी जब तक आपके डेटा में NaN नहीं है। NaNs को संभालते समय, आपको गैर-शून्य मानों के सूचकांकों को निर्धारित करना होगा और केवल उन को बदलना होगा। कुछ इस तरह की कोशिश करो:

df = pd.DataFrame({'text': [
    'a..b?!??', np.nan, '%hgh&12','abc123!!!', '$$$1234', np.nan]})

idx = np.flatnonzero(df['text'].notna())
col_idx = df.columns.get_loc('text')
df.iloc[idx,col_idx] = [
    p.sub('', x) for x in df.iloc[idx,col_idx].tolist()]

df
     text
0      ab
1     NaN
2   hgh12
3  abc123
4    1234
5     NaN

डेटाफ्रेम से निपटना; यदि आप DataFrames के साथ काम कर रहे हैं, जहां हर कॉलम को प्रतिस्थापन की आवश्यकता होती है, तो प्रक्रिया सरल है:

v = pd.Series(df.values.ravel())
df[:] = translate(v).values.reshape(df.shape)

या,

v = df.stack()
v[:] = translate(v)
df = v.unstack()

ध्यान दें कि translate फ़ंक्शन बेंचमार्किंग कोड के साथ नीचे परिभाषित किया गया है।

हर समाधान में ट्रेडऑफ़ होता है, इसलिए यह तय करना कि आपकी आवश्यकताओं में से कौन सा समाधान सबसे उपयुक्त है, इस बात पर निर्भर करेगा कि आप बलिदान करने के लिए क्या चाहते हैं। दो बहुत ही सामान्य विचार प्रदर्शन हैं (जो हमने पहले ही देखे हैं), और स्मृति उपयोग। str.translate एक मेमोरी-भूखा समाधान है, इसलिए सावधानी के साथ उपयोग करें।

एक और विचार आपके रेगेक्स की जटिलता है। कभी-कभी, आप कुछ भी निकालना चाहते हैं जो अल्फ़ान्यूमेरिक या व्हॉट्सएप नहीं है। अन्य, आपको कुछ पात्रों, जैसे कि हाइफ़न, कॉलन और वाक्य टर्मिनेटर [.!?] रखना होगा [.!?] इन्हें स्पष्ट रूप से अपने रेगेक्स में जटिलता जोड़ें, जो इन समाधानों के प्रदर्शन को प्रभावित कर सकता है। सुनिश्चित करें कि आप क्या उपयोग करना है, यह तय करने से पहले अपने डेटा पर इन समाधानों का परीक्षण करें।

अंत में, इस समाधान के साथ यूनिकोड वर्णों को हटा दिया जाएगा। आप अपने रेगेक्स (यदि एक रेगेक्स-आधारित समाधान का उपयोग कर रहे हैं) को str.translate करना str.translate हैं, या बस str.translate साथ str.translate अन्यथा।

और भी अधिक प्रदर्शन के लिए (बड़े एन के लिए), पॉल पैंजर के इस जवाब पर एक नज़र डालें।

अनुबंध

कार्य

def pd_replace(df):
    return df.assign(text=df['text'].str.replace(r'[^\w\s]+', ''))


def re_sub(df):
    p = re.compile(r'[^\w\s]+')
    return df.assign(text=[p.sub('', x) for x in df['text'].tolist()])

def translate(df):
    punct = string.punctuation.replace('|', '')
    transtab = str.maketrans(dict.fromkeys(punct, ''))

    return df.assign(
        text='|'.join(df['text'].tolist()).translate(transtab).split('|')
    )

# MaxU's version (https://.com/a/50444659/4909087)
def pd_translate(df):
    punct = string.punctuation.replace('|', '')
    transtab = str.maketrans(dict.fromkeys(punct, ''))

    return df.assign(text=df['text'].str.translate(transtab))

प्रदर्शन बेंचमार्किंग कोड

from timeit import timeit

import pandas as pd
import matplotlib.pyplot as plt

res = pd.DataFrame(
       index=['pd_replace', 're_sub', 'translate', 'pd_translate'],
       columns=[10, 50, 100, 500, 1000, 5000, 10000, 50000],
       dtype=float
)

for f in res.index: 
    for c in res.columns:
        l = ['a..b?!??', '%hgh&12','abc123!!!', '$$$1234'] * c
        df = pd.DataFrame({'text' : l})
        stmt = '{}(df)'.format(f)
        setp = 'from __main__ import df, {}'.format(f)
        res.at[f, c] = timeit(stmt, setp, number=30)

ax = res.div(res.min()).T.plot(loglog=True) 
ax.set_xlabel("N"); 
ax.set_ylabel("time (relative)");

plt.show()




numpy