深度神经网络 (第 I 部)。准备数据·综合运用
(3/3)·垃圾进垃圾出,70% 的建模时间耗在数据准备,本篇把清洗到消偏态一次跑通
分布偏度与峰度暴露的非正态事实
上表给出四个数字滤波通道的描述统计:第三四分位分别为 0.0273、0.0143、0.0207、0.0278,标准差在 0.0264 到 0.0579 之间,而峰度最高冲到 233.1964(第四通道),远超正态基准值 3。 偏度一列从 0.8513 到 3.0207 不等,说明这四组序列右尾普遍偏厚;外汇与贵金属价差类特征若呈现这种形态,回测里用正态假设算置信区间会系统性低估极端滑点风险。 95% 均值置信带很窄(如 LCL -0.0013 到 UCL 0.0013),表面看均值接近零,但结合峰度可知:均值稳定不代表尾部可控,样本外跳空可能倾向比正态模型预测更频繁。 下方 R 代码用 GGally 的 ggpairs 按 Class 着色,把 2:6、7:10、11:14 列分别画成 DigFilter1~3 的成对散点矩阵;在 MT5 导出同类特征 csv 后,可照此在 R 里复跑,肉眼核对聚类是否和训练标签一致。
require(GGally) evalq(ggpairs(dataSet, columns = class="num">2:class="num">6, mapping = aes(class="type">class="kw">color = Class), title = "DigFilter1"), env) evalq(ggpairs(dataSet, columns = class="num">7:class="num">10, mapping = aes(class="type">class="kw">color = Class), title = "DigFilter2"), env) evalq(ggpairs(dataSet, columns = class="num">11:class="num">14, mapping = aes(class="type">class="kw">color = Class), title = "DigFilter3"), env)
「数据准备七阶段与异常值实战处理」
把原始行情特征送进模型前,先走完七道准备关:插补缺失、剔零方差、切分集合、缩放范围、清异常值、调类别不平衡、降噪并筛预测器。任何模型都建议先对完整数据集做清理,而不是等训练时才补窟窿。 用 caret 的 preProcess 一次性干掉零方差、近零方差以及单类唯一值预测器,相关系数阈值默认 0.9 可改。下方代码在 7906 行样本上跑完,v.rbci 被移除,干净集合剩 13 列。 局部异常值按 IQR 法认定:超出 ±1.5*IQR 即异常,±3*IQR 外算极端。外汇与贵金属特征里这种点常由滤波人造,直接删或换成 0.05/0.95 百分位更稳。 全局异常值用 LOF 看局部密度,Rlof 包支持多核算 k=5~10 邻居。实测绝大多数观测 lofactor 落在 1.6 内,越界数量相对 4000 根训练柱线来讲不大,但测试集必须复用训练集算出的 upper/lower/cap 参数,不能各自为政。 偏度与峰度检查显示,删异常后的 x.out 与插补后的 x.cap 都已接近对称,峰度也回正常,无需额外变换。提醒:外汇贵金属杠杆高、跳空多,异常值处理不当会放大回测虚高,实盘前务必用 MT5 导出的真实tick重跑这套。
require(caret) evalq({preProClean <- preProcess(x = dataSet,method = c("zv", "nzv", "conditionalX", "corr")) dataSetClean <- predict(preProClean, dataSet %>% na.omit)}, env) > env$preProClean$method$remove #[class="num">1] "v.rbci" > dim(env$dataSetClean) [class="num">1] class="num">7906 class="num">13 > colnames(env$dataSetClean) [class="num">1] "Data" "ftlm" "stlm" "rbci" "pcci" [class="num">6] "v.fatl" "v.satl" "v.rftl" "v.rstl" "v.ftlm" [class="num">11] "v.stlm" "v.pcci" "Class" evalq(ggplot(dataSetClean, aes(x = factor(class="num">0),
◍ 用箱线法清洗指标离群值
做特征工程时,ftlm 这类技术指标序列里常混着极端跳变点。直接用 R 的 boxplot.stats 剔除,和按四分位距(IQR)手写阈值过滤,结果完全一致:all.equal 返回 TRUE,说明两套逻辑等价。 在样本集上跑一遍,原始数据行数减去保留下来的 ftlm 有效值,差了 402 条,也就是被判定为离群被丢掉的观测量。外汇与贵金属波动剧烈,这类异常值可能来自跳空或流动性真空,直接进模型会扭曲边界。 remove_outliers 函数把低于 Q1-1.5×IQR 或高于 Q3+1.5×IQR 的点设成 NA,用 foreach 按列批量处理很顺手。 如果不想删数据,capping_outliers 把离群值压到 5% 和 95% 分位上,箱体图对比能看出分布被「削峰」但样本量保住了。MT5 导出的 csv 丢进 R 照抄这段,能马上看到你自己的指标里有多少离群点。
remove_outliers <- function(x, na.rm = TRUE, ...) { qnt <- quantile(x, probs = c(.class="num">25, .class="num">75), na.rm = na.rm, ...) H <- class="num">1.5 * IQR(x, na.rm = na.rm) y <- x y[x < (qnt[class="num">1] - H)] <- NA y[x > (qnt[class="num">2] + H)] <- NA y } capping_outliers <- function(x, na.rm = TRUE, ...) { qnt <- quantile(x, probs = c(.class="num">25, .class="num">75), na.rm = na.rm, ...) caps <- quantile(x, probs = c(.class="num">05, .class="num">95), na.rm = na.rm, ...) H <- class="num">1.5 * IQR(x, na.rm = na.rm) y <- x y[x < (qnt[class="num">1] - H)] <- caps[class="num">1] y[x > (qnt[class="num">2] + H)] <- caps[class="num">2] y }
用局部离群因子和分位数给特征掐头去尾
这段脚本在 R 环境里先对原始特征矩阵 x 和裁剪版 x.cap 跑 LOF 离群检测:DMwR2 的 lofactor 用 k=10 算一遍,Rlof 的 lof 则对 k=5:10 并行跑 Minkowski 距离。把 Rlof.x.cap 第 6 列(k=10 结果)做直方图后,筛选值 ≥1.6 的样本数得出 32 个,说明在该邻域半径下约有 32 个高异常度点,外汇与贵金属特征里这类点多半是跳空或流动性断裂造成的噪声,需谨慎对待。 自写 prep.outlier 函数不走 LOF,而是用四分位距(IQR×1.5)定上下栅栏,再用 5%/95% 分位数做 cap 边界。对训练集前 4000 行逐列套用后,ftlm 列的 lower 约 -0.222、upper 约 0.221,cap1/cap2 落在 ±0.17 附近;stlm 的上下栅栏则在 ±0.596,cap 边界 ±0.40。这些数字直接可作为 MT5 自定义指标里 winsorize 的阈值参考。 实操上,把 cap1/cap2 两行当特征裁剪上下限,能在不删样本的前提下压住极端值。外汇与贵金属波动高危,阈值最好随品种波动率重算,别直接套用上面这组静态数。
mapping = aes(class="type">class="kw">color = Class), title = "PredCap1"), env) evalq(ggpairs(dataSetCap, columns = class="num">8:class="num">13, mapping = aes(class="type">class="kw">color = Class), title = "PredCap2"), env) ##------DMwR2------------------- require(DMwR2) evalq(lof.x <- lofactor(x,class="num">10), env) evalq(lof.x.cap <- lofactor(x.cap,class="num">10), env) par(mfrow = c(class="num">1, class="num">3)) boxplot(env$lof.x, main = "lof.x", boxwex = class="num">0.5) boxplot(env$lof.x.cap, main = "lof.x.cap", boxwex = class="num">0.5) hist(env$lof.x.cap, breaks = class="num">20) par(mfrow = c(class="num">1, class="num">1)) require(Rlof) evalq(Rlof.x <- lof(x, c(class="num">5:class="num">10), cores = class="num">2, method = &class="macro">#x27;minkowski&class="macro">#x27;), env) evalq(Rlof.x.cap <- lof(x.cap, c(class="num">5:class="num">10), cores = class="num">2, method = &class="macro">#x27;minkowski&class="macro">#x27;), env) par(mfrow = c(class="num">2, class="num">3)) hist(env$Rlof.x.cap[ ,class="num">6], breaks = class="num">20) hist(env$Rlof.x.cap[ ,class="num">5], breaks = class="num">20) hist(env$Rlof.x.cap[ ,class="num">4], breaks = class="num">20) hist(env$Rlof.x.cap[ ,class="num">3], breaks = class="num">20) hist(env$Rlof.x.cap[ ,class="num">2], breaks = class="num">20) hist(env$Rlof.x.cap[ ,class="num">1], breaks = class="num">20) par(mfrow = c(class="num">1, class="num">1)) > sum(env$Rlof.x.cap[ ,class="num">6] >= class="num">1.6) [class="num">1] class="num">32 #-----prep.outlier-------------- prep.outlier <- function(x, na.rm = TRUE, ...) { qnt <- quantile(x, probs = c(.class="num">25, .class="num">75), na.rm = na.rm, ...) H <- class="num">1.5 * IQR(x, na.rm = na.rm) caps <- quantile(x, probs = c(.class="num">05, .class="num">95), na.rm = na.rm, ...) list(lower = qnt[class="num">1] - H, upper = qnt[class="num">2] + H, med = median(x), cap1 = caps[class="num">1], cap2 = caps[class="num">2]) } evalq( {train <- x[class="num">1:class="num">4000, ] foreach(i = class="num">1:ncol(train), .combine = "cbind") %do% { prep.outlier(train[ ,i]) %>% unlist() } -> pre.outl colnames(pre.outl) <- colnames(x) class="macro">#pre.outl %<>% t() }, env)
「用分位数截断给特征去极值」
上面那张表是训练集前 4000 行四个指标的分位数快照:v.ftlm 的 lower.25% 是 -0.0901、upper.75% 是 0.0888,而 cap1.5% 与 cap2.95% 分别卡在 -0.0659 和 0.0662。也就是说,超出 5%/95% 分位的点会被压回这两个边界,中位数 med 仅 -0.00086,分布基本对称。
第二张表是另一组三个指标(v.ftlm、v.stlm、v.pcci)的同样统计:v.pcci 的 upper.75% 达 0.1119,cap2.95% 为 0.0770,说明原始数据里约有 5% 样本被向外拉了将近一半的幅度。外汇与贵金属行情跳空时这类离群值很常见,直接进模型会扭曲权重,属于高风险数据预处理环节。
treatOutlier 这个函数给了三种处理路径:impute=TRUE 时把越界值直接替换成 cap1/cap2;fill=FALSE 时打成 NA;else 分支填中位数 med。实战里我倾向 impute 填边界,保留样本量比删行更稳。
下面两段 evalq 把规则套到 train 和 test(取 4001:6000 行)上,用 pre.outl 里存好的分位数逐列处理,最后两行 boxplot 能让你在 R 里直观对比去极值前后的须线变化。开 MT5 把同类指标导出到 csv,再用这套阈值跑一遍,离群压缩效果立刻能验证。
#---------treatOutlier--------------------------------- treatOutlier <- function(x, impute = TRUE, fill = FALSE, lower, upper, med, cap1, cap2){ if (impute) { x[x < lower] <- cap1 x[x > upper] <- cap2 class="kw">return(x) } if (!fill) { x[x < lower | x > upper] <- NA class="kw">return(x) } else { x[x < lower | x > upper] <- med class="kw">return(x) } } #------------ evalq( { foreach(i = class="num">1:ncol(train), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %do% { stopifnot(exists("pre.outl", envir = env)) lower = pre.outl[&class="macro">#x27;lower.class="num">25%&class="macro">#x27;, i] upper = pre.outl[&class="macro">#x27;upper.class="num">75%&class="macro">#x27;, i] med = pre.outl[&class="macro">#x27;med&class="macro">#x27;, i] cap1 = pre.outl[&class="macro">#x27;cap1.class="num">5%&class="macro">#x27;, i] cap2 = pre.outl[&class="macro">#x27;cap2.class="num">95%&class="macro">#x27;, i] treatOutlier(x = train[ ,i], impute = T, fill = T, lower = lower, upper = upper, med = med, cap1 = cap1, cap2 = cap2) } -> train.out colnames(train.out) <- colnames(train) }, env ) #------------- evalq( {test <- x[class="num">4001:class="num">6000, ] foreach(i = class="num">1:ncol(test), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %do% { stopifnot(exists("pre.outl", envir = env)) lower = pre.outl[&class="macro">#x27;lower.class="num">25%&class="macro">#x27;, i] upper = pre.outl[&class="macro">#x27;upper.class="num">75%&class="macro">#x27;, i] med = pre.outl[&class="macro">#x27;med&class="macro">#x27;, i] cap1 = pre.outl[&class="macro">#x27;cap1.class="num">5%&class="macro">#x27;, i] cap2 = pre.outl[&class="macro">#x27;cap2.class="num">95%&class="macro">#x27;, i] treatOutlier(x = test[ ,i], impute = T, fill = T, lower = lower, upper = upper, med = med, cap1 = cap1, cap2 = cap2) } -> test.out colnames(test.out) <- colnames(test) }, env ) #--------------- evalq(boxplot(train, main = "train with outliers"), env) evalq(boxplot(train.out, main = "train.out without outliers"), env)
◍ 剔除极值后分布形态瞬间归位
把原始序列和去掉离群值后的版本都丢进偏度、峰度函数,差距一眼就能看出来。原始 x 里 ftlm 偏度 4.22、pcci 偏度 6.49,峰度更是夸张——pcci 超额峰度 247.98,v.fatl 也有 145.95,典型厚尾尖峰。 去掉 outliers 之后,sk.out 里所有通道偏度都落到 -0.08 到 0.02 之间,k.out 的超额峰度全部在 -0.16 到 -0.03 附近,基本贴着正态。说明那些极端跳空和报价异常才是把分布扯变形的元凶。 对外汇和贵金属来说,这种厚尾是常态高风险,欧美秒跳、黄金突发事件都会制造离群点。做策略回测时若不分桶剔除,参数拟合可能只是被少数极端 K 线带偏。 直接复现这段 R 评估代码,在 MT5 导出的 tick 或分钟序列上跑一遍,看你自己品种的 sk 和 k 落点。若原始峰度超 50,先清洗再建模。
evalq(boxplot(test.out, main = "test.out without outliers"), env) #------------------------ evalq({ sk <- skewness(x) sk.out <- skewness(x.out) sk.cap <- skewness(x.cap) }, env) > env$sk ftlm stlm rbci pcci v.fatl Skewness class="num">4.219857 class="num">1.785286 class="num">2.304655 class="num">6.491546 class="num">5.274871 v.satl v.rftl v.rstl v.ftlm v.stlm Skewness class="num">2.677162 class="num">3.954098 class="num">1.568675 class="num">1.207227 class="num">0.8516043 v.pcci Skewness class="num">3.031012 > env$sk.out ftlm stlm rbci pcci Skewness -class="num">0.04272076 -class="num">0.07893945 -class="num">0.02460354 class="num">0.01485785 v.fatl v.satl v.rftl v.rstl Skewness class="num">0.00780424 -class="num">0.02640635 -class="num">0.04663711 -class="num">0.04290957 v.ftlm v.stlm v.pcci Skewness -class="num">0.0009597876 class="num">0.01997082 class="num">0.0007462494 > env$sk.cap ftlm stlm rbci pcci Skewness -class="num">0.03329392 -class="num">0.07911245 -class="num">0.02847851 class="num">0.01915228 v.fatl v.satl v.rftl v.rstl Skewness class="num">0.01412182 -class="num">0.02617518 -class="num">0.03412228 -class="num">0.04596505 v.ftlm v.stlm v.pcci Skewness class="num">0.008181183 class="num">0.009661169 class="num">0.002252508 require(PerformanceAnalytics) evalq({ k <- kurtosis(x) k.out <- kurtosis(x.out) k.cap <- kurtosis(x.cap) }, env) > env$k ftlm stlm rbci pcci Excess Kurtosis class="num">84.61177 class="num">16.77141 class="num">45.01858 class="num">247.9795 v.fatl v.satl v.rftl v.rstl Excess Kurtosis class="num">145.9547 class="num">36.99944 class="num">74.4307 class="num">13.57613 v.ftlm v.stlm v.pcci Excess Kurtosis class="num">86.36448 class="num">23.06635 class="num">233.5408 > env$k.out ftlm stlm rbci Excess Kurtosis -class="num">0.003083449 -class="num">0.1668102 -class="num">0.1197043 pcci v.fatl v.satl Excess Kurtosis -class="num">0.05113439 -class="num">0.02738558 -class="num">0.04341552 v.rftl v.rstl v.ftlm Excess Kurtosis -class="num">0.01219999 -class="num">0.1316499 -class="num">0.0287925 v.stlm v.pcci Excess Kurtosis -class="num">0.1530424 -class="num">0.09950709
峰度偏负暗示指标分布更平
把 ftlm、stlm、rbci 等九条常用辅助线的超额峰度(Excess Kurtosis)拉出来看,数值全部落在 -0.218 到 -0.307 之间,没有一条为正。 这意味着这些序列的尾部比正态分布更薄,中间段更集中,极端值出现的概率偏低。做外汇或贵金属时这类分布容易让人误判突破力度,属于高风险误判点。 直接在 MT5 里用 kurtosis 函数对对应指标缓冲数组跑一遍,就能复现上表;若你换周期后峰度转正,原有关键位参考就需要重估。
env$k.cap
ftlm stlm rbci
Excess Kurtosis -class="num">0.2314336 -class="num">0.3075185 -class="num">0.2982044
pcci v.fatl v.satl
Excess Kurtosis -class="num">0.2452504 -class="num">0.2389486 -class="num">0.2331203
v.rftl v.rstl v.ftlm
Excess Kurtosis -class="num">0.2438431 -class="num">0.2673441 -class="num">0.2180059
v.stlm v.pcci
Excess Kurtosis -class="num">0.2763058 -class="num">0.2698028「三个压缩包分别装了什么」
DARCH12_1.zip 里是系列第一部分的 R 脚本集合:dataRaw.R、PrepareData.R、FUNCTION.R,外加一张展示 Rstudio 初始会话的示意图,底层数据文件是 Cotir.RData。把它解压进 Rstudio 后,三个脚本会直接出现在工程目录里,可立即改路径跑通数据预处理。 如果想对照另一套思路,ACTF.zip 收的是 V. Kravchuk 那篇《跟随趋势和市场周期的新自适应方法》原文,适合做方法交叉验证。 R_intro.zip 则是 R 语言本身的参考资料包,给没踩过 R 坑的 MT5 玩家补环境基础。三个包也都能从 Git/Part_I 拉取,不用死守网盘链接。
◍ 收束
上面那段 R 代码是整套深度神经网络系列里最容易被忽略的落点:用轻量环境隔离数据、再用 ZigZag 把价格序列压成拐点向量。env <- new .env() 建的是一个独立命名空间,ls(env) 返回 character(0) 说明初始为空,往里塞 env$a <- 23 后 ls(env) 才出现 "a",这种隔离能避免不同工具变量互相污染。 ZZ 函数里 par 默认 c(25,5),ch=25 在 Dig 未知时会走 ch <- ch/(10^(Dig-1)) 做小数位修正;mode=5 时取 High、Low 双列交给 TTR::ZigZag,change=25 点、percent=F、lastExtreme=T,循环把 NA 向前填充。外汇与贵金属波动大,这类拐点提取只是特征工程一步,实盘信号仍可能失效,务必在 MT5 外接 R 跑一遍验证。 系列写到这,从预测因子制定到降维、贝叶斯调参、袋封融合都铺完了。剩下要做的不是再读文章,而是把 ZIP 里的 R-intro(约 15.3 MB)下下来,把 Dig 和 env 在脚本头部补上,让 ZigZag 在你的品种上先跑出第一条折线。
env <- new .env() ls(env) character( class="num">0 ) env$a <- class="num">23 ls(env) [ class="num">1 ] "a" > env$a [ class="num">1 ] class="num">23 #------ZZ----------------------------------- par <- c(class="num">25, class="num">5) ZZ <- function(x, par) { # x - vector require(TTR) require(magrittr) ch = par[class="num">1] mode = par[class="num">2] if (ch > class="num">1) ch <- ch/(class="num">10 ^ (Dig - class="num">1)) class="kw">switch(mode, xx <- x$Close, xx <- x$Med, xx <- x$Typ, xx <- x$Wd, xx <- x %>% select(High,Low)) zz <- ZigZag(xx, change = ch, percent = F, retrace = F, lastExtreme = T) n <- class="num">1:length(zz) for (i in n) { if (is.na(zz[i])) zz[i] = zz[i - class="num">1]} class="kw">return(zz) }