scala объединяет кортежи, используя нечеткое сопоставление строк

Вход:

val input = List((a, 10 Inches), (a, 10.00 inches), (a, 15 in), (b, 2 cm), (b, 2.00 CM))

Мне нравится иметь выход

val output = List((a, 10 Inches, 0.66), (b, 2 cm, 1))

У меня также есть служебная функция, которая возвращает true для нечеткого соответствия ("10 дюймов", "10,00 дюймов").

fuzzyMatch(s1, s2) returns

true for s1 = "10 Inches" and s2 = "10.00 inches"
false for s1 = "10 Inches" and s2 = "15 in"
false for s1 = "10.00 inches" and s2 = "15 in"
true for s1 = "2 cm" and s2 = "2.00 CM"

Output = List of (unique_name, max occurred string value, (max number of occurrences/total occurrences))

Как я могу уменьшить этот ввод выше для вывода

Что у меня есть до сих пор

val tupleMap = input.groupBy(identity).mapValues(_.size)
val totalOccurrences = input.groupBy(_._1).mapValues(_.size)
val maxNumberOfValueOccurrences = tupleMap.groupBy(_._1._1).mapValues(_.values.max)
val processedInput = tupleMap
      .filter {
        case (k, v) => v == maxNumberOfValueOccurrences(k._1)
      }
      .map {
        case (k, v) => (k._1, k._2, v.toDouble / totalOccurrences(k._1))
      }.toSeq

который дает отношения для точных совпадений. Как я могу поместить туда свое нечеткое совпадение, чтобы оно группировало все похожие значения и вычисляло отношение? Нечеткое сопоставленное значение может быть любым совпадением.

По сути, это пользовательская группа, созданная с помощью моего метода fuzzyMatch(...). Но я не могу придумать решение здесь.

Еще немного подумав, я получил что-то вроде ниже. Буду признателен за лучшие решения.

val tupleMap: Map[String, Seq[String]] = input.groupBy(_._1).mapValues(_.map(_._2))

val result = tupleMap mapValues {
list =>
val valueCountsMap: mutable.Map[String, Int] = mutable.Map[String, Int]()

list foreach {
  value =>
    // Using fuzzy match to find the best match
    // findBestMatch (uses fuzzyMatch) returns the Option(key) 
    // if there exists a similar key, if not returns None
    val bestMatch = findBestMatch(value, valueCountsMap.keySet.toSeq) 
    if (bestMatch.isDefined) {
      val newValueCount = valueCountsMap.getOrElse(bestMatch.get, 0) + 1
      valueCountsMap(bestMatch.get) = newValueCount
    } else {
      valueCountsMap(value) = 1
    }
}

val maxOccurredValueNCount: (String, Int) = valueCountsMap.maxBy(_._2)
(maxOccurredValueNCount._1, maxOccurredValueNCount._2)
}

person yalkris    schedule 01.02.2018    source источник
comment
Ваш код не соответствует данным вашего примера. В частности, как найти максимальное значение? И если вы уже можете извлекать числовые значения, зачем вам вообще fuzzyMatch? Просто преобразуйте строку в числовое значение и сопоставьте ее.   -  person SergGr    schedule 02.02.2018
comment
Одно из требований — найти максимальное значение с помощью fuzzyMatch. В 15 дюймах, 15,00 дюймов и 10 в fuzzyMatch говорится, что 15 дюймов и 15,00 дюймов похожи, а 10 дюймов - нет. При этом мы можем сказать, что 15 дюймов / 15,00 дюймов - это максимальное значение.   -  person yalkris    schedule 03.02.2018


Ответы (2)


Вот подход к предварительной обработке вашего input с помощью fuzzy-match, который затем будет использоваться в качестве входных данных вашим существующим кодом.

Идея состоит в том, чтобы сначала сгенерировать 2-комбинации ваших кортежей input, выполнить их нечеткое сопоставление, чтобы создать карту различных наборов, состоящую из совпадающих значений для каждого ключа, и, наконец, использовать карту для нечеткого сопоставления вашего исходного input.

Чтобы охватить больше произвольных случаев, я расширил ваш input:

val input = List(
  ("a", "10 in"), ("a", "15 in"), ("a", "10 inches"), ("a", "15 Inches"), ("a", "15.00 inches"),
  ("b", "2 cm"), ("b", "4 cm"), ("b", "2.00 CM"),
  ("c", "7 cm"), ("c", "7 in")
)

// Trivialized fuzzy match
def fuzzyMatch(s1: String, s2: String): Boolean = {
  val st1 = s1.toLowerCase.replace(".00", "").replace("inches", "in")
  val st2 = s2.toLowerCase.replace(".00", "").replace("inches", "in")
  st1 == st2
}

// Create a Map of Sets of fuzzy-matched values from all 2-combinations per key
val fuzMap = input.combinations(2).foldLeft( Map[String, Seq[Set[String]]]() ){
  case (m, Seq(t1: Tuple2[String, String], t2: Tuple2[String, String])) =>
    if (fuzzyMatch(t1._2, t2._2)) {
      val fuzSets = m.getOrElse(t1._1, Seq(Set(t1._2, t2._2))).map(
        x => if (x.contains(t1._2) || x.contains(t2._2)) x ++ Set(t1._2, t2._2) else x
      )
      if (!fuzSets.flatten.contains(t1._2) && !fuzSets.flatten.contains(t2._2))
        m + (t1._1 -> (fuzSets :+ Set(t1._2, t2._2)))
      else
        m + (t1._1 -> fuzSets)
    }
    else
      m
}
// fuzMap: scala.collection.immutable.Map[String,Seq[Set[String]]] = Map(
//   a -> List(Set(10 in, 10 inches), Set(15 in, 15 Inches, 15.00 inches)), 
//   b -> List(Set(2 cm, 2.00 CM)))
// )

Обратите внимание, что для больших input может иметь смысл сначала использовать ключ groupBy и генерировать 2 комбинации для каждого ключа.

Следующим шагом будет нечеткое сопоставление исходного ввода с использованием созданной карты:

// Fuzzy-match original input using fuzMap
val fuzInput = input.map{ case (k, v) => 
  if (fuzMap.get(k).isDefined) {
    val fuzValues = fuzMap(k).map{
      case x => if (x.contains(v)) Some(x.min) else None
    }.flatten
    if (!fuzValues.isEmpty)
      (k, fuzValues.head)
    else
      (k, v)
  }
  else
    (k, v)
}
// fuzInput: List[(String, String)] = List(
//   (a,10 in), (a,15 Inches), (a,10 in), (a,15 Inches), (a,15 Inches),
//   (b,2 cm), (b,4 cm), (b,2 cm),
//   (c,7 cm), (c,7 in)
// )
person Leo C    schedule 02.02.2018

Если по какой-то причине подход с преобразованием в числовые значения у вас не работает, вот код, который, кажется, делает то, что вы хотите:

def fuzzyMatch(s1: String, s2: String): Boolean = {
  // fake implementation
  val matches = List(("15 Inches", "15.00 inches"), ("2 cm", "2.00 CM"))
  s1.equals(s2) || matches.exists({
    case (m1, m2) => (m1.equals(s1) && m2.equals(s2)) || (m1.equals(s2) && m2.equals(s1))
  })
}

 def test(): Unit = {
  val input = List(("a", "15 Inches"), ("a", "15.00 inches"), ("a", "10 in"), ("b", "2 cm"), ("b", "2.00 CM"))
  val byKey = input.groupBy(_._1).mapValues(l => l.map(_._2))
  val totalOccurrences = byKey.mapValues(_.size)
  val maxByKey = byKey.mapValues(_.head) //random "max" selection logic

  val processedInput: List[(String, String, Double)] = maxByKey.map({
    case (mk, mv) =>
      val matchCount = byKey(mk).count(tv => fuzzyMatch(tv, mv))
      (mk, mv, matchCount / totalOccurrences(mk).asInstanceOf[Double])
  })(breakOut)

  println(processedInput)
}

Это печатает

Список ((b, 2 см, 1,0), (a, 15 дюймов, 0,6666666666666666))

person SergGr    schedule 02.02.2018
comment
Это вышеприведенное решение не будет работать для этого ниже. ) Проблема в вашей логике случайного максимального выбора. Максимальное значение должно быть основано на нечетком сопоставленном значении. Не случайно. - person yalkris; 03.02.2018
comment
@yalkris, очевидно, вы должны использовать свою настоящую логику в расчетах maxByKey. Вот почему я добавил комментарий к этой строке, что я использовал по сути логику случайного выбора вместо вашей реальной, поскольку вы не указали свою реальную в вопросе. - person SergGr; 03.02.2018
comment
Спасибо. Мое требование состоит в том, чтобы (a,15 дюймов), (a,15,00 дюймов), (a,10 дюймов) 15 дюймов встречались дважды, поэтому результат должен быть (a, 15,00 дюймов или 15 дюймов, 0,6666). Как бы вы сделали это, используя метод fuzzyMatch, который соответствует 15,00 дюймам и 15 дюймам? - person yalkris; 03.02.2018
comment
@yalkris, я прав, что когда вы начинаете с List(("a", "10 in"), ("a", "15.00 inches"), ("a", "15 Inches"), ("b", "2 cm"), ("b", "2.00 CM")), вы получаете (a,10 in,0.33333)? Если это так, проблема не в логике группировки, а в логике выбора максимального значения. Ваш вопрос подразумевает, что вы уже знаете, как выбрать максимальное значение из этих строк. Если это не так - вам следует изменить свой вопрос, потому что эта проблема не имеет ничего общего с нечетким сопоставлением. - person SergGr; 03.02.2018
comment
Нет. У меня есть только метод fuzzyMatch, который принимает две строки и возвращает true, если они похожи. Я хочу вернуть максимальное значение и его отношение к общему количеству событий. - person yalkris; 03.02.2018
comment
@yalkris, вы, очевидно, не можете вернуть максимальное значение, используя только fuzzyMatch. Нет ничего, что определяло бы порядок различных значений, так как же следует выбирать максимальные значения? - person SergGr; 03.02.2018
comment
@yalkris, у меня вопрос. Возможно, я неправильно понял ваш пример. Предположим, что начальный список List(("a", "10 in"), ("a", "10.00 inches"), ("a", "15 Inches")). Какой ответ вы ожидаете ("a", "10 in", 0.666) или ("a", "15 Inches", 0.333)? Если это первый, вы можете получить его только с fuzzyMatch. Если второй - шансов нет. - person SergGr; 03.02.2018
comment
@yalkris, извините, вы заметили, что в моем последнем комментарии пример другой? Есть два "10 in" и "10.00 inches" и только один "15 Inches"? Если ответ по-прежнему ("a", "15 Inches", 0.666), не могли бы вы описать, по какой логике должно быть рассчитано это значение? - person SergGr; 03.02.2018
comment
Мне жаль. Да. Судя по вашему мнению, я хочу ("a", "10.00 inches",0.66) - person yalkris; 03.02.2018
comment
@yalkris, ага! Ваш исходный пример сбивает с толку, что именно означает max. Это можно сделать, но мне потребуется некоторое время, чтобы обновить ответ. - person SergGr; 03.02.2018
comment
Давайте продолжим обсуждение в чате. - person yalkris; 03.02.2018