web-dev-qa-db-ja.com

固定幅ファイルを読み取るためのより高速な方法

Rに読み込む必要がある多くの固定幅ファイル(つまり、分離文字がない)を使用しています。したがって、通常、文字列を変数に解析するための列幅の定義があります。 _read.fwf_を使用して、問題なくデータを読み取ることができます。ただし、大きなファイルの場合、これにはlong時間がかかる場合があります。最近のデータセットの場合、約500,000行と143変数のデータセットを読み取るのに800秒かかりました。

_seer9 <- read.fwf("~/data/rawdata.txt", 
  widths = cols,
  header = FALSE,
  buffersize = 250000,
  colClasses = "character",
  stringsAsFactors = FALSE))
_

Rの_data.table_パッケージのfreadは、固定幅ファイルを解析しないことを除いて、ほとんどのデータ読み取り問題を解決するのに最適です。ただし、各行を単一の文字列(約500,000行、1列)として読み取ることができます。これには3〜5秒かかります。 (私はdata.tableが大好きです。)

_seer9 <- fread("~/data/rawdata.txt", colClasses = "character",
               sep = "\n", header = FALSE, verbose = TRUE)
_

テキストファイルを解析する方法については、SOに関する良い投稿がいくつかあります。JHowardの提案 here を参照して、開始列と終了列のマトリックスを作成し、substrを参照してください。データを解析します。strsplitを使用するには、GSeeの提案 here を参照してください。このデータを使用してこれを機能させる方法がわかりませんでした(また、Michael Smithは、data.tableメーリングリストで私の能力を超えたsedimplement。 )これで、freadsubstr()を使用して、約25〜30秒ですべてを実行できます。最後には時間がかかります(5秒?)。

_end_col <- cumsum(cols)
start_col <- end_col - cols + 1
start_end <- cbind(start_col, end_col) # matrix of start and end positions
text <- lapply(seer9, function(x) {
        apply(start_end, 1, function(y) substr(x, y[1], y[2])) 
        })
dt <- data.table(text$V1)
setnames(dt, old = 1:ncol(dt), new = seervars)
_

私はこれがこれ以上改善できるかどうか疑問に思っていますか?固定幅のファイルを読み取らなければならないのは私だけではないので、これを高速化できれば、さらに大きなファイル(数百万行)の読み込みも許容できるようになります。 parallelの代わりにmclapplylapplyおよび_data.table_と一緒に使用しようとしましたが、何も変更されませんでした。 (おそらくRの経験が浅いためです。)これを非常に高速に行うためにRcpp関数を作成できると思いますが、それは私のスキルセットを超えています。また、lapplyを使用していない可能性があり、適切に適用します。

私のdata.table実装(magrittrチェーンを使用)は同じ時間を要します。

_text <- seer9[ , apply(start_end, 1, function(y) substr(V1, y[1], y[2]))] %>% 
  data.table(.)
_

誰かがこれの速度を改善するための提案をすることができますか?それともこれはそれが得られるのと同じくらい良いですか?

以下は、実際のデータにリンクするのではなく、R内に同様のdata.tableを作成するコードです。 331文字、500,000行にする必要があります。データ内の欠落フィールドをシミュレートするスペースがありますが、これは[〜#〜]ではありません[〜#〜]スペース区切りデータです。 (誰かが興味がある場合、私は生のSEERデータを読み取っています。)他の人を助ける場合に備えて、列幅(cols)と変数名(seervars)も含めます。これらは、SEERデータの実際の列と変数の定義です。

_seer9 <-
  data.table(rep((paste0(paste0(letters, 1000:1054, " ", collapse = ""), " ")),
                 500000))

cols = c(8,10,1,2,1,1,1,3,4,3,2,2,4,4,1,4,1,4,1,1,1,1,3,2,2,1,2,2,13,2,4,1,1,1,1,3,3,3,2,3,3,3,3,3,3,3,2,2,2,2,1,1,1,1,1,6,6,6,2,1,1,2,1,1,1,1,1,2,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,5,4,10,3,3,2,2,2,3,1,1,1,1,2,2,1,1,2,1,9,5,5,1,1,1,2,2,1,1,1,1,1,1,1,1,2,3,3,3,3,3,3,1,4,1,4,1,1,3,3,3,3,2,2,2,2)
seervars <- c("CASENUM", "REG", "MAR_STAT", "RACE", "Origin", "NHIA", "SEX", "AGE_DX", "YR_BRTH", "PLC_BRTH", "SEQ_NUM", "DATE_mo", "DATE_yr", "SITEO2V", "LATERAL", "HISTO2V", "BEHO2V", "HISTO3V", "BEHO3V", "GRADE", "DX_CONF", "REPT_SRC", "EOD10_SZ", "EOD10_EX", "EOD10_PE", "EOD10_ND", "EOD10_PN", "EOD10_NE", "EOD13", "EOD2", "EOD4", "EODCODE", "TUMOR_1V", "TUMOR_2V", "TUMOR_3V", "CS_SIZE", "CS_EXT", "CS_NODE", "CS_METS", "CS_SSF1", "CS_SSF2", "CS_SSF3", "CS_SSF4", "CS_SSF5", "CS_SSF6", "CS_SSF25", "D_AJCC_T", "D_AJCC_N", "D_AJCC_M", "D_AJCC_S", "D_SSG77", "D_SSG00", "D_AJCC_F", "D_SSG77F", "D_SSG00F", "CSV_ORG", "CSV_DER", "CSV_CUR", "SURGPRIM", "SCOPE", "SURGOTH", "SURGNODE", "RECONST", "NO_SURG", "RADIATN", "RAD_BRN", "RAD_SURG", "SS_SURG", "SRPRIM02", "SCOPE02", "SRGOTH02", "REC_NO", "O_SITAGE", "O_SEQCON", "O_SEQLAT", "O_SURCON", "O_SITTYP", "H_BENIGN", "O_RPTSRC", "O_DFSITE", "O_LEUKDX", "O_SITBEH", "O_EODDT", "O_SITEOD", "O_SITMOR", "TYPEFUP", "AGE_REC", "SITERWHO", "ICDOTO9V", "ICDOT10V", "ICCC3WHO", "ICCC3XWHO", "BEHANAL", "HISTREC", "BRAINREC", "CS0204SCHEMA", "RAC_RECA", "RAC_RECY", "NHIAREC", "HST_STGA", "AJCC_STG", "AJ_3SEER", "SSG77", "SSG2000", "NUMPRIMS", "FIRSTPRM", "STCOUNTY", "ICD_5Dig", "CODKM", "STAT_REC", "IHS", "HIST_SSG_2000", "AYA_RECODE", "LYMPHOMA_RECODE", "DTH_CLASS", "O_DTH_CLASS", "EXTEVAL", "NODEEVAL", "METSEVAL", "INTPRIM", "ERSTATUS", "PRSTATUS", "CSSCHEMA", "CS_SSF8", "CS_SSF10", "CS_SSF11", "CS_SSF13", "CS_SSF15", "CS_SSF16", "VASINV", "SRV_TIME_MON", "SRV_TIME_MON_FLAG", "SRV_TIME_MON_PA", "SRV_TIME_MON_FLAG_PA", "INSREC_PUB", "DAJCC7T", "DAJCC7N", "DAJCC7M", "DAJCC7STG", "ADJTM_6VALUE", "ADJNM_6VALUE", "ADJM_6VALUE", "ADJAJCCSTG")
_

UPDATE:LaFは、生の.txtファイルから7秒未満で読み取り全体を実行しました。たぶんもっと速い方法があるかもしれませんが、私は何かがかなり良くできるとは思えません。素晴らしいパッケージ。

2015年7月27日更新これに対する小さな更新を提供したかっただけです。私は新しいreadrパッケージを使用し、readr :: read_fwfを使用して5秒でファイル全体を読み取ることができました。

_seer9_readr <- read_fwf("path_to_data/COLRECT.TXT",
  col_positions = fwf_widths(cols))
_

また、更新されたstringi :: stri_sub関数は、base :: substr()の少なくとも2倍の速度です。したがって、freadを使用してファイルを読み取る(約4秒)後の各コードの解析に適用される上記のコードでは、143:変数の抽出にstringi :: stri_subを使用した場合、base :: substrの19に対して約8秒かかりました。したがって、freadとstri_subを実行するには、まだ約12秒しかかかりません。悪くない。

_seer9 <-  fread("path_to_data/COLRECT.TXT",     
  colClasses = "character", 
  sep = "\n", 
  header = FALSE)
text <- seer9[ , apply(start_end, 1, function(y) substr(V1, y[1], y[2]))] %>% 
  data.table(.)
_

2015年12月10日更新:

素晴らしいベンチマークとiotoolsパッケージを追加した@ -MichaelChiricoによる 下の回答 も参照してください。

43
Mark Danese

(これと その他の主要な質問 の間で固定幅ファイルの効果的な読み取りについて)そのようなファイルを読み取るためのオファーにはかなりの量のオプションがあるので、いくつかのベンチマークが適切だと思います。

比較のために、次の大容量(400 MB)ファイルを使用します。これは、ランダムに定義されたフィールドと幅を持つランダムな文字の集まりです。

set.seed(21394)
wwidth = 400L
rrows = 1000000

#creating the contents at random
contents = 
  write.table(replicate(rrows, paste0(sample(letters, wwidth, replace = TRUE),
                                      collapse = "")), file="testfwf.txt",
              quote = FALSE, row.names = FALSE, col.names = FALSE)

#defining the fields & writing a dictionary
n_fields = 40L
endpoints = unique(c(1L, sort(sample(wwidth, n_fields - 1L)), wwidth + 1L))
cols = ist(beg = endpoints[-(n_fields + 1L)],
             end = endpoints[-1L] - 1L)

dict = data.frame(column = paste0("V", seq_len(length(endpoints)) - 1L)),
                  start = endpoints[-length(endpoints)] - 1,
                  length = diff(endpoints))

write.csv(dict, file = "testdic.csv", quote = FALSE, row.names = FALSE)

これら2つのスレッド間で言及されている5つのメソッドを比較します(作成者が希望する場合は他のメソッドを追加します)。基本バージョン(read.fwf)、in2csvの結果をfread(@AnandaMahtoの提案)、Hadleyの新しいreadrread_fwf)、LaF/ffbase(@jwijfflsの提案)を使用したもの、および改善された(合理化された)質問作成者(@MarkDanese)が提案したバージョンのfreadstringistri_subを組み合わせたもの。

ベンチマークコードは次のとおりです。

library(data.table)
library(stringi)
library(readr)
library(LaF); library(ffbase)
library(microbenchmark)

microbenchmark(times = 5L,
               utils = read.fwf("testfwf.txt", diff(endpoints), header = FALSE),
               in2csv = 
                 fread(paste("in2csv -f fixed -s",
                             "~/Desktop/testdic.csv",
                             "~/Desktop/testfwf.txt")),
               readr = read_fwf("testfwf.txt", fwf_widths(diff(endpoints))),
               LaF = {
                 my.data.laf = 
                   laf_open_fwf('testfwf.txt', column_widths=diff(endpoints),
                                column_types = rep("character", 
                                                   length(endpoints) - 1L))
                 my.data = laf_to_ffdf(my.data.laf, nrows = rrows)
                 as.data.frame(my.data)},
               fread = fread(
                 "testfwf.txt", header = FALSE, sep = "\n"
                 )[ , lapply(seq_len(length(cols$beg)),
                             function(ii) 
                               stri_sub(V1, cols$beg[ii], cols$end[ii]))])

そして出力:

# Unit: seconds
#    expr       min        lq      mean    median        uq       max neval cld
#   utils 423.76786 465.39212 499.00109 501.87568 543.12382 560.84598     5   c
#  in2csv  67.74065  68.56549  69.60069  70.11774  70.18746  71.39210     5 a  
#   readr  10.57945  11.32205  15.70224  14.89057  19.54617  22.17298     5 a  
#     LaF 207.56267 236.39389 239.45985 237.96155 238.28316 277.09798     5  b 
#   fread  14.42617  15.44693  26.09877  15.76016  20.45481  64.40581     5 a  

したがって、readrfread + stri_subは、最速でかなり競争力があるようです。ビルトインread.fwfは明らかに敗者です。

ここでのreadrの本当の利点は、列タイプを事前に指定できることです。 freadを使用すると、後でconvertと入力する必要があります。

編集:いくつかの選択肢を追加する

@AnandaMahtoの提案では、新しい勝者のように見えるものを含め、いくつかのオプションを追加しています。時間を節約するために、新しい比較で上記の最も遅いオプションを除外しました。新しいコードは次のとおりです。

library(iotools)

microbenchmark(times = 5L,
               readr = read_fwf("testfwf.txt", fwf_widths(diff(endpoints))),
               fread = fread(
                 "testfwf.txt", header = FALSE, sep = "\n"
                 )[ , lapply(seq_len(length(cols$beg)),
                             function(ii) 
                               stri_sub(V1, cols$beg[ii], cols$end[ii]))],
               iotools = input.file("testfwf.txt", formatter = dstrfw, 
                                    col_types = rep("character",
                                                    length(endpoints) - 1L), 
                                    widths = diff(endpoints)),
               awk = fread(paste(
                 "awk -v FIELDWIDTHS='", 
                 paste(diff(endpoints), collapse = " "), 
                 "' -v OFS=', ' '{$1=$1 \"\"; print}' < ~/Desktop/testfwf.txt", 
                 collapse = " "), header = FALSE))

そして新しい出力:

# Unit: seconds
#     expr       min        lq      mean    median        uq       max neval cld
#    readr  7.892527  8.016857 10.293371  9.527409  9.807145 16.222916     5  a 
#    fread  9.652377  9.696135  9.796438  9.712686  9.807830 10.113160     5  a 
#  iotools  5.900362  7.591847  7.438049  7.799729  7.845727  8.052579     5  a 
#      awk 14.440489 14.457329 14.637879 14.472836 14.666587 15.152156     5   b

したがって、iotoolsは非常に高速で非常に一貫しているようです。

31
MichaelChirico

LaFパッケージを使用できます。これは、固定幅の大きなファイルを処理するために作成されています(これもメモリに収まりきらない)。これを使用するには、最初にlaf_open_fwfを使用してファイルを開く必要があります。その後、必要なデータを読み取る通常のデータフレームと同じように、結果のオブジェクトにインデックスを付けることができます。以下の例では、ファイル全体を読み取りますが、特定の列や行を読み取ることもできます。

library(LaF)
laf <- laf_open_fwf("foo.dat", column_widths = cols, 
  column_types=rep("character", length(cols)),
  column_names = seervars)
seer9 <- laf[,]

(500,000ではなく)5000行を使用した例では、read.fwfを使用して28秒、LaFを使用して1.6秒かかりました。

追加(500,000ではなく)50,000行を使用する例では、read.fwfを使用すると258秒、LaFを使用すると7秒かかりました。

30

あなたが使っているOSはわかりませんが、これはLinuxで私にはかなり簡単に機能しました:

ステップ1awkのコマンドを作成して、ファイルをcsvに変換します

他のソフトウェアでもデータを使用する場合は、実際のcsvファイルに保存することができます。

myCommand <- paste(
  "awk -v FIELDWIDTHS='", 
  paste(cols, collapse = " "), 
  "' -v OFS=',' '{$1=$1 \"\"; print}' < ~/rawdata.txt", 
  collapse = " ")

ステップ2:作成したコマンドでfreadを直接使用します

seer9 <- fread(myCommand)

私は明らかにあなたとJanよりも遅いシステムを使用しているので、これを計時していません:-)

昨日、この種のパーサーを作成しましたが、これはヘッダーファイルへの非常に特定の種類の入力用だったので、列幅を使用できるようにフォーマットする方法を示します。

フラットファイルをcsvに変換する

最初にダウンロード 問題のツール

OS X Mavericks(私がコンパイルした場所)を使用している場合は、binディレクトリからバイナリをダウンロードするか、srcに移動してclang++ csv_iterator.cpp parse.cpp main.cpp -o flatfileparserを使用してコンパイルできます。

フラットファイルパーサーには2つのファイルが必要です。CSVヘッダーファイルでは、5つおきの要素で変数の幅が指定されています(これも、非常に具体的なアプリケーションが原因です)。

cols = c(8,10,1,2,1,1,1,3,4,3,2,2,4,4,1,4,1,4,1,1,1,1,3,2,2,1,2,2,13,2,4,1,1,1,1,3,3,3,2,3,3,3,3,3,3,3,2,2,2,2,1,1,1,1,1,6,6,6,2,1,1,2,1,1,1,1,1,2,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,5,4,10,3,3,2,2,2,3,1,1,1,1,2,2,1,1,2,1,9,5,5,1,1,1,2,2,1,1,1,1,1,1,1,1,2,3,3,3,3,3,3,1,4,1,4,1,1,3,3,3,3,2,2,2,2)
writeLines(sapply(c(-1, cols), function(x) paste0(',,,,', x)), '~/tmp/header.csv')

結果の~/tmp/header.csvflatfileparserと同じディレクトリにコピーします。フラットファイルも同じディレクトリに移動すると、フラットファイルで実行できます。

./flatfileparser header.csv yourflatfile

yourflatfile.csvが生成されます。パイピングを使用して、上記のヘッダーを手動で追加します(Bashの>>)。

CSVファイルをすばやく読み取る

Hadleyの実験的な fastreadパッケージ を使用して、ファイル名をfastread::read_csvに渡すと、data.frameが生成されます。私は彼がfwfファイルをサポートしているとはまだ信じていません。

2