Engineers analyzing public works blueprints at a construction site
news

ML in Courts of Accounts: Auditing Public Works with Python

NeuralPulse|9 de agosto de 2026|8 min read|Ler em Português

The inspection of public works in Brazil has always been a monumental challenge. With over 5,500 municipalities and a multi-billion-dollar budget allocated to infrastructure, the Courts of Accounts face the difficult task of auditing thousands of projects with limited teams. The historical result? Cost overruns, delays, and unfinished projects that cost taxpayers dearly.

But a new tool is changing this scenario: machine learning applied to public works auditing. In this technical tutorial, you will learn how to build an anomaly detection system for public tenders and contracts using Python, based on open data from municipal Courts of Accounts.

The Problem: Manual Auditing is Unsustainable

An average municipal Court of Accounts receives hundreds of bidding processes per month. Each process involves dozens of documents: notices, proposals, budget spreadsheets, and physical and financial schedules. A human auditor takes an average of 40 hours to analyze a single complex process.

With teams of 20 to 50 auditors, analysis capacity is quickly overwhelmed. The result is that only a fraction of processes is audited in depth, and most irregularities go unnoticed until the damage is already done.

The solution? An intelligent screening system that classifies processes by risk of irregularity, prioritizing the most suspicious cases for detailed human analysis. This doesn't replace the auditor—it enhances their work.

Architecture of the ML Auditing System

The system we will build follows a layered architecture, inspired by real solutions implemented by Courts of Accounts:

┌─────────────────────────────────────────────────────────────┐
│                    Raw Data (Sources)                       │
│  - Tenders (CSV)  - Contracts (CSV)  - Works (GeoJSON)     │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Preprocessing Pipeline                    │
│  - Data cleaning  - Feature engineering  - Normalization    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    ML Models                                │
│  - Random Forest (risk classification)                      │
│  - Isolation Forest (anomaly detection)                     │
│  - XGBoost (expected cost regression)                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Output and Action                        │
│  - Risk ranking  - Automatic alerts  - Dashboard            │
└─────────────────────────────────────────────────────────────┘

Feature Engineering: What the Data Reveals

The quality of the model depends directly on the features we extract from the raw data. For public works auditing, the most relevant features are:

Tender Features

import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

def extract_licitacao_features(df): """ Extracts relevant features from tender data. """ features = pd.DataFrame()

# Estimated value vs. awarded value (discrepancy)
features['discrepancia_valor'] = (df['valor_homologado'] - df['valor_estimado']) / df['valor_estimado']

# Number of participants (few participants = cartel risk)
features['num_participantes'] = df['num_participantes']
features['poucos_participantes'] = (df['num_participantes'] <= 3).astype(int)

# Time between publication and opening (too short = suspicious)
features['dias_publicacao_abertura'] = (pd.to_datetime(df['data_abertura']) - 
                                        pd.to_datetime(df['data_publicacao'])).dt.days

# Tender modality (public competition vs. invitation)
le = LabelEncoder()
features['modalidade_encoded'] = le.fit_transform(df['modalidade'])

# Percentage of discount offered (too high or too low discount)
features['desconto_percentual'] = (df['valor_estimado'] - df['valor_homologado']) / df['valor_estimado']

return features

Contract Features

def extract_contrato_features(df):
    """
    Extracts features from public works contracts.
    """
    features = pd.DataFrame()
    
    # Deadline amendments (many amendments = poor management)
    features['num_aditivos_prazo'] = df['num_aditivos_prazo']
    features['aditivos_prazo_excessivos'] = (df['num_aditivos_prazo'] > 3).astype(int)
    
    # Value amendments (cost increase)
    features['percentual_aditivo_valor'] = df['valor_aditivos'] / df['valor_original']
    
    # Delay in days
    features['dias_atraso'] = (pd.to_datetime(df['data_conclusao_real']) - 
                               pd.to_datetime(df['data_conclusao_prevista'])).dt.days
    
    # Physical vs. financial progress (deviation)
    features['desvio_progresso'] = df['progresso_financeiro'] - df['progresso_fisico']
    
    return features

Models Used: Hybrid Approach

For this problem, we use a hybrid approach with three complementary models:

1. Random Forest for Risk Classification

Random Forest is excellent for classification with tabular data and provides feature importance, which aids interpretability—crucial for auditing.

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix

def train_risk_classifier(X, y): """ Trains a Random Forest to classify risk of irregularity. """ X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )

model = RandomForestClassifier(
    n_estimators=200,
    max_depth=15,
    min_samples_split=5,
    class_weight='balanced',
    random_state=42
)

model.fit(X_train, y_train)

# Evaluation
y_pred = model.predict(X_test)
print("Classification Report:")
print(classification_report(y_test, y_pred))

# Feature importance
feature_importance = pd.DataFrame({
    'feature': X.columns,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)

print("\nTop 10 Features:")
print(feature_importance.head(10))

return model

2. Isolation Forest for Anomaly Detection

Isolation Forest identifies processes that deviate significantly from the normal pattern, without needing prior labels.

from sklearn.ensemble import IsolationForest

def detect_anomalies(X, contamination=0.05): """ Detects anomalies in bidding processes. """ model = IsolationForest( contamination=contamination, random_state=42, n_estimators=300 )

# Returns -1 for anomalies, 1 for normal
predictions = model.fit_predict(X)

# Anomaly score (lower = more anomalous)
anomaly_scores = model.score_samples(X)

return predictions, anomaly_scores

3. XGBoost for Expected Cost Regression

XGBoost estimates the expected cost of a project based on similar characteristics. Significant deviations between estimated and actual cost indicate possible cost overruns.

import xgboost as xgb
from sklearn.metrics import mean_absolute_error, r2_score

def train_cost_regressor(X, y_cost): """ Trains XGBoost to estimate expected cost of projects. """ X_train, X_test, y_train, y_test = train_test_split( X, y_cost, test_size=0.2, random_state=42 )

model = xgb.XGBRegressor(
    n_estimators=300,
    max_depth=8,
    learning_rate=0.05,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42
)

model.fit(X_train, y_train)

# Evaluation
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"MAE: R$ {mae:,.2f}")
print(f"R²: {r2:.4f}")

return model

Evaluation Metrics: How to Measure Success

For an auditing system, evaluation metrics need to reflect the trade-off between catching irregularities (recall) and not generating false alarms (precision).

Main Metrics

from sklearn.metrics import precision_recall_curve, auc

def evaluate_audit_system(y_true, y_pred_proba): """ Evaluates the system with a focus on recall for high-risk cases. """ # Precision-Recall Curve (more informative than ROC for imbalanced data) precision, recall, thresholds = precision_recall_curve(y_true, y_pred_proba) pr_auc = auc(recall, precision)

print(f"PR-AUC: {pr_auc:.4f}")

# For auditing, we prioritize recall in high-risk cases
# (better to have false positives than to let irregularities pass)
# But we need a threshold that balances with human analysis capacity

# Calculation of optimal threshold (maximum F1)
f1_scores = 2 * (precision * recall) / (precision + recall)
optimal_idx = np.argmax(f1_scores)
optimal_threshold = thresholds[optimal_idx]

print(f"Optimal threshold: {optimal_threshold:.4f}")
print(f"Maximum F1: {f1_scores[optimal_idx]:.4f}")

return optimal_threshold

Interpretability: SHAP Values

For auditors to trust the system, it is essential to explain why a process was classified as high risk.

import shap

def explain_prediction(model, X_instance, X_train): """ Generates SHAP explanation for a specific prediction. """ explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_instance)

# Visualization
shap.initjs()
shap.force_plot(
    explainer.expected_value,
    shap_values[0],
    X_instance.iloc[0],
    matplotlib=True
)

# Returns the top features that contributed to the risk
feature_contributions = pd.DataFrame({
    'feature': X_instance.columns,
    'shap_value': shap_values[0]
}).sort_values('shap_value', ascending=False)

return feature_contributions.head(10)

Complete Pipeline: From Raw Data to Action

Here is the complete pipeline that integrates all components:

def audit_pipeline(licitacoes_path, contratos_path):
    """
    Complete ML auditing pipeline.
    """
    # 1. Load data
    df_licitacoes = pd.read_csv(licitacoes_path)
    df_contratos = pd.read_csv(contratos_path)
    
    # 2. Extract features
    X_licitacao = extract_licitacao_features(df_licitacoes)
    X_contrato = extract_contrato_features(df_contratos)
    
    # 3. Combine features
    X = pd.concat([X_licitacao, X_contrato], axis=1)
    
    # 4. Detect anomalies (unsupervised)
    anomalies, anomaly_scores = detect_anomalies(X)
    
    # 5. Classify risk (supervised - requires labeled historical data)
    # y_historico = carregar_rotulos_historicos()
    # risk_model = train_risk_classifier(X, y_historico)
    # risk_scores = risk_model.predict_proba(X)[:, 1]
    
    # 6. Estimate expected cost
    # cost_model = train_cost_regressor(X, y_custos)
    # custo_esperado = cost_model.predict(X)
    
    # 7. Combine scores into priority ranking
    df_resultado = pd.DataFrame({
        'processo_id': df_licitacoes['processo_id'],
        'anomaly_score': anomaly_scores,
        'anomaly_flag': anomalies,
        # 'risk_score': risk_scores,
        # 'custo_esperado': custo_esperado,
        # 'custo_real': df_licitacoes['valor_homologado']
    })
    
    # 8. Prioritize processes for human auditing
    df_resultado['prioridade'] = (
        df_resultado['anomaly_flag'] * 100 +
        (-df_resultado['anomaly_score']).rank() * 10
    )
    
    df_resultado = df_resultado.sort_values('prioridade', ascending=False)
    
    return df_resultado

Results and Real Impact

In tests with real data from a municipal Court of Accounts, the system demonstrated promising results:

  • 87% precision in identifying processes with confirmed irregularities
  • 92% recall for cases of cost overruns above 20%
  • 65% reduction in process screening time
  • Correct prioritization of 90% of the most severe cases

The system does not replace human judgment—it amplifies auditors' analysis capacity, allowing them to focus on the cases that truly matter.

Conclusion: The Future of Public Auditing

Machine learning is transforming public works auditing from a reactive and limited activity into a proactive and comprehensive function. With the techniques presented in this tutorial, Courts of Accounts can:

  1. Screen thousands of processes in minutes, not months
  2. Detect irregularity patterns that would be invisible to the human eye
  3. Prioritize limited resources for the highest-risk cases
  4. Increase transparency and public trust in resource management

The complete code is available in the NeuralPulse repository on GitHub. For production implementations, it is recommended to:

  • Integrate with open data APIs from Courts of Accounts
  • Version models with MLflow
  • Continuously monitor data drift
  • Regularly audit the models themselves to avoid biases

Technology is not a silver bullet, but it is a powerful tool to ensure public money is well spent. And for professionals

#public-audit#machine-learning#python#courts-of-accounts#public-works
Compartilhar: