数据科学与机器学习(第 02 部分):逻辑回归(基础篇)
📘

数据科学与机器学习(第 02 部分):逻辑回归(基础篇)

第 1/3 篇

◍ 用 sigmoid 把线性输出压进 0 到 1

逻辑回归和前一篇说的线性回归不是一回事:它本质是分类器,靠一条“希格玛”S 曲线把任意线性输出收拢到 0~1 区间。公式写成 p = 1 / (1 + e^-(c + m1x1 + … + mnxn)),其中 t 是线性模型算出的连续值,p 就是该样本落入某一类的概率。 因为 y 被强制夹在 0 和 1 之间,原本的回归直线就变成了概率响应曲线。做二元判定时只保留 (0,1) 两种离散结果,像多空、真假、高低都归到这一对里,我们后面要写的库也只处理这种二值情形。 落点具体归 0 还是归 1,靠阈值切。默认阈值是 0.5,p≥0.5 四舍五入为 1(赢家侧),p<0.5 记为 0(输家侧)。所以一个算出 0.8 的点会被判为 1,而 0.3 会判为 0——这就是逻辑回归和线性回归在输出解释上的根本分叉。外汇与贵金属波动剧烈,概率判定仅代表倾向,实盘须自担高风险。

「从线性拟合到分类边界的切换」

线性回归面向连续变量,目标是画出一条 y = a·x + b 的直线去逼近数值;逻辑回归面向分类变量,用的是逻辑方程把输出压到 0~1 区间,用来判断样本归哪一类。两者底层都是广义线性模型,但前者解回归问题、后者解分类问题,别在贵金属信号判定里把二者混用。 动手写模型前,有三步能显著降低后续调参成本:先收集并做探索性分析,看特征分布和缺失情况;再清理数据,处理异常值与重复样本;最后核对准确性,确认标签没有标反或错位。外汇与贵金属市场噪声大,这步偷懒会让分类模型在 MT5 回测里过拟合概率明显上升。

用泰坦尼克号数据跑通第一张分布图

这一节不扯理论,直接上手把数据拉进来画分布。样本用的是 1912 年 4 月 15 日泰坦尼克号沉没的乘客记录,字段里 survival 标 0/1、class 分一至三等、sex 分男女、age 与 fare 为连续值。先把数据读进变量 titanic_data,再按列逐个看。 sns.countplot(x="Survived", data=titanic_data) 跑出来能看到,生还者明显少于遇难者,全船大概只有一半不到的人活下来。按性别拆开看,女性生还数大概是男性的两倍多,这个差异在原始计数图里非常直观。 按舱位分组再用 hue='Pclass' 画,三等舱登船人数最多,但存活比例最低,头等舱存活倾向显著更高。年龄和票价不能用 countplot,因为取值太散,改用直方图:age 直接 .plot.hist(),fare 用 30 个 bin 看长尾分布。 十二列里只先可视化了五列,剩下的留到清洗阶段处理。下面三段代码就是本节实际落地的绘图调用,复制进 Jupyter 就能复现。

MQL5 / C++
sns.countplot(x=&class="macro">#x27;Survived&class="macro">#x27;, hue=&class="macro">#x27;Sex&class="macro">#x27;, data=titanic_data)
titanic_data[&class="macro">#x27;Age&class="macro">#x27;].plot.hist() 
titanic_data[&class="macro">#x27;Fare&class="macro">#x27;].plot.hist(bins=class="num">30, figsize=(class="num">10,class="num">10))

◍ 把脏列剔掉再做标签编码

做逻辑回归这类模型,输入列只能是双精度或整数,字符串和无意义字段必须提前清掉。Name、Ticket、Cabin、Embarked 这几列要么没信息量,要么缺失太多(Cabin 前 5 行就露馅),直接用表格软件手动删最省事,删完再读进来看新结构。 性别列里是 male / female 字符串,得转成 0/1 才能喂模型。下面这个函数把指定成员串按逗号拆开,按顺序编成从 0 递增的整数——先出现的 male 拿 0,female 拿 1。脚本跑完打印出 male total=577 Encoded To=0、female total=314 Encoded To=1,说明 891 行里性别分布就是这么切的。 别把顺序当默认 调用时 members="male,female" 决定编码方向,串里第一个值永远拿 0。要是你写成 "female,male",那女性就变 0 了。这个函数不限于两个值,只要类别有意义就能扩。 Age 列还有 177 个缺失值(891 行里占约 19.9%),用 pandas 的 isnull().sum() 一查就定位到。处理办法是用全体非零值的平均值填坑,本例算出来均值是 30.0,所有 0.0 缺口被替换后,数组前几项从 22.0 38.0 26.0 … 0.0 变成 … 30.0,平均年龄填补即生效。外汇和贵金属数据也常遇跳空缺失,这种均值回填思路可借鉴,但行情缺失往往隐含事件风险,直接填均值可能扭曲波动结构,需谨慎。

MQL5 / C++
new_data = pd.read_csv(r&class="macro">#x27;C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\892B47EBC091D6EF95E3961284A76097\MQL5\Files\titanic.csv&class="macro">#x27;)
new_data.head(class="num">5)
class="type">void CLogisticRegression::LabelEncoder(class="type">class="kw">string &src[],class="type">int &EncodeTo[],class="type">class="kw">string members="male,female")
{
   class="type">class="kw">string MembersArray[];
   class="type">class="kw">ushort separator = StringGetCharacter(m_delimiter,class="num">0);
   StringSplit(members,separator,MembersArray); class=class="str">"cmt">//convert members list to an array
   ArrayResize(EncodeTo,ArraySize(src)); class=class="str">"cmt">//make the EncodeTo array same size as the source array

      class="type">int binary=class="num">0;
      for(class="type">int i=class="num">0;i<ArraySize(MembersArray);i++) class=class="str">"cmt">// loop the members array
      {
         class="type">class="kw">string val = MembersArray[i];
         binary = i; class=class="str">"cmt">//binary to assign to a member
         class="type">int label_counter = class="num">0;
         
         for (class="type">int j=class="num">0; j<ArraySize(src); j++)
         {
            class="type">class="kw">string source_val = src[j];
            if (val == source_val)
             {
               EncodeTo[j] = binary;
               label_counter++;
             }
         }
         Print(MembersArray[binary]," total =",label_counter," Encoded To = ",binary);
      }
}

「从 CSV 抽列并做标签编码」

做逻辑回归特征工程时,第一步往往不是算权重,而是把 CSV 里某一列单独抽出来变成数组。下面这段 MT5 代码干的就是这事:指定列号,跳过表头,把其余行读进 string 数组。 void CLogisticRegression::GetDatatoArray(int from_column_number, string &toArr[]) {

int handle = FileOpen(m_filename,FILE_READFILE_WRITEFILE_CSVFILE_ANSI,m_delimiter);

int counter=0; if (handle == INVALID_HANDLE) Print(__FUNCTION__," Invalid csv handle err=",GetLastError()); else { int column = 0, rows=0; while (!FileIsEnding(handle)) { string data = FileReadString(handle); column++; if (column==from_column_number) { if (rows>=1) { counter++; ArrayResize(toArr,counter); toArr[counter-1]=data; } } if (FileIsLineEnding(handle)) { rows++; column=0; } } } FileClose(handle); } 逐行拆解:FileOpen 用 FILE_CSV|FILE_ANSI 按分隔符打开文件,handle 无效就打印函数名和错误码。while 循环里 FileReadString 每次读一个单元格,column 计数到目标列号才进内层判断;rows>=1 专门跳过第一行表头。每读到一个有效值 counter 自增,ArrayResize 把目标数组拉长一格,再赋值。遇到行尾把 column 归零、rows 加一,最后 FileClose 释放句柄。 实际调用时,Init 读入 titanic.csv 和逗号分隔符,GetDatatoArray(4,Sex) 抽第 4 列(性别),再交给 LabelEncoder 按 "male,female" 映射成整型。ArrayPrint(SexEncoded) 在日志里打出编码结果,肉眼就能核对 0/1 分布。外汇与贵金属数据同样可用这套抽列逻辑,但品种跳空造成的缺失值会让编码偏斜,属高风险场景,结果仅作概率参考。 别把表头当特征喂进去 上面代码用 rows>=1 硬跳首行,若你的 CSV 没表头却套用同样逻辑,第一行真实数据会被静默丢弃。接数据前先确认文件结构,或把阈值改成 rows>=0 并外部标记。

MQL5 / C++
class="type">void CLogisticRegression::GetDatatoArray(class="type">int from_column_number, class="type">class="kw">string &toArr[])
{
  class="type">int handle  = FileOpen(m_filename,FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI,m_delimiter);
  class="type">int counter=class="num">0;
  if (handle == INVALID_HANDLE)
    Print(__FUNCTION__," Invalid csv handle err=",GetLastError());
  else
  {
    class="type">int column = class="num">0, rows=class="num">0;
    class="kw">while (!FileIsEnding(handle))
    {
      class="type">class="kw">string data = FileReadString(handle);
      column++;
      if (column==from_column_number)
      {
        if (rows>=class="num">1)
        {
          counter++;
          ArrayResize(toArr,counter);
          toArr[counter-class="num">1]=data;
        }
      }
      if (FileIsLineEnding(handle))
      {
        rows++;
        column=class="num">0;
      }
    }
  }
  FileClose(handle);
}
class="macro">#include "LogisticRegressionLib.mqh";
CLogisticRegression Logreg;
class="type">void OnStart()
{
  Logreg.Init("titanic.csv",",");
  class="type">class="kw">string Sex[];
  class="type">int SexEncoded[];
  Logreg.GetDatatoArray(class="num">4,Sex);
  Logreg.LabelEncoder(Sex,SexEncoded,"male,female");
  ArrayPrint(SexEncoded);
}
class="type">void CLogisticRegression::FixMissingValues(class="type">class="kw">double &Arr[])
{
  class="type">int counter=class="num">0; class="type">class="kw">double mean=class="num">0, total=class="num">0;
  for (class="type">int i=class="num">0; i<ArraySize(Arr); i++)
  {

用均值回填数组里的零值缺口

处理行情采样数组时,常会遇到某些 Bar 因流动性断层或指标未触发而写入了 0。若直接拿去算波动率或做归一化,0 会拉偏统计,必须先用有效数据的均值补位。 下面这段逻辑先遍历数组,只累加非 0 元素并计数,得到 mean = total / counter,随后 Print 出四舍五入的均值并 ArrayPrint 原始数组,方便你对照填补前后的差异。 第二个循环再次扫一遍数组,凡是 Arr[i]==0 的位置,统一写成 MathRound(mean)。注意这里用四舍五入而非保留小数,若你的价差精度到小数点后三位,应改成归一化乘因子再取整,否则黄金和外汇交叉盘会出现跳变。 在 MT5 里跑完这段,控制台会先打“mean X before Arr”和原数组,再打“After Arr”和补零后的数组。实际验证时建议故意构造一个含 3 个 0、其余为随机点差的数组,看回填后 ArrayPrint 的 0 是否全部消失。外汇与贵金属杠杆高,数组清洗只解决数据完整性,不预示任何方向。

MQL5 / C++
   if (Arr[i]!=class="num">0)
      {
         counter++;
         total += Arr[i];
      }
   }
   mean = total/counter; class=class="str">"cmt">//all the values divided by their total number
   Print("mean ",MathRound(mean)," before Arr");
   ArrayPrint(Arr);
   
   for (class="type">int i=class="num">0; i<ArraySize(Arr); i++)
   {
      if (Arr[i]==class="num">0)
      {
         Arr[i] = MathRound(mean); class=class="str">"cmt">//replace zero values in array
      }
   }
   Print("After Arr");
   ArrayPrint(Arr);
}

常见问题

sigmoid 用 1/(1+e^-z) 把任意实数 z 映射成 0~1 之间的概率值,z 越大越接近 1,越小越接近 0。
把连续预测改成概率输出,再设 0.5 为阈值,大于判一类小于判另一类,就得到了分类边界。
小布可读取你的数据文件并直接生成分布图与基础统计,省去手写抽列和绘图代码的过程。
会报错或把缺失当类别污染模型,应先剔掉无关列、用均值回填缺口再做编码。
用该列均值回填零值缺口最稳妥,能保留整体分布特征且不引入假类别。