Rajan Sharma commited on
Commit
044dc7d
·
verified ·
1 Parent(s): e324bb0

Update auto_metrics.py

Browse files
Files changed (1) hide show
  1. auto_metrics.py +77 -421
auto_metrics.py CHANGED
@@ -1,432 +1,88 @@
1
- from __future__ import annotations
2
- from typing import Dict, Any, Tuple, Optional, List, Union
3
  import pandas as pd
4
  import numpy as np
5
- from data_registry import DataRegistry
6
- from schema_mapper import MappingResult
7
- import re
8
 
9
- def _get(reg: DataRegistry, mapping: MappingResult, concept: str) -> Tuple[Optional[pd.DataFrame], Optional[str]]:
10
- if concept not in mapping.resolved:
11
- return None, None
12
- tname, col = mapping.resolved[concept]
13
- return reg.get(tname), col
14
-
15
- def _clean_numeric_series(series: pd.Series) -> pd.Series:
16
- """Clean numeric data, handling various missing value representations."""
17
- cleaned = series.replace(['', '—', '-', 'null', 'NULL', 'N/A', 'n/a', ' ', 'nan'], np.nan)
18
- return pd.to_numeric(cleaned, errors='coerce')
19
-
20
- def _fmt_tbl(df: pd.DataFrame, max_rows: int = 20) -> str:
21
- if df is None or df.empty:
22
- return "_<empty table>_"
23
- df2 = df.copy()
24
- if len(df2) > max_rows:
25
- df2 = df2.head(max_rows)
26
- return df2.to_markdown(index=False)
27
-
28
- def _detect_numeric_columns(df: pd.DataFrame) -> List[str]:
29
- """Detect columns that contain numeric data (even if stored as strings)."""
30
- numeric_cols = []
31
- for col in df.columns:
32
- # Try to convert a sample to numeric
33
- sample = df[col].dropna().head(100)
34
- if len(sample) > 0:
35
- numeric_sample = pd.to_numeric(sample, errors='coerce')
36
- # If more than 50% can be converted to numeric, consider it numeric
37
- if numeric_sample.notna().sum() > len(sample) * 0.5:
38
- numeric_cols.append(col)
39
- return numeric_cols
40
-
41
- def _detect_categorical_columns(df: pd.DataFrame, max_unique_ratio: float = 0.3) -> List[str]:
42
- """Detect categorical columns with reasonable number of unique values."""
43
- categorical_cols = []
44
- for col in df.columns:
45
- if df[col].dtype == 'object': # String-like columns
46
- unique_ratio = df[col].nunique() / len(df)
47
- # If unique ratio is low, likely categorical
48
- if 0 < unique_ratio <= max_unique_ratio:
49
- categorical_cols.append(col)
50
- return categorical_cols
51
-
52
- def _find_best_grouping_column(df: pd.DataFrame, preferred_patterns: List[str] = None) -> Optional[str]:
53
- """Find the best column to group by based on healthcare patterns and characteristics."""
54
- if preferred_patterns is None:
55
- preferred_patterns = [
56
- r'facility|hospital|clinic|center|centre|institution|provider|site|location',
57
- r'specialty|service|department|unit|division|program|type|category',
58
- r'zone|region|area|district|network|system|catchment',
59
- r'practitioner|physician|doctor|nurse|staff',
60
- r'procedure|treatment|intervention|therapy|service_type',
61
- r'name|id|identifier'
62
- ]
63
-
64
- categorical_cols = _detect_categorical_columns(df)
65
-
66
- # Score columns based on pattern matching and characteristics
67
- scored_cols = []
68
- for col in categorical_cols:
69
- score = 0
70
- col_lower = col.lower()
71
-
72
- # Pattern matching score
73
- for i, pattern in enumerate(preferred_patterns):
74
- if re.search(pattern, col_lower):
75
- score += (len(preferred_patterns) - i) * 10 # Higher score for earlier patterns
76
- break
77
-
78
- # Characteristics score
79
- unique_count = df[col].nunique()
80
- total_count = len(df)
81
-
82
- # Prefer columns with reasonable number of groups (not too few, not too many)
83
- if 2 <= unique_count <= min(50, total_count // 5):
84
- score += 5
85
-
86
- # Prefer columns with less missing data
87
- missing_ratio = df[col].isna().sum() / len(df)
88
- score += (1 - missing_ratio) * 3
89
-
90
- scored_cols.append((col, score))
91
-
92
- if scored_cols:
93
- scored_cols.sort(key=lambda x: x[1], reverse=True)
94
- return scored_cols[0][0]
95
-
96
- return None
97
-
98
- def _find_best_metric_column(df: pd.DataFrame, grouping_col: str = None) -> Optional[str]:
99
- """Find the best numeric column to analyze as a healthcare metric."""
100
- numeric_cols = _detect_numeric_columns(df)
101
-
102
- if not numeric_cols:
103
- return None
104
-
105
- # Healthcare-relevant metric patterns
106
- healthcare_metric_patterns = [
107
- r'wait|delay|time|duration|length',
108
- r'cost|price|expense|fee|charge|budget',
109
- r'volume|count|number|quantity|throughput|capacity',
110
- r'rate|ratio|percent|percentage|score|index',
111
- r'outcome|result|mortality|morbidity|readmission',
112
- r'satisfaction|quality|performance|efficiency',
113
- r'utilization|occupancy|availability',
114
- r'median|mean|average|percentile|p\d+|90th|95th'
115
- ]
116
-
117
- # Score numeric columns
118
- scored_cols = []
119
- for col in numeric_cols:
120
- score = 0
121
- col_lower = col.lower()
122
-
123
- # Prefer columns with healthcare-relevant names
124
- for pattern in healthcare_metric_patterns:
125
- if re.search(pattern, col_lower):
126
- score += 10
127
- break
128
-
129
- # Prefer columns with reasonable variance
130
- try:
131
- clean_series = _clean_numeric_series(df[col])
132
- if not clean_series.isna().all():
133
- std_dev = clean_series.std()
134
- mean_val = clean_series.mean()
135
- if mean_val != 0 and std_dev / abs(mean_val) > 0.1: # Coefficient of variation > 0.1
136
- score += 5
137
- except:
138
- pass
139
-
140
- # Prefer columns with less missing data
141
- missing_ratio = df[col].isna().sum() / len(df)
142
- score += (1 - missing_ratio) * 3
143
-
144
- scored_cols.append((col, score))
145
-
146
- if scored_cols:
147
- scored_cols.sort(key=lambda x: x[1], reverse=True)
148
- return scored_cols[0][0]
149
-
150
- return None
151
-
152
- def compute_generic_rankings(reg: DataRegistry, mapping: MappingResult,
153
- entity_concept: str, metric_concept: str,
154
- ranking_name: str) -> Optional[pd.DataFrame]:
155
- """Generic function to compute rankings for any healthcare entity by any metric."""
156
- df, entity_col = _get(reg, mapping, entity_concept)
157
- if df is None or entity_col is None:
158
- return None
159
-
160
- # Find metric column
161
- metric_col = None
162
- df_metric, mapped_metric_col = _get(reg, mapping, metric_concept)
163
-
164
- if df_metric is not None and mapped_metric_col is not None and df_metric is df:
165
- metric_col = mapped_metric_col
166
  else:
167
- # Fallback: find best numeric column
168
- metric_col = _find_best_metric_column(df, entity_col)
169
-
170
- if metric_col is None:
171
- return None
172
-
173
- # Clean the data
174
- df_clean = df[df[entity_col].notna() & (df[entity_col] != '') & (df[entity_col].astype(str).str.strip() != '')].copy()
175
- df_clean[metric_col] = _clean_numeric_series(df_clean[metric_col])
176
- df_clean = df_clean[df_clean[metric_col].notna()]
177
-
178
- if df_clean.empty:
179
- return None
180
-
181
- # Group and calculate statistics
182
- grouped = df_clean.groupby(entity_col, dropna=True)[metric_col].agg(['mean', 'count', 'std']).reset_index()
183
- grouped = grouped.rename(columns={
184
- 'mean': f'avg_{metric_concept}',
185
- 'count': 'record_count',
186
- 'std': f'std_{metric_concept}'
187
- })
188
-
189
- # Sort by average metric (adjust based on whether higher or lower is better)
190
- # For healthcare metrics like wait times, errors, costs - higher is typically worse
191
- grouped = grouped.sort_values(f'avg_{metric_concept}', ascending=False)
192
- grouped['rank'] = np.arange(1, len(grouped) + 1)
193
-
194
- # Round numeric columns
195
- numeric_cols = grouped.select_dtypes(include=[np.number]).columns
196
- grouped[numeric_cols] = grouped[numeric_cols].round(1)
197
-
198
- return grouped
199
-
200
- def compute_comparative_analysis(reg: DataRegistry, mapping: MappingResult,
201
- grouping_concept: str, metric_concept: str) -> Optional[pd.DataFrame]:
202
- """Generic function to compare healthcare metrics across different groups."""
203
- df, group_col = _get(reg, mapping, grouping_concept)
204
- if df is None or group_col is None:
205
- return None
206
-
207
- # Find metric column
208
- metric_col = None
209
- df_metric, mapped_metric_col = _get(reg, mapping, metric_concept)
210
-
211
- if df_metric is not None and mapped_metric_col is not None and df_metric is df:
212
- metric_col = mapped_metric_col
213
- else:
214
- metric_col = _find_best_metric_column(df, group_col)
215
-
216
- if metric_col is None:
217
- return None
218
-
219
- # Clean data
220
- df_clean = df[df[group_col].notna() & (df[group_col] != '')].copy()
221
- df_clean[metric_col] = _clean_numeric_series(df_clean[metric_col])
222
- df_clean = df_clean[df_clean[metric_col].notna()]
223
-
224
- if df_clean.empty:
225
- return None
226
-
227
- # Group and analyze
228
- grouped = df_clean.groupby(group_col, dropna=True)[metric_col].agg(['mean', 'count', 'std']).reset_index()
229
- grouped = grouped.rename(columns={
230
- 'mean': f'avg_{metric_concept}',
231
- 'count': 'record_count',
232
- 'std': f'std_{metric_concept}'
233
- })
234
-
235
- # Calculate overall average for comparison
236
- overall_avg = df_clean[metric_col].mean()
237
- grouped['vs_overall_avg'] = (grouped[f'avg_{metric_concept}'] - overall_avg).round(1)
238
-
239
- # Sort by average metric
240
- grouped = grouped.sort_values(f'avg_{metric_concept}', ascending=False)
241
-
242
- # Round numeric columns
243
- numeric_cols = grouped.select_dtypes(include=[np.number]).columns
244
- grouped[numeric_cols] = grouped[numeric_cols].round(1)
245
-
246
- return grouped
247
-
248
- def compute_capacity_metrics(reg: DataRegistry, mapping: MappingResult) -> Optional[pd.DataFrame]:
249
- """Compute healthcare capacity-related metrics if available."""
250
- capacity_concepts = [
251
- 'capacity', 'beds', 'staffed_beds', 'occupied_beds', 'available_beds',
252
- 'volume', 'throughput', 'utilization', 'occupancy',
253
- 'appointments', 'procedures', 'admissions', 'discharges',
254
- 'staffing', 'fte', 'personnel'
255
- ]
256
-
257
- results = []
258
- for concept in capacity_concepts:
259
- df, col = _get(reg, mapping, concept)
260
- if df is not None and col is not None:
261
- clean_series = _clean_numeric_series(df[col])
262
- if not clean_series.isna().all():
263
- results.append({
264
- 'metric': f'{concept}_total',
265
- 'value': float(np.nansum(clean_series))
266
- })
267
- results.append({
268
- 'metric': f'{concept}_average',
269
- 'value': float(np.nanmean(clean_series))
270
- })
271
- results.append({
272
- 'metric': f'{concept}_records',
273
- 'value': int((~clean_series.isna()).sum())
274
- })
275
-
276
- if results:
277
- return pd.DataFrame(results)
278
- return None
279
-
280
- def compute_cost_metrics(reg: DataRegistry, mapping: MappingResult) -> Optional[pd.DataFrame]:
281
- """Compute healthcare cost-related metrics if available."""
282
- cost_concepts = [
283
- 'cost', 'price', 'expense', 'fee', 'charge', 'budget', 'funding',
284
- 'fixed_cost', 'variable_cost', 'operational_cost', 'capital_cost',
285
- 'reimbursement', 'revenue', 'billing', 'payment'
286
- ]
287
-
288
- results = []
289
- for concept in cost_concepts:
290
- df, col = _get(reg, mapping, concept)
291
- if df is not None and col is not None:
292
- clean_series = _clean_numeric_series(df[col])
293
- if not clean_series.isna().all():
294
- results.append({
295
- 'component': f'{concept}_total',
296
- 'value': float(np.nansum(clean_series))
297
- })
298
- results.append({
299
- 'component': f'{concept}_average',
300
- 'value': float(np.nanmean(clean_series))
301
- })
302
-
303
- if results:
304
- return pd.DataFrame(results)
305
- return None
306
-
307
- def auto_discover_healthcare_analysis_opportunities(reg: DataRegistry) -> Dict[str, List[str]]:
308
- """Automatically discover what healthcare analyses are possible with the available data."""
309
- opportunities = {
310
- 'provider_rankings': [],
311
- 'service_comparisons': [],
312
- 'regional_analysis': [],
313
- 'outcome_metrics': [],
314
- 'efficiency_metrics': []
315
- }
316
-
317
- for table_name, df in reg._tables.items():
318
- if df.empty:
319
- continue
320
-
321
- # Find potential healthcare grouping columns
322
- categorical_cols = _detect_categorical_columns(df)
323
- numeric_cols = _detect_numeric_columns(df)
324
-
325
- # Healthcare-specific categorization
326
- provider_cols = [col for col in categorical_cols if re.search(r'facility|hospital|clinic|provider', col.lower())]
327
- service_cols = [col for col in categorical_cols if re.search(r'specialty|service|department|procedure', col.lower())]
328
- regional_cols = [col for col in categorical_cols if re.search(r'zone|region|area|district', col.lower())]
329
-
330
- outcome_cols = [col for col in numeric_cols if re.search(r'outcome|mortality|readmission|infection|complication', col.lower())]
331
- efficiency_cols = [col for col in numeric_cols if re.search(r'wait|time|throughput|utilization|length_of_stay', col.lower())]
332
-
333
- # Suggest healthcare-specific analyses
334
- for provider_col in provider_cols[:2]:
335
- for metric_col in (efficiency_cols + outcome_cols)[:2]:
336
- opportunities['provider_rankings'].append(f"{provider_col} by {metric_col}")
337
-
338
- for service_col in service_cols[:2]:
339
- for metric_col in (efficiency_cols + outcome_cols)[:2]:
340
- opportunities['service_comparisons'].append(f"{metric_col} across {service_col}")
341
-
342
- for regional_col in regional_cols[:2]:
343
- for metric_col in (efficiency_cols + outcome_cols)[:2]:
344
- opportunities['regional_analysis'].append(f"{metric_col} by {regional_col}")
345
-
346
- opportunities['outcome_metrics'].extend(outcome_cols[:3])
347
- opportunities['efficiency_metrics'].extend(efficiency_cols[:3])
348
-
349
- return opportunities
350
-
351
- def build_data_findings_markdown(reg: DataRegistry, mapping: MappingResult, topn: int = 5):
352
- """Build generic healthcare data analysis report based on available data and mappings."""
353
- missing: List[str] = []
354
- sections = []
355
-
356
- # Auto-discover healthcare analysis opportunities
357
- opportunities = auto_discover_healthcare_analysis_opportunities(reg)
358
-
359
- # Healthcare-specific analysis patterns
360
- analysis_patterns = [
361
- ('provider rankings', ['facility', 'provider', 'hospital', 'clinic'], ['wait_time', 'wait_median', 'wait_days', 'wait_p90', 'cost', 'outcome']),
362
- ('service analysis', ['specialty', 'service', 'department', 'procedure', 'treatment'], ['wait_time', 'wait_median', 'wait_days', 'cost', 'outcome']),
363
- ('regional comparison', ['zone', 'region', 'area', 'district', 'network'], ['wait_time', 'wait_median', 'cost', 'outcome']),
364
- ('quality metrics', ['facility', 'service'], ['mortality', 'readmission', 'infection', 'complication', 'satisfaction']),
365
- ]
366
-
367
- for analysis_name, entity_concepts, metric_concepts in analysis_patterns:
368
- found_analysis = False
369
- for entity_concept in entity_concepts:
370
- for metric_concept in metric_concepts:
371
- result = compute_generic_rankings(reg, mapping, entity_concept, metric_concept, analysis_name)
372
- if result is not None and not result.empty:
373
- sections.append(f"**Top {entity_concept.title()} by {metric_concept.replace('_', ' ').title()}**\n\n{_fmt_tbl(result.head(topn))}")
374
- found_analysis = True
375
- break
376
- if found_analysis:
377
- break
378
-
379
- if not found_analysis:
380
- missing.append(analysis_name)
381
-
382
- # Healthcare-specific comparative analyses
383
- comparison_patterns = [
384
- ('regional_performance', ['zone', 'region', 'area', 'district'], ['wait_time', 'wait_median', 'cost', 'outcome']),
385
- ('service_performance', ['specialty', 'service', 'department'], ['wait_time', 'wait_median', 'cost', 'outcome']),
386
- ('provider_comparison', ['facility', 'hospital', 'clinic'], ['efficiency', 'utilization', 'throughput']),
387
- ]
388
-
389
- for analysis_name, group_concepts, metric_concepts in comparison_patterns:
390
- found_analysis = False
391
- for group_concept in group_concepts:
392
- for metric_concept in metric_concepts:
393
- result = compute_comparative_analysis(reg, mapping, group_concept, metric_concept)
394
- if result is not None and not result.empty:
395
- sections.append(f"**{group_concept.title()} Performance Comparison**\n\n{_fmt_tbl(result)}")
396
- found_analysis = True
397
- break
398
- if found_analysis:
399
- break
400
-
401
- if not found_analysis:
402
- missing.append(analysis_name)
403
-
404
- # Healthcare capacity analysis
405
- capacity = compute_capacity_metrics(reg, mapping)
406
- if capacity is not None and not capacity.empty:
407
- sections.append(f"**Healthcare Capacity Analysis**\n\n{_fmt_tbl(capacity)}")
408
  else:
409
- missing.append("capacity_analysis")
410
 
411
- # Healthcare cost analysis
412
- costs = compute_cost_metrics(reg, mapping)
413
- if costs is not None and not costs.empty:
414
- sections.append(f"**Healthcare Cost Analysis**\n\n{_fmt_tbl(costs)}")
415
  else:
416
- missing.append("cost_analysis")
417
 
418
- # Build final healthcare report
419
- if sections:
420
- md = (
421
- "### Healthcare Data Analysis Results\n\n" +
422
- "\n\n".join(sections) +
423
- "\n\n**Clinical Data Quality Notes**\n"
424
- "- Analysis performed on available healthcare data columns\n"
425
- "- Missing values and empty entries excluded from calculations\n"
426
- "- Numeric values rounded to 1 decimal place for clinical relevance\n"
427
- "- Rankings prioritize areas that may require clinical attention or resource allocation\n"
428
- "- Record counts indicate data volume and statistical reliability\n"
429
- )
430
  else:
431
  md = "### Healthcare Data Analysis Results\n\nNo analyzable healthcare patterns found in the provided data. Consider uploading data with healthcare facility, service, or outcome metrics."
432
 
 
1
+ # auto_metrics.py
 
2
  import pandas as pd
3
  import numpy as np
4
+ from typing import Dict, List, Any, Tuple
 
 
5
 
6
+ def build_data_findings_markdown(data_registry, mapping) -> Tuple[str, List[str]]:
7
+ """Build markdown summary of data findings with healthcare-specific metrics."""
8
+ findings = []
9
+ missing_keys = []
10
+
11
+ # Facility distribution findings
12
+ if "facility_distribution" in mapping.resolved:
13
+ facility_file = mapping.resolved["facility_distribution"]
14
+ df = data_registry.get(facility_file)
15
+
16
+ if df is not None:
17
+ findings.append("### Facility Distribution Findings")
18
+
19
+ # Total facilities
20
+ total_facilities = len(df)
21
+ findings.append(f"- Total healthcare facilities: {total_facilities}")
22
+
23
+ # Facility type breakdown
24
+ if 'facility_type' in df.columns:
25
+ type_counts = df['facility_type'].value_counts()
26
+ findings.append("- Facility type distribution:")
27
+ for ftype, count in type_counts.items():
28
+ findings.append(f" - {ftype}: {count}")
29
+
30
+ # Geographic distribution
31
+ if 'city' in df.columns:
32
+ top_cities = df['city'].value_counts().head(5)
33
+ findings.append("- Top 5 cities by facility count:")
34
+ for city, count in top_cities.items():
35
+ findings.append(f" - {city}: {count}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  else:
37
+ missing_keys.append("facility_distribution")
38
+
39
+ # Bed capacity findings
40
+ if "bed_capacity" in mapping.resolved:
41
+ bed_file = mapping.resolved["bed_capacity"]
42
+ df = data_registry.get(bed_file)
43
+
44
+ if df is not None:
45
+ findings.append("### Bed Capacity Findings")
46
+
47
+ # Total beds
48
+ if 'beds_current' in df.columns:
49
+ total_current = df['beds_current'].sum()
50
+ total_prev = df['beds_prev'].sum()
51
+ total_change = total_current - total_prev
52
+ total_pct = (total_change / total_prev) * 100 if total_prev > 0 else 0
53
+
54
+ findings.append(f"- Total staffed beds (current): {total_current}")
55
+ findings.append(f"- Total staffed beds (previous): {total_prev}")
56
+ findings.append(f"- Overall change: {total_change} ({total_pct:.1f}%)")
57
+
58
+ # Zone-level analysis
59
+ if 'zone' in df.columns:
60
+ zone_summary = df.groupby('zone').agg({
61
+ 'beds_current': 'sum',
62
+ 'beds_prev': 'sum'
63
+ }).reset_index()
64
+
65
+ zone_summary['change'] = zone_summary['beds_current'] - zone_summary['beds_prev']
66
+ zone_summary['percent_change'] = (zone_summary['change'] / zone_summary['beds_prev']) * 100
67
+
68
+ findings.append("- Zone-level bed capacity:")
69
+ for _, row in zone_summary.iterrows():
70
+ findings.append(f" - {row['zone']}: {row['beds_current']} beds ({row['percent_change']:.1f}% change)")
71
+
72
+ # Identify worst-performing zone
73
+ worst_zone = zone_summary.loc[zone_summary['percent_change'].idxmin()]
74
+ findings.append(f"- Largest percentage decrease: {worst_zone['zone']} ({worst_zone['percent_change']:.1f}%)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  else:
76
+ missing_keys.append("bed_capacity")
77
 
78
+ # Long-term care findings
79
+ if "long_term_care" in mapping.resolved:
80
+ findings.append("### Long-Term Care Findings")
81
+ findings.append("- Long-term care capacity analysis requires facility distribution data")
82
  else:
83
+ missing_keys.append("long_term_care")
84
 
85
+ return "\n".join(findings), missing_keys
 
 
 
 
 
 
 
 
 
 
 
86
  else:
87
  md = "### Healthcare Data Analysis Results\n\nNo analyzable healthcare patterns found in the provided data. Consider uploading data with healthcare facility, service, or outcome metrics."
88