简介面向高能物理与数据科学领域中使用 ROOT 框架的 C 开发者plotIt 是一套专注直方图绘制与样式定制的小型工具库。它封装了 ROOT 直方图创建、填充、绘图及后处理流程支持单维/多维直方图、误差棒、自定义配色、轴标签与统计信息帮助用户将精力从样板代码转移到数据分析本身。资源以 zip 格式提供共 17 个文件、约 22KB。其中 6 个头文件与 3 个 C 源文件构成核心库Makefile 与 3 个 shell 脚本负责编译和环境配置另有 Python 配置、YAML 示例及 README 辅助文档结构紧凑便于直接解压编译或按需集成到现有项目。包内含完整源码与可运行示例可让读者快速理解 API 设计、直方图绘制逻辑和项目构建方式已有 256 人学习适合希望提高 ROOT 可视化效率并减少重复代码的初学者与进阶开发者。1. plotIt 是干什么的给 ROOT 直方图绘图省掉一半样板代码plotIt 是一个面向 ROOT 数据格式的直方图绘制工具核心解决高能物理分析里最重复的那部分工作pre-selection 图上几十个变量每一个都要分箱、设坐标轴标题、填 Data 和蒙特卡洛样本、再做归一化叠加。手写 TTree::Draw 加 TCanvas 的样板代码一张图二十行起步五十张图就是上千行而且每次改样本、改 binning 都得动 C 代码再重新编译。plotIt 的做法是把画什么、分箱多少、哪些样本叠在一起、颜色怎么分配全部抽到一份配置文件里绘图变成一条命令。它最合适的人是写分析代码的物理工作者也适合刚拿到 ROOT 文件、想快速盘一遍数据分布的下游同学。下面我就按“怎么跑起来 → 怎么批量用 → 坏了怎么查”这个顺序把话说透。2. 先立住TTree 直方图绘制的痛点与 plotIt 的设计取舍2.1 手画一个变量要同时管五件事假设你要画某个 TTree 里muon_pt的分布再叠加两个 MC 样本和 Data 做比较。传统写法里你需要同时关心这五件事Draw 的表达式和 weight 怎么写、分箱边界怎么设、画布和 Pad 怎么创建、坐标轴标题和统计框怎么调、最后用什么格式存盘。这五件事全混在一个脚本里彼此耦合。// 传统 TTree::Draw 方式画一个变量并叠加三个样本 TCanvas* c new TCanvas(c, c, 800, 600); TH1F* h_data new TH1F(h_data, ;p_{T}^{#mu} [GeV];Events, 20, 0, 100); tree_data-Draw(muon_pth_data, , goff); TH1F* h_sig new TH1F(h_sig, , 20, 0, 100); tree_sig-Draw(muon_pth_sig, weight_sf, goff); h_sig-Scale(h_data-Integral() / h_sig-Integral()); TH1F* h_bkg new TH1F(h_bkg, , 20, 0, 100); tree_bkg-Draw(muon_pth_bkg, weight_sf, goff); h_bkg-Scale(h_data-Integral() / h_bkg-Integral()); h_sig-SetLineColor(kRed); h_bkg-SetLineColor(kBlue); h_sig-Draw(hist); h_bkg-Draw(hist same); h_data-Draw(epsame); c-SaveAs(muon_pt.png);这段代码里有几个容易被新手忽略的坑h_data这种重定向语法要求目标直方图已经存在而且分箱必须两边保持一致否则叠加时 shape 全是歪的Scale 的归一化系数算的是 Integral不是 entries两者在有 weight 时差很远goff选项关掉图形绘制在批处理里是必须的忘了加就可能卡在 GUI 上。每多一个变量、多一个样本这段代码就要复制一遍改分箱要改三个地方颜色一多就乱。我见过不少分析仓库里这类脚本最终膨胀到几百行维护成本非常高。2.2 plotIt 的核心取舍把“画什么”和“怎么画”拆开plotIt 的设计思路和手写脚本不一样它把直方图的生成过程拆成两层。第一层是“画什么”在一份配置文件里声明变量名、变量表达式、分箱区间、权重表达式和剪枝条件。第二层是“怎么画”用命令行参数控制输出格式、画布尺寸、样式模板、叠加方式、是否需要比值图。这两层在配置文件层面是彻底分开的你不需要碰一行 C 代码。这种设计能站住是因为高能物理分析的绘图需求其实高度套路化变量列表基本固定样本列表基本固定变的只是某个阶段的 binning 或者某个 cut 的上限。把这些放在配置文件里改一格分箱就是改一个数字不需要重新编译。真正的 C 代码只是执行配置的引擎对用户来说是个黑匣子也没关系因为配置文件的语义足够简单出了问题基本都能从输出日志定位。我自己用下来的体会是plotIt 不是要取代 ROOT 的绘图能力而是把精力从“反复写样板代码”转移到“决策分箱和样本表现”上。它把绘图的入口从代码变成了数据这也是它值得投入时间的原因。2.3 两种方式的边界什么时候直接 TTree::Draw 反而更快手写方式plotIt 方式画单个调试图、快速看某个变量形状合适少引入依赖系统批量产出几十上百张图效率很低代码冗余需要完全自定义画布布局、任意拼接 Pad灵活但代码量巨大运行环境经常更换、没有编译权限只要 ROOT 可用就能跑如果你只是临时看一个变量的分布手写两三行 TTree::Draw 确实更快完全没有必要上 plotIt。但一旦你的分析进入“今天改一次 binning、明天加一个样本”的状态配置驱动的优势就出来了。我的经验是第一次把配置文件写好可能多花半小时但后面每次改动都是秒级完成这个“慢开头”非常值。3. 从最小配置跑到第一个输出plotIt 前 30 分钟的用法3.1 安装与依赖先确认 ROOT 版本和文件格式plotIt 是建在 ROOT 之上的工具。安装前先确认两件事本机 ROOT 版本是不是 6 系列以及你要读的 ROOT 文件是普通的 TTree 还是 TTree 的 friend 形式因为配置里需要按实际文件结构写变量名。如果 ROOT 环境变量还没初始化通常要先source /path/to/root/bin/thisroot.sh。不同发行版对编译依赖的要求不太一样但核心就一条保证能通过root-config --cflags --libs拿到正确的编译参数。装好之后先跑plotIt --help确认版本和当前支持的命令行开关因为不同小版本的开关名偶尔不太一致。3.2 最小配置文件变量、分箱、样本各归各plotIt 不同版本的配置格式有细微差异但核心结构是一致的一个变量区块描述“画什么”一个样本区块描述“用哪些树、权重是什么”。下面是我习惯的最小配置文件结构你可以直接照着改!-- plotIt_mini.xml: 最小可用配置画一个 muon_pt 变量 -- plotIt variables variable namemuon_pt expressionmuon_pt titlep_{T}^{#mu} [GeV];Events binning(20, 0, 100) / /variables samples sample nameData filedata.root treeEvents colorkBlack markerkFullCircle / sample nameMC filemc.root treeEvents weightweight_sf colorkRed fillkSolid / /samples stacks stack namemain samplesMC, Data normsample/ /stacks /plotIt这段配置里几个字段值得说清楚。expression是树里真实存在的分支名或者一个合法的 ROOT 表达式比如sqrt(px*pxpy*py)不能用那种重定向语法。binning写成(20, 0, 100)表示 20 个 bin、范围 0 到 100这是绘图里最常用的分箱写法改成(50, 0, 200)只需要动一个括号。normsample的含义是按样本归一化也就是让每个样本的面积都归一为 1 再叠加不适合展示相对比例如果要把 MC 归一化到 Data 的积分需要额外写normlumi或按横截面缩放这个后面细说。配置文件里的元素名在不同 plotIt 版本里可能叫variables也可能叫别的但变量、样本、叠加这三个核心概念是通用的。3.3 跑命令一条指令出图日志里藏了所有答案配置文件写好之后常见调用方式是这样的plotIt -c plotIt_mini.xml -o ./output --format png --loglevel info-c指定配置文件-o指定输出目录--format选择输出图片格式--loglevel info让日志打到 info 级别。跑完以后输出目录里会多出以变量名命名的图片文件比如muon_pt.png同时终端里会打印每个直方图的条目数、积分值和归一化系数。我这里喜欢盯着三行日志看第一是每个样本的 entries 数如果 MC 样本显示 0说明 tree 名或者文件名写错了第二是归一化系数如果系数是 NaN 或者负数说明权重列里有非法值第三是 finalize 阶段有没有警告信息比如分箱溢出。--loglevel debug通常能打出每个变量到底从哪棵树、哪个表达式生成的排查时把它打开比瞎猜变量名拼写错误效率高得多。第一次跑通以后这段命令行几乎不用再改真正高频改动的是配置文件本身。4. 花样都在配置里叠加、比值图与统一样式的正确姿势4.1 MC 样本叠加与顺序问题大多数分析图不是单画一个直方图而是把几个 MC 过程堆叠起来再把 Data 点画在最上面。在 plotIt 里这完全由stacks区块描述。我印象最深的是 stack 的绘制顺序排在前面的样本画在底层后面的样本叠在上面。如果配置文件里把信号样本写在背景样本前面信号就会被背景盖住肉眼很难看出来。!-- 推荐顺序背景在前信号在后Data 点在最上层 -- stacks stack namemain samplesQCD, Wjets, Zjets, Signal, Data normmc/ /stacksnorm参数是这里最容易搞错的地方。写normmc表示把 MC 样本归一化到 Data 的总积分这是数据驱动 luminosity 分析最常见的需求写normsample则每个样本独立归一为 1适合看形状而不是比例。我一般会在分析早期用sample模式检查每个 MC 过程的形状对不对确认没有 bug 之后换成mc模式出正式图。有个细节如果 Data 的积分因为剪枝条件变化而明显变少MC 的归一化系数也会跟着变不要下意识觉得是 plotIt 出了 bug先回去查剪枝条件才是正路。4.2 Data/MC 比值图别把 ratio 写在变量里很多期刊和图注要求 MC 和 Data 不只是画在同一张图上还要在下方附一张比值图来展示数据与模拟的偏离程度。plotIt 的常见做法是在配置里显式声明一个比值变量或者把 ratio pad 作为输出选项打开。不同版本对 ratio 的支持方式不太一样我习惯先看--ratio这一个开关是否存在不存在就在配置里用比值表达式生成附加图。!-- 在配置里额外生成一个比值变量 -- variables variable namemuon_pt_ratio expressionmuon_pt titleData / MC;p_{T}^{#mu} [GeV];Data/MC binning(20, 0, 100) ratioData/MC / /variables需要额外说明的是ratio 图里最怕的一件事是分母里有空 bin也就是某个分箱里 MC 积分恰好为 0导致比值变成无穷大。plotIt 对空 bin 的默认处理可能只是跳过不画但在输出日志里会有 warning。如果你发现画出来的比值图上有断点第一反应应该是检查是哪个 bin、哪个样本的积分为 0而不是去调坐标范围。断点的解法一般是加一条剪枝条件把那个区域排除或者改用更粗的分箱把这些区域的 MC 统计量垫起来。4.3 样式模板从一张图到一百张图的统一管理出场率高的第二个需求是统一风格所有输出图片的字体、字号、画布尺寸、统计框位置、Logo 位置必须一致。这部分属于“怎么画”的范畴我不建议在变量配置里逐个设置颜色和线型因为变量几十个之后你根本维护不过来。plotIt 这类工具普遍支持一个全局样式文件在启动时传入即可plotIt -c plotIt_mini.xml -s my_style.json -o output_png --format png样式文件里常见的字段包括画布宽高、轴标签字号、MC 颜色映射表、Data 点样式、统计框开关。如果工具版不支持 JSON 样式文件用 ROOT 的SetStyle脚本在初始化阶段设置也是一样效果。关键是“全局生效”项目里哪怕几百张图风格调整只改一个文件重跑一遍全部生效这比手动改代码里每个SetLineColor要可靠得多。5. plotIt 四个高发坑的排查记录现象、原因、解决顺序5.1 直方图全空entries 为 0图片上只有空坐标轴用 ROOT 的 TTree 画图时最容易犯的错误是文件名对、树名不对或者变量名拼写和分支实际名字不一致。plotIt 只要有一个变量匹配不到就不会报 fatal 错误而是直接在日志里标记“变量未找到”输出一张空图。这个坑之所以高发是因为 ROOT 文件里的分支名经常带长前缀比如Muon_pt而不是muon_pt大小写差一点就静默失败。排查顺序建议先看日志里每个样本的 entries 行是不是大于 0再确认变量表达式在 ROOT 交互环境里能否用tree-GetBranch(muon_pt)查到。最后用--loglevel debug再跑一次看它到底从哪个文件、哪个分支取的数据。解决方式很简单把配置里的expression改成与实际分支一致的名字即可。这种现象的本质是配置驱动工具的静默失败太多了日志不仔细看就是在给后面埋雷。5.2 stack 顺序与预期完全相反背景盖住了信号第一次用 plotIt 出 MC 叠加图时不少同学会发现自己明明是“信号在上方”结果信号过程被堆在底部完全看不到。原因在于 stack 的绘制顺序是配置文件中samples列表的声明顺序先声明的先画在底层。如果配置文件先写了 Signal 再写 QCD那 QCD 会覆盖在 Signal 上面。解决方式是调换stacks区块里样本的顺序把背景过程全部放在前面信号过程放中间Data 放在最后。这里我还会加一条经验性的建议如果叠加图里过程数量超过 5 个打开日志检查输出的绘制顺序列表确认每个样本的 z-order 符合预期而不要用肉眼在 PNG 上猜因为颜色相近的时候肉眼判断根本不靠谱。5.3 归一化后的 MC 总数和 Data 差很多但单个直方图 entries 看起来正常现象是 MC 形状看着没问题但叠加后 MC 总数明显低于 Data归一化似乎完全没生效。常见原因有两个一是权重列里有大量 0 或负值导致积分被拉低二是normsample写错了让它按样本自身积分归一化而不是按 Data 积分归一化。负权重在高能物理 MC 里是合法存在的比如 NLO 计算的某些区域但负权重过多会让总积分很不稳定画出来的误差棒也容易低估。解决方式是先手动在 ROOT 里对同一变量算一遍tree-Draw(muon_pt, weight_sf, goff)对比 entries 和 Integral 与 plotIt 日志输出是否一致。如果手动结果和 plotIt 结果一致那就是权重本身的问题需要在配置里加剪枝条件去除极端权重如果不一致那反而说明配置里有拼写错误让某个样本意外读成了别的树。5.4 中文字体乱码或方框标题里写了中文输出图全是方块物理图的坐标轴标题通常是英文但有些国内组会习惯在图上标注中文或者文件名本身包含中文。现象是 ROOT 的默认字体不含中文字形所有中文标题在输出 PNG 里都会变成方框。这个问题其实和 plotIt 无关是 ROOT 渲染字体的问题。解决方式分两步第一统一把标题里的中文改成英文或 LaTeX 数学符号这是最省事的路线第二如果必须用中文在样式配置里设置一个支持中文的字体比如系统自带的 simhei.ttf并在初始化脚本里用gStyle-SetLabelFont、gStyle-SetTitleFont指定这个字体的编号。注意单改标题字体还不够坐标轴标签字体是另一套字号和字体 id要一起改才能完全避免方框。6. 最后八百米批量出图之后我会先做这三项自检跑完 plotIt 不等于工作完成输出图能看也不代表结果是对的。我现在的习惯是出图之后立刻跑一个小脚本对输出 ROOT 文件做三项自检全过了才把图放进分析笔记。第一步是核对直方图名称与配置变量一一对应确认没有漏画第二步是检验每个直方图的 entries 总数和源 TTree 的条目数能否对得上避免剪枝条件误伤第三步是检查归一化系数是否接近预期而不是出现了 NaN 或负数。# check_plotit_output.py: 对 plotIt 产物做快速自检 import ROOT f ROOT.TFile.Open(plotit_output.root, READ) expected { muon_pt: 10000, muon_eta: 10000, } for name, expected_entries in expected.items(): h f.Get(name) if not h: print(f[FAIL] missing histogram: {name}) continue entries h.GetEntries() integral h.Integral() status OK if abs(entries - expected_entries) 10 else CHECK print(f[{status}] {name}: entries{entries}, integral{integral:.1f}) f.Close()这个脚本里我特意把 entries 和 integral 分开打印因为两者反映的问题不一样entries 对不上是样本读取或剪枝的问题integral 对不上则主要是归一化和权重的问题。checked 的数值范围要按你的分析实际填写我这里只是演示写法。比值图的自检我用过更笨但很有效的办法用一个权重全为 1 的假 MC 跑一遍同样的配置看比值图是否稳定在 1 附近如果明显偏移说明配置里的 ratio 表达式写错了而不是绘图引擎的问题。这三步做完我才会把图发给合作者。这个习惯救过我一次有一次跑完图觉得 MC 形状很好自检脚本却鸣了警追下去发现是权重列在某个文件里被人为归一化过分布整体偏了一个量级。如果那批图直接发出去后面的全套数字都要推倒重来。希望这个检查流程也能帮到你——工具能摊平操作成本但校验的责任始终在你自己手里。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?