我需要有一个简单的线性趋势计算的X值无论是作为双
或日期时间
。因此,基本上为单个项目的Y值始终是一个双
但X的类型而变化。除此之外,我需要从值计算的基本统计信息。这些措施包括:
- 坡
- Y轴截距
- 相关系数
- R平方值
用于计算公式
既然要求是做计算以类似的方式,因为它会在Excel中完成的,我用同样的变化对公式为Excel使用。这也使得它的简单检查计算的正确性。因此,公式为:
线
哪里
-
m
is slope -
x
is the horizontal axis value -
b
is the Y-intercept
斜率计算
哪里
-
x
andy
are individual values -
accented x
andy
are averages for the corresponding values
的相关系数
又在哪里
-
点¯x
和ÿ
是单个值 -
重音点¯x
和ÿ
是用于相应的值的平均值
R平方值
哪里
-
Ÿ
是个人价值 -
重音ÿ
(带帽子)是对应的计算趋势值 -
ñ
是值的计数。
类物品的价值
的第一件事是为实际价值的物品,无论是创建类双
和日期时间
。基本上类是简单,只是性能点¯x
和ÿ
。但是,事情变得有点复杂,因为类型点¯x
变化。而不是使用的对象
属性我想有不同的项目类型不同的类,并且能够使用双
和日期时间
类型,而不是对象。这种方法很快就会导致用一个抽象基类泛型。
然而,使用泛型为点¯x
引入了一个新的问题,如何使用相同的计算对于两个不同的数据类型。因为我没有关于任何计算的具体要求,我决定将转换点¯x
值总是要翻一番。为了能在计算中使用此值的额外的属性ConvertedX定义。
这些类看起来像以下
抽象基类价值的物品

namespace TrendCalculus {
/// <summary>
/// Base class for value items
/// </summary>
/// <typeparam name="TX">Type definition for X</typeparam>
public abstract class ValueItem<TX> : IValueItem {
private double _y;
/// <summary>
/// Raised when the data in the item is changed
/// </summary>
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// The actual value for X
/// </summary>
public abstract TX X { get; set; }
/// <summary>
/// The value for X for calculations
/// </summary>
public abstract double ConvertedX { get; set; }
/// <summary>
/// Y value of the data item
/// </summary>
public double Y {
get {
return this._y;
}
set {
if (this._y != value) {
this._y = value;
this.NotifyPropertyChanged("Y");
}
}
}
/// <summary>
/// This method fires the property changed event
/// </summary>
/// <param name="propertyName">Name of the changed property</param>
protected void NotifyPropertyChanged(string propertyName) {
System.ComponentModel.PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null) {
handler(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// Creates a copy of the value item
/// </summary>
/// <returns>The copy</returns>
public abstract object CreateCopy();
/// <summary>
/// Creates a new trend item
/// </summary>
/// <returns>The trend item</returns>
public abstract object NewTrendItem();
}
}
值项类双
值

namespace TrendCalculus {
/// <summary>
/// Class for number items where X is double
/// </summary>
public class NumberItem : ValueItem<double> {
private double _x;
/// <summary>
/// X actual value of the data item
/// </summary>
public override double X {
get {
return this._x;
}
set {
if (this._x != value) {
this._x = value;
this.NotifyPropertyChanged("X");
}
}
}
/// <summary>
/// The value for X for calculations
/// </summary>
public override double ConvertedX {
get {
return this.X;
}
set {
if (this.X != value) {
this.X = value;
}
}
}
/// <summary>
/// Creates a new trend item
/// </summary>
/// <returns>The trend item</returns>
public override object NewTrendItem() {
return new NumberItem();
}
/// <summary>
/// Creates a copy of the value item
/// </summary>
/// <returns>The copy</returns>
public override object CreateCopy() {
return new NumberItem() {
X = this.X,
Y = this.Y
};
}
}
}
值项类的日期时间
值

namespace TrendCalculus {
/// <summary>
/// Class for number items where X is datetime
/// </summary>
public class DateItem : ValueItem<System.DateTime> {
private System.DateTime _x;
/// <summary>
/// X actual value of the data item
/// </summary>
public override System.DateTime X {
get {
return this._x;
}
set {
if (this._x != value) {
this._x = value;
this.NotifyPropertyChanged("X");
}
}
}
/// <summary>
/// The value for X for calculations
/// </summary>
public override double ConvertedX {
get {
double returnValue = 0;
if (this.X != null) {
returnValue = this.X.ToOADate();
}
return returnValue;
}
set {
System.DateTime converted = System.DateTime.FromOADate(value);
if (this.X != converted) {
this.X = converted;
}
}
}
/// <summary>
/// Creates a new trend item
/// </summary>
/// <returns>The trend item</returns>
public override object NewTrendItem() {
return new DateItem();
}
/// <summary>
/// Creates a copy of the value item
/// </summary>
/// <returns>The copy</returns>
public override object CreateCopy() {
return new DateItem() {
X = this.X,
Y = this.Y
};
}
}
}
正如你可能已经注意到了抽象类实现IValueItem
接口。此接口用于数据项的集合。该接口可帮助收集处理,因为它定义了所有必要的方法和属性以及无需知道实际数据类型点¯x
,这将需要如果抽象类定义将被使用。所以界面看起来像这样

namespace TrendCalculus {
/// <summary>
/// Interace which each value item type must implement in order to be usable in calculation
/// </summary>
public interface IValueItem {
/// <summary>
/// Raised when the data in the item is changed
/// </summary>
event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Returns the value for X for calculations
/// </summary>
double ConvertedX { get; set; }
/// <summary>
/// Y value of the data item
/// </summary>
double Y { get; set; }
/// <summary>
/// Creates a copy of the value item
/// </summary>
/// <returns>The copy</returns>
object CreateCopy();
/// <summary>
/// Creates a new trend item
/// </summary>
/// <returns>The trend item</returns>
object NewTrendItem();
}
}
值列表
接下来的事情是创造价值的物品清单。当然,一个简单的列表可以做,反而使事情更容易使用,我想有一个集合这将满足以下条件
- 集合中的变化是由WPF自动检测
-
仅执行项
IValueItem
可以被添加到集合 - 集合中的任何变化将区分数据更改通知。这将包括添加或删除项目,而且改变的项目的属性值。
由于这些我继承了一个新的类,从的ObservableCollection
如下

namespace TrendCalculus {
/// <summary>
/// List of item values
/// </summary>
public class ValueList<TValueItem> : System.Collections.ObjectModel.ObservableCollection<TValueItem>
where TValueItem : IValueItem {
/// <summary>
/// Raised when items in the value list change or data in existing items change
/// </summary>
public event System.EventHandler DataChanged;
/// <summary>
/// Type of the items in the list
/// </summary>
public ValueListTypes ListType { get; private set; }
/// <summary>
/// Default constructor
/// </summary>
private ValueList() {
this.CollectionChanged += ValueList_CollectionChanged;
}
/// <summary>
/// Constructor with the list type information
/// </summary>
/// <param name="listType"></param>
internal ValueList(ValueListTypes listType) : this() {
this.ListType = listType;
}
/// <summary>
/// Handles collection changed events for data items
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ValueList_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
// Delete PropertyChanged event handlers from items removed from collection
if (e.OldItems != null) {
foreach (IValueItem item in e.OldItems) {
item.PropertyChanged -= item_PropertyChanged;
}
}
// Add PropertyChanged event handlers to items inserted into collection
if (e.NewItems != null) {
foreach (IValueItem item in e.NewItems) {
item.PropertyChanged += item_PropertyChanged;
}
}
this.NotifyDataChanged(this);
}
/// <summary>
/// Handles Property changed events from individual items in the collection
/// </summary>
/// <param name="sender">Item that has changed</param>
/// <param name="e">Event arguments</param>
private void item_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e) {
this.NotifyDataChanged(sender);
}
/// <summary>
/// Raises DataChanged event
/// </summary>
/// <param name="sender">Item that hsa changed</param>
private void NotifyDataChanged(object sender) {
System.EventHandler handler = this.DataChanged;
if (handler != null) {
handler(sender, new System.EventArgs());
}
}
}
}
正如你所看到的构造电线CollectionChanged
事件,因此任何修改集合将被注意到。当集合改变时的PropertyChanged
事件中的所有项目是有线因此,如果出现个人价值的物品的属性的修改,集合通知。这两个事件处理程序提高DataChanged
如有变化发生的事件。
计算
该计算是由做LinearTrend
类。的用法是首先DataItems
集合填充有适当的值的项目,完成后,计算
方法被调用。计算填充以下性质
-
计算
,该值为true后计算
被调用。但是,此类跟踪数据项集合中的变化通过听DataChanged
所以如果以任何方式对源数据的变化,这个属性被设置为false事件 -
坡
包含了计算的斜坡 -
拦截
包含Y的值时,Y轴被超越 -
CORREL
包含的相关系数 -
R2
包含了R平方值 -
DataItems
包含源数据 -
TrendItems
包含在源数据中的每个独特的X值计算出的趋势值 -
StartPoint可以
返回的第一个X值计算出的趋势值 -
端点
返回的最后一个X值计算出的趋势值
这样计算的编码部分看起来像这样

/// <summary>
/// Default constructor
/// </summary>
public LinearTrend() {
this.DataItems = new ValueList<TValueItem>(ValueListTypes.DataItems);
this.TrendItems = new ValueList<TValueItem>(ValueListTypes.TrendItems);
this.Calculated = false;
this.DataItems.DataChanged += DataItems_DataChanged;
}
/// <summary>
/// Handles DataChanged event from the data item collection
/// </summary>
/// <param name="sender">Item that has changed</param>
/// <param name="e"></param>
private void DataItems_DataChanged(object sender, System.EventArgs e) {
if (this.Calculated) {
this.Calculated = false;
this.Slope = null;
this.Intercept = null;
this.Correl = null;
this.TrendItems.Clear();
}
}
/// <summary>
/// Calculates the trendline
/// </summary>
/// <returns>True if succesful</returns>
public bool Calculate() {
double slopeNumerator;
double slopeDenominator;
double correlDenominator;
double r2Numerator;
double r2Denominator;
double averageX;
double averageY;
TValueItem trendItem;
if (this.DataItems.Count == 0) {
return false;
}
// Calculate slope
averageX = this.DataItems.Average(item => item.ConvertedX);
averageY = this.DataItems.Average(item => item.Y);
slopeNumerator = this.DataItems.Sum(item => (item.ConvertedX - averageX)
* (item.Y - averageY));
slopeDenominator = this.DataItems.Sum(item => System.Math.Pow(item.ConvertedX - averageX, 2));
this.Slope = slopeNumerator / slopeDenominator;
// Calculate Intercept
this.Intercept = averageY - this.Slope * averageX;
// Calculate correlation
correlDenominator = System.Math.Sqrt(
this.DataItems.Sum(item => System.Math.Pow(item.ConvertedX - averageX, 2))
* this.DataItems.Sum(item => System.Math.Pow(item.Y - averageY, 2)));
this.Correl = slopeNumerator / correlDenominator;
// Calculate trend points
foreach (TValueItem item in this.DataItems.OrderBy(dataItem => dataItem.ConvertedX)) {
if (this.TrendItems.Where(existingItem
=> existingItem.ConvertedX == item.ConvertedX).FirstOrDefault() == null) {
trendItem = (TValueItem)item.NewTrendItem();
trendItem.ConvertedX = item.ConvertedX;
trendItem.Y = this.Slope.Value * item.ConvertedX + this.Intercept.Value;
this.TrendItems.Add(trendItem);
}
}
// Calculate r-squared value
r2Numerator = this.DataItems.Sum(
dataItem => System.Math.Pow(dataItem.Y
- this.TrendItems.Where(
calcItem => calcItem.ConvertedX == dataItem.ConvertedX).First().Y, 2));
r2Denominator = this.DataItems.Sum(dataItem => System.Math.Pow(dataItem.Y, 2))
- (System.Math.Pow(this.DataItems.Sum(dataItem => dataItem.Y), 2) / this.DataItems.Count);
this.R2 = 1 - (r2Numerator / r2Denominator);
this.Calculated = true;
return true;
}
正如你可以看到我已经在计算中使用LINQ。这本来是可能的,甚至更凝结的计算,但为了帮助单独调试我计算分子和分母。但作为一个侧面说明,在这里使用LINQ简化了代码了很多。
测试应用程序
现在,为了测试功能,让我们创建一个小的测试应用程序。应用程序应能够生成两个双
和日期时间
值作为试验材料,并显示计算的结果。该窗口看起来是这个双重
价值
并用一个例子日期时间
值
该代码非常简单。“生成值” -按钮创建了测试数据的随机
对象,并创建测试材料时,可以按下“计算”按钮上显示结果

namespace TrendTest {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class TestWindow : System.Windows.Window {
TrendCalculus.LinearTrend<TrendCalculus.IValueItem> linearTrend
= new TrendCalculus.LinearTrend<TrendCalculus.IValueItem>();
public TestWindow() {
InitializeComponent();
this.UseDouble.IsChecked = true;
this.Values.ItemsSource = linearTrend.DataItems;
this.TrendItems.ItemsSource = this.linearTrend.TrendItems;
}
private void GenerateValues_Click(object sender, System.Windows.RoutedEventArgs e) {
System.Random random = new System.Random();
linearTrend.DataItems.Clear();
for (int counter = 0; counter < 10; counter++) {
if (this.UseDouble.IsChecked.Value) {
linearTrend.DataItems.Add(new TrendCalculus.NumberItem() {
X = System.Math.Round(random.NextDouble() * 100),
Y = System.Math.Round(random.NextDouble() * 100)
});
} else {
linearTrend.DataItems.Add(new TrendCalculus.DateItem() {
X = System.DateTime.Now.AddDays(System.Math.Round(random.NextDouble() * -100)).Date,
Y = System.Math.Round(random.NextDouble() * 100)
});
}
}
}
private void Calculate_Click(object sender, System.Windows.RoutedEventArgs e) {
if (this.linearTrend.Calculate()) {
this.TrendItems.ItemsSource = this.linearTrend.TrendItems;
this.Slope.Text = this.linearTrend.Slope.ToString();
this.Intercept.Text = this.linearTrend.Intercept.ToString();
this.Correl.Text = this.linearTrend.Correl.ToString();
this.R2.Text = this.linearTrend.R2.ToString();
this.StartX.Text = this.linearTrend.StartPoint.ConvertedX.ToString();
this.StartY.Text = this.linearTrend.StartPoint.Y.ToString();
this.EndX.Text = this.linearTrend.EndPoint.ConvertedX.ToString();
this.EndY.Text = this.linearTrend.EndPoint.Y.ToString();
}
}
private void UseDouble_Checked(object sender, System.Windows.RoutedEventArgs e) {
this.linearTrend.DataItems.Clear();
}
private void UseDatetime_Checked(object sender, System.Windows.RoutedEventArgs e) {
this.linearTrend.DataItems.Clear();
}
private void DataItemsToClipboard_Click(object sender, System.Windows.RoutedEventArgs e) {
System.Text.StringBuilder clipboardData = new System.Text.StringBuilder();
clipboardData.AppendFormat("{0}\t{1}\t{2}", "Actual X", "Converted X", "Y").AppendLine();
foreach (TrendCalculus.IValueItem item in linearTrend.DataItems) {
if (item is TrendCalculus.DateItem) {
clipboardData.AppendFormat("{0}\t{1}\t{2}",
((TrendCalculus.DateItem)item).X.ToShortDateString(), item.ConvertedX, item.Y);
} else {
clipboardData.AppendFormat("{0}\t{1}\t{2}",
((TrendCalculus.NumberItem)item).X.ToString(), item.ConvertedX, item.Y);
}
clipboardData.AppendLine();
}
System.Windows.Clipboard.SetText(clipboardData.ToString());
}
}
}
为了方便地测试计算的“复制到剪贴板” - 按钮包含了复制源数据与制表符作为分隔符剪贴板使数据可以很容易地粘贴到Excel中。