{"action":"created","comment":{"url":"https://api.github.com/repos/dotnet/machinelearning/pulls/comments/216471197","pull_request_review_id":153960867,"id":216471197,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxNjQ3MTE5Nw==","diff_hunk":"@@ -67,386 +79,547 @@ private static VersionInfo GetVersionInfo()\n                 loaderSignature: LoaderSignature);\n         }\n \n-        public override bool CanSavePfa => true;\n-\n-        // These are parallel to Infos.\n-        private readonly ColumnType[] _types;\n-        private KeyToValueMap[] _kvMaps;\n-\n         /// <summary>\n-        /// Convenience constructor for public facing API.\n+        /// Create a <see cref=\"KeyToValueTransform\"/> that takes and transforms one column.\n         /// </summary>\n-        /// <param name=\"env\">Host Environment.</param>\n-        /// <param name=\"input\">Input <see cref=\"IDataView\"/>. This is the output from previous transform or loader.</param>\n-        /// <param name=\"name\">Name of the output column.</param>\n-        /// <param name=\"source\">Name of the input column.  If this is null '<paramref name=\"name\"/>' will be used.</param>\n-        public KeyToValueTransform(IHostEnvironment env, IDataView input, string name, string source = null)\n-            : this(env, new Arguments() { Column = new[] { new Column() { Source = source ?? name, Name = name } } }, input)\n+        public KeyToValueTransform(IHostEnvironment env, string columnName)\n+            : this(env, (columnName, columnName))\n         {\n         }\n \n         /// <summary>\n-        /// Public constructor corresponding to SignatureDataTransform.\n+        /// Create a <see cref=\"KeyToValueTransform\"/> that takes multiple pairs of columns.\n         /// </summary>\n-        public KeyToValueTransform(IHostEnvironment env, Arguments args, IDataView input)\n-            : base(env, LoaderSignature, Contracts.CheckRef(args, nameof(args)).Column,\n-                input, TestIsKey)\n+        public KeyToValueTransform(IHostEnvironment env, params (string input, string output)[] columns)\n+            : base(Contracts.CheckRef(env, nameof(env)).Register(nameof(KeyToValueTransform)), columns)\n         {\n-            Host.AssertNonEmpty(Infos);\n-            Host.Assert(Infos.Length == Utils.Size(args.Column));\n-\n-            ComputeKVMapsAndMetadata(out _types, out _kvMaps);\n         }\n \n-        private KeyToValueTransform(IHost host, ModelLoadContext ctx, IDataView input)\n-            : base(host, ctx, input, TestIsKey)\n+        /// <summary>\n+        /// Factory method for SignatureDataTransform.\n+        /// </summary>\n+        public static IDataTransform Create(IHostEnvironment env, Arguments args, IDataView input)\n         {\n-            Host.AssertValue(ctx);\n-\n-            // *** Binary format ***\n-\n-            ComputeKVMapsAndMetadata(out _types, out _kvMaps);\n+            Contracts.CheckValue(env, nameof(env));\n+            env.CheckValue(args, nameof(args));\n+            env.CheckValue(input, nameof(input));\n+            env.CheckNonEmpty(args.Column, nameof(args.Column));\n \n+            var transformer = new KeyToValueTransform(env, args.Column.Select(c => (c.Source ?? c.Name, c.Name)).ToArray());\n+            return transformer.MakeDataTransform(input);\n         }\n \n-        public static KeyToValueTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input)\n+        /// <summary>\n+        /// Factory method for SignatureLoadModel.\n+        /// </summary>\n+        public static KeyToValueTransform Create(IHostEnvironment env, ModelLoadContext ctx)\n         {\n             Contracts.CheckValue(env, nameof(env));\n-            env.CheckValue(ctx, nameof(ctx));\n+            var host = env.Register(nameof(KeyToValueTransform));\n+            host.CheckValue(ctx, nameof(ctx));\n             ctx.CheckAtModel(GetVersionInfo());\n-            env.CheckValue(input, nameof(input));\n-            env.CheckValue(env, nameof(env));\n-            var h = env.Register(LoaderSignature);\n-            return h.Apply(\"Loading Model\", ch => new KeyToValueTransform(h, ctx, input));\n+            return new KeyToValueTransform(host, ctx);\n         }\n \n+        private KeyToValueTransform(IHost host, ModelLoadContext ctx)\n+            : base(host, ctx)\n+        {\n+        }\n+\n+        /// <summary>\n+        /// Factory method for SignatureLoadDataTransform.\n+        /// </summary>\n+        public static IDataTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input)\n+            => Create(env, ctx).MakeDataTransform(input);\n+\n+        /// <summary>\n+        /// Factory method for SignatureLoadRowMapper.\n+        /// </summary>\n+        public static IRowMapper Create(IHostEnvironment env, ModelLoadContext ctx, ISchema inputSchema)\n+            => Create(env, ctx).MakeRowMapper(inputSchema);\n+\n         public override void Save(ModelSaveContext ctx)\n         {\n             Host.CheckValue(ctx, nameof(ctx));\n             ctx.CheckAtModel();\n             ctx.SetVersionInfo(GetVersionInfo());\n \n             // *** Binary format ***\n-            SaveBase(ctx);\n-        }\n+            // <base>\n \n-        protected override JToken SaveAsPfaCore(BoundPfaContext ctx, int iinfo, ColInfo info, JToken srcToken)\n-        {\n-            Contracts.AssertValue(ctx);\n-            Contracts.Assert(0 <= iinfo && iinfo < Infos.Length);\n-            Contracts.Assert(Infos[iinfo] == info);\n-            Contracts.AssertValue(srcToken);\n-            Contracts.Assert(CanSavePfa);\n-            return _kvMaps[iinfo].SavePfa(ctx, srcToken);\n+            SaveColumns(ctx);\n         }\n \n-        // Computes the types of the columns and constructs the kvMaps.\n-        private void ComputeKVMapsAndMetadata(out ColumnType[] types, out KeyToValueMap[] kvMaps)\n+        protected override IRowMapper MakeRowMapper(ISchema inputSchema)\n+            => new Mapper(this, inputSchema);\n+\n+        private sealed class Mapper : MapperBase\n         {\n-            Contracts.Assert(Utils.Size(Infos) > 0);\n+            private readonly KeyToValueTransform _parent;\n+            private readonly ColumnType[] _types;\n+            private readonly KeyToValueMap[] _kvMaps;\n \n-            var md = Metadata;\n-            types = new ColumnType[Infos.Length];\n-            kvMaps = new KeyToValueMap[Infos.Length];\n-            for (int iinfo = 0; iinfo < types.Length; iinfo++)\n+            public Mapper(KeyToValueTransform parent, ISchema inputSchema)\n+                : base(parent.Host.Register(nameof(Mapper)), parent, inputSchema)\n             {\n-                // Inherit slot names from source.\n-                using (var bldr = md.BuildMetadata(iinfo, Source.Schema, Infos[iinfo].Source, MetadataUtils.Kinds.SlotNames))\n-                {\n-                }\n+                _parent = parent;\n+                ComputeKVMapsAndMetadata(inputSchema, out _types, out _kvMaps);\n \n-                // Construct kvMaps.\n-                Contracts.Assert(types[iinfo] == null);\n-                var typeSrc = Infos[iinfo].TypeSrc;\n-                var typeVals = Source.Schema.GetMetadataTypeOrNull(MetadataUtils.Kinds.KeyValues, Infos[iinfo].Source);\n-                Host.Check(typeVals != null, \"Metadata KeyValues does not exist\");\n-                Host.Check(typeVals.VectorSize == typeSrc.ItemType.KeyCount, \"KeyValues metadata size does not match column type key count\");\n-                if (!typeSrc.IsVector)\n-                    types[iinfo] = typeVals.ItemType;\n-                else\n-                    types[iinfo] = new VectorType(typeVals.ItemType.AsPrimitive, typeSrc.AsVector);\n-                Func<int, ColumnType, ColumnType, KeyToValueMap> func = GetKeyMetadata<int, int>;\n-                var meth = func.GetMethodInfo().GetGenericMethodDefinition().MakeGenericMethod(new Type[] { typeSrc.ItemType.RawType,\n-                    types[iinfo].ItemType.RawType });\n-                kvMaps[iinfo] = (KeyToValueMap)meth.Invoke(this, new object[] { iinfo, typeSrc, typeVals });\n             }\n-            md.Seal();\n-        }\n-\n-        private KeyToValueMap GetKeyMetadata<TKey, TValue>(int iinfo, ColumnType typeKey, ColumnType typeVal)\n-        {\n-            Host.Assert(0 <= iinfo && iinfo < Infos.Length);\n-            Host.AssertValue(typeKey);\n-            Host.AssertValue(typeVal);\n-            Host.Assert(typeKey.ItemType.RawType == typeof(TKey));\n-            Host.Assert(typeVal.ItemType.RawType == typeof(TValue));\n-\n-            var keyMetadata = default(VBuffer<TValue>);\n-            Source.Schema.GetMetadata<VBuffer<TValue>>(MetadataUtils.Kinds.KeyValues, Infos[iinfo].Source, ref keyMetadata);\n-            Host.Check(keyMetadata.Length == typeKey.ItemType.KeyCount);\n-\n-            VBufferUtils.Densify<TValue>(ref keyMetadata);\n-            return new KeyToValueMap<TKey, TValue>(this, typeKey.ItemType.AsKey, typeVal.ItemType.AsPrimitive, keyMetadata.Values, iinfo);\n-        }\n \n-        protected override ColumnType GetColumnTypeCore(int iinfo)\n-        {\n-            Host.Assert(0 <= iinfo & iinfo < _types.Length);\n-            var type = _types[iinfo];\n-            Host.Assert(type != null);\n-            return type;\n-        }\n-\n-        protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer)\n-        {\n-            Host.AssertValueOrNull(ch);\n-            Host.AssertValue(input);\n-            Host.Assert(0 <= iinfo && iinfo < Infos.Length);\n-            disposer = null;\n-\n-            return _kvMaps[iinfo].GetMappingGetter(input);\n-        }\n+            public override RowMapperColumnInfo[] GetOutputColumns()\n+            {\n+                var result = new RowMapperColumnInfo[_parent.ColumnPairs.Length];\n+                for (int i = 0; i < _parent.ColumnPairs.Length; i++)\n+                {\n+                    var meta = RowColumnUtils.GetMetadataAsRow(InputSchema, ColMapNewToOld[i],\n+                        x => x == MetadataUtils.Kinds.SlotNames);\n+                    result[i] = new RowMapperColumnInfo(_parent.ColumnPairs[i].output, _types[i], meta);\n+                }\n+                return result;\n+            }\n \n-        /// <summary>\n-        /// A map is an object capable of creating the association from an input type, to an output\n-        /// type. This mapping is constructed from key metadata, with the input type being the key type\n-        /// and the output type being the type specified by the key metadata.\n-        /// </summary>\n-        private abstract class KeyToValueMap\n-        {\n-            /// <summary>\n-            /// The item type of the output type, that is, either the output type or,\n-            /// if a vector, the item type of that type.\n-            /// </summary>\n-            protected readonly PrimitiveType TypeOutput;\n+            protected override Delegate MakeGetter(IRow input, int iinfo, out Action disposer)\n+            {\n+                Host.AssertValue(input);\n+                Host.Assert(0 <= iinfo && iinfo < _types.Length);\n+                disposer = null;\n+                return _kvMaps[iinfo].GetMappingGetter(input);\n+            }\n \n-            /// <summary>\n-            /// The column index in Infos.\n-            /// </summary>\n-            protected readonly int InfoIndex;\n+            // Computes the types of the columns and constructs the kvMaps.\n+            private void ComputeKVMapsAndMetadata(ISchema schema, out ColumnType[] types, out KeyToValueMap[] kvMaps)\n+            {\n+                types = new ColumnType[_parent.ColumnPairs.Length];\n+                kvMaps = new KeyToValueMap[_parent.ColumnPairs.Length];\n+                for (int iinfo = 0; iinfo < types.Length; iinfo++)\n+                {\n+                    // Construct kvMaps.\n+                    Contracts.Assert(types[iinfo] == null);\n+                    var typeSrc = schema.GetColumnType(ColMapNewToOld[iinfo]);\n+                    var typeVals = schema.GetMetadataTypeOrNull(MetadataUtils.Kinds.KeyValues, ColMapNewToOld[iinfo]);\n+                    Host.Check(typeVals != null, \"Metadata KeyValues does not exist\");\n+                    Host.Check(typeVals.VectorSize == typeSrc.ItemType.KeyCount, \"KeyValues metadata size does not match column type key count\");\n+                    if (!typeSrc.IsVector)\n+                        types[iinfo] = typeVals.ItemType;\n+                    else\n+                        types[iinfo] = new VectorType(typeVals.ItemType.AsPrimitive, typeSrc.AsVector);\n+\n+                    // MarshalInvoke with two generic params.\n+                    Func<int, ColumnType, ColumnType, KeyToValueMap> func = GetKeyMetadata<int, int>;\n+                    var meth = func.GetMethodInfo().GetGenericMethodDefinition().MakeGenericMethod(\n+                        new Type[] { typeSrc.ItemType.RawType, types[iinfo].ItemType.RawType });\n+                    kvMaps[iinfo] = (KeyToValueMap)meth.Invoke(this, new object[] { iinfo, typeSrc, typeVals });\n+                }\n+            }\n \n+            private KeyToValueMap GetKeyMetadata<TKey, TValue>(int iinfo, ColumnType typeKey, ColumnType typeVal)\n+            {\n+                Host.Assert(0 <= iinfo && iinfo < _parent.ColumnPairs.Length);\n+                Host.AssertValue(typeKey);\n+                Host.AssertValue(typeVal);\n+                Host.Assert(typeKey.ItemType.RawType == typeof(TKey));\n+                Host.Assert(typeVal.ItemType.RawType == typeof(TValue));\n+\n+                var keyMetadata = default(VBuffer<TValue>);\n+                InputSchema.GetMetadata(MetadataUtils.Kinds.KeyValues, ColMapNewToOld[iinfo], ref keyMetadata);\n+                Host.Check(keyMetadata.Length == typeKey.ItemType.KeyCount);\n+\n+                VBufferUtils.Densify(ref keyMetadata);\n+                return new KeyToValueMap<TKey, TValue>(this, typeKey.ItemType.AsKey, typeVal.ItemType.AsPrimitive, keyMetadata.Values, iinfo);\n+            }\n             /// <summary>\n-            /// The parent transform.\n+            /// A map is an object capable of creating the association from an input type, to an output\n+            /// type. This mapping is constructed from key metadata, with the input type being the key type\n+            /// and the output type being the type specified by the key metadata.\n             /// </summary>\n-            protected readonly KeyToValueTransform Parent;\n-\n-            protected KeyToValueMap(KeyToValueTransform trans, PrimitiveType typeVal, int iinfo)\n+            private abstract class KeyToValueMap\n             {\n-                // REVIEW: Is there a better way to perform this first assert value?\n-                Contracts.AssertValue(trans);\n-                Parent = trans;\n-                Parent.Host.AssertValue(typeVal);\n-                Parent.Host.Assert(0 <= iinfo && iinfo < Parent.Infos.Length);\n-                TypeOutput = typeVal;\n-                InfoIndex = iinfo;\n-            }\n-\n-            public abstract Delegate GetMappingGetter(IRow input);\n+                /// <summary>\n+                /// The item type of the output type, that is, either the output type or,\n+                /// if a vector, the item type of that type.\n+                /// </summary>\n+                protected readonly PrimitiveType TypeOutput;\n+\n+                /// <summary>\n+                /// The column index in Infos.\n+                /// </summary>\n+                protected readonly int InfoIndex;\n+\n+                /// <summary>\n+                /// The parent transform.\n+                /// </summary>\n+                protected readonly Mapper Parent;\n+\n+                protected KeyToValueMap(Mapper mapper, PrimitiveType typeVal, int iinfo)\n+                {\n+                    // REVIEW: Is there a better way to perform this first assert value?\n+                    Contracts.AssertValue(mapper);\n+                    Parent = mapper;\n+                    Parent.Host.AssertValue(typeVal);\n+                    Parent.Host.Assert(0 <= iinfo && iinfo < Parent._types.Length);\n+                    TypeOutput = typeVal;\n+                    InfoIndex = iinfo;\n+                }\n \n-            public abstract JToken SavePfa(BoundPfaContext ctx, JToken srcToken);\n-        }\n+                public abstract Delegate GetMappingGetter(IRow input);\n \n-        private class KeyToValueMap<TKey, TValue> : KeyToValueMap\n-        {\n-            private readonly TValue[] _values;\n-            private readonly TValue _na;\n+                public abstract JToken SavePfa(BoundPfaContext ctx, JToken srcToken);\n+            }\n \n-            private readonly bool _naMapsToDefault;\n-            private readonly RefPredicate<TValue> _isDefault;\n+            private class KeyToValueMap<TKey, TValue> : KeyToValueMap\n+            {\n+                private readonly TValue[] _values;\n+                private readonly TValue _na;\n \n-            private readonly ValueMapper<TKey, UInt32> _convertToUInt;\n+                private readonly bool _naMapsToDefault;\n+                private readonly RefPredicate<TValue> _isDefault;\n \n-            public KeyToValueMap(KeyToValueTransform trans, KeyType typeKey, PrimitiveType typeVal, TValue[] values, int iinfo)\n-                : base(trans, typeVal, iinfo)\n-            {\n-                Parent.Host.AssertValue(values);\n-                Parent.Host.Assert(typeKey.RawType == typeof(TKey));\n-                Parent.Host.Assert(TypeOutput.RawType == typeof(TValue));\n-                _values = values;\n+                private readonly ValueMapper<TKey, UInt32> _convertToUInt;\n \n-                // REVIEW: May want to include more specific information about what the specific value is for the default.\n-                using (var ch = Parent.Host.Start(\"Getting NA Predicate and Value\"))\n+                public KeyToValueMap(Mapper parent, KeyType typeKey, PrimitiveType typeVal, TValue[] values, int iinfo)\n+                    : base(parent, typeVal, iinfo)\n                 {\n-                    _na = Conversions.Instance.GetNAOrDefault<TValue>(TypeOutput.ItemType, out _naMapsToDefault);\n+                    Parent.Host.AssertValue(values);\n+                    Parent.Host.Assert(typeKey.RawType == typeof(TKey));\n+                    Parent.Host.Assert(TypeOutput.RawType == typeof(TValue));\n+                    _values = values;\n \n-                    if (_naMapsToDefault)\n+                    // REVIEW: May want to include more specific information about what the specific value is for the default.\n+                    using (var ch = Parent.Host.Start(\"Getting NA Predicate and Value\"))\n                     {\n-                        // Only initialize _isDefault if _defaultIsNA is true as this is the only case in which it is used.\n-                        _isDefault = Conversions.Instance.GetIsDefaultPredicate<TValue>(TypeOutput.ItemType);\n-                        RefPredicate<TValue> del;\n-                        if (!Conversions.Instance.TryGetIsNAPredicate<TValue>(TypeOutput.ItemType, out del))\n+                        _na = Conversions.Instance.GetNAOrDefault<TValue>(TypeOutput.ItemType, out _naMapsToDefault);\n+\n+                        if (_naMapsToDefault)\n                         {\n-                            ch.Warning(\"There is no NA value for type '{0}'. The missing key value \" +\n-                                \"will be mapped to the default value of '{0}'\", TypeOutput.ItemType);\n+                            // Only initialize _isDefault if _defaultIsNA is true as this is the only case in which it is used.\n+                            _isDefault = Conversions.Instance.GetIsDefaultPredicate<TValue>(TypeOutput.ItemType);\n+                            RefPredicate<TValue> del;\n+                            if (!Conversions.Instance.TryGetIsNAPredicate<TValue>(TypeOutput.ItemType, out del))\n+                            {\n+                                ch.Warning(\"There is no NA value for type '{0}'. The missing key value \" +\n+                                    \"will be mapped to the default value of '{0}'\", TypeOutput.ItemType);\n+                            }\n                         }\n                     }\n-                }\n-\n-                bool identity;\n-                _convertToUInt = Conversions.Instance.GetStandardConversion<TKey, UInt32>(typeKey, NumberType.U4, out identity);\n-            }\n \n-            private void MapKey(ref TKey src, ref TValue dst)\n-            {\n-                uint uintSrc = 0;\n-                _convertToUInt(ref src, ref uintSrc);\n-                // Assign to NA if key value is not in valid range.\n-                if (0 < uintSrc && uintSrc <= _values.Length)\n-                    dst = _values[uintSrc - 1];\n-                else\n-                    dst = _na;\n-            }\n+                    bool identity;\n+                    _convertToUInt = Conversions.Instance.GetStandardConversion<TKey, UInt32>(typeKey, NumberType.U4, out identity);\n+                }\n \n-            public override Delegate GetMappingGetter(IRow input)\n-            {\n-                // When constructing the getter, there are a few cases we have to consider:\n-                // If scalar then it's just a straightforward mapping.\n-                // If vector, then we have to detect whether the mapping should be mapped to\n-                // dense or sparse. Almost all cases will map to dense (as the NA key value\n-                // represented by sparsity will map to the NA of the corresponding type) but\n-                // if enough key values map to the default value of the output type sparsifying\n-                // may be desirable, as is the case when the default value is equal to the\n-                // NA value.\n-\n-                Parent.Host.AssertValue(input);\n-\n-                if (!Parent._types[InfoIndex].IsVector)\n+                private void MapKey(ref TKey src, ref TValue dst)\n                 {\n-                    var src = default(TKey);\n-                    ValueGetter<TKey> getSrc = Parent.GetSrcGetter<TKey>(input, InfoIndex);\n-                    ValueGetter<TValue> retVal =\n-                        (ref TValue dst) =>\n-                        {\n-                            getSrc(ref src);\n-                            MapKey(ref src, ref dst);\n-                        };\n-                    return retVal;\n+                    uint uintSrc = 0;\n+                    _convertToUInt(ref src, ref uintSrc);\n+                    // Assign to NA if key value is not in valid range.\n+                    if (0 < uintSrc && uintSrc <= _values.Length)\n+                        dst = _values[uintSrc - 1];\n+                    else\n+                        dst = _na;\n                 }\n-                else\n-                {\n-                    var src = default(VBuffer<TKey>);\n-                    var dstItem = default(TValue);\n-                    int maxSize = TypeOutput.IsKnownSizeVector ? TypeOutput.VectorSize : Utils.ArrayMaxSize;\n-                    ValueGetter<VBuffer<TKey>> getSrc = Parent.GetSrcGetter<VBuffer<TKey>>(input, InfoIndex);\n-                    ValueGetter<VBuffer<TValue>> retVal =\n-                        (ref VBuffer<TValue> dst) =>\n-                        {\n-                            getSrc(ref src);\n-                            int srcSize = src.Length;\n-                            int srcCount = src.Count;\n-                            var srcValues = src.Values;\n-                            var dstValues = dst.Values;\n-                            var dstIndices = dst.Indices;\n-\n-                            int islotDst = 0;\n \n-                            if (src.IsDense)\n+                public override Delegate GetMappingGetter(IRow input)\n+                {\n+                    // When constructing the getter, there are a few cases we have to consider:\n+                    // If scalar then it's just a straightforward mapping.\n+                    // If vector, then we have to detect whether the mapping should be mapped to\n+                    // dense or sparse. Almost all cases will map to dense (as the NA key value\n+                    // represented by sparsity will map to the NA of the corresponding type) but\n+                    // if enough key values map to the default value of the output type sparsifying\n+                    // may be desirable, as is the case when the default value is equal to the\n+                    // NA value.\n+\n+                    Parent.Host.AssertValue(input);\n+\n+                    if (!Parent._types[InfoIndex].IsVector)\n+                    {\n+                        var src = default(TKey);\n+                        ValueGetter<TKey> getSrc = input.GetGetter<TKey>(Parent.ColMapNewToOld[InfoIndex]);\n+                        ValueGetter<TValue> retVal =\n+                            (ref TValue dst) =>\n                             {\n-                                Utils.EnsureSize(ref dstValues, srcSize, maxSize, keepOld: false);\n-\n-                                for (int slot = 0; slot < srcSize; ++slot)\n-                                {\n-                                    MapKey(ref srcValues[slot], ref dstValues[slot]);\n-\n-                                    // REVIEW:\n-                                    // The current implementation always maps dense to dense, even if the resulting columns could benefit from\n-                                    // sparsity. This would only occur if there are key values that map over half of the keys to the default value.\n-                                    // One way to rule out the helpfulness of sparsifying is to have a flag that indicates whether any key maps to\n-                                    // default, still need a good method for discerning when to implement sparsity (would either need precomputation\n-                                    // of the amount of default values or allow for some dynamic updating to sparsity when the requisite number of\n-                                    // defaults is hit. We assume that if the user was willing to densify the data into key values that they will\n-                                    // be fine with this output being dense.\n-                                }\n-                                islotDst = srcSize;\n-                            }\n-                            else if (!_naMapsToDefault)\n+                                getSrc(ref src);\n+                                MapKey(ref src, ref dst);\n+                            };\n+                        return retVal;\n+                    }\n+                    else\n+                    {\n+                        var src = default(VBuffer<TKey>);\n+                        var dstItem = default(TValue);\n+                        int maxSize = TypeOutput.IsKnownSizeVector ? TypeOutput.VectorSize : Utils.ArrayMaxSize;\n+                        ValueGetter<VBuffer<TKey>> getSrc = input.GetGetter<VBuffer<TKey>>(Parent.ColMapNewToOld[InfoIndex]);\n+                        ValueGetter<VBuffer<TValue>> retVal =\n+                            (ref VBuffer<TValue> dst) =>\n                             {\n-                                // Sparse input will always result in dense output unless the key metadata maps back to key types.\n-                                // Currently this always maps sparse to dense, as long as the output type's NA does not equal its default value.\n-                                Utils.EnsureSize(ref dstValues, srcSize, maxSize, keepOld: false);\n-\n-                                var srcIndices = src.Indices;\n-                                int nextExplicitSlot = src.Count == 0 ? srcSize : srcIndices[0];\n-                                int islot = 0;\n-                                for (int slot = 0; slot < srcSize; ++slot)\n+                                getSrc(ref src);\n+                                int srcSize = src.Length;\n+                                int srcCount = src.Count;\n+                                var srcValues = src.Values;\n+                                var dstValues = dst.Values;\n+                                var dstIndices = dst.Indices;\n+\n+                                int islotDst = 0;\n+\n+                                if (src.IsDense)\n                                 {\n-                                    if (nextExplicitSlot == slot)\n+                                    Utils.EnsureSize(ref dstValues, srcSize, maxSize, keepOld: false);\n+\n+                                    for (int slot = 0; slot < srcSize; ++slot)\n                                     {\n-                                        // Current slot has an explicitly defined value.\n-                                        Parent.Host.Assert(islot < src.Count);\n-                                        MapKey(ref srcValues[islot], ref dstValues[slot]);\n-                                        nextExplicitSlot = ++islot == src.Count ? srcSize : srcIndices[islot];\n-                                        Parent.Host.Assert(slot < nextExplicitSlot);\n+                                        MapKey(ref srcValues[slot], ref dstValues[slot]);\n+\n+                                        // REVIEW:\n+                                        // The current implementation always maps dense to dense, even if the resulting columns could benefit from\n+                                        // sparsity. This would only occur if there are key values that map over half of the keys to the default value.\n+                                        // One way to rule out the helpfulness of sparsifying is to have a flag that indicates whether any key maps to\n+                                        // default, still need a good method for discerning when to implement sparsity (would either need precomputation\n+                                        // of the amount of default values or allow for some dynamic updating to sparsity when the requisite number of\n+                                        // defaults is hit. We assume that if the user was willing to densify the data into key values that they will\n+                                        // be fine with this output being dense.\n                                     }\n-                                    else\n+                                    islotDst = srcSize;\n+                                }\n+                                else if (!_naMapsToDefault)\n+                                {\n+                                    // Sparse input will always result in dense output unless the key metadata maps back to key types.\n+                                    // Currently this always maps sparse to dense, as long as the output type's NA does not equal its default value.\n+                                    Utils.EnsureSize(ref dstValues, srcSize, maxSize, keepOld: false);\n+\n+                                    var srcIndices = src.Indices;\n+                                    int nextExplicitSlot = src.Count == 0 ? srcSize : srcIndices[0];\n+                                    int islot = 0;\n+                                    for (int slot = 0; slot < srcSize; ++slot)\n                                     {\n-                                        Parent.Host.Assert(slot < nextExplicitSlot);\n-                                        dstValues[slot] = _na;\n+                                        if (nextExplicitSlot == slot)\n+                                        {\n+                                            // Current slot has an explicitly defined value.\n+                                            Parent.Host.Assert(islot < src.Count);\n+                                            MapKey(ref srcValues[islot], ref dstValues[slot]);\n+                                            nextExplicitSlot = ++islot == src.Count ? srcSize : srcIndices[islot];\n+                                            Parent.Host.Assert(slot < nextExplicitSlot);\n+                                        }\n+                                        else\n+                                        {\n+                                            Parent.Host.Assert(slot < nextExplicitSlot);\n+                                            dstValues[slot] = _na;\n+                                        }\n                                     }\n+                                    islotDst = srcSize;\n                                 }\n-                                islotDst = srcSize;\n-                            }\n-                            else\n-                            {\n-                                // As the default value equals the NA value for the output type, we produce sparse output.\n-                                Utils.EnsureSize(ref dstValues, srcCount, maxSize, keepOld: false);\n-                                Utils.EnsureSize(ref dstIndices, srcCount, maxSize, keepOld: false);\n-                                var srcIndices = src.Indices;\n-                                for (int islotSrc = 0; islotSrc < srcCount; ++islotSrc)\n+                                else\n                                 {\n-                                    // Current slot has an explicitly defined value.\n-                                    Parent.Host.Assert(islotSrc < srcCount);\n-                                    MapKey(ref srcValues[islotSrc], ref dstItem);\n-                                    if (!_isDefault(ref dstItem))\n+                                    // As the default value equals the NA value for the output type, we produce sparse output.\n+                                    Utils.EnsureSize(ref dstValues, srcCount, maxSize, keepOld: false);\n+                                    Utils.EnsureSize(ref dstIndices, srcCount, maxSize, keepOld: false);\n+                                    var srcIndices = src.Indices;\n+                                    for (int islotSrc = 0; islotSrc < srcCount; ++islotSrc)\n                                     {\n-                                        dstValues[islotDst] = dstItem;\n-                                        dstIndices[islotDst++] = srcIndices[islotSrc];\n+                                        // Current slot has an explicitly defined value.\n+                                        Parent.Host.Assert(islotSrc < srcCount);\n+                                        MapKey(ref srcValues[islotSrc], ref dstItem);\n+                                        if (!_isDefault(ref dstItem))\n+                                        {\n+                                            dstValues[islotDst] = dstItem;\n+                                            dstIndices[islotDst++] = srcIndices[islotSrc];\n+                                        }\n                                     }\n                                 }\n-                            }\n-                            dst = new VBuffer<TValue>(srcSize, islotDst, dstValues, dstIndices);\n-                        };\n-                    return retVal;\n+                                dst = new VBuffer<TValue>(srcSize, islotDst, dstValues, dstIndices);\n+                            };\n+                        return retVal;\n+                    }\n                 }\n-            }\n \n-            public override JToken SavePfa(BoundPfaContext ctx, JToken srcToken)\n-            {\n-                Contracts.AssertValue(ctx);\n-                Contracts.AssertValue(srcToken);\n-                var info = Parent.Infos[InfoIndex];\n-                var outType = PfaUtils.Type.PfaTypeOrNullForColumnType(TypeOutput);\n-                if (outType == null)\n-                    return null;\n-\n-                // REVIEW: To map the missing key to the *default* value is\n-                // wrong, but the alternative is we have a bunch of null unions everywhere\n-                // probably, which I am not prepared to do.\n-                var defaultToken = PfaUtils.Type.DefaultTokenOrNull(TypeOutput);\n-                JArray jsonValues;\n-                if (TypeOutput.IsText)\n+                public override JToken SavePfa(BoundPfaContext ctx, JToken srcToken)\n                 {\n-                    jsonValues = new JArray();\n-                    for (int i = 0; i < _values.Length; ++i)\n-                        jsonValues.Add(_values[i].ToString());\n+                    Contracts.AssertValue(ctx);\n+                    Contracts.AssertValue(srcToken);\n+                    var outType = PfaUtils.Type.PfaTypeOrNullForColumnType(TypeOutput);\n+                    if (outType == null)\n+                        return null;\n+\n+                    // REVIEW: To map the missing key to the *default* value is\n+                    // wrong, but the alternative is we have a bunch of null unions everywhere\n+                    // probably, which I am not prepared to do.\n+                    var defaultToken = PfaUtils.Type.DefaultTokenOrNull(TypeOutput);\n+                    JArray jsonValues;\n+                    if (TypeOutput.IsText)\n+                    {\n+                        jsonValues = new JArray();\n+                        for (int i = 0; i < _values.Length; ++i)\n+                            jsonValues.Add(_values[i].ToString());\n+                    }\n+                    else\n+                        jsonValues = new JArray(_values);\n+\n+                    string cellName = ctx.DeclareCell(\"KeyToValueMap\", PfaUtils.Type.Array(outType), jsonValues);\n+                    JObject cellRef = PfaUtils.Cell(cellName);\n+\n+                    var srcType = Parent.InputSchema.GetColumnType(Parent.ColMapNewToOld[InfoIndex]);\n+                    if (srcType.IsVector)\n+                    {\n+                        var funcName = ctx.GetFreeFunctionName(\"mapKeyToValue\");\n+                        ctx.Pfa.AddFunc(funcName, new JArray(PfaUtils.Param(\"key\", PfaUtils.Type.Int)),\n+                            outType, PfaUtils.If(PfaUtils.Call(\"<\", \"key\", 0), defaultToken,\n+                            PfaUtils.Index(cellRef, \"key\")));\n+                        var funcRef = PfaUtils.FuncRef(\"u.\" + funcName);\n+                        return PfaUtils.Call(\"a.map\", srcToken, funcRef);\n+                    }\n+                    return PfaUtils.If(PfaUtils.Call(\"<\", srcToken, 0), defaultToken, PfaUtils.Index(cellRef, srcToken));\n                 }\n-                else\n-                    jsonValues = new JArray(_values);\n+            }\n+\n+        }\n+    }\n \n-                string cellName = ctx.DeclareCell(\"KeyToValueMap\", PfaUtils.Type.Array(outType), jsonValues);\n-                JObject cellRef = PfaUtils.Cell(cellName);\n+    public sealed class KeyToValueEstimator : TrivialEstimator<KeyToValueTransform>\n+    {\n+        public KeyToValueEstimator(IHostEnvironment env, string columnName)\n+            : base(Contracts.CheckRef(env, nameof(env)).Register(nameof(KeyToValueEstimator)), new KeyToValueTransform(env, columnName))\n+        {\n+        }\n+\n+        public KeyToValueEstimator(IHostEnvironment env, params (string input, string output)[] columns)\n+            : base(Contracts.CheckRef(env, nameof(env)).Register(nameof(KeyToValueEstimator)), new KeyToValueTransform(env, columns))\n+        {\n+        }\n+\n+        public override SchemaShape GetOutputSchema(SchemaShape inputSchema)\n+        {\n+            Host.CheckValue(inputSchema, nameof(inputSchema));\n+            var result = inputSchema.Columns.ToDictionary(x => x.Name);\n+            foreach (var colInfo in Transformer.Columns)\n+            {\n+                if (!inputSchema.TryFindColumn(colInfo.input, out var col))\n+                    throw Host.ExceptSchemaMismatch(nameof(inputSchema), \"input\", colInfo.input);\n+                if (!col.IsKey)\n+                    throw Host.ExceptSchemaMismatch(nameof(inputSchema), \"input\", colInfo.input, \"key type\", col.GetTypeString());\n+\n+                if (!col.Metadata.TryFindColumn(MetadataUtils.Kinds.KeyValues, out var keyMetaCol))\n+                    throw Host.ExceptParam(nameof(inputSchema), $\"Input column '{colInfo.input}' doesn't contain key values metadata\");\n+\n+                SchemaShape metadata = null;\n+                if (col.Metadata.TryFindColumn(MetadataUtils.Kinds.SlotNames, out var slotCol))\n+                    metadata = new SchemaShape(new[] { slotCol });\n+\n+                result[colInfo.output] = new SchemaShape.Column(colInfo.output, col.Kind, keyMetaCol.ItemType, keyMetaCol.IsKey, metadata);\n+            }\n+\n+            return new SchemaShape(result.Values);\n+        }\n+    }\n+\n+    /// <summary>\n+    /// Extension methods for the static-pipeline over <see cref=\"PipelineColumn\"/> objects.\n+    /// </summary>\n+    public static class KeyToValueStaticExtensions\n+    {\n+        private interface IColInput\n+        {\n+            PipelineColumn Input { get; }\n+        }\n+\n+        private sealed class OutKeyColumn<T1, T2, T3> : Key<T2, T3>, IColInput\n+        {\n+            public PipelineColumn Input { get; }\n \n-                if (info.TypeSrc.IsVector)\n+            public OutKeyColumn(Key<T1, Key<T2, T3>> input)\n+                : base(Reconciler.Inst, input)\n+            {\n+                Input = input;\n+            }\n+        }\n+\n+        private sealed class OutScalarColumn<TKey, TValue> : Scalar<TValue>, IColInput\n+        {\n+            public PipelineColumn Input { get; }\n+\n+            public OutScalarColumn(Key<TKey, TValue> input)\n+                : base(Reconciler.Inst, input)\n+            {\n+                Input = input;\n+            }\n+        }\n+\n+        private sealed class OutVectorColumn<TKey, TValue> : Vector<TValue>, IColInput\n+        {\n+            public PipelineColumn Input { get; }\n+\n+            public OutVectorColumn(Vector<Key<TKey, TValue>> input)\n+                : base(Reconciler.Inst, input)\n+            {\n+                Input = input;\n+            }\n+        }\n+\n+        private sealed class OutVarVectorColumn<TKey, TValue> : VarVector<TValue>, IColInput\n+        {\n+            public PipelineColumn Input { get; }\n+\n+            public OutVarVectorColumn(VarVector<Key<TKey, TValue>> input)\n+                : base(Reconciler.Inst, input)\n+            {\n+                Input = input;\n+            }\n+        }\n+\n+        private sealed class Reconciler : EstimatorReconciler\n+        {\n+            public static Reconciler Inst = new Reconciler();\n+\n+            private Reconciler() { }\n+\n+            public override IEstimator<ITransformer> Reconcile(IHostEnvironment env,\n+                PipelineColumn[] toOutput,\n+                IReadOnlyDictionary<PipelineColumn, string> inputNames,\n+                IReadOnlyDictionary<PipelineColumn, string> outputNames,\n+                IReadOnlyCollection<string> usedNames)\n+            {\n+                var cols = new (string input, string output)[toOutput.Length];\n+                for (int i = 0; i < toOutput.Length; ++i)\n                 {\n-                    var funcName = ctx.GetFreeFunctionName(\"mapKeyToValue\");\n-                    ctx.Pfa.AddFunc(funcName, new JArray(PfaUtils.Param(\"key\", PfaUtils.Type.Int)),\n-                        outType, PfaUtils.If(PfaUtils.Call(\"<\", \"key\", 0), defaultToken,\n-                        PfaUtils.Index(cellRef, \"key\")));\n-                    var funcRef = PfaUtils.FuncRef(\"u.\" + funcName);\n-                    return PfaUtils.Call(\"a.map\", srcToken, funcRef);\n+                    var outCol = (IColInput)toOutput[i];\n+                    cols[i] = (inputNames[outCol.Input], outputNames[toOutput[i]]);\n                 }\n-                return PfaUtils.If(PfaUtils.Call(\"<\", srcToken, 0), defaultToken, PfaUtils.Index(cellRef, srcToken));\n+                return new KeyToValueEstimator(env, cols);\n             }\n         }\n+\n+        /// <summary>\n+        /// Convert a key column to a column containing the corresponding value.\n+        /// </summary>\n+        public static Key<T2, T3> ToValue<T1, T2, T3>(this Key<T1, Key<T2, T3>> input)\n+        {\n+            Contracts.CheckValue(input, nameof(input));\n+            return new OutKeyColumn<T1, T2, T3>(input);\n+        }\n+","path":"src/Microsoft.ML.Data/Transforms/KeyToValueTransform.cs","position":870,"original_position":870,"commit_id":"d0dd362a803ab850b7f458707286805882740057","original_commit_id":"d0dd362a803ab850b7f458707286805882740057","user":{"login":"TomFinley","id":8295757,"node_id":"MDQ6VXNlcjgyOTU3NTc=","avatar_url":"https://avatars1.githubusercontent.com/u/8295757?v=4","gravatar_id":"","url":"https://api.github.com/users/TomFinley","html_url":"https://github.com/TomFinley","followers_url":"https://api.github.com/users/TomFinley/followers","following_url":"https://api.github.com/users/TomFinley/following{/other_user}","gists_url":"https://api.github.com/users/TomFinley/gists{/gist_id}","starred_url":"https://api.github.com/users/TomFinley/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TomFinley/subscriptions","organizations_url":"https://api.github.com/users/TomFinley/orgs","repos_url":"https://api.github.com/users/TomFinley/repos","events_url":"https://api.github.com/users/TomFinley/events{/privacy}","received_events_url":"https://api.github.com/users/TomFinley/received_events","type":"User","site_admin":false},"body":"This is not presently possible, since you cannot have key-values that are themselves keys with key-values... you can however have key. So I think an overload for `Key<K1, Key<K2>>` would make sense, but not this.","created_at":"2018-09-10T21:00:17Z","updated_at":"2018-09-10T21:00:17Z","html_url":"https://github.com/dotnet/machinelearning/pull/856#discussion_r216471197","pull_request_url":"https://api.github.com/repos/dotnet/machinelearning/pulls/856","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/dotnet/machinelearning/pulls/comments/216471197"},"html":{"href":"https://github.com/dotnet/machinelearning/pull/856#discussion_r216471197"},"pull_request":{"href":"https://api.github.com/repos/dotnet/machinelearning/pulls/856"}}},"pull_request":{"url":"https://api.github.com/repos/dotnet/machinelearning/pulls/856","id":214047956,"node_id":"MDExOlB1bGxSZXF1ZXN0MjE0MDQ3OTU2","html_url":"https://github.com/dotnet/machinelearning/pull/856","diff_url":"https://github.com/dotnet/machinelearning/pull/856.diff","patch_url":"https://github.com/dotnet/machinelearning/pull/856.patch","issue_url":"https://api.github.com/repos/dotnet/machinelearning/issues/856","number":856,"state":"open","locked":false,"title":"Converted KeyToValue to estimator","user":{"login":"Zruty0","id":41337831,"node_id":"MDQ6VXNlcjQxMzM3ODMx","avatar_url":"https://avatars1.githubusercontent.com/u/41337831?v=4","gravatar_id":"","url":"https://api.github.com/users/Zruty0","html_url":"https://github.com/Zruty0","followers_url":"https://api.github.com/users/Zruty0/followers","following_url":"https://api.github.com/users/Zruty0/following{/other_user}","gists_url":"https://api.github.com/users/Zruty0/gists{/gist_id}","starred_url":"https://api.github.com/users/Zruty0/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Zruty0/subscriptions","organizations_url":"https://api.github.com/users/Zruty0/orgs","repos_url":"https://api.github.com/users/Zruty0/repos","events_url":"https://api.github.com/users/Zruty0/events{/privacy}","received_events_url":"https://api.github.com/users/Zruty0/received_events","type":"User","site_admin":false},"body":"Converted KeyToValue to estimator, added Pigsty extensions.","created_at":"2018-09-07T21:51:28Z","updated_at":"2018-09-10T21:00:17Z","closed_at":null,"merged_at":null,"merge_commit_sha":"df9229134bb2b68297fe6c19188be82b012ee57c","assignee":{"login":"Zruty0","id":41337831,"node_id":"MDQ6VXNlcjQxMzM3ODMx","avatar_url":"https://avatars1.githubusercontent.com/u/41337831?v=4","gravatar_id":"","url":"https://api.github.com/users/Zruty0","html_url":"https://github.com/Zruty0","followers_url":"https://api.github.com/users/Zruty0/followers","following_url":"https://api.github.com/users/Zruty0/following{/other_user}","gists_url":"https://api.github.com/users/Zruty0/gists{/gist_id}","starred_url":"https://api.github.com/users/Zruty0/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Zruty0/subscriptions","organizations_url":"https://api.github.com/users/Zruty0/orgs","repos_url":"https://api.github.com/users/Zruty0/repos","events_url":"https://api.github.com/users/Zruty0/events{/privacy}","received_events_url":"https://api.github.com/users/Zruty0/received_events","type":"User","site_admin":false},"assignees":[{"login":"Zruty0","id":41337831,"node_id":"MDQ6VXNlcjQxMzM3ODMx","avatar_url":"https://avatars1.githubusercontent.com/u/41337831?v=4","gravatar_id":"","url":"https://api.github.com/users/Zruty0","html_url":"https://github.com/Zruty0","followers_url":"https://api.github.com/users/Zruty0/followers","following_url":"https://api.github.com/users/Zruty0/following{/other_user}","gists_url":"https://api.github.com/users/Zruty0/gists{/gist_id}","starred_url":"https://api.github.com/users/Zruty0/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Zruty0/subscriptions","organizations_url":"https://api.github.com/users/Zruty0/orgs","repos_url":"https://api.github.com/users/Zruty0/repos","events_url":"https://api.github.com/users/Zruty0/events{/privacy}","received_events_url":"https://api.github.com/users/Zruty0/received_events","type":"User","site_admin":false}],"requested_reviewers":[{"login":"Ivanidzo4ka","id":1523833,"node_id":"MDQ6VXNlcjE1MjM4MzM=","avatar_url":"https://avatars3.githubusercontent.com/u/1523833?v=4","gravatar_id":"","url":"https://api.github.com/users/Ivanidzo4ka","html_url":"https://github.com/Ivanidzo4ka","followers_url":"https://api.github.com/users/Ivanidzo4ka/followers","following_url":"https://api.github.com/users/Ivanidzo4ka/following{/other_user}","gists_url":"https://api.github.com/users/Ivanidzo4ka/gists{/gist_id}","starred_url":"https://api.github.com/users/Ivanidzo4ka/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ivanidzo4ka/subscriptions","organizations_url":"https://api.github.com/users/Ivanidzo4ka/orgs","repos_url":"https://api.github.com/users/Ivanidzo4ka/repos","events_url":"https://api.github.com/users/Ivanidzo4ka/events{/privacy}","received_events_url":"https://api.github.com/users/Ivanidzo4ka/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[{"id":923215346,"node_id":"MDU6TGFiZWw5MjMyMTUzNDY=","url":"https://api.github.com/repos/dotnet/machinelearning/labels/api","name":"api","color":"f9d0c4","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/dotnet/machinelearning/pulls/856/commits","review_comments_url":"https://api.github.com/repos/dotnet/machinelearning/pulls/856/comments","review_comment_url":"https://api.github.com/repos/dotnet/machinelearning/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dotnet/machinelearning/issues/856/comments","statuses_url":"https://api.github.com/repos/dotnet/machinelearning/statuses/d0dd362a803ab850b7f458707286805882740057","head":{"label":"Zruty0:feature/key-to-value","ref":"feature/key-to-value","sha":"d0dd362a803ab850b7f458707286805882740057","user":{"login":"Zruty0","id":41337831,"node_id":"MDQ6VXNlcjQxMzM3ODMx","avatar_url":"https://avatars1.githubusercontent.com/u/41337831?v=4","gravatar_id":"","url":"https://api.github.com/users/Zruty0","html_url":"https://github.com/Zruty0","followers_url":"https://api.github.com/users/Zruty0/followers","following_url":"https://api.github.com/users/Zruty0/following{/other_user}","gists_url":"https://api.github.com/users/Zruty0/gists{/gist_id}","starred_url":"https://api.github.com/users/Zruty0/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Zruty0/subscriptions","organizations_url":"https://api.github.com/users/Zruty0/orgs","repos_url":"https://api.github.com/users/Zruty0/repos","events_url":"https://api.github.com/users/Zruty0/events{/privacy}","received_events_url":"https://api.github.com/users/Zruty0/received_events","type":"User","site_admin":false},"repo":{"id":141308775,"node_id":"MDEwOlJlcG9zaXRvcnkxNDEzMDg3NzU=","name":"machinelearning","full_name":"Zruty0/machinelearning","owner":{"login":"Zruty0","id":41337831,"node_id":"MDQ6VXNlcjQxMzM3ODMx","avatar_url":"https://avatars1.githubusercontent.com/u/41337831?v=4","gravatar_id":"","url":"https://api.github.com/users/Zruty0","html_url":"https://github.com/Zruty0","followers_url":"https://api.github.com/users/Zruty0/followers","following_url":"https://api.github.com/users/Zruty0/following{/other_user}","gists_url":"https://api.github.com/users/Zruty0/gists{/gist_id}","starred_url":"https://api.github.com/users/Zruty0/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Zruty0/subscriptions","organizations_url":"https://api.github.com/users/Zruty0/orgs","repos_url":"https://api.github.com/users/Zruty0/repos","events_url":"https://api.github.com/users/Zruty0/events{/privacy}","received_events_url":"https://api.github.com/users/Zruty0/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/Zruty0/machinelearning","description":"ML.NET is an open source and cross-platform machine learning framework for .NET.","fork":true,"url":"https://api.github.com/repos/Zruty0/machinelearning","forks_url":"https://api.github.com/repos/Zruty0/machinelearning/forks","keys_url":"https://api.github.com/repos/Zruty0/machinelearning/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Zruty0/machinelearning/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Zruty0/machinelearning/teams","hooks_url":"https://api.github.com/repos/Zruty0/machinelearning/hooks","issue_events_url":"https://api.github.com/repos/Zruty0/machinelearning/issues/events{/number}","events_url":"https://api.github.com/repos/Zruty0/machinelearning/events","assignees_url":"https://api.github.com/repos/Zruty0/machinelearning/assignees{/user}","branches_url":"https://api.github.com/repos/Zruty0/machinelearning/branches{/branch}","tags_url":"https://api.github.com/repos/Zruty0/machinelearning/tags","blobs_url":"https://api.github.com/repos/Zruty0/machinelearning/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Zruty0/machinelearning/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Zruty0/machinelearning/git/refs{/sha}","trees_url":"https://api.github.com/repos/Zruty0/machinelearning/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Zruty0/machinelearning/statuses/{sha}","languages_url":"https://api.github.com/repos/Zruty0/machinelearning/languages","stargazers_url":"https://api.github.com/repos/Zruty0/machinelearning/stargazers","contributors_url":"https://api.github.com/repos/Zruty0/machinelearning/contributors","subscribers_url":"https://api.github.com/repos/Zruty0/machinelearning/subscribers","subscription_url":"https://api.github.com/repos/Zruty0/machinelearning/subscription","commits_url":"https://api.github.com/repos/Zruty0/machinelearning/commits{/sha}","git_commits_url":"https://api.github.com/repos/Zruty0/machinelearning/git/commits{/sha}","comments_url":"https://api.github.com/repos/Zruty0/machinelearning/comments{/number}","issue_comment_url":"https://api.github.com/repos/Zruty0/machinelearning/issues/comments{/number}","contents_url":"https://api.github.com/repos/Zruty0/machinelearning/contents/{+path}","compare_url":"https://api.github.com/repos/Zruty0/machinelearning/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Zruty0/machinelearning/merges","archive_url":"https://api.github.com/repos/Zruty0/machinelearning/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Zruty0/machinelearning/downloads","issues_url":"https://api.github.com/repos/Zruty0/machinelearning/issues{/number}","pulls_url":"https://api.github.com/repos/Zruty0/machinelearning/pulls{/number}","milestones_url":"https://api.github.com/repos/Zruty0/machinelearning/milestones{/number}","notifications_url":"https://api.github.com/repos/Zruty0/machinelearning/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Zruty0/machinelearning/labels{/name}","releases_url":"https://api.github.com/repos/Zruty0/machinelearning/releases{/id}","deployments_url":"https://api.github.com/repos/Zruty0/machinelearning/deployments","created_at":"2018-07-17T15:27:29Z","updated_at":"2018-09-07T17:08:30Z","pushed_at":"2018-09-10T20:49:48Z","git_url":"git://github.com/Zruty0/machinelearning.git","ssh_url":"git@github.com:Zruty0/machinelearning.git","clone_url":"https://github.com/Zruty0/machinelearning.git","svn_url":"https://github.com/Zruty0/machinelearning","homepage":"https://dot.net/ml","size":21989,"stargazers_count":0,"watchers_count":0,"language":"C#","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"dotnet:master","ref":"master","sha":"a6e3c0a79c225c84f882cb8c8c6013f070cf0e7a","user":{"login":"dotnet","id":9141961,"node_id":"MDEyOk9yZ2FuaXphdGlvbjkxNDE5NjE=","avatar_url":"https://avatars0.githubusercontent.com/u/9141961?v=4","gravatar_id":"","url":"https://api.github.com/users/dotnet","html_url":"https://github.com/dotnet","followers_url":"https://api.github.com/users/dotnet/followers","following_url":"https://api.github.com/users/dotnet/following{/other_user}","gists_url":"https://api.github.com/users/dotnet/gists{/gist_id}","starred_url":"https://api.github.com/users/dotnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dotnet/subscriptions","organizations_url":"https://api.github.com/users/dotnet/orgs","repos_url":"https://api.github.com/users/dotnet/repos","events_url":"https://api.github.com/users/dotnet/events{/privacy}","received_events_url":"https://api.github.com/users/dotnet/received_events","type":"Organization","site_admin":false},"repo":{"id":132021166,"node_id":"MDEwOlJlcG9zaXRvcnkxMzIwMjExNjY=","name":"machinelearning","full_name":"dotnet/machinelearning","owner":{"login":"dotnet","id":9141961,"node_id":"MDEyOk9yZ2FuaXphdGlvbjkxNDE5NjE=","avatar_url":"https://avatars0.githubusercontent.com/u/9141961?v=4","gravatar_id":"","url":"https://api.github.com/users/dotnet","html_url":"https://github.com/dotnet","followers_url":"https://api.github.com/users/dotnet/followers","following_url":"https://api.github.com/users/dotnet/following{/other_user}","gists_url":"https://api.github.com/users/dotnet/gists{/gist_id}","starred_url":"https://api.github.com/users/dotnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dotnet/subscriptions","organizations_url":"https://api.github.com/users/dotnet/orgs","repos_url":"https://api.github.com/users/dotnet/repos","events_url":"https://api.github.com/users/dotnet/events{/privacy}","received_events_url":"https://api.github.com/users/dotnet/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dotnet/machinelearning","description":"ML.NET is an open source and cross-platform machine learning framework for .NET.","fork":false,"url":"https://api.github.com/repos/dotnet/machinelearning","forks_url":"https://api.github.com/repos/dotnet/machinelearning/forks","keys_url":"https://api.github.com/repos/dotnet/machinelearning/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dotnet/machinelearning/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dotnet/machinelearning/teams","hooks_url":"https://api.github.com/repos/dotnet/machinelearning/hooks","issue_events_url":"https://api.github.com/repos/dotnet/machinelearning/issues/events{/number}","events_url":"https://api.github.com/repos/dotnet/machinelearning/events","assignees_url":"https://api.github.com/repos/dotnet/machinelearning/assignees{/user}","branches_url":"https://api.github.com/repos/dotnet/machinelearning/branches{/branch}","tags_url":"https://api.github.com/repos/dotnet/machinelearning/tags","blobs_url":"https://api.github.com/repos/dotnet/machinelearning/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dotnet/machinelearning/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dotnet/machinelearning/git/refs{/sha}","trees_url":"https://api.github.com/repos/dotnet/machinelearning/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dotnet/machinelearning/statuses/{sha}","languages_url":"https://api.github.com/repos/dotnet/machinelearning/languages","stargazers_url":"https://api.github.com/repos/dotnet/machinelearning/stargazers","contributors_url":"https://api.github.com/repos/dotnet/machinelearning/contributors","subscribers_url":"https://api.github.com/repos/dotnet/machinelearning/subscribers","subscription_url":"https://api.github.com/repos/dotnet/machinelearning/subscription","commits_url":"https://api.github.com/repos/dotnet/machinelearning/commits{/sha}","git_commits_url":"https://api.github.com/repos/dotnet/machinelearning/git/commits{/sha}","comments_url":"https://api.github.com/repos/dotnet/machinelearning/comments{/number}","issue_comment_url":"https://api.github.com/repos/dotnet/machinelearning/issues/comments{/number}","contents_url":"https://api.github.com/repos/dotnet/machinelearning/contents/{+path}","compare_url":"https://api.github.com/repos/dotnet/machinelearning/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dotnet/machinelearning/merges","archive_url":"https://api.github.com/repos/dotnet/machinelearning/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dotnet/machinelearning/downloads","issues_url":"https://api.github.com/repos/dotnet/machinelearning/issues{/number}","pulls_url":"https://api.github.com/repos/dotnet/machinelearning/pulls{/number}","milestones_url":"https://api.github.com/repos/dotnet/machinelearning/milestones{/number}","notifications_url":"https://api.github.com/repos/dotnet/machinelearning/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dotnet/machinelearning/labels{/name}","releases_url":"https://api.github.com/repos/dotnet/machinelearning/releases{/id}","deployments_url":"https://api.github.com/repos/dotnet/machinelearning/deployments","created_at":"2018-05-03T16:20:42Z","updated_at":"2018-09-10T19:35:24Z","pushed_at":"2018-09-10T20:54:03Z","git_url":"git://github.com/dotnet/machinelearning.git","ssh_url":"git@github.com:dotnet/machinelearning.git","clone_url":"https://github.com/dotnet/machinelearning.git","svn_url":"https://github.com/dotnet/machinelearning","homepage":"https://dot.net/ml","size":21848,"stargazers_count":3569,"watchers_count":3569,"language":"C#","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":556,"mirror_url":null,"archived":false,"open_issues_count":296,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":556,"open_issues":296,"watchers":3569,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dotnet/machinelearning/pulls/856"},"html":{"href":"https://github.com/dotnet/machinelearning/pull/856"},"issue":{"href":"https://api.github.com/repos/dotnet/machinelearning/issues/856"},"comments":{"href":"https://api.github.com/repos/dotnet/machinelearning/issues/856/comments"},"review_comments":{"href":"https://api.github.com/repos/dotnet/machinelearning/pulls/856/comments"},"review_comment":{"href":"https://api.github.com/repos/dotnet/machinelearning/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dotnet/machinelearning/pulls/856/commits"},"statuses":{"href":"https://api.github.com/repos/dotnet/machinelearning/statuses/d0dd362a803ab850b7f458707286805882740057"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"TomFinley"}}