diff --git a/src/Microsoft.ML.Core/Utilities/DoubleParser.cs b/src/Microsoft.ML.Core/Utilities/DoubleParser.cs index bad57cdd3c..22bd8ea82e 100644 --- a/src/Microsoft.ML.Core/Utilities/DoubleParser.cs +++ b/src/Microsoft.ML.Core/Utilities/DoubleParser.cs @@ -17,6 +17,21 @@ internal static class DoubleParser private const ulong TopThreeBits = 0xE000000000000000UL; private const char InfinitySymbol = '\u221E'; + // Note for future development: DoubleParser is a static class and DecimalMarker is a + // static variable, which means only one instance of these can exist at once. As such, + // the value of DecimalMarker cannot vary when datasets with differing decimal markers + // are loaded together at once, which would result in not being able to accurately read + // the dataset with the differing decimal marker. Although this edge case where we attempt + // to load in datasets with different decimal markers at once is unlikely to occur, we + // should still be aware of this and plan to fix it in the future. + + // The decimal marker that separates the integer part from the fractional part of a number + // written in decimal from can vary across different cultures as either '.' or ','. The + // default decimal marker in ML .NET is '.', however through this static char variable, + // we allow users to specify the decimal marker used in their datasets as ',' as well. + [BestFriend] + internal static char DecimalMarker = '.'; + // REVIEW: casting ulong to Double doesn't always do the right thing, for example // with 0x84595161401484A0UL. Hence the gymnastics several places in this code. Note that // long to Double does work. The work around is: @@ -555,6 +570,12 @@ private static bool TryParseCore(ReadOnlySpan span, ref int ich, ref bool break; case '.': + if (DecimalMarker != '.') // Decimal marker was not '.', but we encountered a '.', which must be an error. + return false; // Since this was an error, return false, which will later make the caller to set NaN as the out value. + goto LPoint; + case ',': + if (DecimalMarker != ',') // Same logic as above. + return false; goto LPoint; // The common cases. @@ -571,7 +592,7 @@ private static bool TryParseCore(ReadOnlySpan span, ref int ich, ref bool break; } - // Get digits before '.' + // Get digits before the decimal marker, which may be '.' or ',' uint d; for (; ; ) { @@ -593,14 +614,14 @@ private static bool TryParseCore(ReadOnlySpan span, ref int ich, ref bool } Contracts.Assert(i < span.Length); - if (span[i] != '.') + if (span[i] != DecimalMarker) goto LAfterDigits; LPoint: Contracts.Assert(i < span.Length); - Contracts.Assert(span[i] == '.'); + Contracts.Assert(span[i] == DecimalMarker); - // Get the digits after '.' + // Get the digits after the decimal marker, which may be '.' or ',' for (; ; ) { if (++i >= span.Length) diff --git a/src/Microsoft.ML.Data/DataLoadSave/Text/TextLoader.cs b/src/Microsoft.ML.Data/DataLoadSave/Text/TextLoader.cs index eb198427d1..6bc58de054 100644 --- a/src/Microsoft.ML.Data/DataLoadSave/Text/TextLoader.cs +++ b/src/Microsoft.ML.Data/DataLoadSave/Text/TextLoader.cs @@ -474,6 +474,12 @@ public class Options [Argument(ArgumentType.AtMostOnce, Name = nameof(Separator), Visibility = ArgumentAttribute.VisibilityType.EntryPointsOnly, HelpText = "Source column separator.", ShortName = "sep")] public char[] Separators = new[] { Defaults.Separator }; + /// + /// The character that should be used as the decimal marker. Default value is '.'. Only '.' and ',' are allowed to be decimal markers. + /// + [Argument(ArgumentType.AtMostOnce, Name = "Decimal Marker", HelpText = "Character symbol used to separate the integer part from the fractional part of a number written in decimal form.", ShortName = "decimal")] + public char DecimalMarker = Defaults.DecimalMarker; + /// /// Specifies the input columns that should be mapped to columns. /// @@ -541,6 +547,7 @@ internal static class Defaults internal const bool AllowQuoting = false; internal const bool AllowSparse = false; internal const char Separator = '\t'; + internal const char DecimalMarker = '.'; internal const bool HasHeader = false; internal const bool TrimWhitespace = false; internal const bool ReadMultilines = false; @@ -1071,7 +1078,7 @@ private static VersionInfo GetVersionInfo() //verWrittenCur: 0x0001000A, // Added ForceVector in Range //verWrittenCur: 0x0001000B, // Header now retained if used and present //verWrittenCur: 0x0001000C, // Removed Min and Contiguous from KeyType, and added ReadMultilines flag to OptionFlags - verWrittenCur: 0x0001000D, // Added escapeChar option + verWrittenCur: 0x0001000D, // Added escapeChar option and decimal marker option to allow for ',' to be a decimal marker verReadableCur: 0x0001000A, verWeCanReadBack: 0x00010009, loaderSignature: LoaderSignature, @@ -1103,6 +1110,7 @@ private enum OptionFlags : uint // Input size is zero for unknown - determined by the data (including sparse rows). private readonly int _inputSize; private readonly char[] _separators; + private readonly char _decimalMarker; private readonly Bindings _bindings; private readonly Parser _parser; @@ -1219,6 +1227,11 @@ internal TextLoader(IHostEnvironment env, Options options = null, IMultiStreamSo } } + if (options.DecimalMarker != '.' && options.DecimalMarker != ',') + throw _host.ExceptUserArg(nameof(Options.DecimalMarker), "Decimal marker cannot be the '{0}' character. It must be '.' or ','.", options.DecimalMarker); + if (!options.AllowQuoting && options.DecimalMarker == ',' && _separators.Contains(',')) + throw _host.ExceptUserArg(nameof(Options.AllowQuoting), "Quoting must be allowed if decimal marker and separator are the ',' character."); + _decimalMarker = options.DecimalMarker; _escapeChar = options.EscapeChar; if(_separators.Contains(_escapeChar)) throw _host.ExceptUserArg(nameof(Options.EscapeChar), "EscapeChar '{0}' can't be used both as EscapeChar and separator", _escapeChar); @@ -1387,6 +1400,7 @@ private TextLoader(IHost host, ModelLoadContext ctx) // int: number of separators // char[]: separators // char: escapeChar + // char: decimal marker // bindings int cbFloat = ctx.Reader.ReadInt32(); host.CheckDecode(cbFloat == sizeof(float)); @@ -1414,10 +1428,13 @@ private TextLoader(IHost host, ModelLoadContext ctx) if (ctx.Header.ModelVerWritten >= 0x0001000D) { _escapeChar = ctx.Reader.ReadChar(); + _decimalMarker = ctx.Reader.ReadChar(); + host.CheckDecode(_decimalMarker == '.' || _decimalMarker == ','); } else { _escapeChar = Defaults.EscapeChar; + _decimalMarker = Defaults.DecimalMarker; } host.CheckDecode(!_separators.Contains(_escapeChar)); @@ -1463,6 +1480,7 @@ void ICanSaveModel.Save(ModelSaveContext ctx) // int: number of separators // char[]: separators // char: escapeChar + // char: decimal marker // bindings ctx.Writer.Write(sizeof(float)); ctx.Writer.Write(_maxRows); @@ -1472,6 +1490,7 @@ void ICanSaveModel.Save(ModelSaveContext ctx) ctx.Writer.Write(_inputSize); ctx.Writer.WriteCharArray(_separators); ctx.Writer.Write(_escapeChar); + ctx.Writer.Write(_decimalMarker); _bindings.Save(ctx); } @@ -1612,6 +1631,7 @@ public BoundLoader(TextLoader loader, IMultiStreamSource files) public DataViewRowCursor GetRowCursor(IEnumerable columnsNeeded, Random rand = null) { _host.CheckValueOrNull(rand); + DoubleParser.DecimalMarker = _loader._decimalMarker; var active = Utils.BuildArray(_loader._bindings.OutputSchema.Count, columnsNeeded); return Cursor.Create(_loader, _files, active); } @@ -1619,6 +1639,7 @@ public DataViewRowCursor GetRowCursor(IEnumerable columns public DataViewRowCursor[] GetRowCursorSet(IEnumerable columnsNeeded, int n, Random rand = null) { _host.CheckValueOrNull(rand); + DoubleParser.DecimalMarker = _loader._decimalMarker; var active = Utils.BuildArray(_loader._bindings.OutputSchema.Count, columnsNeeded); return Cursor.CreateSet(_loader, _files, active, n); } diff --git a/test/BaselineOutput/Common/EntryPoints/core_manifest.json b/test/BaselineOutput/Common/EntryPoints/core_manifest.json index 84e8b329b9..6a0c17a44f 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_manifest.json +++ b/test/BaselineOutput/Common/EntryPoints/core_manifest.json @@ -369,6 +369,18 @@ "\t" ] }, + { + "Name": "Decimal Marker", + "Type": "Char", + "Desc": "Character symbol used to separate the integer part from the fractional part of a number written in decimal form.", + "Aliases": [ + "decimal" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": "." + }, { "Name": "TrimWhitespace", "Type": "Bool", diff --git a/test/Microsoft.ML.Tests/TextLoaderTests.cs b/test/Microsoft.ML.Tests/TextLoaderTests.cs index 4768f2d82c..a4d44c5cc2 100644 --- a/test/Microsoft.ML.Tests/TextLoaderTests.cs +++ b/test/Microsoft.ML.Tests/TextLoaderTests.cs @@ -840,6 +840,168 @@ public void TestTextLoaderBackCompat_VerWritt_0x0001000C() Assert.Equal("Iris-setosa", previewIris.RowView[0].Values[index].Value.ToString()); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestCommaAsDecimalMarker(bool useCsvVersion) + { + // When userCsvVersion == false: + // Datasets iris.txt and iris-decimal-marker-as-comma.txt are the exact same, except for their + // decimal markers. Decimal marker in iris.txt is '.', and ',' in iris-decimal-marker-as-comma.txt. + + // When userCsvVersion == true: + // Check to confirm TextLoader can read data from a CSV file where the separator is ',', decimals + // are enclosed with quotes, and with the decimal marker being ','. + + // Do these checks with both float and double as types of features being read, to test decimal marker + // recognition with both doubles and floats. + TestCommaAsDecimalMarkerHelper(useCsvVersion); + TestCommaAsDecimalMarkerHelper(useCsvVersion); + } + + private void TestCommaAsDecimalMarkerHelper(bool useCsvVersion) + { + // Datasets iris.txt and iris-decimal-marker-as-comma.txt are the exact same, except for their + // decimal markers. Decimal marker in iris.txt is '.', and ',' in iris-decimal-marker-as-comma.txt. + // Datasets iris.txt and iris-decimal-marker-as-comma.csv have the exact same data, however the .csv + // version has ',' as decimal marker and separator, and feature values are enclosed with quotes. + // T varies as either float or double, so that decimal markers can be tested for both floating + // point value types. + var mlContext = new MLContext(seed: 1); + + // Read dataset with period as decimal marker. + string dataPathDecimalMarkerPeriod = GetDataPath("iris.txt"); + var readerDecimalMarkerPeriod = new TextLoader(mlContext, new TextLoader.Options() + { + Columns = new[] + { + new TextLoader.Column("Label", DataKind.UInt32, 0), + new TextLoader.Column("Features", typeof(T) == typeof(double) ? DataKind.Double : DataKind.Single, new [] { new TextLoader.Range(1, 4) }), + }, + DecimalMarker = '.' + }); + var textDataDecimalMarkerPeriod = readerDecimalMarkerPeriod.Load(GetDataPath(dataPathDecimalMarkerPeriod)); + + // Load values from iris.txt + DataViewSchema columnsPeriod = textDataDecimalMarkerPeriod.Schema; + using DataViewRowCursor cursorPeriod = textDataDecimalMarkerPeriod.GetRowCursor(columnsPeriod); + UInt32 labelPeriod = default; + ValueGetter labelDelegatePeriod = cursorPeriod.GetGetter(columnsPeriod[0]); + VBuffer featuresPeriod = default; + ValueGetter> featuresDelegatePeriod = cursorPeriod.GetGetter>(columnsPeriod[1]); + + // Iterate over each row and save labels and features to array for future comparison + int count = 0; + UInt32[] labels = new uint[150]; + T[][] features = new T[150][]; + while (cursorPeriod.MoveNext()) + { + //Get values from respective columns + labelDelegatePeriod(ref labelPeriod); + featuresDelegatePeriod(ref featuresPeriod); + labels[count] = labelPeriod; + features[count] = featuresPeriod.GetValues().ToArray(); + count++; + } + + // Read dataset with comma as decimal marker. + // Dataset is either the .csv version or the .txt version. + string dataPathDecimalMarkerComma; + TextLoader.Options options = new TextLoader.Options() + { + Columns = new[] + { + new TextLoader.Column("Label", DataKind.UInt32, 0), + new TextLoader.Column("Features", typeof(T) == typeof(double) ? DataKind.Double : DataKind.Single, new [] { new TextLoader.Range(1, 4) }) + }, + }; + // Set TextLoader.Options for the .csv or .txt cases. + if (useCsvVersion) + { + dataPathDecimalMarkerComma = GetDataPath("iris-decimal-marker-as-comma.csv"); + options.DecimalMarker = ','; + options.Separator = ","; + options.AllowQuoting = true; + options.HasHeader = true; + } + else + { + dataPathDecimalMarkerComma = GetDataPath("iris-decimal-marker-as-comma.txt"); + options.DecimalMarker = ','; + } + var readerDecimalMarkerComma = new TextLoader(mlContext, options); + var textDataDecimalMarkerComma = readerDecimalMarkerComma.Load(GetDataPath(dataPathDecimalMarkerComma)); + + // Load values from dataset with comma as decimal marker + DataViewSchema columnsComma = textDataDecimalMarkerComma.Schema; + using DataViewRowCursor cursorComma = textDataDecimalMarkerComma.GetRowCursor(columnsComma); + UInt32 labelComma = default; + ValueGetter labelDelegateComma = cursorComma.GetGetter(columnsComma[0]); + VBuffer featuresComma = default; + ValueGetter> featuresDelegateComma = cursorComma.GetGetter>(columnsComma[1]); + + // Check values from dataset with comma as decimal marker match those in iris.txt (period decimal marker) + count = 0; + while (cursorComma.MoveNext()) + { + //Get values from respective columns + labelDelegateComma(ref labelComma); + featuresDelegateComma(ref featuresComma); + Assert.Equal(labels[count], labelComma); + Assert.Equal(features[count], featuresComma.GetValues().ToArray()); + count++; + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestWrongDecimalMarkerInputs(bool useCommaAsDecimalMarker) + { + // When DecimalMarker does not match the actual decimal marker used in the dataset, + // we obtain values of NaN. Check that the values are indeed NaN in this case. + // Do this check for both cases where decimal markers in the dataset are '.' and ','. + var mlContext = new MLContext(seed: 1); + + // Try reading a dataset where '.' is the actual decimal marker, but DecimalMarker = ',', + // and vice versa. + string dataPath; + TextLoader.Options options = new TextLoader.Options() + { + Columns = new[] + { + new TextLoader.Column("Label", DataKind.UInt32, 0), + new TextLoader.Column("Features", DataKind.Single, new [] { new TextLoader.Range(1, 4) }) + }, + }; + if (useCommaAsDecimalMarker) + { + dataPath = GetDataPath("iris.txt"); // Has '.' as decimal marker inside dataset + options.DecimalMarker = ','; // Choose wrong decimal marker on purpose + } + else + { + dataPath = GetDataPath("iris-decimal-marker-as-comma.txt"); // Has ',' as decimal marker inside dataset + options.DecimalMarker = '.'; // Choose wrong decimal marker on purpose + } + var reader = new TextLoader(mlContext, options); + var textData = reader.Load(GetDataPath(dataPath)); + + // Check that the features being loaded are NaN. + DataViewSchema columns = textData.Schema; + using DataViewRowCursor cursor = textData.GetRowCursor(columns); + VBuffer featuresPeriod = default; + ValueGetter> featuresDelegatePeriod = cursor.GetGetter>(columns[1]); + + // Iterate over each row and check that feature values are NaN. + while (cursor.MoveNext()) + { + featuresDelegatePeriod.Invoke(ref featuresPeriod); + foreach(float feature in featuresPeriod.GetValues().ToArray()) + Assert.Equal(feature, Single.NaN); + } + } + private class IrisNoFields { } diff --git a/test/data/iris-decimal-marker-as-comma.csv b/test/data/iris-decimal-marker-as-comma.csv new file mode 100644 index 0000000000..db1986ec8e --- /dev/null +++ b/test/data/iris-decimal-marker-as-comma.csv @@ -0,0 +1,151 @@ +Label,Sepal,length,Sepal,width,Petal length,Petal width +0,"5,1","3,5","1,4","0,2" +0,"4,9","3,0","1,4","0,2" +0,"4,7","3,2","1,3","0,2" +0,"4,6","3,1","1,5","0,2" +0,"5,0","3,6","1,4","0,2" +0,"5,4","3,9","1,7","0,4" +0,"4,6","3,4","1,4","0,3" +0,"5,0","3,4","1,5","0,2" +0,"4,4","2,9","1,4","0,2" +0,"4,9","3,1","1,5","0,1" +0,"5,4","3,7","1,5","0,2" +0,"4,8","3,4","1,6","0,2" +0,"4,8","3,0","1,4","0,1" +0,"4,3","3,0","1,1","0,1" +0,"5,8","4,0","1,2","0,2" +0,"5,7","4,4","1,5","0,4" +0,"5,4","3,9","1,3","0,4" +0,"5,1","3,5","1,4","0,3" +0,"5,7","3,8","1,7","0,3" +0,"5,1","3,8","1,5","0,3" +0,"5,4","3,4","1,7","0,2" +0,"5,1","3,7","1,5","0,4" +0,"4,6","3,6","1,0","0,2" +0,"5,1","3,3","1,7","0,5" +0,"4,8","3,4","1,9","0,2" +0,"5,0","3,0","1,6","0,2" +0,"5,0","3,4","1,6","0,4" +0,"5,2","3,5","1,5","0,2" +0,"5,2","3,4","1,4","0,2" +0,"4,7","3,2","1,6","0,2" +0,"4,8","3,1","1,6","0,2" +0,"5,4","3,4","1,5","0,4" +0,"5,2","4,1","1,5","0,1" +0,"5,5","4,2","1,4","0,2" +0,"4,9","3,1","1,5","0,1" +0,"5,0","3,2","1,2","0,2" +0,"5,5","3,5","1,3","0,2" +0,"4,9","3,1","1,5","0,1" +0,"4,4","3,0","1,3","0,2" +0,"5,1","3,4","1,5","0,2" +0,"5,0","3,5","1,3","0,3" +0,"4,5","2,3","1,3","0,3" +0,"4,4","3,2","1,3","0,2" +0,"5,0","3,5","1,6","0,6" +0,"5,1","3,8","1,9","0,4" +0,"4,8","3,0","1,4","0,3" +0,"5,1","3,8","1,6","0,2" +0,"4,6","3,2","1,4","0,2" +0,"5,3","3,7","1,5","0,2" +0,"5,0","3,3","1,4","0,2" +1,"7,0","3,2","4,7","1,4" +1,"6,4","3,2","4,5","1,5" +1,"6,9","3,1","4,9","1,5" +1,"5,5","2,3","4,0","1,3" +1,"6,5","2,8","4,6","1,5" +1,"5,7","2,8","4,5","1,3" +1,"6,3","3,3","4,7","1,6" +1,"4,9","2,4","3,3","1,0" +1,"6,6","2,9","4,6","1,3" +1,"5,2","2,7","3,9","1,4" +1,"5,0","2,0","3,5","1,0" +1,"5,9","3,0","4,2","1,5" +1,"6,0","2,2","4,0","1,0" +1,"6,1","2,9","4,7","1,4" +1,"5,6","2,9","3,6","1,3" +1,"6,7","3,1","4,4","1,4" +1,"5,6","3,0","4,5","1,5" +1,"5,8","2,7","4,1","1,0" +1,"6,2","2,2","4,5","1,5" +1,"5,6","2,5","3,9","1,1" +1,"5,9","3,2","4,8","1,8" +1,"6,1","2,8","4,0","1,3" +1,"6,3","2,5","4,9","1,5" +1,"6,1","2,8","4,7","1,2" +1,"6,4","2,9","4,3","1,3" +1,"6,6","3,0","4,4","1,4" +1,"6,8","2,8","4,8","1,4" +1,"6,7","3,0","5,0","1,7" +1,"6,0","2,9","4,5","1,5" +1,"5,7","2,6","3,5","1,0" +1,"5,5","2,4","3,8","1,1" +1,"5,5","2,4","3,7","1,0" +1,"5,8","2,7","3,9","1,2" +1,"6,0","2,7","5,1","1,6" +1,"5,4","3,0","4,5","1,5" +1,"6,0","3,4","4,5","1,6" +1,"6,7","3,1","4,7","1,5" +1,"6,3","2,3","4,4","1,3" +1,"5,6","3,0","4,1","1,3" +1,"5,5","2,5","4,0","1,3" +1,"5,5","2,6","4,4","1,2" +1,"6,1","3,0","4,6","1,4" +1,"5,8","2,6","4,0","1,2" +1,"5,0","2,3","3,3","1,0" +1,"5,6","2,7","4,2","1,3" +1,"5,7","3,0","4,2","1,2" +1,"5,7","2,9","4,2","1,3" +1,"6,2","2,9","4,3","1,3" +1,"5,1","2,5","3,0","1,1" +1,"5,7","2,8","4,1","1,3" +2,"6,3","3,3","6,0","2,5" +2,"5,8","2,7","5,1","1,9" +2,"7,1","3,0","5,9","2,1" +2,"6,3","2,9","5,6","1,8" +2,"6,5","3,0","5,8","2,2" +2,"7,6","3,0","6,6","2,1" +2,"4,9","2,5","4,5","1,7" +2,"7,3","2,9","6,3","1,8" +2,"6,7","2,5","5,8","1,8" +2,"7,2","3,6","6,1","2,5" +2,"6,5","3,2","5,1","2,0" +2,"6,4","2,7","5,3","1,9" +2,"6,8","3,0","5,5","2,1" +2,"5,7","2,5","5,0","2,0" +2,"5,8","2,8","5,1","2,4" +2,"6,4","3,2","5,3","2,3" +2,"6,5","3,0","5,5","1,8" +2,"7,7","3,8","6,7","2,2" +2,"7,7","2,6","6,9","2,3" +2,"6,0","2,2","5,0","1,5" +2,"6,9","3,2","5,7","2,3" +2,"5,6","2,8","4,9","2,0" +2,"7,7","2,8","6,7","2,0" +2,"6,3","2,7","4,9","1,8" +2,"6,7","3,3","5,7","2,1" +2,"7,2","3,2","6,0","1,8" +2,"6,2","2,8","4,8","1,8" +2,"6,1","3,0","4,9","1,8" +2,"6,4","2,8","5,6","2,1" +2,"7,2","3,0","5,8","1,6" +2,"7,4","2,8","6,1","1,9" +2,"7,9","3,8","6,4","2,0" +2,"6,4","2,8","5,6","2,2" +2,"6,3","2,8","5,1","1,5" +2,"6,1","2,6","5,6","1,4" +2,"7,7","3,0","6,1","2,3" +2,"6,3","3,4","5,6","2,4" +2,"6,4","3,1","5,5","1,8" +2,"6,0","3,0","4,8","1,8" +2,"6,9","3,1","5,4","2,1" +2,"6,7","3,1","5,6","2,4" +2,"6,9","3,1","5,1","2,3" +2,"5,8","2,7","5,1","1,9" +2,"6,8","3,2","5,9","2,3" +2,"6,7","3,3","5,7","2,5" +2,"6,7","3,0","5,2","2,3" +2,"6,3","2,5","5,0","1,9" +2,"6,5","3,0","5,2","2,0" +2,"6,2","3,4","5,4","2,3" +2,"5,9","3,0","5,1","1,8" diff --git a/test/data/iris-decimal-marker-as-comma.txt b/test/data/iris-decimal-marker-as-comma.txt new file mode 100644 index 0000000000..d9f3b06b4a --- /dev/null +++ b/test/data/iris-decimal-marker-as-comma.txt @@ -0,0 +1,151 @@ +#Label Sepal length Sepal width Petal length Petal width +0 5,1 3,5 1,4 0,2 +0 4,9 3,0 1,4 0,2 +0 4,7 3,2 1,3 0,2 +0 4,6 3,1 1,5 0,2 +0 5,0 3,6 1,4 0,2 +0 5,4 3,9 1,7 0,4 +0 4,6 3,4 1,4 0,3 +0 5,0 3,4 1,5 0,2 +0 4,4 2,9 1,4 0,2 +0 4,9 3,1 1,5 0,1 +0 5,4 3,7 1,5 0,2 +0 4,8 3,4 1,6 0,2 +0 4,8 3,0 1,4 0,1 +0 4,3 3,0 1,1 0,1 +0 5,8 4,0 1,2 0,2 +0 5,7 4,4 1,5 0,4 +0 5,4 3,9 1,3 0,4 +0 5,1 3,5 1,4 0,3 +0 5,7 3,8 1,7 0,3 +0 5,1 3,8 1,5 0,3 +0 5,4 3,4 1,7 0,2 +0 5,1 3,7 1,5 0,4 +0 4,6 3,6 1,0 0,2 +0 5,1 3,3 1,7 0,5 +0 4,8 3,4 1,9 0,2 +0 5,0 3,0 1,6 0,2 +0 5,0 3,4 1,6 0,4 +0 5,2 3,5 1,5 0,2 +0 5,2 3,4 1,4 0,2 +0 4,7 3,2 1,6 0,2 +0 4,8 3,1 1,6 0,2 +0 5,4 3,4 1,5 0,4 +0 5,2 4,1 1,5 0,1 +0 5,5 4,2 1,4 0,2 +0 4,9 3,1 1,5 0,1 +0 5,0 3,2 1,2 0,2 +0 5,5 3,5 1,3 0,2 +0 4,9 3,1 1,5 0,1 +0 4,4 3,0 1,3 0,2 +0 5,1 3,4 1,5 0,2 +0 5,0 3,5 1,3 0,3 +0 4,5 2,3 1,3 0,3 +0 4,4 3,2 1,3 0,2 +0 5,0 3,5 1,6 0,6 +0 5,1 3,8 1,9 0,4 +0 4,8 3,0 1,4 0,3 +0 5,1 3,8 1,6 0,2 +0 4,6 3,2 1,4 0,2 +0 5,3 3,7 1,5 0,2 +0 5,0 3,3 1,4 0,2 +1 7,0 3,2 4,7 1,4 +1 6,4 3,2 4,5 1,5 +1 6,9 3,1 4,9 1,5 +1 5,5 2,3 4,0 1,3 +1 6,5 2,8 4,6 1,5 +1 5,7 2,8 4,5 1,3 +1 6,3 3,3 4,7 1,6 +1 4,9 2,4 3,3 1,0 +1 6,6 2,9 4,6 1,3 +1 5,2 2,7 3,9 1,4 +1 5,0 2,0 3,5 1,0 +1 5,9 3,0 4,2 1,5 +1 6,0 2,2 4,0 1,0 +1 6,1 2,9 4,7 1,4 +1 5,6 2,9 3,6 1,3 +1 6,7 3,1 4,4 1,4 +1 5,6 3,0 4,5 1,5 +1 5,8 2,7 4,1 1,0 +1 6,2 2,2 4,5 1,5 +1 5,6 2,5 3,9 1,1 +1 5,9 3,2 4,8 1,8 +1 6,1 2,8 4,0 1,3 +1 6,3 2,5 4,9 1,5 +1 6,1 2,8 4,7 1,2 +1 6,4 2,9 4,3 1,3 +1 6,6 3,0 4,4 1,4 +1 6,8 2,8 4,8 1,4 +1 6,7 3,0 5,0 1,7 +1 6,0 2,9 4,5 1,5 +1 5,7 2,6 3,5 1,0 +1 5,5 2,4 3,8 1,1 +1 5,5 2,4 3,7 1,0 +1 5,8 2,7 3,9 1,2 +1 6,0 2,7 5,1 1,6 +1 5,4 3,0 4,5 1,5 +1 6,0 3,4 4,5 1,6 +1 6,7 3,1 4,7 1,5 +1 6,3 2,3 4,4 1,3 +1 5,6 3,0 4,1 1,3 +1 5,5 2,5 4,0 1,3 +1 5,5 2,6 4,4 1,2 +1 6,1 3,0 4,6 1,4 +1 5,8 2,6 4,0 1,2 +1 5,0 2,3 3,3 1,0 +1 5,6 2,7 4,2 1,3 +1 5,7 3,0 4,2 1,2 +1 5,7 2,9 4,2 1,3 +1 6,2 2,9 4,3 1,3 +1 5,1 2,5 3,0 1,1 +1 5,7 2,8 4,1 1,3 +2 6,3 3,3 6,0 2,5 +2 5,8 2,7 5,1 1,9 +2 7,1 3,0 5,9 2,1 +2 6,3 2,9 5,6 1,8 +2 6,5 3,0 5,8 2,2 +2 7,6 3,0 6,6 2,1 +2 4,9 2,5 4,5 1,7 +2 7,3 2,9 6,3 1,8 +2 6,7 2,5 5,8 1,8 +2 7,2 3,6 6,1 2,5 +2 6,5 3,2 5,1 2,0 +2 6,4 2,7 5,3 1,9 +2 6,8 3,0 5,5 2,1 +2 5,7 2,5 5,0 2,0 +2 5,8 2,8 5,1 2,4 +2 6,4 3,2 5,3 2,3 +2 6,5 3,0 5,5 1,8 +2 7,7 3,8 6,7 2,2 +2 7,7 2,6 6,9 2,3 +2 6,0 2,2 5,0 1,5 +2 6,9 3,2 5,7 2,3 +2 5,6 2,8 4,9 2,0 +2 7,7 2,8 6,7 2,0 +2 6,3 2,7 4,9 1,8 +2 6,7 3,3 5,7 2,1 +2 7,2 3,2 6,0 1,8 +2 6,2 2,8 4,8 1,8 +2 6,1 3,0 4,9 1,8 +2 6,4 2,8 5,6 2,1 +2 7,2 3,0 5,8 1,6 +2 7,4 2,8 6,1 1,9 +2 7,9 3,8 6,4 2,0 +2 6,4 2,8 5,6 2,2 +2 6,3 2,8 5,1 1,5 +2 6,1 2,6 5,6 1,4 +2 7,7 3,0 6,1 2,3 +2 6,3 3,4 5,6 2,4 +2 6,4 3,1 5,5 1,8 +2 6,0 3,0 4,8 1,8 +2 6,9 3,1 5,4 2,1 +2 6,7 3,1 5,6 2,4 +2 6,9 3,1 5,1 2,3 +2 5,8 2,7 5,1 1,9 +2 6,8 3,2 5,9 2,3 +2 6,7 3,3 5,7 2,5 +2 6,7 3,0 5,2 2,3 +2 6,3 2,5 5,0 1,9 +2 6,5 3,0 5,2 2,0 +2 6,2 3,4 5,4 2,3 +2 5,9 3,0 5,1 1,8