手把手 | 用StackOverflow访问数据实现主成分分析(PCA)

简介:

主成分分析(PCA:Principal Component Analysis)非常有助于我们理解高维数据,我利用Stack Overflow的每日访问数据对主成分分析进行了实践和探索,你可以在rstudio :: conf 2018上找到其中一篇演讲的录音。演讲的重点主要是我对于PCA的理解,而这篇文章中,我将主要介绍我是如何实现PCA的,以及我是如何制作演讲中使用到的图表的。

rstudio :: conf 2018

https://www.rstudio.com/resources/videos/understanding-pca-using-shiny-and-stack-overflow-data/

高维数据

此次分析使用的是去年Stack Overflow上注册用户访问量前500的标签数据。为了简化处理,本文只使用了10%的注册流量数据进行分析,但实际上我已经对所有流量数据进行了类似的分析,并获得了几乎相同的结果。

标签数据

https://stackoverflow.com/tags

现在,把每个注册用户都想象成高维空间中的一个点,空间的坐标轴是R、JavaScript、C++等技术。那么,在这个高维空间中,做相似工作的人对应的点就会彼此接近。接下来PCA会把这个高维空间转变成一个新的具有特殊特征的“特殊”高维空间。

在数据库中适当地抽取数据后,最开始的数据看起来就像下面这样:

 
library(tidyverse)
library(scales)
tag_percents
## # A tibble: 28 , 791 , 663 x 3
## User Tag Value
## < int > <chr> <dbl>
## 1 1 exception-handling 0.000948
## 2 1 jsp 0.000948
## 3 1 merge 0.00284
## 4 1 casting 0.00569
## 5 1 io 0.000948
## 6 1 twitter-bootstrap -3 0.00569
## 7 1 sorting 0.00474
## 8 1 mysql 0.000948
## 9 1 svg 0.000948
## 10 1 model-view-controller 0.000948
## # ... with 28 , 791 , 653 more rows

可以看出,数据很干净,每行只有用户编号和技术标签。这里的User列是随机ID,而非Stack Overflow的标识符。在Stack Overflow中,我们公开了大量数据,但流量数据(即哪些用户访问过哪些问题)是没有公开的。

对高维数据进行真正的匿名化其实是非常困难的,而这里为了进行脱敏处理,我的做法是随机化数据顺序,并用数字替换Stack Overflow的标识符。Value列表示过去一年该用户对该标签的浏览量占该标签总浏览量的比例。

部分数据链接:

https://stackoverflow.blog/2010/06/13/introducing-stack-exchange-data-explorer/

https://cloud.google.com/bigquery/public-data/stackoverflow,

https://meta.stackexchange.com/questions/19579/where-are-the-stack-exchange-data-dumps

先不考虑脱敏的问题,我们首先看看用户主要浏览的技术标签有哪些,这张图表给了我们一个直观的概念。.

 
tag_percents %>%
group_by(Tag) %>%
summarise(Value = mean(Value)) %>%
arrange(desc(Value)) %>%
top_n( 15 ) %>%
mutate(Tag = reorder(Tag, Value)) %>%
ggplot(aes(Tag, Value, label = Tag, fill = Tag)) +
geom_col(alpha = 0.9 , show.legend = FALSE) +
geom_text(aes(Tag, 0.001 ), hjust = 0 ,
color = "white" , size = 4 , family = "IBMPlexSans-Bold" ) +
coord_flip() +
labs (x = NULL , y = "Average % of a user's traffic" ) +
scale_y_continuous(labels = percent_format(), expand = c( 0.015 , 0 )) +
theme(axis.text.y=element_blank())
2d173f78b04317c1425f88cdc931e4733dab7a03

实施PCA

我们喜欢干净的数据,一是因为它就是我们查询数据库的结果,二是因为它可用于实现PCA等机器学习算法的探索性数据分析。为了实现PCA,我们需要一个矩阵,在这个例子里稀疏矩阵(sparse matrix)就是最佳选择——因为大多数开发人员只访问一小部分技术标签,因此我们的矩阵中会有很多零。tidytext软件包中有一个函数cast_sparse(),它可以把上面的数据转换为稀疏矩阵。

 
sparse_tag_matrix <- tag_percents %>%
tidytext::cast_sparse(User, Tag, Value)

R中有几个实现PCA的算法是体会不到稀疏矩阵的美感的,比如prcomp()——此算法的第一步就是将刚刚制作好的稀疏矩阵强制转换成一个常规矩阵,然后你要在那里干坐一辈子等它运行完,因为在它运行的时候电脑根本没有内存让你去做其他事了(别问我是怎么知道的)。当然,R中也有一个程序包利用了稀疏矩阵的优势——irlba。

在建立模型前,也别忘记先用scale()函数将你的矩阵规范化,这对于PCA的实现非常重要。

 
tags_scaled <- scale(sparse_tag_matrix)
tags_pca <- irlba::prcomp_irlba(tags_scaled, n = 64 )

其中prcomp_irlba()函数的参数n代表我们想要得到的主成分个数。

那么这一步究竟发生了什么?我们会在接下来的章节中慢慢介绍。

 
class (tags_pca)
## [ 1 ] "irlba_prcomp" "prcomp"
names(tags_pca)
## [ 1 ] "scale" "totalvar" "sdev" "rotation" "center" "x"

PCA的结果分析

我喜欢处理数据框格式的数据,所以接下来我要用tidy()函数来整理我的PCA结果,以便用dplyr包处理输出结果和用ggplot2绘图。 broom包并不能完美地处理irlba的输出结果,所以我会将它们与我自己的数据框经过一点修整后合并到一起。

 
library(broom)
tidied_pca <- bind_cols(Tag = colnames(tags_scaled),
tidy(tags_pca$rotation)) %>%
gather(PC, Contribution, PC1:PC64)
tidied_pca
## # A tibble: 39 , 232 x 3
## Tag PC Contribution
## <chr> <chr> <dbl>
## 1 exception-handling PC1 -0.0512
## 2 jsp PC1 0.00767
## 3 merge PC1 -0.0343
## 4 casting PC1 -0.0609
## 5 io PC1 -0.0804
## 6 twitter-bootstrap -3 PC1 0.0855
## 7 sorting PC1 -0.0491
## 8 mysql PC1 0.0444
## 9 svg PC1 0.0409
## 10 model-view-controller PC1 0.0398
## # ... with 39 , 222 more rows

注意到这里我的数据框的每一行只有一个技术标签及它构成的主成分。

那么从整体来看,这些结果又是什么样子的呢?请见下图:

 
tidied_pca %>%
filter(PC %in% paste0( "PC" , 1 : 6 )) %>%
ggplot(aes(Tag, Contribution, fill = Tag)) +
geom_col(show.legend = FALSE, alpha = 0.8 ) +
theme(axis.text.x = element_blank(),
axis.ticks.x = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank()) +
labs (x = "Stack Overflow tags" ,
y = "Relative importance in each principal component" ) +
facet_wrap(~ PC, ncol = 2 )
640280da7ca084adb0d72b54b828599fab9c5eab

很漂亮吧有木有!我们上面看的是前六个主成分,图中x轴上是按字母顺序排列的单个Stack Overflow标签,纵轴表示该技术标签对这一PC的贡献度。我们也可以看出有关联的技术可能是以相同的字母开头,故而会排列在一起,例如PC4中的橙色等。

下面让我们主要分析一下第一个主成分的构成。

 
tidied_pca %>%
filter(PC == "PC1" ) %>%
top_n( 40 , abs (Contribution)) %>%
mutate(Tag = reorder(Tag, Contribution)) %>%
ggplot(aes(Tag, Contribution, fill = Tag)) +
geom_col(show.legend = FALSE, alpha = 0.8 ) +
theme(axis.text.x = element_text(angle = 90 , hjust = 1 , vjust = 0.5 ),
xis.ticks.x = element_blank()) +
labs (x = "Stack Overflow tags" ,
y = "Relative importance in principle component" )
29d9783435d3eb19cc3b7c2895094c676e79cae4

现在我们可以看到哪些技术标签对这个成分有贡献。从贡献为正的标签来看,主要有前端Web开发技术,如HTML、JavaScript、jQuery、CSS等。从贡献为负的标签来看,主要有Python,C ++以及低级技术词汇,如字符串(strings)、列表(lists)等。这意味着Stack Overflow的用户之间最大的差异在于他们是使用前端Web技术更多一些还是Python和一些低级技术更多一些。

那么第二个主成分又是怎样的呢?

 
tidied_pca %>%
filter(PC == "PC2" ) %>%
top_n( 40 , abs (Contribution)) %>%
mutate(Tag = reorder(Tag, Contribution)) %>%
ggplot(aes(Tag, Contribution, fill = Tag)) +
geom_col(show.legend = FALSE, alpha = 0.8 ) +
theme(axis.text.x = element_text(angle = 90 , hjust = 1 , vjust = 0.5 ),
axis.ticks.x = element_blank()) +
labs (x = "Stack Overflow tags" ,
y = "Relative importance in principle component" )
65c5894709df72a706da4c13cc51a9e564ae157f

第一个主成分是两种软件工程的对比,但第二个主成分则更像是一个结果为是/否的二分类变量。它告诉了我们开发人员工作中是否使用C#、.NET、Visual Studio和Microsoft技术堆栈的其余部分。这意味着Stack Overflow的用户之间的第二大差异在于他们是否访问了这些类型的微软技术问题。

我们可以继续研究其他的主成分,了解更多关于Stack Overflow技术生态系统的知识,但其实我已经在视频中进行了相关内容的讲解,也研究了那些与我们数据科学人员相关的技术。我还制作了一个名叫Shiny的应用程序,在上面你可以随意选择你想研究的主成分。而且我敢打赌,只要你用过一次Shiny,你就能想象到我是如何开始这项研究的!

高维平面的映射

PCA最酷的地方在于它能帮我们思考和推理高维数据,其中一项功能就是将高维数据映射到可绘图的二维平面上。接下来我们来看看它是如何做到这一点的。

其实这一步用broom :: augment()就能实现,并且还能计算出每个成分对整个数据集方差解释的百分比。

 
percent_variation <- tags_pca$sdev^ 2 / sum(tags_pca$sdev^ 2 )
augmented_pca <- bind_cols(User = rownames(tags_scaled),
tidy(tags_pca$x))
augmented_pca
## # A tibble: 164 , 915 x 65
## User PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 2.16 5.70 1.63 0.967 0.0214 -1.37 -1.98 -2.94 -0.860
## 2 2 0.350 3.38 -6.12 -10.0 1.39 0.882 5.35 -3.30 -2.73
## 3 3 2.75 -3.91 0.801 1.73 1.24 -0.837 2.03 2.76 0.300
## 4 4 3.27 -3.37 -1.00 2.39 -3.59 -2.68 0.449 -2.82 -1.25
## 5 5 9.44 -4.24 3.88 -1.62 -2.96 4.01 -1.32 -3.54 3.25
## 6 6 5.47 -5.13 1.57 2.94 -0.170 0.342 3.34 6.09 1.72
## 7 7 4.30 -0.442 -1.52 0.329 -2.13 0.908 -3.30 -5.02 -1.39
## 8 8 -0.691 0.668 -1.76 -7.74 -2.94 -5.28 -9.71 5.28 0.732
## 9 9 3.84 -2.65 0.760 1.34 2.06 -0.927 1.35 5.11 -2.69
## 10 10 3.23 4.13 2.81 2.68 -1.12 -1.30 -0.319 -1.23 -0.723
## # ... with 164 , 905 more rows, and 55 more variables: PC10 <dbl>,
## # PC11 <dbl>, PC12 <dbl>, PC13 <dbl>, PC14 <dbl>, PC15 <dbl>,
## # PC16 <dbl>, PC17 <dbl>, PC18 <dbl>, PC19 <dbl>, PC20 <dbl>,
## # PC21 <dbl>, PC22 <dbl>, PC23 <dbl>, PC24 <dbl>, PC25 <dbl>,
## # PC26 <dbl>, PC27 <dbl>, PC28 <dbl>, PC29 <dbl>, PC30 <dbl>,
## # PC31 <dbl>, PC32 <dbl>, PC33 <dbl>, PC34 <dbl>, PC35 <dbl>,
## # PC36 <dbl>, PC37 <dbl>, PC38 <dbl>, PC39 <dbl>, PC40 <dbl>,
## # PC41 <dbl>, PC42 <dbl>, PC43 <dbl>, PC44 <dbl>, PC45 <dbl>,
## # PC46 <dbl>, PC47 <dbl>, PC48 <dbl>, PC49 <dbl>, PC50 <dbl>,
## # PC51 <dbl>, PC52 <dbl>, PC53 <dbl>, PC54 <dbl>, PC55 <dbl>,
## # PC56 <dbl>, PC57 <dbl>, PC58 <dbl>, PC59 <dbl>, PC60 <dbl>,
## # PC61 <dbl>, PC62 <dbl>, PC63 <dbl>, PC64 <dbl>

注意到这里我其实有更广阔的数据框可供使用,并且我还没有使用gather()函数——为了便于绘图。对象percent_variation是一个矢量,它包含了每个主成分对整个数据集的方差解释的百分比。

 
augmented_pca %>%
mutate(User = as.integer(User)) %>%
filter(User %% 2 == 0 ) %>%
ggplot(aes(PC1, PC2)) +
geom_point(size = 1.3 , color = "midnightblue" , alpha = 0.1 ) +
labs (x = paste0( "Principal component 1 (" , percent(percent_variation[ 1 ]), ")" ),
y = paste0( "Principal component 2 (" , percent(percent_variation[ 2 ]), ")" ),
title = "Projection of Stack Overflow traffic on to the first two principal components" ,
subtitle = "The very high dimensional space can be projected down onto components we have explored" )
52563b15df1d110c7b23f2d33f661e315660fd7a

可以看出,为了尽量减少过度绘图,这个图里我把每两个人用一个点表示。还记得第一个主成分是前端开发人员到Python和低级技术人员的横向拓展,而第二个主成分则全部是关于微软技术堆栈的。由上我们可以看到描述Stack Overflow标签的高维数据是如何投影到前两个主成分的。可以注意到我已在每个轴中添加了方差百分比,同时这些数字并不是很高,这也与我们现实生活中的情况相吻合,即事实上Stack Overflow的用户之间差异很大,如果你想将这些主成分中的任意一个用于降维或作为模型中的预测变量,请慎重考虑。

应用

说到现实生活,我发现PCA非常有助于我们理解高维数据集。比如说,基于完全相同的数据,我最近在使用PCA探索的另一个问题是亚马逊可能考虑让哪些城市成为其第二总部。实际上,PCA给出的主成分结果以及不同技术对其的贡献率已经不尽相同——因为几个月已经过去了,而且用户们在高维空间中也不是完全静止的。如果你有任何问题或反馈,请及时联系我。


原文发布时间为:2018-05-31

本文来自云栖社区合作伙伴“大数据文摘”,了解相关信息可以关注“大数据文摘”。

相关文章
|
缓存 网络协议 网络安全
程序员必知的计算机网络的166个核心概念(上)
程序员必知的计算机网络的166个核心概念
|
11月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
887 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
Web App开发 Python
【Chromedriver】下载、安装及配置
简介:【Chromedriver】下载、安装及配置
11022 60
【Chromedriver】下载、安装及配置
|
10月前
|
人工智能 自然语言处理 Cloud Native
在阿里云,零门槛,即刻拥有DeepSeek-R1满血版
DeepSeek 是一款强大的推理模型,尤其擅长数学、代码和自然语言处理等复杂任务。通过阿里云平台,用户可以快速调用满血版 DeepSeek API 或部署不同尺寸的模型,无需编码,最快5分钟完成,最低0元起。方案提供100万免费Token,支持弹性算力,降低硬件成本,加速创新。 解决方案链接:[点击查看](https://www.aliyun.com/solution/tech-solution/deepseek-r1-for-platforms?utm_content=g_1000401616)
448 17
|
10月前
|
存储 人工智能 算法
AAAI 2025| S5VH: 基于选择性状态空间的高效自监督视频哈希
AAAI 2025| S5VH: 基于选择性状态空间的高效自监督视频哈希
171 1
|
5月前
|
SQL DataWorks 监控
免费玩转阿里云DataWorks!智能Copilot+用户画像实战,开发效率翻倍攻略
DataWorks是阿里云推出的一站式大数据开发与治理平台,具备数据集成、开发、管理、安全及智能监控等功能,支持多行业数据中台建设。其可视化界面与强大调度能力,助力企业高效完成数据处理与分析。
876 0
|
6月前
|
人工智能 自然语言处理 监控
无需编程,我用 AI 模型结合 RPA 自动化,用 2 天时间手搓小红书营销产品
这是一篇关于如何用ai 和无代码方式,为运营提供一套“小红书爆款生产流水线”的工具,系统可自动采集对标博主笔记、分析热点数据并生成选题草稿,用户仅需补充细节即可完成高质量内容创作。流程涵盖关键词采集、对标博主监控、高价值笔记筛选、AI文案与图片创作及多账号矩阵发布。相比传统方式,该方法大幅提升效率,1小时可完成10篇内容创作,助力创作者在竞争中脱颖而出。文中还详细解析了关键词采集、对标博主分析、自动化排版等关键步骤,适合希望提升内容生产效率的运营者参考。
|
数据格式 Python
【嵌入式】波特率9600,发送8个字节需要多少时间,如何计算?
波特率9600,发送 `01 03 00 00 00 04 44 09` (8字节) 需要多少时间,如何计算?
859 7
|
监控 关系型数据库 MySQL
如何监控和诊断 MySQL 数据库的性能问题?
【10月更文挑战第28天】监控和诊断MySQL数据库的性能问题是确保数据库高效稳定运行的关键
1312 1
|
安全 Java Unix
【C++ 包裹类 std::thread】探索C++11 std::thread:如何使用它来创建、销毁和管理线程
【C++ 包裹类 std::thread】探索C++11 std::thread:如何使用它来创建、销毁和管理线程
711 0