A basic decision tree in Ruby

August 23, 2019 – Jean Anquetil – 3-minute read

This article was written before Drivy was acquired by Getaround, and became Getaround EU. Some references to Drivy may therefore remain in the post

Recently, we did a rework of the user’s profile completion flow in our Drivy web and mobile applications. We went from a basic single screen form to a multi-steps one. The idea was to simplify the flow and ask only for the information needed depending on the user’s answers. As we had to deal with multiple possible paths, we decided to work on a little decision tree algorithm.

Multi steps flow
Fig 1. The new profile flow

What do we need to define our decision tree?

Let’s say that our decision tree is made up of multiple steps and for every steps there’s one or several possible answers that we will call outcomes.

Decision tree drawing
Fig 2. First step
Decision tree drawing
Fig 3. Complete flow made up of outcomes

If we know the path, which is made up of outcomes, we will be able to find the next step.

Decision tree drawing
Fig 4. The path made up of outcomes

Decision tree declaration

The idea is to have a collection where the first element is a step class name and the second one is an object composed of the possible step’s outcomes. For each step’s outcome, we define a collection where the first element is a step class name and the second one is an object…and so on.

Let’s imagine that we need to ask if the user wants to rent a car or if they own a car and want to add it on the platform.

DECISION_TREE = [
  RoleStep, {
    RoleStep::DRIVER => [
      DriverStep, {
        DriverStep::EligibleLicenseYears => [
          ...
        ],
        DriverStep::IneligibleLicenseYears => [
          ...
        ]
      }
    ],
    RoleStep::OWNER => [
      OwnerStep, {
        OwnerStep::CarHasManualTransmission => [
          ...
        ],
        OwnerStep::CarHasAutomaticTransmission => [
          ...
        ]
      }
    ]
  }
]

Step class declaration

The step class would gather its possible outcomes and everything else related to it. For instance, in almost all of our steps we had to deal with a form so this is where we defined it. We could define a #can_skip? method that could check if the step has already been filled, or could be skippable somehow. Doing this way, it becomes really convenient to define specific rules on steps.

class RoleStep
  DRIVER = :driver
  OWNER = :owner

  def self.form_class
    RoleStepForm
  end
end

Finding the next step

Once we have our decision tree declared with all of its steps, we need to build a small recursive method that will find the next step according to the path (cf figure 4).

def next_step(path: [], steps: DECISION_TREE)
  return steps if path.empty?

  next_steps = steps.last[path.shift]

  next_step(path: path, steps: Array(next_steps))
end

And here you are, you can now iterate through the DECISION_TREE, giving a path or not, to find the according next step.

puts next_step.first
# => RoleStep

puts next_step(path: [RoleStep::OWNER]).first
# => OwnerStep
Did you enjoy this post? Join Getaround's engineering team!
View openings