Unity中画2D图表(5)——给定一系列散点,拟合出一条曲线

一、散点数据、拟合直线、拟合曲线

  • 蓝色圆点是数据样本
  • 直线为拟合的直线
  • 曲线是拟合出来的曲线
    在这里插入图片描述

二、C#中曲线拟合的实现

  • 0、曲线拟合的一般步骤(以平面坐标XY为例)
    【1】给定计算拟合的阶数k,k的取值最大为【样本个数-1】
    【2】计算出拟合参数列表
    【3】给定一个x值,用拟合参数计算对应的y值
    【4】计算所有的y值

注意:拟合阶数是一个参数变量,根据值的不同,存在欠拟合和过拟合的情况

1、使用到的包——MathNet

using MathNet.Numerics;

2、曲线拟合的函数

Fit.Polynomial(X, Y, k)

X和Y是一个同型的double数组
k是阶数,为int类型

3、一个简单地测试例子

double[] X = Enumerable.Range(1, 7).Select(r => (double)r).ToArray();
double[] Y = new double[] { 1, 3, 90, 150, 7, 6, 4 };
double[] parameters = Fit.Polynomial(X, Y, 5);  //此处拟合阶数为5(阶数能取的最大值为:【样本个数 - 1】 = 6),此时返回的参数列表长度为【阶数+1】= 6
  • 此处拟合阶数为5(阶数能取的最大值为:【样本个数 - 1】 = 6)
  • 参数列表长度为【阶数+1】= 6

4、如何通过给定的x坐标值和拟合参数,计算y值

以上图为例,参数组合parameters计算出来为:[913.28 , -1786.49 , 1156.04 , -317.26 , 38.92 , -1.76]
简要记录为[p0,p1,p2,p3,p4,p5]

给定一个x值,要计算对应的y值,公式为
y = f(x) = p0 * x^0 + p1 * x^1 + p2 * x^2 + … + p5 * x^5

/// <summary>
/// 根据拟合参数和给定的x值,计算对应的y值
  /// y = f(x) = p0 * x^0 + p1 * x^1 + p2 * x^2 + .... + pk * x^k
  /// </summary>
  /// <param name="X">x的坐标系列</param>
  /// <param name="paras">拟合参数数组</param>
  /// <returns>拟合出来的Y系列值</returns>
  public double[] getNewY(double[]X,double[]paras)
  {
      var newY = X.Select(x =>
              paras
                  .Select((p, index) => p * math.pow(x, index))  //计算多项式的每一项
                  .Sum()                                                     //所有项求和  
      ).ToArray();

      return newY;
  }

5、原有样本太少,中间插入更多的估值点

  • 【1】生成更密的x系列
 /// <summary>
    /// 在x1和x2之间插值,步进为step,生成一个新的系列[x1...x2]
    /// </summary>
    /// <param name="x1">起始值</param>
    /// <param name="x2">结束值</param>
    /// <param name="step">步进值</param>
    /// <returns>返回的系列</returns>
    public double[] genNewX(double x1, double x2, double step)
    {
        List<double> newX = new();
        newX.Add(x1);

        double currentX = x1;
        while (currentX <= x2)
        {
            currentX += step;
            newX.Add(currentX);
        }

        if (!newX.Contains(x2)) newX.Add(x2);

        return newX.ToArray();
    }
  • 【2】计算x新列对应的y系列值
double[] parameters = Fit.Polynomial(X.ToArray(), Y.ToArray(), 5);

double[] newX = genNewX(X.Min(), X.Max(), 0.1f);
Debug.Log("生成新的X坐标完成");

//计算新的拟合值
var newY = getNewY(newX, parameters).ToList();
Debug.Log("生成新的Y坐标完成");

三、代码清单

using MathNet.Numerics;
using System;
using System.Collections.Generic;
using UnityEngine;
using XCharts.Runtime;
using System.Linq;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;

/// <summary>
/// 曲线拟合
/// </summary>
public class FitPolynomial : MonoBehaviour
{
    public GameObject charRoot;
    public List<double> X = new List<double>();
    public List<double> Y = new List<double>();

    // Start is called before the first frame update

#if UNITY_EDITOR
    [ContextMenu("曲线拟合")]
#endif
    void FitPolynomialTest()
    {
        /*
         * 多项式拟合如何计算?
         */
        double[] X = Enumerable.Range(1, 7).Select(r => (double)r).ToArray();
        double[] Y = new double[] { 1, 3, 90, 150, 7, 6, 4 };
        double[] parameters = Fit.Polynomial(X, Y, 5);  //阶数最大为【样本个数 - 1】

        Debug.Log($"参数数组的大小为:{parameters.Length}");

        //拟合参数的数组:数组大小为【阶数 + 1】,阶数最大只能为【样本个数 - 1】
        parameters.ToList().ForEach(x => Debug.Log(x));  //显示参数

        //计算结果值
        List<double> result = new List<double>();
        for (int i = 1; i < 8; i++)
        {
            result.Add(parameters[0] * math.pow(i, 0) + parameters[1] * math.pow(i, 1) + parameters[2] * math.pow(i, 2) + parameters[3] * math.pow(i, 3) + parameters[4] * math.pow(i, 4) + parameters[5] * math.pow(i, 5));
        }

        List<double> result2 = getNewY(X, parameters).ToList();

        //Debug.Log($"Pow(2,0) = {math.pow(2,0)}  \n  Pow(2,1) = {math.pow(2, 1)}  \n  Pow(2,2) = {math.pow(2, 2)} ");
        for (int i = 0; i < X.Length; i++)
        {
            Debug.Log($"{X[i]}    {Y[i]}   {result[i]}   {result2[i]}");
        }
    }

    /// <summary>
    /// 根据拟合参数和给定的x值,计算对应的y值
    /// y = f(x) = p0 * x^0 + p1 * x^1 + p2 * x^2 + .... + pk * x^k
    /// </summary>
    /// <param name="X">x的坐标系列</param>
    /// <param name="paras">拟合参数数组</param>
    /// <returns>拟合出来的Y系列值</returns>
    public double[] getNewY(double[]X,double[]paras)
    {
        var newY = X.Select(x =>
                paras
                    .Select((p, index) => p * math.pow(x, index))  //计算多项式的每一项
                    .Sum()                                                     //所有项求和  
        ).ToArray();

        return newY;
    }

    /// <summary>
    /// 在x1和x2之间插值,步进为step,生成一个新的系列[x1...x2]
    /// </summary>
    /// <param name="x1">起始值</param>
    /// <param name="x2">结束值</param>
    /// <param name="step">步进值</param>
    /// <returns>返回的系列</returns>
    public double[] genNewX(double x1, double x2, double step)
    {
        List<double> newX = new();
        newX.Add(x1);

        double currentX = x1;
        while (currentX <= x2)
        {
            currentX += step;
            newX.Add(currentX);
        }

        if (!newX.Contains(x2)) newX.Add(x2);

        return newX.ToArray();
    }

#if UNITY_EDITOR
    [ContextMenu("画散点图并拟合一条直线")]
#endif
    void DrawLine()
    {
        Flow();
    }

    private async UniTask Flow()
    {
        //【1】=============画散点图=============
        var scatter = charRoot.AddComponent<ScatterChart>();
        scatter.Init();//初始化极为重要,不然在running状态会报空

        var title = scatter.GetOrAddChartComponent<Title>();
        title.text = "用给定的散点拟合出一条直线和一条曲线";         //主标题

        await UniTask.Delay(TimeSpan.FromSeconds(2.0f));
        scatter.SetSize(1024, 768);
        scatter.AddSerie<Serie>("数据分布图");
        Debug.Log($"数据长度为:{scatter.series[0].data.Count}"); //默认自带10个数据,需要删除
        scatter.series[0].data.Clear();

        await UniTask.Delay(TimeSpan.FromSeconds(2.0f));
        var X = new List<double> { 1, 2, 3, 4, 5, 6,7,8 ,9,10,11,12,13};
        var Y = new List<double> { -20, -8, 6, 48,200,300,500, 800,500,100, 20,2,0 };

        var test = X.Zip(Y, (x, y) => new List<double> { x, y }).ToList();

        X.Zip(Y, (x, y) => new List<double> { x, y }).ToList().ForEach(t =>
        {
            //Debug.Log($"添加数据:{t[0]} => {t[1]}");
            scatter.AddData(0, t);
        });
        scatter.series[0].symbol.size = 9;//设置大小

        //【2】=============直线拟合计算=============
        await UniTask.Delay(TimeSpan.FromSeconds(2.0f));
        var s = Fit.Line(X.ToArray(), Y.ToArray());
        double b = s.Item1; //截距
        double k = s.Item2; //斜率

        //【3】=============画拟合出来的直线=============
        var line1 = scatter.AddChartComponent<MarkLine>(); //添加一根直线
        line1.data.Clear();//清空默认值,添加组的时候,会默认包含一个item

        //增加一个端点数据
        await UniTask.Delay(TimeSpan.FromSeconds(2.0f));
        var p1 = new MarkLineData();
        p1.type = MarkLineType.None;
        p1.group = 1;
        p1.xPosition = 0;
        p1.yPosition = 0;
        p1.xValue = X.Min();
        p1.yValue = X.Min() * k + b;
        p1.zeroPosition = false;

        //标注【直线方程式】
        await UniTask.Delay(TimeSpan.FromSeconds(2.0f));
        var op = b > 0 ? "+" : "-";

        var kStr = Math.Round(k, 2, MidpointRounding.AwayFromZero).ToString();
        var bStr = Math.Round(math.abs(b), 2, MidpointRounding.AwayFromZero).ToString();
        p1.name = $"y = {kStr} * x {op}{bStr}";
        p1.label.formatter = "{b}";

        //直线的形状设置
        p1.lineStyle.type = LineStyle.Type.Solid;
        p1.startSymbol.type = SymbolType.None;
        p1.endSymbol.type = SymbolType.None;

        //添加第二个端点数据
        var p2 = new MarkLineData();
        p2.type = MarkLineType.None;
        p2.group = 1;
        p2.xPosition = 0;
        p2.yPosition = 0;
        p2.xValue = X.Max();
        p2.yValue = X.Max() * k + b;
        p2.zeroPosition = false;
        p2.name = $"y = {kStr} * x {op}{bStr}"; ;//"y = 0 * x + 15";
        p2.label.formatter = "{b}";

        Debug.Log($"y = {kStr} * x {op}{bStr}");

        //直线的形状设置
        p2.lineStyle.type = LineStyle.Type.Solid;
        p2.startSymbol.type = SymbolType.None;
        p2.endSymbol.type = SymbolType.None;

        //端点数据加入直线中
        line1.data.Add(p1);
        line1.data.Add(p2);

        //【4】曲线拟合:拟合参数的计算
        double[] parameters = Fit.Polynomial(X.ToArray(), Y.ToArray(), 5);

        double[] newX = genNewX(X.Min(), X.Max(), 0.1f);
        Debug.Log("生成新的X坐标完成");

        //计算新的拟合值
        var newY = getNewY(newX, parameters).ToList();
        Debug.Log("生成新的Y坐标完成");

        //画拟合曲线
        var serie2 = scatter.AddSerie<Line>("拟合曲线");
        newX.Zip(newY, (x, y) => new List<double> { x, y }).ToList().ForEach(t =>
        {
            scatter.AddData(1, t);
        });

        scatter.RefreshAllComponent();
    }
}

在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值