news 2026/10/4 1:26:09

R语言ggradar包实战:从数据归一化到NBA球员雷达图绘制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
R语言ggradar包实战:从数据归一化到NBA球员雷达图绘制

雷达图这个东西在体育数据分析里出场率是真的高。不管是NBA官网的球员对比,还是各种数据媒体的赛季盘点,你总能看到这种从中心向外发散、像蜘蛛网一样的多边形图表。我第一次在项目报告里想复现这种图,用的是R语言,翻了好几个包,最后真正让我稳定出图的还是ggradar。这个包基于ggplot2封装,写起来很顺手,但对数据格式有严格的要求,新手第一次用基本都会在归一化和列类型上栽跟头。

这篇博文我打算用NBA季后赛的真实球员数据(整理后的近似值,仅作演示)来做例子,从数据结构讲到出图调优,把ggradar的常见坑一次性聊透。内容适合两类人:一是R已经入门、想画多维对比图但不太清楚怎么组织数据的人;二是已经会ggplot2、想快速搞定极坐标雷达图又不想手搓坐标变换的人。看完你应该能直接照着代码跑出一张能放进报告里的图,也知道出问题时该往哪查。

1. 项目思路与数据集设计

1.1 为什么选雷达图看NBA季后赛数据

雷达图的核心价值一句话就能说清:把多个维度的数值放到同一张图上,用“形状”代替“数字”做对比。常规赛数据样本大、球员定位模糊,但季后赛不一样,轮换缩短、战术针对性极强,每个球员在球队里的角色会被放大——有人是篮板怪兽,有人是外线炮台,有人是组织大脑。这些差异用表格看是一堆数字,用雷达图看就是完全不同的几何形状。

拿一张图对比约基奇和戴维斯,得分、篮板、助攻、盖帽几个维度摊开,约老师的多边形明显更“膨胀”,浓眉则在盖帽这一根轴上拉满。这种视觉冲击力,是条形图和散点图给不了的。我还喜欢拿雷达图做“球员能力模型”:就像学生成绩单上六科雷达图一样,哪一科偏科、哪一科全面,一眼就能捕捉到。

1.2 数据从哪里来,怎么构造

这次演示我选了三位2023-24赛季季后赛代表球员:约基奇、戴维斯、爱德华兹。每个球员记录六个维度的场均数据:得分、篮板、助攻、抢断、盖帽、三分命中率。为什么选这六项?得分和篮板是基础产量,助攻和抢断代表组织与防守积极性,盖帽体现护框能力,三分命中率则投射效率——维度之间相关性不能太强,否则雷达图的“形状”会被某一个隐藏因子主导,看不出差异。

在R里面,我推荐用tibble直接构造:

library(tidyverse) playoff <- tibble( player = c("Jokic", "Davis", "Edwards"), points = c(28.7, 27.8, 27.6), rebounds = c(13.4, 15.6, 7.0), assists = c(8.7, 4.1, 6.5), steals = c(1.2, 1.5, 1.5), blocks = c(0.9, 1.6, 0.8), three_pt_pct = c(0.355, 0.235, 0.400) )

这里有个细节值得注意:三分命中率已经是0到1之间的小数,但得分、篮板这些原始单位和量级完全不同。直接拿去画图,ggradar会很难受,整张图会被得分这一根轴撑爆,篮板、助攻全部被压扁。所以数据准备阶段最重要的动作不是计算,而是归一化。

1.3 归一化:ggradar绘图的隐藏前提

ggradar本质上是在ggplot2的极坐标系上画点连线,所有维度共用同一个半径方向上的刻度。如果你把“28.7分”和“13.4篮板”直接塞进去,两者量级差异太大会导致视觉失衡——大数值轴把整个圆撑满,小数值轴贴在内圈动都动不了。解决方式就是min-max归一化,把每一列线性映射到0到1区间。

公式很简单:

x_scaled = (x - min(x)) / (max(x) - min(x))

用dplyr处理非常顺手:

playoff_scaled <- playoff %>% mutate(across(points:three_pt_pct, ~ (.x - min(.x)) / (max(.x) - min(.x))))

这里across的作用是批量对指定列做变换,points:three_pt_pct是一个连续选列的范围写法,刚好把六项指标都选进去,而player列因为是字符型,不会被误处理。执行后你可以用summary(playoff_scaled)检查,每一列的min都应该是0、max都应该是1。

做完这一步,真正的ggradar绘图才能开始。

2. ggradar绘图原理与核心细节

2.1 ggradar到底做了什么

很多初学者第一次用ggradar,以为它是个独立的绘图系统,其实它是对ggplot2能力的封装。ggplot2画雷达图本身是能画的,但要自己写coord_polar、自己安排轴标签位置、自己调网格线,代码量不小。ggradar把这些都包好了:你只需要把数据格式整理对,剩下的事情它来。

它的绘图逻辑是:数据框的每一行对应图上的一组多边形(比如一个球员),每一列对应从圆心向外发散的一条半径轴,数值大小决定这根轴上点的远近,最后依次把每个维度上的点连起来,闭合形成一个多边形。多个球员的数据放在同一个数据框里,图就会叠加出多个多边形,方便对比。

这就像你用圆规画多边形:中心是圆心,每条刻度线是一根半径,数据值就相当于半径的长度。ggradar只是把“读数、点标、连线”自动化了而已。

2.2 数据规范:先过这关再谈美化

ggradar对数据格式的要求非常严格,我在第一次跑的时候被报错折磨了半天,所以这里单独拿出来说:

  • 必须是一个data.frame或tibble,第一列是字符型/因子型的分组变量,其余列必须全是数值型。
  • 不能包含NA值,只要有一个缺失,极坐标计算时就会报错或者画出断线。
  • 归一化后所有数值列的范围最好落在0到1之间,并且要对应grid.min、grid.max的参数。
  • 如果有多行数据,每一行就是一个独立的多边形,行数太多会糊成一团,建议不超过5组。

有一个经常被忽略的问题是:数值列的类型。从CSV读入的数据,某些列可能会被解析成字符型,尤其是三分命中率这种0.355的数据。如果直接画图,ggradar会报类似“must be numeric”的错误。所以读入数据后,我习惯先用str()看一遍每列类型,确认没问题才继续。

2.3 关键参数拆解

ggradar的绘图函数参数很多,但真正影响成图质量的就那么几个。我按使用频率排个序:

参数作用我的常用值
grid.min内圈最小值0
grid.mid中圈网格线0.5
grid.max外圈最大值1
values.radar刻度标签c("0", "0.5", "1")
axis.labels每根轴显示的变量名c("得分", "篮板")
group.colours每组数据的颜色自定义色板
fill是否填充多边形内部TRUE
fill.alpha填充透明度0.15
group.line.width多边形边框粗细1.2
group.point.size顶点圆点大小2.5
axis.label.size轴标签字号4.5
legend.position图例位置"bottom"

这里最关键的理解是grid.min、grid.mid和grid.max必须与你的数据范围对应。归一化后数据都在0到1之间,那这三个参数就设为0、0.5、1。如果你的数据做了0-100的百分制归一化,那么这三个参数也要改成0、50、100,否则数据点会跑到网格之外。

values.radar是另一个容易踩坑的地方。它控制的是网格线上的刻度文本,数量必须跟网格线的数量一致。比如你只设了grid.min、grid.mid、grid.max三条网格线,那么values.radar传三个标签就够了,传五个反而会错位。

2.4 为什么不用ggplot2手搓

可能有人会问,既然ggradar只是封装,我直接用ggplot2画不行吗?答案是:能,但性价比太低。手搓雷达图需要做这几件事:把宽表数据转成长表、用coord_polar把笛卡尔坐标变成极坐标、调整scale_y_continuous让刻度对齐、再手动控制多边形闭合时首尾坐标的重复。这些步骤加在一起,足够写五六十行代码,而且调试起来很容易出鬼问题。

ggradar把这些隐藏细节全部处理好了。你只需要组织好数据框架,把绘图参数调一调,代码量能压缩到十几行以内。尤其是做快速探索性分析的时候,速度优势非常明显。但代价就是:它对数据格式的容忍度低,不符合规则就罢工。这恰恰是本文要重点讲透的部分。

3. 从零到一:完整绘图实操

3.1 环境安装与包加载

先解决安装问题。ggradar目前不在CRAN的正式源里,需要通过GitHub安装。不过在安装之前,确保你已经装好了ggplot2、dplyr这些基础包。

install.packages("ggplot2") install.packages("dplyr") # 方法一:从GitHub安装(推荐) if (!requireNamespace("devtools", quietly = TRUE)) { install.packages("devtools") } devtools::install_github("ricardo-bion/ggradar")

如果网络环境不好,GitHub安装失败,可以试试国内镜像加速,或者直接用install.packages("ggradar")碰碰运气——部分镜像站会收录它。加载的时候也很简单:

library(ggradar) library(tidyverse)

我自己实际用下来,最稳妥的还是devtools从GitHub装,一次性把依赖包都拉齐。装好之后建议跑一下packageVersion("ggradar")确认版本号,避免后续报错时不知道是哪里的问题。

3.2 数据准备完整代码

还是用刚才构造的playoff数据,完整的处理流程我写一遍。这个流程可以当成模板用,换任何数据集都成立:

library(tidyverse) library(ggradar) playoff <- tibble( player = c("Jokic", "Davis", "Edwards"), points = c(28.7, 27.8, 27.6), rebounds = c(13.4, 15.6, 7.0), assists = c(8.7, 4.1, 6.5), steals = c(1.2, 1.5, 1.5), blocks = c(0.9, 1.6, 0.8), three_pt_pct = c(0.355, 0.235, 0.400) ) # 缺失值检查与处理 playoff <- playoff %>% drop_na() # 归一化:每一列缩放到0-1 playoff_scaled <- playoff %>% mutate(across(points:three_pt_pct, ~ (.x - min(.x)) / (max(.x) - min(.x)))) # 验证结果 summary(playoff_scaled)

这段代码里drop_na()是我后来加的习惯。真实数据基本都不干净,手动整理时漏掉一个缺失值,后面ggradar画图就会开始报错,所以提前清理能省很多事。用across(points:three_pt_pct, ...)这种方式,比一个列一个列地写mutate要干净得多,也方便扩展到更多维度。

3.3 基准图绘制

数据处理完后,绘图本身非常简洁:

playoff_scaled %>% ggradar( grid.min = 0, grid.mid = 0.5, grid.max = 1, values.radar = c("0", "0.5", "1"), axis.labels = c("Points", "Rebounds", "Assists", "Steals", "Blocks", "3P%") )

跑完这段,你会得到一张具有三个彩色多边形的基本雷达图。这时候只是“能看”的阶段。三个球员的多边形叠在一起,靠默认颜色区分,图例在右侧。但默认样式有几个明显问题:配色不够高级、多边形填充太满、标签字号偏小。所以下一步做视觉调优。

3.4 视觉参数调优:让图能放进报告

我自己画图有个习惯:默认参数只是起点,最终出图一定按使用场景再调一遍。下面这个版本是我常用的“报告级”配置:

playoff_scaled %>% ggradar( grid.min = 0, grid.mid = 0.5, grid.max = 1, values.radar = c("0", "0.5", "1"), axis.labels = c("得分", "篮板", "助攻", "抢断", "盖帽", "三分命中率"), group.colours = c("#E64B35", "#4DBBD5", "#00A087"), fill = TRUE, fill.alpha = 0.15, group.line.width = 1.2, group.point.size = 2.5, axis.label.size = 4.5, gridline.label.offset = 0.12, legend.position = "bottom" ) + theme_minimal(base_size = 14)

这里有几个调优思路值得展开说:

第一,group.colours一定要手动指定。ggradar默认配色是ggplot2的调色板,在极坐标下颜色会偏亮、偏刺眼。我选了三组比较耐看的颜色:红色系、蓝色系、绿色系,彼此区分度高,打印出来也不会混淆。

第二,fill = TRUE配合fill.alpha = 0.15。填充色能让多边形看起来更加“实”,但alpha值不能太高,否则多个球员重叠区域会变成一团深色,看不清边界。0.15是我试过很多次之后觉得比较舒服的透明度。

第三,legend.position = "bottom"。雷达图本身就是圆的,右侧图例容易把整体视觉重心带偏,放底部更协调。如果你想更彻底,还可以用legend.position = "none"去掉图例,改用图形标签直接标注,但那样需要额外用annotate,代码会复杂不少。

第四,这个包返回的对象本身是ggplot对象,所以后面还能继续叠加theme相关的调整。我在例子末尾加了theme_minimal(base_size = 14),让整张图的字体更协调。

3.5 进阶:多个球员叠加与分面小图

如果你的数据里不止三位球员,比如想对比一整支首发阵容,多边形的叠加会变得拥挤。这时候有两个处理思路:

一是直接在同一个图里画,但只保留2到4组。超过四组,图基本就看不清谁是谁了。二是做分面小图,每个球员一个面板。不过要注意,ggradar对分面支持不算友好,因为每个面板都会重复绘制极坐标网格,版面利用率不高。实际操作中,我更推荐用列表循环加patchwork拼图,而不是facet_wrap:

library(patchwork) plots <- split(playoff_scaled, playoff_scaled$player) %>% map(~ ggradar(.x, grid.min = 0, grid.mid = 0.5, grid.max = 1, values.radar = c("0", "0.5", "1"), axis.labels = c("得分", "篮板", "助攻", "抢断", "盖帽", "三分命中率"), fill = TRUE, fill.alpha = 0.15 )) reduce(plots, `+`) + plot_layout(ncol = 1)

这个技巧在做球员简历或者报告附录的时候很好用。单球员独图,信息密度更高,也能顺便加上球员照片、球衣号码等元素,直接变成一页可视化卡片。

4. 常见报错与排查技巧实录

4.1 最经典的坑:数值没归一化

我先说一下这个坑的典型表现:图形“扁了”,某个方向被拉得特别长,其他方向全部贴在内圈,整体几乎看不出雷达图的形状。如果你看到这种情况,十有八九是直接把原始数据丢给ggradar了,没有做min-max归一化。

排查方式很简单,跑一遍summary()或者range()逐列检查数值范围。比如points列是28左右,three_pt_pct列是0.3左右,两者量级差100倍,极坐标下小数值轴自然会被压缩成一条线。把归一化补上,图立刻恢复正常。

4.2 中文标签变方块

这是中文本地化最常见的问题。axis.labels可以传中文,但绘图字体如果没有中文字体支持,图上就是一个个方块或者问号。原因不在ggradar本身,而在ggplot2的字体渲染机制。

我推荐用showtext包解决,它对中文支持最省心:

library(showtext) showtext_auto() font_add("heiti", "C:/Windows/Fonts/simhei.ttf")

如果你的系统是macOS,字体路径改成对应的字体文件,比如"PingFang.ttc"。加载showtext并启用自动渲染后,再跑一次绘图代码,中文就能正常显示。这个坑比较隐蔽,因为代码不会报错,只会默默输出带方块子的图。

4.3 数据列类型不对导致的报错

如果你读入的是CSV,某列被读成了字符型,ggradar会报错说需要数值型。最典型的例子是三分命中率,有些数据里直接带上百分号,比如"35.5%",那这一列就变成了字符串。

处理方法也很标准,先转成数值,再去掉百分号:

playoff <- playoff %>% mutate(three_pt_pct = as.numeric(gsub("%", "", three_pt_pct)) / 100)

这里有几个细节要注意:gsub去掉百分号之后as.numeric转换,不除以100的话数据范围是35.5而不是0.355,所以换算要写全。如果数据里本身是小数点格式,直接as.numeric()就行。

4.4 图例位置和大小调整的各种尝试

ggradar的图例参数经常被忽略,但实际影响很大。默认图例在右侧,标签文字小,位置也比较尴尬。我调试时踩过几个坑:想把图例放到底部,但改完位置之后文字还是很小,需要额外设置legend.text.size;或者图例标题占了一块空间,整体图形又变小了一圈。

我的建议是:单组数据时直接legend.position = "none"去掉图例,干净利落;多组对比时放在底部,并且用legend.text.size = 12这类参数把字号调大。因为雷达图的网格和标签已经承载了大量信息,图例的存在感不应该太强。

4.5 常见问题速查表

现象原因解决方案
图像被压扁、方向不平衡数据未归一化用min-max归一化到0-1
中文标签显示为方块字体不支持中文用showtext加载中文字体
报错“must be numeric”数据列是字符型as.numeric()转换
图形缺失或断线数据含NAdrop_na()清理
图例挤占画布图例位置和大小不合理调整legend.position和字号
数据点超出网格数据范围大于grid.max对齐grid.min和grid.max

我在实际项目中已经把这些排查步骤固化成了SOP:拿到数据先str()看类型,再summary()看范围,然后归一化,最后才进ggradar。这套流程基本能避开九成以上的坑。


最后再分享一个我自己的使用习惯。刚开始画雷达图,我总是先纠结配色、字体、透明度这些视觉参数,后来发现最核心的还是“指标选得对不对”。雷达图好看的前提,是每个维度本身有意义、彼此又能区分出差异。如果六个维度全是强相关的数据,画出来就是一块畸形的多边形,颜色再高级也救不回来。所以现在每画一张雷达图之前,我都会先问自己一句:这些维度放在一起,到底想表达什么差异?想清楚了再动手,图自然就有说服力。希望这篇内容能帮你少走点弯路,快速画出第一张能看的雷达图。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 1:25:55

MRAM与PIC18F4455的工业存储方案:SPI接口掉电不丢数据实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:25:21

Cocos Creator v2.0 2D闯关安卓游戏课程设计:从搭建到打包全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:25:07

旋转矩阵的本质:从空间直觉到工程实现的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:24:42

MR25H40CDF与STM32F767BI:工业MRAM存储选型与掉电保存实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:24:36

@Transactional滥用导致连接池耗尽的根因与修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:24:35

低功耗测量探头选型与实操:从nA级信号到干净波形的全流程指南

1. 低功耗测量的核心挑战与探头选型逻辑搞低功耗测量的朋友多半有过这种体验&#xff1a;板子明明设计得挺好&#xff0c;休眠电流理论值算下来只有几微安&#xff0c;可示波器上抓出来的波形却像心电图一样上蹿下跳&#xff0c;底噪大得离谱&#xff0c;根本分不清哪些是真实的…

作者头像 李华