use std::collections::{HashMap, HashSet};
use chrono::NaiveDate;
use std::sync::Arc;
use crate::{
Result, NppesError,
data_types::*,
};
pub struct NppesAnalytics<'a> {
providers: &'a [NppesRecord],
taxonomy_ref: Option<&'a [TaxonomyReference]>,
other_names: Option<&'a [OtherNameRecord]>,
practice_locations: Option<&'a [PracticeLocationRecord]>,
endpoints: Option<&'a [EndpointRecord]>,
}
impl<'a> NppesAnalytics<'a> {
pub fn new(providers: &'a [NppesRecord]) -> Self {
Self {
providers,
taxonomy_ref: None,
other_names: None,
practice_locations: None,
endpoints: None,
}
}
pub fn with_taxonomy_reference(mut self, taxonomy_ref: &'a [TaxonomyReference]) -> Self {
self.taxonomy_ref = Some(taxonomy_ref);
self
}
pub fn with_other_names(mut self, other_names: &'a [OtherNameRecord]) -> Self {
self.other_names = Some(other_names);
self
}
pub fn with_practice_locations(mut self, practice_locations: &'a [PracticeLocationRecord]) -> Self {
self.practice_locations = Some(practice_locations);
self
}
pub fn with_endpoints(mut self, endpoints: &'a [EndpointRecord]) -> Self {
self.endpoints = Some(endpoints);
self
}
pub fn dataset_stats(&self) -> DatasetStats {
let total_providers = self.providers.len();
let individual_count = self.providers.iter()
.filter(|p| p.entity_type == Some(EntityType::Individual))
.count();
let organization_count = total_providers - individual_count;
let active_count = self.providers.iter()
.filter(|p| p.is_active())
.count();
let inactive_count = total_providers - active_count;
let unique_states = self.providers.iter()
.filter_map(|p| p.mailing_address.state.as_ref())
.map(|s| s.as_code())
.collect::<HashSet<_>>()
.len();
let unique_taxonomy_codes = self.providers.iter()
.flat_map(|p| &p.taxonomy_codes)
.map(|t| &t.code)
.collect::<HashSet<_>>()
.len();
DatasetStats {
total_providers,
individual_providers: individual_count,
organization_providers: organization_count,
active_providers: active_count,
inactive_providers: inactive_count,
unique_states,
unique_taxonomy_codes,
}
}
pub fn find_by_npi(&self, npi: &Npi) -> Option<&NppesRecord> {
self.providers.iter().find(|p| &p.npi == npi)
}
pub fn find_by_name(&self, name_query: &str) -> Vec<&NppesRecord> {
let query_lower = name_query.to_lowercase();
self.providers.iter()
.filter(|p| {
let display_name = p.display_name().to_lowercase();
display_name.contains(&query_lower)
})
.collect()
}
pub fn find_by_state(&self, state: &str) -> Vec<&NppesRecord> {
let state_enum = StateCode::from_code(state);
self.providers.iter()
.filter(|p| {
p.mailing_address.state.as_ref()
.map(|s| Some(s) == state_enum.as_ref())
.unwrap_or(false)
})
.collect()
}
pub fn find_by_taxonomy_code(&self, taxonomy_code: &str) -> Vec<&NppesRecord> {
self.providers.iter()
.filter(|p| {
p.taxonomy_codes.iter().any(|t| t.code == taxonomy_code)
})
.collect()
}
pub fn find_by_entity_type(&self, entity_type: EntityType) -> Vec<&NppesRecord> {
self.providers.iter()
.filter(|p| p.entity_type.as_ref() == Some(&entity_type))
.collect()
}
pub fn provider_count_by_state(&self) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for provider in self.providers {
if let Some(state) = &provider.mailing_address.state {
*counts.entry(state.as_code().to_string()).or_insert(0) += 1;
}
}
counts
}
pub fn provider_count_by_taxonomy(&self) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for provider in self.providers {
for taxonomy in &provider.taxonomy_codes {
*counts.entry(taxonomy.code.clone()).or_insert(0) += 1;
}
}
counts
}
pub fn provider_count_by_entity_type(&self) -> HashMap<EntityType, usize> {
let mut counts = HashMap::new();
for provider in self.providers {
if let Some(ref entity_type) = provider.entity_type {
*counts.entry(entity_type.clone()).or_insert(0) += 1;
}
}
counts
}
pub fn top_states_by_provider_count(&self, limit: usize) -> Vec<(String, usize)> {
let mut state_counts: Vec<_> = self.provider_count_by_state().into_iter().collect();
state_counts.sort_by(|a, b| b.1.cmp(&a.1));
state_counts.truncate(limit);
state_counts
}
pub fn top_taxonomy_codes_by_provider_count(&self, limit: usize) -> Vec<(String, usize)> {
let mut taxonomy_counts: Vec<_> = self.provider_count_by_taxonomy().into_iter().collect();
taxonomy_counts.sort_by(|a, b| b.1.cmp(&a.1));
taxonomy_counts.truncate(limit);
taxonomy_counts
}
pub fn providers_enumerated_between(&self, start_date: NaiveDate, end_date: NaiveDate) -> Vec<&NppesRecord> {
self.providers.iter()
.filter(|p| {
if let Some(enum_date) = p.enumeration_date {
enum_date >= start_date && enum_date <= end_date
} else {
false
}
})
.collect()
}
pub fn providers_updated_between(&self, start_date: NaiveDate, end_date: NaiveDate) -> Vec<&NppesRecord> {
self.providers.iter()
.filter(|p| {
if let Some(update_date) = p.last_update_date {
update_date >= start_date && update_date <= end_date
} else {
false
}
})
.collect()
}
pub fn active_providers(&self) -> Vec<&NppesRecord> {
self.providers.iter()
.filter(|p| p.is_active())
.collect()
}
pub fn inactive_providers(&self) -> Vec<&NppesRecord> {
self.providers.iter()
.filter(|p| !p.is_active())
.collect()
}
pub fn providers_with_primary_taxonomy(&self) -> Vec<&NppesRecord> {
self.providers.iter()
.filter(|p| p.primary_taxonomy().is_some())
.collect()
}
pub fn get_taxonomy_description(&self, taxonomy_code: &str) -> Option<&TaxonomyReference> {
self.taxonomy_ref?
.iter()
.find(|t| t.code == taxonomy_code)
}
pub fn enrich_with_taxonomy_descriptions(&self) -> Result<Vec<EnrichedProvider>> {
if self.taxonomy_ref.is_none() {
return Err(NppesError::DataValidation {
message: "Taxonomy reference data required for enrichment".to_string(),
field: None,
value: None,
context: Default::default(),
});
}
let taxonomy_map: HashMap<&str, &TaxonomyReference> = self.taxonomy_ref.unwrap()
.iter()
.map(|t| (t.code.as_str(), t))
.collect();
let mut enriched_providers = Vec::new();
for provider in self.providers {
let enriched_taxonomies: Vec<_> = provider.taxonomy_codes.iter()
.map(|tc| {
let taxonomy_ref = taxonomy_map.get(tc.code.as_str());
EnrichedTaxonomyCode {
code: tc.code.clone(),
license_number: tc.license_number.clone(),
license_state: tc.license_state.clone(),
is_primary: tc.is_primary,
taxonomy_group: tc.taxonomy_group.clone(),
display_name: taxonomy_ref.and_then(|t| t.display_name.clone()),
classification: taxonomy_ref.and_then(|t| t.classification.clone()),
specialization: taxonomy_ref.and_then(|t| t.specialization.clone()),
}
})
.collect();
enriched_providers.push(EnrichedProvider {
provider: provider.clone(),
enriched_taxonomies,
});
}
Ok(enriched_providers)
}
pub fn create_npi_index(&self) -> HashMap<&Npi, &NppesRecord> {
self.providers.iter()
.map(|p| (&p.npi, p))
.collect()
}
pub fn create_state_index(&self) -> HashMap<String, Vec<&NppesRecord>> {
let mut index = HashMap::new();
for provider in self.providers {
if let Some(state) = &provider.mailing_address.state {
index.entry(state.as_code().to_string())
.or_insert_with(Vec::new)
.push(provider);
}
}
index
}
}
#[derive(Debug, Clone)]
pub struct DatasetStats {
pub total_providers: usize,
pub individual_providers: usize,
pub organization_providers: usize,
pub active_providers: usize,
pub inactive_providers: usize,
pub unique_states: usize,
pub unique_taxonomy_codes: usize,
}
impl DatasetStats {
pub fn print_summary(&self) {
println!("=== NPPES Dataset Statistics ===");
println!("Total Providers: {}", self.total_providers);
println!(" Individual Providers: {}", self.individual_providers);
println!(" Organization Providers: {}", self.organization_providers);
println!("Active Providers: {}", self.active_providers);
println!("Inactive Providers: {}", self.inactive_providers);
println!("Unique States: {}", self.unique_states);
println!("Unique Taxonomy Codes: {}", self.unique_taxonomy_codes);
if self.total_providers > 0 {
let individual_percent = (self.individual_providers as f64 / self.total_providers as f64) * 100.0;
let active_percent = (self.active_providers as f64 / self.total_providers as f64) * 100.0;
println!("Individual Provider Percentage: {:.1}%", individual_percent);
println!("Active Provider Percentage: {:.1}%", active_percent);
}
}
}
#[derive(Debug, Clone)]
pub struct EnrichedProvider {
pub provider: NppesRecord,
pub enriched_taxonomies: Vec<EnrichedTaxonomyCode>,
}
#[derive(Debug, Clone)]
pub struct EnrichedTaxonomyCode {
pub code: String,
pub license_number: Option<String>,
pub license_state: Option<String>,
pub is_primary: bool,
pub taxonomy_group: Option<String>,
pub display_name: Option<String>,
pub classification: Option<String>,
pub specialization: Option<String>,
}
pub struct ProviderQuery<'a> {
analytics: &'a NppesAnalytics<'a>,
filters: Vec<Box<dyn Fn(&NppesRecord) -> bool + 'a>>,
}
impl<'a> ProviderQuery<'a> {
pub fn new(analytics: &'a NppesAnalytics<'a>) -> Self {
Self {
analytics,
filters: Vec::new(),
}
}
pub fn entity_type(mut self, entity_type: EntityType) -> Self {
let entity_type = Arc::new(entity_type);
self.filters.push(Box::new(move |p| p.entity_type.as_ref() == Some(&*entity_type)));
self
}
pub fn state<S: AsRef<str> + 'a>(mut self, state: S) -> Self {
let state_enum = StateCode::from_code(state.as_ref());
self.filters.push(Box::new(move |p| {
p.mailing_address.state.as_ref()
.map(|s| Some(s) == state_enum.as_ref())
.unwrap_or(false)
}));
self
}
pub fn taxonomy_code<S: AsRef<str> + 'a>(mut self, taxonomy_code: S) -> Self {
let taxonomy_code = taxonomy_code.as_ref().to_string();
self.filters.push(Box::new(move |p| {
p.taxonomy_codes.iter().any(|t| t.code == taxonomy_code)
}));
self
}
pub fn active_only(mut self) -> Self {
self.filters.push(Box::new(|p| p.is_active()));
self
}
pub fn inactive_only(mut self) -> Self {
self.filters.push(Box::new(|p| !p.is_active()));
self
}
pub fn enumerated_between(mut self, start_date: NaiveDate, end_date: NaiveDate) -> Self {
self.filters.push(Box::new(move |p| {
if let Some(enum_date) = p.enumeration_date {
enum_date >= start_date && enum_date <= end_date
} else {
false
}
}));
self
}
pub fn execute(self) -> Vec<&'a NppesRecord> {
self.analytics.providers.iter()
.filter(|provider| {
self.filters.iter().all(|filter| filter(provider))
})
.collect()
}
pub fn count(self) -> usize {
self.execute().len()
}
}