python - পান্ডাসহ দ্রুত বিরামচিহ্ন অপসারণ




regex string (2)

এটি একটি স্ব-উত্তর পোস্ট। নীচে আমি এনএলপি ডোমেনে একটি সাধারণ সমস্যাটির রূপরেখা দিচ্ছি এবং এটি সমাধানের জন্য কয়েকটি পারফরম্যান্ট পদ্ধতি প্রস্তাব করছি।

পাঠ্য পরিষ্কার এবং প্রাক-প্রক্রিয়াজাতকরণের সময় প্রায়শই বিরামচিহ্ন অপসারণ করার প্রয়োজন দেখা দেয়। string.punctuation যেকোন চরিত্র হিসাবে সংজ্ঞায়িত করা হয়েছে: string.punctuation :

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

এটি একটি সাধারণ যথেষ্ট সমস্যা এবং অ্যাড বমিভাবের আগে জিজ্ঞাসা করা হয়েছিল। সর্বাধিক প্রতিমূর্তিযুক্ত সমাধানে পান্ডাস str.replace ব্যবহার করা হয়। যাইহোক, যে পরিস্থিতিতে প্রচুর পাঠ্য জড়িত সেগুলির জন্য আরও পারফরম্যান্স সমাধান বিবেচনা করা প্রয়োজন।

কয়েক হাজার রেকর্ডের সাথে লেনদেন করার সময় str.replace কিছু ভাল, পারফরম্যান্ট বিকল্প কী কী?


সেটআপ

প্রদর্শনের উদ্দেশ্যে, আসুন আমরা এই ডেটাফ্রেমটি বিবেচনা করি।

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

এটি কোড করা খুব সহজ এবং বেশ পঠনযোগ্য তবে ধীর slow

regex.sub

এটি re গ্রন্থাগার থেকে 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. ধাপ ১ এ যোগদানের জন্য যে বিভাজকটি ব্যবহৃত হয়েছিল তার উপর স্ট্রিংটি বিভক্ত করুন ফলাফলের তালিকার আপনার প্রাথমিক কলামের সমান দৈর্ঘ্য থাকতে হবে

এখানে, এই উদাহরণে, আমরা পাইপ বিভাজক বিবেচনা করি । যদি আপনার ডেটাতে পাইপ থাকে, তবে আপনাকে অবশ্যই অন্য একটি বিভাজক বেছে নিতে হবে।

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 এখন পর্যন্ত সেরা সঞ্চালন করে। নোট করুন যে নীচের গ্রাফটিতে ম্যাক্সউয়ের উত্তর থেকে আরেকটি বৈকল্পিক Series.str.translate অন্তর্ভুক্ত রয়েছে।

(মজার বিষয় হচ্ছে, আমি এটি দ্বিতীয়বার পুনরায় পুনরায় পুনরুদ্ধার করেছি এবং ফলাফলগুলি আগের চেয়ে কিছুটা আলাদা the দ্বিতীয় রান চলাকালীন, মনে হয় re.sub সত্যই খুব অল্প পরিমাণে ডেটার জন্য str.translate re.sub দিয়ে জিতেছে))

translate ব্যবহারের সাথে একটি অন্তর্নিহিত ঝুঁকি রয়েছে (বিশেষত, কোন বিভাজকটি ব্যবহার করা উচিত তা ক্ষুদ্রতর নয় এটি সিদ্ধান্ত নেওয়ার প্রক্রিয়াটি স্বয়ংক্রিয়করণের সমস্যা), তবে বাণিজ্য বন্ধগুলি ঝুঁকির পক্ষে মূল্যবান।

অন্যান্য বিবেচ্য বিষয়

তালিকা বোধগম্য পদ্ধতিগুলির সাথে 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

ডেটা ফ্রেমগুলির সাথে ডিলিং; আপনি যদি ডেটা ফ্রেমগুলি নিয়ে কাজ করে থাকেন, যেখানে প্রতিটি কলামের প্রতিস্থাপনের প্রয়োজন হয়, পদ্ধতিটি সহজ:

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 দিয়ে যেতে পারেন।

আরও বেশি পারফরম্যান্সের জন্য (বৃহত্তর এন এর জন্য), পল পানজারের এই উত্তরটি একবার দেখুন।

উপাঙ্গ

ক্রিয়াকলাপ

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()

নম্পতি ব্যবহার করে আমরা এ পর্যন্ত পোস্ট করা সেরা পদ্ধতির চেয়ে স্বাস্থ্যকর গতি অর্জন করতে পারি। প্রাথমিক কৌশলটি একই --- একটি বড় সুপার স্ট্রিং তৈরি করুন। তবে প্রক্রিয়াজাতকরণটি অসাড়তার মধ্যে অনেক দ্রুত বলে মনে হয়, সম্ভবতঃ কারণ আমরা কিছু না-কিছু-কিছু প্রতিস্থাপনের বিকল্পটির সরলতা পুরোপুরি কাজে লাগিয়েছি।

ছোট (মোট 0x110000 অক্ষরের চেয়ে কম) সমস্যার জন্য আমরা স্বয়ংক্রিয়ভাবে একটি 0x110000 পাই, বড় সমস্যার জন্য আমরা একটি ধীর পদ্ধতি ব্যবহার করি যা str.split উপর নির্ভর করে না।

মনে রাখবেন যে আমি সমস্ত প্রাক্পম্পটেবলগুলি ফাংশন থেকে সরিয়ে নিয়েছি। এছাড়াও মনে রাখবেন, translate এবং pd_translate বিনামূল্যে তিনটি বৃহত্তম সমস্যার একমাত্র সম্ভাব্য pd_translate জানতে পারে যেখানে 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://.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()




numpy