Skip to main content

Bid Evaluation

Status Python License

A flexible Python library for evaluating competitive bids using multiple weighted criteria. Designed to help procurement professionals, project managers, and organizations systematically score and rank vendor bids based on various evaluation factors.

⚠️ Alpha Stage: This library is in early development. APIs may change. Feedback welcome!

🆕 Multi-Stage Evaluation — Evaluate bids in sequential stages with automatic filtering between them. Eliminate unqualified bids at the technical stage before scoring economics. Supports score thresholds, top-N filters, tie-breaking rules, and weighted stage combinations. Jump to docs →

Features

  • Multiple Evaluation Strategies: Linear normalization, threshold-based scoring, ratio-based scoring, formula expressions, and custom functions
  • Multi-Stage Evaluation: Sequential stages with filtering between them — eliminate bids that don't meet technical requirements before scoring economics
  • Flexible Configuration: Dictionary, YAML, JSON, or fluent interface
  • Automatic Weight Normalization: Optional scaling of weights to sum to 1.0
  • Built-in Statistics: Automatic calculation of min, max, mean, median, std dev, and quartiles
  • Pandas Integration: Works seamlessly with DataFrames for input and output

🚀 Quick Start

Try Online

Library Demo (hosted): https://bidevaluation.streamlit.app/

Installation

git clone https://github.com/escobar-david/bid_evaluation.git
cd bid_evaluation
pip install .

Install with demo dependencies (Streamlit apps):

pip install ".[demos]"

Or install directly from GitHub:

pip install git+https://github.com/escobar-david/bid_evaluation.git

Basic Usage

from bid_evaluation import Evaluator
import pandas as pd

# Load your bids
bids = pd.DataFrame({
    'vendor': ['Company A', 'Company B', 'Company C'],
    'bid_amount': [50_000_000, 45_000_000, 52_000_000],
    'experience': [8, 10, 6],
})

# Configure and evaluate
result = (Evaluator()
    .min_ratio('bid_amount', weight=0.6)
    .linear('experience', weight=0.4, higher_is_better=True)
    .evaluate(bids))

# View results
print(result[['vendor', 'ranking', 'final_score']])

# Export to Excel
result.to_excel('evaluation_results.xlsx')

Output:

     vendor  ranking  final_score
1  Company B        1        88.33
0  Company A        2        66.67
2  Company C        3        40.00

Staged Evaluation (Technical → Economic)

from bid_evaluation import StagedEvaluator

result = (StagedEvaluator()
    # Stage 1: Technical — bids scoring below 60 are eliminated
    .add_stage('Technical', filter_type='score_threshold', threshold=60)
        .linear('experience', 0.4, higher_is_better=True)
        .direct('quality_score', 0.6)
    # Stage 2: Economic — only surviving bids are ranked
    .add_stage('Economic')
        .min_ratio('bid_amount', 1.0)
    .evaluate(bids))

Eliminated bids are marked in the eliminated_at_stage column and excluded from the final ranking. Full staged evaluation docs →

📚 Examples

🎨 Streamlit Demos (Open Source)

Single-stage demo (basic evaluation):

pip install ".[demos]"
streamlit run demos/streamlit_demo.py

Staged evaluation demo (multi-stage with filtering):

pip install ".[demos]"
streamlit run demos/streamlit_staged_demo.py

If you have more than 1 python version (streamlit could be associated with a different python installation):

python -m streamlit run demos/streamlit_demo.py

These demos are provided as open-source examples for library users.

📖 Documentation

Available Criteria

LinearCriterion

Simple linear normalization (0-100)

evaluator.linear('experience', weight=0.3, higher_is_better=True)

Parameters:

  • column: Column name to evaluate
  • weight: Criterion weight (0-1)
  • higher_is_better: If True, higher values score better (default: True)

ThresholdCriterion

Assign scores based on value ranges

evaluator.threshold('team_size', weight=0.2, thresholds=[
    (0, 5, 60),           # 0-4 people: 60 points
    (5, 10, 80),          # 5-9 people: 80 points
    (10, float('inf'), 100)  # 10+ people: 100 points
])

Parameters:

  • column: Column name to evaluate
  • weight: Criterion weight
  • thresholds: List of (lower, upper, score) tuples

MinimumRatioCriterion

Score based on ratio to minimum value (common for prices)

evaluator.min_ratio('bid_amount', weight=0.5)

Formula: score = (min_value / value) * 100

Best for: Price evaluation where lower is better


DirectScoreCriterion

Use pre-evaluated scores (e.g., from evaluation committee)

evaluator.direct('committee_score', weight=0.3, input_scale=10)

Parameters:

  • input_scale: Original scale of scores (default: 100)
  • Automatically converts to 0-100 scale

FormulaCriterion

Score bids using a math expression. Uses simpleeval for safe evaluation.

evaluator.formula('bid_amount', weight=0.4,
                  formula='100 - abs(value - target) / target * 100',
                  variables={'target': 50_000_000})

Available in formulas:

  • value — the current bid value
  • min, max, mean, median, std — statistics from all values
  • Custom variables passed via variables dict
  • Functions: abs, min, max, sqrt, log, log10, exp, clip(x, lo, hi)

Config-based:

config = {
    'bid_amount': {
        'type': 'formula',
        'weight': 0.4,
        'formula': '100 - abs(value - target) / target * 100',
        'variables': {'target': 50_000_000}
    }
}

Scores are automatically clipped to 0–100. Invalid expressions return 0.


CustomCriterion

Define your own evaluation logic

def proximity_to_budget(values, stats):
    """Penalize bids far from reference budget"""
    reference = 50_000_000
    deviation = abs((values - reference) / reference) * 100
    return (100 - deviation * 2).clip(lower=0)

evaluator.custom('bid_amount', weight=0.2, func=proximity_to_budget)

Function signature:

def my_function(values: pd.Series, stats: dict) -> pd.Series:
    """
    Args:
        values: Column values to evaluate
        stats: Auto-calculated statistics (min, max, mean, median, std, q25, q75)
    
    Returns:
        Series of scores (0-100)
    """
    return scores

Configuration Methods

Fluent Interface (Recommended)

result = (Evaluator()
    .min_ratio('price', 0.4)
    .linear('experience', 0.3, higher_is_better=True)
    .direct('quality', 0.3)
    .evaluate(bids_df))

Dictionary Configuration

config = {
    'price': {'type': 'min_ratio', 'weight': 0.4},
    'experience': {'type': 'linear', 'weight': 0.3, 'higher_is_better': True},
    'quality': {'type': 'direct', 'weight': 0.3}
}

evaluator = Evaluator.from_config(config)
result = evaluator.evaluate(bids_df)

YAML Configuration

# config.yaml
criteria:
  price:
    type: min_ratio
    weight: 0.4
  
  experience:
    type: linear
    weight: 0.3
    higher_is_better: true
  
  quality:
    type: direct
    weight: 0.3
evaluator = Evaluator.from_yaml('config.yaml')
result = evaluator.evaluate(bids_df)

Multi-Stage Evaluation

Real-world procurement often evaluates bids in stages: a technical stage eliminates unqualified bids, then an economic stage ranks the survivors. StagedEvaluator supports this pattern.

Quick Start

from bid_evaluation import StagedEvaluator
import pandas as pd

bids = pd.DataFrame({
    'vendor': ['Alpha', 'Beta', 'Gamma', 'Delta'],
    'experience': [15, 3, 10, 7],
    'quality_score': [88, 45, 92, 65],
    'bid_amount': [120_000, 85_000, 145_000, 95_000],
})

result = (StagedEvaluator()
    .add_stage('Technical', filter_type='score_threshold', threshold=60)
        .linear('experience', 0.4, higher_is_better=True)
        .direct('quality_score', 0.6)
    .add_stage('Economic')
        .min_ratio('bid_amount', 1.0)
    .evaluate(bids))

print(result[['vendor', 'technical_score', 'eliminated_at_stage', 'final_score', 'ranking']])

How It Works

  1. Bids are evaluated in sequential stages, each with its own criteria
  2. After each stage (except the last), a filter can eliminate bids:
    • score_threshold — bids must score at or above a minimum
    • top_n — only the top N bids advance (with configurable tie-breaking)
  3. Only surviving bids advance to the next stage
  4. Eliminated bids are marked with the stage where they were removed

Filter Types

# Score threshold: bids must score >= 60 to advance
.add_stage('Technical', filter_type='score_threshold', threshold=60)

# Top N: only the best 5 bids advance
.add_stage('Shortlist', filter_type='top_n', top_n=5)

# Top N with tie-breaking: exclude tied bids at the cutoff
.add_stage('Shortlist', filter_type='top_n', top_n=5, on_tie='exclude')

Final Score Modes

# Default: ranking based on the last stage's score only
staged = StagedEvaluator(final_score_mode='last_stage')

# Weighted combination: weighted average of all stage scores
staged = StagedEvaluator(final_score_mode='weighted_combination')

Config-Based Setup

config = {
    'final_score_mode': 'last_stage',
    'stages': [
        {
            'name': 'Technical',
            'weight': 0.6,
            'filter': {'type': 'score_threshold', 'threshold': 60},
            'criteria': {
                'experience': {'type': 'linear', 'weight': 0.4, 'higher_is_better': True},
                'quality_score': {'type': 'direct', 'weight': 0.6}
            }
        },
        {
            'name': 'Economic',
            'weight': 0.4,
            'criteria': {
                'bid_amount': {'type': 'min_ratio', 'weight': 1.0}
            }
        }
    ]
}

result = StagedEvaluator.from_config(config).evaluate(bids)

# Also available: from_yaml() and from_json()

Output Columns

The result DataFrame includes:

  • {name}_score — score per stage
  • {name}_ranking — ranking within each stage
  • eliminated_at_stage — stage name where the bid was eliminated, or None
  • final_score — overall score (from last stage or weighted combination)
  • ranking — final ranking (NaN for eliminated bids)

Inspection

# Summary of all stages, criteria, and filters
staged.summary()

# Per-stage statistics (after evaluation)
staged.get_statistics()

# Detailed stage results (advanced/eliminated indices)
staged.get_stage_results()

For full documentation, see README_STAGED.md.


Working with Results

# Evaluate
result = evaluator.evaluate(bids_df)

# Access results
print(result[['vendor', 'ranking', 'final_score']])

# Detailed scores
score_cols = [c for c in result.columns if c.startswith('score_')]
print(result[['vendor'] + score_cols])

# Get statistics
stats = evaluator.get_statistics()
for criterion, values in stats.items():
    print(f"{criterion}: min={values['min']}, max={values['max']}")

# Export
result.to_excel('results.xlsx', index=False)
result.to_csv('results.csv', index=False)

Weight Normalization

# Automatic normalization (default)
evaluator = Evaluator(normalize_weights=True)
evaluator.linear('price', 0.6)
evaluator.linear('quality', 0.4)
# Weights sum to 1.0 automatically

# Manual weights (sum must equal desired total)
evaluator = Evaluator(normalize_weights=False)
evaluator.linear('price', 60)
evaluator.linear('quality', 40)
# Final score = sum of weighted scores

🛣️ Roadmap

Planned features (vote with 👍 on issues):

  • Multi-stage evaluation - Sequential stages with filtering between them
  • Formula criterion - User-defined math expressions via simpleeval
  • Unit tests - Test coverage for core and staged evaluation
  • Admissibility checks - Required fields, min/max validation, document verification
  • Report generation - PDF/Excel reports with charts and detailed breakdowns
  • Template library - Pre-configured setups for common procurement types
  • Better documentation - Video tutorials, comprehensive guides
  • Performance optimization - Handle larger datasets efficiently

💡 Use Cases

This library is useful for:

  • 🏛️ Government procurement evaluation committees
  • 🏢 Companies bidding on public contracts
  • 💼 Procurement consultants and advisors
  • 🔬 Researchers studying procurement processes
  • 📊 Anyone needing objective, transparent bid evaluation

📧 Contact

📄 License

MIT License

⭐ Star History

If you find this useful, give it a star! ⭐

It helps others discover the project and motivates continued development.


Metadata

Release files for bid-evaluation 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bid-evaluation 0.1.0
File Size Uploaded
bid_evaluation-0.1.0.tar.gz 28.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bid-evaluation 0.1.0
File Interpreter ABI Platform
bid_evaluation-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 49.0 kB

Release files / bid_evaluation-0.1.0.tar.gz

Download URL bid_evaluation-0.1.0.tar.gz
Size 28.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d376dd9ca302429239f191b653c1222fd6cb0f0fa577602c1dc7a19a386a9eb6
BLAKE2b-256 checksum
How to use checksums
81a0650ff27da1518e530dd61abc4486fa15b3e60526a425845926a584ce87c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Release files / bid_evaluation-0.1.0-py3-none-any.whl

Download URL bid_evaluation-0.1.0-py3-none-any.whl
Size 20.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4c8661e50a0e3206bf7e9af7e7297565d66596c631ff0d0c8e7abbcd2490fb5c
BLAKE2b-256 checksum
How to use checksums
32826dc020306ec2a4b17ec628fc549ce85b1471300695b1aa882375ed6af00c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page