feat: Update cleaner and uploader to load gender data from parent surveys

Implement graph views for parent gender data
This commit is contained in:
rebuilt 2025-05-23 16:35:12 -07:00
parent 7ebe8234cb
commit 9d8543afae
16 changed files with 262 additions and 98 deletions

View file

@ -3,4 +3,5 @@ class Parent < ApplicationRecord
has_many :parent_languages
has_and_belongs_to_many :languages, join_table: :parent_languages
has_and_belongs_to_many :races, join_table: :parent_races
has_and_belongs_to_many :genders, join_table: :parent_genders
end

View file

@ -57,9 +57,18 @@ class SurveyItemResponse < ActiveRecord::Base
else
race.map(&:id)
end
SurveyItemResponse.joins("JOIN parent_races on survey_item_responses.parent_id = parent_races.parent_id JOIN parents on parents.id = parent_races.parent_id").where(
school:, academic_year:, survey_item: survey_items
).where("parent_races.race_id": id).group(:survey_item).having("count(*) >= 10").average(:likert_score)
SurveyItemResponse.joins([parent: :races]).where(races: { id: }, survey_item: survey_items, school:, academic_year:).group(:survey_item).having("count(*) >= 10").average(:likert_score)
}
scope :averages_for_parent_gender, lambda { |survey_items, school, academic_year, gender|
id = if gender.instance_of? ::Gender
gender.id
else
gender.map(&:id)
end
SurveyItemResponse.joins([parent: :genders]).where(genders: { id: }, survey_item: survey_items, school:, academic_year:).group(:survey_item).having("count(*) >= 10").where.not(parent: nil).average(:likert_score)
}
scope :averages_for_language, lambda { |survey_items, school, academic_year, designations|

View file

@ -0,0 +1,78 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Parent
class Base
def label
raise NotImplementedError
end
def basis
raise NotImplementedError
end
def show_irrelevancy_message?(construct:)
raise NotImplementedError
end
def show_insufficient_data_message?(construct:, school:, academic_years:)
raise NotImplementedError
end
def insufficiency_message
raise NotImplementedError
end
def score(construct:, school:, academic_year:)
raise NotImplementedError
end
def type
raise NotImplementedError
end
def n_size(construct:, school:, academic_year:)
raise NotImplementedError
end
def show_insufficient_data_message?(construct:, school:, academic_years:)
false
end
def basis
"parent surveys"
end
def type
:parent
end
def bubble_up_averages(construct:, averages:)
name = construct.class.name.downcase
send("#{name}_bubble_up_averages", construct:, averages:)
end
def measure_bubble_up_averages(construct:, averages:)
construct.parent_scales.map do |scale|
scale_bubble_up_averages(construct: scale, averages:)
end.remove_blanks.average
end
def scale_bubble_up_averages(construct:, averages:)
construct.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end
def show_irrelevancy_message?(construct:)
return false if @show_irrelevancy_message == false
construct.survey_items.parent_survey_items.count.zero?
end
end
end
end
end
end

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Parent
class Gender < Base
attr_reader :genders, :label
def initialize(genders:, label:, show_irrelevancy_message:)
@genders = genders
@label = label
@show_irrelevancy_message = show_irrelevancy_message
end
def n_size(construct:, school:, academic_year:)
id = if genders.instance_of? ::Gender
genders.id
else
genders.map(&:id)
end
SurveyItemResponse.joins([parent: :genders]).where(genders: { id: }, survey_item: construct.parent_survey_items, school:, academic_year:).select(:parent_id).distinct.count
end
def score(construct:, school:, academic_year:)
return Score::NIL_SCORE if n_size(construct:, school:, academic_year:) < 10
averages = SurveyItemResponse.averages_for_parent_gender(construct.parent_survey_items, school, academic_year, genders)
average = bubble_up_averages(construct:, averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold: true,
meets_admin_data_threshold: false)
end
end
end
end
end
end

View file

@ -4,7 +4,7 @@ module Analyze
module Graph
module Column
module Parent
class Language < ColumnBase
class Language < Base
attr_reader :language, :label
def initialize(languages:, label:, show_irrelevancy_message:)
@ -13,18 +13,6 @@ module Analyze
@show_irrelevancy_message = show_irrelevancy_message
end
def basis
"parent surveys"
end
def show_insufficient_data_message?(construct:, school:, academic_years:)
false
end
def type
:parent
end
def n_size(construct:, school:, academic_year:)
SurveyItemResponse.joins([parent: :languages]).where(languages: { designation: designations }, survey_item: construct.parent_survey_items, school:, academic_year:).select(:parent_id).distinct.count
end
@ -44,29 +32,6 @@ module Analyze
def designations
language.map(&:designation)
end
def bubble_up_averages(construct:, averages:)
name = construct.class.name.downcase
send("#{name}_bubble_up_averages", construct:, averages:)
end
def measure_bubble_up_averages(construct:, averages:)
construct.parent_scales.map do |scale|
scale_bubble_up_averages(construct: scale, averages:)
end.remove_blanks.average
end
def scale_bubble_up_averages(construct:, averages:)
construct.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end
def show_irrelevancy_message?(construct:)
return false if @show_irrelevancy_message == false
construct.survey_items.parent_survey_items.count.zero?
end
end
end
end

View file

@ -4,7 +4,7 @@ module Analyze
module Graph
module Column
module Parent
class Race < ColumnBase
class Race < Base
attr_reader :race, :label
def initialize(races:, label:, show_irrelevancy_message:)
@ -13,18 +13,6 @@ module Analyze
@show_irrelevancy_message = show_irrelevancy_message
end
def basis
"parent surveys"
end
def show_insufficient_data_message?(construct:, school:, academic_years:)
false
end
def type
:parent
end
def n_size(construct:, school:, academic_year:)
designation = if race.instance_of? ::Race
race.designation
@ -44,29 +32,6 @@ module Analyze
meets_student_threshold: true,
meets_admin_data_threshold: false)
end
def bubble_up_averages(construct:, averages:)
name = construct.class.name.downcase
send("#{name}_bubble_up_averages", construct:, averages:)
end
def measure_bubble_up_averages(construct:, averages:)
construct.parent_scales.map do |scale|
scale_bubble_up_averages(construct: scale, averages:)
end.remove_blanks.average
end
def scale_bubble_up_averages(construct:, averages:)
construct.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end
def show_irrelevancy_message?(construct:)
return false if @show_irrelevancy_message == false
construct.survey_items.parent_survey_items.count.zero?
end
end
end
end

View file

@ -0,0 +1,42 @@
# frozen_string_literal: true
module Analyze
module Graph
class ParentsByGender
def to_s
"Parents by Gender"
end
def slug
"parents-by-gender"
end
def columns
[].tap do |array|
Gender.all.each do |gender|
label = if gender.designation.match(/\or\s/i)
[gender.designation.split("or").first.squish]
else
gender.designation.split(" ", 2).compact
end
array << Analyze::Graph::Column::Parent::Gender.new(genders: gender, label:, show_irrelevancy_message: false)
end
array << Analyze::Graph::Column::Parent::Gender.new(genders: Gender.all, label: ["All Parent"], show_irrelevancy_message: true)
end
end
def source
Analyze::Source::SurveyData.new(slices: nil, graph: self)
end
def slice
Analyze::Slice::ParentsByGroup.new(graph: self)
end
def group
Analyze::Group::Base.new(name: "Student Gender", slug: "student-gender", graph: self)
end
end
end
end

View file

@ -166,7 +166,8 @@ module Analyze
"students-by-sped" => Analyze::Graph::StudentsBySped.new(speds: selected_speds),
"students-by-ell" => Analyze::Graph::StudentsByEll.new(ells: selected_ells),
"parents-by-race" => Analyze::Graph::ParentsByRace.new,
"parents-by-language" => Analyze::Graph::ParentsByLanguage.new }
"parents-by-language" => Analyze::Graph::ParentsByLanguage.new,
"parents-by-gender" => Analyze::Graph::ParentsByGender.new }
end
# The last item will per slice type will be selected as the default slice
@ -183,7 +184,8 @@ module Analyze
"students-by-sped" => nil,
"students-by-ell" => nil,
"parents-by-race" => Analyze::Graph::ParentsByRace.new,
"parents-by-language" => Analyze::Graph::ParentsByLanguage.new }
"parents-by-language" => Analyze::Graph::ParentsByLanguage.new,
"parents-by-gender" => Analyze::Graph::ParentsByGender.new }
end
def graphs

View file

@ -79,7 +79,7 @@ class Cleaner
headers = headers.to_set
headers = headers.merge(Set.new(["Raw Income", "Income", "Raw ELL", "ELL", "Raw SpEd", "SpEd", "Progress Count",
"Race", "Gender", "Raw Housing Status", "Housing Status", "Home Language", "Home Languages", "Declared Races of Children from Parents"])).to_a
"Race", "Gender", "Raw Housing Status", "Housing Status", "Home Language", "Home Languages", "Declared Races of Children from Parents", "Declared Genders of Children from Parents"])).to_a
filtered_headers = include_all_headers(headers:)
filtered_headers = remove_unwanted_headers(headers: filtered_headers)
log_headers = (filtered_headers + ["Valid Duration?", "Valid Progress?", "Valid Grade?",

View file

@ -31,6 +31,7 @@ class SurveyItemValues
row["Housing Status"] = housing
row["Home Languages"] = languages.join(",")
row["Declared Races of Children from Parents"] = races_of_children.join(",")
row["Declared Genders of Children from Parents"] = genders_of_children.join(",")
end
def normalize_headers(headers:)
@ -146,6 +147,25 @@ class SurveyItemValues
end
end
def genders_of_children
@genders_of_children ||= [].tap do |gender_codes|
matches = headers.select do |header|
# Explanation:
# ^: Start of the string.
# (?!.*text): Negative lookahead — ensures that the word text does not appear anywhere in the string.
# .*?: Lazily match any characters (to get to the word gender).
# gender: Match the word gender
header.match(/^(?!.*text).*?gender/i)
end
matches.each do |match|
code = row[match]&.strip
gender_codes << Gender.qualtrics_code_from(code).to_i unless code.nil?
end
gender_codes << 99 if gender_codes.empty?
end.uniq.sort
end
def races
@races ||= begin
race_codes ||= self_report = value_from(pattern: /Race\s*self\s*report/i)

View file

@ -118,6 +118,10 @@ class SurveyResponsesDataLoader
tmp_races = row.races_of_children.map { |race| races[race] }.reject(&:nil?)
parent.races.concat(tmp_races)
parent.genders.delete_all
tmp_genders = row.genders_of_children.map { |race| genders[race] }.reject(&:nil?)
parent.genders.concat(tmp_genders)
parent.housing = housings[row.housing] if row.housing.present?
parent.save
end