malawi_hiv_program_reports 1.1.16 → 1.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 394a402d0b5e0c9b210bbf5feff0959854bb2b8d1322ecd97b07107201dcd694
4
- data.tar.gz: 51615fcd686c06088e928b6b10e1bba26a754825e9c3d891e0e7c43c00647098
3
+ metadata.gz: 22a3e944fcb8f4a9e0d862b50321ead38c607a3488c002096735853b06922be5
4
+ data.tar.gz: fb58caba60b6795e915efaadd08fea81e2817129ef500843da95ba48047028aa
5
5
  SHA512:
6
- metadata.gz: aa30a38678f9f6dbeded4d8b3fe6ae920a46c6bea63937906f330cdf50b2cbc1ca6ae95edcf004caa2b4ca5501c6dbf627210f2f496dd49e1ba3964815e3a28f
7
- data.tar.gz: cccee12cb7e4e5d9e5e685230b63309f93ad6db7199de3ae244d461a95e0f825f71a0d82f9b01a535636ad2c45fffc617dfcc68460d1f9508853c059ee71be9e
6
+ metadata.gz: 91ae6c317a44e67d502cc97f3d3d72d54ace89d8def8326f8fc2ea7901e1b4693c0754084b33a1bf9c9d32626dd840b72df1e1c56ee558547ed0ff5056756403
7
+ data.tar.gz: 8d52d72581ab43cc207e34bbb20ecf62d942cd104c854e0890c4b1712346d64a609907cd447c9f912298be40b78d0affd36e4be1a8ef123a86f7fd749258a83b
@@ -29,7 +29,7 @@ module MalawiHivProgramReports
29
29
  rebuild_report if rebuild
30
30
  process_initialization
31
31
  process_data
32
- flatten_and_sort_data
32
+ flatten_the_report
33
33
  rescue StandardError => e
34
34
  Rails.logger.info("Error processing location #{location}: #{e.message}")
35
35
  Rails.logger.info(e.backtrace.join("\n"))
@@ -38,12 +38,12 @@ module MalawiHivProgramReports
38
38
 
39
39
  private
40
40
 
41
- GENDER = %w[M F].freeze
42
- AGGREGATE_GENDER_ROWS = %w[M FP FNP FBf].freeze
41
+ GENDER = %w[Male Female].freeze
42
+ AGGREGATE_GENDER_ROWS = %w[Male FP FNP FBf].freeze
43
43
 
44
44
  def process_initialization
45
45
  init_report
46
- init_aggregate_rows if aggregation
46
+ init_aggregate_rows
47
47
  end
48
48
 
49
49
  def init_report
@@ -85,6 +85,7 @@ module MalawiHivProgramReports
85
85
  regimen = data['regimen'] || 'unknown'
86
86
  regimen = 'unknown' unless COHORT_REGIMENS.include?(regimen)
87
87
  patient_id = data['patient_id']
88
+ maternal_status = data['maternal_status']
88
89
  # we need to handle regimes that only have one P to become PP. Otherwise if it is already PP or PA we leave
89
90
  # it as is. Regimens are in this format NUMBERLETTERS
90
91
  regimen = regimen.gsub(/(\d+[A-Za-z]*P)\z/, '\1P') if regimen.match?(/\A\d+[A-Za-z]*[^P]P\z/)
@@ -92,63 +93,65 @@ module MalawiHivProgramReports
92
93
  report[age_group.to_s][gender.to_s][regimen.to_s] << patient_id
93
94
  report[age_group.to_s][gender.to_s]['tx_curr'] << patient_id
94
95
  report[age_group.to_s][gender.to_s]['total'] << patient_id
95
- process_aggregate_rows(gender:, regimen:, patient_id:) if aggregation
96
+ process_aggregate_rows(gender:, regimen:, patient_id:, maternal_status:)
96
97
  end
97
98
  end
98
99
 
99
- def process_maternal_data
100
- result = MalawiHivProgramReports::Pepfar::ViralLoadCoverage2.new(start_date:, end_date:, location:)
101
- .vl_maternal_status(maternal.keys)
102
- # result comes in this form: { FP: [], FBf: [] }
103
- # we need to loop through the keys
104
- result.each_key do |key|
105
- result[key].each do |patient_id|
106
- report['All'][key.to_s]['tx_curr'] << patient_id
107
- report['All'][key.to_s][maternal[patient_id].to_s] << patient_id
108
- report['All'][key.to_s]['total'] << patient_id
109
- end
110
- end
111
- end
112
- # rubocop:enable Metrics/AbcSize
113
-
114
- def process_aggregate_rows(gender:, regimen:, patient_id:)
100
+ def process_aggregate_rows(gender:, regimen:, patient_id:, maternal_status: nil)
115
101
  if gender == 'M'
116
102
  report['All']['M']['tx_curr'] << patient_id
117
103
  report['All']['M'][regimen.to_s] << patient_id
118
104
  report['All']['M']['total'] << patient_id
119
- else
120
- maternal[patient_id] = regimen
105
+ return
121
106
  end
122
- end
123
107
 
124
- # we need to flatten the data
125
- def flatten_data
126
- flat_data = []
127
- report.each do |age_group, gender_data|
128
- gender_data.each do |gender, regimen_data|
129
- flat_data << {
130
- 'age_group' => age_group,
131
- 'gender' => gender,
132
- **regimen_data
133
- }
134
- end
108
+ case maternal_status
109
+ when 'FP'
110
+ report['All']['FP']['tx_curr'] << patient_id
111
+ report['All']['FP'][regimen.to_s] << patient_id
112
+ report['All']['FP']['total'] << patient_id
113
+ when 'FBf'
114
+ report['All']['FBf']['tx_curr'] << patient_id
115
+ report['All']['FBf'][regimen.to_s] << patient_id
116
+ report['All']['FBf']['total'] << patient_id
117
+ else
118
+ report['All']['FNP']['tx_curr'] << patient_id
119
+ report['All']['FNP'][regimen.to_s] << patient_id
120
+ report['All']['FNP']['total'] << patient_id
135
121
  end
136
- flat_data
137
122
  end
123
+ # rubocop:enable Metrics/AbcSize
138
124
 
139
- def flatten_and_sort_data
140
- flat_data = flatten_data
125
+ def flatten_the_report
126
+ result = []
127
+ @report.each do |age_group, age_group_report|
128
+ next if age_group == 'Unknown'
141
129
 
142
- age_group_order = ['Unknown', '<1 year', '1-4 years', '5-9 years', '10-14 years', '15-19 years',
143
- '20-24 years', '25-29 years', '30-34 years', '35-39 years', '40-44 years', '45-49 years',
144
- '50-54 years', '55-59 years', '60-64 years', '65-69 years', '70-74 years', '75-79 years',
145
- '80-84 years', '85-89 years', '90 plus years', 'All']
146
- gender_order = %w[F M FP FNP FBF]
130
+ age_group_report.each_key do |gender|
131
+ next if gender == 'Unknown'
147
132
 
148
- flat_data.sort_by do |row|
149
- [age_group_order.index(row['age_group']) || Float::INFINITY,
150
- gender_order.index(row['gender']) || Float::INFINITY]
133
+ result << process_age_group_report(age_group, gender, age_group_report[gender])
134
+ end
151
135
  end
136
+
137
+ new_group = pepfar_age_groups.map { |age_group| age_group }
138
+ gender_scores = { 'Female' => 0, 'Male' => 1, 'FNP' => 3, 'FP' => 2, 'FBf' => 4 }
139
+ result_scores = result.sort_by do |item|
140
+ gender_score = gender_scores[item[:gender]] || 999
141
+ age_group_score = new_group.index(item[:age_group]) || 999
142
+ [gender_score, age_group_score]
143
+ end
144
+ # remove all unknown age groups
145
+ result_scores.reject { |item| item[:age_group].match?(/unknown/i) }
146
+ end
147
+
148
+ def process_age_group_report(age_group, gender, age_group_report)
149
+ {
150
+ age_group:,
151
+ gender:,
152
+ # spread the age group report
153
+ **age_group_report
154
+ }
152
155
  end
153
156
 
154
157
  # rubocop:disable Metrics/MethodLength
@@ -158,16 +161,23 @@ module MalawiHivProgramReports
158
161
  prescriptions.patient_id,
159
162
  COALESCE(regimens.name, 'unknown') AS regimen,
160
163
  prescriptions.age_group,
161
- prescriptions.gender
164
+ CASE prescriptions.gender
165
+ WHEN 'M' THEN 'Male'
166
+ WHEN 'F' THEN 'Female'
167
+ ELSE 'Unknown'
168
+ END gender,
169
+ prescriptions.maternal_status
162
170
  FROM (
163
171
  SELECT
164
172
  tcm.patient_id,
165
173
  GROUP_CONCAT(DISTINCT(tcm.drug_id) ORDER BY tcm.drug_id ASC) AS drugs,
166
174
  disaggregated_age_group(date(earliest_start_date.birthdate), date('#{end_date}')) AS age_group,
175
+ c.maternal_status,
167
176
  earliest_start_date.gender
168
177
  FROM cdr_temp_current_medication #{current_partition} tcm
169
178
  INNER JOIN cdr_temp_patient_outcomes #{current_partition} AS outcomes ON outcomes.patient_id = tcm.patient_id AND outcomes.cum_outcome = 'On antiretrovirals'
170
179
  INNER JOIN cdr_temp_cohort_members #{current_partition} AS earliest_start_date ON earliest_start_date.patient_id = tcm.patient_id AND earliest_start_date.gender IN ('M','F')
180
+ LEFT JOIN cdr_temp_maternal_status #{current_partition} c ON c.patient_id = tcm.patient_id
171
181
  GROUP BY tcm.patient_id
172
182
  ) AS prescriptions
173
183
  LEFT JOIN (
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ #
3
+ require 'redis'
2
4
 
3
5
  module MalawiHivProgramReports
4
6
  module Moh
@@ -21,10 +23,7 @@ module MalawiHivProgramReports
21
23
  @definition = kwargs[:definition] || 'pepfar'
22
24
  @iteration = Concurrent::AtomicFixnum.new(0)
23
25
  @start_time = Time.now
24
- @redis = Redis.new
25
- @redis.set('cumulative_progress', 0)
26
- @redis.set('cumulative_status', 'running')
27
- @redis.set('cumulative_time_taken', 0)
26
+ prepare_redis
28
27
  definition = @definition.downcase
29
28
  raise ArgumentError, "Invalid outcomes definition: #{definition}" unless %w[moh pepfar].include?(definition)
30
29
  end
@@ -59,7 +58,7 @@ module MalawiHivProgramReports
59
58
  queue = Queue.new
60
59
  locations.each { |loc| queue << loc }
61
60
 
62
- threads = Array.new(65) do
61
+ threads = Array.new(configured_threads) do
63
62
  Thread.new do
64
63
  until queue.empty?
65
64
  loc = begin
@@ -86,6 +85,22 @@ module MalawiHivProgramReports
86
85
  end
87
86
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
88
87
 
88
+ def configured_threads
89
+ threads = ::Utils::ConfigReader.application_read('max_threads')&.to_i
90
+ Rails::logger.info("Max threads: #{threads}")
91
+ threads.positive? ? threads : 60
92
+ rescue StandardError => e
93
+ Rails.logger.info("Error reading max threads: #{e.message}")
94
+ 60
95
+ end
96
+
97
+ def prepare_redis
98
+ @redis = Redis.new
99
+ @redis.set('cumulative_progress', 0)
100
+ @redis.set('cumulative_status', 'running')
101
+ @redis.set('cumulative_time_taken', 0)
102
+ end
103
+
89
104
  # update progress in redis and channel
90
105
  def update_progress(current_iteration, total)
91
106
  percentage = (current_iteration.to_f / total * 100).round(2)
@@ -161,13 +176,22 @@ module MalawiHivProgramReports
161
176
  outcome = MalawiHivProgramReports::Moh::CumulativeOutcome.new(end_date:, location:, definition:, rebuild:,
162
177
  start_date:)
163
178
  rebuild ? outcome.find_report : outcome.update_outcomes_by_definition
179
+ other_dependables(location)
164
180
  end_time = Time.now
165
181
  time_taken = ((end_time - start_time) / 60).round(2)
166
182
  save_completed_site(location:, time_taken:)
167
183
  end
168
184
 
185
+ def other_dependables(loc)
186
+ Pepfar::TxTb.new(start_date:, end_date:, location: loc, occupation: nil).process_tb_data
187
+ Pepfar::MaternalStatus.new(start_date:, end_date:, location: loc).process_data
188
+ end
189
+
169
190
  # rubocop:disable Metrics/CyclomaticComplexity
170
191
  def prepare_tables
192
+ create_cdr_temp_maternal_status unless check_if_table_exists('cdr_temp_maternal_status')
193
+ create_cdr_tb_confirmed_and_on_treatment unless check_if_table_exists('cdr_tb_confirmed_and_on_treatment')
194
+ create_tb_screened unless check_if_table_exists('cdr_tb_screened')
171
195
  create_cdr_other_patient_types unless check_if_table_exists('cdr_other_patient_types')
172
196
  create_temp_potential_cohort_members_table unless check_if_table_exists('cdr_temp_potential_cohort_members')
173
197
  create_min_drug_orders_table unless check_if_table_exists('cdr_temp_min_drug_orders')
@@ -193,6 +217,18 @@ module MalawiHivProgramReports
193
217
  SQL
194
218
  end
195
219
 
220
+ def create_cdr_temp_maternal_status
221
+ ActiveRecord::Base.connection.execute <<~SQL
222
+ CREATE TABLE IF NOT EXISTS cdr_temp_maternal_status (
223
+ patient_id INT(11) NOT NULL,
224
+ site_id INT(11) NOT NULL,
225
+ maternal_status VARCHAR(5) DEFAULT NULL,
226
+ PRIMARY KEY (patient_id, site_id)
227
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
228
+ PARTITION BY LIST(site_id) (#{partition_by_site})
229
+ SQL
230
+ end
231
+
196
232
  def create_cdr_temp_cohort_status
197
233
  ActiveRecord::Base.connection.execute <<~SQL
198
234
  CREATE TABLE IF NOT EXISTS cdr_temp_cohort_status (
@@ -303,6 +339,40 @@ module MalawiHivProgramReports
303
339
  SQL
304
340
  end
305
341
 
342
+ def create_cdr_tb_confirmed_and_on_treatment
343
+ ActiveRecord::Base.connection.execute <<~SQL
344
+ CREATE TABLE IF NOT EXISTS cdr_tb_confirmed_and_on_treatment (
345
+ patient_id INT(11) NOT NULL,
346
+ site_id INT(11) NOT NULL,
347
+ gender VARCHAR(50) DEFAULT NULL,
348
+ age_group VARCHAR(50) DEFAULT NULL,
349
+ tb_confirmed_date DATE DEFAULT NULL,
350
+ has_tb_confirmed_date BOOLEAN DEFAULT NULL,
351
+ enrollment_date DATE DEFAULT NULL,
352
+ prev_reading DATE DEFAULT NULL,
353
+ PRIMARY KEY (patient_id, site_id)
354
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
355
+ PARTITION BY LIST(site_id) (#{partition_by_site})
356
+ SQL
357
+ end
358
+
359
+ def create_tb_screened
360
+ ActiveRecord::Base.connection.execute <<~SQL
361
+ CREATE TABLE IF NOT EXISTS cdr_tb_screened (
362
+ patient_id INT(11) NOT NULL,
363
+ site_id INT(11) NOT NULL,
364
+ gender VARCHAR(255) NULL,
365
+ screened_date DATE NULL,
366
+ enrollment_date DATE NULL,
367
+ age_group VARCHAR(255) NULL,
368
+ tb_status VARCHAR(255) NULL,
369
+ screening_methods VARCHAR(255) NULL,
370
+ PRIMARY KEY (patient_id, site_id)
371
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
372
+ PARTITION BY LIST(site_id) (#{partition_by_site})
373
+ SQL
374
+ end
375
+
306
376
  def create_cdr_other_patient_types
307
377
  ActiveRecord::Base.connection.execute <<~SQL
308
378
  CREATE TABLE IF NOT EXISTS cdr_other_patient_types (
@@ -485,6 +555,8 @@ module MalawiHivProgramReports
485
555
  def clear_tables
486
556
  # if locations is empty then we truncating otherwise we clear the locations
487
557
  if locations.empty?
558
+ ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_tb_confirmed_and_on_treatment')
559
+ ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_tb_screened')
488
560
  ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_other_patient_types')
489
561
  ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_potential_cohort_members')
490
562
  ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_min_drug_orders')
@@ -493,6 +565,8 @@ module MalawiHivProgramReports
493
565
  ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_external_clients')
494
566
  ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_reason_for_starting_art')
495
567
  else
568
+ ActiveRecord::Base.connection.execute("DELETE FROM cdr_tb_confirmed_and_on_treatment WHERE site_id IN (#{locations.join(',')})")
569
+ ActiveRecord::Base.connection.execute("DELETE FROM cdr_tb_screened WHERE site_id IN (#{locations.join(',')})")
496
570
  ActiveRecord::Base.connection.execute("DELETE FROM cdr_other_patient_types WHERE site_id IN (#{locations.join(',')})")
497
571
  ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_potential_cohort_members WHERE site_id IN (#{locations.join(',')})")
498
572
  ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_min_drug_orders WHERE site_id IN (#{locations.join(',')})")
@@ -22,7 +22,109 @@ module MalawiHivProgramReports
22
22
  end
23
23
 
24
24
  def find_report
25
- MalawiHivProgramReports::Pepfar::ViralLoadCoverage2.new(tx_curr_definition: 'pepfar', start_date: @start_date, end_date: @end_date, location: @location).vl_maternal_status(@patient_ids)
25
+ vl_maternal_status
26
+ end
27
+
28
+ def process_data
29
+ clear_maternal_status
30
+ load_pregnant_women
31
+ load_breast_feeding
32
+ end
33
+
34
+ private
35
+
36
+ def vl_maternal_status(patient_list)
37
+ return { FP: [], FBf: [] } if patient_list.blank?
38
+
39
+ pregnant = pregnant_women(patient_list).map { |woman| woman['person_id'].to_i }
40
+ return { FP: pregnant, FBf: [] } if (patient_list - pregnant).blank?
41
+
42
+ feeding = breast_feeding(patient_list - pregnant).map { |woman| woman['person_id'].to_i }
43
+
44
+ {
45
+ FP: pregnant,
46
+ FBf: feeding
47
+ }
48
+ end
49
+
50
+ def pregnant_women(patient_list)
51
+ ActiveRecord::Base.connection.execute <<~SQL
52
+ SELECT person_id
53
+ FROM cdr_temp_maternal_status #{current_partition}
54
+ WHERE maternal_status = 'FP' AND patient_od IN (#{patient_list.join(',')})
55
+ SQL
56
+ end
57
+
58
+ def breast_feeding(patient_list)
59
+ ActiveRecord::Base.connection.execute <<~SQL
60
+ SELECT person_id
61
+ FROM cdr_temp_maternal_status #{current_partition}
62
+ WHERE maternal_status = 'FBf' AND patient_id IN (#{patient_list.join(',')})
63
+ SQL
64
+ end
65
+
66
+ def load_pregnant_women
67
+ ActiveRecord::Base.connection.execute <<~SQL
68
+ INSERT INTO cdr_temp_maternal_status #{current_partition} (patient_id, site_id, maternal_status)
69
+ SELECT o.person_id, #{location}, 'FP' as maternal_status
70
+ FROM obs #{current_partition} o
71
+ INNER JOIN cdr_temp_cohort_members #{current_partition} c ON c.patient_id = o.person_id AND c.gender = 'F'
72
+ LEFT JOIN obs #{current_partition} a ON a.person_id = o.person_id AND a.obs_datetime > o.obs_datetime AND a.concept_id IN (#{pregnant_concepts.to_sql}) AND a.voided = 0
73
+ AND a.obs_datetime >= DATE(#{ActiveRecord::Base.connection.quote(start_date)}) AND a.obs_datetime < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
74
+ WHERE a.obs_id is null
75
+ AND o.obs_datetime >= DATE(#{ActiveRecord::Base.connection.quote(start_date)})
76
+ AND o.obs_datetime < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
77
+ AND o.voided = 0
78
+ AND o.concept_id in (#{pregnant_concepts.to_sql})
79
+ AND o.value_coded IN (#{yes_concepts.join(',')})
80
+ GROUP BY o.person_id
81
+ SQL
82
+ end
83
+
84
+ def load_breast_feeding
85
+ ActiveRecord::Base.connection.execute <<~SQL
86
+ INSERT INTO cdr_temp_maternal_status #{current_partition} (patient_id, site_id, maternal_status)
87
+ SELECT o.person_id, #{location}, 'FBf' as maternal_status
88
+ FROM obs #{current_partition} o
89
+ INNER JOIN cdr_temp_cohort_members #{current_partition} c ON c.patient_id = o.person_id AND c.gender = 'F'
90
+ LEFT JOIN obs #{current_partition} a ON a.person_id = o.person_id AND a.obs_datetime > o.obs_datetime AND a.concept_id IN (#{breast_feeding_concepts.to_sql}) AND a.voided = 0
91
+ AND a.obs_datetime >= DATE(#{ActiveRecord::Base.connection.quote(start_date)}) AND a.obs_datetime < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
92
+ WHERE a.obs_id is null
93
+ AND o.obs_datetime >= DATE(#{ActiveRecord::Base.connection.quote(start_date)})
94
+ AND o.obs_datetime < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
95
+ AND o.voided = 0
96
+ AND o.concept_id IN (#{breast_feeding_concepts.to_sql})
97
+ AND o.value_coded IN (#{yes_concepts.join(',')})
98
+ AND o.person_id NOT IN (SELECT c.patient_id FROM cdr_temp_maternal_status #{current_partition} c)
99
+ GROUP BY o.person_id
100
+ SQL
101
+ end
102
+
103
+ def clear_maternal_status
104
+ ActiveRecord::Base.connection.execute <<~SQL
105
+ DELETE FROM cdr_temp_maternal_status where site_id = #{location}
106
+ SQL
107
+ end
108
+
109
+ def yes_concepts
110
+ @yes_concepts ||= ::ConceptName.where(name: 'Yes').select(:concept_id).map do |record|
111
+ record['concept_id'].to_i
112
+ end
113
+ end
114
+
115
+ def pregnant_concepts
116
+ @pregnant_concepts ||= ::ConceptName.where(name: ['Is patient pregnant?', 'patient pregnant'])
117
+ .select(:concept_id)
118
+ end
119
+
120
+ def breast_feeding_concepts
121
+ @breast_feeding_concepts ||= ::ConceptName.where(name: ['Breast feeding?', 'Breast feeding', 'Breastfeeding'])
122
+ .select(:concept_id)
123
+ end
124
+
125
+ def encounter_types
126
+ @encounter_types ||= ::EncounterType.where(name: ['HIV CLINIC CONSULTATION', 'HIV STAGING'])
127
+ .select(:encounter_type_id)
26
128
  end
27
129
  end
28
130
  end