Data Smoothing with the Nadaraya-Watson Kernel Technique Using C#

Data smoothing is a classical statistics technique that removes noise from a dataset so that overall patterns and trends are visible. There are many algorithms for smoothing. I put together a demo of data smoothing using the Nadaraya-Watson (NW) kernel smoothing technique, with the C# language.

The output of the demo program is:

Begin Nadaraya-Watson kernel smoothing demo

Loading simple XXy data
Done

First three data X:
  0.1000  0.1000
  0.2000  0.2000
  0.3000  0.3000

First three data y:
  0.0998
  0.3987
  0.0955

Creating NW kernel smoother with RBF sigma = 0.1600
Done

Computing smoothed y values
Done

   X             y        smooth y
-------------------------------------
  0.10  0.10    0.0998    0.2069
  0.20  0.20    0.3987    0.2303
  0.30  0.30    0.0955    0.2474
  0.50  0.50    0.5794    0.5599
  0.60  0.60    0.5646    0.6681
  0.70  0.70    0.9442    0.7357
  0.90  0.90    0.4833    0.5556
  1.00  1.00    0.5415    0.5255

End NW kernel smoothing demo

The source data is:

0.1000, 0.1000, 0.0998
0.2000, 0.2000, 0.3987
0.3000, 0.3000, 0.0955
0.5000, 0.5000, 0.5794
0.6000, 0.6000, 0.5646
0.7000, 0.7000, 0.9442
0.9000, 0.9000, 0.4833
1.0000, 1.0000, 0.5415

There are eight data items. The first two values on each line are the elements of a two-dimensional X vector, such as (0.10, 0.10). The third value is a dependent y value. The data is artificial but you can imagine the X values are inputs to some system and the y values are outputs.

Data smoothing is most often used with small datasets that have a single x value and a single y value. This allows the source data and the smoothed data to be visualized using a graph. However, I used data with two x values, such as (0.10, 0.10), where both values are artificially the same, to demonstrate that NW kernel smoothing can use multidimensional X data with several values.

Understanding the Radial Basis Function

In order to understand NW kernel smoothing, you must understand the radial basis function (RBF) function. The RBF function computes a measure of similarity between two vectors. There are several RBF versions. The version used by the demo is often called the sigma (bandwidth) version:

rbf(v1, v2) = exp(-1 * ||v1 - v2||^2 / (2 * sigma^2))

The v1 and v2 are vectors. The exp() function is Euler’s constant (approximately 2.71828) raised to a power. The ||v1 – v2||^2 is the squared Euclidean distance between vectors v1 and v2. The sigma, also called the bandwidth, is an arbitrary constant.

If the two vectors passed to RBF are the same, then rbf(v1, v2) = 1.0 (maximum similarity). As the two vectors get more and more different, the value of rbf(v1, v2) gets closer and closer to 0. RBF is symmetric so rbf(v1, v2) = rbf(v2, v1).

Suppose v1 = (2.50, -3.25, 1.00) and v2 = (2.0, -3.0, 1.0) and sigma = 1.5, then the squared distance term is:

||v1 - v2||^2 = (2.50 - 2)^2 + (-3.25 - (-3))^2 + (1.00 - 1)^2
              = (0.50)^2 + (-0.25)^2 + (0.00)^2
              = 0.2500 + 0.0625 + 0.0000
              = 0.3125

and the RBF value is:

rbf(v1, v2) = exp( -1 * ||v1 - v2||^2 / (2 * sigma^2) )
            = exp( -0.3125 / (2 * 1.5^2) )
            = exp( -0.3125 / 4.50 )
            = exp(-0.0694)

Computing a NW Kernel Smoothed y Value

Computing a NW smoothed data point is best explained using a concrete example. Suppose you want to compute the smoothed y’ value for x = (0.70, 0.70) in the demo data. The actual y value is 0.9442. The smoothed y’ value is 0.7357. It is computed as follows:

x = (0.70, 0.70) 

x0 = (0.10, 0.10)  w0 = rbf(x, x0) = 0.0000  y0 = 0.0998  w0 * y0 = 0.0000
x1 = (0.20, 0.20)  w1 = rbf(x, x1) = 0.0001  y1 = 0.3987  w1 * y1 = 0.0000
x2 = (0.30, 0.30)  w2 = rbf(x, x2) = 0.0019  y2 = 0.0955  w2 * y2 = 0.0001
x3 = (0.50, 0.50)  w3 = rbf(x, x3) = 0.2096  y3 = 0.5794  w3 * y3 = 0.1214
x4 = (0.60, 0.60)  w4 = rbf(x, x4) = 0.6766  y4 = 0.5646  w4 * y4 = 0.3820
x5 = (0.70, 0.70)  w5 = rbf(x, x5) = 1.0000  y5 = 0.9442  w5 * y5 = 0.9442
x6 = (0.90, 0.90)  w6 = rbf(x, x6) = 0.2096  y6 = 0.4833  w6 * y6 = 0.1013
x7 = (1.00, 1.00)  w7 = rbf(x, x7) = 0.0297  y7 = 0.5415  w7 * y7 = 0.0161
                                    -------                        -------
                       sum weights = 2.1276        sum weighted y = 1.5653

smooth y' = sum weighted y / sum weights
          = 1.5653 / 2.1276
          = 0.7357

There are eight data points. A weight, wi, is computed for each data point, where the weights are the RBF values between the x value and each data points. Each weight is multiplied by associated actual y value, and those products are summed to give the numerator term of the smoothed value. The denominator is the sum of the weight values.



I miss the days when I used to read a daily newspaper to get all my news and information. Here is some old health advice.


Demo program. Replace “lt” (less than), “gt”, “lte”, “gte” with Boolean operator symbols (my blog editor chokes on symbols).

using System;
using System.IO;
using System.Collections.Generic;

namespace KernelNWSmoothing
{
  internal class KernelNWSmoothingProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("\nBegin Nadaraya-Watson" +
        " kernel smoothing demo ");

      Console.WriteLine("\nLoading simple XXy data ");
      string dataFile = "..\\..\\..\\Data\\simple_xxy_8.txt";
      double[][] X = MatLoad(dataFile,
        new int[] { 0, 1 }, ',', "#");
      double[] y = MatToVec(MatLoad(dataFile,
        new int[] { 2 }, ',', "#"));
      Console.WriteLine("Done ");

      Console.WriteLine("\nFirst three data X: ");
      for (int i = 0; i "lt" 3; ++i)
        VecShow(X[i], 4, 8, true);

      Console.WriteLine("\nFirst three data y: ");
      for (int i = 0; i "lt" 3; ++i)
        Console.WriteLine(y[i].ToString("F4").PadLeft(8));

      double sigma = 0.16; // larger sigma  = smoother
      Console.WriteLine("\nCreating NW kernel smoother" +
        " with RBF sigma = " + sigma.ToString("F4"));
      KernelSmoother ks = new KernelSmoother("rbf", sigma);
      Console.WriteLine("Done ");

      Console.WriteLine("\nComputing smoothed y values ");
      ks.Fit(X, y); // copy dataX and dataY
      Console.WriteLine("Done ");

      Console.WriteLine("\n   X         y        smooth y");
      Console.WriteLine("--------------------------------");
      for (int i = 0; i "lt" X.Length; ++i)
      {
        double[] x = X[i];
        double actY = y[i];
        double smoothY = ks.SmoothedY(x);
        VecShow(x, 2, 6, false);
        Console.Write("    " + actY.ToString("F4"));
        Console.WriteLine("    " + smoothY.ToString("F4"));
      }

      Console.WriteLine("\nEnd NW kernel smoothing demo ");
      Console.ReadLine();
    } // Main

    // ------------------------------------------------------
    // helpers for Main(): MatLoad(), MatToVec(), VecShow()
    // ------------------------------------------------------

    static double[][] MatLoad(string fn, int[] usecols,
      char sep, string comment)
    {
      List"lt"double[]"gt" result =
        new List"lt"double[]"gt"();
      string line = "";
      FileStream ifs = new FileStream(fn, FileMode.Open);
      StreamReader sr = new StreamReader(ifs);
      while ((line = sr.ReadLine()) != null)
      {
        if (line.StartsWith(comment) == true)
          continue;
        string[] tokens = line.Split(sep);
        List"lt"double"gt" lst = new List"lt"double"gt"();
        for (int j = 0; j "lt" usecols.Length; ++j)
          lst.Add(double.Parse(tokens[usecols[j]]));
        double[] row = lst.ToArray();
        result.Add(row);
      }
      sr.Close(); ifs.Close();
      return result.ToArray();
    }

    static double[] MatToVec(double[][] M)
    {
      int nRows = M.Length;
      int nCols = M[0].Length;
      double[] result = new double[nRows * nCols];
      int k = 0;
      for (int i = 0; i "lt" nRows; ++i)
        for (int j = 0; j "lt" nCols; ++j)
          result[k++] = M[i][j];
      return result;
    }

    static void VecShow(double[] vec, int dec, int wid,
      bool newLine)
    {
      for (int i = 0; i "lt" vec.Length; ++i)
        Console.Write(vec[i].ToString("F" + dec).
          PadLeft(wid));
      if (newLine == true)
        Console.WriteLine("");
    }

  } // class Program

  // ========================================================

  public class KernelSmoother
  {
    public string kernel; // 
    public double sigma;  // RBF sigma, aka bandwidth
    public double[][] dataX;
    public double[] dataY;

    public KernelSmoother(string kernel, double sigma)
    {
      if (kernel != "rbf")
        Console.WriteLine("Only rbf kernel supported ");
      this.kernel = kernel; // dummy not used this version
      this.sigma = sigma;
    }

    public void Fit(double[][] dataX, double[] dataY)
    {
      this.dataX = dataX; // copy by reference
      this.dataY = dataY;
    }

    public double SmoothedY(double[] x)
    {
      int n = this.dataX.Length;
      double numer = 0.0; // sum of weighted y values
      double denom = 0.0; // sum of weights (RBF values)
      for (int i = 0; i "lt" n; ++i)
      {
        double currRbf = RbfSigma(x, this.dataX[i],
          this.sigma);
        numer += currRbf * this.dataY[i];
        denom += currRbf;
      }
      return numer / denom; // assume no div by 0
    }

    private static double RbfSigma(double[] x1, double[] x2,
      double sigma)
    {
      // rbf(x1,x2) = exp(-1 * ||x1-x2||^2 / (2 * sigma^2))
      int dim = x1.Length;
      double sum = 0.0;
      for (int i = 0; i "lt" dim; ++i)
        sum += (x1[i] - x2[i]) * (x1[i] - x2[i]);
      return Math.Exp(-1 * sum / (2 * sigma * sigma));
    }

    private static double RbfGamma(double[] x1, double[] x2,
      double gamma)
    {
      // rbf(x1,x2) = exp(-1 * gamma * ||x1-x2||^2
      int dim = x1.Length;
      double sum = 0.0;
      for (int i = 0; i "lt" dim; ++i)
        sum += (x1[i] - x2[i]) * (x1[i] - x2[i]);
      return Math.Exp(-1 * gamma * sum);
    }

  } // class KernelSmoother

} // ns

Demo data:

# simple_xxy_8.txt
#
0.1000, 0.1000, 0.0998
0.2000, 0.2000, 0.3987
0.3000, 0.3000, 0.0955
0.5000, 0.5000, 0.5794
0.6000, 0.6000, 0.5646
0.7000, 0.7000, 0.9442
0.9000, 0.9000, 0.4833
1.0000, 1.0000, 0.5415
This entry was posted in Machine Learning. Bookmark the permalink.

Leave a Reply