某小区业主群QQ聊天记录,时间跨度将近一年半,历经从业委会选举到换物业公司全过程,应该还是比较有代表性和戏剧性的一段时期。
聊天记录通过QQ消息面板导出为文本格式。
1、读取并整理数据
> library(pacman)
> p_load(dplyr, stringr)
>
> dt <- data.table::fread("./data_set/业委会工作群.txt",
> sep="\n",encoding = "UTF-8",
> header = F,blank.lines.skip = T)
> head(dt)
## V1
## 1: 2018-08-24 11:32:44 (2643202289)
## 2: 一户一票勺11人
## 3: 2018-08-24 11:33:34 (576284020)
## 4: 这个最好是 到时候 各个 候选委员上台 当面来个简短的介绍,比较好,了解比较直观\r
## 然后在投票 是不是好一点\r光看图 眼下几位 都还是不错的,盲目的投也不大好吧
## 5: 2018-08-24 11:35:09 (2643202289)
## 6: @5-1-1504 邓 我刚说了本周日上午开大会现场,他们会上台
> # 因为QQ群分享文件也会有时间戳信息,所以不能用时间来标记。使用日期标记
> # 提取日期时间用户名信息到向量a中
> ptn <- "[2019]{4}+-+[0-9]{2}+-+[0-9]{2}"
> a <- str_subset(dt$V1, ptn)
>
> # 用'-----'符号替换a
> dt$V1[str_detect(dt$V1, ptn)] <- "-----"
> # 拼接所有字符串,用'-----'符号切分
> b <- paste0(dt$V1, collapse = "") %>%
> str_split("-----") %>% unlist()
>
> length(a)
## [1] 13744
> # 需要去掉开头的空行
> length(b)
## [1] 13745
> df <- tibble(a = a, txt = b[2:13745])
>
> # 将a列拆分为date、time、user
> df <- tidyr::separate(df, a, into = c("date", "time", "user"), sep = " ")
>
> # 将date转换为日期型
> df$date <- as.Date(df$date)
> head(df)
## # A tibble: 6 x 4
## date time user txt
##
## 1 2019-01-01 15:32:~ 12-1-2804怡日健(250~ 不交物业费也可以交停车费
## 2 2019-01-01 15:42:~ 7-2-502(50356990~ @22-2-2902李 现在最好一个月一交,管委会要求我们年后才能选聘新的物业~
## 3 2019-01-01 16:33:~ 22-2-2902李(67596~ 谢谢各位的解答[表情]
## 4 2019-01-01 16:54:~ 12-1-aa02(455351~ 有没有废品回收的电话?
## 5 2019-01-01 17:38:~ 22-1-801杨(404454~ 各位,物业管理费怎么交啊?
## 6 2019-01-01 17:39:~ 10/1/2604 暂时别交,年后换物业公司了再交
> # 去掉user为“系统消息”的行
> df <- df %>% filter(!str_detect(user,"系统消息"))
>
> df[sample(nrow(df), 5), ]
## # A tibble: 5 x 4
## date time user txt
##
## 1 2019-05-21 12:23:46 12-1-2怡日健(250614780) @马革裹尸 @宣传员
## 2 2019-06-11 20:48:28 22-2-204(951781731) [图片]
## 3 2019-02-27 20:38:28 21-2-2300(345353385) 这么看来估计比康城物业还差!
## 4 2019-04-15 14:27:43 22栋1-1001(609385192) 记者采访 在家的业主做下向导 物业抢~
## 5 2019-02-26 11:59:44 19-1-1801
可以看到,有的内容就只有“[图片]”,还有的只有“[表情]”,这些无用信息将在后面清除。
2、数据集格式转化
将同一用户的发言信息合并在一起。
> df2 <- df %>%
> # 注意顺序
+ select(txt, user) %>%
+ unstack() %>% t()
> text <- df2 %>% lapply(paste, collapse = " ") %>% unlist
> df3 <- data.frame(user = dimnames(df2)[[2]], text = text)
> # 因子型转换为字符型
> df3 <- purrr::map_if(df3, is.factor, as.character)
>
> # 预处理函数
> preprocessor = function(x) {
+ # 清除非中文字符
+ x <- gsub("[^\u4E00-\u9FA5]", "", x)
+ # 清除“表情”、“图片”
+ x <- gsub("表情|图片", " ", x)
+ # 多个空格转换为一个
+ x <- gsub("\\s+", " ", x)
+ # 清除收尾的空格
+ x <- trimws(x, which = "both")
+ return(x)
+ }
>
> df3$text <- preprocessor(df3$text)
3、中文分词
> p_load(jiebaR, cidian)
>
> decode_scel(scel = "./dict/日常用语大词库.scel", output = "./dict/rc.txt")
> user <- "./dict/rc.txt"
> stopwords <- "./dict/stopwords_wf.txt"
> wk <- worker(user = user, stop_word = stopwords)
> df3$words <- lapply(df3$text, segment, wk) %>%
+ lapply(paste0, collapse = " ") %>% unlist
> df3 <- df3 %>% as_tibble %>% select(-text) %>% filter(words != "")
> df3$words[1]
## [1] "取 快递 的位 置 楼栋 阿姨 收 给钱 喝不起 啤酒 没得 垃圾 卖楼 滴水 满意 厉害
+ 轻点 会就 睡着 真的 舒服 重手 疼 几天 掉 砖 组织 收集 证据 掉 砖 面积 维修
+ 进度 掉 砖 比例 已报 告 危险 墙面 速度 维修 掉 砖 情况 发生 事故 好像 年
+ 开发商 就解放了 出钱 修理 拖时间 拖 一年 是一年 在小区 住 天 外墙 修 几天
+ 维权 干嘛 地下 车位 打扫 地下车库 他妈 得一 层 灰 收费 违停 物业 配 拖车
+ 一期 业委 主任 贪污 公款 支持 业委 哪家 物业公司 中标 哪家 中标 进度 一步
+ 发群 里 咯 参与 报名 意味着 拦路 效果 奇佳 坚定 支持 业委 更换 垃圾 物业"
4、分析常用话题词汇
> p_load(text2vec, ggplot2)
>
> it <- itoken(df3$words, ids = df3$user, progressbar = F)
>
> vocab <- create_vocabulary(it)
> vocab
## Number of docs: 728
## 0 stopwords: ...
## ngram_min = 1; ngram_max = 1
## Vocabulary:
## term term_count doc_count
## 1: 一丁 1 1
## 2: 一万 1 1
## 3: 一万个 1 1
## 4: 一万元 1 1
## 5: 一下周 1 1
## ---
## 14588: 说 612 200
## 14589: 小区 719 229
## 14590: 业委会 785 226
## 14591: 业主 833 217
## 14592: 物业 1742 385
> # 设置画图字体
> p_load(showtext)
>
> font_add("PingFang", regular = "PingFang Regular.ttf")
> showtext_auto()
>
> vocab %>% filter(term_count > 5) %>% arrange(-term_count) %>%
+ ggplot(aes(term, term_count)) +
+ geom_jitter(alpha = 0.1, size = 2.5, width = 0.25, height = 0.25) +
+ geom_text(aes(label = term), check_overlap = TRUE, vjust = 1.5) +
+ geom_abline(color = "red") + labs(x = NULL, y = NULL) +
+ theme(axis.text.y = element_blank(), axis.ticks.y = element_blank())
词语都聚集到一起了,放大Y轴再看看:
> vocab %>% filter(term_count > 10 & term_count < 220) %>%
arrange(-term_count) %>%
+ ggplot(aes(term, term_count)) +
+ geom_jitter(alpha = 0.1, size = 2.5, width = 0.25, height = 0.25) +
+ geom_text(aes(label = term), check_overlap = TRUE, vjust = 1.5) +
+ geom_abline(color = "red") + labs(x = NULL, y = NULL) +
+ theme(axis.text.y = element_blank(), axis.ticks.y = element_blank())
去掉单字的分词,再看看:
> vocab %>% filter(str_length(term) > 1) %>%
+ filter(term_count > 10 & term_count < 220) %>%
+ arrange(-term_count) %>%
+ ggplot(aes(term, term_count)) +
+ geom_jitter(alpha = 0.1, size = 2.5, width = 0.25, height = 0.25) +
+ geom_text(aes(label = term), check_overlap = TRUE, vjust = 1.5) +
+ geom_abline(color = "red") + labs(x = NULL, y = NULL) +
+ theme(axis.text.y = element_blank(), axis.ticks.y = element_blank())
查看频率最高的15个词:
> vocab %>% filter(str_length(term) > 1) %>%
+ select(term, term_count) %>% top_n(15) %>%
+ ggplot(aes(term_count, reorder(term, term_count))) +
+ geom_col() + labs(x = NULL, y = NULL)
词云图:
> p_load(wordcloud2)
>
> vocab %>% select(term, term_count) %>%
+ filter(term_count > 10) %>% arrange(-term_count) %>%
+ wordcloud2(size = 1, color = "random-dark",
+ backgroundColor = "white", minRotation = -pi/4,
+ maxRotation = pi/4, fontFamily = "PingFang")
创建dtm矩阵:
> # 使用hash词向量
> vectorize.hash <- hash_vectorizer(hash_size = 2^14, ngram = c(1L, 2L))
> dtm <- create_dtm(it, vectorizer = vectorize.hash)
> dim(dtm)
## [1] 728 16384
> # 将dtm转换为矩阵
> dtm.mat <- as.matrix(dtm)
转换为矩阵是因为R对矩阵的计算效果更高。后续可基于dtm矩阵做更多的分析。