Eps: PMML-Backed Machine Learning Inside a Ruby Process
Machine learning for Ruby
At a glance
- What is it?
- Eps trains small predictive models from arrays of Ruby hashes and stores them as PMML, so a Rails app can serve a model built in Python or R without running a second language at request time. The trade-off is that its built-in model set is narrow and the split heuristic is simple.
- Who is it for?
- Adopt Eps if your team is already in Ruby, your feature set is tabular, and you want predictions computed in-process from a file you can commit to source control. Do not adopt it if you need deep learning, gradient boosting trained in Ruby, or a large feature matrix, since the README only claims serving support for LightGBM, linear regression, and naive Bayes, and only when the model arrives as PMML.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 79 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap Eps fills: predictions without a Python sidecar
Most Ruby applications that need a prediction end up running a second service. A Python process holds the model, the Rails app posts features to it over HTTP, and now you own a deployment, a network hop, and a serialization format on both ends. Eps takes the opposite position: the model is a file, the file is PMML, and the prediction happens inside the Ruby process that already has the data. The README frames this as serving models built elsewhere, and the gem ships examples under test/support/python and test/support/r for LightGBM, linear regression, and naive Bayes. The audience is a Rails or plain Ruby team with tabular data, a target column, and no appetite for a second runtime. It is not aimed at teams doing deep learning, nor at anyone who needs a model type the gem cannot read.
Training from an array of hashes and what the split actually does
The entry point is Eps::Model.new(data, target: :price) where data is an array of hashes and target names the column to predict. The README states the target can be numeric for regression or categorical for classification, and that the gem splits the data into training and validation sets once you have 30 or more data points. That threshold is the whole rule. Below 30 rows there is no validation split, which means model.summary has nothing held-out to report on. Above it, the split is random unless you pass split: :listed_at, in which case the named field orders the split. The README calls the time-based split highly recommended when your data has a timestamp, and that advice is worth taking literally: a random split on time-ordered rows lets the model see the future during training. Performance lands in model.summary as validation RMSE for regression (lower is better) or validation accuracy for classification (higher is better). There is no cross-validation loop described, and no hyperparameter search beyond the text feature options.
Feature typing is inferred, and the inference rules matter
Eps decides how to treat a column from its Ruby class. Numeric types are numeric features. Strings and booleans are categorical. A string with multiple words is treated as a bag of words, which the README links to the bag-of-words model. That last rule is the one that catches people: a free-text field and a single-token category look identical in Ruby, so a description column silently becomes a text feature while a state column becomes categorical. You can force the issue with text_features: [:description], and the advanced form takes a hash per field with min_occurrences, max_features, min_length, case_sensitive, tokenizer, and stop_words. The README's own guidance is to convert ids to strings so they are treated as categorical, and to derive weekday and month from dates rather than feeding raw timestamps. Those are manual steps. Eps does not parse a date into components for you, and it does not one-hot encode a numeric-looking id that you forgot to stringify. The README says plainly that feature engineering is typically the best way to improve performance, which is an admission that the built-in modelling is not where the gains live.
Putting it in a Rails app: the Base class and the file on disk
The full example defines a class inheriting from Eps::Base with a build method and a predict method, and the README recommends keeping it in app/ml_models/price_model.rb. Build reads rows, maps each to a feature hash, calls Eps::Model.new with target and split, prints the summary, and writes model.to_pmml to a file. The predict path lazily memoizes Eps::Model.load_pmml(File.read(model_file)) and calls model.predict on a single feature hash. Two operational details are easy to miss. First, the README notes that after creating app/ml_models in Rails you should run bin/spring stop so the directory is autoloaded. Second, build sets @model = nil to force a reload from the file, which implies the running process caches the model object and will not pick up a rebuilt file on its own. The README suggests checking the .pmml file into source control or storing it with a separate tool, Trove. That choice determines your deployment story: a committed model means a deploy is required to change predictions, while external storage means the process needs a reload path you write yourself.
Serving PMML from Python or R, and the consistency trap
The cross-language path is the most distinctive part of the gem. You install sklearn2pmml in Python or the pmml package in R, train there, export PMML, and load it in Ruby with Eps::Model.load_pmml. The README points at example scripts in the repository for each supported combination and states that Eps can serve LightGBM, linear regression, and naive Bayes models, directing you to ONNX Runtime and Scoruby for anything else. The constraint is that feature construction now exists in two languages. The README calls consistent feature implementation important and highly recommends verifying it programmatically, and the excerpt cuts off mid-sentence while describing that verification. This is the failure mode to plan for: a categorical encoding, a string case difference, or a different tokenizer on the Python side produces predictions that look plausible and are wrong. Nothing in the described API detects that drift for you. If you take this path, the verification is code you write, not a feature you enable.
Monitoring, and what Eps.metrics does not cover
The README's monitoring advice is to persist predictions to the database and then call Eps.metrics(actual, predicted) against the stored values. It names RMSE, MAE, ME, and accuracy, with thresholds: alert when RMSE or MAE rise, when ME drifts from zero, or when accuracy falls. That is a reasonable starting frame, but it is a comparison of two arrays. It does not schedule itself, does not store history, and does not distinguish data drift from concept drift. The ME check is the interesting one, since a mean error that moves away from zero suggests systematic bias rather than noise, and that is often the first sign that a feature pipeline changed. Note also that this monitoring assumes you have ground truth, which for a price model means waiting until the house actually sells. For targets with long feedback loops, the alert arrives late.
Where Eps is the wrong tool
Eps is a thin layer, and the thinness is the point, but it also sets hard boundaries. If your problem needs a model type outside LightGBM, linear regression, or naive Bayes, the README sends you to ONNX Runtime or Scoruby rather than promising coverage. If you want to train a gradient boosted model in Ruby itself, Eps trains its own models and serves LightGBM from PMML, which is a different thing. If your data is images, sequences, or text long enough that bag-of-words is inadequate, the feature model here will not carry you. And if you have fewer than 30 rows, there is no validation split, so model.summary cannot tell you whether the model generalizes. The honest read is that Eps is a serving and packaging tool with a small built-in trainer attached, and the packaging is the more durable half.
The alternative: keep the model in Python and call it
The obvious alternative is to leave training and inference in Python and expose the model over HTTP or a queue from a separate service. The difference in approach is where the prediction executes. With a Python service you get the full ecosystem: any model class, any preprocessing library, cross-validation helpers, and feature code that lives next to training code by construction. You pay for it with a second deployable, a network round trip per prediction, and a serialization contract you maintain. Eps removes the second process and the round trip, and in exchange you accept a narrower model set, a manual feature pipeline, and the two-language consistency problem described above. There is also a middle option the README itself mentions: serve PMML through ONNX Runtime or Scoruby if Eps cannot read your model format. That keeps the in-process prediction but changes which library does the loading.
Licence, maintenance, and what a rebuild costs
Eps is MIT licensed, which permits commercial use, modification, and redistribution provided the copyright notice and permission notice are retained. That is the standard permissive position; it is not legal advice, and if you ship the gem inside a distributed product you should read the licence text rather than this summary. On maintenance: the repository is not archived and the last push recorded here is 2026-06-29, but no recent releases were retrieved, so there is no version history in this material to reason about. The practical upgrade cost is low because the API surface shown in the README is small (Model.new, predict, to_pmml, load_pmml, Base, metrics) and the artifact is a file format rather than a live service. The real recurring cost is the rebuild: every schema change, every new feature, and every new category value means re-running build and re-committing or re-uploading the PMML. Because Eps infers feature types from Ruby classes, a column that changes from integer to string changes its meaning to the model, and nothing in the described API flags that for you.
Editorial conclusion
Adopt Eps if your team is already in Ruby, your feature set is tabular, and you want predictions computed in-process from a file you can commit to source control. Do not adopt it if you need deep learning, gradient boosting trained in Ruby, or a large feature matrix, since the README only claims serving support for LightGBM, linear regression, and naive Bayes, and only when the model arrives as PMML. Before committing, verify two things: that the PMML your training environment emits round-trips through Eps::Model.load_pmml, and that your serving-side feature code produces byte-identical values to the training-side code, because the README treats that consistency as a requirement rather than a convenience.
Community notes