feat: add special education disaggregation

mciea-main
rebuilt 2 years ago
parent a9b4f97a84
commit acfdaf5587

@ -30,7 +30,9 @@ export default class extends Controller {
"&grades=" + "&grades=" +
this.selected_items("grade").join(",") + this.selected_items("grade").join(",") +
"&ells=" + "&ells=" +
this.selected_items("ell").join(","); this.selected_items("ell").join(",") +
"&speds=" +
this.selected_items("sped").join(",");
this.go_to(url); this.go_to(url);
} }
@ -124,19 +126,11 @@ export default class extends Controller {
return item.id; return item.id;
})[0]; })[0];
const groups = new Map([
['gender', 'students-by-gender'],
['grade', 'students-by-grade'],
['income', 'students-by-income'],
['race', 'students-by-race'],
['ell', 'students-by-ell'],
])
if (target.name === 'slice' || target.name === 'group') { if (target.name === 'slice' || target.name === 'group') {
if (selected_slice === 'students-and-teachers') { if (selected_slice === 'students-and-teachers') {
return 'students-and-teachers'; return 'students-and-teachers';
} }
return groups.get(this.selected_group()); return `students-by-${this.selected_group()}`;
} }
return window.graph; return window.graph;

@ -0,0 +1,7 @@
class Sped < ApplicationRecord
scope :by_designation, -> { all.map { |sped| [sped.designation, sped] }.to_h }
include FriendlyId
friendly_id :designation, use: [:slugged]
end

@ -11,6 +11,7 @@ class SurveyItemResponse < ActiveRecord::Base
belongs_to :gender belongs_to :gender
belongs_to :income belongs_to :income
belongs_to :ell belongs_to :ell
belongs_to :sped
has_one :measure, through: :survey_item has_one :measure, through: :survey_item
@ -39,6 +40,11 @@ class SurveyItemResponse < ActiveRecord::Base
academic_year:, ell:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score) academic_year:, ell:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score)
} }
scope :averages_for_sped, lambda { |survey_items, school, academic_year, sped|
SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, sped:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score)
}
scope :averages_for_race, lambda { |school, academic_year, race| scope :averages_for_race, lambda { |school, academic_year, race|
SurveyItemResponse.joins("JOIN student_races on survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where( SurveyItemResponse.joins("JOIN student_races on survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where(
school:, academic_year:, grade: school.grades(academic_year:) school:, academic_year:, grade: school.grades(academic_year:)

@ -8,7 +8,7 @@ module Analyze
include Analyze::Graph::Column::EllColumn::ScoreForEll include Analyze::Graph::Column::EllColumn::ScoreForEll
include Analyze::Graph::Column::EllColumn::EllCount include Analyze::Graph::Column::EllColumn::EllCount
def label def label
%w[Not-ELL] ["Not ELL"]
end end
def basis def basis

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module SpedColumn
class NotSped < GroupedBarColumnPresenter
include Analyze::Graph::Column::SpedColumn::ScoreForSped
include Analyze::Graph::Column::SpedColumn::SpedCount
def label
["Not Special", "Education"]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def sped
::Sped.find_by_slug "not-special-education"
end
end
end
end
end
end

@ -0,0 +1,42 @@
module Analyze
module Graph
module Column
module SpedColumn
module ScoreForSped
def score(year_index)
academic_year = academic_years[year_index]
meets_student_threshold = sufficient_student_responses?(academic_year:)
return Score::NIL_SCORE unless meets_student_threshold
averages = SurveyItemResponse.averages_for_sped(measure.student_survey_items, school, academic_year,
sped)
average = bubble_up_averages(averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end
def bubble_up_averages(averages:)
measure.student_scales.map do |scale|
scale.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end.remove_blanks.average
end
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
yearly_counts = SurveyItemResponse.where(school:, academic_year:,
sped:, survey_item: measure.student_survey_items).group(:sped).select(:response_id).distinct(:response_id).count
yearly_counts.any? do |count|
count[1] >= 10
end
end
end
end
end
end
end

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module SpedColumn
class Sped < GroupedBarColumnPresenter
include Analyze::Graph::Column::SpedColumn::ScoreForSped
include Analyze::Graph::Column::SpedColumn::SpedCount
def label
%w[Special Education]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def sped
::Sped.find_by_slug "special-education"
end
end
end
end
end
end

@ -0,0 +1,18 @@
module Analyze
module Graph
module Column
module SpedColumn
module SpedCount
def type
:student
end
def n_size(year_index)
SurveyItemResponse.where(sped:, survey_item: measure.student_survey_items, school:, grade: grades(year_index),
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
end
end
end
end
end

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module SpedColumn
class Unknown < GroupedBarColumnPresenter
include Analyze::Graph::Column::SpedColumn::ScoreForSped
include Analyze::Graph::Column::SpedColumn::SpedCount
def label
%w[Unknown]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def sped
::Sped.find_by_slug "unknown"
end
end
end
end
end
end

@ -1,9 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
#
module Analyze module Analyze
module Graph module Graph
class StudentsByEll class StudentsByEll
include Analyze::Graph::Column::GenderColumn include Analyze::Graph::Column::EllColumn
attr_reader :ells attr_reader :ells
def initialize(ells:) def initialize(ells:)

@ -0,0 +1,43 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsBySped
include Analyze::Graph::Column::SpedColumn
attr_reader :speds
def initialize(speds:)
@speds = speds
end
def to_s
"Students by SpEd"
end
def slug
"students-by-sped"
end
def columns
[].tap do |array|
speds.each do |sped|
array << column_for_sped_code(code: sped.slug)
end
array << Analyze::Graph::Column::AllStudent
end
end
private
def column_for_sped_code(code:)
CFR[code]
end
CFR = {
"special-education" => Analyze::Graph::Column::SpedColumn::Sped,
"not-special-education" => Analyze::Graph::Column::SpedColumn::NotSped,
"unknown" => Analyze::Graph::Column::SpedColumn::Unknown
}.freeze
end
end
end

@ -0,0 +1,13 @@
module Analyze
module Group
class Sped
def name
"SpEd"
end
def slug
"sped"
end
end
end
end

@ -67,6 +67,19 @@ module Analyze
end end
end end
def speds
@speds ||= Sped.all.order(id: :ASC)
end
def selected_speds
@selected_speds ||= begin
sped_params = params[:speds]
return speds unless sped_params
sped_params.split(",").map { |sped| Sped.find_by_slug sped }.compact
end
end
def graphs def graphs
@graphs ||= [Analyze::Graph::AllData.new, @graphs ||= [Analyze::Graph::AllData.new,
Analyze::Graph::StudentsAndTeachers.new, Analyze::Graph::StudentsAndTeachers.new,
@ -74,7 +87,8 @@ module Analyze
Analyze::Graph::StudentsByGrade.new(grades: selected_grades), Analyze::Graph::StudentsByGrade.new(grades: selected_grades),
Analyze::Graph::StudentsByGender.new(genders: selected_genders), Analyze::Graph::StudentsByGender.new(genders: selected_genders),
Analyze::Graph::StudentsByIncome.new(incomes: selected_incomes), Analyze::Graph::StudentsByIncome.new(incomes: selected_incomes),
Analyze::Graph::StudentsByEll.new(ells: selected_ells)] Analyze::Graph::StudentsByEll.new(ells: selected_ells),
Analyze::Graph::StudentsBySped.new(speds: selected_speds)]
end end
def graph def graph
@ -107,7 +121,7 @@ module Analyze
def groups def groups
@groups = [Analyze::Group::Ell.new, Analyze::Group::Gender.new, Analyze::Group::Grade.new, Analyze::Group::Income.new, @groups = [Analyze::Group::Ell.new, Analyze::Group::Gender.new, Analyze::Group::Grade.new, Analyze::Group::Income.new,
Analyze::Group::Race.new] Analyze::Group::Race.new, Analyze::Group::Sped.new]
end end
def group def group

@ -7,8 +7,9 @@ class DemographicLoader
CSV.parse(File.read(filepath), headers: true) do |row| CSV.parse(File.read(filepath), headers: true) do |row|
process_race(row:) process_race(row:)
process_gender(row:) process_gender(row:)
process_income(row:) create_from_column(column: "Income", row:, model: Income)
process_ell(row:) create_from_column(column: "ELL", row:, model: Ell)
create_from_column(column: "Special Ed Status", row:, model: Sped)
end end
end end
@ -33,18 +34,11 @@ class DemographicLoader
gender.save gender.save
end end
def self.process_income(row:) def self.create_from_column(column:, row:, model:)
designation = row["Income"] designation = row[column]
return unless designation return unless designation
Income.find_or_create_by!(designation:) model.find_or_create_by!(designation:)
end
def self.process_ell(row:)
designation = row["ELL"]
return unless designation
Ell.find_or_create_by!(designation:)
end end
end end

@ -62,6 +62,10 @@ class SurveyResponsesDataLoader
@ells ||= Ell.by_designation @ells ||= Ell.by_designation
end end
def speds
@speds ||= Sped.by_designation
end
def process_row(row:, rules:) def process_row(row:, rules:)
return unless row.dese_id? return unless row.dese_id?
return unless row.school.present? return unless row.school.present?
@ -91,12 +95,14 @@ class SurveyResponsesDataLoader
grade = row.grade grade = row.grade
income = incomes[row.income.parameterize] income = incomes[row.income.parameterize]
ell = ells[row.ell] ell = ells[row.ell]
sped = speds[row.sped]
if survey_item_response.present? if survey_item_response.present?
survey_item_response.update!(likert_score:, grade:, gender:, recorded_date: row.recorded_date, income:, ell:) survey_item_response.update!(likert_score:, grade:, gender:, recorded_date: row.recorded_date, income:, ell:,
sped:)
[] []
else else
SurveyItemResponse.new(response_id: row.response_id, academic_year: row.academic_year, school: row.school, survey_item:, SurveyItemResponse.new(response_id: row.response_id, academic_year: row.academic_year, school: row.school, survey_item:,
likert_score:, grade:, gender:, recorded_date: row.recorded_date, income:, ell:) likert_score:, grade:, gender:, recorded_date: row.recorded_date, income:, ell:, sped:)
end end
end end

@ -25,3 +25,7 @@
<% @presenter.ells.each do |ell| %> <% @presenter.ells.each do |ell| %>
<%= render(partial: "checkboxes", locals: {id: "ell-#{ell.slug}", item: ell, selected_items: @presenter.selected_ells, name: "ell", label_text: ell.designation}) %> <%= render(partial: "checkboxes", locals: {id: "ell-#{ell.slug}", item: ell, selected_items: @presenter.selected_ells, name: "ell", label_text: ell.designation}) %>
<% end %> <% end %>
<% @presenter.speds.each do |sped| %>
<%= render(partial: "checkboxes", locals: {id: "sped-#{sped.slug}", item: sped, selected_items: @presenter.selected_speds, name: "sped", label_text: sped.designation}) %>
<% end %>

@ -1,11 +1,11 @@
Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender,Income,ELL Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender,Income,ELL,Special Ed Status
1,American Indian or Alaskan Native,2,Male,Economically Disadvantaged - N,ELL 1,American Indian or Alaskan Native,2,Male,Economically Disadvantaged - N,ELL,Special Education
2,Asian or Pacific Islander,1,Female,Economically Disadvantaged - Y,Not ELL 2,Asian or Pacific Islander,1,Female,Economically Disadvantaged - Y,Not ELL,Not Special Education
3,Black or African American,4,Non-Binary,Unknown,Unknown 3,Black or African American,4,Non-Binary,Unknown,Unknown,Unknown
4,Hispanic or Latinx,99,Unknown,, 4,Hispanic or Latinx,99,Unknown,,,
5,White or Caucasian,,,, 5,White or Caucasian,,,,,
6,Prefer not to disclose,,,, 6,Prefer not to disclose,,,,,
7,Prefer to self-describe,,,, 7,Prefer to self-describe,,,,,
8,Middle Eastern,,,, 8,Middle Eastern,,,,,
99,Race/Ethnicity Not Listed,,,, 99,Race/Ethnicity Not Listed,,,,,
100,Multiracial,,,, 100,Multiracial,,,,,

1 Race Qualtrics Code Race/Ethnicity Gender Qualtrics Code Sex/Gender Income ELL Special Ed Status
2 1 American Indian or Alaskan Native 2 Male Economically Disadvantaged - N ELL Special Education
3 2 Asian or Pacific Islander 1 Female Economically Disadvantaged - Y Not ELL Not Special Education
4 3 Black or African American 4 Non-Binary Unknown Unknown Unknown
5 4 Hispanic or Latinx 99 Unknown
6 5 White or Caucasian
7 6 Prefer not to disclose
8 7 Prefer to self-describe
9 8 Middle Eastern
10 99 Race/Ethnicity Not Listed
11 100 Multiracial

@ -0,0 +1,12 @@
class CreateSpeds < ActiveRecord::Migration[7.0]
def change
create_table :speds do |t|
t.string :designation
t.string :slug
t.timestamps
end
add_index :speds, :designation
end
end

@ -0,0 +1,5 @@
class AddSpedToSurveyItemResponse < ActiveRecord::Migration[7.0]
def change
add_reference :survey_item_responses, :sped, foreign_key: true
end
end

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 20_230_912_223_701) do ActiveRecord::Schema[7.0].define(version: 2023_10_04_211430) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -406,6 +406,14 @@ ActiveRecord::Schema[7.0].define(version: 20_230_912_223_701) do
t.index ["school_id"], name: "index_scores_on_school_id" t.index ["school_id"], name: "index_scores_on_school_id"
end end
create_table "speds", force: :cascade do |t|
t.string "designation"
t.string "slug"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["designation"], name: "index_speds_on_designation"
end
create_table "student_races", force: :cascade do |t| create_table "student_races", force: :cascade do |t|
t.bigint "student_id", null: false t.bigint "student_id", null: false
t.bigint "race_id", null: false t.bigint "race_id", null: false
@ -448,14 +456,23 @@ ActiveRecord::Schema[7.0].define(version: 20_230_912_223_701) do
t.datetime "recorded_date" t.datetime "recorded_date"
t.bigint "income_id" t.bigint "income_id"
t.bigint "ell_id" t.bigint "ell_id"
t.bigint "sped_id"
t.index ["academic_year_id"], name: "index_survey_item_responses_on_academic_year_id" t.index ["academic_year_id"], name: "index_survey_item_responses_on_academic_year_id"
t.index ["ell_id"], name: "index_survey_item_responses_on_ell_id" t.index ["ell_id"], name: "index_survey_item_responses_on_ell_id"
t.index ["gender_id"], name: "index_survey_item_responses_on_gender_id" t.index ["gender_id"], name: "index_survey_item_responses_on_gender_id"
t.index ["income_id"], name: "index_survey_item_responses_on_income_id" t.index ["income_id"], name: "index_survey_item_responses_on_income_id"
t.index ["response_id"], name: "index_survey_item_responses_on_response_id" t.index ["response_id"], name: "index_survey_item_responses_on_response_id"
<<<<<<< HEAD
t.index %w[school_id academic_year_id survey_item_id], name: "by_school_year_and_survey_item" t.index %w[school_id academic_year_id survey_item_id], name: "by_school_year_and_survey_item"
t.index %w[school_id academic_year_id], name: "index_survey_item_responses_on_school_id_and_academic_year_id" t.index %w[school_id academic_year_id], name: "index_survey_item_responses_on_school_id_and_academic_year_id"
t.index %w[school_id survey_item_id academic_year_id grade], name: "index_survey_responses_on_grade" t.index %w[school_id survey_item_id academic_year_id grade], name: "index_survey_responses_on_grade"
=======
t.index ["school_id", "academic_year_id", "survey_item_id"], name: "by_school_year_and_survey_item"
t.index ["school_id", "academic_year_id"], name: "index_survey_item_responses_on_school_id_and_academic_year_id"
t.index ["school_id", "survey_item_id", "academic_year_id", "grade"], name: "index_survey_responses_on_grade"
t.index ["school_id"], name: "index_survey_item_responses_on_school_id"
t.index ["sped_id"], name: "index_survey_item_responses_on_sped_id"
>>>>>>> 48e795f (feat: add special education disaggregation)
t.index ["student_id"], name: "index_survey_item_responses_on_student_id" t.index ["student_id"], name: "index_survey_item_responses_on_student_id"
t.index ["survey_item_id"], name: "index_survey_item_responses_on_survey_item_id" t.index ["survey_item_id"], name: "index_survey_item_responses_on_survey_item_id"
end end
@ -504,6 +521,7 @@ ActiveRecord::Schema[7.0].define(version: 20_230_912_223_701) do
add_foreign_key "survey_item_responses", "genders" add_foreign_key "survey_item_responses", "genders"
add_foreign_key "survey_item_responses", "incomes" add_foreign_key "survey_item_responses", "incomes"
add_foreign_key "survey_item_responses", "schools" add_foreign_key "survey_item_responses", "schools"
add_foreign_key "survey_item_responses", "speds"
add_foreign_key "survey_item_responses", "students" add_foreign_key "survey_item_responses", "students"
add_foreign_key "survey_item_responses", "survey_items" add_foreign_key "survey_item_responses", "survey_items"
add_foreign_key "survey_items", "scales" add_foreign_key "survey_items", "scales"

@ -1,11 +1,11 @@
Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender,Income,ELL Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender,Income,ELL,Special Ed Status
1,American Indian or Alaskan Native,2,Male,Economically Disadvantaged N,ELL 1,American Indian or Alaskan Native,2,Male,Economically Disadvantaged N,ELL,Special Education
2,Asian or Pacific Islander,1,Female,Economically Disadvantaged Y,Not ELL 2,Asian or Pacific Islander,1,Female,Economically Disadvantaged Y,Not ELL,Not Special Education
3,Black or African American,4,Non-Binary,Unknown,Unknown 3,Black or African American,4,Non-Binary,Unknown,Unknown,Unknown
4,Hispanic or Latinx,99,Unknown,, 4,Hispanic or Latinx,99,Unknown,,,
5,White or Caucasian,,,, 5,White or Caucasian,,,,,
6,Prefer not to disclose,,,, 6,Prefer not to disclose,,,,,
7,Prefer to self-describe,,,, 7,Prefer to self-describe,,,,,
8,Middle Eastern,,,, 8,Middle Eastern,,,,,
99,Race/Ethnicity Not Listed,,,, 99,Race/Ethnicity Not Listed,,,,,
100,Multiracial,,,, 100,Multiracial,,,,,

1 Race Qualtrics Code Race/Ethnicity Gender Qualtrics Code Sex/Gender Income ELL Special Ed Status
2 1 American Indian or Alaskan Native 2 Male Economically Disadvantaged – N ELL Special Education
3 2 Asian or Pacific Islander 1 Female Economically Disadvantaged – Y Not ELL Not Special Education
4 3 Black or African American 4 Non-Binary Unknown Unknown Unknown
5 4 Hispanic or Latinx 99 Unknown
6 5 White or Caucasian
7 6 Prefer not to disclose
8 7 Prefer to self-describe
9 8 Middle Eastern
10 99 Race/Ethnicity Not Listed
11 100 Multiracial

@ -21,6 +21,10 @@ describe DemographicLoader do
["ELL", "Not ELL", "Unknown"] ["ELL", "Not ELL", "Unknown"]
end end
let(:speds) do
["Special Education", "Not Special Education", "Unknown"]
end
before :each do before :each do
DemographicLoader.load_data(filepath:) DemographicLoader.load_data(filepath:)
end end
@ -68,5 +72,12 @@ describe DemographicLoader do
expect(Ell.find_by_designation(ell).designation).to eq ell expect(Ell.find_by_designation(ell).designation).to eq ell
end end
end end
it "load all the special ed designations" do
expect(Sped.all.count).to eq 3
speds.each do |sped|
expect(Sped.find_by_designation(sped).designation).to eq sped
end
end
end end
end end

@ -251,6 +251,44 @@ RSpec.describe SurveyItemValues, type: :model do
end end
end end
context ".sped" do
before :each do
attleboro
ay_2022_23
end
it 'translates "active" into "Special Education"' do
headers = ["Raw SpEd"]
row = { "Raw SpEd" => "active" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.sped).to eq "Special Education"
end
it 'translates "exited" into "Not Special Education"' do
headers = ["Raw SpEd"]
row = { "Raw SpEd" => "exited" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.sped).to eq "Not Special Education"
end
it 'translates blanks into "Not Special Education' do
headers = ["Raw SpEd"]
row = { "Raw SpEd" => "" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.sped).to eq "Not Special Education"
end
it 'tranlsates NA into "Unknown"' do
headers = ["Raw SpEd"]
row = { "Raw SpEd" => "NA" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.sped).to eq "Unknown"
row = { "Raw SpEd" => "#NA" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.sped).to eq "Unknown"
end
end
context ".valid_duration" do context ".valid_duration" do
context "when duration is valid" do context "when duration is valid" do
it "returns true" do it "returns true" do

Loading…
Cancel
Save