Documentation Index
Fetch the complete documentation index at: https://mintlify.com/open-contracting/cardinal-rs/llms.txt
Use this file to discover all available pages before exploring further.
The Indicators module calculates red flags and procurement indicators across OCDS releases. It processes line-delimited JSON files in parallel and returns indicator scores grouped by entity.
Function Signature
impl Indicators {
pub fn run(
buffer: impl BufRead + Send,
settings: Settings,
map: &bool,
) -> Result<Self, anyhow::Error>
}
buffer
impl BufRead + Send
required
Buffered reader containing line-delimited JSON releases. Each line must be a valid OCDS release.
Configuration specifying which indicators to calculate and their thresholds. Enable indicators by setting their field to Some(...) in the Settings struct.
Whether to include mapping data linking contracting processes to organizations. Set to true to populate the maps field in results.
Basic Usage
use std::fs::File;
use std::io::BufReader;
use ocdscardinal::{Indicators, Settings};
fn main() -> Result<(), anyhow::Error> {
let file = File::open("releases.jsonl")?;
let reader = BufReader::new(file);
// Configure which indicators to calculate
let mut settings = Settings::default();
settings.R036 = Some(Default::default());
settings.R038 = Some(Default::default());
// Calculate indicators
let indicators = Indicators::run(reader, settings, &false)?;
// Access results
for (group, entities) in indicators.results() {
for (entity_id, scores) in entities {
for (indicator, score) in scores {
println!("{:?} {} {:?}: {}", group, entity_id, indicator, score);
}
}
}
Ok(())
}
Settings Configuration
Global Settings
Currency code for amount comparisons (e.g., "USD"). Required for indicators that compare bid amounts.
Exclude contracting processes by procurement method details:settings.exclusions = Some(Exclusions {
procurement_method_details: Some("Emergency|Direct".to_string()),
});
no_price_comparison_procurement_methods
Pipe-separated procurement methods where price comparison indicators should not apply.
price_comparison_procurement_methods
Pipe-separated procurement methods where price comparison indicators should apply. Overrides no_price_comparison_procurement_methods.
Indicator-Specific Settings
Each indicator has its own configuration struct. Enable an indicator by setting it to Some(...):
Submission period indicator:settings.R003 = Some(R003 {
threshold: Some(15), // Days
procurement_methods: Some("open|selective".to_string()),
procurement_method_details: Some(HashMap::from([
("emergency".to_string(), 10),
("international".to_string(), 25),
])),
});
No competitive bids:settings.R018 = Some(R018 {
procurement_methods: Some("open|selective".to_string()),
});
Winner’s bid close to next lowest (threshold is a ratio):settings.R024 = Some(FloatThreshold {
threshold: Some(0.05), // 5% difference
});
Frequent winner:settings.R025 = Some(R025 {
percentile: Some(75),
threshold: Some(0.05),
});
Single bidder wins (no configuration):settings.R028 = Some(Default::default());
Bids differ by same amount:settings.R030 = Some(Default::default());
Supplier awarded many contracts:settings.R035 = Some(IntegerThreshold {
threshold: Some(1), // Count
});
Supplier offered identical prices:settings.R036 = Some(Default::default());
Many disqualified bids:settings.R038 = Some(R038 {
threshold: Some(0.5), // 50% ratio
minimum_submitted_bids: Some(2),
minimum_contracting_processes: Some(2),
});
Item classification patterns:settings.R048 = Some(R048 {
digits: Some(2),
threshold: Some(10),
minimum_contracting_processes: Some(20),
});
Winner’s bid close to next lowest (alternative calculation):settings.R058 = Some(FloatThreshold {
threshold: Some(0.5),
});
Result Structure
The Indicators struct contains the calculated results:
pub struct Indicators {
pub results: IndexMap<Group, IndexMap<String, HashMap<Indicator, f64>>>,
pub meta: HashMap<Indicator, RoundMap>,
pub maps: Maps,
// ... internal fields
}
Accessing Results
results
IndexMap<Group, IndexMap<String, HashMap<Indicator, f64>>>
Nested map structure:
- Group: Entity type (OCID, Buyer, ProcuringEntity, Tenderer)
- String: Entity ID (e.g., OCID value or organization ID)
- HashMap: Indicator scores for that entity
let results = indicators.results();
// Iterate over all results
for (group, entities) in results {
println!("Group: {:?}", group);
for (entity_id, scores) in entities {
for (indicator, score) in scores {
println!(" {} - {:?}: {}", entity_id, indicator, score);
}
}
}
meta
HashMap<Indicator, RoundMap>
Additional metadata for indicators, such as intermediate calculations or statistics.
Relationships between entities (only populated if map = true):pub struct Maps {
pub ocid_buyer_r038: HashMap<String, String>,
pub ocid_procuringentity_r038: HashMap<String, String>,
pub ocid_tenderer: HashMap<String, HashSet<String>>,
pub ocid_tenderer_r024: HashMap<String, HashSet<String>>,
pub ocid_tenderer_r028: HashMap<String, HashSet<String>>,
pub ocid_tenderer_r030: HashMap<String, HashSet<String>>,
pub ocid_tenderer_r035: HashMap<String, HashSet<String>>,
pub ocid_tenderer_r058: HashMap<String, HashSet<String>>,
}
Complete Example
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use ocdscardinal::{Indicators, Settings, R038, Group, Indicator};
fn analyze_procurement_data() -> Result<(), anyhow::Error> {
let file = File::open("data/releases.jsonl")?;
let reader = BufReader::new(file);
// Configure multiple indicators
let mut settings = Settings::default();
settings.currency = Some("USD".to_string());
settings.R036 = Some(Default::default());
settings.R038 = Some(R038 {
threshold: Some(0.5),
minimum_submitted_bids: Some(2),
minimum_contracting_processes: Some(2),
});
// Calculate with mapping enabled
let results = Indicators::run(reader, settings, &true)?;
// Find contracting processes with high disqualification rates
if let Some(ocids) = results.results().get(&Group::OCID) {
for (ocid, scores) in ocids {
if let Some(score) = scores.get(&Indicator::R038) {
if *score > 0.6 {
println!("High disqualification rate in {}: {:.1}%",
ocid, score * 100.0);
// Access mapping data
if let Some(tenderers) = results.maps.ocid_tenderer.get(ocid) {
println!(" Tenderers: {:?}", tenderers);
}
}
}
}
}
Ok(())
}
- Parallel processing: Uses Rayon for automatic parallelization
- Streaming: Processes data in chunks without loading entire file into memory
- Scalability: Can handle millions of releases efficiently
For best performance, ensure the input buffer is properly sized. BufReader::new() uses an 8KB buffer by default, but you can create a larger buffer with BufReader::with_capacity().
Notes
- Contracting processes with
tender.status = "cancelled" are automatically excluded
- Awards must have a final status (
active, cancelled, or unsuccessful) for results to be stable
- Bids with status
invited or withdrawn are not considered “submitted”
- Invalid JSON lines are logged and skipped without stopping execution