Frekwenza: Another Ruby TF-IDF Gem

One of the personal projects I was working on a few months ago has text processing in it. I needed to classify text messages into several groups depending on what they’re talking about.

The problem was there are a lot of noises in the messages, not to mention typos and slangs. I figured that I need to clean the noises from the text before I continue to the processing part, since the noises disrupted the classification.

Ruby-Tf-Idf

Since I was working with Ruby, I looked for Ruby gems to calculate TF-IDF so I can use that to measure the relevancy of a word in the text and drop the irrelevant ones. I decided to use Ruby-Tf-Idf as it’s designed with a workflow that covers my need and the documentation is quite clear.

While I think Mathieu Ripert did a good job on Ruby-Tf-Idf (otherwise I wouldn’t have used it in my project), I find several things that I’d like to change.

1. Bug on string downcasing

In split_docs method, downcase! is used instead of downcase. This causes the method to raise an error on cases when the downcased string is already using lower case characters, as downcase! will return nil and gsub is called on the nil value.

See the code snippet below, taken from Ruby-Tf-Idf’s ruby-tf-idf.rb on line 95-105.

def split_docs(docs)

  splitted_docs = []
  docs.each do |d|
    begin
      splitted_docs << d.downcase!.gsub(/,|\.|\'/,'').split(/\s+/)
    rescue
    end
  end
  splitted_docs
end

The raised error will be rescued by the rescue block, but then the value of splitted_docs will not be appended. This causes the method to return a wrong result.

2. Hard-coded stop words only supports English and French

I was working with text documents written in Indonesian. It wasn’t very convenient since I have to add Indonesian stop words to the source code, rebuild the gem, and replacing the one I installed straight from RubyGems to have Indonesian language support.

If I choose to use the stop words, Ruby-Tf-Idf will proceed to use both the English and French stop words too. So this is how I initialize the object when I don’t want to use any stop words.

t = RubyTfIdf::TfIdf.new corpus, limit, false

And here’s when I want to use stop words. I can’t specify which set of stop words to use.

t = RubyTfIdf::TfIdf.new corpus, limit, true

I submitted a pull request for fixing the downcasing bug and adding a list of Indonesian stop words. But as the project goes, I have my third wish.

3. Changing stop words requires modification of gem code

After working on the project for a while, I realized that I have to compare the results when using two different sets of Indonesian stop words. That means I need to keep two versions of Ruby-Tf-Idf gem in my machine, one for the first set of stop words and one for the other set.

I decided that I need to build something that can accept stop words from sources other than hard-coded variables in the gem’s code.

Frekwenza

Not long after I finished the project, I started building Frekwenza based on Ruby-Tf-Idf’s code. I wanted something that’s pretty close to Ruby-Tf-Idf, but with some changes those aren’t compatible with Ruby-Tf-Idf. So I wrote it under a different project, and gave it a different name.

Starting with the split_docs method, the following snippet is the method in Frekwenza.

def split_docs(docs)
  words = []
  docs.each do |d|
     words << d.downcase.gsub(/[^a-z0-9]/, ' ').split(' ')
  end
  words
end

Besides the switch from downcase! to downcase method, I also modified the regex used for gsub. On Frekwenza, any non-alphanumeric characters will be substituted with a space before the string is split.

And more importantly, with Frekwenza, we can pass the path of a file containing our list of stop words for it to use.

t = Frekwenza::TfIdf.new corpus, limit, "stop_words.txt"

If we’ve loaded the file beforehand and we have the list of stop words in the form of an array of string, we can simply pass the array instead of the file location.

t = Frekwenza::TfIdf.new corpus, limit, ["some", "stop", "words"]

Of course we’re free to not use any stop words too, as we can do with Ruby-Tf-Idf.

t = Frekwenza::TfIdf.new corpus, limit

Since the object initialization is a bit different, it won’t work right away if you just switched from Ruby-Tf-Idf to Frekwenza. You need to make several adjustments in the parameters, and you need to keep Ruby-Tf-Idf’s hard-coded stop words you’ve been using somewhere else and pass it to Frekwenza.

Conclusion

While Ruby-Tf-Idf is quite well-documented, easy to use, and seems to be designed to use in a use-case similar to mine, it doesn’t offer a lot of flexibility.

We can take only the relevant part of the source code and modify it for our own uses when we’re doing simple experiments. But it’s better to have it as a flexible component when using it as a library in a software project.

Think of Frekwenza as an upgrade for Ruby-Tf-Idf that gives better flexibility, yet doesn’t provide a good backward compatibility.