不同的质控方法、生长曲线和插补方法,在不同的批次上表现并不一样。与其凭经验选择,不如在自己的数据上检验:故意破坏或隐藏一部分数据,再看各方法能把结果恢复到什么程度。tidypptsda 为此提供了五个函数:
| 函数 | 检验什么 | 做法 |
|---|---|---|
validate_outliers() |
体重异常值检测方法(逐日回溯追踪,以及 rlm、loess、stl、sd 及其组合) | 在干净的体重记录中加入人为误差,看各方法找出了多少、误删了多少、ADG 偏了多少 |
validate_adg() |
生长曲线模型(poly1/2/3、gompertz),以及曲线外推还是切线外推(extrap) |
按连续若干天分块的 k 折交叉验证;隐藏测定开头或结尾若干天(模拟晚进站、早出栏),看 ADG 变化多少 |
validate_weight_fill() |
体重填充方法(线性插值、poly1/2/3、gompertz),以及外推方式(extrap) |
隐藏区间内部分有记录的日子(随机、成段、开头、结尾),重新填充,与实测日体重和 ADG 比较 |
validate_adfi() |
日采食填充方法(loess_pop、loess_lm、linear) | 隐藏部分有记录的日子(随机、成段、开头、结尾),这些日子的体重也一并隐藏,重新填充,与实测比较 |
validate_truncation() |
整条流程:任意几套质控设置 | 删掉一部分猪若干天的全部访问(体重和采食),从体重清洗起重跑,与全部数据的 ADG、ADFI、FCR 比较 |
比较设置、做最终决定时,请以
validate_truncation() 为准。
前四个函数快,适合单独看某一步。从 1.5.0
起,它们都先删掉被隐藏日子的记录,再对剩下的记录重新清洗,被隐藏的数据不再影响其余记录的取舍:
| 函数 | 1.5.0 之前用到的被隐藏信息 | 现在 |
|---|---|---|
validate_outliers() |
“干净记录”由 qc 的方法判定,偏向这种方法 |
干净记录取所有被比较方法都保留的记录(reference = "consensus") |
validate_adg()(交叉验证) |
按天随机分折,留出日子的前后一天都在训练集里,衡量的更像是插值能力 | 按连续 block_days(默认
7)天分块再分折,训练集重新清洗 |
validate_adg()(截断)、validate_weight_fill()
|
异常值清洗(包括从末期往回的逐日追踪)在隐藏之前用全部记录做过 | 剩下的记录重新清洗 |
validate_adfi() |
被隐藏的采食日仍用真实体重 | 传入 weight
时,这些日子的体重记录也删掉,日体重用剩下的记录重新计算 |
剩下的局限:它们每次只看一步,其余步骤仍用全部数据的结果(例如
validate_adfi()
的区间起止、采食记录的校正都来自全部数据)。各步骤之间的相互影响,只有
validate_truncation() 能看到。
五个函数都返回 ppt_validation
对象,$summary
是各方法的汇总比较,其余元素是每头猪的明细。结果可以用 seed
复现;数据量大时用 max_pigs
随机抽取一部分猪(几百头已经足够),用 cores 并行。
运行时先显示一行概况(猪数、方法数、场景和重复次数),然后按
[1/3]、[2/3]
编号显示每个阶段:进行中显示进度条、已完成数和预计剩余时间(ETA),完成后留下一行结果和耗时。最耗时的通常是”重新清洗”阶段:每个折、每个截断场景或每个隐藏方案都要把剩下的记录完整清洗一遍。最后打印一张汇总表,每列最好的值标成绿色并带
*,完整结果在 $summary
中。verbose = 0 时不显示任何信息。
每个参数的详细说明和更多例子,见《验证函数使用指南》。
下面用示例数据演示。FIRE 的猪进站较晚,统一使用 60–120 kg 区间。
qc_n <- qc_config()
qc_f <- qc_config(stage_range = c(60, 120))
v_n <- prep_visits(nedap, verbose = 0)
v_f <- prep_visits(fire, verbose = 0)
w_n <- clean_weight(v_n, qc_n, verbose = 0)
w_f <- clean_weight(v_f, qc_f, verbose = 0)
# 汇总表保留 3 位有效数字,方便阅读
tidy <- function(x) x$summary[, lapply(.SD, function(v) if (is.double(v)) signif(v, 3) else v)]validate_outliers()
qc 和 methods
中的每种方法各清洗一遍,所有方法都保留的记录视为”干净记录”(reference = "consensus",默认),由它们(不做异常值检测)算出的
ADG 视为”真值”。这样哪种方法都不占便宜;reference = "qc" 是
1.5.0 之前的做法,只用 qc 判定;rate(默认 5%)的干净记录,加上或减去
size(默认 3–15 kg)的误差;methods
中的每种方法重新清洗、重新计算 ADG;reps 次,取平均。sensitivity(检出率):人为误差中被判为异常的比例,越高越好;false_positive_rate(误删率):干净记录中被判为异常的比例,越低越好;precision(精确率):判为异常的记录中,真正是人为误差的比例;adg_bias、adg_rmse:ADG
与真值之差的平均值和均方根(g/d);adg_spearman:ADG
排名与真值的相关;none 一行表示只做 weight_range
范围检查、不做异常值检测的结果,作为对照;track+rlm+loess
是默认设置(先逐日回溯追踪,再用 rlm 和 loess);track
只做追踪;其余各行都在关闭追踪的情况下只用对应的方法。
vo_n <- validate_outliers(v_n, qc_n, reps = 3, verbose = 0)
tidy(vo_n)
#> method sensitivity false_positive_rate precision adg_bias adg_rmse adg_spearman n_pigs
#> <char> <num> <num> <num> <num> <num> <num> <num>
#> 1: none 0.011 0.00e+00 1.000 -0.2550 1.870 0.999 29
#> 2: track+rlm+loess 0.940 5.58e-03 0.901 0.5540 2.190 0.999 29
#> 3: track 0.681 5.08e-04 0.986 0.0806 1.260 0.999 29
#> 4: rlm 0.885 1.94e-03 0.961 0.3430 1.650 0.999 29
#> 5: loess 0.903 8.24e-05 0.998 0.0352 0.848 1.000 29
#> 6: rlm+loess 0.917 1.98e-03 0.961 0.3990 1.630 0.999 29
#> 7: stl 0.946 2.75e-03 0.949 0.1390 0.806 1.000 29
#> 8: sd 0.599 0.00e+00 1.000 -0.3240 1.710 0.999 29
vo_f <- validate_outliers(v_f, qc_f, reps = 3, verbose = 0)
tidy(vo_f)
#> method sensitivity false_positive_rate precision adg_bias adg_rmse adg_spearman n_pigs
#> <char> <num> <num> <num> <num> <num> <num> <num>
#> 1: none 0.00502 0.00e+00 1.000 2.010 7.28 0.993 25
#> 2: track+rlm+loess 0.91800 4.93e-03 0.908 2.280 9.66 0.999 25
#> 3: track 0.66900 4.44e-05 0.999 1.070 4.29 0.999 25
#> 4: rlm 0.86800 2.53e-03 0.948 1.750 7.25 0.999 25
#> 5: loess 0.89900 4.00e-04 0.992 0.257 2.58 1.000 25
#> 6: rlm+loess 0.90800 2.62e-03 0.949 1.750 7.09 0.999 25
#> 7: stl 0.88800 1.69e-03 0.965 1.020 3.64 0.999 25
#> 8: sd 0.57300 0.00e+00 1.000 0.391 3.32 0.999 25实际的称重误差多数是偏低的(猪只有前腿站在秤上),可以用
direction = "down" 单独检验:
tidy(validate_outliers(v_n, qc_n, reps = 3, direction = "down", verbose = 0))
#> method sensitivity false_positive_rate precision adg_bias adg_rmse adg_spearman n_pigs
#> <char> <num> <num> <num> <num> <num> <num> <num>
#> 1: none 0.0103 0.000000 1.000 0.1460 2.69 0.999 29
#> 2: track+rlm+loess 0.9370 0.005310 0.904 0.3910 2.06 0.999 29
#> 3: track 0.6670 0.000480 0.987 -0.7710 1.53 1.000 29
#> 4: rlm 0.8740 0.002320 0.953 0.3840 1.96 1.000 29
#> 5: loess 0.9050 0.000261 0.995 0.0129 1.07 1.000 29
#> 6: rlm+loess 0.9150 0.002400 0.953 0.4010 1.91 0.999 29
#> 7: stl 0.9350 0.002400 0.954 -0.0431 0.92 0.999 29
#> 8: sd 0.5580 0.000000 1.000 -0.1400 1.40 1.000 29track+rlm+loess
检出率最高之一(3–15 kg
的人为误差找出九成以上),误删的好记录约
0.5%,是各方法中最多的:它把三种方法判为异常的记录都删掉。只做追踪(track)几乎不误删,但只能找出约
7 成:小于追踪容差(8%,至少 2 kg)的误差会被保留。none),ADG
的误差也只有几 g/d。各方法 ADG
的差别主要来自它们误删了哪些好记录:例如 FIRE 上,rlm
会删掉一部分偏离二次曲线、但其实正常的记录,ADG 误差约 7 g/d,loess 不到
3 g/d。plot_adg() 的图逐站检查。注意:“干净记录”是各方法的共识,并非完全没有误差:所有方法都漏掉的真实误差,仍留在干净记录里。$settings$n_clean
是干净记录的条数。
validate_adg()
生长曲线决定两件事:区间的起止时间,以及区间两端(首个有记录日之前、末个有记录日之后)的日体重。这里比较的是最终的日增重(adg,补齐后日体重的回归斜率)。
block_days 天(默认 7 天)切成块,再把块随机分成
k 组(默认 5
组),轮流留出一组,用其余各组拟合曲线,预测留出日子的体重。cv_rmse、cv_mae
是预测误差(kg)在各头猪之间的中位数,反映曲线描述数据的能力;cv_share_over
是误差超过 cv_limit(默认 10
kg)的猪的比例。用中位数是因为灵活的曲线(如
poly3)在个别猪身上可能外推出离谱的值(上万公斤),一两头这样的猪就会把平均值拉得面目全非;这类风险单独看
cv_share_over(1.6.1
之前用的是平均值)。按块留出,留出日子的邻近几天也不在训练集里,更接近真实的成段缺失;block_days = 1
是 1.5.0 之前按单天分折的做法,误差会明显偏小。late,模拟晚进站)或结尾(early,模拟早出栏)的
trim_days 天(默认 10 天和 20 天),重新计算
ADG,与同一模型用全部数据算出的 ADG
比较。bias_*、rmse_*(g/d)和
spearman_*
反映曲线外推的稳定性,这正是测定期没有覆盖整个体重区间时最关心的问题。两种检验都先删掉被隐藏日子的全部体重记录,再用 qc
的设置重新清洗剩下的记录。
va_n <- validate_adg(w_n, verbose = 0)
tidy(va_n)
#> model cv_rmse cv_mae cv_share_over bias_late10 rmse_late10 spearman_late10 n_late10
#> <char> <num> <num> <num> <num> <num> <num> <int>
#> 1: poly1+start 1.76 1.41 0 11.4 23.6 0.960 29
#> 2: poly2+start 1.75 1.40 0 24.0 31.3 0.969 29
#> 3: poly3+start 1.61 1.26 0 26.1 37.2 0.959 29
#> 4: gompertz+start 1.54 1.21 0 17.4 26.8 0.963 29
#> bias_early10 rmse_early10 spearman_early10 n_early10 bias_late20 rmse_late20 spearman_late20
#> <num> <num> <num> <int> <num> <num> <num>
#> 1: 4.30 21.5 0.997 29 -5.10 29.0 0.939
#> 2: 7.43 23.0 0.996 29 9.87 28.2 0.947
#> 3: 4.83 23.9 0.996 29 17.00 36.2 0.915
#> 4: 5.70 22.5 0.999 29 5.83 26.8 0.937
#> n_late20 bias_early20 rmse_early20 spearman_early20 n_early20
#> <int> <num> <num> <num> <int>
#> 1: 29 0.236 51.9 0.971 29
#> 2: 29 14.100 49.9 0.976 29
#> 3: 29 -3.190 60.1 0.933 29
#> 4: 29 -0.114 52.0 0.981 29
va_f <- validate_adg(w_f, qc = qc_f, verbose = 0)
tidy(va_f)
#> model cv_rmse cv_mae cv_share_over bias_late10 rmse_late10 spearman_late10 n_late10
#> <char> <num> <num> <num> <num> <num> <num> <int>
#> 1: poly1+start 2.01 1.63 0 -16.7 78.4 0.796 25
#> 2: poly2+start 1.90 1.54 0 -49.1 109.0 0.638 25
#> 3: poly3+start 1.83 1.47 0 -72.6 126.0 0.508 25
#> 4: gompertz+start 1.89 1.53 0 -48.2 108.0 0.657 25
#> bias_early10 rmse_early10 spearman_early10 n_early10 bias_late20 rmse_late20 spearman_late20
#> <num> <num> <num> <int> <num> <num> <num>
#> 1: -17.00 40.2 0.963 25 144 167 0.812
#> 2: -6.77 30.7 0.938 25 147 184 0.811
#> 3: 17.10 44.3 0.856 25 134 170 0.795
#> 4: -9.18 40.6 0.937 25 147 184 0.814
#> n_late20 bias_early20 rmse_early20 spearman_early20 n_early20
#> <int> <num> <num> <num> <int>
#> 1: 24 -66.2 90.6 0.891 25
#> 2: 24 -93.3 129.0 0.762 25
#> 3: 25 34.5 116.0 0.592 25
#> 4: 24 -91.0 124.0 0.802 25block_days = 1)误差全面更小(NEDAP
gompertz 1.39 kg、poly2 1.50
kg),因为留出的日子两边都有记录。灵活模型的风险不在中位数上,而在少数猪身上:留出一整段时,poly3
可能外推出离谱的值,这时看
cv_share_over。示例数据中各模型都没有误差超过 10 kg
的猪。calc_adg()
结果中的
adg_type、n_curve_days):大多数猪都是
observed
时,选哪种曲线影响很小;靠曲线填充的天数多时,应优先看截断检验,FIRE
这类晚进站的批次可考虑 curve_model = "poly1"。extrap = c("curve", "tangent", "start")
同时比较几种外推方式(curve_extrap):记录范围内都用同一条曲线,范围之外切线外推改为直线,斜率取首、末
14
天记录的稳健斜率;"start"(默认)只在首个记录之前用直线。不是
"curve" 的模型标为 poly2+tangent 等。
va_t <- validate_adg(w_n, models = c("poly1", "poly2"), extrap = c("curve", "start", "tangent"),
trim_days = c(10, 20, 30), verbose = 0)
tidy(va_t)[, c("model", "cv_rmse", grep("^rmse_", names(va_t$summary), value = TRUE)), with = FALSE]
#> Key: <model>
#> model cv_rmse rmse_late10 rmse_early10 rmse_late20 rmse_early20 rmse_late30 rmse_early30
#> <char> <num> <num> <num> <num> <num> <num> <num>
#> 1: poly1 1.83 27.0 21.5 51.2 51.9 82.6 64.0
#> 2: poly1+start 1.76 23.6 21.5 29.0 51.9 84.5 64.0
#> 3: poly1+tangent 1.73 23.5 21.0 28.9 51.3 84.4 66.2
#> 4: poly2 1.85 33.9 23.0 44.8 49.9 69.2 75.0
#> 5: poly2+start 1.75 31.3 23.0 28.2 49.9 86.8 74.8
#> 6: poly2+tangent 1.72 31.4 22.0 28.4 50.0 86.9 64.5在示例 NEDAP 数据上,开头缺 10–20 天时,开头切线明显改善 ADG(poly2
开头缺 20 天:45 → 28 g/d);开头缺 30 天时反而变差(69 → 87
g/d),外推太远时直线不如曲线。末端切线在结尾缺 30 天时改善 poly2(75 →
65 g/d)。但末端的采食是按外推的体重填充的,从整条流程看,末端用切线会让
FCR 排名变差(见第 5 节),所以默认
"start"。分块交叉验证中,含首末两块的那几折也要外推,所以外推方式对
cv_rmse 也有一些影响。
validate_weight_fill()
对每头猪,先用全部记录算出区间和 ADG(作为参照),再按
scenarios
隐藏区间内一部分有记录的日子(场景的写法同
validate_adfi()),删掉这些日子的全部体重记录,对剩下的记录重新清洗,然后用
methods 中的每种方法重新填充、重新计算 ADG:
linear:calc_adg()
的做法,两个有记录的日子之间线性插值,首尾用
curve_model(默认 poly2);poly1、poly2、poly3、gompertz:所有缺失的日子都用该生长曲线填充。指标:daily_rmse、daily_mae、daily_bias
是被隐藏日子的填充体重与实测日体重之差(kg);adg_bias、adg_rmse(g/d)和
adg_spearman 是 ADG 与参照 ADG 的比较。
sc_w <- c("random30", "blocks3", "blocks6", "late10", "late20", "early15")
vw_n <- validate_weight_fill(w_n, scenarios = sc_w, reps = 2, verbose = 0)
tidy(vw_n)
#> scenario method daily_rmse daily_mae daily_bias adg_bias adg_rmse adg_spearman n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <num> <int>
#> 1: random30 linear+start 0.835 0.601 -0.0632 0.311 6.06 0.996 29
#> 2: random30 poly1+start 1.620 1.320 0.2220 -5.190 12.60 0.983 29
#> 3: random30 poly2+start 1.440 1.160 0.1090 -5.620 10.90 0.993 29
#> 4: random30 poly3+start 1.170 0.927 -0.0385 -6.590 11.80 0.993 29
#> 5: random30 gompertz+start 1.190 0.949 -0.0290 -6.540 9.89 0.994 29
#> 6: blocks3 linear+start 1.090 0.833 -0.0878 -0.762 15.30 0.995 29
#> 7: blocks3 poly1+start 1.770 1.400 0.2070 -5.480 12.40 0.984 29
#> 8: blocks3 poly2+start 1.530 1.230 0.0291 -6.530 15.50 0.991 29
#> 9: blocks3 poly3+start 1.330 1.060 -0.2160 -5.910 20.30 0.985 29
#> 10: blocks3 gompertz+start 1.270 1.020 -0.1970 -7.310 12.40 0.990 29
#> 11: blocks6 linear+start 2.920 1.080 0.2590 13.700 90.60 0.878 29
#> 12: blocks6 poly1+start 1.760 1.420 0.3570 -0.732 44.60 0.945 29
#> 13: blocks6 poly2+start 3.120 1.510 0.4010 5.220 92.20 0.865 29
#> 14: blocks6 poly3+start 1.380 1.080 -0.0393 4.040 55.50 0.954 29
#> 15: blocks6 gompertz+start 2.990 1.300 0.1780 9.770 92.20 0.872 29
#> 16: late10 linear+start 1.970 1.430 -0.9670 10.200 21.20 0.980 29
#> 17: late10 poly1+start 2.130 1.680 -0.2040 4.490 20.40 0.972 29
#> 18: late10 poly2+start 2.310 1.870 -0.7500 8.740 23.80 0.979 29
#> 19: late10 poly3+start 2.010 1.600 -0.6590 3.990 23.70 0.979 29
#> 20: late10 gompertz+start 1.650 1.260 -0.3170 0.113 16.60 0.983 29
#> 21: late20 linear+start 2.430 1.670 0.3530 -6.770 50.40 0.922 29
#> 22: late20 poly1+start 3.180 2.380 1.3700 -19.400 52.30 0.913 29
#> 23: late20 poly2+start 2.700 2.120 0.7580 -10.900 51.70 0.913 29
#> 24: late20 poly3+start 2.240 1.670 0.4390 -12.100 53.40 0.927 29
#> 25: late20 gompertz+start 2.240 1.590 0.5820 -13.500 46.00 0.935 29
#> 26: early15 linear+start 1.540 1.240 0.2860 11.900 44.20 0.986 29
#> 27: early15 poly1+start 1.610 1.310 -0.3640 4.710 43.80 0.985 29
#> 28: early15 poly2+start 1.520 1.250 -0.1000 7.430 43.50 0.985 29
#> 29: early15 poly3+start 1.660 1.310 0.5430 7.530 46.10 0.986 29
#> 30: early15 gompertz+start 1.480 1.200 0.3760 8.370 43.30 0.989 29
#> scenario method daily_rmse daily_mae daily_bias adg_bias adg_rmse adg_spearman n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <num> <int>
vw_f <- validate_weight_fill(w_f, scenarios = sc_w, reps = 2, verbose = 0)
tidy(vw_f)
#> scenario method daily_rmse daily_mae daily_bias adg_bias adg_rmse adg_spearman n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <num> <int>
#> 1: random30 linear+start 0.934 0.709 -0.0762 -2.100 11.0 0.987 25
#> 2: random30 poly1+start 1.780 1.380 0.1630 -4.210 43.7 0.971 25
#> 3: random30 poly2+start 1.450 1.150 -0.1260 3.260 13.6 0.984 25
#> 4: random30 poly3+start 1.310 1.030 -0.0923 4.550 15.7 0.976 25
#> 5: random30 gompertz+start 1.460 1.170 -0.1440 0.618 17.3 0.981 25
#> 6: blocks3 linear+start 1.350 1.010 -0.0542 0.482 20.7 0.981 25
#> 7: blocks3 poly1+start 2.080 1.640 0.1780 0.549 49.0 0.942 25
#> 8: blocks3 poly2+start 1.750 1.370 -0.1620 7.170 27.5 0.963 25
#> 9: blocks3 poly3+start 1.760 1.360 -0.0955 8.470 27.6 0.947 25
#> 10: blocks3 gompertz+start 1.770 1.390 -0.2130 4.850 31.2 0.962 25
#> 11: blocks6 linear+start 1.790 1.250 0.2270 -10.400 77.0 0.889 25
#> 12: blocks6 poly1+start 2.180 1.710 0.6350 -2.920 81.1 0.906 25
#> 13: blocks6 poly2+start 2.080 1.600 0.2120 -1.880 80.5 0.891 25
#> 14: blocks6 poly3+start 2.280 1.600 0.2310 15.400 60.8 0.845 25
#> 15: blocks6 gompertz+start 2.130 1.650 0.1940 -3.950 83.5 0.899 25
#> 16: late10 linear+start 1.920 1.440 0.5200 -11.100 37.1 0.879 25
#> 17: late10 poly1+start 1.790 1.430 -0.5770 3.070 47.3 0.940 25
#> 18: late10 poly2+start 2.240 1.810 0.2340 -6.590 42.5 0.875 25
#> 19: late10 poly3+start 2.270 1.810 0.4540 -9.800 46.2 0.871 25
#> 20: late10 gompertz+start 2.240 1.830 0.2360 -8.750 43.5 0.886 25
#> 21: late20 linear+start 1.940 1.530 -0.6190 7.130 57.9 0.845 25
#> 22: late20 poly1+start 2.480 2.020 -0.9930 19.400 75.6 0.856 25
#> 23: late20 poly2+start 2.260 1.810 -0.9040 16.500 69.4 0.795 25
#> 24: late20 poly3+start 2.560 1.910 -0.5030 2.770 92.5 0.742 25
#> 25: late20 gompertz+start 2.310 1.860 -0.9750 16.400 71.1 0.802 25
#> 26: early15 linear+start 2.300 1.660 -0.4740 -15.300 60.2 0.930 24
#> 27: early15 poly1+start 2.450 1.970 -0.5780 -22.500 62.6 0.953 24
#> 28: early15 poly2+start 2.330 1.720 -0.4590 -15.100 60.1 0.943 24
#> 29: early15 poly3+start 2.970 2.450 -0.3700 -2.360 50.7 0.829 24
#> 30: early15 gompertz+start 2.280 1.640 -0.2750 -8.710 50.8 0.940 24
#> scenario method daily_rmse daily_mae daily_bias adg_bias adg_rmse adg_spearman n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <num> <int>blocks6,约三成的日子),个别猪会出大错:NEDAP
上一头猪被隐藏的日子紧挨着最后几天记录,重新清洗时,从末期往回的逐日追踪在三周的空档之后接不上,把最后两天的记录也删掉了,末端只能靠曲线外推,ADG
偏高约 670 g/d,拉高了 linear 的
adg_rmse。真实缺失时也会发生同样的事,这正是不泄露的检验才看得到的。early15),末个有记录日之后没有实测,各方法都只能靠曲线,结果接近;ADG
的误差在 40–60 g/d,比中间缺失大得多。validate_adfi()
只使用区间内有记录日子比例不低于 min_coverage(默认
0.8)的猪。对每头猪,按 scenarios
隐藏一部分有记录的日子:
random30:随机隐藏 30% 的日子;blocks3:隐藏 3 段连续的日子,每段 3–10 天;late20:隐藏区间开头的 20 天(晚进站);early15:隐藏区间结尾的 15 天(早出栏)。数字可以改,例如
random50、blocks6、late10。然后用
methods 中的每种方法(默认
loess_pop、loess_lm 和
linear)重新插补,在原本有记录的日子上计算
ADFI,与实测的 ADFI 比较。
插补要用到日体重。传入
weight(clean_weight()
的结果)时,被隐藏日子的体重记录也一并删掉,剩下的记录重新清洗、重新算日体重,和这头猪真的没进站时一样;不传时沿用全部数据的日体重(1.5.0
之前的做法,会给出警告)。
loess_pop
的群体曲线对每头猪都用留一法拟合:只用其他猪的数据,被隐藏的日子不会通过曲线”泄露”回来。
指标:mean_abs_error_pct、p95_abs_error_pct
是个体 ADFI 误差绝对值的平均和 95% 分位数(占实测 ADFI
的百分比);bias_pct 是平均偏差;spearman 是
ADFI 排名与实测的相关;daily_rmse
是被隐藏日子的日采食误差(g/d)。
f_n <- calc_adfi(v_n, calc_adg(w_n, verbose = 0), verbose = 0)
sc <- c("random30", "random50", "blocks3", "blocks6", "late10", "late20", "early15")
vf_n <- validate_adfi(f_n, w_n, scenarios = sc, verbose = 0)
tidy(vf_n)
#> scenario method mean_abs_error_pct p95_abs_error_pct bias_pct spearman daily_rmse n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <int>
#> 1: random30 loess_pop 1.100 2.55 0.01770 0.989 455 29
#> 2: random30 loess_lm 1.090 2.52 0.06180 0.990 457 29
#> 3: random30 linear 1.010 2.42 0.07510 0.989 507 29
#> 4: random50 loess_pop 1.810 4.27 -0.10200 0.980 466 29
#> 5: random50 loess_lm 1.810 4.31 -0.00435 0.982 467 29
#> 6: random50 linear 1.740 4.39 0.10600 0.977 514 29
#> 7: blocks3 loess_pop 1.290 3.08 0.00254 0.987 477 29
#> 8: blocks3 loess_lm 1.360 3.26 -0.34500 0.981 483 29
#> 9: blocks3 linear 1.610 4.84 -0.21300 0.973 533 29
#> 10: blocks6 loess_pop 1.940 5.48 -0.51600 0.976 484 29
#> 11: blocks6 loess_lm 2.390 6.05 -0.88700 0.959 508 29
#> 12: blocks6 linear 2.420 6.80 -0.35700 0.953 529 29
#> 13: late10 loess_pop 0.724 1.75 -0.41000 0.988 313 29
#> 14: late10 loess_lm 1.540 2.55 1.31000 0.990 498 29
#> 15: late10 linear 1.540 2.55 1.31000 0.990 498 29
#> 16: late20 loess_pop 1.340 3.50 -0.29700 0.980 321 29
#> 17: late20 loess_lm 4.490 7.06 4.26000 0.954 679 29
#> 18: late20 linear 4.490 7.06 4.26000 0.954 679 29
#> 19: early15 loess_pop 3.700 8.51 3.39000 0.969 863 29
#> 20: early15 loess_lm 3.700 8.51 3.39000 0.969 863 29
#> 21: early15 linear 3.700 8.51 3.39000 0.969 863 29
#> scenario method mean_abs_error_pct p95_abs_error_pct bias_pct spearman daily_rmse n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <int>
f_f <- calc_adfi(v_f, calc_adg(w_f, verbose = 0), verbose = 0)
vf_f <- validate_adfi(f_f, w_f, scenarios = sc, verbose = 0)
tidy(vf_f)
#> scenario method mean_abs_error_pct p95_abs_error_pct bias_pct spearman daily_rmse n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <int>
#> 1: random30 loess_pop 1.46 3.81 0.482 0.921 637 23
#> 2: random30 loess_lm 1.48 3.70 0.497 0.924 633 23
#> 3: random30 linear 1.53 4.23 0.246 0.919 683 23
#> 4: random50 loess_pop 2.04 4.92 0.369 0.918 666 23
#> 5: random50 loess_lm 2.02 5.09 0.344 0.916 670 23
#> 6: random50 linear 2.22 6.41 -0.155 0.910 700 23
#> 7: blocks3 loess_pop 2.22 5.43 -0.337 0.886 647 23
#> 8: blocks3 loess_lm 2.39 6.22 -0.487 0.861 668 23
#> 9: blocks3 linear 2.79 8.48 0.197 0.839 747 23
#> 10: blocks6 loess_pop 3.30 7.79 -0.583 0.772 683 23
#> 11: blocks6 loess_lm 4.71 9.17 -0.978 0.693 760 23
#> 12: blocks6 linear 5.29 13.20 -1.070 0.691 833 23
#> 13: late10 loess_pop 1.56 3.18 -0.429 0.918 548 23
#> 14: late10 loess_lm 2.14 7.47 -0.952 0.883 656 23
#> 15: late10 linear 2.14 7.47 -0.952 0.883 656 23
#> 16: late20 loess_pop 2.55 6.86 -0.743 0.862 562 23
#> 17: late20 loess_lm 6.95 12.20 -2.380 0.443 820 23
#> 18: late20 linear 6.95 12.20 -2.380 0.443 820 23
#> 19: early15 loess_pop 4.00 6.88 -1.280 0.740 857 22
#> 20: early15 loess_lm 4.00 6.88 -1.280 0.740 857 22
#> 21: early15 linear 4.00 6.88 -1.280 0.740 857 22
#> scenario method mean_abs_error_pct p95_abs_error_pct bias_pct spearman daily_rmse n_pigs
#> <char> <char> <num> <num> <num> <num> <num> <int>loess_pop 明显更好:NEDAP
开头缺 20 天时,误差从 4.5% 降到 1.3%,偏差从 +4.3% 降到约 0;FIRE
开头缺 20 天时,误差从 7.0% 降到 2.6%,排名相关从 0.44 升到
0.86。晚进站的猪只有后期数据,单猪回归的直线往前推时,前期采食会被高估。loess_pop 也略好:FIRE
缺 6 段时,误差从 4.7%(loess_lm)降到 3.3%。loess_lm 6.8% →
7.0%):这一步对日体重并不敏感。区间起止和各步骤之间的相互影响,见下一节的整条流程检验。这和《数据准备》中的结论一致:保证测定期两端有记录,比选择插补方法更重要。这也是
calc_fcr() 把两端外推超过 15
天的猪标记为低可信度的原因。
validate_truncation()
adg_type == "observed"),随机分成两组;late)、结尾(early)或中间(gap)若干天的全部访问,体重和采食都删;第二组保持完整;从
clean_weight()
起重跑整条流程。再换第二组截断,第一组完整。这样每头猪在每次重复中恰好被截断一次,而群体曲线和校正模型始终能看到完整区间的数据,和实际批次中”只有部分猪晚进站或早出栏”的情况一样;reps 次,每次重新随机分组。被删掉的日子不会以任何方式参与计算,所以结果反映的是”一头猪真的缺了这些天”时会发生什么。
每个设置、场景和性状(adg、adfi、fcr)一行,取各次重复的平均,_sd
结尾的列是重复之间的标准差:
| 指标 | 含义 | 怎么看 |
|---|---|---|
bias |
截断后 − 全部数据的平均差 | 越接近 0 越好。偏差比误差更要紧:实际批次中只有部分猪缺数据,偏差会让这些猪在排名中系统性地占便宜或吃亏 |
rmse、mae_pct
|
均方根误差、平均相对误差(%) | 越小越好 |
pearson、spearman
|
与全部数据结果的相关、排名相关 | 越接近 1 越好 |
slope |
截断后对全部数据的回归斜率 | 小于 1:个体差异被压缩,大于 1:被放大;越接近 1 越好,对育种值估计很重要 |
top_agree |
全部数据下最好的 top(默认
20%)的猪,截断后仍在最好之列的比例 |
越高越好;猪少时很粗(20 头猪只看 4 头) |
n、n_lost
|
有结果的猪数;截断后失去结果的猪数 | 截断太多时,猪会因测定期太短等原因被剔除 |
vt <- validate_truncation(
v_n,
settings = list(default = qc_config(),
curve = qc_config(curve_extrap = "curve"),
lm = qc_config(impute_method = "loess_lm", curve_extrap = "curve")),
scenarios = c("late20", "early20", "gap10"), reps = 2, verbose = 0
)
vt$summary[trait == "fcr", .(setting, scenario, mae_pct = signif(mae_pct, 3),
spearman = signif(spearman, 3), slope = signif(slope, 3),
top_agree, n, n_lost)]
#> setting scenario mae_pct spearman slope top_agree n n_lost
#> <char> <char> <num> <num> <num> <num> <num> <num>
#> 1: default late20 1.800 0.923 1.010 0.75 21 0
#> 2: curve late20 3.380 0.771 0.853 0.75 21 0
#> 3: lm late20 4.570 0.804 1.100 0.75 21 0
#> 4: default early20 2.760 0.947 1.050 0.75 21 0
#> 5: curve early20 2.760 0.947 1.050 0.75 21 0
#> 6: lm early20 2.760 0.947 1.050 0.75 21 0
#> 7: default gap10 0.696 0.988 0.963 1.00 21 0
#> 8: curve gap10 0.699 0.988 0.963 1.00 21 0
#> 9: lm gap10 1.440 0.979 0.955 1.00 21 0default
是现在的默认设置(impute_method = "loess_pop",curve_extrap = "start"),curve
是两端都沿曲线外推,lm 是 1.3.0
之前的做法(采食都用单猪回归,体重两端都沿曲线外推)。在示例 NEDAP
数据上,开头缺 20 天时,默认设置的 FCR 排名相关约 0.92,另两种约
0.77–0.80。
在一个 48 个站、457 头猪(测定区间被完整覆盖)的 NEDAP 批次上,每个场景重复 2 次:
| 场景 | 指标 | 单猪回归 | 群体曲线 | 默认(群体曲线 + 开头切线 + 末端单猪回归) |
|---|---|---|---|---|
| 开头缺 20 天 | ADFI 误差 | 7.50% | 5.87% | 4.90% |
| FCR 排名相关 | 0.804 | 0.815 | 0.856 | |
| 中间缺 20 天 | ADFI 误差 | 2.88% | 2.20% | 2.31% |
| 结尾缺 20 天 | ADFI 偏差 | +23 g/d | −43 g/d | +22 g/d |
| FCR 误差 | 4.51% | 5.82% | 4.50% | |
| 结尾缺 30 天 | ADFI 偏差 | −28 g/d | −129 g/d | −29 g/d |
| FCR 误差 | 6.65% | 9.31% | 6.65% |
max_edge_extrap_days)。大批次数据建议抽样并行运行:
visits <- prep_visits(read_station("数据/"))
w <- clean_weight(visits, cores = 4)
a <- calc_adg(w, cores = 4)
f <- calc_adfi(visits, a, cores = 4)
vo <- validate_outliers(visits, max_pigs = 300, cores = 4)
va <- validate_adg(w, max_pigs = 300, cores = 4)
vw <- validate_weight_fill(w, max_pigs = 300, cores = 4)
vf <- validate_adfi(f, w, max_pigs = 300, cores = 4)
vt <- validate_truncation(visits, settings = list(default = qc_config(),
curve = qc_config(curve_extrap = "curve")),
reps = 2, cores = 4)
# 把四张汇总表保存下来
fwrite(vo$summary, "验证_异常值.csv", bom = TRUE)
fwrite(va$summary, "验证_生长曲线.csv", bom = TRUE)
fwrite(vw$summary, "验证_体重填充.csv", bom = TRUE)
fwrite(vf$summary, "验证_采食插补.csv", bom = TRUE)
fwrite(vt$summary, "验证_整体截断.csv", bom = TRUE)建议的步骤:
validate_truncation() 为准;$settings
一起保存,写论文或复查时可以直接引用。validate_outliers()
的”干净记录”是各方法的共识,所有方法都漏掉的真实误差仍在其中。validate_weight_fill()
隐藏的是整天的记录,与真实缺失(不进站、耳标脱落)相同;但被隐藏的日子是随机选的,而真实缺失可能与猪的状态有关。validate_adg() 的截断检验以同一模型用全部数据算出的 ADG
为参照,衡量的是稳定性;如果全部数据本身就需要外推,参照值也带有外推误差。validate_truncation()
同样以全部数据的结果为参照,而不是未知的真值;它只用测定区间被完整覆盖的猪,这些猪不一定能代表进站晚、出栏早的猪。