Simple Report Data Class
The Report#gpas method is utilizing the tap method. This feels like too much magic for something so simple (Related: Should I Tap That Hash). It took me a bit to unravel what that method does, which is essentially:

def gpas
  {
    average: student_terms.average :term_gpa,
    minimum: student_terms.minimum :term_gpa,
    maximum: student_terms.maximum :term_gpa
  }
end


This looks much cleaner and immediately easy to understand -- and it's the same number of lines of code.

I haven't dealt with Ruby much in a few years, but prior to Ruby 2.2 the String#to_sym method was a source of memory leaks, because symbol objects never get garbage collected, and the to_sym method always returned a new Symbol object. The symbolize_hash method could be just such a memory leak. I'm actually wondering why you aren't using classes here instead of a Hash. You are creating these Hashes and then manipulating their data. This feels like you need some additional classes to push this behavior into.

There's also no benefit to checking for @year_term_ids.empty? before adding the "where" condition in load_year_terms. Just a simple:

def load_year_terms
  YearTerm.includes(student_year_terms: [ :student ]).where(id: @year_term_ids)
end


will suffice. In fact this feels like it should be a static method on the YearTerm class itself:

class YearTerm &lt; ActiveRecord::Base
  def self.including_student_for(year_term_ids)
    YearTerm.includes(student_year_terms: [ :student ]).where(id: @year_term_ids)
  end
end


Then just call YearTerm.including_student_for(@year_term_ids) in your Report class.