Почему мой скрипт на Python намного медленнее, чем его R-эквивалент?

ПРИМЕЧАНИЕ: этот вопрос описывает, почему скрипт такой медленный. Однако, если вы более склонны к улучшению, вы можете взглянуть на мой пост на CodeReview, который направлен на повышение производительности.

Я работаю над проектом, который обрабатывает текстовые файлы (.lst).

Наименование файла с именами (fileName) важны, потому что я буду извлекать node (например, abessijn) и component (например, WR-PEA) из них в информационный кадр. Примеры:

abessijn.WR-P-E-A.lst
A-bom.WR-P-E-A.lst
acroniem.WR-P-E-C.lst
acroniem.WR-P-E-G.lst
adapter.WR-P-E-A.lst
adapter.WR-P-E-C.lst
adapter.WR-P-E-G.lst

Каждый файл состоит из одной или нескольких строк. Каждая строка состоит из предложения (внутри <sentence> теги). Пример (abessijn.WR-PEA.lst)

/home/nobackup/SONAR/COMPACT/WR-P-E-A/WR-P-E-A0000364.data.ids.xml:  <sentence>Vooral mijn abessijn ruikt heerlijk kruidig .. : ) )</sentence>
/home/nobackup/SONAR/COMPACT/WR-P-E-A/WR-P-E-A0000364.data.ids.xml:  <sentence>Mijn abessijn denkt daar heel anders over .. : ) ) Maar mijn kinderen richt ik ook niet af , zit niet in mijn bloed .</sentence>

Из каждой строки я извлекаю предложение, делаю небольшие изменения и называю его sentence, Далее идет элемент под названием leftContext, который занимает первую часть раскола между node (например, abessijn) и предложение, из которого оно вышло. Наконец, из leftContext Я получаю слово "предыдущий", то есть слово "предшествующий". node в sentence или самое правильное слово в leftContext (с некоторыми ограничениями, такими как опция соединения, образованного дефисом). Пример:

ID | filename             | node | component | precedingWord      | leftContext                               |  sentence
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1    adapter.WR-P-P-F.lst  adapter  WR-P-P-F   aanpassingseenheid  Een aanpassingseenheid (                      Een aanpassingseenheid ( adapter ) , 
2    adapter.WR-P-P-F.lst  adapter  WR-P-P-F   toestel             Het toestel (                                 Het toestel ( adapter ) draagt zorg voor de overbrenging van gegevens
3    adapter.WR-P-P-F.lst  adapter  WR-P-P-F   de                  de aansluiting tussen de sensor en de         de aansluiting tussen de sensor en de adapter , 
4    airbag.WS-U-E-A.lst   airbag   WS-U-E-A   den                 ja voor den                                   ja voor den airbag op te pompen eh :p
5    airbag.WS-U-E-A.lst   airbag   WS-U-E-A   ne                  Dobby , als ze valt heeft ze dan wel al ne    Dobby , als ze valt heeft ze dan wel al ne airbag hee

Этот фрейм данных экспортируется как dataset.csv.

После этого у меня появляется намерение: я создаю таблицу частот, которая принимает node а также precedingWord в учетную запись. Из переменной я определяю neuter а также non_neuter Например, (в Python)

neuter = ["het", "Het"]
non_neuter = ["de","De"]

и категория отдыха unspecified, когда precedingWord это элемент из списка, назначьте его переменной. Пример вывода таблицы частот:

node    |   neuter   | nonNeuter   | unspecified
-------------------------------------------------
A-bom       0          4             2
acroniem    3          0             2
act         3          2             1

Список частот экспортируется как частот.csv.


Я начал с R, учитывая, что позже я проведу статистический анализ частот. Мой текущий скрипт R (также доступен в виде вставки):

# ---
# STEP 0: Preparations
  start_time <- Sys.time()
  ## 1. Set working directory in R
    setwd("")

  ## 2. Load required library/libraries
    library(dplyr)
    library(mclm)
    library(stringi)

  ## 3. Create directory where we'll save our dataset(s)
    dir.create("../R/dataset", showWarnings = FALSE)


# ---
# STEP 1: Loop through files, get data from the filename

    ## 1. Create first dataframe, based on filename of all files
    files <- list.files(pattern="*.lst", full.names=T, recursive=FALSE)
    d <- data.frame(fileName = unname(sapply(files, basename)), stringsAsFactors = FALSE)

    ## 2. Create additional columns (word & component) based on filename
    d$node <- sub("\\..+", "", d$fileName, perl=TRUE)
    d$node <- tolower(d$node)
    d$component <- gsub("^[^\\.]+\\.|\\.lst$", "", d$fileName, perl=TRUE)


# ---
# STEP 2: Loop through files again, but now also through its contents
# In other words: get the sentences

    ## 1. Create second set which is an rbind of multiple frames
    ## One two-column data.frame per file
    ## First column is fileName, second column is data from each file
    e <- do.call(rbind, lapply(files, function(x) {
        data.frame(fileName = x, sentence = readLines(x, encoding="UTF-8"), stringsAsFactors = FALSE)
    }))

    ## 2. Clean fileName
     e$fileName <- sub("^\\.\\/", "", e$fileName, perl=TRUE)

    ## 3. Get the sentence and clean
    e$sentence <- gsub(".*?<sentence>(.*?)</sentence>", "\\1", e$sentence, perl=TRUE)
    e$sentence <- tolower(e$sentence)
        # Remove floating space before/after punctuation
        e$sentence <- gsub("\\s(?:(?=[.,:;?!) ])|(?<=\\( ))", "\\1", e$sentence, perl=TRUE)
    # Add space after triple dots ...
      e$sentence <- gsub("\\.{3}(?=[^\\s])", "... ", e$sentence, perl=TRUE)

    # Transform HTML entities into characters
    # It is unfortunate that there's no easier way to do this
    # E.g. Python provides the HTML package which can unescape (decode) HTML
    # characters
        e$sentence <- gsub("&apos;", "'", e$sentence, perl=TRUE)
        e$sentence <- gsub("&amp;", "&", e$sentence, perl=TRUE)
      # Avoid R from wrongly interpreting ", so replace by single quotes
        e$sentence <- gsub("&quot;|\"", "'", e$sentence, perl=TRUE)

      # Get rid of some characters we can't use such as ³ and ¾
      e$sentence <- gsub("[^[:graph:]\\s]", "", e$sentence, perl=TRUE)


# ---
# STEP 3:
# Create final dataframe

  ## 1. Merge d and e by common column name fileName
    df <- merge(d, e, by="fileName", all=TRUE)

  ## 2. Make sure that only those sentences in which df$node is present in df$sentence are taken into account
    matchFunction <- function(x, y) any(x == y)
    matchedFrame <- with(df, mapply(matchFunction, node, stri_split_regex(sentence, "[ :?.,]")))
    df <- df[matchedFrame, ]

  ## 3. Create leftContext based on the split of the word and the sentence
    # Use paste0 to make sure we are looking for the node, not a compound
    # node can only be preceded by a space, but can be followed by punctuation as well
    contexts <- strsplit(df$sentence, paste0("(^| )", df$node, "( |[!\",.:;?})\\]])"), perl=TRUE)
    df$leftContext <- sapply(contexts, `[`, 1)

  ## 4. Get the word preceding the node
    df$precedingWord <- gsub("^.*\\b(?<!-)(\\w+(?:-\\w+)*)[^\\w]*$","\\1", df$leftContext, perl=TRUE)

  ## 5. Improve readability by sorting columns
    df <- df[c("fileName", "component", "precedingWord", "node", "leftContext", "sentence")]

  ## 6. Write dataset to dataset dir
    write.dataset(df,"../R/dataset/r-dataset.csv")


# ---
# STEP 4:
# Create dataset with frequencies

  ## 1. Define neuter and nonNeuter classes
    neuter <- c("het")
    non.neuter<- c("de")

  ## 2. Mutate df to fit into usable frame
    freq <- mutate(df, gender = ifelse(!df$precedingWord %in% c(neuter, non.neuter), "unspecified",
      ifelse(df$precedingWord %in% neuter, "neuter", "non_neuter")))

  ## 3. Transform into table, but still usable as data frame (i.e. matrix)
  ## Also add column name "node"
    freqTable <- table(freq$node, freq$gender) %>%
      as.data.frame.matrix %>%
      mutate(node = row.names(.))

  ## 4. Small adjustements
    freqTable <- freqTable[,c(4,1:3)]

  ## 5. Write dataset to dataset dir
    write.dataset(freqTable,"../R/dataset/r-frequencies.csv")


    diff <- Sys.time() - start_time # calculate difference
    print(diff) # print in nice format

Однако, поскольку я использую большой набор данных (16 500 файлов, все с несколькими строками), это заняло довольно много времени. В моей системе весь процесс занял около часа с четвертью. Я подумал про себя, что должен существовать язык, который мог бы делать это быстрее, поэтому я пошел и научил себя немного Python и задал много вопросов здесь, на SO.

Наконец-то я придумал следующий скрипт ( вставка).

import os, pandas as pd, numpy as np, regex as re

from glob import glob
from datetime import datetime
from html import unescape

start_time = datetime.now()

# Create empty dataframe with correct column names
columnNames = ["fileName", "component", "precedingWord", "node", "leftContext", "sentence" ]
df = pd.DataFrame(data=np.zeros((0,len(columnNames))), columns=columnNames)

# Create correct path where to fetch files
subdir = "rawdata"
path = os.path.abspath(os.path.join(os.getcwd(), os.pardir, subdir))

# "Cache" regex
# See http://stackru.com/q/452104/1150683
p_filename = re.compile(r"[./\\]")

p_sentence = re.compile(r"<sentence>(.*?)</sentence>")
p_typography = re.compile(r" (?:(?=[.,:;?!) ])|(?<=\( ))")
p_non_graph = re.compile(r"[^\x21-\x7E\s]")
p_quote = re.compile(r"\"")
p_ellipsis = re.compile(r"\.{3}(?=[^ ])")

p_last_word = re.compile(r"^.*\b(?<!-)(\w+(?:-\w+)*)[^\w]*$", re.U)

# Loop files in folder
for file in glob(path+"\\*.lst"):
    with open(file, encoding="utf-8") as f:
        [n, c] = p_filename.split(file.lower())[-3:-1]
        fn = ".".join([n, c])
        for line in f:
            s = p_sentence.search(unescape(line)).group(1)
            s = s.lower()
            s = p_typography.sub("", s)
            s = p_non_graph.sub("", s)
            s = p_quote.sub("'", s)
            s = p_ellipsis.sub("... ", s)

            if n in re.split(r"[ :?.,]", s):
                lc = re.split(r"(^| )" + n + "( |[!\",.:;?})\]])", s)[0]

                pw = p_last_word.sub("\\1", lc)

                df = df.append([dict(fileName=fn, component=c, 
                                   precedingWord=pw, node=n, 
                                   leftContext=lc, sentence=s)])
            continue

# Reset indices
df.reset_index(drop=True, inplace=True)

# Export dataset
df.to_csv("dataset/py-dataset.csv", sep="\t", encoding="utf-8")

# Let's make a frequency list
# Create new dataframe

# Define neuter and non_neuter
neuter = ["het"]
non_neuter = ["de"]

# Create crosstab
df.loc[df.precedingWord.isin(neuter), "gender"] = "neuter"
df.loc[df.precedingWord.isin(non_neuter), "gender"] = "non_neuter"
df.loc[df.precedingWord.isin(neuter + non_neuter)==0, "gender"] = "rest"

freqDf = pd.crosstab(df.node, df.gender)

freqDf.to_csv("dataset/py-frequencies.csv", sep="\t", encoding="utf-8")

# How long has the script been running?
time_difference = datetime.now() - start_time
print("Time difference of", time_difference)

Убедившись, что выходные данные обоих скриптов идентичны, я решил проверить их.

Я использую 64-разрядную версию Windows 10 с четырехъядерным процессором и оперативной памятью 8 ГБ. Для R я использую RGui 64 bit 3.2.2, а Python работает на версии 3.4.3 (Anaconda) и выполняется в Spyder. Обратите внимание, что я использую Python в 32-битной среде, потому что в будущем я хотел бы использовать модуль nltk, и они не рекомендуют пользователям использовать 64-битную версию.

Я обнаружил, что R закончил примерно через 55 минут. Но Python работает уже два часа подряд, и я вижу в проводнике переменных, что он работает только в business.wr-p-p-g.lst (файлы отсортированы в алфавитном порядке). Это ваааааыыы медленнее!

Итак, я сделал тестовый пример и увидел, как оба сценария работают с гораздо меньшим набором данных. Я взял около 100 файлов (вместо 16 500) и запустил скрипт. Опять же, R был намного быстрее. R закончил примерно за 2 секунды, Python за 17!

Видя, что целью Python было сделать все более гладко, я был озадачен. Я читал, что Python был быстрым (и R довольно медленным), так где я ошибся? В чем проблема? Python медленнее читает файлы и строки или выполняет регулярные выражения? Или R просто лучше приспособлен для работы с информационными фреймами и не может быть побежден пандами? Или мой код просто плохо оптимизирован и должен ли Python действительно быть победителем?

Мой вопрос таков: почему Python медленнее, чем R в этом случае, и - если это возможно - как мы можем улучшить Python до блеска?

Каждый, кто хочет попробовать любой сценарий, может загрузить данные испытаний, которые я использовал здесь. Пожалуйста, дайте мне знать, когда вы загрузили файлы.

1 ответ

Решение

Самое ужасно неэффективное, что вы делаете, это называете DataFrame.append метод в цикле, т.е.

df = pandas.DataFrame(...)
for file in files:
    ...
    for line in file:
        ...
        df = df.append(...)

Структуры данных NumPy разработаны с учетом функционального программирования, поэтому эта операция не предназначена для итеративного императивного режима, поскольку вызов не меняет ваш фрейм данных на месте, но создает новый, что приводит к огромное увеличение времени и сложности памяти. Если вы действительно хотите использовать фреймы данных, накапливайте свои строки в list и передать его DataFrame конструктор, например

pre_df = []
for file in files:
    ...
    for line in file:
        ...
        pre_df.append(processed_line)

df = pandas.DataFrame(pre_df, ...)

Это самый простой способ, поскольку он внесет минимальные изменения в ваш код. Но лучший (и красивый в вычислительном отношении) способ - это выяснить, как лениво генерировать ваш набор данных. Это может быть легко достигнуто путем разделения вашего рабочего процесса на дискретные функции (в смысле стиля функционального программирования) и составления их с использованием ленивых выражений генератора и / или imap, ifilter функции высшего порядка. Затем вы можете использовать полученный генератор для построения вашего фрейма данных, например

df = pandas.DataFrame.from_records(processed_lines_generator, columns=column_names, ...)

Что касается чтения нескольких файлов за один прогон, вы можете прочитать это.

PS

Если у вас есть проблемы с производительностью, вы должны профилировать свой код, прежде чем пытаться оптимизировать его.

Другие вопросы по тегам