Извлечение строк из файла PDF с нерегулярным интервалом в аккуратный R-кадр данных - PullRequest
0 голосов
/ 16 мая 2018

Я изо всех сил пытаюсь самообучиться процессу преобразования нерегулярно расположенной таблицы PDF в аккуратный массив данных в R. Моя цель - извлечь данные о населении из самой последней пакистанской переписи, которая в настоящее время распределена по 137 отдельным PDF-файлам. , Здесь - пример целевого файла. Мне удалось собрать воедино некоторые шаги, необходимые из других руководств, чтобы разбить pdf на текстовые строки, но я запутался в регулярных выражениях, которые, я думаю, будут необходимы для дальнейшего преобразования текста в фрейм данных.

Шаги, которые я смог выяснить до сих пор:

# import file
district_import <- pdf_text("http://www.pbscensus.gov.pk/sites/default/files/bwpsr/kp/ABBOTTABAD_BLOCKWISE.pdf")

# convert text to string
data <- toString(district_import)

# convert text to character lines
data <- read_lines(data)

# clean up page headers and footers
header_row_1 <- grep("POPULATION AND HOUSEHOLD DETAIL FROM BLOCK TO DISTRICT LEVEL", data)
header_row_2 <- grep("KHYBER PAKHTUNKHWA", data)
header_row_3 <- grep("ADMIN UNIT", data)
footer_row <- grep("Page ", data)

data <- data[- c(header_row_1, header_row_2, header_row_3, footer_row)]

На этом этапе я могу произвести следующее:

> head(data, 15)
 [1] "ABBOTTABAD DISTRICT                                              1,332,912      216,534"
 [2] "     ABBOTTABAD TEHSIL                                             981,590      161,445"
 [3] "           ABBOTTABAD CANTONMENT                                   138,311        21183"
 [4] "                        CHARGE NO 01                              138,311         21183"
 [5] "                              CIRCLE NO 01                         12,150          1847"
 [6] "                                     023010101                      5,131           705"
 [7] "                                     023010102                      2,654           435"
 [8] "                                     023010103                      1,004           173"
 [9] "                                     023010104                      2,216           349"
[10] "                                     023010105                         94            14"
[11] "                                     023010106                      1,051           171"
[12] "                              CIRCLE NO 02                         15,383          2435"
[13] "                                     023010201                      1,352           211"
[14] "                                     023010202                      1,019           161"
[15] "                                     023010203                      4,079           691"

(Обратите внимание, что хотя это выглядит как таковое через точку отсечения, длина начальных пробелов не является одинаковой по всему документу для различных административных округов округа, и я ожидаю, что она не будет согласованной в 137 округах что в конечном итоге я буду стремиться к тому, чтобы объединиться и объединиться в единый общенациональный фрейм данных.)

Мой желаемый результат с этого момента - преобразовать его в аккуратный кадр данных по следующим строкам с блоком переписи (шестизначными кодами, не обозначенными как таковые в оригинальном pdf-файле) в качестве базовой единицы организации. :

             district         sub_lvl01             sub_lvl02    sub_lvl03    sub_lvl04 census_block population household
                <chr>             <chr>                 <chr>        <chr>        <chr>        <chr>      <chr>     <chr>
1 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01 CIRCLE NO 01    023010101      5,131       705
2 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01 CIRCLE NO 01    023010102      2,654       435
3 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01 CIRCLE NO 01    023010103      1,004       173
4 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01 CIRCLE NO 01    023010104      2,216       349
5 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01 CIRCLE NO 01    023010105         94        14
6 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01 CIRCLE NO 01    023010106      1,051       171
... etc 

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

Играя на regex101.com, я думал, что этот код, по крайней мере, позволит мне извлечь данные о населении и домохозяйстве:

 pop_hh_str <- str_match_all(data, "(?!\\d{6})(?<=\\s)\\d*[,.]*\\d*[,.]*\\d*")

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

Буду очень признателен за любые рекомендации о том, как решить эту проблему!

Ответы [ 2 ]

0 голосов
/ 16 мая 2018

DATA

(поскольку я не хочу устанавливать pdftools, я воссоздаю ваши данные вручную):

data <- c("ABBOTTABAD DISTRICT                                              1,332,912      216,534", "     ABBOTTABAD TEHSIL                                             981,590      161,445", "           ABBOTTABAD CANTONMENT                                   138,311        21183", "                        CHARGE NO 01                              138,311         21183", "                              CIRCLE NO 01                         12,150          1847", "                                     023010101                      5,131           705", "                                     023010102                      2,654           435", "                                     023010103                      1,004           173", "                                     023010104                      2,216           349", "                                     023010105                         94            14", "                                     023010106                      1,051           171", "                              CIRCLE NO 02                         15,383          2435", "                                     023010201                      1,352           211", "                                     023010202                      1,019           161", "                                     023010203                      4,079           691")
# data is now identical to what you showed as 15 lines of your `data`

ОБРАБОТКА: Разделение строк по пробелам

Обычно в таких случаях это будет сделано:

strsplit(data, "\\s+") # "\\s+" meaning: 1 or more white spaces

Но в этом случае символы могут иметь 1 пробел между словами, поэтому мы хотим more than 1 white spaces, то есть "\\s{2,}" (как минимум два слова) в качестве разделителей столбцов. Во-вторых, существуют пробелы перед / / после данных, иногда. Таким образом, мы предварительно очищаем пробелы в начале / конце строки trimws()

Таким образом:

strsplit(trimws(data), "\\s{2,}")

Затем мы можем связать эти значения строка за строкой, используя Reduce()

df <- Reduce(rbind, strsplit(trimws(data), "\\s{2,}"))
rownames(df) <- 1:dim(df)[1] # just give at least numbers as rownames
df <- as.data.frame(df)

выход:

   [,1]                    [,2]        [,3]     
1  "ABBOTTABAD DISTRICT"   "1,332,912" "216,534"
2  "ABBOTTABAD TEHSIL"     "981,590"   "161,445"
3  "ABBOTTABAD CANTONMENT" "138,311"   "21183"  
4  "CHARGE NO 01"          "138,311"   "21183"  
5  "CIRCLE NO 01"          "12,150"    "1847"   
6  "023010101"             "5,131"     "705"    
7  "023010102"             "2,654"     "435"    
8  "023010103"             "1,004"     "173"    
9  "023010104"             "2,216"     "349"    
10 "023010105"             "94"        "14"     
11 "023010106"             "1,051"     "171"    
12 "CIRCLE NO 02"          "15,383"    "2435"   
13 "023010201"             "1,352"     "211"    
14 "023010202"             "1,019"     "161"    
15 "023010203"             "4,079"     "691" 

С этого момента вам нужно будет создавать вспомогательные столбцы, в которых есть счетчики, в строке которых появился тип информации .... Такой подсчет поможет вам разделить фрейм данных на суб-фреймы. split() будет очень полезно ...

Я написал несколько функций, которые могут быть полезны для классификации «уровня» строки в data vec путем подсчета, имеет ли оно более k пробелов в начале или нет.

not.more.than.k.leading.whitespaces <- function(s, k) {
  !grepl(paste0("^\\s{", k, ",}"), s)
}

leveler <- function(s, k) {
  cumsum(not.more.than.k.leading.whitespaces(s, k))
}

Я бы использовал их так:

df$level0 <- leveler(data, 0)
df$level1 <- leveler(data, 5)
df$level2 <- leveler(data, 11)
df$level3 <- leveler(data, 24)
df$level4 <- leveler(data, 37)

# important helper function:
annotate.by.first.row <- function(df, col, col.title) {
  # take first row's column content and add it to the df as a column content
  info <- df[1, col]
  rowsn <- dim(df)[1]
  df.new <- df[2:rowsn, ]
  df.new[, col.title] <- info
  df.new
}

# split data frame to a list of sub data frames
df.l0 <- split(df, df$level0)
# apply our helper function for annotation column generation
# using the information of the first row of the sub data frames
df.a0.l <- lapply(df.l0, annotate.by.first.row, 1, "district")

# cycle through: split, flatten, annotate.by.first.row
# to add next first row information as a column
df.s1.ll <- lapply(df.a0.l, function(df) split(df, df$level1))
df.s1.l <- unlist(df.s1.ll, recursive = FALSE)
df.a1.l <- lapply(df.s1.l, annotate.by.first.row, 1, "thesil")

# repeat the cycles ...
df.s2.ll <- lapply(df.a1.l, function(df) split(df, df$level2))
df.s2.l <- unlist(df.s2.ll, recursive = FALSE)
df.a2.l <- lapply(df.s2.l, annotate.by.first.row, 1, "cantonment")

df.s3.ll <- lapply(df.a2.l, function(df) split(df, df$level3))
df.s3.l <- unlist(df.s3.ll, recursive = FALSE)
df.a3.l <- lapply(df.s3.l, annotate.by.first.row, 1, "charge")

df.s4.ll <- lapply(df.a3.l, function(df) split(df, df$level4))
df.s4.l <- unlist(df.s4.ll, recursive = FALSE)
df.a4.l <- lapply(df.s4.l, annotate.by.first.row, 1, "circle")

# fuse subdata frames by `Reduce(rbind, ...)`
res.df <- Reduce(rbind, df.a4.l)
res.cleaned.df <- res.df[, c("district", "thesil", "cantonment", "charge", "circle", "V1", "V2", "V3")]

Посредством таких последовательных шагов разделения, выравнивания, аннотирования в первом ряду вы можете попасть туда, куда хотите.

> res.cleaned.df
#               district            thesil            cantonment       charge
# 6  ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 7  ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 8  ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 9  ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 10 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 11 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 13 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 14 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
# 15 ABBOTTABAD DISTRICT ABBOTTABAD TEHSIL ABBOTTABAD CANTONMENT CHARGE NO 01
#          circle        V1    V2  V3
# 6  CIRCLE NO 01 023010101 5,131 705
# 7  CIRCLE NO 01 023010102 2,654 435
# 8  CIRCLE NO 01 023010103 1,004 173
# 9  CIRCLE NO 01 023010104 2,216 349
# 10 CIRCLE NO 01 023010105    94  14
# 11 CIRCLE NO 01 023010106 1,051 171
# 13 CIRCLE NO 02 023010201 1,352 211
# 14 CIRCLE NO 02 023010202 1,019 161
# 15 CIRCLE NO 02 023010203 4,079 691

Чтобы сделать это немного более компактно и регулярно:

# abstract over the split-flatten-annotate cycle/pattern by:
spl.fl.annotate <- function(df.a.l, col, col.name) {
  df.sN.ll <- lapply(df.a.l, function(df) split(df, df[, col]))
  df.sN.l  <- unlist(df.sN.ll, recursive = FALSE)
  lapply(df.sN.l, annotate.by.first.row, 1, col.name)
}

# now the cycles can be written as:
df.a0.l <- spl.fl.annotate(list(`0` = df), "level0", "district")
df.a1.l <- spl.fl.annotate(df.a0.l, "level1", "thesil")
df.a2.l <- spl.fl.annotate(df.a1.l, "level2", "cantonment")
df.a3.l <- spl.fl.annotate(df.a2.l, "level3", "charge")
df.a4.l <- spl.fl.annotate(df.a3.l, "level4", "circle")

# fuse subdata frames by `Reduce(rbind, ...)`
res.df <- Reduce(rbind, df.a4.l)
res.cleaned.df <- res.df[, c("district", "thesil", "cantonment", "charge", "circle", "V1", "V2", "V3")]
0 голосов
/ 16 мая 2018

Я могу помочь вам с небольшим количеством кода для передачи census_block в data.frame. Если вы можете получить справочную таблицу для блоков переписи, вы можете добавить остальные данные.

Продолжая ваш вектор данных:

library(stringr)

# find the rows which have 9 digits + a space
data1 <- data[which(str_detect(data, "\\d{9} "))]
# remove spaces in front of the line
data1 <- str_remove(data1, " +")
# replace all other spaces with 1 space
data1 <- str_replace_all(data1, " +", " ")

# create data.frame and split the value column into 3 with new headers.
library(tidyr)
library(dplyr)
df <- data1  %>% 
  as_data_frame() %>% 
  separate(value ,into = c("census_block", "population", "household"), sep = " ")
df  
# A tibble: 1,106 x 3
   census_block population household
   <chr>        <chr>      <chr>    
 1 023010101    5,131      705      
 2 023010102    2,654      435      
 3 023010103    1,004      173      
 4 023010104    2,216      349      
 5 023010105    94         14       
 6 023010106    1,051      171      
 7 023010201    1,352      211      
 8 023010202    1,019      161      
 9 023010203    4,079      691      
10 023010204    2,171      345      
# ... with 1,096 more rows
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...