Code
library(pdftools)
library(tidyverse)
library(stringr)
library(tidytext)
library(jiebaR)
Rui
May 12, 2023
以企业名称划分:
查看:
raw_data[1:2]
## [1] "华汇工程设计集团股份有限公司(913306007210107199), 33069615, 盛红美, 0575-88208121没有警告提示信息。 "
## [2] "浙江工越信息科技有限公司(913306027731278557), 33069606, 陶亚芳, 136267592631.警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05_0)的3%,本年应付职工薪酬(QC51)与营业收入(QC05_0)的比例过高或过低。请确认本年应付职工薪酬(QC51)和营业收入(QC05_0)填报是否正确!若事实如此,请予以说明。数据:本年应付职工薪酬(本年贷方累计发生额)(qc51):859 千元,营业收入(qc05_0):2730 千元说明:因研发人员工资提高,因此数据为营业收入的31%。2.警告:“平衡关系508”中,“企业核心技术所属《国家重点支持的高新技术领域》(qb16_1)”与上年数据不同,请对变化原因予以说明。数据:企业核心技术所属国家重点支持的高新技术领域(qb16_1):050303 ,pre2021qb16_1:010107说明:按照企业核心技术所属国家重点支持的高新技术领域更新填报 "
删除第一个字符串到“.”之间的所有字符串:
raw_data[1:2]
## [1] "华汇工程设计集团股份有限公司(913306007210107199), 33069615, 盛红美, 0575-88208121没有警告提示信息。 "
## [2] "警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05_0)的3%,本年应付职工薪酬(QC51)与营业收入(QC05_0)的比例过高或过低。请确认本年应付职工薪酬(QC51)和营业收入(QC05_0)填报是否正确!若事实如此,请予以说明。数据:本年应付职工薪酬(本年贷方累计发生额)(qc51):859 千元,营业收入(qc05_0):2730 千元说明:因研发人员工资提高,因此数据为营业收入的31%。2.警告:“平衡关系508”中,“企业核心技术所属《国家重点支持的高新技术领域》(qb16_1)”与上年数据不同,请对变化原因予以说明。数据:企业核心技术所属国家重点支持的高新技术领域(qb16_1):050303 ,pre2021qb16_1:010107说明:按照企业核心技术所属国家重点支持的高新技术领域更新填报 "
777家企业中,有部分企业没有警告;有些企业不止一个警告。要将带有“没有警告提示信息”的字符串剔除出去。
保险起见,再将包含“警告”或“错误”的字符串提取出来:
包含“错误”的记录有几条?
将诸如“2.警告”等字符替换为“警告”;将诸如“2.错误”等字符串替换为“错误”:
对于“警告”:
警告与错误 | 出现次数 |
---|---|
警告:“第3步 (二)经济概况”中,正常情况下,营业利润(QC120)应该为正数!如为负值请附上原因说明。 | 293 |
警告:“第3步 (二)经济概况”中,一般情况下,利润总额应大于或等于0,如果为负数,请予以说明。 | 248 |
警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05_0)的3%,本年应付职工薪酬(QC51)与营业收入(QC05_0)的比例过高或过低。请确认本年应付职工薪酬(QC51)和营业收入(QC05_0)填报是否正确!若事实如此,请予以说明。 | 187 |
剔除“说明:”冗余词汇,因为列名已经包含该信息了:
jiebaR
进行分词按照警告一列对说明进行汇总:
创建 tokens 列表:
合并 tokens 列表为数据框:
tidytext
计算 TF-IDF关于 TF-IDF 的相关解释可以参见这篇博客:http://www.ruanyifeng.com/blog/2013/03/tf-idf.html。写得非常棒!
book | word | n | total |
---|---|---|---|
警告:“第3步 (二)经济概况”中,正常情况下,营业利润(QC120)应该为正数!如为负值请附上原因说明。 | 公司 | 154 | 2795 |
警告:“第3步 (二)经济概况”中,一般情况下,利润总额应大于或等于0,如果为负数,请予以说明。 | 公司 | 136 | 2407 |
警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05_0)的3%,本年应付职工薪酬(QC51)与营业收入(QC05_0)的比例过高或过低。请确认本年应付职工薪酬(QC51)和营业收入(QC05_0)填报是否正确!若事实如此,请予以说明。 | 公司 | 105 | 2050 |
book | word | n | total | tf | idf | tf_idf |
---|---|---|---|---|---|---|
警告:“第3步 (二)经济概况”中,正常情况下,营业利润(QC120)应该为正数!如为负值请附上原因说明。 | 公司 | 154 | 2795 | 0.0550984 | 0.3586338 | 0.0197601 |
警告:“第3步 (二)经济概况”中,一般情况下,利润总额应大于或等于0,如果为负数,请予以说明。 | 公司 | 136 | 2407 | 0.0565019 | 0.3586338 | 0.0202635 |
警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05_0)的3%,本年应付职工薪酬(QC51)与营业收入(QC05_0)的比例过高或过低。请确认本年应付职工薪酬(QC51)和营业收入(QC05_0)填报是否正确!若事实如此,请予以说明。 | 公司 | 105 | 2050 | 0.0512195 | 0.3586338 | 0.0183690 |
book | word | n | tf_idf | book_freq |
---|---|---|---|---|
错误:“第4步 (二)经济概况续”中,“其中:本年折旧(qc61)”必须大于或等于0(不能为负数)。 | 处置 | 1 | 0.8329000 | 1 |
错误:“第4步 (二)经济概况续”中,“其中:本年折旧(qc61)”必须大于或等于0(不能为负数)。 | 固定资产 | 1 | 0.7370060 | 1 |
错误:“第4步 (二)经济概况续”中,“其中:本年折旧(qc61)”必须大于或等于0(不能为负数)。 | 年 | 1 | 0.1764198 | 1 |
整理汇总:
检查一下警告种类:
main_shuoming <- list()
i <- 1
for (j in uni_jinggao) {
str <- top5 %>% filter(book == j) %>% "$"(word) %>% paste(collapse = "|")
zongjie <- raw_text %>% filter(警告 == j) %>% "$"(说明)
idx <- zongjie %>% str_detect(str)
zongjie <- zongjie[idx]
temp <- data.frame("警告" = rep(j, length(zongjie)), "主要说明" = zongjie)
main_shuoming[[i]] <- temp
i <- i + 1
}
列表转换为数据框:
main_shuoming <- main_shuoming %>% purrr::map_dfr(~.)
str(main_shuoming)
## 'data.frame': 1415 obs. of 2 variables:
## $ 警告 : chr "警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05"| __truncated__ "警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05"| __truncated__ "警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05"| __truncated__ "警告:“平衡关系138”中,本年应付职工薪酬(QC51)超过营业收入(QC05_0)的30%或本年应付职工薪酬(QC51)小于营业收入(QC05"| __truncated__ ...
## $ 主要说明: chr "因研发人员工资提高,因此数据为营业收入的31%。" "人员成本上涨,本企业主要为人员成本" "我公司属于软件开发行业,主要支出用于员工工资 " "销售少,经营收入少。员工工资照常发放 " ...
警告 | 警告出现频率 | 说明关键词 | 主要说明 |
---|---|---|---|
警告:“平衡关系104”中,其他收益(QC232)超过1亿元,请确认是否为录入错误!若事实如此,请予以说明。 | 1 | 额外;补贴;收益;政府;我司 | 因我司有政府补贴等额外收益 |
警告:“平衡关系108”中,请确认净利润(QC12)是否漏填,如果确实为0,则请按实际情况予以说明。 | 1 | 不好;盈利;销售;去年;单位 | 我单位去年销售不好,没有盈利。 |
警告:“平衡关系110”中,净利润(QC12)超过营业收入(QC05_0)的30%,请确认净利润(QC12)和营业收入(QC05_0)填报是否正确!若事实如此,请予以说明。 | 31 | 净利润;利润;营业;收入;较高 | 公司产品市场收益不错,同时有部分营业外收入,净利润相对客观,情况属实。 |
---
title: "使用TF-IDF提取警告说明关键词"
author: "Rui"
date: "2023-05-12"
categories: [R, jiebaR, tidytext]
image: "TulpenMiesbach.jpg"
format:
html:
code-fold: true
code-tools: true
---
```{r setup, include = FALSE}
# 设置默认参数
knitr::opts_chunk$set(
echo = TRUE,
fig.align = "center",
message = FALSE,
warning = FALSE,
collapse = TRUE
)
```
```{=html}
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=1352749002&auto=1&height=66"></iframe>
```
![](TulpenMiesbach.jpg)
## 读取 pdf 文件
```{r}
library(pdftools)
library(tidyverse)
library(stringr)
library(tidytext)
library(jiebaR)
```
```{r}
# 读取PDF文件并将其拆分成行
raw_data <- pdf_text("text/警告汇总.pdf") %>% paste(collapse = "")
```
## 文本划分
以企业名称划分:
```{r}
raw_data <- raw_data %>%
str_split("\\d+\\s++\\s") %>%
unlist()
```
```{r}
raw_data <- raw_data[-1] # 正好是777家企业
raw_data <- raw_data %>% str_remove_all("\n")
```
查看:
```{r}
raw_data[1:2]
```
删除第一个字符串到“.”之间的所有字符串:
```{r}
raw_data <- raw_data %>% str_remove_all("^[^.]*\\.")
```
```{r}
raw_data[1:2]
```
777家企业中,有部分企业没有警告;有些企业不止一个警告。要将带有“没有警告提示信息”的字符串剔除出去。
```{r}
raw_data <- raw_data[!str_detect(raw_data, "没有警告提示信息")]
```
保险起见,再将包含“警告”或“错误”的字符串提取出来:
```{r}
raw_data <- raw_data[str_detect(raw_data, "(警告:)|(错误:)")]
```
包含“错误”的记录有几条?
```{r}
raw_data %>% str_count("错误:") %>% sum()
```
将诸如“2.警告”等字符替换为“警告”;将诸如“2.错误”等字符串替换为“错误”:
```{r}
raw_data <- raw_data %>% str_replace_all("\\d+\\.+警告", "警告")
raw_data <- raw_data %>% str_replace_all("\\d+\\.+错误", "错误")
```
```{r}
data <- raw_data %>%
paste(collapse = "") %>%
str_split("(?=警告:)|(?=错误:)|(?=数据:)|(?=说明:)") %>% # ?=..用于防止去除分隔符
unlist()
```
```{r}
data <- data[data != ""]
```
## 提取警告和说明
```{r}
jinggao <- data[str_detect(data, "(警告:)|(错误:)")]
shuju <- data[str_detect(data, "数据:")]
shuoming <- data[str_detect(data, "说明:")]
```
对于“警告”:
```{r}
uni_jinggao <- unique(jinggao)
num_jinggao <- jinggao %>% table() %>% as.data.frame()
colnames(num_jinggao) <- c("警告与错误", "出现次数")
num_jinggao <- num_jinggao %>% arrange(desc(出现次数))
```
```{r}
num_jinggao %>% head(3) %>% knitr::kable()
```
```{r, eval=FALSE}
write_excel_csv(num_jinggao, "output/警告类型汇总.csv")
```
## 警告与说明合并
:::{.callout-note}
要注意:一种警告往往对应着多种说明。可以使用 `group_by` 对警告进行汇总,每种警告对应的多个说明合并起来。这样,每种警告就像是文章标题,对应的说明合集就像是文章的内容。然后可以计算 TF-IDF 来提取“每篇文章”的特征(关键词)。
:::
```{r}
raw_text <- data.frame("警告" = jinggao, "说明" = shuoming)
```
剔除“说明:”冗余词汇,因为列名已经包含该信息了:
```{r}
raw_text <- raw_text %>%
mutate(说明 = str_remove(说明, "说明:"))
```
## 使用 `jiebaR` 进行分词
按照警告一列对说明进行汇总:
```{r}
text <- raw_text %>%
group_by(警告) %>%
summarise(说明汇总 = paste(说明, collapse = ";")) %>%
ungroup()
```
### 加载分词引擎,设置停用词
```{r}
# 加载分词引擎,并配置停止词过滤
work <- worker(stop_word = "text/cn_stopwords.txt")
```
### 对于单个“文章”
```{r}
text$说明汇总[1] %>% segment(work)
```
### 对所有“文章”
创建 tokens 列表:
```{r}
token_list <- list()
for (i in 1:nrow(text)) {
tok <- text$说明汇总[i] %>%
str_replace_all("[0-9, a-z, A-Z]", "") %>% # 去除阿拉伯数字、英文字母
segment(work)
temp <- data.frame("book" = rep(text$警告[i], length(tok)),
"word" = tok)
token_list[[i]] <- temp
}
```
合并 tokens 列表为数据框:
```{r}
tokens_df <- token_list %>% purrr::map_dfr(~.)
```
## 使用 `tidytext` 计算 TF-IDF
关于 TF-IDF 的相关解释可以参见这篇博客:<http://www.ruanyifeng.com/blog/2013/03/tf-idf.html>。写得非常棒!
:::{.callout-warning}
TF-IDF 有一个缺点:容易把一些生僻词、人名、地名当作文章的关键词。从本篇文章的结果多少可以看出这个缺点。处理方法是在分词时将这些词剔除,或者在提取关键词后进行人工检查。
:::
```{r}
tokens_df <- tokens_df %>%
count(book, word, sort = TRUE)
```
```{r}
total_words <- tokens_df %>%
group_by(book) %>%
summarize(total = sum(n))
```
```{r}
tokens_df <- tokens_df %>%
left_join(total_words)
```
```{r}
tokens_df %>% head(3) %>% knitr::kable()
```
```{r}
tokens_df <- tokens_df %>%
bind_tf_idf(word, book, n)
tokens_df %>% head(3) %>% knitr::kable()
```
```{r}
TF_IDF <- tokens_df %>%
select(book, word, n, tf_idf) %>%
arrange(desc(book), desc(tf_idf))
```
:::{.callout-note}
注意 `desc()` 只接受一个列名,想要按照多列进行降序排序,应该为 `arrange(desc(book), desc(tf_idf))`,而不是 `arrange(desc(book, tf_idf))`
:::
```{r}
TF_IDF <- TF_IDF %>%
left_join(num_jinggao, by = c("book" = "警告与错误")) %>%
rename("book_freq" = "出现次数")
TF_IDF %>% head(3) %>% knitr::kable()
```
整理汇总:
```{r, eval=FALSE}
TF_IDF %>% select(-tf_idf) %>%
group_by(book) %>%
summarise(word = paste(word, collapse = ";")) %>%
ungroup() %>%
rename("警告" = "book", "说明关键词" = "word") %>%
write_excel_csv("output/警告说明关键词提取结果.csv")
```
## 筛选出 TF-IDF 较高的 tokens
```{r}
top5 <- TF_IDF %>%
group_by(book) %>%
arrange(desc(tf_idf)) %>%
slice_head(n = 5)
```
检查一下警告种类:
```{r}
top5$book %>% unique() %>% length()
```
```{r}
main_shuoming <- list()
i <- 1
for (j in uni_jinggao) {
str <- top5 %>% filter(book == j) %>% "$"(word) %>% paste(collapse = "|")
zongjie <- raw_text %>% filter(警告 == j) %>% "$"(说明)
idx <- zongjie %>% str_detect(str)
zongjie <- zongjie[idx]
temp <- data.frame("警告" = rep(j, length(zongjie)), "主要说明" = zongjie)
main_shuoming[[i]] <- temp
i <- i + 1
}
```
列表转换为数据框:
```{r}
main_shuoming <- main_shuoming %>% purrr::map_dfr(~.)
str(main_shuoming)
```
```{r}
out <- top5 %>%
group_by(book) %>%
summarise(说明关键词 = paste(word, collapse = ";"),
警告出现频率 = as.integer(mean(book_freq))) %>%
ungroup() %>%
rename("警告" = "book") %>%
right_join(main_shuoming, by = "警告") %>%
select(警告, 警告出现频率, everything())
out %>% head(3) %>% knitr::kable()
```