Translating Data Science Logic into Web Apps: A Developer’s Guide

Written by Technically reviewed by Jim Freeze · Reviewed

Occasionally a Rails application needs to calculate something from the data in its database. This could be ranking products, summarizing sensor readings or forecasting potential sales from prior purchases. While Python is well-established in the field of data science, Ruby teams face a very practical decision – can the current application manage this requirement, or will a separate service be needed for this feature?

Ruby has mathematical functions and collection tools, but imperfect input, memory usage and how quickly customers expect a result also matter. Consider these issues when designing your application.

Leveraging Ruby’s Native Mathematical Modules

First, look at the standard Math module, which provides trigonometric and transcendental functions through calls to C library routines in CRuby. By using native functions, you don’t have to write common calculations yourself, though a native function does not guarantee that the whole algorithm will be fast.

Use Math.erf (error function), and Math.erfc (its complementary error function), for probability-related calculations. At large negative and positive values, the error function approaches -1 and +1 respectively; since these values are rounded off to the nearest float, they can sometimes appear to be exactly -1 and +1. There is also Math.frexp, which returns a fraction and a base two exponent that together represent a float.

Choose your numeric types deliberately. The Complex class allows you to define Complex numbers (numbers with real and imaginary parts), and supports basic arithmetic operations, division operations, and power operations. However, using Complex does not eliminate rounding errors if you perform operations on floating point values. Use Rational when you need exact fractional arithmetic.

Compare floating point results with an acceptable tolerance related to your problem. Even a simple calculation has an input contract. What happens when the dataset is empty? Should missing readings cause an error? Or should your application exclude them? With a numeric array named scores, scores.sum.fdiv(scores.length) calculates a floating-point mean. Before attempting to reach that expression, check for an empty array. Your determination of “no readings” is part of defining the calculation.

Data Clustering and Transformation via Enumerables

Using the Enumerable mixin, Ruby developers can apply rules to group and filter collections. group_by collects elements under a key returned by a block. partition divides elements into matching and nonmatching groups. These methods help organize data, but they do not themselves perform statistical clustering such as k-means.

Enumerable methodReturn typeAlgorithmic use case
chunk {... }EnumeratorCreates groups of consecutive elements returning the same key produced by a block; useful for runs in ordered readings.
minmax_by {... }ArrayReturns the minimum and maximum element(s) based on a scoring block; returns two nil values if the input is empty.
slice_after {... }EnumeratorEnds each group after an element satisfies a condition, such as a marker that signals the end of a batch.
tallyHashTallies how many times each distinct value appears; e.g., labels or response codes.

Grouping an entire collection differs from finding consecutive runs. Sorting the input first can change the meaning and cost of the operation. Note that group_by stores the collected elements in memory.

You can create an Enumerator::Chain to visit several enumerators sequentially without first merging them together into one array. Chain does not make all later operations lazy. Use Enumerator::Lazy for supported filtering/mapping steps that are designed to run on-demand. Storing all results in an array with to_a still requires memory to hold those results.

In Rails, first find out if the database can handle the aggregation. Active Record supports group, count, sum and average aggregations which avoid loading model objects just to get totals. If the calculation needs each record in Ruby, use find_each to fetch records in batches. Be aware of the order that find_each will traverse through the records when the algorithm depends on a particular sequence. See the Active Record querying guide for both approaches.

Architecting Algorithmic Services and Complexity

Once the calculation grows, a Plain Old Ruby Object (PORO) provides a home for the logic outside the controller and makes it easier to test. Calling a PORO from a controller still executes that work during the request.

Here is a small service object that accepts a validated numeric array and returns the count and mean:

class ReadingSummary
  def call(readings)
    return { count: 0, mean: nil } if readings.empty?

    {
      count: readings.length,
      mean: readings.sum.fdiv(readings.length)
    }
  end
end

ReadingSummary.new.call([12, 18, 24])
# => { count: 3, mean: 18.0 }

The application handles database access and validates the input; the object handles the calculation. Tests can cover empty input, a single reading and known results without invoking any controller code. More complicated statistical methods can follow the same boundary.

Check runtime using a realistic amount of input. Some calculations will produce quadratic growth when every record is compared against every other record. For graph coloring, a scheduling heuristic is different from a search for an optimal coloring. Identify the actual algorithm before assigning a complexity label.

If the calculation takes too long for a web request, Active Job allows you to enqueue jobs into your Rails application via methods such as perform_later. Once you have configured a suitable queue backend and running workers, you can save the calculation’s result so your application can retrieve it. Using jobs changes where and when the calculation is performed – it does not reduce the calculation’s CPU or memory requirements.

Domain knowledge matters too. Rijk de Wet, a developer at Omni Calculator, has a data-science background that includes building and evaluating a swarm-intelligence clustering algorithm. His calculator work covers mathematics and engineering. When developing similar tools in Ruby, the practical lesson here is to critically evaluate the assumptions of your model alongside the implementation of its results.

Conclusion

The debate about language choice remains active among developers. For a team developing a Rails application, start by understanding what your calculation needs to perform and how much computing power that calculation needs.

Ruby’s Math and Enumerable tools allow you to support useful application-level analysis. Active Record supports database aggregation which helps reduce the number of records Ruby processes. Additionally, writing custom logic inside a smaller service object makes testing your custom logic simpler. When the calculation takes too long for a request, consider a properly configured job system or a specialized numerical service. Base the choice on measured behavior, required accuracy and the tools the calculation needs.