File: Common\Formulas\TimeSeriesAndForecasting.cs
Project: ndp\fx\src\DataVisualization\System.Windows.Forms.DataVisualization.csproj (System.Windows.Forms.DataVisualization)
//-------------------------------------------------------------
// <copyright company=’Microsoft Corporation’>
//   Copyright © Microsoft Corporation. All Rights Reserved.
// </copyright>
//-------------------------------------------------------------
// @owner=alexgor, deliant
//=================================================================
//  File:		TimeSeriesAndForecasting.cs
//
//  Namespace:	System.Web.UI.WebControls[Windows.Forms].Charting.Formulas
//
//	Classes:	TimeSeriesAndForecasting
//
//  Purpose:	This class is used for calculations of 
//				time series and forecasting
//
//	Reviewed:	GS - August 7, 2002
//				AG - August 7, 2002
//
//===================================================================
 
using System;
using System.Globalization;
 
#if Microsoft_CONTROL
	namespace System.Windows.Forms.DataVisualization.Charting.Formulas
#else
	namespace System.Web.UI.DataVisualization.Charting.Formulas
#endif
{
	/// <summary>
	/// This class is used for calculations of 
	/// time series and forecasting
	/// </summary>
	internal class TimeSeriesAndForecasting : IFormula
	{
		#region Enumeration
 
		/// <summary>
		/// AxisName of regression
		/// </summary>
		internal enum RegressionType
		{
			/// <summary>
			/// Polynomial trend
			/// </summary>
			Polynomial,
 
			/// <summary>
			/// IsLogarithmic trend
			/// </summary>
			Logarithmic,
 
			/// <summary>
			/// Power trend
			/// </summary>
			Power,
 
			/// <summary>
			/// Exponential trend
			/// </summary>
			Exponential
		}
 
		#endregion
 
		#region Properties
 
		/// <summary>
		/// Formula Module name
		/// </summary>
        virtual public string Name { get { return SR.FormulaNameTimeSeriesAndForecasting; } }
 
		#endregion
 
		#region Methods
		
		/// <summary>
		/// Public constructor.
		/// </summary>
		public TimeSeriesAndForecasting()
		{
			
		}
 
        /// <summary>
        /// The first method in the module, which converts a formula 
        /// name to the corresponding private method.
        /// </summary>
        /// <param name="formulaName">String which represent a formula name</param>
        /// <param name="inputValues">Arrays of doubles - Input values</param>
        /// <param name="outputValues">Arrays of doubles - Output values</param>
        /// <param name="parameterList">Array of strings - Formula parameters</param>
        /// <param name="extraParameterList">Array of strings - Extra Formula parameters from DataManipulator object</param>
        /// <param name="outLabels">Array of strings - Used for Labels. Description for output results.</param>
		virtual public void Formula( string formulaName, double [][] inputValues, out double [][] outputValues, string [] parameterList, string [] extraParameterList, out string [][] outLabels )
		{
			string name;
 
			name = formulaName.ToUpper(System.Globalization.CultureInfo.InvariantCulture);
 
			// Not used for these formulas.
			outLabels = null;
 
			try
			{
				switch( name )
				{
					case "FORECASTING":
						Forecasting( inputValues, out outputValues, parameterList );
						break;
					default:
						outputValues = null; 
						break;
				}
			}
			catch( IndexOutOfRangeException )
			{
				throw new InvalidOperationException( SR.ExceptionFormulaInvalidPeriod(name) );
			}
			catch( OverflowException )
			{
				throw new InvalidOperationException( SR.ExceptionFormulaNotEnoughDataPoints( name ) );
			}
		}
 
		#endregion
		
		#region Formulas
 
        /// <summary>
        /// Forecasting formula predicts future values of the time series variable. 
        /// Multiple regressions are used for this forecasting model. Any method 
        /// of fitting equations to data may be called regression. Such equations 
        /// are valuable for at least two purposes: making predictions and judging 
        /// the strength of relationships. Of the various methods of performing 
        /// regression, Last Square is the most widely used. This formula returns 
        /// two more series, which represents upper and lower bond of error. Error 
        /// is based on standard deviation and represents a linear combination of 
        /// approximation error and forecasting error.
        /// ---------------------------------------------------------
        /// Input: 
        /// 	- Y values.
        /// Output: 
        /// 	- Forecasting
        /// 	- upper bond error
        /// 	- lower bond error
        /// Parameters: 
        /// 	- Polynomial degree (Default: 2 - Linear regression )
        /// 	- Forecasting period (Default: Half of the series length )
        /// 	- Returns Approximation error (Default: true)
        /// 	- Returns Forecasting error (Default: true)
        /// </summary>
        /// <param name="inputValues">Arrays of doubles - Input values</param>
        /// <param name="outputValues">Arrays of doubles - Output values</param>
        /// <param name="parameterList">Array of strings - Parameters</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        private void Forecasting(double[][] inputValues, out double[][] outputValues, string[] parameterList)
        {
            // Polynomial degree
            int degree;
            RegressionType regressionType = RegressionType.Polynomial;
 
            if (String.Equals(parameterList[0],"Exponential", StringComparison.OrdinalIgnoreCase))
            {
                regressionType = RegressionType.Exponential;
                degree = 2;
            }
            else if (String.Equals(parameterList[0],"Linear", StringComparison.OrdinalIgnoreCase))
            {
                regressionType = RegressionType.Polynomial;
                degree = 2;
            }
            else if (String.Equals(parameterList[0],"IsLogarithmic", StringComparison.OrdinalIgnoreCase) || 
                     String.Equals(parameterList[0],"Logarithmic",   StringComparison.OrdinalIgnoreCase))
            {
                regressionType = RegressionType.Logarithmic;
                degree = 2;
            }
            else if (String.Equals(parameterList[0],"Power", StringComparison.OrdinalIgnoreCase))
            {
                regressionType = RegressionType.Power;
                degree = 2;
            }
            else
            {
                if (parameterList.Length < 1 || 
                    !int.TryParse(parameterList[0], NumberStyles.Any, CultureInfo.InvariantCulture, out degree))
                {
                    degree = 2;
                }
            }
 
 
            if (degree > 5 || degree < 1)
                throw new InvalidOperationException(SR.ExceptionForecastingDegreeInvalid);
 
            if (degree > inputValues[0].Length)
                throw new InvalidOperationException(SR.ExceptionForecastingNotEnoughDataPoints(degree.ToString(System.Globalization.CultureInfo.InvariantCulture)));
 
            // Forecasting period
            int period;
            if (parameterList.Length < 2 || 
                !int.TryParse(parameterList[1], NumberStyles.Any, CultureInfo.InvariantCulture, out period))
            {
                period = inputValues[0].Length / 2;
            }
 
            // Approximation error
            bool approximationError;
            if (parameterList.Length < 3 || 
                !bool.TryParse(parameterList[2], out approximationError))
            {
                approximationError = true;
            }
 
            // Forecasting error
            bool forecastingError;
            if (parameterList.Length < 4 || 
                !bool.TryParse(parameterList[3], out forecastingError))
            {
                forecastingError = true;
            }
 
            double[][] tempOut;
            // Find regresion
            Regression(regressionType, inputValues, out tempOut, degree, period);
 
            // If error disabled get out from procedure
            if (!forecastingError && !approximationError)
            {
                outputValues = tempOut;
                return;
            }
 
            double[][] inputErrorEst = new double[2][];
            double[][] outputErrorEst;
            inputErrorEst[0] = new double[inputValues[0].Length / 2];
            inputErrorEst[1] = new double[inputValues[0].Length / 2];
 
            for (int index = 0; index < inputValues[0].Length / 2; index++)
            {
                inputErrorEst[0][index] = inputValues[0][index];
                inputErrorEst[1][index] = inputValues[1][index];
            }
 
            Regression(regressionType, inputErrorEst, out outputErrorEst, degree, inputValues[0].Length / 2);
 
            // Find the average for forecasting error
            double error = 0;
            for (int index = inputValues[0].Length / 2; index < outputErrorEst[1].Length; index++)
            {
                error += (outputErrorEst[1][index] - inputValues[1][index]) * (outputErrorEst[1][index] - inputValues[1][index]);
            }
            error /= inputValues[0].Length - inputValues[0].Length / 2;
            error = Math.Sqrt(error);
            error /= (inputValues[0].Length / 4);
 
            // Find the standard deviation
            double dev = 0;
            for (int index = 0; index < inputValues[0].Length; index++)
            {
                dev += (tempOut[1][index] - inputValues[1][index]) * (tempOut[1][index] - inputValues[1][index]);
            }
            dev /= inputValues[0].Length;
            dev = Math.Sqrt(dev);
 
            outputValues = new double[4][];
            outputValues[0] = tempOut[0];
            outputValues[1] = tempOut[1];
            outputValues[2] = new double[tempOut[0].Length];
            outputValues[3] = new double[tempOut[0].Length];
 
            if (!approximationError)
                dev = 0;
 
            if (!forecastingError)
                error = 0;
 
            for (int index = 0; index < inputValues[0].Length; index++)
            {
                outputValues[2][index] = tempOut[1][index] + 2 * dev;
                outputValues[3][index] = tempOut[1][index] - 2 * dev;
            }
            double sumError = 0;
            for (int index = inputValues[0].Length; index < tempOut[0].Length; index++)
            {
                sumError += error;
                outputValues[2][index] = tempOut[1][index] + sumError + 2 * dev;
                outputValues[3][index] = tempOut[1][index] - sumError - 2 * dev;
            }
        }
 
        /// <summary>
        /// Any method of fitting equations to data may be called regression. 
        /// Such equations are valuable for at least two purposes: making 
        /// predictions and judging the strength of relationships. Of the 
        /// various methods of performing regression, Last Square is the 
        /// most widely used. 
        /// </summary>
        /// <param name="regressionType">AxisName of regression Polynomial, exponential, etc.</param>
        /// <param name="inputValues">Arrays of doubles - Input values</param>
        /// <param name="outputValues">Arrays of doubles - Output values</param>
        /// <param name="polynomialDegree">Polynomial degree (Default: 2 - Linear regression )</param>
        /// <param name="forecastingPeriod">Forecasting period (Default: Half of the series length )</param>
		private void Regression( RegressionType regressionType, double [][] inputValues, out double [][] outputValues, int polynomialDegree, int forecastingPeriod )
		{
			if( regressionType == RegressionType.Exponential )
			{
				double [] oldYValues = new double[ inputValues[1].Length ];
				for( int index = 0; index < inputValues[1].Length; index++ )
				{
					oldYValues[ index ] = inputValues[1][index];
					if( inputValues[1][index] <= 0 )
					{
                        throw new InvalidOperationException(SR.ExceptionForecastingExponentialRegressionHasZeroYValues);
					}
					inputValues[1][index] = Math.Log( inputValues[1][index] );
				}
 
				
 
				PolynomialRegression( regressionType, inputValues, out outputValues, 2, forecastingPeriod, 0 );
 
				inputValues[1] = oldYValues;
			}
			else if( regressionType == RegressionType.Logarithmic )
			{
				double interval;
				double first = inputValues[0][0];
 
				// Find Interval for X values
				interval = Math.Abs( inputValues[0][0] - inputValues[0][inputValues[0].Length - 1] ) / ( inputValues[0].Length - 1 );
			
				if( interval <= 0 )
					interval = 1;
 
				for( int index = 0; index < inputValues[0].Length; index++ )
				{
					inputValues[0][index] = Math.Log( inputValues[0][index] );
				}
 
				PolynomialRegression( regressionType, inputValues, out outputValues, 2, forecastingPeriod, interval );
 
				// Create Y values based on approximation.
				for( int i = 0; i < outputValues[0].Length; i++ )
				{
					// Set X value
					outputValues[0][i] = first + i * interval;
				}
			}
			else if( regressionType == RegressionType.Power )
			{
				double [] oldYValues = new double[ inputValues[1].Length ];
				double interval;
				double first = inputValues[0][0];
 
				for( int index = 0; index < inputValues[1].Length; index++ )
				{
					oldYValues[ index ] = inputValues[1][index];
					if( inputValues[1][index] <= 0 )
					{
                        throw new InvalidOperationException(SR.ExceptionForecastingPowerRegressionHasZeroYValues);
					}
				}
 
				// Find Interval for X values
				interval = Math.Abs( inputValues[0][0] - inputValues[0][inputValues[0].Length - 1] ) / ( inputValues[0].Length - 1 );
			
				if( interval <= 0 )
					interval = 1;
 
				PolynomialRegression( regressionType, inputValues, out outputValues, 2, forecastingPeriod, interval );
 
				inputValues[1] = oldYValues;
 
				// Create Y values based on approximation.
				for( int i = 0; i < outputValues[0].Length; i++ )
				{
					// Set X value
					outputValues[0][i] = first + i * interval;
				}
			}
			else
			{
				PolynomialRegression( regressionType, inputValues, out outputValues, polynomialDegree, forecastingPeriod, 0 );
			}
		}
 
        /// <summary>
        /// Any method of fitting equations to data may be called regression. 
        /// Such equations are valuable for at least two purposes: making 
        /// predictions and judging the strength of relationships. Of the 
        /// various methods of performing regression, Last Square is the 
        /// most widely used. 
        /// </summary>
        /// <param name="regressionType">AxisName of regression Polynomial, exponential, etc.</param>
        /// <param name="inputValues">Arrays of doubles - Input values</param>
        /// <param name="outputValues">Arrays of doubles - Output values</param>
        /// <param name="polynomialDegree">Polynomial degree (Default: 2 - Linear regression )</param>
        /// <param name="forecastingPeriod">Forecasting period (Default: Half of the series length )</param>
        /// <param name="logInterval">Interval for logarithmic scale</param>
		private void PolynomialRegression( RegressionType regressionType, double [][] inputValues, out double [][] outputValues, int polynomialDegree, int forecastingPeriod, double logInterval )
		{
			double [] coefficients = new double [polynomialDegree];
			int size = inputValues[0].Length;
			double minimumX = double.MaxValue;
			double interval = 1.0;
 
			// Find Interval for X values
			interval = Math.Abs( inputValues[0][0] - inputValues[0][inputValues[0].Length - 1] ) / ( inputValues[0].Length - 1 );
			
			if( interval <= 0 )
				interval = 1;
 
			if( regressionType != RegressionType.Logarithmic )
			{
				// Avoid Rounding error because of big X values.
				// Find Minimum X value 
				for( int xIndex = 0; xIndex < inputValues[0].Length; xIndex++ )
				{
					if( minimumX > inputValues[0][xIndex] )
						minimumX = inputValues[0][xIndex];
				}
 
				// Change X values.
				for( int xIndex = 0; xIndex < inputValues[0].Length; xIndex++ )
				{
					inputValues[0][xIndex] -= minimumX - 1;
				}
			}
 
			if( regressionType == RegressionType.Power )
			{
				for( int index = 0; index < inputValues[0].Length; index++ )
				{
					inputValues[0][index] = Math.Log( inputValues[0][index] );
					inputValues[1][index] = Math.Log( inputValues[1][index] );
				}
			}
 
			double [][] mainDeterminant = new double [polynomialDegree][];
			for(int arrayIndex = 0;  arrayIndex < polynomialDegree; arrayIndex++)
			{
				mainDeterminant[arrayIndex] = new double [polynomialDegree];
			}
 
			// Main determinant
			for( int k = 0; k < polynomialDegree; k++ )
			{
				for( int i = 0; i < polynomialDegree; i++ )
				{
					mainDeterminant[i][k] = 0;
					for( int j = 0; j < inputValues[0].Length; j++ )
					{
						mainDeterminant[i][k] += (double)Math.Pow( inputValues[0][j], (i+k) );
					}
				}
			}
			double mainValue = Determinant(mainDeterminant);
			
			// Coefficient determinant
			for( int i = 0; i < polynomialDegree; i++ )
			{
				double [][] coeffDeterminant = CopyDeterminant(mainDeterminant);
				for( int k = 0; k < polynomialDegree; k++ )
				{
					coeffDeterminant[i][k] = 0;
					for( int j = 0; j < inputValues[0].Length; j++ )
					{
						coeffDeterminant[i][k] += (double)inputValues[1][j] * (double)Math.Pow( inputValues[0][j], k );
					}
				}
				coefficients[i] = Determinant(coeffDeterminant) / mainValue;
			}
 
			// Create output arrays for approximation and forecasting
			outputValues = new double[2][];
			outputValues[0] = new double[size + forecastingPeriod];
			outputValues[1] = new double[size + forecastingPeriod];
 
			if( regressionType == RegressionType.Polynomial )
			{
				// Create Y values based on approximation.
				for( int i = 0; i < size + forecastingPeriod; i++ )
				{
					// Set X value
					outputValues[0][i] = inputValues[0][0] + i * interval;
 
					outputValues[1][i] = 0;
					for( int j = 0; j < polynomialDegree; j++ )
					{
						outputValues[1][i]+= (double)coefficients[j]*Math.Pow(outputValues[0][i],j);
					}
				}
			}
			else if( regressionType == RegressionType.Exponential )
			{
				// Create Y values based on approximation.
				for( int i = 0; i < size + forecastingPeriod; i++ )
				{
					// Set X value
					outputValues[0][i] = inputValues[0][0] + i * interval;
 
					outputValues[1][i]= Math.Exp( coefficients[0] ) * Math.Exp( coefficients[1] * outputValues[0][i] );
				}
			}
			else if( regressionType == RegressionType.Logarithmic )
			{
				// Create Y values based on approximation.
				for( int i = 0; i < size + forecastingPeriod; i++ )
				{
					// Set X value
					outputValues[0][i] = Math.Exp( inputValues[0][0] ) + i * logInterval;
					
					outputValues[1][i]= coefficients[1] * Math.Log( outputValues[0][i] ) + coefficients[0];
				}
			}
			else if( regressionType == RegressionType.Power )
			{
				// Create Y values based on approximation.
				for( int i = 0; i < size + forecastingPeriod; i++ )
				{
					// Set X value
					outputValues[0][i] = Math.Exp( inputValues[0][0] ) + i * logInterval;
					
					outputValues[1][i]= Math.Exp( coefficients[0] ) * Math.Pow( outputValues[0][i], coefficients[1] );
				}
			}
 
			if( regressionType != RegressionType.Logarithmic )
			{
				// Return X values.
				for( int xIndex = 0; xIndex < size + forecastingPeriod; xIndex++ )
				{
					outputValues[0][xIndex] += minimumX - 1;
				}
			}
		}
 
		/// <summary>
		/// This method recalculates determinant. This method is used for 
		/// recursive calls for sub determinants too.
		/// </summary>
		/// <param name="inputDeterminant">Input determinant</param>
		/// <returns>Result of determinant</returns>
		private double Determinant( double [][] inputDeterminant )
		{
			double sum = 0;
			double sign = 1.0;
 
			// Determinant is 2X2 - calculate value
			if( inputDeterminant.Length == 2 )
			{
				return inputDeterminant[0][0]*inputDeterminant[1][1] - inputDeterminant[0][1]*inputDeterminant[1][0];
			}
 
			// Determinant is biger than 2X2. Go to recursive 
			// call of Determinant method
			for( int column = 0; column < inputDeterminant.GetLength(0); column++ )
			{
				// Make sub determinant
				double [][] newDeterminant = MakeSubDeterminant( inputDeterminant, column );
 
				sum += sign * Determinant( newDeterminant ) * inputDeterminant[column][0];
				sign *= -1.0;
			}
			return sum;
		}
 
		/// <summary>
		/// This method will create a new determinant, which is 
		/// smaller by one rank (dimension). Specified column 
		/// and zero rows will be skipped.
		/// </summary>
		/// <param name="inputDeterminant">Input determinant</param>
		/// <param name="columnPos">Position of column, which has to be skipped</param>
		/// <returns>New determinant</returns>
		private double [][] MakeSubDeterminant( double [][] inputDeterminant, int columnPos )
		{
			// Get Determinant Size
			int size = inputDeterminant.GetLength(0);
 
			// Prepare sub Determinant
			double [][] newDeterminant = new double [size - 1][];
			for(int arrayIndex = 0;  arrayIndex < size - 1; arrayIndex++)
			{
				newDeterminant[arrayIndex] = new double [size - 1];
			}
 
 
			int newColumn = 0;
			// Copy columns
			for( int column = 0; column < size; column++ )
			{
				// Skeep this column
				if( column == columnPos )
					continue;
 
				// Copy rows
				for( int  row = 1; row < size; row++ )
				{
					newDeterminant[newColumn][row-1] = inputDeterminant[column][row];
				}
 
				// Go to new column for new determinant
				newColumn++;
			}
 
			// Return new determinant
			return newDeterminant;
		}
 
		/// <summary>
		/// This method will copy determinant
		/// </summary>
		/// <param name="inputDeterminant">Input determinant</param>
		/// <returns>New determinant</returns>
		private double [][] CopyDeterminant( double [][] inputDeterminant )
		{
			// Get Determinant Size
			int size = inputDeterminant.GetLength(0);
 
			// Prepare sub Determinant
			double [][] newDeterminant = new double [size][];
			for(int arrayIndex = 0;  arrayIndex < size; arrayIndex++)
			{
				newDeterminant[arrayIndex] = new double [size];
			}
 
			// Copy columns
			for( int column = 0; column < size; column++ )
			{
				// Copy rows
				for( int  row = 0; row < size; row++ )
				{
					newDeterminant[column][row] = inputDeterminant[column][row];
				}
			}
 
			// Return new determinant
			return newDeterminant;
		}
 
		#endregion
	}
}