最近在不少二次元社区和虚拟主播讨论区里经常能看到一种很有意思的分析方式给角色、主播或者前后辈关系打上“阵营九宫格”标签再用坐标轴映射出“印象坐标分类光谱”。这种玩法本质上是一种二维语义分类模型把复杂的人物印象投影到两到三个可解释的维度上再通过可视化呈现出来。作为一名 Python 爱好者我第一反应是这种九宫格不就是一个经典的可视化分类问题吗与其手动在图片软件里一个个拖位置不如直接用 Python 写一个通用工具输入角色名和坐标就能自动生成一张“阵营九宫格印象坐标分类光谱图”。这样不管是做虚拟主播关系分析、游戏角色阵营梳理还是团队协作风格盘点都能复用同一套代码。本文就围绕这个场景完整讲解从数据模型设计、坐标映射、光谱渐变到可视化导出的全过程。读者不需要太深厚的基础只要能跑 Python跟着步骤就能生成属于自己的九宫格光谱图。1. 背景与核心概念1.1 什么是阵营九宫格阵营九宫格最早源于经典桌面角色扮演游戏中的阵营体系它用两个互相独立的轴来描述一个角色或群体的行为倾向横轴代表“守序—中立—混乱”描述的是对规则、传统、秩序的尊重程度。纵轴代表“善良—中立—邪恶”描述的是道德取向和利他/利己倾向。两个轴交叉就形成 3×3 的九宫格。每个格子都有明确的含义比如“守序善良”是经典圣骑士形象“混乱邪恶”则是典型的破坏者。在二次元社区和虚拟主播文化中这种九宫格被进一步泛化两个轴的语义可以替换成任何二元对立的维度。例如横轴理性—感性。纵轴强势—弱势。或者是“前后辈关系”中的横轴亲密—疏远。纵轴依赖—独立。这种做法的本质是把人对人的“印象”这种模糊概念降维到二维坐标里形成可视化、可比较、可传播的分类标签。1.2 印象坐标分类光谱是什么“印象坐标分类光谱”可以理解成一种比九宫格更精细的表示方式。九宫格只能表达“在哪一个格子”而光谱式表示允许对象落在格子内部的具体位置甚至可以在两个格子之间过渡。例如A 角色在“善良”轴上的得分是 6.5满分为 10在“守序”轴上的得分是 7.2那么这个点会落在“守序善良”格子的内部靠近右上角而 B 角色得分是 4.8 和 5.1则会更接近中心格。用连续坐标代替离散格子能表达更多细节同一格子内的不同距离可以表达“典型程度”。靠近轴线的点可以表达“模糊地带”。颜色深浅可以表达“强度”或“置信度”。1.3 为什么用 Python 做这件事如果只是做一张两张图用 PPT 或在线画图工具也能完成。但一旦角色数量超过 20 个或者需要批量更新坐标、统一风格、生成不同版本的图手动操作就会非常痛苦。用 Python 的好处是数据与表现分离坐标数据用 CSV 或 JSON 保存图只是数据的可视化结果。可批量生成修改数据后重新运行脚本即可。可定制样式颜色、字体、轴标签、网格样式全部代码可控。可扩展后续接入 Web 服务、生成 GIF、自动分析聚类都很方便。本文的代码不依赖任何特定社区或人物只实现通用能力。读者拿到后可以替换成自己的数据集。2. 环境准备与版本说明2.1 运行环境本文示例代码在以下环境中验证通过操作系统Windows 10 / macOS 均可Linux 同样支持。Python 版本3.8 及以上。建议使用虚拟环境避免污染全局 Python。2.2 依赖库需要安装以下 Python 库库名用途matplotlib绘制九宫格图形numpy数值计算与坐标转换pandas读取和整理 CSV 数据安装命令如下pip install matplotlib numpy pandas如果你使用的是国内镜像源可以加-i参数加速pip install matplotlib numpy pandas -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 示例项目结构本文采用以下项目结构grid-align/ ├── data/ │ └── characters.csv ├── output/ │ └── grid_chart.png ├── config.py ├── grid_visualizer.py └── run.py如果读者只是快速测试也可以先不考虑目录结构把代码写在同一个.py文件中。后面我会按完整项目的思路来组织代码。3. 核心设计坐标模型与光谱映射3.1 定义二维坐标轴在编码之前先把抽象模型固定下来。我们约定横轴 X范围从 -1.0 到 1.0负值代表“左侧语义”正值代表“右侧语义”。纵轴 Y范围从 -1.0 到 1.0负值代表“下方语义”正值代表“上方语义”。原点 (0, 0) 代表中立。举一个通用的例子把横轴定义为“守序 → 混乱”把纵轴定义为“善良 → 邪恶”守序善良X -1Y 1 附近。绝对中立X 0Y 0。混乱邪恶X 1Y -1。在“前后辈关系”场景中可以把横轴定义为“公事公办 → 私人亲密”纵轴定义为“后辈主导 → 前辈主导”。具体语义完全由使用者决定代码不关心轴的名字。3.2 从离散九宫格到连续光谱如果我们只显示网格那么任意一个坐标点只需要判断它落在哪一个格子。但“光谱”要求我们显示渐变效果。实现思路把图形背景绘制成 3×3 的色块网格。每个色块颜色 基础色 坐标偏移影响。每个角色点用散点绘制点的颜色可以用第三个指标映射例如“印象强度”。这样做的好处是角色点落到哪个网格一目了然同时网格内部有渐变表达光谱连续感。3.3 颜色映射方案颜色是光谱可视化中最重要的部分。这里推荐两种方案方案一按九宫格固定颜色。每个格子使用固定颜色例如格子颜色守序善良#4C72B0中立善良#55A868混乱善良#C44E52守序中立#8172B3绝对中立#CCB974混乱中立#64B5CD守序邪恶#8C8C8C中立邪恶#6E6E6E混乱邪恶#2E2E2E方案二连续渐变映射。把 X 轴映射到色相Hue把 Y 轴映射到亮度Lightness这样每个坐标点都有唯一的颜色。这种方式更接近“光谱”的感觉。本文的示例代码采用混合方案先绘制固定颜色的九宫格背景再叠加半透明的渐变网格最后绘制角色散点。3.4 数据文件格式为了便于维护使用 CSV 文件保存角色数据name,x,y,color,size 角色A,-0.8,0.9,#FF6666,120 角色B,0.2,0.3,#66FF66,80 角色C,0.9,-0.8,#6666FF,150字段说明字段类型含义namestr角色/人物名称xfloat横轴坐标范围 [-1, 1]yfloat纵轴坐标范围 [-1, 1]colorstr散点颜色可选留空则自动分配sizeint散点大小可选默认 1004. 完整实战案例生成阵营九宫格印象坐标分类光谱图下面进入正题。我们将完成一个可以实际运行的项目输入一份 CSV 数据输出一张带九宫格背景和角色散点的光谱图。4.1 创建项目结构和数据文件首先创建目录结构mkdir -p grid-align/data mkdir -p grid-align/output然后创建数据文件data/characters.csvname,x,y,color,size 守序善良型,-0.85,0.85,#E74C3C,160 中立善良型,-0.1,0.9,#3498DB,120 混乱善良型,0.8,0.8,#2ECC71,140 守序中立型,-0.9,0.1,#9B59B6,110 绝对中立型,0,0,#95A5A6,200 混乱中立型,0.85,-0.1,#F39C12,130 守序邪恶型,-0.8,-0.85,#1ABC9C,90 中立邪恶型,0.1,-0.9,#E67E22,100 混乱邪恶型,0.9,-0.8,#C0392B,150这里我用了 9 个示例数据点实际使用时替换成你自己的角色名和坐标即可。4.2 编写配置文件 config.py配置文件集中管理轴名称、颜色、输出路径等参数方便后续调整。# config.py # 项目配置文件 # 输出图片路径 OUTPUT_PATH output/grid_chart.png # 横轴左右标签 X_AXIS_LEFT 守序 X_AXIS_RIGHT 混乱 # 纵轴上下标签 Y_AXIS_TOP 善良 Y_AXIS_BOTTOM 邪恶 # 图表标题 TITLE 阵营九宫格印象坐标分类光谱图 # 是否显示网格线 SHOW_GRID True # 是否显示背景渐变 SHOW_GRADIENT True # 坐标点透明度 POINT_ALPHA 0.85 # 字体大小 FONT_SIZE 12这些配置项会在主程序中读取。如果你不习惯用配置文件也可以直接把参数写在grid_visualizer.py中。4.3 编写核心可视化模块 grid_visualizer.py核心模块负责加载数据、绘制网格和散点、保存图片。# grid_visualizer.py import os import numpy as np import pandas as pd import matplotlib.pyplot as plt from matplotlib.patches import Rectangle from matplotlib.colors import LinearSegmentedColormap import matplotlib.font_manager as fm import config def load_data(csv_path): 读取 CSV 数据文件返回 DataFrame。 df pd.read_csv(csv_path) # 确保必备列存在 required_cols [name, x, y] for col in required_cols: if col not in df.columns: raise ValueError(fCSV 文件缺少必需列: {col}) # 如果 color 列不存在补默认值 if color not in df.columns: df[color] #333333 # 如果 size 列不存在补默认值 if size not in df.columns: df[size] 100 return df def draw_background(ax): 绘制九宫格背景。 # 9 个格子的基准颜色 grid_colors [ #C0392B, #E67E22, #F1C40F, #2ECC71, #95A5A6, #3498DB, #9B59B6, #8E44AD, #1ABC9C, ] # 说明这里颜色顺序是 3行3列按 从左到右、从上到下 排列。 # 实际使用时可以根据自己的语义调整颜色顺序。 for row in range(3): for col in range(3): idx row * 3 col color grid_colors[idx] ax.add_patch( Rectangle( (col - 1.5, 1.5 - row - 1), width1, height1, facecolorcolor, alpha0.15, edgecolorgray, linewidth1.2, ) ) def draw_gradient(ax): 绘制背景渐变增强光谱感。 # 生成一个 2D 网格用于渐变填充 x np.linspace(-1, 1, 256) y np.linspace(-1, 1, 256) X, Y np.meshgrid(x, y) # 渐变值 横坐标 和 纵坐标 的线性组合 Z (X 1) / 2 * 0.6 (Y 1) / 2 * 0.4 ax.imshow( Z, extent[-1.5, 1.5, -1.5, 1.5], originupper, cmapcoolwarm, alpha0.08, aspectauto, ) def draw_grid_lines(ax): 绘制网格分割线。 # 绘制垂直线 for x in [-0.5, 0.5]: ax.axvline(xx, colorgray, linestyle--, linewidth1) # 绘制水平线 for y in [-0.5, 0.5]: ax.axhline(yy, colorgray, linestyle--, linewidth1) # 绘制中心十字虚线 ax.axvline(x0, colorblack, linestyle:, linewidth0.8, alpha0.6) ax.axhline(y0, colorblack, linestyle:, linewidth0.8, alpha0.6) def draw_points(ax, df): 在图上绘制角色散点。 for _, row in df.iterrows(): ax.scatter( row[x], row[y], srow[size], colorrow[color], alphaconfig.POINT_ALPHA, edgecolorswhite, linewidths1.5, zorder5, ) ax.annotate( row[name], (row[x], row[y]), textcoordsoffset points, xytext(8, 6), fontsizeconfig.FONT_SIZE - 2, zorder6, ) def setup_ax(ax): 设置坐标轴范围和标签。 ax.set_xlim(-1.5, 1.5) ax.set_ylim(-1.5, 1.5) # 设置坐标轴标签 ax.text(0, -1.25, config.X_AXIS_LEFT, hacenter, vacenter, fontsizeconfig.FONT_SIZE) ax.text(1.4, -1.25, config.X_AXIS_RIGHT, hacenter, vacenter, fontsizeconfig.FONT_SIZE) ax.text(-1.45, 0, config.Y_AXIS_BOTTOM, hacenter, vacenter, fontsizeconfig.FONT_SIZE, rotation90) ax.text(-1.45, 1.35, config.Y_AXIS_TOP, hacenter, vacenter, fontsizeconfig.FONT_SIZE, rotation90) ax.set_xticks([]) ax.set_yticks([]) # 隐藏边框 for spine in ax.spines.values(): spine.set_visible(False) def create_chart(csv_pathos.path.join(data, characters.csv)): 主函数加载数据并生成图表。 df load_data(csv_path) # 创建画布 fig, ax plt.subplots(figsize(10, 10)) # 绘制背景 draw_background(ax) if config.SHOW_GRADIENT: draw_gradient(ax) # 绘制网格线 if config.SHOW_GRID: draw_grid_lines(ax) # 绘制散点 draw_points(ax, df) # 设置坐标轴 setup_ax(ax) # 设置标题 ax.set_title(config.TITLE, fontsizeconfig.FONT_SIZE 4, pad20) # 保存图片 output_path config.OUTPUT_PATH os.makedirs(os.path.dirname(output_path), exist_okTrue) plt.savefig(output_path, dpi200, bbox_inchestight) print(f图片已保存到: {output_path}) # 显示图片 plt.show() if __name__ __main__: create_chart()这段代码有几个关键点我逐一解释一下。draw_background函数中Rectangle的(col - 1.5, 1.5 - row - 1)是每个格子的左下角坐标。因为matplotlib的Rectangle接收的是左下角坐标和宽高我需要把 3×3 的格子布局转换到[-1.5, 1.5]的坐标系中。draw_gradient函数生成一个二维渐变背景extent参数让它和坐标范围对齐。alpha0.08让渐变不至于盖住网格底色。draw_points函数遍历 DataFrame 的每一行用scatter绘制散点用annotate在点旁边标注名称。zorder5和zorder6确保散点和文字显示在背景上方。4.4 编写入口文件 run.py入口文件只需要一行方便运行# run.py from grid_visualizer import create_chart if __name__ __main__: create_chart()4.5 运行与验证在项目根目录执行python run.py如果一切正常你会看到output/grid_chart.png文件生成图片中包括3×3 的九宫格背景。半透明渐变层。虚线网格线。角色散点和名称标注。横轴和纵轴的语义标签。如果你用的是 macOS 或 Linux 系统可能会出现中文字体显示为方框的情况。这是因为系统缺少中文字体。解决方法是在grid_visualizer.py中手动指定中文字体# 在 create_chart 函数开头添加 plt.rcParams[font.sans-serif] [Arial Unicode MS, SimHei, PingFang SC] plt.rcParams[axes.unicode_minus] False4.6 结果扩展自动分配颜色如果 CSV 中没有指定颜色可以按角色所在格子自动分配颜色。增加一个函数def auto_color_by_cell(x, y): 根据坐标所在的格子自动生成颜色。 from matplotlib.colors import to_hex # 简单的颜色映射X 偏移影响 R 通道Y 偏移影响 B 通道 r (x 1) / 2 * 255 g 120 b (y 1) / 2 * 255 return to_hex((r / 255, g / 255, b / 255))在load_data之后对没有color列的数据执行if color not in df.columns: df[color] df.apply(lambda row: auto_color_by_cell(row[x], row[y]), axis1)这样不指定颜色也能生成彩色图。5. 常见问题与排查思路在实际运行这段代码时大家可能会遇到一些典型问题。我把它们整理成一张表方便排查。问题现象常见原因解决思路中文名称显示为方框系统缺少中文字体手动指定系统已安装的中文字体名图片保存后没有内容坐标设置超出边界检查 CSV 中 x、y 是否都在 [-1, 1] 范围内散点重叠严重多个角色坐标过于接近加入微小的随机扰动或使用更大的画布报错KeyError: nameCSV 表头不是英文检查 CSV 第一行是否包含 name,x,y 列名背景渐变没有显示SHOW_GRADIENT False修改 config.py 中的配置plt.show()闪退无 GUI 环境去掉plt.show()只保留savefig图片颜色太淡背景 alpha 值过低调大grid_colors的 alpha 值横轴标签位置不对坐标系理解偏差记住matplotlib默认(0,0)在左下角生成图片时崩溃提示内存不足画布尺寸过大降低figsize或dpi5.1 中文乱码问题的详细处理中文字体问题是最常见的这里单独说明。首先查看系统有哪些字体可用import matplotlib.font_manager as fm fonts [f.name for f in fm.fontManager.ttflist] for font in sorted(set(fonts)): print(font)执行后选择一个支持中文的字体名称然后全局设置plt.rcParams[font.sans-serif] [SimHei] # Windows # 或者 plt.rcParams[font.sans-serif] [Arial Unicode MS] # macOS # 或者 plt.rcParams[font.sans-serif] [Noto Sans CJK SC] # Linux注意SimHei是 Windows 自带字体Arial Unicode MS是 macOS 自带字体。Linux 需要安装fonts-noto-cjk包。5.2 坐标范围校验为了避免数据点漂移出图可以在load_data函数中增加校验逻辑def load_data(csv_path): df pd.read_csv(csv_path) # 校验坐标范围 if df[x].min() -1 or df[x].max() 1: raise ValueError(x 列数据超出范围 [-1, 1]) if df[y].min() -1 or df[y].max() 1: raise ValueError(y 列数据超出范围 [-1, 1]) return df这样在数据错误时能更快发现问题而不是等到图片生成后才发觉。6. 最佳实践与工程建议6.1 数据管理与命名规范把数据从代码中分离出来是最重要的一步。即使只是个人自用的小工具也建议使用data/目录单独存放 CSV 文件不要硬编码在 Python 里。命名方面建议CSV 文件名使用小写加下划线如member_impressions.csv。列名统一英文避免在代码里混用中英文字段名。坐标列尽量用x、y不要用xx、yy这种不明确的名称。6.2 配置化的可维护性把会变的参数抽到config.py中是一个成本极低但收益很高的工程习惯。例如调整图片尺寸、透明度、输出路径都不需要改核心代码只改配置文件。如果后续要支持多个版本的九宫格图甚至可以建立多套配置config/ ├── config_default.py ├── config_gray.py └── config_colorful.py运行时指定加载哪一套配置即可。6.3 异常处理与日志记录在个人脚本里可以忽略异常处理但如果你想把这个工具分享给团队建议加入简单的异常处理import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) def create_chart(csv_path): try: df load_data(csv_path) logging.info(f成功加载 {len(df)} 条数据) except FileNotFoundError: logging.error(f数据文件不存在: {csv_path}) raise except Exception as e: logging.error(f加载数据失败: {e}) raise这样在批量运行或定时生成图片时可以快速定位问题。6.4 安全性提醒这里虽然是一个本地绘图小工具但也要提醒一点如果 CSV 中的角色名称来自用户输入并且在最终 Web 页面展示需要做好转义防止注入问题。本文的场景是本地脚本正常运行不会有安全风险。不过如果后续把工具封装成 Web 服务就要注意用户上传 CSV 的合法性校验、文件大小限制以及不要直接用eval或exec处理用户数据。6.5 性能优化思路对于几十个点的可视化性能不是瓶颈。但如果角色数量成千上万比如做全平台角色阵营分析散点绘制会变慢。可以考虑增加采样率先绘制轮廓再绘制细节。用ax.scatter一次性传入所有点而不是循环调用。把大图划分成多个小图按格子切片输出。循环调用scatter在数据量大时开销较高推荐改为批量方式def draw_points_batch(ax, df): ax.scatter( df[x], df[y], sdf[size], cdf[color], alphaconfig.POINT_ALPHA, edgecolorswhite, linewidths1.5, zorder5, ) for _, row in df.iterrows(): ax.annotate(row[name], (row[x], row[y]), xytext(8, 6), textcoordsoffset points, fontsizeconfig.FONT_SIZE - 2)6.6 关于九宫格语义的延伸思考九宫格本质上是一种“二维语义投影”。在实际使用中同一套代码可以服务完全不同的场景角色/主播的阵营分析。团队成员的协作风格盘点。用户对产品的印象分群。前后辈关系中的互动倾向。关键是定义好两个轴的语义并且坐标系方向要固定。如果你在 A 场景中定义“左边是守序右边是混乱”在 B 场景中就不要反过来否则团队沟通时会产生歧义。7. 总结与下一步学习建议本文从一个社区常见的“阵营九宫格”场景出发实现了一个通用的 Python 可视化工具。整个过程中我们完成了这些关键工作用二维坐标模型表达九宫格分类。通过 CSV 文件管理角色数据实现数据与表现分离。使用 matplotlib 绘制九宫格背景、渐变光谱和角色散点。配置了自定义轴标签、颜色、大小和输出路径。处理了中文乱码、坐标越界、批量绘制等常见问题。如果想继续深入可以从这几个方向扩展交互式可视化用plotly或flaskecharts实现网页端拖拽调整坐标。聚类分析把大量角色的坐标数据用sklearn做聚类自动发现阵营群组。时间序列光谱记录每周角色坐标变化生成动态演变图谱。导出多种风格根据配置生成简洁黑白版、彩色渐变版、像素风版本。在实际项目中最值得优先关注的是数据质量。数据比代码更重要如果坐标语义定义混乱再好看的图也只会误导人。建议在动手之前先把两个轴的含义和坐标方向在团队内对齐。最后提醒一句如果只是临时用一次可以手动拖图但如果这个九宫格图会反复用、多人用、持续更新花半小时把代码整理成上面的形式之后每次更新数据只需改 CSV 文件一秒钟就能重新出图。这套思路对所有“分类 坐标 可视化”需求都是通用的读者可以自由发挥。