ML in Courts of Accounts: Auditing Public Works with Python
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:
- Screen thousands of processes in minutes, not months
- Detect irregularity patterns that would be invisible to the human eye
- Prioritize limited resources for the highest-risk cases
- 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
Related Articles
AI in Curation of Historical Photographic Collections in 2026
How machine learning systems are revolutionizing the cataloging, restoration, and curation of historical photographic collections in museums and public archives in Brazil and worldwide.
ML in Newborn Screening: Detecting Rare Diseases in Newborns
Machine learning models analyze genomic and metabolic data to identify rare diseases in newborns, enabling early interventions that save lives.
Nuclear Decommissioning: Robots and ML in 2026
Autonomous robots, machine learning, and predictive monitoring are transforming nuclear decommissioning. Analysis of real projects and efficiency data.