Redes y contratos públicos
Resultado del análisis que hicimos en el equipo Databuesos para el Datatón Anticorrupción 2019 (México)
import pymongo
import pandas as pd
import json
import matplotlib.pyplot as plt
import networkx as nx
from NetworkUtils import draw_network
dir_datos = 'd:/datos/licitaciones_compranet'
myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient['dataton2019']
resultado = mydb.contrataciones.find({}, {'_id': 0, 'parties.contactPoint': 1, 'parties.roles': 1, 'parties.id': 1, 'parties.address': 1})
df_contactos = pd.DataFrame([{**p.get('contactPoint', ''), **p.get('address', ''), 'tenderer_id': p['id'] }
for x in resultado for p in x['parties'] if (p['roles'] in [['tenderer'], ['tenderer', 'supplier']]) &
((bool(p.get('address', None))) | (bool(p.get('contactPoint', None))))])
df_contactos.to_pickle(f'{dir_datos}/tenderers_contacpoint.pkl')
df_contactos.head()
- De la base de datos de contrataciones seleccionamos los datos de contacto de los que han ganado licitaciones. Estos tiene datos como nombre de la persona de contacto, email, teléfono, número de fax, dirección y id del proveedor
df_contactos = pd.read_pickle(f'{dir_datos}/tenderers_contacpoint.pkl')
df_contactos.head()
df_contactos.shape
- De todos los contratos encontramos 563693 puntos de contacto. Muchos de estos se repiten porque un contratista que ganó varias veces aparecerá como un contacto por cada contrato ganado.
- Calculamos todas las combinaciones únicas de telefono y tenderer_id.
- Luego verificamos si existen casos en los que varios tenderer_id comparten el:
- teléfono: 700 casos en los que eso ocurre.
- Email: 839 casos.
- Nombre: 706 casos
- Número de fax: 135 casos
- Dirección de la calle: 172 casos
- Todos estos son signos de sospecha.
- En muchos casos hay cuentas de funcionarios públicos. Habría que verificar cuál es su papel.
- La pregunta relevante es ¿Hay casos en los que contratistas que tienen contactos en común hayan participado en un mismo proceso de licitación?
variables_contacto = ['telephone', 'email', 'name', 'streetAddress', 'faxNumber']
casos = []
for var_duplicated in variables_contacto:
# Encontramos todos los valores únicos de la variable de contacto y de tenderer_id
dups_direccion = df_contactos.loc[lambda x: (~x.duplicated(subset=['tenderer_id', var_duplicated])) & (x[var_duplicated].notnull())]\
.loc[lambda x: (x[var_duplicated].duplicated()) & (~x['name'].str[:22].eq('- (Cuenta administrada')), var_duplicated].unique()
# Encontramos cuáles son los tenderers_id que comparten un mismo contacto
tenderers_dup_id = [df_contactos.loc[lambda x: x[var_duplicated].eq(dup)].drop_duplicates(subset=['tenderer_id'])['tenderer_id'].tolist()
for dup in dups_direccion]
# Buscamos los contratos en los que participaron los ids asociados
queries_dup = [[{'parties.id': i} for i in x] for x in tenderers_dup_id]
for q in queries_dup:
resultado = list(mydb.contrataciones.find({'$and': q}, {'_id': 0, 'ocid': 1}))
if resultado:
tenderers_id = [x['parties.id'] for x in q]
ocids = list({x['ocid'] for x in resultado})
casos.append({'tenderer_ids': tenderers_id, 'contratos_ocid': ocids, 'variable': var_duplicated})
print(q)
with open('datos/casos_colusion.json', 'w', encoding='utf8') as jsonfile:
json.dump(casos, jsonfile)
- El resultado que encontramos es que existen 571 casos de contratistas posiblemente relacionados en una misma licitación.
with open('datos/casos_colusion.json', 'r', encoding='utf8') as jsonfile:
casos = json.load(jsonfile)
casos_ocid = list({c for cas in casos for c in cas['contratos_ocid']})
len(casos_ocid)
casos_contratos = list(mydb.contrataciones.find({'ocid': {'$in': casos_ocid}}))
len(casos_contratos)
datos_contrato = [{'titulo': c['contracts'][0]['title'],
'descr': c['contracts'][0].get('description', ''),
'valor': c['contracts'][0]['value']['amount'],
'dependencia_id': c['buyer']['id'],
'dependencia_nombre': c['buyer']['name'],
'uc_id': c['tender']['procuringEntity']['id'],
'uc_name': c['tender']['procuringEntity']['name'],
'ocid': c['ocid'],
'fecha': c['date'],
}
for c in casos_contratos if c.get('contracts', None)]
df_datos_contratos = pd.DataFrame(datos_contrato).set_index('ocid')
df_datos_contratos.head()
casos_contratos[0]
participantes_contrato = {c['ocid']: [p['id'] for p in c['parties'] if p['roles'] in [['tenderer'], ['tenderer', 'supplier']]] for c in casos_contratos}
# Número de particpantes que estaban asociados en cada contrato
asociados_contrato = {o: c['tenderer_ids'] for c in casos for o in c['contratos_ocid']}
# ganador contrato
ganadores_contrato = {c['ocid']: [p['id'] for p in c['parties'] if p['roles']==['tenderer', 'supplier']] for c in casos_contratos}
# dataframe
df_asoc = pd.DataFrame([participantes_contrato, asociados_contrato, ganadores_contrato]).T\
.rename(columns={0: 'part', 1: 'asoc', 2: 'gana'})\
.assign(N_part=lambda x: x['part'].str.len(),
N_asoc=lambda x: x['asoc'].str.len(),
N_gana=lambda x: x['gana'].str.len(),
prop_asoc_part=lambda x: x['N_asoc'].div(x['N_part']),
part_mayo=lambda x: x['prop_asoc_part'].ge(0.5),
asoc_ganadores=lambda x: x.apply(lambda y: list(set(y['gana']).intersection(set(y['asoc']))), axis=1),
N_asoc_ganadores=lambda x: x['asoc_ganadores'].str.len(),
part_nogana=lambda x: x.apply(lambda y: list(set(y['part']).difference(set(y['gana']))), axis=1))\
.join(df_datos_contratos)
df_asoc.to_pickle(f'{dir_datos}/df_asociados.pkl')
df_asoc.head()
df_asoc = pd.read_pickle(f'{dir_datos}/df_asociados.pkl')
- ¿En cuántos de estos casos los contratistas representaban el 50% de los proponentes o más?
print('Los contratistas representaban el 50% de los proponentes o más en', df_asoc.part_mayo.sum(), 'licitaciones')
- ¿En cuántos de estos casos los asociados fueron los únicos proponentes?
print('¿En cuántos de estos casos los asociados fueron los únicos proponentes?', df_asoc.prop_asoc_part.eq(1).sum(), 'licitaciones')
- De estos casos ¿en cuántas licitaciones los que estaban relacionados ganaron un concurso?
print('En', df_asoc.N_asoc_ganadores.gt(0).sum(), 'licitaciones ganó al menos uno de los contratistas asociados')
- ¿Cuántos contratistas asociados recibieron un contrato?
print(df_asoc.N_asoc_ganadores.sum(), ' contratistas asociados ganaron una licitación')
- ¿En qué dependencias, unidades compradoras y servidores públicos ocurre más esto?
df_asoc.groupby(['uc_name'])['part'].count().sort_values(ascending=False)
df_asoc.groupby(['dependencia_nombre'])['part'].count().sort_values(ascending=False)
nodos = []
red = 1
for c, vals in df_asoc.iterrows():
nodos.append({'id': c, 'tipo': 'contrato'})
for p in vals['part_nogana']:
nodos.append({'id': p, 'tipo': 'tenderer'})
links.append({'origen_id': p, 'destino_id': c, 'accion': 'participa', 'red': red})
for p in vals['gana']:
nodos.append({'id': p, 'tipo': 'supplier'})
links.append({'origen_id': p, 'destino_id': c, 'accion': 'gana', 'red': red})
for p1 in vals['asoc']:
for p2 in vals['asoc']:
if p1!=p2:
links.append({'origen_id': p1, 'destino_id': p2, 'accion': 'asociado', 'red': red})
nodos.append({'id': vals['uc_id'], 'tipo': 'uc'})
links.append({'origen_id': vals['uc_id'], 'destino_id': c, 'accion': 'compra', 'red': red})
red+=1
df_nodos = pd.DataFrame(nodos)\
.assign(num=lambda x:x.index)
dicc_nodo_num = {v:k for k,v in df_nodos['id'].to_dict().items()}
df_links = pd.DataFrame(links)\
.assign(origen_num=lambda x: x['origen_id'].map(dicc_nodo_num),
destino_num=lambda x: x['destino_id'].map(dicc_nodo_num))\
.dropna()
df_links.head()
df_nodos.to_csv('datos/asociados_nodos.csv', index=False)
df_links.to_csv('datos/asociados_links.csv', index=False)
df_nodos_graph = df_nodos.set_index('id')
for red in df_links.red.unique():
G = nx.from_pandas_edgelist(df_links.query('red==@red'), source='origen_id', target='destino_id', edge_attr=['accion'])
dicc_color_edges = {'gana': 'green', 'asociado': 'red', 'participa': '#3292a8', 'compra': 'orange'}
dicc_color_nodos = {'contrato': '#3292a8', 'supplier': 'pink', 'tenderer': 'blue', 'uc': 'orange'}
color_edges = [dicc_color_edges[e[2]['accion']] for e in G.edges(data=True)]
color_nodes = [dicc_color_nodos[df_nodos_graph.loc[[i], 'tipo'].tolist()[0]] for i in G.nodes]
fig, ax = plt.subplots()
draw_network(G, color_edges=color_edges, color_nodes=color_nodes, axes=ax, labels=[1, 2, 4, 5], text_size=8)
fig.savefig(f'graficas/redes/red_{red}.png', dpi=200)
plt.cla()
print(df_asoc.loc[[x for x in df_links.query('red==@red')['destino_id'].unique() if 'ocds' in x][0]])
Tareas:
Procesar telefonos
procesar múltiples mails
Es posible obtener más datos de los contratistas a partir del RUCP, como el sitio web, giro del negocio
Buscar otra anomalía: todos los contratos con métodos abiertos en los que solo participa un proponente. Buscar contratos en los que todos los particpantes reciben contrato.
- Buscar otra anomalía: todos los contratos con métodos abiertos en los que solo participa un proponente.
- Buscar contratos en los que todos los particpantes reciben contrato.
mydb.func_contrat.count_documents({})
mydb.func_contrat.find_one()
nombre_func_contrat = [f'{r["nombres"]} {r["primerApellido"]} {r["segundoApellido"]}'
for r in mydb.func_contrat.find({}, {'_id':0, 'nombres': 1, 'primerApellido': 1, 'segundoApellido':1}) if all([r["nombres"], r["primerApellido"], r["segundoApellido"]])]
mydb.serv_sanc.count_documents({})
mydb.serv_sanc.find_one()
nombre_serv_sanc = [f'{r["nombres"]} {r["primerApellido"]} {r["segundoApellido"]}' for r in mydb.serv_sanc.find({}, {'_id':0, 'nombres': 1, 'primerApellido': 1, 'segundoApellido':1})]
casos_func = [p['contactPoint']['name'] for c in casos_contratos for p in c['parties'] if p['roles']==['procuringEntity']]
set(casos_func).intersection(set(nombre_serv_sanc))
yt = list(set(df_contactos.name.unique().tolist()).intersection(set(nombre_serv_sanc)))
len(yt)
yt
mydb.part_sanc.count_documents({})
mydb.part_sanc.find_one()
rfc_sanc = [r['rfc'] for r in mydb.part_sanc.find({'rfc': {'$ne': ''}}, {'_id':0, 'rfc': 1})]
result1 = mydb.contrataciones.find({'contracts': {'$exists': True}},
['ocid', 'parties.id', 'parties.roles', 'parties.contactPoint', 'contracts.value.amount', 'date'])
l1 = list(result1)
ocid_tenderer = pd.DataFrame([(c['ocid'], p['id']) for c in l1 for p in c['parties']
if p['roles'] in [['tenderer', 'supplier'], ['tenderer']]],
columns=['ocid', 'tenderer_id'],
).set_index('ocid')
ocid_tenderer.head()
ocid_funcionario = pd.DataFrame([(c['ocid'], p['contactPoint'].get('name', ''), p['id'],
c['contracts'][0]['value']['amount'], c['date'])
for c in l1 for p in c['parties']
if p['roles']==['procuringEntity']], columns=['ocid', 'funcionario_id', 'uc_id', 'valor_contrato', 'fecha'])\
.set_index('ocid')
ocid_dependencia = pd.DataFrame([(c['ocid'], p['id'])
for c in l1 for p in c['parties']
if p['roles']==['buyer']], columns=['ocid', 'dep_id'])\
.set_index('ocid')
ocid_funcionario.head()
ocid_tender_fun = ocid_tenderer.join([ocid_funcionario, ocid_dependencia])
ocid_tender_fun
ocid_tender_fun.to_csv(f'{dir_datos}/ocid_tender_fun.csv')