【问题标题】:parsing and avoiding nested loops in Python在 Python 中解析和避免嵌套循环
【发布时间】:2021-02-22 21:11:10
【问题描述】:

我有一个 sql 表,其中 12000 个条目存储在数据框 df1 中,如下所示:

id name
00001 angiocarcoma
00261 shrimp allergy

我还有一个包含 20000 个条目的表,它存储在数据框 df 中:

Entry_name CA
TRGV2 3BHS1 HSD3B1 3BH HSDB3
TRGJ1 3BP1 SH3BP1 IF

目的是匹配一个句子中来自df1的名称与来自df的CA(用“”空格分隔)的每个可能组合,条件是CA单元格值的长度应该大于2。最简单的逻辑将在句子中搜索来自 df1 的所有名称值,如果找到匹配项,则在同一个句子中搜索 CA 值。但这样做会限制资源使用。

以下是我尝试过的代码,我只能想到嵌套循环来完成任务。如果我使用两个函数,那么我将创建一个函数调用开销,如果我尝试递归执行它,那么如果我超出了 Python 中的 recurive 函数调用,这会强制内核关闭。 通过向它传递一个句子(我必须解析 500k 个句子)来调用以下函数:

 def disease_search(nltk_tokens_sen):
  for dis_index in range(len(df1)): 
        disease_name=df1.at[dis_index,'name']
        regex_for_dis = rf"\b{disease_name}\b"
        matches_for_dis= re.findall(regex_for_dis, nltk_tokens_sen, re.IGNORECASE | re.MULTILINE)
        if len(matches_for_dis)!=0:
            disease_marker(nltk_tokens_sen, disease_name)
        

如果上述函数找到匹配项,则调用此函数:

    def disease_marker(nltk_tokens_sen, disease_name):
     for zz in range(len(df)):
      biomarker_txt=((df.at[zz,'CA'])) 
      biomarker = biomarker_txt.split(" ")
      for tt in range(len(biomarker)):
        if len(biomarker[tt])>2:
            matches_for_marker = re.findall(rf"\b{re.escape(biomarker[tt])}\b", nltk_tokens_sen)
            if len(matches_for_marker)!=0:
                print("Match_found:", disease_name, biomarker[tt] )

我需要完全改变我的逻辑还是有一个 Pythonic 运行时有效的方法来实现它?

【问题讨论】:

  • 尝试列表理解:fruits = ["apple", "banana", "cherry", "kiwi", "mango"] fruits1=["this is a apple sentence", "this is no snetence", "这是猕猴桃句子","apple is good","mango is good as well","这是苹果和香蕉"] abbg=len(fruits) tpog=len(fruits1) newlist = [(fruits[ x],fruits1[y]) for x in range(abbg) for y in range(tpog) if fruits[x] in fruits1[y]] print(newlist)
  • 我建议您首先使用 timeit 或任何其他时间测量库来建立基线。没有它,优化很难。如果列表更短,O(n) 可能比 O(lg n) 快,但如果列表更长,则可能会更慢。大多数计算机现在至少有 16GB 内存,因此通常处理能力/运行时间是限制。

标签: python python-3.x regex for-loop nested


【解决方案1】:

优化和修改

  • 使用Aho-Corasick算法在字符串中查找多个子字符串(比逐次检查每个子字符串与字符串的速度要快得多)
  • 由于疾病和生物标志物是独立的,因此在字符串中查找所有疾病和所有生物标志物,并对两个结果执行product(即查找生物标志物一次,而不是每个发现的疾病一次)
  • OP 需要疾病名称和 ID 以及生物标志物 CA 和 Entry_name 以及每个结果。

结果:

  • 33 X 加速单个 nltk 句子(107 ms 与 3.54 s)
  • 538e3 X 加速后续 nltk 语句(6.57 us vs. 3.54 s) (在后续的 nltk 句子中使用相同的键时)

新代码

文件:main.py

##################################################################
# Imports keyword data from simulate_data module and processes
# on nltk sentence
##################################################################
if __name__ == "__main__":
  from process import Finder, format_result
  from simulate_data import df_disease, df_biomarker

  # Sentence to process
  nltk_sentence = 'very hard angiocarcoma diagnosed 3BHS1'

  # Set up search engine
  finder = Finder(df_disease, df_biomarker)

  # Process and loop through results
  for d, m in finder.process(nltk_sentence):
      format_result(d, m)

文件:process.py

from itertools import product
import ahocorasick as ahc

def make_aho_automaton(keywords):
    A = ahc.Automaton()  # initialize
    for (key, cat) in keywords:
        A.add_word(key, (cat, key)) # add keys and categories
    A.make_automaton() # generate automaton
    return A

class Finder():
    def  __init__(self, df_disease, df_biomarker):
        ' Initialize Automatons for Disease and biomarkers '
        #   Disease and biomarker Keywords
        #      Pairs of (name, id) with name as key and id as category
        #      note: used underscore on variable names (e.g. _id) to ensure against shadowing builtins
        disease_keys = ((_name.lower(), _id) for _name, _id in zip(df_disease['name'], df_disease['id']))
        #     Pairs of (CA, Entry_name) with CA as key and Entry_name as category
        biomarker_keys = (
            (_ca_entry, _entry_name) 
            for _ca, _entry_name in zip(df_biomarker['CA'], df_biomarker['Entry_name']) 
            for _ca_entry in _ca.split() if len(_ca_entry) >= 3
        )
        
        # Create Aho-Corasick automatons
        #    Surround keywords with space so we find only only word boundaries
        self.disease_automaton = make_aho_automaton((f' {keyw} ', cat) for keyw, cat in disease_keys)
        self.biomarker_automaton = make_aho_automaton((f' {keyw} ', cat) for keyw, cat in biomarker_keys)
        
    def find_keywords(self, line, A):
        ' Finds key words in line that exist in Automaton A (as generator)'
        # ensure line has space at beginning and end, but remove from key
        return ((keyw.strip(), cat) for end_index, (cat, keyw) in A.iter(f' {line} '))
    
    def process(self, nltk_sentence):
        sentence = f' {nltk_sentence} '  # ensures sentence as space at both ends (for keyword matching)
        for d, m in product(self.find_keywords(sentence, self.disease_automaton), 
                            self.find_keywords(sentence, self.biomarker_automaton)):
            yield d, m
  
def format_result(d, m):
    ' Format results for printing '
    return print(f'Match_found: Disease: name {d[0].title()} with id {d[1]}, Biomarker CA {m[0]} with Entry_name {m[1]}')

文件:simulate_data.py

import string
from random import randint, choice
import pandas as pd

def random_word(min_length = 2, max_length = 5, upper = False):
    ' Generate random word '
    if upper:
        # Used for biomarker
        letters = string.ascii_uppercase + string.digits
    else:
        # used by disease
        letters = string.ascii_lowercase
    
    return ''.join(choice(letters) for _ in range(randint(min_length, max_length)))

def random_sentence(min_length = 1, max_length = 3, upper = False):
    '''Random sentence -- upper (True) generates letters and digits, 
      letters only for lower
    '''
    return ' '.join(random_word(upper = upper) for _ in range(randint(min_length, max_length)))

diseases = {"id":['00001', '00261'],
        'name':['angiocarcoma', 'shrimp allergy']}
biomarkers = {'Entry_name':['TRGV2', 'TRGJ1'],
          'CA':['3BHS1 HSD3B1 3BH HSDB3', '3BP1 SH3BP1 IF']}

N = 10000
for i in range(N):
    diseases['id'].append(str(i+300).zfill(5))
    diseases['name'].append(random_sentence(min_length = 1, max_length = 5, upper = False))
    biomarkers['Entry_name'].append(random_word(4, 5, True))
    biomarkers['CA'].append(random_sentence(2, 6, True))
    
df_disease = pd.DataFrame(diseases)
df_biomarker = pd.DataFrame(biomarkers)

输出

示例 1 -- 在单个句子上使用 nltk 句子

nltk_sentence = 'allergy very hard angiocarcoma diagnosed 3BH 3BHS1'
finder = Finder(df_disease, df_biomarker)  # Finder for disease and biomarker keys
for d, m in finder.process(nltk_sentence): # apply to nltk_sentence
    format_result(d, m)
    
 # Out:
 # Match_found: Disease: name Angiocarcoma with id 00001, Biomarker CA 3BH with Entry_name TRGV2
 # Match_found: Disease: name Angiocarcoma with id 00001, Biomarker CA 3BHS1 with Entry_name TRGV2

示例 2--循环多个 nltk 语句

sentences = ("Very hard angiocarcoma diagnosed 3BHS1\n"                # Note: Un-Capitalized A in Angiocarcoma
             "very hard Angiocarcoma diagnosed IF\n"                   # Note: Biomarker IF is too short)
             "very hard angiocarcoma diagnosed 3BP0\n"                 # Note: Un-Capitalized A in Angiocarcoma
             "Very hard Angiocarcoma diagnosed 3BP0 3BHS1\n"           # Note: Capitalized A in Angiocarcoma
             "Fish allergy very hard angiocarcoma diagnosed 3BP0 3BHS1"
            )

for sentence in sentences.split('\n'):
    print(sentence)
    for d, m in finder.process(sentence):
        format_result(d, m)
    print()
 
# Out:
# Very hard angiocarcoma diagnosed 3BHS1
# Match_found: Disease: name Angiocarcoma with id 00001, Biomarker CA 3BHS1 with Entry_name TRGV2
#  
very hard Angiocarcoma diagnosed IF
# 
very hard angiocarcoma diagnosed 3BP0
# 
Very hard Angiocarcoma diagnosed 3BP0 3BHS1
# 
# Fish allergy very hard angiocarcoma diagnosed 3BP0 3BHS1
# Match_found: Disease: name Angiocarcoma with id 00001, Biomarker CA 3BHS1 with Entry_name TRGV2

时序测试

使用了超过 10K 记录的数据集(参见上面的模拟数据.py)

修改 OP 代码(使时序比较公平)

  • 使用生成器来避免打印(会减慢时间)
  • 使用比 df 和 df1 更有目的性的名称(实际上与时间无关)

修改后的 OP 代码(用于计时原始)

def disease_search(nltk_tokens_sen):
  for dis_index in range(len(df1)): 
        disease_name = df_disease.at[dis_index,'name']
        regex_for_dis = rf"\b{disease_name}\b"
        matches_for_dis= re.findall(regex_for_dis, nltk_tokens_sen, re.IGNORECASE | re.MULTILINE)
        if len(matches_for_dis)!=0:
            yield from disease_marker(nltk_tokens_sen, disease_name)

def disease_marker(nltk_tokens_sen, disease_name):
     for zz in range(len(df_biomarkers)):
      biomarker_txt=((df_biomarkers.at[zz,'CA'])) 
      biomarker = biomarker_txt.split(" ")
      for tt in range(len(biomarker)):
        if len(biomarker[tt])>2:
            matches_for_marker = re.findall(rf"\b{re.escape(biomarker[tt])}\b", nltk_tokens_sen)
            if len(matches_for_marker)!=0:
                yield disease_name, biomarker[tt]

    

计时测试

使用 Jupyter Notebook 的“魔术命令”

定时新代码

%timeit finder = Finder(df_disease, df_biomarker) # Initilization
107 ms ± 4.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit list(finder.process(nltk_sentence))       # Per nltk string
6.57 µs ± 618 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)    
Total: ~107 ms to setup and 6.57 us per nltk sentence

计时原码

 %timeit list(disease_search(nltk_sentence))
 3.54 s ± 446 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

【讨论】:

  • 看起来很有希望,我会试一试告诉你。
  • “make_aho_automaton”是一个内置函数吗? nad 如何打印对应的 'id' 对应的 'disease' 和 entry_name 对应的 'CA' 值?
  • @Sam--Aho-Corasick 是一个 Python 模块。您可以像其他模块一样安装在您的环境中(例如pip install pyahocorasick)。与安装其他模块(如 Pandas 和 Numpy)的方式相同。关于获取“id”和“entry_name”,我相信这是可以做到的。但是,除非我错过了,否则您没有在帖子中提供这些内容。我只是想产生同样的结果。你也想要这些物品吗?
  • 每个句子每次都会调用“process”函数。它可能会产生开销,在主代码中使用它而不是每次调用不是更好吗?
  • @DarryIG 我已经安装了模块,但它仍然显示未定义名称 make_aho_automaton
【解决方案2】:

试试这个,让我知道。由于比 pandas DataFrame 更快的访问结构(如列表和字典)和有效项目的快速初步选择,这应该更节省时间,这不使用库 re。

# necessary imports
import pandas as pd
import itertools
import re

# test dataframes
df1 = pd.DataFrame({
    'id': ['00001','00261','00002'],
    'name': ['angiocarcoma', 'shrimp allergy', 'fish allergy']
})

df = pd.DataFrame({
    'Entry_name': ['TRGV2','TRGJ1','TRGJ2'],
    'CA': ['3BHS1 HSD3B1 3BH HSDB3', '3BP1 SH3BP1 IF', '3BP0']
})

# redesign data structures you work with
# set() will deduplicate for you
disease_list = list(set(df1['name']))
CA_list = list(set(df['CA']))
valid_CA_list_tmp = list(itertools.chain(*[x.split() for x in CA_list]))
valid_CA_list = [x for x in valid_CA_list_tmp if len(x)>2]

# the function
def disease_search_v2(nltk_tokens_sen):
    """Takes string as input"""
    
    found_diseases_preliminary = [x for x in disease_list if x.lower() in nltk_tokens_sen.lower()]
    found_CA_preliminary = [x for x in valid_CA_list if x.lower() in nltk_tokens_sen.lower()]
    
    found_diseases = [x for x in found_diseases_preliminary if re.search(rf"\b{x}\b", nltk_tokens_sen)]
    found_CA = [x for x in found_CA_preliminary if re.search(rf"\b{x}\b", nltk_tokens_sen)]

    if len(found_diseases) > 0 and len(found_CA) > 0:
        return {x:found_CA for x in found_diseases}
    else:
        return {}

# testing cases
disease_search_v2('very hard angiocarcoma diagnosed 3BHS1')
disease_search_v2('very hard angiocarcoma diagnosed IF')
disease_search_v2('very hard angiocarcoma diagnosed 3BP0')
disease_search_v2('very hard angiocarcoma diagnosed 3BP0 3BHS1')
disease_search_v2('fish allergy very hard angiocarcoma diagnosed 3BP0 3BHS1')
disease_search_v2('fish allergy very hard angiocarcoma diagnosed 3BP0 3BHS1\nfish allergy very hard angiocarcoma diagnosed 3BP0 3BHS1\n')

【讨论】:

【解决方案3】:

根据您粘贴在 cmets 中的链接,您正尝试遍历所有可用的疾病名称以在给定的单词段落中查找疾病。我建议你遍历段落的单词并在 DataFrame 中找到匹配项

您可以尝试执行以下步骤,

  1. 将 nltk_tokens_sen 拆分为单词列表,并将其命名为 nltk_tokens_words

  2. 您可以使用 DF 字符串过滤器,例如 match & contains,而不是遍历整个 DataFrame 以从给定的单词列表中查找匹配的行。这将减少整个 DF 的循环。

    filtered_rows = (df1['name'].str.contains(string) for string in nltk_tokens_words)

  3. 使用np创建组合标记并申请得到过滤后的DF

    combined_mask = np.vstack(filtered_rows).all(axis=0)

    df1[combined_mask]

  4. 对第二个 DF 重复相同的步骤。

试试这个,如果这对你有帮助,请告诉我!!

【讨论】:

  • 我正在努力加快这个过程,并希望它尽可能正确。如果我将句子单词与数据框值进行比较,那么我会失去很多积极的结果。例如,数据框中的疾病条目是癌症,句子中的单词是癌症,这意味着我不会匹配。
猜你喜欢
  • 2017-08-27
  • 2017-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-25
  • 2021-08-19
  • 2017-09-20
  • 2023-03-15
相关资源
最近更新 更多