世界杯期间,很多赛前分析都会写“谁更强”“谁更有机会赢”。

但如果放到 CSDN,单纯写球评就有点不合适。

技术文章更适合换一种方式:

用 Python 把球队能力做成可视化图表。

今天我们以阿根廷 vs 奥地利为例,画一张球队能力雷达图。

雷达图适合展示多维指标对比,比如:

  • 进攻能力
  • 防守稳定
  • 控球能力
  • 压迫强度
  • 反击速度
  • 近期状态

这种图不负责预测比分,也不做确定结论。

它的作用是把赛前分析从“主观感觉”变成“结构化对比”。

当然,足球不是 Excel 表格,比赛也不会因为雷达图好看就按图执行。人类已经够爱假装自己能预测世界了,别让 matplotlib 也背锅。


一、为什么用雷达图?

雷达图适合展示多个维度的能力差异。

比如阿根廷和奥地利这场:

阿根廷的优势可能在:

  • 进攻上限
  • 控球能力
  • 大赛经验
  • 核心球员状态

奥地利的优势可能在:

  • 高位压迫
  • 纵向推进
  • 身体对抗
  • 比赛节奏冲击

如果只写文字,读者需要自己在脑子里对比。

如果画成雷达图,一眼就能看出两队能力结构差异。

这就是数据可视化的价值。

不是让结论更神秘,而是让信息更直观。


二、安装依赖

本文只需要一个第三方库:

pip install matplotlib

如果你本地已经安装过,可以直接跳过。

查看版本:

python -c "import matplotlib; print(matplotlib.__version__)"

建议使用 Python 3.9 以上版本。


三、准备球队能力数据

这里我们先手动构造一组示例数据。

注意:下面的分数是为了演示代码和图表效果,不是官方评分,也不是赛果预测。

创建文件:

worldcup_radar_chart.py

先写入数据:

teams = {
    "阿根廷": {
        "进攻": 9.2,
        "防守": 8.4,
        "控球": 9.0,
        "压迫": 8.1,
        "反击": 8.3,
        "近期状态": 9.1,
    },
    "奥地利": {
        "进攻": 7.8,
        "防守": 7.9,
        "控球": 7.4,
        "压迫": 8.8,
        "反击": 8.2,
        "近期状态": 8.0,
    },
}

这里设计了 6 个维度。

为什么是这 6 个?

因为它们比较适合赛前分析:

  • 进攻:能不能创造机会
  • 防守:能不能限制对手
  • 控球:能不能掌控节奏
  • 压迫:能不能逼迫对手失误
  • 反击:能不能快速转换
  • 近期状态:最近比赛表现是否稳定

阿根廷的优势在控球和进攻。

奥地利的特点是压迫强度高,节奏更直接。

这正好适合用雷达图对比。


四、雷达图的基本原理

雷达图本质上是极坐标图。

普通折线图是横轴、纵轴。

雷达图是把多个指标平均分布在一个圆上,每个指标对应一个角度。

如果有 6 个指标,就把圆分成 6 个方向。

为了让图形闭合,需要把第一项数据再追加到最后。

比如:

values = [9.2, 8.4, 9.0, 8.1, 8.3, 9.1]
values += values[:1]

角度也要闭合:

angles += angles[:1]

不闭合的话,图形就会缺一条边。
那画出来就像球队踢到一半突然下班,视觉上非常不负责任。


五、完整代码

下面是完整可运行代码。

import math
from pathlib import Path

import matplotlib.pyplot as plt


def setup_chinese_font() -> None:
    """
    设置中文字体。
    不同系统字体名称可能不同,如果中文显示异常,可以修改这里。
    """
    plt.rcParams["font.sans-serif"] = [
        "Microsoft YaHei",
        "SimHei",
        "Arial Unicode MS",
        "DejaVu Sans",
    ]
    plt.rcParams["axes.unicode_minus"] = False


def close_loop(values: list[float]) -> list[float]:
    """
    雷达图需要闭合图形,所以把第一个值追加到最后。
    """
    return values + values[:1]


def build_angles(metric_count: int) -> list[float]:
    """
    根据指标数量生成每个指标对应的角度。
    """
    angles = [
        2 * math.pi * index / metric_count
        for index in range(metric_count)
    ]
    return angles + angles[:1]


def validate_team_data(teams: dict[str, dict[str, float]]) -> list[str]:
    """
    校验所有球队是否使用相同指标。
    """
    if not teams:
        raise ValueError("teams 数据不能为空")

    metric_names = list(next(iter(teams.values())).keys())

    for team_name, metrics in teams.items():
        current_metrics = list(metrics.keys())

        if current_metrics != metric_names:
            raise ValueError(
                f"{team_name} 的指标不一致:{current_metrics} != {metric_names}"
            )

        for metric_name, score in metrics.items():
            if not 0 <= score <= 10:
                raise ValueError(
                    f"{team_name} 的 {metric_name} 分数必须在 0 到 10 之间"
                )

    return metric_names


def draw_radar_chart(
    teams: dict[str, dict[str, float]],
    title: str,
    output_file: str,
) -> None:
    """
    绘制多球队能力雷达图。
    """
    setup_chinese_font()

    metric_names = validate_team_data(teams)
    metric_count = len(metric_names)
    angles = build_angles(metric_count)

    fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"polar": True})

    for team_name, metrics in teams.items():
        values = list(metrics.values())
        closed_values = close_loop(values)

        ax.plot(angles, closed_values, linewidth=2, label=team_name)
        ax.fill(angles, closed_values, alpha=0.15)

    ax.set_title(title, fontsize=16, pad=20)

    ax.set_xticks(angles[:-1])
    ax.set_xticklabels(metric_names, fontsize=11)

    ax.set_ylim(0, 10)
    ax.set_yticks([2, 4, 6, 8, 10])
    ax.set_yticklabels(["2", "4", "6", "8", "10"], fontsize=9)

    ax.legend(loc="upper right", bbox_to_anchor=(1.2, 1.1))
    ax.grid(True)

    output_path = Path(output_file)
    fig.tight_layout()
    fig.savefig(output_path, dpi=180, bbox_inches="tight")
    plt.close(fig)

    print(f"雷达图已生成:{output_path.resolve()}")


def main() -> None:
    teams = {
        "阿根廷": {
            "进攻": 9.2,
            "防守": 8.4,
            "控球": 9.0,
            "压迫": 8.1,
            "反击": 8.3,
            "近期状态": 9.1,
        },
        "奥地利": {
            "进攻": 7.8,
            "防守": 7.9,
            "控球": 7.4,
            "压迫": 8.8,
            "反击": 8.2,
            "近期状态": 8.0,
        },
    }

    draw_radar_chart(
        teams=teams,
        title="阿根廷 vs 奥地利 球队能力雷达图",
        output_file="argentina_vs_austria_radar.png",
    )


if __name__ == "__main__":
    main()

六、运行脚本

在终端执行:

python worldcup_radar_chart.py

运行后会生成图片:

argentina_vs_austria_radar.png

这张图可以直接放到 CSDN 正文中。

如果中文显示成方块,通常是系统没有对应中文字体。

可以把这里:

plt.rcParams["font.sans-serif"] = [
    "Microsoft YaHei",
    "SimHei",
    "Arial Unicode MS",
    "DejaVu Sans",
]

改成本机已有字体。

Windows 常用:

"Microsoft YaHei"

macOS 常用:

"Arial Unicode MS"

Linux 可以安装中文字体后再配置。


七、代码拆解

1. 为什么要校验数据?

这段函数负责检查不同球队的指标是否一致:

def validate_team_data(teams: dict[str, dict[str, float]]) -> list[str]:
    if not teams:
        raise ValueError("teams 数据不能为空")

    metric_names = list(next(iter(teams.values())).keys())

    for team_name, metrics in teams.items():
        current_metrics = list(metrics.keys())

        if current_metrics != metric_names:
            raise ValueError(
                f"{team_name} 的指标不一致:{current_metrics} != {metric_names}"
            )

        for metric_name, score in metrics.items():
            if not 0 <= score <= 10:
                raise ValueError(
                    f"{team_name} 的 {metric_name} 分数必须在 0 到 10 之间"
                )

    return metric_names

如果阿根廷有 6 个指标,奥地利只有 5 个指标,图就会错位。

所以先校验。

别等图画出来才发现数据歪了。那就像比赛踢完才发现球门装反,虽然荒诞,但人类项目里类似事情并不少见。


2. 为什么要闭合数据?

雷达图是一个多边形。

如果不把第一个点追加到最后,线条不会闭合。

def close_loop(values: list[float]) -> list[float]:
    return values + values[:1]

角度也一样:

def build_angles(metric_count: int) -> list[float]:
    angles = [
        2 * math.pi * index / metric_count
        for index in range(metric_count)
    ]
    return angles + angles[:1]

指标数量是 6,所以雷达图会把圆分成 6 个方向。


3. 为什么用 polar=True?

这里创建的是极坐标图:

fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"polar": True})

普通坐标图适合折线图、柱状图。

极坐标更适合雷达图。


4. 为什么设置 y 轴范围 0 到 10?

因为我们的评分设定是 10 分制:

ax.set_ylim(0, 10)
ax.set_yticks([2, 4, 6, 8, 10])

如果后续你改成百分制,比如 0 到 100,只要修改这里即可:

ax.set_ylim(0, 100)
ax.set_yticks([20, 40, 60, 80, 100])

八、如何让数据更真实?

上面的评分是演示数据。

如果要更接近真实分析,可以把指标来源拆开。

比如:

进攻能力

可以参考:

  • 场均进球
  • 场均射门
  • 射正率
  • 关键传球
  • xG

防守稳定

可以参考:

  • 场均失球
  • 被射门次数
  • 抢断
  • 拦截
  • 门将扑救

控球能力

可以参考:

  • 控球率
  • 传球成功率
  • 中场推进次数
  • 后场出球稳定性

压迫强度

可以参考:

  • 前场抢断
  • 高位逼抢次数
  • 对手后场失误次数
  • PPDA

反击速度

可以参考:

  • 快攻次数
  • 转换进攻推进距离
  • 反击射门数
  • 边路推进成功率

近期状态

可以参考:

  • 最近 5 场战绩
  • 最近 5 场进球
  • 最近 5 场失球
  • 核心球员状态
  • 伤停情况

如果只是写 CSDN 示例,不需要一口气接入所有真实数据。

先把图画出来,再逐步接真实数据源。

不然一开始就想做全自动体育数据平台,最后很可能只收获一个半成品文件夹和一点精神损耗。开发者对此应该都不陌生。


九、改成读取 JSON 数据

如果不想把数据写死在 Python 代码里,可以单独建一个 teams.json

{
  "阿根廷": {
    "进攻": 9.2,
    "防守": 8.4,
    "控球": 9.0,
    "压迫": 8.1,
    "反击": 8.3,
    "近期状态": 9.1
  },
  "奥地利": {
    "进攻": 7.8,
    "防守": 7.9,
    "控球": 7.4,
    "压迫": 8.8,
    "反击": 8.2,
    "近期状态": 8.0
  }
}

然后修改读取逻辑:

import json
from pathlib import Path


def load_team_data(file_path: str) -> dict[str, dict[str, float]]:
    path = Path(file_path)

    if not path.exists():
        raise FileNotFoundError(f"文件不存在:{file_path}")

    with path.open("r", encoding="utf-8") as file:
        return json.load(file)

main() 中使用:

def main() -> None:
    teams = load_team_data("teams.json")

    draw_radar_chart(
        teams=teams,
        title="阿根廷 vs 奥地利 球队能力雷达图",
        output_file="argentina_vs_austria_radar.png",
    )

这样后续要画法国、挪威、日本、巴西,只需要换 JSON 文件即可。


十、扩展:支持多场比赛批量出图

如果世界杯期间每天要做多场比赛,可以把数据组织成这样:

{
  "阿根廷_vs_奥地利": {
    "阿根廷": {
      "进攻": 9.2,
      "防守": 8.4,
      "控球": 9.0,
      "压迫": 8.1,
      "反击": 8.3,
      "近期状态": 9.1
    },
    "奥地利": {
      "进攻": 7.8,
      "防守": 7.9,
      "控球": 7.4,
      "压迫": 8.8,
      "反击": 8.2,
      "近期状态": 8.0
    }
  },
  "法国_vs_伊拉克": {
    "法国": {
      "进攻": 8.9,
      "防守": 8.5,
      "控球": 8.6,
      "压迫": 8.2,
      "反击": 8.7,
      "近期状态": 8.8
    },
    "伊拉克": {
      "进攻": 7.0,
      "防守": 7.2,
      "控球": 6.8,
      "压迫": 7.1,
      "反击": 7.5,
      "近期状态": 6.9
    }
  }
}

然后循环生成多张图:

def draw_multiple_matches(matches: dict[str, dict[str, dict[str, float]]]) -> None:
    for match_name, teams in matches.items():
        output_file = f"{match_name}_radar.png"
        title = f"{match_name.replace('_', ' ')} 球队能力雷达图"

        draw_radar_chart(
            teams=teams,
            title=title,
            output_file=output_file,
        )

这样可以把世界杯日更变成一个可复用的小工具。

每天只维护数据,脚本负责生成图表。


十一、雷达图适合放在哪里?

如果你是写 CSDN,建议文章结构这样安排:

  1. 开头说明世界杯场景
  2. 给出指标设计
  3. 给出 Python 代码
  4. 展示生成图
  5. 解释图中差异
  6. 提醒数据只是分析辅助,不是赛果预测

如果是发公众号或今日头条,可以把代码部分删掉,只保留雷达图和图解。

如果是发掘金,可以进一步封装成组件或做成自动化脚本。

同一个选题,换平台可以换表达方式。

这比每天复制一篇“赛前分析报告”清醒得多,终于不像内容流水线在自我催眠。


十二、总结

本文用 Python 和 matplotlib 绘制了一张世界杯球队能力雷达图。

以阿根廷 vs 奥地利为例,我们把球队能力拆成 6 个维度:

  • 进攻
  • 防守
  • 控球
  • 压迫
  • 反击
  • 近期状态

通过雷达图,可以更直观地看到两支球队的风格差异。

阿根廷更突出的是进攻、控球和近期状态。
奥地利更值得关注的是压迫强度和转换速度。

这类图表非常适合世界杯期间做技术化内容:

既能挂热点,又有代码;
既能讲数据,又不写成玄学预测;
既适合 CSDN,也能扩展到公众号、知乎和头条。

技术人追世界杯热点,不一定要写球评。

也可以用 Python 把比赛看点画出来。


轻量补充

如果你长期使用 ChatGPT、Claude、Gemini、Cursor 或 Kiro 这类 AI 工具做代码生成、图表解释、文章改写和数据整理,也可以把 gpt68.com 作为第三方 AI 会员充值平台入口之一了解。它解决的是 AI 工具订阅充值流程问题,不是替代工具本身。使用前建议看清套餐说明、到账说明和售后规则。

更多推荐