雷达图这个东西在体育数据分析里出场率是真的高。不管是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()转换 |
| 图形缺失或断线 | 数据含NA | drop_na()清理 |
| 图例挤占画布 | 图例位置和大小不合理 | 调整legend.position和字号 |
| 数据点超出网格 | 数据范围大于grid.max | 对齐grid.min和grid.max |
我在实际项目中已经把这些排查步骤固化成了SOP:拿到数据先str()看类型,再summary()看范围,然后归一化,最后才进ggradar。这套流程基本能避开九成以上的坑。
最后再分享一个我自己的使用习惯。刚开始画雷达图,我总是先纠结配色、字体、透明度这些视觉参数,后来发现最核心的还是“指标选得对不对”。雷达图好看的前提,是每个维度本身有意义、彼此又能区分出差异。如果六个维度全是强相关的数据,画出来就是一块畸形的多边形,颜色再高级也救不回来。所以现在每画一张雷达图之前,我都会先问自己一句:这些维度放在一起,到底想表达什么差异?想清楚了再动手,图自然就有说服力。希望这篇内容能帮你少走点弯路,快速画出第一张能看的雷达图。