Repository: CitrineInformatics/lolo (language: python)

Issue:
Make `CategoricalEncoder` serializable
Use `.map` instead of `.mapValues` to make `decoding` field in `CategoricalEncoder` serializable.

Fixes #237
CategoricalEncoder is not serializable
`CategoricalEncoder` is not serializable in Scala 2.12 since `mapValues` produces Map instance that is not serializable (see https://github.com/scala/bug/issues/7005).


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index eae4fb47..ed2efeff 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -24,7 +24,7 @@ jobs:
   include:
   - stage: test
     script:
-    - sbt +test
+    - sbt -sbt-launch-repo https://repo1.maven.org/maven2 +test
   - stage: test
     before_install:
       - - wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh
@@ -44,7 +44,7 @@ jobs:
     - nosetests
   - stage: release
     name: maven central
-    script: sbt ci-release
+    script: sbt -sbt-launch-repo https://repo1.maven.org/maven2 ci-release
   - stage: release
     name: pypi
     addons:
@@ -74,4 +74,4 @@ jobs:
         secure: fb/NiAH7E1zA2iRUT+NMywH7dh0HF3qLM/JhjWLQWQ8l/pxOy7HPgk1CDEkZip/4emH2ur5+bGAD0pm0ewfDWfvjTPA+woV5IEebK2KL3Gm4Oam4fZP27NZ5zfYs6Q1dx3YPjRUgI3gtJ+0Y1tVKvZrUaZHityDMJDuEsYjdmPnFsA6s6U/5GYeQfWaYjZcflqYd01H8K9kbCeBsRBlOUm1lCXmt13R4uBOoMNAkuR7knYUOE1VM6VJWTN2T0iKphS9agxPHh/9/B9gjCQExhynOWSs5E2WhhDnWUoAJgyaZdxZQOxT2jJKA4dJGMCtyQScWItjsglcpthd+DsNtx/0vF1fDV3tvdjRVfegnhcF9fMUeI30O5jGTqNhimkiQsV1L+Sn6LFVAJKZ2yGzjqyS+8wn/uGOLQ04R46aw7KcfDJthzPU+dx0W78scTnkOnOx0R6eLH/HBn619h415JSsYpQ/D4A2VzODhHfScgzEe9xoL61ArhYz/wAqEAvit8WbFs4D7ZVnBc/98wsBD2NKUlmTZfkjPDAZQr49S1Na6UKlK6p4gLkXE6hfwgkFwxLiaQxOMfg0u/7VbbJzfrB2568HerS/oilDq+Bp+jNKUwyo08P0qD+8a4Gif1uk1UcipixDWbCvNlfr1H7+2FrN0+b92Xmfjd+CTMKmfv7o=
       distributions: sdist bdist_wheel
       on:
-        tags: true
\ No newline at end of file
+        tags: true
diff --git a/python/Makefile b/python/Makefile
index 03f911ca..8a58363e 100644
--- a/python/Makefile
+++ b/python/Makefile
@@ -1,3 +1,3 @@
 all:
-	cd ..; sbt assembly
+	cd ..; sbt -sbt-launch-repo https://repo1.maven.org/maven2 assembly
 	pip install -e .
diff --git a/src/main/scala/io/citrine/lolo/encoders/CategoricalEncoder.scala b/src/main/scala/io/citrine/lolo/encoders/CategoricalEncoder.scala
index a0bda2ed..d535271b 100644
--- a/src/main/scala/io/citrine/lolo/encoders/CategoricalEncoder.scala
+++ b/src/main/scala/io/citrine/lolo/encoders/CategoricalEncoder.scala
@@ -10,7 +10,9 @@ package io.citrine.lolo.encoders
 class CategoricalEncoder[T](encoding: Map[T, Char]) extends Serializable {
 
   /** Inverse of the encoding */
-  lazy val decoding: Map[Char, T] = encoding.groupBy(_._2).mapValues(_.keys.head).toMap
+  lazy val decoding: Map[Char, T] = encoding
+    .groupBy { case (_, value) => value }
+    .map { case (key, value) => key -> value.keys.head }
 
   /**
     * Just call the encoding.  Use 0 for unknown inputs


Test patch:
diff --git a/src/test/scala/io/citrine/lolo/encoders/CategoricalEncoderTest.scala b/src/test/scala/io/citrine/lolo/encoders/CategoricalEncoderTest.scala
index 6ca83bf2..76f6f7de 100644
--- a/src/test/scala/io/citrine/lolo/encoders/CategoricalEncoderTest.scala
+++ b/src/test/scala/io/citrine/lolo/encoders/CategoricalEncoderTest.scala
@@ -1,6 +1,8 @@
 package io.citrine.lolo.encoders
 
-import org.junit.Test
+import org.junit.{ Assert, Test }
+
+import java.io._
 
 /**
   * Created by maxhutch on 12/1/16.
@@ -15,13 +17,50 @@ class CategoricalEncoderTest {
   def testStringEncoder(): Unit = {
     val inputs = Seq("foo", "bar", "foobar")
     val encoder = CategoricalEncoder.buildEncoder(inputs)
-    val testValues = inputs ++ Seq("barfoo")
+    assertEncode(encoder, inputs ++ Seq("barfoo"))
+    Assert.assertEquals(0, encoder.encode("barfoo"))
+  }
+
+  @Test
+  def testSerializable(): Unit = {
+    val inputs = Seq("foo", "bar", "foobar")
+    val encoder: CategoricalEncoder[String] = CategoricalEncoder.buildEncoder(inputs)
+    for (k <- encoder.decoding.keySet) {
+      encoder.decode(k)
+    }
+    val deserialized = deserialize(serialize(encoder))
+    assertEncode(deserialized, inputs)
+  }
+
+  private def assertEncode(encoder: CategoricalEncoder[String], testValues: Seq[String]): Unit = {
     for (test <- testValues) {
-      assert(encoder.encode(test) == encoder.encode(test))
+      Assert.assertEquals(encoder.encode(test), encoder.encode(test))
+    }
+  }
+
+  private def serialize(encoder: CategoricalEncoder[String]): Array[Byte] = {
+    val outputStream = new ByteArrayOutputStream(512)
+    val out = new ObjectOutputStream(outputStream)
+    try {
+      out.writeObject(encoder)
+      outputStream.toByteArray
+    } catch {
+      case _: IOException => throw new AssertionError("cannot serialize")
+    } finally {
+      if (out != null) out.close()
     }
-    assert(encoder.encode("barfoo") == 0)
   }
 
+  private def deserialize(bytes: Array[Byte]): CategoricalEncoder[String] = {
+    val in = new ObjectInputStream(new ByteArrayInputStream(bytes))
+    try {
+      in.readObject().asInstanceOf[CategoricalEncoder[String]]
+    } catch {
+      case _: IOException => throw new AssertionError("cannot deserialize")
+    } finally {
+      if (in != null) in.close()
+    }
+  }
 }
 
 /** Companion driver */
@@ -33,5 +72,6 @@ object CategoricalEncoderTest {
     */
   def main(argv: Array[String]): Unit = {
     new CategoricalEncoderTest().testStringEncoder()
+    new CategoricalEncoderTest().testSerializable()
   }
 }