Skip to main content

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.
settings
Settings
required
Configuration specifying which indicators to calculate and their thresholds. Enable indicators by setting their field to Some(...) in the Settings struct.
map
&bool
required
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
Option<String>
Currency code for amount comparisons (e.g., "USD"). Required for indicators that compare bid amounts.
exclusions
Option<Exclusions>
Exclude contracting processes by procurement method details:
settings.exclusions = Some(Exclusions {
    procurement_method_details: Some("Emergency|Direct".to_string()),
});
no_price_comparison_procurement_methods
Option<String>
Pipe-separated procurement methods where price comparison indicators should not apply.
price_comparison_procurement_methods
Option<String>
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(...):
R003
Option<R003>
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),
    ])),
});
R018
Option<R018>
No competitive bids:
settings.R018 = Some(R018 {
    procurement_methods: Some("open|selective".to_string()),
});
R024
Option<FloatThreshold>
Winner’s bid close to next lowest (threshold is a ratio):
settings.R024 = Some(FloatThreshold {
    threshold: Some(0.05),  // 5% difference
});
R025
Option<R025>
Frequent winner:
settings.R025 = Some(R025 {
    percentile: Some(75),
    threshold: Some(0.05),
});
R028
Option<Empty>
Single bidder wins (no configuration):
settings.R028 = Some(Default::default());
R030
Option<Empty>
Bids differ by same amount:
settings.R030 = Some(Default::default());
R035
Option<IntegerThreshold>
Supplier awarded many contracts:
settings.R035 = Some(IntegerThreshold {
    threshold: Some(1),  // Count
});
R036
Option<Empty>
Supplier offered identical prices:
settings.R036 = Some(Default::default());
R038
Option<R038>
Many disqualified bids:
settings.R038 = Some(R038 {
    threshold: Some(0.5),  // 50% ratio
    minimum_submitted_bids: Some(2),
    minimum_contracting_processes: Some(2),
});
R048
Option<R048>
Item classification patterns:
settings.R048 = Some(R048 {
    digits: Some(2),
    threshold: Some(10),
    minimum_contracting_processes: Some(20),
});
R058
Option<FloatThreshold>
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:
  1. Group: Entity type (OCID, Buyer, ProcuringEntity, Tenderer)
  2. String: Entity ID (e.g., OCID value or organization ID)
  3. 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.
maps
Maps
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(())
}

Performance

  • 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

Build docs developers (and LLMs) love