2013-12-17 69 views
2

我已經採用一個DataTable作爲參數,並返回NormalData類型的對象爲在數據表中 NormalData定義中的每個列的函數重構LINQ方法

public class NormalData 
{ 
    //AttributeName = ColumnName of DataTable 
    public string AttributeName { get; set; } 

    //each column will have its mean and standard deviation computed 
    public double Mean { get; set; } 
    public double StandardDeviation { get; set; } 

    //a DataTable with three columns will create an IEnumerable<NormalData> 
    //with a count of three 
} 

以下工作,但我想一個我如何實現這第二個觀點:

public static IEnumerable<NormalData> GetNormalDataByTableColumns(DataTable dt) 
{ 
    //get list of column names to iterate over 
    List<string> columnList = GetDataTableColumnNames(dt); 
    List<NormalData> normalDataList = new List<NormalData>(); 
    for (int i = 0; i < columnList.Count; i++) 
    { 
     //creates a NormalData object for each column in the DataTable 
     NormalData normalData = new NormalData(); 

     //find average 
     normalData.Mean = GetColumnAverage(dt, columnList[i]); 

     //find stDev 
     normalData.StandardDeviation = GetColumnStDev(dt,columnList[i],normalData.Mean); 
     normalData.AttributeName = columnList[i]; 

     //add to NormalDataList 
     normalDataList.Add(normalData); 
    } 

    return normalDataList; 
} 

private static List<string> GetDataTableColumnNames(DataTable dt) 
{ 
    return (from DataColumn dc in dt.Columns 
     select dc.ColumnName).ToList(); 
} 

private static double GetColumnAverage(DataTable dt, string columnName) 
{ 
    return dt.AsEnumerable().Average(x => x.Field<double>(columnName)); 
} 

private static double GetColumnStDev(DataTable dt, string columnName,double average) 
{ 
    var squaredDiffs = (dt.AsEnumerable() 
     .Sum(x => (x.Field<double>(columnName) - average) * 
     x.Field<double>(columnName) - average)); 

    return Math.Sqrt(squaredDiffs/dt.Rows.Count); 
} 

我的感覺是糟糕的設計是參數列表GetColumnAverageGetColumnStDev都必須參加。實際上,他們應該只需要一個數值類型列表(不一定是雙精度值,但是此時硬編碼)來計算它們的值。然而,這是我今天早上得到這個工作的唯一途徑。在這個設計中我打破了什麼規則?我該如何修改這個以使GetColumn..函數只接受在columnListfor循環中迭代的DataColumn

編輯:average變量爲每列更改,不能重新使用。或者是否有可能這是好的設計,如果我不需要計算標準偏差並且是,只有平均值,我需要重載這些方法的版本?

+4

這個問題似乎是題外話,因爲它所需要的代碼審查,而不是要求特定的編程問題。 – Servy

+1

@Servy當然我有偏見,但我不同意。我相信有一個基本的LINQ方法因子的概念,我沒有正確實現,而這正是我所掌握的。但是,如果大多數人同意你的意見,那麼對於被移植到CodeReview的問題我沒有任何問題。 – wootscootinboogie

+0

除非您希望方法返回名爲St. Dev的對象,否則請考慮更有意義的名稱。 – Magus

回答

1
  1. 可以使用,而不是毫無意義的i的有意義的迭代器columnName一個foreach循環替換您for循環。

    或者,您可以用Select替換循環。

  2. 我會把列選擇邏輯放在那個循環中,從計算中分離出來。
  3. 你不需要,如果您使用以下身份的平均傳遞給StdDev功能:

    (從http://en.wikipedia.org/wiki/Standard_deviation

你的循環就變成了:

foreach (string columnName in columnList) 
{ 
    var columnData = dt.AsEnumerable().Select(x => x.Field<double>(columnName)); 

    //creates a NormalData object for each column in the DataTable 
    NormalData normalData = new NormalData(); 
    normalData.Mean = columnData.Average(); 
    normalData.StandardDeviation = StdDev(columnData); 
    normalData.AttributeName = columnName; 

    //add to NormalDataList 
    normalDataList.Add(normalData); 
} 

有一個輔助方法:

public static double StdDev(IEnumerable<double> seq) 
{ 
    long count = 0; 
    double sum = 0; 
    double sumOfSquares = 0; 
    foreach(var value in seq) 
    { 
     sum += value; 
     sumOfSquares += value * value; 
     count++; 
    } 
    double average = sum/count; 
    double averageSquare = sumOfSquares/count; 
    return Math.Sqrt(averageSquare - average * average); 
} 
1

免責聲明:已經有一段時間,因爲我已經觸及.NET所以這都可能是廢話:)

當你看到像這樣的一個或兩個參數靜態方法,我總是問我自己「可以這樣方法屬於對象本身」,所以作爲一個例子,如果你有這樣的方法:

public static String FullAddress(Address address){ 
//Build full address from address properties 
} 

這將是昭然若揭,我認爲這種方法應該屬於地址對象即直接address.FullAddress上。

此外,我看方法名稱,看看我傳遞的是否真的與任務相關。對於像平均值和標準差這樣的東西,這些方法只應該真正考慮這些值,而不管它們是來自DataTable,文件還是其他任何東西。

考慮到這一點我的第一個任務是提取平均計算,即(只是顯示界面,你猜實現)

interface NormalCalculator { 
    double CalculateAverage(IEnumerable<double> values) 
    double CalculateStDev(IEnumerable<double> values) 
} 

回到我最初說,大約看到一個方法的單輸入,你詢問這些方法是否可以直接在IEnumerable上生存,而不是作爲單獨的類。現在,我們只能將其作爲擴展方法來執行,並且您知道已經有一種平均擴展方法。

那麼問題是爲什麼不把另一個擴展方法添加到IEnumerable來計算標準偏差。

喜歡的東西:Standard Deviation in LINQ

另外,我不認爲這將是太多濫用添加一個擴展方法來獲取得到一個即值

public static class DataTableExtensions 
{ 
    public static IEnumerable<Double> Values(this DataTable dt, DataColumn column) 
    { 
     return dt.AsEnumerable().Select(x => x.Field<double>(column.ColumnName)); 
    } 
} 

然後所有的留在你的轉化方法是:

public static IEnumerable<NormalData> GetNormalDataByTableColumns(DataTable dt) 
    { 
     dt.Columns.Select(c => { 
      var values = dt.Values(c); 
      return new NormalData(values.Average(), values.StDev, c.ColoumnName); //Added constructor too as it makes sense to me, but can use named args if not 
     }); 
    } 

最後,這基本上是一個映射器/適配器/變壓器,無論你怎麼稱呼它(即它需要在一個對象並將其映射/調整或轉換爲另一個)。

我不知道如果你的測試在你的應用程序中很多,但將它作爲一個靜態工具方法意味着每次你可能想要測試使用NormalData的東西時,你必須用正確的初始數據創建一個DataTable建立。我會把它移到一個單獨的映射/轉換類中,即

interface INormalDataMapper { 
    NormalData Map(DataTable dataTable); 
} 

這是你可以用你的POCO對象模擬Map方法返回。雖然這可能是太多的信息:)。

克里斯

1

怎麼是這樣的:

public sealed class NormalData 
{ 
    private readonly string _attributeName; 
    private uint _count; 
    private double _sum; 
    private double _sumOfSquares; 

    private NormalData(string attributeName) 
    { 
     _attributeName = attributeName; 
    } 

    public string AttributeName 
    { 
     get { return _attributeName; } 
    } 

    public double Mean 
    { 
     get { return _count == 0 ? double.NaN : _sum/_count; } 
    } 

    public double StandardDeviation 
    { 
     get 
     { 
      if (_count == 0) return double.NaN; 

      var diff = _sumOfSquares - (_sum * _sum/_count); 
      return Math.Sqrt(diff/_count); 
     } 
    } 

    public double EstimatedStandardDeviation 
    { 
     get 
     { 
      if (_count < 2) return double.NaN; 

      var diff = _sumOfSquares - (_sum * _sum/_count); 
      return Math.Sqrt(diff/(_count - 1)); 
     } 
    } 

    public void Add(double value) 
    { 
     _count = checked(_count + 1); 
     _sum += value; 
     _sumOfSquares += (value * value); 
    } 

    public static NormalData Create(string attributeName) 
    { 
     return new NormalData(attributeName, 0, 0, 0); 
    } 
} 

public static IEnumerable<NormalData> GetNormalDataByTableColumns(DataTable dt) 
{ 
    var normalDataList = dt.Columns.Cast<DataColumn>().Select(c => NormalData.Create(c.ColumnName)).ToList(); 

    foreach (DataRow row in dt.Rows) 
    { 
     foreach (NormalData item in normalDataList) 
     { 
      double value = row.Field<double>(item.AttributeName); 
      item.Add(value); 
     } 
    } 

    return normalDataList; 
}