Explorando os contratos da cidade de São Paulo

Na primeira parte deste tutorial, procurei mostrar como:

  1. Fazer o cadastro na API do SOF
  2. Acessar os registros de empenhos (a execução orçamentária)
  3. Utilizar o Pandas para explorar algumas análises
  4. Fazer download da base em CSV

Neste segundo tutorial, vamos focar em outra consulta disponível na mesma API: os contratos.

A Prefeitura de São Paulo ainda não dispõe de um sistema centralizado para gestão de contratos (existe um em implementação).

A boa notícia é que, com relação a contratos, se pode considerar uma das cidades mais transparentes do Brasil por ser uma das poucas a disponibilizar os termos de seus contratos e convênios na íntegra, desde 2014, nesta página do Portal da Transparência. Com o Marco Regulatório da Sociedade Civil, vigente a partir deste ano, também começam a aparecer por lá os Termos de Cooperação (sem transferência de recursos) que antes não eram publicados.Também as doações começaram a aparecer aí.

A má notícia é que nessa base do Portal você consegue extrair as informações publicadas em Diário Oficial, mas pode haver inconsistências (geradas por erros de publicação no D.O.). Às vezes há duplicidade, republicação, valores equivocados (pois são digitados manualmente na hora de publicar).

Por isso, podemos considerar que o registro mais confiável que há sobre a execução dos contratos é o do próprio sistema de Execução Orçamentária (o SOF), uma vez que é necessário cadastrar os contratos para realizar os pagamentos. E é daí a relevância desta API.

Importante!

Não tenho certeza se os contratos são imediatamente cadastrados no SOF ou se passam a existir no sistema apenas quando é gerado empenho vinculado a ele. Aqui neste exemplo eu encontrei empenhos zerados em alguns contratos, o que indica que não. Ainda assim, se quiser tratar no universo inteiro de contratos e não apenas da execução orçamentária, recomendo o 'double check' com a base do Portal da Transparência -- explico mais no último passo deste tutorial.

Passo 1. Consulta à API

Seguiremos os passos detalhados no tutorial anterior para acessar essa consulta na API e utilizar o Pandas para montar um DataFrame.


In [1]:
import pandas as pd
import requests
import json
import numpy as np

In [2]:
TOKEN = '198f959a5f39a1c441c7c863423264'
base_url = "https://gatewayapi.prodam.sp.gov.br:443/financas/orcamento/sof/v2.1.0"
headers={'Authorization' : str('Bearer ' + TOKEN)}

Um parêntesis! Como saber qual é o código do órgão que quero pesquisar?

Na consulta anterior, eu indiquei que o Código da Educação no Orçamento, por exemplo, é 16; o Fundo Municipal de Saúde, 84. Quem mexe com o orçamento, acaba se acostumando com os números. Mas como saber o código de todas as secretarias e empresas municipais? Existe uma consulta específica para isso na API, que você pode fazer lá no próprio console. Mas vamos trazer essa tabela para cá, para facilitar:


In [3]:
url_orgaos = '{base_url}/consultarOrgaos?anoExercicio=2017'.format(base_url=base_url)

In [4]:
request_orgaos = requests.get(url_orgaos,
                                headers=headers,
                                verify=True).json()

In [5]:
df_orgaos = pd.DataFrame(request_orgaos['lstOrgaos'])

In [15]:
df_orgaos


Out[15]:
codOrgao txtDescricaoOrgao
0 01 Autarquia Hospitalar Municipal
1 02 Hospital do Servidor Público Municipal
2 03 Instituto de Previdência Municipal de São Paulo
3 04 Serviço Funerário do Município de São Paulo
4 05 Autarquia Hospitalar Municipal Regional Leste
5 06 Autarquia Hospitalar Municipal Regional Sudeste
6 07 Fundo Municipal de Desenvolvimento Social
7 08 Fundo Municipal do Idoso
8 09 Câmara Municipal de São Paulo
9 10 Tribunal de Contas do Município de São Paulo
10 11 Secretaria do Governo Municipal
11 12 SECRETARIA MUNICIPAL DAS PREFEITURAS REGIONAIS
12 13 Secretaria Municipal de Gestão
13 14 Secretaria Municipal de Habitação
14 15 Secretaria Municipal de Modernização, Gestão ...
15 16 Secretaria Municipal de Educação
16 17 Secretaria Municipal da Fazenda
17 18 Secretaria Municipal da Saúde
18 19 SECRETARIA MUNICPAL DE ESPORTES E LAZER
19 20 SECRETARIA MUNICIPAL DE MOBILIDADE E TRANSPORTES
20 21 PROCURADORIA GERAL DO MUNICIPIO DE SÃO PAULO
21 22 SECRETARIA MUNICIPAL DE SERVIÇOS E OBRAS
22 23 SECRETARIA MUNICIPAL DE INOVAÇÃO E TECNOLOGIA
23 24 Secretaria Municipal de Assistência e Desenvol...
24 25 Secretaria Municipal de Cultura
25 26 Secretaria Municipal de Abastecimento
26 27 Secretaria Municipal do Verde e do Meio Ambiente
27 28 Encargos Gerais do Município
28 29 Secretaria Municipal de Comunicação
29 30 SECRETARIA MUNICIPAL DE TRABALHO E EMPREENDEDO...
... ... ...
68 69 PREFEITURA REGIONAL DE VILA PRUDENTE
69 70 PREFEITURA REGIONAL SÃO MATEUS
70 71 PREFEITURA REGIONAL CIDADE TIRADENTES
71 72 PREFEITURA REGIONAL SAPOPEMBA
72 74 Secretaria Municipal de Comunicação e Informaç...
73 75 Fundo Municipal de Parques
74 76 Fundo Especial de Despesas da Câmara Municipal...
75 77 Fundo Especial de Despesas do Tribunal de Contas
76 78 Secretaria Municipal de Licenciamento
77 79 Secretaria Municipal de Políticas para as Mulh...
78 80 FUNDAÇÃO PAULISTANA DE EDUCAÇÃO, TECNOLOGIA E ...
79 81 Autoridade Municipal de Limpeza Urbana
80 82 FUNDAÇÃO CATAVENTO
81 83 Companhia Metropolitana de Habitação de São Paulo
82 84 FUNDO MUNICIPAL DE SAÚDE
83 85 Fundação Theatro Municipal de São Paulo
84 86 Fundo Municipal de Saneamento Ambiental e Infr...
85 87 Fundo Municipal de Desenvolvimento de Trânsito
86 88 Fundo de Preservação do Patrimônio Histórico e...
87 89 Fundo Municipal de Esportes, Lazer e Recreação
88 90 Fundo Municipal dos Direitos da Criança e do A...
89 91 Fundo Municipal de Habitação
90 92 Fundo Mun. do Sist. dos Corredores Segregados ...
91 93 Fundo Municipal de Assistência Social
92 94 Fundo Especial do Meio Ambiente e Desenvolvime...
93 95 Fundo Especial de Promoção de Atividades Cultu...
94 96 Fundo Municipal de Turismo
95 97 Fundo de Proteção do Patrimônio Cultural e Amb...
96 98 Fundo de Desenvolvimento Urbano
97 99 Fundo Municipal de Iluminação Pública

98 rows × 2 columns

Atenção para uma distinção conceitual importante!

  • ÓRGÃO = Administração Direta (Prefeituras Regionais, Secretarias)
  • EMPRESA = Administração Indireta (Fundações, Empresas Públicas)

Os campos na consulta da API são diferentes. Então se você inserir o Código da Prodam (uma empresa pública) no Campo Órgão, por exemplo, a consulta vai retornar 0 Registros.

Pronto, aí temos a lista completa. Fecha parêntesis!

Seguindo, vamos fazer a consulta dos contratos da Secretaria do Verde e Meio Ambiente -- Órgão 27, conforme a lista acima.


In [7]:
url_contratos = '{base_url}/consultaContrato?anoContrato=2017&codOrgao=27'.format(base_url=base_url)

In [8]:
request_contratos = requests.get(url_contratos,
                                headers=headers,
                                verify=True).json()

In [9]:
number_of_pages = request_contratos['metadados']['qtdPaginas']

todos_contratos = []
todos_contratos = todos_contratos + request_contratos['lstContratos']
if number_of_pages>1:
    for p in range(2, number_of_pages+1):
        request_contratos = requests.get(url_contratos + pagination.format(PAGE=p), headers=headers, verify=True).json()
        todos_contratos = todos_contratos + request_contratos['lstContratos']

Passo 2. Mãos ao Pandas!


In [10]:
df_contratos = pd.DataFrame(todos_contratos)

Vamos checar como ficou nossa base de dados de contratos, mostrando os últimos 5 registros:


In [11]:
df_contratos.tail()


Out[11]:
anoContrato codContrato codEmpresa codEvento codModalidade codOrgao codTipoContratacao datAssinaturaContrato datPublicacaoContrato datVigencia ... txtTipoContratacao valAditamentos valAnulacao valAnuladoEmpenho valEmpenhadoLiquido valLiquidado valPago valPrincipal valReajustes valTotalEmpenhado
75 2017 3487 01 None 6 27 5 None None 12/04/2017 ... Nota de Empenho 0 0 0.0 1896.0 1896.00 1896.00 1896.0 0 1896.0
76 2017 2259 01 None 8 27 5 None None 31/12/2017 ... Nota de Empenho 0 0 0.0 1000.0 0.00 0.00 1000.0 0 1000.0
77 2017 2216 01 None 6 27 5 None None 15/03/2017 ... Nota de Empenho 0 0 0.0 2565.0 2565.00 2565.00 2565.0 0 2565.0
78 2017 2221 01 None 8 27 5 None None 31/12/2017 ... Nota de Empenho 0 0 0.0 7500.0 1878.26 1878.26 7500.0 0 7500.0
79 2017 196 01 None 6 27 5 None None 31/12/2017 ... Nota de Empenho 0 0 0.0 2232.0 413.42 413.42 2232.0 0 2232.0

5 rows × 25 columns

São muitas as colunas dessa consulta (a visualização acima omite algumas). Vamos checar quais são:


In [12]:
list(df_contratos)


Out[12]:
['anoContrato',
 'codContrato',
 'codEmpresa',
 'codEvento',
 'codModalidade',
 'codOrgao',
 'codTipoContratacao',
 'datAssinaturaContrato',
 'datPublicacaoContrato',
 'datVigencia',
 'numOriginalContrato',
 'txtDescricaoModalidade',
 'txtDescricaoOrgao',
 'txtObjetoContrato',
 'txtRazaoSocial',
 'txtTipoContratacao',
 'valAditamentos',
 'valAnulacao',
 'valAnuladoEmpenho',
 'valEmpenhadoLiquido',
 'valLiquidado',
 'valPago',
 'valPrincipal',
 'valReajustes',
 'valTotalEmpenhado']

In [16]:
df_contratos.to_excel('contratos.xls')

Por modalidade de contratação


In [17]:
df_contratos.groupby(['txtDescricaoModalidade'])['valEmpenhadoLiquido', 'valPago'].sum().sort_values(['valEmpenhadoLiquido'], ascending=False)


Out[17]:
valEmpenhadoLiquido valPago
txtDescricaoModalidade
Dispensa de Licitação 6405664.44 3188859.58
Pregão 6301100.70 543575.43
Inexigibilidade 181947.92 8349.92
Não Aplicável 21000.00 14378.26

Vamos remover o txtRazaoSocial, para não nos atrapalhar mais tarde (vamos obter o dado de outro local). Esta consulta não retorna o nome das empresas contratadas, mas "PREFEITURA DE SÃO PAULO" para todos os valores -- provavelmente um erro da API.


In [18]:
df_contratos.drop('txtRazaoSocial', axis=1, inplace=True)

Cruzando as bases

Infelizmente, a consulta não vem "pronta". Vamos precisar cruzar com a base de empenhos do primeiro tutorial para conseguir, por exemplo, saber quais são os credores POR CONTRATO. A imagem abaixo foi exibida durante o lançamento da API, no Café Hacker da Controladoria Geral do Município e da Secretaria Municipal da Fazenda, e explica como as consultas se relacionam (o 'Simplificada' é por conta deles, hehe):

O Código de Contrato é um parâmetro facultativo da consulta de empenhos, mas não vem no retorno de dados (outro possível furo da API, que vamos alertar aos desenvolvedores para tentar corrigir). O que isso significa? Que precisaríamos consultar um a um para obter todos os contratos na lista de empenhos! Felizmente, a programação tá aí pra isso. Vamos criar uma estrutura de repetição semelhante à que fizemos para a paginação:


In [19]:
url_empenho = '{base_url}/consultaEmpenhos?anoEmpenho=2017&mesEmpenho=08&codOrgao=27'.format(base_url=base_url)
num_contrato = '&codContrato={CONTRATO}'

In [20]:
lista_contratos = list(df_contratos['codContrato'])
len(lista_contratos)


Out[20]:
80

In [21]:
request_empenhos = requests.get(url_empenho,
                                headers=headers,
                                verify=True).json()

In [22]:
def add_codigo_contrato(empenhos, cod_contrato):
    """""
    Adiciona Código de Contrato no dict de cada empenho consultado. 
    """""
    for item in empenhos:
        item.update({'codContrato': cod_contrato})
    return empenhos

In [28]:
todos_empenhos = []

todos_empenhos = todos_empenhos + request_empenhos['lstEmpenhos']

for n in lista_contratos:
    response = requests.get(url_empenho + num_contrato.format(CONTRATO=n), headers=headers, verify=True).json()
    empenhos_c_cod = add_codigo_contrato(response['lstEmpenhos'], n)
    todos_empenhos = todos_empenhos + empenhos_c_cod

In [30]:
df_empenhos_c_contratos = pd.DataFrame(todos_empenhos)

Agora já temos o DataFrame que junta todos Empenhos do Verde de 2017 com as informações de Contrato, deixando a base mais rica -- e corrigindo essa falha da falta de Razão Social e CNPJ na consulta de contratos!


In [33]:
df_empenhos_c_contratos.head()


Out[33]:
anoEmpenho codCategoria codContrato codElemento codEmpenho codEmpresa codFonteRecurso codFuncao codGrupo codItemDespesa ... txtDescricaoUnidade txtGrupoDespesa txtModalidadeAplicacao txtRazaoSocial valAnuladoEmpenho valEmpenhadoLiquido valLiquidado valPagoExercicio valPagoRestos valTotalEmpenhado
0 2017 3 NaN 30 8458 01 00 18 3 01 ... Gabinete do Secretário Outras Despesas Correntes Aplicações Diretas ATMOSFERA GASES ESPECIAIS E EPI S LTDA - EPP 0.0 8244.00 3474.00 3474.00 0 8244.00
1 2017 3 NaN 30 70426 01 00 18 3 01 ... Gabinete do Secretário Outras Despesas Correntes Aplicações Diretas AGLON COMÉRCIO E REPRESENTACÕES LTDA 0.0 420.00 420.00 0.00 0 420.00
2 2017 3 NaN 30 42021 01 00 18 3 01 ... Gabinete do Secretário Outras Despesas Correntes Aplicações Diretas MANZATOS FARMA LTDA ME 0.0 262.00 262.00 262.00 0 262.00
3 2017 3 NaN 30 34570 01 00 18 3 01 ... Gabinete do Secretário Outras Despesas Correntes Aplicações Diretas CRISTALIA PRODUTOS QUIMICOS FARMACEUTICOS LTDA 0.0 183.00 183.00 183.00 0 183.00
4 2017 3 NaN 30 36186 01 00 18 3 01 ... Gabinete do Secretário Outras Despesas Correntes Aplicações Diretas FARMACE - INDUSTRIA QUIMICO-FARMACEUTICA CEARE... 0.0 271.97 271.97 271.97 0 271.97

5 rows × 42 columns

A tabela acima vai ter muitos valores "Nan" para codContrato -- pois há 493 empenhos e apenas 80 contratos. Como o interesse agora é só nos contratos, vamos retirar esses casos e montar um novo DataFrame que contém apenas contratos com algum empenho relacionado:


In [34]:
df_empenhos_c_contratos = df_empenhos_c_contratos.dropna(axis=0).reset_index(drop=True)

Mudando o formato de número de decimal (tirar aquele .0 dali) para integer:


In [35]:
df_empenhos_c_contratos['codContrato'] = df_empenhos_c_contratos.loc[:,'codContrato'].astype(int)

Agora temos duas bases de dados para trabalhar:

  • df_contratos = Primeira base que extraímos, contém todos os contratos (sem Razão Social ou CNPJ, por limitação da API)
  • df_empenhos_c_contratos = Base que contém todos os empenhos com código de contrato, depois do cruzamento que fizemos

Ambas têm várias colunas em comum. Então, antes de juntá-las, vamos apenas retirar as colunas únicas nas duas, com o método "difference":


In [36]:
cols_to_use = df_empenhos_c_contratos.columns.difference(df_contratos.columns)

In [37]:
df_contratos_empenhados = df_contratos.merge(df_empenhos_c_contratos[cols_to_use], left_index=True, right_index=True, how='outer')

'Top 10' Contratos de 2017

Agora com a lista de todos os contratos do ano, vamos montar uma tabela e ordenar os dados pelo Valor Principal do Contrato. No Manual da API, aprende-se que esse campo 'valPrincipal'significa o "Valor do contrato sem ocorrência de reajustamentos, ou aditamentos".


In [76]:
top10 = df_contratos_empenhados[['txtDescricaoModalidade',
                                 'txtObjetoContrato',
                                 'txtRazaoSocial',
                                 'numCpfCnpj',
                                 'valPrincipal']].sort_values(['valPrincipal'], ascending=False)[:10]

In [77]:
top10


Out[77]:
txtDescricaoModalidade txtObjetoContrato txtRazaoSocial numCpfCnpj valPrincipal
39 Pregão Segurança e Vigilancia Patrimonial Desarmada... WCOM TELECOMUNICAÇÕES LTDA - ME 01544859000124 6833864.80
70 Dispensa de Licitação CONSUMO DE ÁGUA E ESGOTO. SÃO PAULO SECRETARIA MUNICIPAL DE TRANSPORTES 46392155000383 4200000.00
6 Pregão Serviços técnicos de manejo e conservação para... MEDEIROS PAISAGISMO COMERCIO E SERVIÇOS LTDA 04102128000117 3816996.84
71 Dispensa de Licitação FORNECIMENTO DE ENERGIA ELÉTRICA. COMPANHIA DE GAS DE SAO PAULO COMGAS 61856571000117 2200000.00
1 Pregão Serviços de segurança e vigilância patrimonial... HP BRASIL INDÚSTRIA E COMÉRCIO DE EQUIPAMENTOS... 22086683000346 1854994.72
12 Pregão Contração de empresa especializada na prestaçã... SIMPRESS COMERCIO LOCACAO E SERVIÇOS SA 07432517000107 652081.56
0 Pregão Aquisição de ração, grãos e demais suplementos... LICITAVET COMERCIAL LTDA - EPP 09483617000180 408766.67
53 Pregão fornecimento de produtos hortifrutigranjeiros ... ASSOCIAÇÃO NACIONAL DE ORGÃOS MUNICIPAIS DE ME... 03657079000116 206045.19
8 Inexigibilidade Aquisição de peças para os equipamentos Zeiss ... COMERCIAL 3 ALBE LTDA 74400052000191 173598.00
2 Pregão Aquisição de Microcomputador Desktop Tipo I – ... HP BRASIL INDÚSTRIA E COMÉRCIO DE EQUIPAMENTOS... 22086683000346 170589.16

Passo 3 - Só quer salvar em Excel ou CSV?


In [81]:
df_contratos_empenhados.to_excel('exemplos/contratos_empenhados.xls')

In [82]:
df_contratos_empenhados.to_csv('exemplos/contratos_empenhados.csv')

Passo "Bônus" - Comparando com o Portal da Transparência

Como mencionei lá em cima, o Portal da Transparência de São Paulo tem algo muito importante que é publicar os contratos na íntegra nesta página. Não vou entrar nos detalhes dos poréns que surgem aqui -- mas saiba que a ação de publicar o contrato depende de as pessoas subirem o "anexo" certo na hora de enviar o extrato para o Diário Oficial; o que acaba acontecendo é que são cerca de 700 usuários que fazem isso em toda a prefeitura e os erros são frequentes (ex.: subir arquivo de extrato no local de íntegra; indicar modalidade errada; digitar metadado do valor errado etc etc).

Eu já baixei (e arrumei alguns campos) um arquivo de lá com o mesmo exemplo da SVMA. Vamos comparar com o que vem na API do SOF:


In [61]:
df_contratos_portal = pd.read_excel('exemplos/contratos_portal.xls')

In [90]:
df_contratos_portal.sort_values('Valor (R$)', ascending=False).head()


Out[90]:
Órgão Fornecedor CNPJ Objeto Valor (R$) Modalidade Contrato Data de Assinatura Vigência(Dias) Processo Administrativo Licitação Evento Data da Publicação
0 VERDE E MEIO AMBIENTE DEMAX SERVIÇOS E COMÉRCIO LTDA 48.096.044/0001-93 Serviços técnicos de manejo e conservação do L... 13566843.56 COMPRA POR ATA DE REGISTRO DE PREÇO 010/SVMA/2015 2017-08-29 360 2014-0.307.071-3 007/SVMA/2014 EXTRATO DE ADITAMENTO 2017-08-30
3 VERDE E MEIO AMBIENTE DEMAX SERVIÇOS E COMÉRCIO LTDA 48.096.044/0001-93 Serviços técnicos de manejo e conservação do L... 13566843.56 COMPRA POR ATA DE REGISTRO DE PREÇO 010/SVMA/2015 2017-06-21 360 2014-0.307.071-3 007/SVMA/2014 EXTRATO DE ADITAMENTO 2017-06-27
1 VERDE E MEIO AMBIENTE DEMAX SERVIÇOS E COMÉRCIO LTDA 48.096.044/0001-93 Serviços técnicos de manejo e conservação do L... 13566843.56 COMPRA POR ATA DE REGISTRO DE PREÇO 010/SVMA/2015 2017-08-08 360 2014-0.307.071-3 007/SVMA/2014 EXTRATO DE ADITAMENTO 2017-08-11
2 VERDE E MEIO AMBIENTE DEMAX SERVIÇOS E COMÉRCIO LTDA 48.096.044/0001-93 Serviços técnicos de manejo e conservação do L... 13566843.56 COMPRA POR ATA DE REGISTRO DE PREÇO 010/SVMA/2015 2017-06-28 360 2014-0.307.071-3 007/SVMA/2014 EXTRATO DE ADITAMENTO 2017-07-01
4 VERDE E MEIO AMBIENTE PRESSSEG SERVIÇOS DE SEGURANÇA EIRELLI 08.818.229/0001-40 Prestação de Serviços de Vigilância e Seguranç... 10909360.70 PREGÃO ELETRÔNICO 019/SVMA/2017 2017-05-15 360 2013-0.282.697-9 001/SVMA/2014 EXTRATO DE ADITAMENTO 2017-05-18

In [98]:
df_contratos_portal.groupby('Modalidade')['Valor (R$)'].sum()


Out[98]:
Modalidade
ACORDO DE COOPERAÇÃO                   1.127556e+06
COMPRA POR ATA DE REGISTRO DE PREÇO    1.604432e+08
CONVÊNIO                               7.728465e+06
DISPENSA                               2.873275e+06
INEXIGIBILIDADE                        5.218800e+05
PREGÃO ELETRÔNICO                      1.251965e+08
PREGÃO PRESENCIAL                      1.478043e+06
TOMADA DE PREÇOS                       2.989997e+05
Name: Valor (R$), dtype: float64

Vantagens:

  • Tem o número do Processo que gerou o contrato -- outra demanda de melhoria para a API!
  • Inclui outras modalidades: Acordo de cooperação
  • Traz itens mais recentes (4 dias atrás) ainda não cadastrados no SOF (ainda não houve pagamentos)

Desvantagens:

  • Repete valores e itens idênticos -- são compras com o mesmo valor, no mesmo processo? Ou houve retificação no D.O e foi republicado? Não dá para saber. Neste caso o empenho é mais confiável;
  • Existe possibilidade de erro humano ao cadastrar modalidade de licitação, valor.