主题
漫跑数据 · 可视化操作手册
本手册只记录可视化层:从数据库长表 → Tableau Prep 流程 → .hyper 提取 → .twb 工作簿。 数据入库、建表、每周同步数据库的部分见 ../漫跑数据入库/数据库操作手册.md,本手册不重复。
所有描述都来自对现存文件的静态解析与独立复算。凡写"实测"的, 都是把源数据重新算过一遍、结果与文件内数值对齐的。
本手册写作期间(2026-10-17),
漫跑数据分析.twb被另存过一次 (450919 → 308235 字节)。第四章以后的内容以改动后的文件为准; 早期版本的痕迹只在问题清单里作为历史说明保留。
一条贯穿全文的原则:筛选器决定"看哪一段",不决定"系统是什么"。 换周期只改筛选器的区间,图表结构不用动。所以本手册讲筛选器时, 只讲它作用在什么字段、以什么顺序生效、影响哪些表,不评价具体区间选得对不对。
〇、先看这张图:可视化链路
┌──────────────────────────────────────────┐
│ MySQL 漫跑库.漫跑记录 │
│ 宽表:689 行 × 35 列 │
│ 学号 / 姓名 / 班级 + 16 天 × (开始,结束)│
└───────────────────┬──────────────────────┘
│ Prep「漫跑记录」LoadSql
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Tableau Prep 流程:漫跑数据分析-v2.tfl │
│ │
│ 主链:读库 → 清理1 → 转置-开始 ┐ │
│ └ 转置-结束 ┴→ 清理2/3 → 联接1 → 清理终 → 并集1 ─┐│
│ 旁链:旧格式-高一.csv → 转置5 → 清理旧 ────────────────────────────┴┤│
│ ││
└──────────────────────────────────────────────────────────────────────┘│
▼
┌──────────────────────────────────────────┐
│ 漫跑数据分析-2026R3.hyper │
│ 长表:13091 行 × 10 列 │
│ 日期 / 打卡状态 / 开始 / 结束 / 持续时间 │
└───────────────────┬──────────────────────┘
│ .twb 数据源连接(hyper)
▼
┌──────────────────────────────────────────┐
│ 漫跑数据分析.twb │
│ 6 张工作表 + 1 个空仪表板 │
└──────────────────────────────────────────┘四段产物,职责分明:
| 环节 | 文件 | 内容 | 谁改 |
|---|---|---|---|
| 源 | 漫跑数据分析.xlsx | 689 人 × 16 天宽表,人工维护 | 每周覆盖 |
| 库 | MySQL 漫跑库.漫跑记录 | 同上,35 列 | 04_导入更新.py |
| 流程 | 漫跑数据分析-v2.tfl | 13 个节点,转置 + 连接 + 并集 | 结构变更时才动 |
| 提取 | 漫跑数据分析-2026R3.hyper | 长表 13091 行,Tableau 直接读 | 每次跑流程重生成 |
| 分析 | 漫跑数据分析.twb | 字段、计算、图表 | 改图时动 |
长表的意义:宽表一人一行,日期藏在列名里;长表一天一条,
日期变成真正的字段, 才能拖时间轴、算连续天数、做趋势。转置那一步就是把"列名里的日期"变成"行里的值"。
一、数据源现状(先对齐口径,否则后面所有图都会误读)
1.1 规模
| 项 | 值 | 来源 |
|---|---|---|
| 班级 sheet | 16(另有 1 个「示例」空表头) | 漫跑数据分析.xlsx |
| 学生 | 689 人 | 去重后的学号数 |
| 日期列 | 32 列 = 16 天 × (开始,结束) | 表头 |
| 日期 | 16 天:9 月 10 天 + 10 月 6 天 | 表头 |
| 库内列数 | 35 = 学号/姓名/班级 + 32 | SHOW COLUMNS |
| 长表行数 | 13091 | Hyper nullCounts 反推 |
| 长表列数 | 10 | Hyper 元数据 |
1.2 16 个跑步日与当天实际记录数
下表的"完整打卡"是 Excel 口径(开始、结束都非空):
| # | 日期 | 星期 | 完整打卡 | 库内 | 备注 |
|---|---|---|---|---|---|
| 1 | 2026/09/10 | 四 | 180 | 180 | 全期最高,开学首日 |
| 2 | 2026/09/14 | 一 | 143 | 143 | |
| 3 | 2026/09/15 | 二 | 131 | 131 | |
| 4 | 2026/09/16 | 三 | 129 | 129 | |
| 5 | 2026/09/17 | 四 | 137 | 137 | |
| 6 | 2026/09/20 | 日 | 148 | 148 | 周日,疑为调休上课 |
| 7 | 2026/09/21 | 一 | 131 | 131 | |
| 8 | 2026/09/22 | 二 | 122 | 122 | |
| 9 | 2026/09/23 | 三 | 127 | 127 | |
| 10 | 2026/09/28 | 一 | 108 | 108 | 9 月末 |
| 11 | 2026/10/08 | 四 | 0 | 0 | 两边都空 |
| 12 | 2026/10/09 | 五 | 0 | 0 | 两边都空 |
| 13 | 2026/10/12 | 一 | 0 | 0 | 两边都空 |
| 14 | 2026/10/13 | 二 | 57 | 0 | Excel 有、库里没有 |
| 15 | 2026/10/14 | 三 | 85 | 85 | |
| 16 | 2026/10/15 | 四 | 75 | 75 | |
| 合计 | 1573 | 1516 | 差 57,全部来自 10/13 |
两条关键结论:
- 真正全空的是 10/08、10/09、10/12(3 天),不是 4 天。 10/13 在 Excel 里有 57 条完整记录。
- 数据库落后于 Excel:10/13 那 57 条全部没有进库。 而 Prep 是从库里读的(
LoadSql),所以 Hyper 只有 1516 条 —— 可视化链条看不到这 57 条。
任何"按天平均""出勤率"的计算,分母都不能直接用 16。
1.3 打卡记录数(分三个口径)
| 口径 | Excel | 数据库 | Hyper |
|---|---|---|---|
| 开始 + 结束都齐全 | 1573 | 1516 | 1516 |
| 只有开始 | 5 | 6 | 5 |
| 只有结束 | 3 | 3 | 3 |
| 负时长 | 0 | 0 | 0 |
| 有任意值的记录 | 1578 | 1522 | 1521 |
两处不一致:
- 完整记录差 57:全部来自 10/13(Excel 有、库没有)。
- 只有开始差 1:库里
260001在 10/15 有一条只有开始的记录,Excel 该格为空。 这条在 Hyper 里没有出现(Hyper开始时间非空 1521 = 1516 + 5), 说明它是在 Hyper 生成之后才进库的。
Hyper 的 持续时间 非空数 = 1516,与数据库一致、比 Excel 少 57。 手册后续凡涉及"长表/工作簿"的数字,一律以 1516 为准;涉及"源数据全貌"的,用 1573。
1.4 覆盖情况
- 689 人中 256 人有过任意一条记录(37.2%),其中 254 人至少有一次完整打卡 (另 2 人只有半截记录:
学生A、学生B)。其余 433 人全期空白。 - 各班"有任意记录"人数(分母为班级人数):
| 班级 | 有记录 | 班级 | 有记录 |
|---|---|---|---|
| 高一 1 班 | 25 / 37 | 高一 9 班 | 19 / 43 |
| 高一 2 班 | 13 / 44 | 高一 10 班 | 17 / 42 |
| 高一 3 班 | 22 / 46 | 高一 11 班 | 19 / 44 |
| 高一 4 班 | 24 / 46 | 高一 12 班 | 8 / 43 |
| 高一 5 班 | 15 / 40 | 高一 13 班 | 16 / 44 |
| 高一 6 班 | 15 / 44 | 高一 14 班 | 19 / 44 |
| 高一 7 班 | 6 / 41 | 高一 15 班 | 12 / 44 |
| 高一 8 班 | 20 / 43 | 高一 16 班 | 6 / 44 |
(班级列合计 256,与总人数一致。)
1.5 时间字段的口径
开始时间 取值 13 ~ 55,结束时间 取值 19 ~ 60。
这不是"几点几分"的绝对时刻,而是某个整点小时内的分钟偏移:
| 证据 | 数值 |
|---|---|
开始时间 最小值 | 13(无 0–12) |
开始时间 最大值 | 55(无 60) |
结束时间 最大值 | 60(恰好 3 条) |
结束时间 > 60 | 0 条 |
开始时间 分布峰值 | 21(163 条)、25(144 条)、23(127 条) |
结束时间 分布峰值 | 30(112 条)、28(98 条)、31(97 条) |
开始时间 不同取值 | 42 个 |
结束时间 不同取值 | 41 个 |
若是绝对时刻(0–23 时),不可能:整期没有 0–12 点、没有 13 时以前的记录,也不会出现 60。 若是"从某起点计的分钟数",则不应有 60 这个硬上界。 60 恰好是分钟的周期上界,且只有结束侧触及——这几乎只能是"某小时内第几分钟"。
因此:持续时间 = 结束时间 - 开始时间 只在同一小时内成立。 跨小时的记录(例如开始 55、结束 5)会被算成 5 - 55 = -50。 本数据集实测无负值,说明源数据在录入时已按同小时处理, 但这个前提没有写进任何计算字段,也没有写进流程注释。
仍有未知:不知道是哪一小时(早读前?课间?放学前?)。若需还原真实时刻, 必须知道该小时的起点,而数据里没有这个字段。
1.6 用时分布(实测)
以 Excel 全量(1573 条完整记录)计算:
| 指标 | 值 |
|---|---|
| 最小 | 1 |
| 最大 | 35 |
| 均值 | 8.55 |
| 中位数 | 7 |
| 不同取值 | 33 |
| 众数(前五) | 5(323 条)、6(287 条)、7(268 条)、8(165 条)、9(91 条) |
1573 条里,用时 5–9 的占 1134 条(72.1%)。
若只统计库内/Hyper 的 1516 条(少了 10/13 的 57 条), 均值与中位数差异可忽略,但口径要对齐。
1.7 "全勤"与"打卡天数"(最容易出错的地方)
"全勤"在这个数据集里有 四种互不相等的算法,混用会出错:
| 口径 | 判定 | 实测人数 |
|---|---|---|
| A. 9 月 10 天全勤 | 10 个 9 月日期都有完整记录 | 57 |
| B. 13 天全勤(旧 3 + 9 月 10) | 旧 3 天有值 且 9 月 10 天完整 | 46 |
| C. 13 个有记录日全勤(Excel) | 除 3 个全空日外,13 天全部完整 | 0 |
| D. 19 天全勤(旧 3 + 主链 16) | 含 3 个全空日 | 0 |
工作簿里 周期内是否全勤? 用的是 {FIXED [班级],[姓名] : MIN(...)} = 1, 它会随全局日期筛选器的区间变化:区间设成哪一段,"全勤"就是"哪一段里零缺勤"。 若区间覆盖旧 3 天 + 9 月 10 天,即上表的 B(46 人)。
"打卡天数"则有两个维度交叉,必须同时说清:
| 维度 | 取值 |
|---|---|
| 时间范围 | 13 天(旧 3 + 9 月 10) / 19 天(旧 3 + 主链 16) |
| 打卡判定 | 开始且结束(当前流程与工作簿统一口径) / 开始或结束(早期口径) |
四种组合的人数分布(Excel 全量):
| 范围 | 判定 | 0 天 | 10–13 天 | ≥14 天 | ≥10 天 | 最大值 |
|---|---|---|---|---|---|---|
| 13 天 | 且 | 400 | 96 | 0 | 96 | 13 |
| 13 天 | 或 | 398 | 96 | 0 | 96 | 13 |
| 19 天 | 且 | 386 | 59 | 48 | 107 | 16 |
| 19 天 | 或 | 384 | 59 | 48 | 107 | 16 |
若按库内(缺 10/13 的 57 条)计算 19 天口径,人数会再少一些。
上表的用法:先确定"看哪段时间"(13 天还是 19 天),再确定"怎样算一次打卡"(且还是或)。 两个维度选错,人数就会差几十人。
例如同样是"19 天、且":
≥10 天有 107 人- 其中只有 59 人落在
10–13 天,另 48 人 ≥14 天
所以"排行里显示多少人"完全取决于筛选器上界设在哪,不能跨口径比较。
二、Tableau Prep 流程详解(漫跑数据分析-v2.tfl)
.tfl 是一个 zip 包,内含 5 个条目:
| 条目 | 大小 | 作用 |
|---|---|---|
flow | 36571 B | 节点与连接定义(JSON) |
displaySettings | 9124 B | 画布坐标 |
maestroMetadata | 11960 B | 版本与特性声明 |
flowGraphImage.png | 31917 B | 流程截图 |
flowGraphThumbnail.svg | 2697 B | 缩略图 |
流程版本 majorVersion=1, minorVersion=4,documentId=0343c2da-b31b-4242-bd89-577ef469883e。
2.1 两个输入源
输入 A:漫跑记录(LoadSql) — 主链
| 项 | 值 |
|---|---|
| 节点类型 | .######### |
| connectionId | ######### |
| 连接名 | ########## |
| class | mysql |
| server / port / dbname | ####### / ######### / ########### |
| username | ############# |
| 字段数 | 23(当时的列数,现已扩到 35) |
注意:流程里记录的字段是 23 列,而库里现在是 35 列。 Prep 的 LoadSql 会在每次运行时重新探测列,所以新列能被带上; 但流程节点的 fields 列表是保存时的快照,不随库变化。
输入 B:旧格式-高一(LoadCsv) — 旁链
| 项 | 值 |
|---|---|
| 节点类型 | .v1.LoadCsv |
| 文件 | ~/Desktop/漫跑记录分析/old/子sheets/outputs/旧格式-高一.csv |
| 编码 | UTF-8,逗号分隔,含表头 |
| 字段 | 班级、姓名、学号、2026-09-07、2026-09-08、2026-09-09 |
| 行数 | 689 |
这是 9 月 7/8/9 三天的旧数据,值只有 1 或空(打卡布尔),共 513 个 1。
2.2 主链逐节点
按数据流向顺序,共 8 个节点:
① 漫跑记录(输入)
读 漫跑库.漫跑记录,23 列(保存时)。
② 清理 1(容器,1 个节点)
| 节点 | 类型 | 配置 |
|---|---|---|
| 筛选器 | .v1.FilterOperation | NOT ISNULL([姓名]) |
作用:丢掉没有姓名的行。库里有 689 行,此处应全部保留。
③ 转置 - 开始(SuperUnpivotExtended)
把「列名里带『开始』的列」转成行。
json
unpivotColumns: [{
unpivotColumnName: "开始",
columnInformation: {
bindingsType: "wildcard",
wildcardType: "Contains",
wildcardExpression: "开始"
}
}]
literalColumnName: "转置 1 名称"bindingsType: wildcard+Contains "开始"→ 新增的…开始列会自动进入转置,不用改流程。这是全流程最关键的设计。- 产出:
转置 1 名称(原列名,如2026/09/10开始)+开始(值)。
④ 转置 - 结束(SuperUnpivotExtended)
同上,Contains "结束",产出 转置 1 名称 + 结束。
③④ 并行,都以 ② 的输出为输入。
⑤ 清理 2(容器,3 个节点)
| 顺序 | 节点 | 配置 |
|---|---|---|
| 1 | 只保留 开始 + 另外 4 个 | 保留 开始, 转置 1 名称, 学号, 姓名, 班级 |
| 2 | Add 日期 | DATE(LEFT([转置 1 名称],FIND([转置 1 名称],"开始")-1)) |
| 3 | 将 开始 重命名为 开始时间 | 开始 → 开始时间 |
日期提取逻辑:FIND("开始") 定位到"开始"二字的起始位置,取它之前的全部字符, 再用 DATE() 转日期。对 2026/09/10开始,LEFT(...,FIND-1) = 2026/09/10。
⑥ 清理 3(容器,3 个节点)
同 ⑤,处理 结束: 保留 结束/转置 1 名称/学号/姓名/班级,FIND("结束") 提日期,结束 → 结束时间。
⑦ 联接 1(SuperJoin)
四键全外联接:
json
conditions: [
{left: "[姓名]", right: "[姓名]", comparator: "=="},
{left: "[班级]", right: "[班级]", comparator: "=="},
{left: "[学号]", right: "[学号]", comparator: "=="},
{left: "[日期]", right: "[日期]", comparator: "=="}
],
joinType: "full"- 左 = 清理2(开始侧),右 = 清理3(结束侧)
full= 全外:只有开始或只有结束的记录也保留(对应 1.3 节的 5 条 + 3 条)- 联接后自动移除
日期-1、学号-1、姓名-1、班级-1四个重名列
这里用姓名而非仅学号做键,是本工作簿的一个隐患:689 人里 260001(高一12班)与 260002(高一15班)同名,靠班级+学号区分才安全。 当前四键组合没有产生错误,但多余的条件一旦有一个字段格式漂移(如班级空格),联接就会断。
⑧ 清理 终(容器,2 个节点)
| 顺序 | 节点 | 公式 |
|---|---|---|
| 1 | Add 打卡状态 | IF NOT ISNULL([开始时间]) AND NOT ISNULL([结束时间])THEN TRUE ELSE FALSE END |
| 2 | Add 持续时间 | IF NOT ISNULL([开始时间]) AND NOT ISNULL([结束时间])THEN [结束时间] - [开始时间]ELSE NULL END |
口径定义在这里,不在 Tableau 里:
打卡状态 = TRUE要求开始、结束都非空;- 只有一边的记录 →
打卡状态 = FALSE、持续时间 = NULL。
所以 1.3 节那 8 条半截记录,在长表里是 FALSE + NULL,不会污染用时统计。
⑨ 并集 1(SuperUnion)
把两条链按列名并起来,namespaceFieldMappings 均为空(同名自动对齐)。 并集后移除 Table Names、转置 1 名称(但保留了 转置 1 名称-1,见 3.3)。
- 主链:16 个日期列 × 689 人 → 11024 行(含 3 个全空日)
- 旁链:3 天 × 689 人 → 2067 行
- 合计 13091 行 ✔ 与 Hyper 元数据一致
⑩ 输出(WriteToHyper)
json
hyperOutputFile: "~/Desktop/漫跑记录分析/漫跑数据分析-2026R3.hyper"
tdsOutput: "~/Desktop/漫跑记录分析/漫跑数据分析-2026R3.tds"2026R3.tds 当前不存在(只有 hyper 落盘)。
2.3 旁链逐节点
① 旧格式-高一(LoadCsv)
见 2.1 输入 B。
② 转置 5(SuperUnpivotExtended)
与主链不同,这里用的是字面量列表而非通配符:
json
literalColumn: {
literals: ["2026-09-07","2026-09-08","2026-09-09"],
literalColumnName: "转置 1 名称",
names: ["2026-09-07","2026-09-08","2026-09-09"]
}
unpivotColumns: [{unpivotColumnName: "转置1值", manualBindings: [同上三列]}]转置前有个注解动作:学号 改为字符串(ChangeColumnType), 以便与主链的 学号(也是字符串)对齐。
③ 清理 旧(容器,5 个节点)
| 顺序 | 节点 | 配置 |
|---|---|---|
| 1 | 将 转置1值 重命名为 打卡状态 | 转置1值 → 打卡状态 |
| 2 | 将 打卡状态 重命名为 打卡状态-old | 打卡状态 → 打卡状态-old |
| 3 | Add 日期 | DATEPARSE("yyyy-MM-dd",[转置 1 名称]) |
| 4 | 将 打卡日期 更改为 日期 | 日期 类型 → date |
| 5 | Add 打卡状态 | IFNULL([打卡状态-old] = 1, FALSE) |
命名有点绕:先把"值"叫成 打卡状态,再改名为 打卡状态-old,最后重新算一个布尔 打卡状态。
DATEPARSE("yyyy-MM-dd", ...) 对应旁链列名格式(短横线), 而主链用 DATE(LEFT(...)) 是因为主链列名是 2026/09/10开始(斜杠+后缀)。 两条链的日期解析方式不同,不能互相套用。
2.4 流程的 9 个节点级属性
流程给 9 个节点都挂了 PrimaryKey = 学号(.v2019_1_3.PrimaryKey):
清理 1、清理 2、清理 3、清理 终、清理 旧、联接 1、并集 1、漫跑记录、转置 - 结束
这是流程内的主键声明,用于行级追踪,不影响输出列结构。
2.5 一句话总结流程
把宽表按「开始」「结束」两个通配符各转一次 → 各自从列名里抠出日期 → 用四键全外联接拼回一行一天 → 算时长和打卡状态 → 再并上 9 月 7–9 的旧布尔数据 → 写长表。
三、Hyper 提取(漫跑数据分析-2026R3.hyper)
3.1 文件与规模
| 项 | 值 |
|---|---|
| 大小 | 131072 B(128 KB) |
| 标识 | 文件头 Hyper + 版本字节 |
| 压缩 | lz4 |
| 关系名 | Extract(namespace Extract) |
| 列数 | 10 |
| 行数 | 13091 |
3.2 十列与非空计数
Hyper 元数据里 nullCounts 直接给出每列空值数,行数 13091 减一下就是非空数:
| 列 | 类型 | 空值数 | 非空数 |
|---|---|---|---|
日期 | Date | 0 | 13091 |
打卡状态 | Bool | 0 | 13091 |
持续时间 | BigInt | 11575 | 1516 |
打卡状态-old | BigInt | 12578 | 513 |
转置 1 名称-1 | Varchar | 2067 | 11024 |
开始时间 | BigInt | 11570 | 1521 |
结束时间 | BigInt | 11572 | 1519 |
班级 | Varchar | 0 | 13091 |
姓名 | Varchar | 0 | 13091 |
学号 | Varchar | 0 | 13091 |
交叉验证:
持续时间非空 = 1516 = 1.3 节"完整打卡"数 ✔开始时间非空 = 1521 = 1516 + 5(只有开始)✔结束时间非空 = 1519 = 1516 + 3(只有结束)✔打卡状态-old非空 = 513 = 旁链 CSV 里1的个数 ✔转置 1 名称-1非空 = 11024 = 主链行数 ✔
这五个等式成立,说明 Hyper 与源数据完全对得上,本手册后续所有数字都可放心引用。
3.3 一个容易踩的坑:转置 1 名称-1
并集的两个输入,列名并不一致:
- 主链:联接后同时存在
转置 1 名称(左=开始侧)和转置 1 名称-1(右=结束侧); 并集前只移除了转置 1 名称,转置 1 名称-1被保留下来。 - 旁链:只有
转置 1 名称(值形如2026-09-07),并集时与主链同名不匹配,未进入结果。
所以最终 Hyper 里:
| 列 | 非空行数 | 属于哪条链 | 值形如 |
|---|---|---|---|
转置 1 名称-1 | 11024 | 主链(689 × 16) | 2026/09/10结束(据联接右侧推导) |
| (空) | 2067 | 旁链(689 × 3) | — |
-1 来自联接:右表(清理 3 / 结束侧)的同名列被自动加后缀。 其值形如 2026/09/10结束 是从流程结构推导的(Hyper 内容经 lz4 压缩,未能直接读取字面值); 但"11024 条非空、恰好等于主链行数"是 nullCounts 直接给出的。
结论与直觉相反:这个字段有值的是主链,空的才是旁链。 它记录的是每次打卡的"结束侧原始列名",对分析没有直接价值, 但拖进视图会让人误以为"这个字段只有一部分行有数据"。 真正想区分两条链,应看 打卡状态-old(只有旁链有值,513 条)。
3.4 长表与宽表的行数换算
13091 = 689 × 16(主链:宽表 16 个日期列全部展开)
+ 689 × 3 (旁链 9/7、9/8、9/9)关键:转置是逐列展开,空列也产生行。 主链 16 天 × 689 = 11024 行,其中 10/08、10/09、10/12 这 3 天全空, 贡献了 3 × 689 = 2067 行,这些行的 开始/结束 为 NULL、打卡状态 为 FALSE。
于是:
| 说法 | 含义 | 对不对 |
|---|---|---|
| "主链展开 16 天" | 结构事实(转置按列数展开) | ✔ |
| "只有 13 天有记录" | 内容事实(3 天全空) | ✔ |
| "13091 = 689×13 + 689×3" | 把两者混为一谈 | ✘ |
计算"打卡天数"时,空列会因为 打卡状态=FALSE 被排除,所以不影响结果; 但计算"应跑天数""出勤率"时,用 16 还是 13 会差 19%。
四、TWB 工作簿详解(漫跑数据分析.twb)
4.1 文件头
| 属性 | 值 |
|---|---|
version | 18.1 |
original-version | 18.1 |
source-build | 2026.2.3 (20262.26.0912.1023) |
source-platform | mac |
| 大小 | 308235 B(301 KB) |
| 文档特性 | 12 项,含 ObjectModelEncapsulateLegacy、WorkbookFingerprinting 等 |
4.2 数据源连接
工作簿只有一个数据源 federated.14ozbt31f4e6j91ahfdv914rnb5n,caption 为「输出 数据提取」。
connection class = federated
└─ named-connection name = hyper.01oaehx13d300j146rwjq00bozw8 caption = 输出
└─ connection class = hyper
dbname = ~/Desktop/漫跑记录分析/漫跑数据分析-2026R3.hyper
username = tableau_internal_user
authentication= auth-none
author-locale = zh_CN
└─ relation name = Extract table = [Extract].[Extract] type = tabledbname 指向 漫跑数据分析-2026R3.hyper,即 Prep 流程的当前输出,链路是通的。
date-options start-of-week = monday:周从周一开始,影响 周() 类计算与日期轴分组。 semantic-values 里声明了国家为「中国」,影响地图与本地化格式。
4.3 元数据记录(10 条)
工作簿缓存的列定义,与 Hyper 的 10 列一一对应:
| remote-name | remote-type | local-type | approx-count | 含义 |
|---|---|---|---|---|
日期 | 133 | date | 19 | 19 个不同日期(旧 3 + 主链 16) |
打卡状态 | 11 | boolean | 2 | true / false |
转置 1 名称-1 | 129 | string | 17 | 主链结束侧列名 |
打卡状态-old | 20 | integer | 2 | 0 / 1(旁链) |
开始时间 | 20 | integer | 64 | 分钟偏移 |
结束时间 | 20 | integer | 56 | 分钟偏移 |
持续时间 | 20 | integer | 41 | 用时值个数 |
班级 | 129 | string | 16 | 16 个班 |
姓名 | 129 | string | 1165 | 估算值(含重复) |
学号 | 129 | string | 1168 | 估算值(含重复) |
日期 = 19 与 Hyper 的 19 个日期一致,说明元数据已按当前提取刷新过。 approx-count 是采样估算,不是精确 COUNT(DISTINCT),不要当去重数使用。
4.4 字段清单(11 个 column 定义 + 17 个 column-instance)
原始字段(8 个,来自 Hyper)
| 字段 | 类型 | 角色 | 用途 |
|---|---|---|---|
日期 | date | dimension / ordinal | 时间轴、全局过滤 |
打卡状态 | boolean | dimension / nominal | 全勤判定 |
开始时间 | integer | measure | 出发时刻分布 |
结束时间 | integer | measure | 结束时刻分布 |
持续时间 | integer | measure | 用时分布 |
班级 | string | dimension / nominal | 分组 |
姓名 | string | dimension / nominal | 排行 |
学号 | integer(自定义为 ordinal) | dimension / ordinal | 排行主键 |
计算字段(6 个)
| caption | name | 数据类型 | 角色 | 公式 |
|---|---|---|---|---|
| 打卡天数 | Calculation_0268816372953088 | integer | measure | {FIXED [学号], [姓名], [班级] : SUM(IF [打卡状态] THEN 1 ELSE 0 END)} |
| 结束计数 | Calculation_0399311583469568 | integer | measure | COUNTD(IF NOT ISNULL([结束时间]) THEN [学号] END) |
| 开始计数 | Calculation_0399311583645697 | integer | measure | COUNTD(IF NOT ISNULL([开始时间]) THEN [学号] END) |
| 周期内是否全勤? | Calculation_4496984931803136 | boolean | dimension | {FIXED [班级],[姓名] :MIN(IF [打卡状态] THEN 1 ELSE 0 END)}=1 |
| 全勤计数 | Calculation_4496984936570881 | integer | dimension / ordinal | INT(IF [Calculation_4496984931803136] THEN [姓名] END) |
| 学生是否达标 | Calculation_4496984964349958 | integer | measure | IF [Calculation_4496984931803136] THEN 1 ELSE 0 END |
数据桶(1 个)
| caption | 名称 | 定义 |
|---|---|---|
| 持续时间 (数据桶) | [持续时间 (数据桶)] | bin on [持续时间],size = 27,peg = 0 |
表对象(1 个)
[__tableau_internal_object_id__].[Extract (Extract.Extract)_527E78B7B1044B948C919C36F90C704B] —— Tableau 内部的对象 id,对应 object-graph 里的 Extract 节点。
4.5 六个计算字段的口径逐条
打卡天数
{FIXED [学号], [姓名], [班级] : SUM(IF [打卡状态] THEN 1 ELSE 0 END)}FIXED在学号+姓名+班级这一行级粒度上算,结果是一人一个值(该人在当前日期范围内的打卡天数)。- 这是一个行级 LOD 结果,但字段角色被设为
measure,在工作表里又以SUM()聚合。 一人 N 行、每行都带同一个值,SUM理论上会得到N × 天数。 - 实测当前显示正常(依赖 Tableau 对 LOD 结果的去重)。但这个行为依赖优化器, 不是公式本身保证的,换聚合方式(如
AVG)或换版本时可能出现倍数偏差。
周期内是否全勤?
{FIXED [班级],[姓名] :
MIN(IF [打卡状态] THEN 1 ELSE 0 END)}
=1- 粒度是 班级+姓名,不是学号。同班同名会合并;同名不同班(
学生W)靠班级正常区分。 MIN(...)=1的语义是:该人在当前日期筛选范围内从未缺勤,而不是"跑够了 N 天"。 字段名里的"全勤"容易让人误读成后者。- 它随全局日期过滤器一起变化:把日期范围收窄,"全勤"就变成"在这个范围内没缺"。
全勤计数
INT(IF [Calculation_4496984931803136] THEN [姓名] END)INT() 作用于中文姓名,结果恒为 NULL。这个字段在数学上没有意义。 它只出现在两个窗口的 color-one-way 高亮列表里,没有被任何工作表使用。
学生是否达标
IF [Calculation_4496984931803136] THEN 1 ELSE 0 END周期内是否全勤? 的 0/1 版本,供 SUM() 求"全勤人数"。语义随上一节。
开始计数 / 结束计数
COUNTD(IF NOT ISNULL([开始时间]) THEN [学号] END)
COUNTD(IF NOT ISNULL([结束时间]) THEN [学号] END)去重学号计数,即"有开始 / 有结束的人数"。一人可能跑多次,所以结果必然小于记录数。 当前工作簿未使用这两个字段,只有数据源里挂着实例(usr: 派生)。
4.6 实例(column-instance)全表
实例(column-instance)是"字段 + 聚合方式"的组合,Tableau 用它把同一个字段 以不同粒度重复使用(如 学号 既可以做维度、也可以 COUNT)。
数据源级共声明 17 个,被工作表实际引用的 13 个:
| 实例 | 派生 | 用于 |
|---|---|---|
[none:日期:qk] | None | 全部 6 张表(全局过滤 + 切片) |
[none:班级:nk] | None | 基础表格×2、全勤排名、月汇总班级排名 |
[none:姓名:nk] | None | 基础表格-弃用、全勤排名 |
[none:学号:ok] | None | 全勤排名(行架第一层) |
[none:开始时间:qk] | None | 打卡同学压力 |
[none:结束时间:qk] | None | 打卡同学压力 |
[none:持续时间:qk] | None | 平均跑步时间 |
[ctd:学号:qk] | CountD | 平均跑步时间(人数) |
[cnt:学号:qk] | Count | 打卡同学压力(人数) |
[sum:持续时间:qk] | Sum | 平均跑步时间(颜色) |
[sum:Calculation_0268816372953088:qk] | Sum | 全勤排名、月汇总班级排名 |
[sum:Calculation_4496984964349958:qk] | Sum | 基础表格-数据(全勤人数) |
[none:Calculation_4496984931803136:nk] | None | 基础表格×2(全勤真假) |
数据源级声明但从未被任何工作表引用的(4 个,冗余):
[__tableau_internal_object_id__].[cnt:Extract...]、[attr:学号:ok]、[attr:持续时间:qk]、 [cnt:持续时间:qk]、[ctd:持续时间:qk]
另有若干实例(如 [none:Calculation_0268816372953088:qk]、[sum:Calculation_0268816372953088:qk]) 只出现在各表的内联声明里,数据源级没有 —— 这是 Tableau 的正常行为,不算问题。
4.7 全局筛选器(唯一一条,数据源级)
工作簿里有一条共享(数据源级)筛选器,定义在 <shared-views> 里:
xml
<shared-view name='federated.14ozbt31f4e6j91ahfdv914rnb5n'>
<datasources>
<datasource caption='输出 数据提取' name='federated.14ozbt31f4e6j91ahfdv914rnb5n' />
</datasources>
<datasource-dependencies datasource='federated.14ozbt31f4e6j91ahfdv914rnb5n'>
<column-instance column='[日期]' derivation='None' name='[none:日期:qk]'
pivot='key' type='quantitative' />
<column datatype='date' name='[日期]' role='dimension' type='ordinal' />
</datasource-dependencies>
<filter class='quantitative'
column='[federated.14ozbt31f4e6j91ahfdv914rnb5n].[none:日期:qk]'
context='true'
included-values='in-range-or-null'>
<min>#2026-10-15#</min>
<max>#2026-10-15#</max>
<target field='[federated.14ozbt31f4e6j91ahfdv914rnb5n].[none:日期:qk]' />
</filter>
</shared-view>它的作用(本手册只关心"在系统里起什么作用")
| 属性 | 含义 |
|---|---|
<shared-view> | 数据源级共享,6 张工作表全部受它约束,不需要每张表各放一个 |
column = [none:日期:qk] | 作用对象是 日期 字段 |
context='true' | 上下文筛选器:先于该表内所有其他筛选器执行。表内的 打卡天数 0–15、班级 筛选都在它之后 |
included-values='in-range-or-null' | 区间加空值:日期为空的行也会通过,不会被误删 |
<min>/<max> | 保留的日期闭区间 |
一句话:这是整个工作簿的"时间窗口开关"。改这一处,所有图表的统计范围同时变, 不需要逐表调整;它先算完,表内筛选再在其结果上叠加。
这个区间填什么,只影响"当前看哪一段"。工作簿本身不依赖某个固定区间, 换周期时改
min/max即可,图表结构不变。
表内还有两个筛选器(非全局,只影响单表)
| 工作表 | 筛选器 | 作用 |
|---|---|---|
全勤排名 | 打卡天数 in-range 0–15,context=true | 只列该区间内的人;0–15 覆盖当前全部取值,等于暂时不过滤 |
全勤排名 | 班级 level-members,context=true | 班级多选,默认全选 |
基础表格-弃用、基础表格-数据 | 周期内是否全勤? = true | 只保留"范围内零缺勤"的人 |
平均跑步时间 | 持续时间 非空 | 排除没有完整记录的行 |
打卡同学压力 | 开始时间 非空、结束时间 非空 | 排除半截记录 |
月汇总班级排名 | 无 | 只受全局日期筛选 |
全勤排名 的两条筛选器都带 context=true,与全局日期筛选一样属于上下文级, 在表内先执行。
4.8 六张工作表逐张
表 1:全勤排名
| 项 | 值 |
|---|---|
| 行 | 学号 / (姓名 / 班级) 三层 |
| 列 | SUM(打卡天数) |
| 标记 | Automatic |
| 编码 | 文本 = SUM(打卡天数) |
| 筛选器 | 打卡天数 0–15(context)、班级 全选(context) |
| 排序 | 学号 按 SUM(打卡天数) 降序 |
| 切片 | 打卡天数、日期、班级 |
| simple-id | {D40C7CD0-1773-4D18-94C7-69FD1143C52D} |
水平条形排行:每人一条,长度 = 打卡天数,按天数降序。 行架带 学号 是为了让同名不同人(学生W)分开显示,这是对的做法。
打卡天数 0–15 这个范围当前覆盖所有可能取值(最大 16,但极少), 等于不筛选;它留在这里是为了以后收窄范围时直接改。
表 2:基础表格-弃用
| 项 | 值 |
|---|---|
| 标题 | 全勤人数 |
| 行 | 班级 / 姓名(两层) |
| 列 | 周期内是否全勤? |
| 标记 | Automatic |
| 编码 | 颜色 = 周期内是否全勤? |
| 筛选器 | 周期内是否全勤? = true |
| 切片 | 日期、周期内是否全勤? |
| 样式 | 该列宽 121;标记标签关闭 |
| simple-id | {96E5E0F0-4FF3-4C79-A90D-7F638AB8C5D7} |
每个全勤学生一个点,按班级分行,列上是 true/false。 因为过滤到 true,false 列是空的。表名自带"弃用",是早期草稿。
表 3:基础表格-数据
| 项 | 值 |
|---|---|
| 行 | 班级 |
| 列 | 无(单列文本表) |
| 标记 | Automatic |
| 编码 | 文本 = SUM(学生是否达标) |
| 筛选器 | 周期内是否全勤? = true |
| 切片 | 日期、周期内是否全勤? |
| 标记标签 | 15 条显式标签,全部 label-state=on |
| simple-id | {22336AFF-BB8B-4497-835A-E871BF7A93D9} |
这是"各班全勤人数"表,也是唯一带 maximized=true 的窗口(打开即最大化); 但 15 条标签是冻死在 XML 里的字面量:
| 班级 | 标签值 | 班级 | 标签值 |
|---|---|---|---|
| 高一 1 班 | 14 | 高一 9 班 | 6 |
| 高一 2 班 | 6 | 高一 10 班 | 7 |
| 高一 3 班 | 11 | 高一 11 班 | 11 |
| 高一 4 班 | 18 | 高一 12 班 | 5 |
| 高一 5 班 | 5 | 高一 13 班 | 4 |
| 高一 6 班 | 3 | 高一 14 班 | 6 |
| (高一 7 班 缺失) | — | 高一 15 班 | 6 |
| 高一 8 班 | 10 | 高一 16 班 | 3 |
合计 115。这些数字不会随数据刷新而变。 缺高一 7 班是因为该班最高打卡天数只有 5,0 人全勤,Tableau 未生成标签。
表 4:平均跑步时间
| 项 | 值 |
|---|---|
| 行 | COUNTD(学号)(人数) |
| 列 | 持续时间 |
| 标记 | Area(面积图) |
| 编码 | 颜色 = SUM(持续时间)(蓝色渐变 blue_10_0)文本 = COUNTD(学号)LOD = COUNTD(学号) |
| 筛选器 | 持续时间 非空 |
| 切片 | 日期、持续时间 |
| 样式 | 轴高 57;颜色插值 |
| simple-id | {4F41E126-1F31-4624-A24B-E9B8B1370587} |
表名与内容不符:这不是"平均用时",而是"用时分布"—— 横轴是用时,纵轴是"该用时上有多少人次",面积高度代表分布的密集程度。 "平均"这个词只在颜色编码 SUM(持续时间) 上勉强沾边。
表 5:打卡同学压力
| 项 | 值 |
|---|---|
| 行 | COUNT(学号)(人次),轴标题「人数」 |
| 列 | (开始时间 + 结束时间) 双轴合并 |
| 标记 | Area(3 个 pane) |
| 编码 | 颜色 = [:Measure Names](两条线区分)文本 = COUNT(学号) |
| 筛选器 | 开始时间 非空、结束时间 非空 |
| 切片 | 日期、开始时间、结束时间 |
| 趋势线 | 仅 pane 8(开始时间轴)有:线性拟合,无置信带 |
| 样式 | 结束时间轴 fold=true(双轴折叠) |
| simple-id | {B140757F-5C37-4597-8794-737D5F769FAE} |
三个 pane:
pane 7:无 x 轴名(基线)pane 8:x = 开始时间,带趋势线pane 9:x = 结束时间
图的语义:横轴是分钟偏移(见 1.5),纵轴是"在这一点出发/结束的人次", 两条面积带分别是"出发时刻人数"与"结束时刻人数"。 "压力"指的是人流在时间上的集中程度。
表 6:月汇总班级排名
| 项 | 值 |
|---|---|
| 行 | 班级 |
| 列 | SUM(打卡天数)(轴标题「打卡人次」) |
| 标记 | Automatic |
| 编码 | 文本 = SUM(打卡天数) |
| 筛选器 | 无(仅受全局日期筛选) |
| 排序 | 班级 按 SUM(打卡天数) 降序 |
| 切片 | 日期 |
| simple-id | {ED323CB1-6E83-467A-A7A1-DA79FED6572A} |
班级维度的打卡人次排行。轴标题写"人次"、字段叫"打卡天数", 实际是 SUM(FIXED 天数),即"全班所有人的打卡天数之和",用"人次"更贴切。
4.9 仪表板 1
| 项 | 值 |
|---|---|
| 尺寸 | 1000 × 800(min = max) |
| zones | 0 个(空) |
| 手机版式 | 自动生成(auto-generated=true),700 高,vscroll |
| simple-id | {59A2FA23-3134-4302-91F6-C4C5E8F1A322} |
仪表板是空壳,6 张工作表都没有拖进去。
4.10 窗口(windows)配置要点
| 窗口 | 关键配置 |
|---|---|
基础表格-弃用 | 左侧卡:颜色、日期筛选;高亮 6 个字段 |
基础表格-数据 | maximized=true;左侧卡:全勤筛选、日期筛选 |
全勤排名 | 右侧卡:天数筛选、班级筛选、日期筛选 |
平均跑步时间 | 右侧卡:日期筛选、颜色 |
打卡同学压力 | 右侧卡:日期筛选、颜色(Measure Names) |
月汇总班级排名 | 右侧卡:日期筛选 |
仪表板 1 | active id=-1(无活动部件) |
工作簿共 15 个 tuple-reference,全部属于 基础表格-数据 的 15 条标记标签。 没有任何持久化的选中记录(selection-collection 为空), 所以打开工作簿不会出现"莫名只有几个人被高亮"的情况。
五、图表语义速查(别被表名骗了)
| 工作表名 | 实际画的是 | 一句话读法 |
|---|---|---|
全勤排名 | 每人的打卡天数排行 | 条形长度 = 天数,按天数降序;行架带学号以区分同名 |
基础表格-弃用 | 全勤学生名单矩阵 | 每行一个全勤学生,按班级分组;已弃用 |
基础表格-数据 | 各班全勤人数(单列表) | 数字是冻结的旧值,不是实时值 |
平均跑步时间 | 用时分布(面积图) | 横轴用时、纵轴人次;不是平均值 |
打卡同学压力 | 出发时刻 + 结束时刻的人次曲线 | 双轴叠加,看人流集中在哪个分钟段 |
月汇总班级排名 | 各班打卡人次总和 | 条形长度 = 全班天数之和 |
四条常见误读
把
平均跑步时间当平均值 → 它没有算任何平均数,是把记录按用时值铺开数人头。 想要平均值应看 1.6 节的 8.55,或新建AVG([持续时间])。把
全勤排名的条形长度当"跑了几次" → 是"打卡天数", 口径为FIXED 学号+姓名+班级下的SUM(IF 打卡状态 THEN 1), 即该人在当前日期筛选范围内有多少天满足打卡状态=TRUE。改日期范围,条形就变。把
基础表格-数据的 115 当当前值 → 是建立工作簿时的快照,且缺高一 7 班。 刷新数据不会更新这些数字(它们是mark-label字面量,不是计算)。把"全勤"理解为"跑够了 N 天" →
周期内是否全勤?的定义是 "在筛选范围内从未缺勤"(MIN(打卡) = 1)。范围内只要有任意一天缺, 就判为非全勤,与该人总共跑了多少天无关。
六、已知问题清单(仅记录,不含修复)
按严重度排列。"定位"给到文件内可检索的锚点。
P0 — 数据不一致
Q1. 数据库落后于 Excel:10/13 的 57 条记录没进库
- 现象:Excel 的
2026/10/13列有 57 条完整记录(开始、结束都非空), 数据库同名列 0 条(实测SELECT COUNT(*) ... WHERE 开始 IS NOT NULL AND 结束 IS NOT NULL)。 - 影响链条:Prep 用
LoadSql读库 → Hyper 只有 1516 条(Excel 是 1573)→ 工作簿、仪表板、所有导出都缺这 57 条。 - 影响范围:主要是 10/13 一天;其余 15 天的完整记录三边一致。
- 另有 1 处反向差异:库内
260001(学生D)在 10/15 有"只有开始、没有结束"的半截记录, 而 Excel 该格为空。因此库内半截 6 条 / Excel 半截 5 条。 - 合并后:库内完整 1516 + 半截 6 = 1522 条有值;Excel 完整 1573 + 半截 5 = 1578 条有值。
- 定位:
- Excel:
2026/10/13开始/2026/10/13结束两列(AD、AE 列) - 库:
漫跑记录同名列
- Excel:
- 该问题属"入库同步"范畴,本手册只记录现象,处理办法见
../漫跑数据入库/数据库操作手册.md第四节。 - 附带后果:本手册所有"库内 / Hyper"口径的数字都比"Excel"口径少 57 条, 两套数字并列给出,避免混用。
P1 — 口径与呈现
Q2. 基础表格-数据 的 15 个数字是冻结字面量
- 现象:
mark-labels/mark-label里 15 条label-state=on,值是写死的 3–18。 - 缺高一 7 班:该班最高打卡天数只有 5,0 人全勤,Tableau 未生成标签,属正常。
- 合计 115。这个数无法用当前任何日期范围复现,说明来自更早的数据版本。
- 定位:
worksheets/worksheet[@name='基础表格-数据']/table/mark-labels - 影响:图上数字与当前筛选结果不一致,且刷新不更新(字面量,不是计算)。
Q3. 半截记录在两种口径下结论相反
- 当前口径(流程
清理 终与工作簿打卡天数一致):开始 且 结束都非空才算打卡。 - 半截记录共 6 条(库内),涉及 6 人:
学生A、学生E、学生C、学生F、学生B、学生D。 它们在长表里是打卡状态=FALSE、持续时间=NULL, 不计入打卡天数、也不进用时分布。 - 若改用"任一非空"口径,这些人各会多算 1 天,但也不会进用时统计(时长仍算不出来)。
- 定位:
flow→清理 终/Add 打卡状态;datasources/datasource/column[@caption='打卡天数'] - 说明:本手册只记录现象,不判定哪个口径正确。
Q4. 打卡天数 的 LOD + SUM 组合存在倍数风险
FIXED产出人级常量,SUM()又在行级求和,理论上应为行数 × 天数。- 实测当前显示正常(依赖 Tableau 对 LOD 结果的去重)。
- 定位:
datasources/datasource/column[@caption='打卡天数'] - 影响:换聚合方式(AVG/MIN)或换 Tableau 版本时可能出现倍数偏差。
Q5. 周期内是否全勤? 用姓名而非学号做键
{FIXED [班级],[姓名] : ...}:同班同名会合并。- 本数据集无同班同名,但
学生W两人跨班,靠班级才区分开。 - 语义是"范围内零缺勤",与字段名"全勤"易混淆。
- 定位:
datasources/datasource/column[@caption='周期内是否全勤?']
Q6. 全勤计数 是无效字段
INT(IF ... THEN [姓名] END):INT()作用于字符串姓名,恒为NULL。- 仅在两个窗口的
color-one-way里出现,无工作表使用。 - 定位:
datasources/datasource/column[@caption='全勤计数']
Q7. 数据源级有 4 个冗余 column-instance
[__tableau_internal_object_id__].[cnt:Extract...]、[attr:学号:ok]、[attr:持续时间:qk]、[cnt:持续时间:qk]、[ctd:持续时间:qk]- 早期探索遗留,未被任何工作表引用。
- 定位:
datasources/datasource/column-instance
Q8. 仪表板 1 为空
zones子元素数 = 0,6 张工作表均未上板。- 定位:
dashboards/dashboard[@name='仪表板 1']/zones
P2 — 命名与呈现
Q9. 平均跑步时间 名实不符
- 实为用时分布面积图,无任何平均计算。
- 定位:
worksheets/worksheet[@name='平均跑步时间']
Q10. 打卡同学压力 的三 pane 只有一条趋势线
- 仅
pane 8(开始时间)有trendline fit=linear;pane 9(结束时间)没有。 - 两条面积带放在一起但只有一条有拟合,视觉上会误导。
- 另:
13–60的分钟偏移口径未在任何地方定义(见 1.5)。 - 定位:
worksheets/worksheet[@name='打卡同学压力']/table/panes/pane[@id='8']/trendline
Q11. 基础表格-弃用 仍留在工作簿内
- 已明确弃用但未删除,参与缩略图与窗口列表。
- 定位:
worksheets/worksheet[@name='基础表格-弃用']
Q12. 全勤排名 的天数筛选范围 0–15 依赖"库内缺 10/13"这一前提
- 当前连接的数据(库内,19 天口径)最大打卡天数是 15,所以
0–15完整覆盖、暂时不丢人。 - 但 Excel 全量口径下最大是 16(有 20 人),因为 10/13 那 57 条没进库(见 Q1)。
- 也就是说:一旦 Q1 被修好、10/13 入库,0–15 就会漏掉这 20 人。
- 该筛选是
context=true,在表内最先执行。 - 定位:
worksheets/worksheet[@name='全勤排名']/table/view/filter - 说明:本手册只记录该联动关系,不评价取值是否合适。
Q13. 流程与成品的字段快照不一致
- 流程
LoadSql记录 23 列,库里 35 列;流程LoadCsv指向old/子sheets/outputs/, 属历史目录。 - 定位:
flow→nodes['626aa6a3-...'].fields、connections['b70de1f7-...']
七、每周更新对可视化的影响(现状描述)
只描述"会发生什么",不给改法。
| 变化 | Prep 流程 | Hyper | 工作簿 |
|---|---|---|---|
| 宽表新增日期列 | 通配符转置自动纳入,无需改流程 | 重跑后日期数增加 | 元数据需刷新;否则 approx-count 与列名会滞后 |
| 宽表新增学生 | LoadSql 自动带上 | 行数增加 | 姓名/班级维度自动出现 |
| 某日期列整列为空 | 仍产生 689 行(值为 NULL) | 行数增加、该日 打卡状态=false | 全局筛选器决定是否可见 |
| 新增月份数据 | 需确认流程已重跑 | 日期数从 19 增长 | 把全局筛选器的区间向前延一段即可看到 |
| 删除学生 | 自动消失 | 行数减少 | 排行表中该人自动消失 |
关键点:Prep 侧靠通配符自动适应,工作簿侧靠全局筛选器的区间控制时间窗。 两者配合得当,加数据不需要改结构;需要改的只是筛选器的 min/max。
另外两处会滞后、但不影响正确性的:
基础表格-数据的 15 条标记标签(字面量,不随刷新变化,见 Q2)全勤排名的打卡天数 0–15范围(现有数据最大 15,暂时够用;见 Q12)
八、排查速查
| 现象 | 先查 | 锚点 |
|---|---|---|
| 图上完全没有某段日期 | 全局日期筛选器的区间 | shared-views/shared-view/filter 的 min/max |
| 班级数字不对且刷新无效 | 标记标签字面量 | worksheet[@name='基础表格-数据']/table/mark-labels |
| 排行表少了高分学生 | 打卡天数 的 in-range 上界 | worksheet[@name='全勤排名']/table/view/filter |
| 某个人在自己表里查不到 | 班级 筛选器是否全选 | worksheet[@name='全勤排名']/table/view/filter |
| 重名字段报错或列错位 | 元数据 转置 1 名称-1 与 Hyper 是否一致 | metadata-records/metadata-record |
| 用时分布图横轴超出数据范围 | 数据桶 size=27 | column[@name='[持续时间 (数据桶)]'] |
| 全勤人数与手算不符 | 全勤口径(A/B/C/D 四种) | 本手册 1.7 |
| 时间段落在 13–60 | 分钟偏移口径 | 本手册 1.5 |
| 打卡数比 Excel 少 16 | 库与 Excel 是否同步 | 本手册 Q1 |
附录 A:文件清单
| 文件 | 大小 | 角色 |
|---|---|---|
../漫跑数据分析.xlsx | 110758 B | 源宽表(689 × 35) |
../漫跑数据分析-v2.tfl | 37199 B | Prep 流程(13 节点) |
../漫跑数据分析-2026R3.hyper | 131072 B | 长表提取(13091 × 10),工作簿当前连接它 |
../漫跑数据分析.twb | 308235 B | 工作簿(6 表 + 1 空仪表板) |
../old/子sheets/outputs/旧格式-高一.csv | 25252 B | 旁链输入(9/7–9/9,689 行) |
../old/输出.hyper | 131072 B | 13 天版提取(8957 行),历史产物 |
../old/输出-final.hyper | 131072 B | 更早的实验版本(9 列,含 转置1值) |
../old/temp-输出.hyper | 65536 B | 更早的实验版本(8 列,含 Table Names) |
../old/工作簿 2.twb | — | 更早的工作簿 |
../宣传/月汇总全勤排名_摘要.csv | — | 排行导出(UTF-16,96 行 + 表头) |
附录 B:Prep 节点 ID 对照
| 节点 | 类型 | ID(前 8 位) |
|---|---|---|
| 漫跑记录 | LoadSql | 626aa6a3 |
| 旧格式-高一 | LoadCsv | 0d021b59 |
| 清理 1 | Container | 7ce6e24d |
| 清理 2 | Container | 3f7e7929 |
| 清理 3 | Container | cbae1f49 |
| 清理 旧 | Container | a67ba473 |
| 清理 终 | Container | 248c103a |
| 转置 - 开始 | SuperUnpivotExtended | a528a1ad |
| 转置 - 结束 | SuperUnpivotExtended | db0b8b84 |
| 转置 5 | SuperUnpivotExtended | 76a7653a |
| 联接 1 | SuperJoin | 5887b0d8 |
| 并集 1 | SuperUnion | 48ed450a |
| 输出 | WriteToHyper | 4706da33 |
容器内部节点 ID(按执行顺序):
| 容器 | 内部节点(ID 前 8 位) |
|---|---|
| 清理 1 | 833d119e 筛选器 |
| 清理 2 | 206e7a88 保列 → ec6d6cc7 Add 日期 → 73f926d1 改名 |
| 清理 3 | 8d31f566 保列 → 4727894b Add 日期 → f680bf7b 改名 |
| 清理 旧 | 80a56f6f 改名 → 0563f2ca 改名 → 558ec9d3 Add 日期 → 2c76a51b 改类型 → 0343d335 Add 打卡状态 |
| 清理 终 | de45c31e Add 打卡状态 → 2c0e0ce6 Add 持续时间 |
附录 C:关键 XML 片段
全局筛选器(min/max 是当前值,换周期时改这两个日期即可)
xml
<shared-view name='federated.14ozbt31f4e6j91ahfdv914rnb5n'>
<datasources>
<datasource caption='输出 数据提取' name='federated.14ozbt31f4e6j91ahfdv914rnb5n' />
</datasources>
<datasource-dependencies datasource='federated.14ozbt31f4e6j91ahfdv914rnb5n'>
<column-instance column='[日期]' derivation='None' name='[none:日期:qk]'
pivot='key' type='quantitative' />
<column datatype='date' name='[日期]' role='dimension' type='ordinal' />
</datasource-dependencies>
<filter class='quantitative'
column='[federated.14ozbt31f4e6j91ahfdv914rnb5n].[none:日期:qk]'
context='true'
included-values='in-range-or-null'>
<min>#2026-10-15#</min>
<max>#2026-10-15#</max>
<target field='[federated.14ozbt31f4e6j91ahfdv914rnb5n].[none:日期:qk]' />
</filter>
</shared-view>shared-view 里同时声明了 [none:日期:qk] 这个实例和 [日期] 字段本身, 说明这条筛选器只依赖 日期 一个字段,与任何工作表无关,因此对所有表生效。
表内筛选器(全勤排名)
xml
<filter class='quantitative'
column='[...].[none:Calculation_0268816372953088:qk]'
context='true' included-values='in-range'>
<min>0</min>
<max>15</max>
</filter>
<filter class='categorical'
column='[...].[none:班级:nk]'
context='true'>
<groupfilter function='level-members' level='[none:班级:nk]' />
</filter>两条都带 context='true',与全局日期筛选器同级,先于表内其他计算执行。
数据桶
xml
<column aggregation='None' datatype='integer'
name='[持续时间 (数据桶)]' role='dimension' type='quantitative'>
<calculation class='bin' decimals='1' formula='[持续时间]' peg='0' size='27' />
</column>标记标签(示例,共 15 条)
xml
<mark-label id='0' label-state='on'>
<tuple-reference>
<tuple-descriptor>...</tuple-descriptor>
<tuple>
<value>"高一 4 班"</value>
<value>18</value>
</tuple>
</tuple-reference>
</mark-label>附录 D:本手册数字的来源
| 数字 | 来源 | 校验方式 |
|---|---|---|
| 689 人 | Excel 去重学号 | 与 SHOW COLUMNS/COUNT(*) 一致 |
| 16 天 / 32 列 | Excel 表头 | 与库 35 列 − 3 一致 |
| 1573 / 1516 完整打卡 | Excel 逐格复算 / 库与 Hyper | Excel 1573,库与 2026R3.hyper 均 1516,差 57(10/13) |
| 5 / 3 半截(Excel) / 6(库内) | 逐格复算 | Excel:开始独有 5、结束独有 3;库内开始独有 6(多 260001@10/15) |
| 13091 行 | Hyper nullCounts 反推 | = 689×16 + 689×3 |
| 8957 行(13 天版) | old/输出.hyper 的 nullCounts | = 689×13,且 日期 非空数与日期集合一致 |
| 1356 / 1361 / 1359 | 9 月口径复算 | 与 old/输出.hyper 的 持续/开始/结束 非空数逐项相等 |
| 13 个有记录日(Excel) / 12 个(库内) | 按日计数 | Excel 空日 = 10/08、10/09、10/12;库内再多一个 10/13 |
| 用时 1–35,均值 8.55 | Excel 逐条计算 | 中位 7,众数 5(323 条),33 个不同值 |
| 57 人 9 月全勤 | Excel 逐人判定 | 口径 A |
| 46 人 13 天全勤 | Excel + 旁链 CSV | 口径 B |
| 115(冻结标签) | TWB mark-label 字面量 | 15 条求和 |
| 256 人有任意记录 / 254 人有完整记录 | Excel 逐人判定 | 37.2% / 36.9%;差 2 人是只有半截记录的 学生A、学生B |
关于半截记录:Excel 口径下共 5 条"只有开始"+ 3 条"只有结束",分布在 5 人身上:
学生A:0 条完整 + 1 条半截(唯一记录就是半截)学生B:0 条完整 + 2 条半截(唯一记录都是半截)学生E:8 条完整 + 1 条半截学生C:1 条完整 + 2 条半截学生F:1 条完整 + 2 条半截按"开始且结束"计为未打卡,半截都不计入打卡天数、也不进用时分布。 库内另有
学生D在 10/15 多一条半截,Excel 无此数据(见 Q1)。全部数字均为本手册写作时对现存文件的独立复算。若
漫跑数据分析.twb或 Hyper 后续被改动,第四、五、六章的结构描述需按改动后的文件重新核对。