Mercamadrid

Hemos preparado un código para bajar datos de la actividad comercial de mayoristas en Mercamadrid. Estos datos se pueden bajar desde este link del portal de datos abiertos del Ayuntamiento de Madrid.

La información esta estructura en una serie de campos tales como:

  • Fecha inicial del segmento de tiempo al que se refieren los datos
  • Fecha final del segmento de tiempo al que se refieren los datos
  • Descripción de la mercancia
  • Código de la mercancia
  • Origen de la mercancia
  • Código de este origen
  • Peso de la mercancia en Kilos
  • Precio mínimo registrado
  • Precio máximo registrado
  • Precio más frecuente

He intentado gestionar y minimizar el tamaño del pandas con el modelo propuesto por Matt Harrison, estoy haciendo un esfuerzo para seguir el «chaining» siempre que sea posible.

Presentamos el código necesario para bajar y preparar adecuadamente la información desde su repositorio en datos Madrid, y preparamos una rejilla de datos con los mismos..
En cada subgráfica, una por cada producto, presentamos la evolución del consumo como barras (eje «y» izquierda) )y la evolución de precios (eje «y» derecha). Para claridad se han codificado las barras según temperatura del mes..

Análisis del fichero de Licencias de Taxis de Madrid

Análisis de la planta de Taxis de Madrid

Vamos a analizar el fichero de «Taxis»del Ayuntamiento de Madrid, con información sacada del portal de OpenData : https://datos.madrid.es/portal/site/egob/menuitem.c05c1f754a33a9fbe4b2e4b284f1a5a0/?vgnextoid=4f16216612d39410VgnVCM2000000c205a0aRCRD&vgnextchannel=374512b9ace9f310VgnVCM100000171f5a0aRCRD

Como siempre importamos las librerias necesarias : pandas, numpy, matplotlib,datetime..y añadimos calendar. Usaremos esta última para sacar el literal de un mes según su nº de orden.

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import datetime
import matplotlib.dates as mdates
%matplotlib inline
import matplotlib.ticker as mtick
from matplotlib.ticker import FuncFormatter
from mpl_toolkits.mplot3d import Axes3D
import calendar
pd.options.display.float_format = '{:,.1f}'.format

Preparamos una texto para incluirlo en cada gráfico como fuente…

fuente='Fuente : Ayuntamiento de Madrid, http://datos.madrid.es'

Construimos la URL de la fuente de datos :

path_web='https://datos.madrid.es/egob/catalogo/300219-0-taxi-flota.csv'

Leemos los datos desde su localizacion en ‘path_web’, en este fichero tenemos los datos de Junio de 2018.

taxis=pd.read_csv(path_web,sep=";",encoding='windows-1250',index_col=False)

Vemos unos ejemplos de lineas para asegurar que todo ha ido bien. Podemos chequear también las columnas

len(taxis)

15652

taxis.describe()
CĂłdigo Cilindrada
count 15,652.0 15,652.0
mean 1,348,913.2 1,687.2
std 100,096.0 171.3
min 550,016.0 0.0
25% 1,269,989.8 1,598.0
50% 1,346,296.5 1,598.0
75% 1,433,151.5 1,798.0
max 1,514,196.0 3,498.0

«`python
taxis.columns
«`

Index([‘Código’, ‘Matrícula’, ‘Fecha Matriculación’, ‘Marca’, ‘Modelo’, ‘Tipo’,
‘Variante’, ‘Clasificación medioambiental’, ‘Combustible’, ‘Cilindrada’,
‘Potencia’, ‘Número de Plazas’,
‘Fecha inicio de prestación del servicio de taxi’, ‘Eurotaxi’,
‘Régimen Especial de Eurotaxi’,
‘Fecha inicio Régimen Especial Eurotaxi’,
‘Fecha fin Régimen Especial Eurotaxi’, ‘Fecha’],
dtype=’object’)

Nos aseguraremos de que la columna ‘Fecha Matriculacíon’ tiene el formato Datetime para trabajos posteriores, y la ordenamos a continuación

taxis['Fecha Matriculación']=pd.to_datetime(taxis['Fecha Matriculación'],format='%d/%m/%Y')
taxis=taxis.sort_values(['Fecha Matriculación'])

Busquemos el Taxi con la matrícula más antigua :

print ('Matricula {} de fecha {}'.format(taxis['Matrícula'].iloc[0],taxis['Fecha Matriculación'].iloc[0].strftime('%d/%m/%Y')))

Matricula 2239DTJ de fecha 13/12/2005

..y con la matrícula más moderna :

print ('Matricula {} de fecha {}'.format(taxis['Matrícula'].iloc[-1],taxis['Fecha Matriculación'].iloc[-1].strftime('%d/%m/%Y')))

Matricula 8333KLS de fecha 12/06/2018

Calculemos la lista de número de Taxis por Marca

fig = plt.figure(1, (6,4))
ax = fig.add_subplot(1,1,1)
ax=taxis['Marca'].value_counts().plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Marca')
ax.set_ylabel('Número de coches',size=16)
ax.grid(axis='y')
ax.set_title('Nº de Taxis por Marca')
fig.suptitle(fuente,size=10,x=0,y=-.4)
fig.savefig('Taxis por Marca',bbox_inches = 'tight')

output_32_0

..ahora ordenaremos por combustible usado

fig = plt.figure(1, (6,4))
ax = fig.add_subplot(1,1,1)
ax=taxis['Combustible'].value_counts().plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Combustible')
ax.set_ylabel('Número de coches',size=16)
ax.grid(axis='y')
ax.set_title('Nº de Taxis por Combustible')
fig.suptitle(fuente,size=10,x=0,y=-.4)
fig.savefig('Taxis por Marca',bbox_inches = 'tight')

output_34_0

Ahora trabajemos estos dos últimos campos de manera conjunta. Empezamos con un ‘groupby’ por estos dos campos, añadiendo ‘size()’ para que nos calcule el tamaño de cada campo :

tabla=taxis.groupby([taxis['Marca'],taxis['Combustible']]).size()
tabla

Marca Combustible
CHEVROLET DIESEL 7
GASOLINA TRANSFORMADO GLP 1
CITROEN DIESEL 340
GLP / GASOLINA 345
DACIA DIESEL 281
GLP / GASOLINA 896
FIAT DIESEL 54
GLP / GASOLINA 276
FORD DIESEL 145
GASOLINA-ELECTRICIDAD 4
HYUNDAI DIESEL 7
MERCEDES-BENZ DIESEL 199
GASOLINA – GAS NATURAL 9
GASOLINA TRANSFORMADO GLP 1
NISSAN ELECTRICO 13
OPEL DIESEL 1
PEUGEOT DIESEL 992
PEUGEOT DIESEL 1
RENAULT DIESEL 134
SEAT DIESEL 1875
GASOLINA – GAS NATURAL 199
GASOLINA TRANSFORMADO GLP 48
GLP / GASOLINA 612
SKODA DIESEL 4412
GASOLINA TRANSFORMADO GLP 14
GLP / GASOLINA 207
SSANGYONG GASOLINA 6
GASOLINA TRANSFORMADO GLP 1
GLP / GASOLINA 6
TOYOTA GASOLINA-ELECTRICIDAD 4067
VOLKSWAGEN DIESEL 478
GASOLINA – GAS NATURAL 21
dtype: int64

Ya tenemos la tabla lista, ahora vamos a representarla. He encontrado este modelo de mapa de temperatura de SeaBorn que queda muy bien :

tabla=tabla.reset_index()
import seaborn as sns
sns.set()
matriz=tabla.pivot('Marca','Combustible',0)
f, ax = plt.subplots(figsize=(15, 6))
sns.heatmap(matriz, fmt=".0f", annot=True,linewidths=1, ax=ax, cmap="YlGnBu")

output_39_1

Insertemos ahora una columna con la antigüedad de cada Matricula, simplemente calculando la diferencia entre ‘ahora’ y la fecha de matriculación, que vendrá en días (.days) y la pasamos a años equivalentes dividiendo por 365 (nos olvidamos de los bisiestos!)

ahora=datetime.datetime.today()
taxis['antiguedad']=taxis['Fecha Matriculación'].apply(lambda x : (ahora-x).days/365)

A continuación representamos una gráfica con el nº de matriculaciones ordenadas por meses :

fig = plt.figure(1, (15,6))
ax = fig.add_subplot(1,1,1)

data_series=taxis.groupby([taxis['Fecha Matriculación'].dt.year,taxis['Fecha Matriculación'].dt.month]).size()

ax.set_xlabel('Marca')
ax.set_ylabel('Número de coches',size=16)

ax.grid(axis='y')
ax.set_title('Nº de Matriculaciones de Taxis por mes')

bar_width=0.8
opacity=1
error_config = {'ecolor': '0.3'}
rects1 = ax.bar(np.arange(len(data_series.index.tolist())), data_series.values,bar_width,alpha=opacity)
def format_func(value, tick_number):
anno,mes=data_series.index.tolist()[int(value)]
mes=calendar.month_abbr[mes]
return mes+'-'+str(anno)[-2:]

ax.xaxis.set_major_formatter(plt.FuncFormatter(format_func))

# Preparamos una lista con los tickts que nos interesan : el primero, el último y todos los meses Enero y Julio
lista_ticks=[]
lista_ticks=[i for i in np.arange(3,len(data_series.index.tolist())) if data_series.index[i][1] in (1,7)]
lista_ticks.extend([0,len(data_series)-1]) #añade el primero y el último
ax.xaxis.set_ticks(lista_ticks)
fig.suptitle(fuente,size=15,x=0.5,y=0)
fig.savefig('matriculas por mes',bbox_inches = 'tight')
plt.show()

output_43_0

Utilicemos el campo ‘antiguedad’ para un par de cálculos y para generar una gráfica :

'Antiguedad media del parque de taxis : {:.1f} años, con desviación +/-{:.1f} años'.format(taxis['antiguedad'].mean(),taxis['antiguedad'].std())

‘Antiguedad media del parque de taxis : 4.0 años, con desviación +/-2.4 años’

Se nos ocurre generar una gráfica con la antigüedad media por Marca :

taxis.groupby(['Marca'])['antiguedad'].describe().sort_values(by='mean',ascending=False)
count mean std min 25% 50% 75% max
Marca
HYUNDAI 7.0 8.7 0.5 8.2 8.4 8.8 8.9 9.5
OPEL 1.0 8.7 nan 8.7 8.7 8.7 8.7 8.7
CHEVROLET 8.0 8.1 0.3 7.7 7.8 8.0 8.4 8.5
PEUGEOT 1.0 6.4 nan 6.4 6.4 6.4 6.4 6.4
SKODA 4,633.0 5.0 2.4 0.5 2.9 5.0 6.8 12.6
RENAULT 134.0 4.9 1.8 0.2 4.4 5.2 6.2 8.8
TOYOTA 4,067.0 4.4 2.5 0.1 2.2 4.9 6.5 10.4
MERCEDES-BENZ 209.0 4.1 2.3 0.2 2.7 3.6 5.1 10.0
SEAT 2,734.0 3.9 2.0 0.1 2.4 4.2 5.1 11.0
VOLKSWAGEN 499.0 3.7 2.6 0.1 0.9 3.6 4.9 10.0
PEUGEOT 992.0 3.3 1.7 0.5 2.2 3.1 4.2 9.5
DACIA 1,177.0 2.4 1.0 0.1 1.5 2.8 3.2 3.8
NISSAN 13.0 2.1 0.7 1.0 2.0 2.0 2.2 3.8
CITROEN 685.0 2.0 1.1 0.1 1.2 2.0 2.7 9.3
FIAT 330.0 0.8 1.2 0.1 0.3 0.5 0.9 9.7
FORD 149.0 0.8 0.5 0.1 0.5 0.6 0.9 2.6
SSANGYONG 13.0 0.4 0.2 0.1 0.2 0.5 0.6 0.7

«`python
taxis.groupby(['Marca'])['antiguedad'].mean().sort_values(ascending=False).plot.bar()
«`

output_48_1

Y jugando un poco con Seaborn hemos encontrado este formato de gráficas que muestra muy claramente la contribución de cada generación de coches a esas medias :

sns.set(style="whitegrid", palette="muted")
n_de_marcas=8
#filtramos a las primeras n_marcas por volumen
#datos=taxis.loc[taxis['Marca'].isin(taxis_marca[:n_de_marcas].index)]
lista_topn_marcas=taxis['Marca'].value_counts().index[:n_de_marcas]
datos=taxis.loc[taxis['Marca'].isin(lista_topn_marcas)]
# Draw a categorical scatterplot to show each observation
f, ax = plt.subplots(figsize=(15, 6))
sns.swarmplot(x="Marca", y="antiguedad", ax=ax, data=datos)

output_50_1

Vemos que se ha dejado de vender Skodas. Hay varios huecos en Volkswagen. Fiat ha empezado a vender en los dos últimos años. Peugeot ha dejado de vender también, y Dacia ha recupeado tras algún mes sin vender.

sns.set(style="whitegrid", palette="muted")
n_de_marcas=2
#filtramos a las primeras n_marcas por volumen
#datos=taxis.loc[taxis['Marca'].isin(taxis_marca[:n_de_marcas].index)]
lista_topn_marcas=taxis['Marca'].value_counts().index[:n_de_marcas]
datos=taxis.loc[taxis['Marca'].isin(lista_topn_marcas)]
# Draw a categorical scatterplot to show each observation

f, ax = plt.subplots(figsize=(15, 6))

sns.swarmplot(x="Marca", y="antiguedad",hue='Modelo', ax=ax, data=datos)
#lg=sns.legend_
plt.legend(ncol=2, loc='upper left')

output_52_1

Eso es todo!

Inspecciones de Consumo del Ayuntamiento de Madrid

Análisis de Inspecciones de Consumo en Madrid durante el primer Semestre de 2017

Vamos a analizar el fichero de Inspecciones de Consumo realizadas por  el Ayuntamiento de Madrid durante el primer semestre de 2017, con información sacada del portal de OpenData : http://datos.madrid.es

Como siempre importamos las librerias necesarias : pandas, numpy y matplotlib:

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import datetime
import matplotlib.dates as mdates
%matplotlib inline
import matplotlib.ticker as mtick
from matplotlib.ticker import FuncFormatter
pd.options.display.float_format = '{:,.1f}'.format

Preparamos una texto para incluirlo en cada gráfico como fuente…

fuente='Fuente : Ayuntamiento de Madrid, http://datos.madrid.es'

Preparando la URL de la fuente de datos

path_web='http://datos.madrid.es/egob/catalogo/300079-2-inspecciones-consumo.csv'
inspecciones=pd.read_csv(path_web,sep=";",encoding='windows-1250',index_col=False)

confirmamos que ha bajado correctamente, viendo el nº de registros (nº de inspecciones)

len(inspecciones)

5845

inspecciones.columns

Index([‘FECHA_DE_INSPECCION’, ‘DISTRITO’, ‘TIPO_VIAL_ACCESO_PRINCIPAL’,
‘NOMBRE_VIA_ACCESO_PRINCIPAL’, ‘NUMERO_VIA_ACCESO_PRINCIPAL’,
‘ACTIVIDAD_INSPECTORA’, ‘AMBITO’, ‘EPIGRAFE’, ‘Unnamed: 8′],
dtype=’object’)

Qué es esa columna llamada ‘Unnamed: 8’?. Tiene toda la pinta de ser un error de carga del fichero. Comprobémoslo, y borrémosla si así es :

inspecciones['Unnamed: 8'].head(10)

0 nan
1 nan
2 nan
3 nan
4 nan
5 nan
6 nan
7 nan
8 nan
9 nan
Name: Unnamed: 8, dtype: float64

inspecciones.drop('Unnamed: 8',axis=1,inplace=True)
inspecciones.columns

Index([‘FECHA_DE_INSPECCION’, ‘DISTRITO’, ‘TIPO_VIAL_ACCESO_PRINCIPAL’,
‘NOMBRE_VIA_ACCESO_PRINCIPAL’, ‘NUMERO_VIA_ACCESO_PRINCIPAL’,
‘ACTIVIDAD_INSPECTORA’, ‘AMBITO’, ‘EPIGRAFE’],
dtype=’object’)

Veamos una muestra (las cinco primeras filas) del tipo de datos que nos encontramos :

inspecciones.head(5)
FECHA_DE_INSPECCION DISTRITO TIPO_VIAL_ACCESO_PRINCIPAL NOMBRE_VIA_ACCESO_PRINCIPAL NUMERO_VIA_ACCESO_PRINCIPAL ACTIVIDAD_INSPECTORA AMBITO EPIGRAFE
0 02/01/2017 06 – TETUAN CL ORENSE 69 10 – Inspección 10 – Consumo 951001 – REPARACION DE ORDENADORES Y EQUIPOS D…
1 02/01/2017 20 – SAN BLAS – CANILLEJAS CL RUMANIA 18 10 – Inspección 10 – Consumo 475206 – COMERCIO AL POR MENOR DE MATERIAL ELE…
2 02/01/2017 20 – SAN BLAS – CANILLEJAS CL RUMANIA 16 10 – Inspección 10 – Consumo 475906 – COMERCIO AL POR MENOR DE APARATOS DE …
3 02/01/2017 13 – PUENTE VALLECAS AV ALBUFERA 0 10 – Inspección 10 – Consumo 47800106 – SITUADOS: ARTICULOS NAVIDEŃOS NO AL…
4 02/01/2017 13 – PUENTE VALLECAS AV ALBUFERA 0 10 – Inspección 10 – Consumo 47800108 – SITUADOS: COMPLEMENTOS, BISUTERIA Y…

Reformateamos la columna de FECHA_DE_INSPECCION para convertirlo en datetime

inspecciones['FECHA_DE_INSPECCION']=pd.to_datetime(inspecciones['FECHA_DE_INSPECCION'],format='%d/%m/%Y')

Empecemos con unos gráficos: nº de inspecciones por distritos :

inspecciones.groupby('DISTRITO').size().sort_values().plot.barh()

 

output_30_1

Contruyamos ahora unas tablas, la primera con el top 20 de direcciones con más inspecciones, y la segunda con el top de inspecciones en el distrito con más inspecciones : Barrio Salamanca

inspecciones.groupby(['NOMBRE_VIA_ACCESO_PRINCIPAL','NUMERO_VIA_ACCESO_PRINCIPAL']).size().sort_values(ascending=False).head(20)

NOMBRE_VIA_ACCESO_PRINCIPAL NUMERO_VIA_ACCESO_PRINCIPAL
ESTACION DE HORTALEZA 0 142
AREQUIPA 1 127
LAS AGUILAS 0 99
CAMPILLO DEL MUNDO NUEVO 0 97
CALDERILLA 1 93
MONFORTE DE LEMOS 30 73
ARACNE 3 69
ALCALA 412 56
FLORIDA 2 48
ADOLFO BIOY CASARES 2 42
CASTELLANA 204 39
ARTURO SORIA 126 36
AYERBE 0 30
CLAUDIO MOYANO 0 26
GENERAL PERON 40 26
PUERTO DEL BRUCH 4 25
SILVANO 77 25
RAMON PEREZ DE AYALA 0 22
ALCALA 237 19
GUATAVITA 1 18
dtype: int64

inspecciones[inspecciones['DISTRITO']=='04 - SALAMANCA'].groupby(['NOMBRE_VIA_ACCESO_PRINCIPAL','NUMERO_VIA_ACCESO_PRINCIPAL']).size().sort_values(ascending=False).head(20)

NOMBRE_VIA_ACCESO_PRINCIPAL NUMERO_VIA_ACCESO_PRINCIPAL
ALCALA 237 19
CLAUDIO COELLO 88 10
AMERICA 4 9
HERMOSILLA 26 8
BOCANGEL 1 8
CLAUDIO COELLO 38 7
BRUSELAS 60 7
TOREROS 24 6
BRUSELAS 63 6
MARQUES DE ZAFRA 9 6
BRUSELAS 41 6
GOYA 17 6
LAGASCA 88 6
DON RAMON DE LA CRUZ 17 6
SERRANO 61 6
MARTINEZ IZQUIERDO 14 5
BRUSELAS 38 5
ALCALA 231 5
CARTAGENA 24 5
AMERICA 28 5
dtype: int64

Os preguntáis qué hay en Alcalá, 237 ??. Y en Estación de Hortaleza, 0 ??..en este caso no es inmediato, pero podremos comprobar que se refiere al Mercado Dominical de Hortaleza

Veamos en qué días se realizan esas inspecciones :

inspecciones[inspecciones['NOMBRE_VIA_ACCESO_PRINCIPAL']=='ESTACION DE HORTALEZA'].groupby(['FECHA_DE_INSPECCION']).size().plot.bar()

 

output_36_1

Vemos que esos días se corresponden con Domingo

A continuación veamos la diestribución por meses…(1-> Enero, 2->Febrero, ……)

inspecciones.groupby([inspecciones["FECHA_DE_INSPECCION"].dt.month]).size().plot(kind="bar")

 

output_39_1

Por semanas :

inspecciones.groupby([inspecciones["FECHA_DE_INSPECCION"].dt.week]).size().plot(kind="bar")

 

output_41_1

La semana 15 coincidió con la Semana Santa

...y finalmente la distribución por días :
inspecciones.groupby([inspecciones["FECHA_DE_INSPECCION"].dt.weekday_name]).size().sort_values().plot(kind="bar")

 

output_44_1

Veamos ahora la distribución de las inspecciones según el EPIGRAFE del comercio/local afectado :

inspecciones.groupby(['EPIGRAFE']).size().sort_values(ascending=True).tail(20).plot.barh()

 

output_46_1

Y por último echemos un vistazo a los tipos de ACTIVIDAD_INSPECTORA, viendo la lista de inspecciones de una de ellas…

inspecciones['ACTIVIDAD_INSPECTORA'].unique()

array([’10 – Inspección’, ’30 – Control de Productos’,
’20 – Otros Controles Oficiales’], dtype=object)

inspecciones.groupby(['ACTIVIDAD_INSPECTORA']).size().sort_values(ascending=True).head(2).plot.barh()

 

output_49_1

inspecciones[inspecciones['ACTIVIDAD_INSPECTORA']=='30 - Control de Productos']
FECHA_DE_INSPECCION DISTRITO TIPO_VIAL_ACCESO_PRINCIPAL NOMBRE_VIA_ACCESO_PRINCIPAL NUMERO_VIA_ACCESO_PRINCIPAL ACTIVIDAD_INSPECTORA AMBITO EPIGRAFE Unnamed: 8
159 19/01/2017 12 – USERA CL MARIBLANCA 11 30 – Control de Productos 10 – Consumo 471104 – COM.MENOR PRODUCTOS ALIMENTICIOS NO P… nan
313 23/01/2017 01 – CENTRO CL GRAN VIA 29 30 – Control de Productos 10 – Consumo 476101 – COMERCIO AL POR MENOR DE LIBROS nan
613 02/02/2017 21 – BARAJAS CL AGUETOL 8 30 – Control de Productos 10 – Consumo 471901 – COM.MENOR ESTABL.NO ESPEC.S/PREDOMINI… nan
1199 09/01/2017 07 – CHAMBERI CL RIOS ROSAS 2 30 – Control de Productos 10 – Consumo 475901 – COMERCIO AL POR MENOR DE MUEBLES nan
1489 24/02/2017 01 – CENTRO CL FUENCARRAL 94 30 – Control de Productos 10 – Consumo 477808 – OTRO COMERCIO AL POR MENOR DEL ARTICU… nan
1495 24/02/2017 01 – CENTRO CL FUENCARRAL 94 30 – Control de Productos 10 – Consumo 477808 – OTRO COMERCIO AL POR MENOR DEL ARTICU… nan
1498 24/02/2017 01 – CENTRO CL FUENCARRAL 94 30 – Control de Productos 10 – Consumo 477808 – OTRO COMERCIO AL POR MENOR DEL ARTICU… nan
1503 24/02/2017 01 – CENTRO CL FUENCARRAL 94 30 – Control de Productos 10 – Consumo 477808 – OTRO COMERCIO AL POR MENOR DEL ARTICU… nan
2595 21/01/2017 10 – LATINA AV LAS AGUILAS 0 30 – Control de Productos 10 – Consumo 477101 – COMERCIO MENOR PRENDAS DE VESTIR EN E… nan
5018 31/05/2017 08 – FUENCARRAL-EL PARDO AV MONFORTE DE LEMOS 30 30 – Control de Productos 10 – Consumo 476501 – COMERCIO AL POR MENOR DE JUEGOS Y JUG… nan

Eso es todo, tengo pendiente el aprender a presentar estos datos en mapas, con diagramas de calor etc…Tarea pendiente..

Análisis de multas de tráfico en Madrid de Julio 2017

Análisis de multas de circulación impuestas en Madrid durante Julio 2017

Vamos a analizar el fichero de multas del Ayuntamiento de Madrid, con información sacada del portal de OPenData : http://datos.madrid.es

Como siempre importamos las librerias necesarias : pandas, numpy y matplotlib

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import datetime
import matplotlib.dates as mdates
%matplotlib inline
import matplotlib.ticker as mtick
from matplotlib.ticker import FuncFormatter
pd.options.display.float_format = '{:,.1f}'.format

Preparamos una texto para incluirlo en cada gráfico como fuente…

fuente='Fuente : Ayuntamiento de Madrid, http://datos.madrid.es'

Preparando la URL de la fuente de datos

path_web='http://datos.madrid.es/egob/catalogo/210104-162-multas-circulacion-detalle.csv'

cabecera de las columnas

nombre_columnas=['CALIFICACION','LUGAR','MES','ANIO','HORA','IMP_BOL','DESCUENTO','PUNTOS','DENUNCIANTE','HECHO_BOL','VEL_LIMITE','VEL_CIRCULA','COORDENADA_X','COORDENADA_Y']

Leemos los datos desde su localizacion en ‘path_web’, en este fichero tenemos los datos de Junio de 2017. Al respecto de la la identificacion de la multa en el tiempo tendremos la hora pero no el día del mes, es decir : tendremos las multas puestas a las 13:10 a lo largo de todo el mes, pero no podremos partirlas por día. No encuentro otra razón que no sea evitar cualquier vía de identificación del conductor.

multas=pd.read_csv(path_web,sep=";",encoding='windows-1250',index_col=False,header=None,names=nombre_columnas,skiprows=1)

confirmamos que ha bajado correctamente

multas.columns

Index([‘CALIFICACION’, ‘LUGAR’, ‘MES’, ‘ANIO’, ‘HORA’, ‘IMP_BOL’, ‘DESCUENTO’,
‘PUNTOS’, ‘DENUNCIANTE’, ‘HECHO_BOL’, ‘VEL_LIMITE’, ‘VEL_CIRCULA’,
‘COORDENADA_X’, ‘COORDENADA_Y’],
dtype=’object’)

Convertimos la columna ‘HORA’ con horas tal que 21.30 en datetime

multas['HORA']=pd.to_datetime(multas['HORA'],format='%H.%M')

Añadimos una columna hora_entera, tal que la hora (desde 00 hasta 23) para facilidad de cálculo de algunos gráficos..

for n in range(0,multas.shape[0]):
multas.set_value(n,'hora_entera',multas.loc[n,'HORA'].strftime('%H')+':00');

Hay que tratar un poco los dos campos relacionados con velocidad (limite y velocidad multada:
a) tanto los vaklores de aquellas multas no relacionadas con velocidad en las que el valor es un string de 4 caracteres BS : ‘ ‘
b) Aquellos relacionados con velocidad en los que hay que convertir el string con la velocidad a un integer.
He generado un par de columnas adicionales para contener estos datos ya tratados..

velocidad=lambda x : 0 if x==' ' else int(x.strip())
multas['velocidad_limite']=multas['VEL_LIMITE'].apply(velocidad)
multas['velocidad_circulacion']=multas['VEL_CIRCULA'].apply(velocidad)
multas.columns

Index([‘CALIFICACION’, ‘LUGAR’, ‘MES’, ‘ANIO’, ‘HORA’, ‘IMP_BOL’, ‘DESCUENTO’,
‘PUNTOS’, ‘DENUNCIANTE’, ‘HECHO_BOL’, ‘VEL_LIMITE’, ‘VEL_CIRCULA’,
‘COORDENADA_X’, ‘COORDENADA_Y’, ‘hora_entera’, ‘velocidad_limite’,
‘velocidad_circulacion’],
dtype=’object’)

Empezamos a sacar algunos resultados :

Número total de multas : 235.099 multas en Julio 2017

len(multas)

235099

Cuántos puntos se han perdido en esas multas? : 27.037

puntos_totales=multas.PUNTOS.sum()
print ('Puntos total perdidos: {:,.0f} puntos'.format(puntos_totales))

Puntos total perdidos: 27,037 puntos

Cual es la suma del los importes de esas multas, antes de descuento?

euros_totales=multas.IMP_BOL.sum()
print ('Importe total de multas antes de descuento: {:,.0f} euros'.format(euros_totales))

Importe total de multas antes de descuento: 23,113,010 euros

Cual es el importe de la multa de más y menos importe?

print ('Importe más alto: {:,.0f} euros'.format(multas.IMP_BOL.max()))
print ('Importe más bajo: {:,.0f} euros'.format(multas.IMP_BOL.min()))

Importe más alto: 1,000 euros
Importe más bajo: 30 euros

Veamos la distribución de multas por hora : (recordamos de nuevo que en esta gráfica se representa el acumulado en esa hora de todos los dias del mes)

multas_hist=multas['hora_entera'].value_counts().sort_index(axis=0)

fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=multas_hist.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('Número de multas',size=16)
ax.grid(axis='y')
ax.set_title('Multas acumuladas por hora Jul-17')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('multas_hora_Jul17',bbox_inches = 'tight')

output_42_0

Con porcentajes en vez de números absolutos :

multas_hist_porcentaje=multas['hora_entera'].value_counts().sort_index(axis=0)/len(multas)*100
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=multas_hist_porcentaje.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('% multas',size=16)
ax.grid(axis='y')
ax.set_title('Porcentajes de multas en cada hora Jul-17')
fmt = '%3.1f%%'
yticks = mtick.FormatStrFormatter(fmt)
ax.yaxis.set_major_formatter(yticks)
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('multas_hora_porcentaje_Jul17',bbox_inches = 'tight')

output_44_0

Seguimos con la distribución de euros y puntos perdidos por hora :

multas_euros=multas.sort_values('HORA').groupby("hora_entera",sort=False).IMP_BOL
fig1 = plt.figure()
ax1 = fig1.add_subplot(1,1,1)
ax1 = multas_euros.sum().plot.bar()
ax1.locator_params(axis='y',nbins=10)
ax1.set_xlabel('Hora del día',size=16)
ax1.set_ylabel('Euros',size=16)
ax1.tick_params(axis='x',direction='out', length=6, width=2, colors='black')
#ax1.set_xticklabels(multas_euros['hora_entera'])
ax1.grid(axis='y')
ax1.set_title('Distribucion Euros de multa por hora Jul-17')
fig1.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('euros_hora',bbox_inches = 'tight')

output_46_0

multas_puntos=multas.sort_values('HORA').groupby("hora_entera",sort=False).PUNTOS
fig1 = plt.figure(1,(7,4))
ax1 = fig1.add_subplot(1,1,1)
ax1 = multas_puntos.sum().plot.bar()
ax1.locator_params(axis='y',nbins=10)
ax1.set_xlabel('Hora del día',size=16)
ax1.set_ylabel('Puntos',size=16)
ax1.tick_params(axis='x',direction='out', length=6, width=2, colors='black')
#ax1.set_xticklabels(multas_euros['hora_entera'])
ax1.grid(axis='y')
ax1.set_title('Distribucion Puntos por hora Jul-17')
fig1.suptitle(fuente,size=10,x=1,y=-0.01)
fig1.savefig('puntos_hora_Jul17',bbox_inches = 'tight')

output_47_0

He calculado un par de ratios de interés, euros por multa y puntos por multa a lo largo del día, viendo que la media por la noche es significativamente superior a la media del día. Aquí vemos exclusivamente que las multas de la noche traen más euros y quitan as puntos que las multas de día, cosa que parece razonable, en ambos casos vemos que la hora caliente es de 04:00 a 05:00 de la madrugada, con más de 160€ y casi 0.6 puntos por multa.

ratio_euros_multas=multas_euros.sum()/multas_hist
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=ratio_euros_multas.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('€/multa',size=16)
ax.grid(axis='y')
ax.set_title('Media de € por multa Jul-17')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('media_euros_multa_Jul17',bbox_inches = 'tight')

output_49_0

ratio_puntos_multas=multas_puntos.sum()/multas_hist
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=ratio_puntos_multas.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('Puntos/multa',size=16)
ax.grid(axis='y')
ax.set_title('Media de puntos por multas en cada hora Jul 17')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('media_puntos_multa_Jul17',bbox_inches = 'tight')

output_50_0

Toca ahora analizar las multas según su tipo, siguiendo con la terminología del fichero : con el HECHO_BOL, el hecho descrito en el boletín de multa. Empezamos por las multas más frecuentes según tipo, podemos ver que más de 55000 multas vienen de saltarse las restricciones de trafico en zonas con circulación limitado, como ejemplo el centro de MAdrid. Analizaremos en profundidad este hecho en otro post. Continuaremos con un par de tablas con los puntos y euros de los hechos de multas que más puntos retiran (slatarse un semaforo en rojo) y euros recaudan (circular por zonas limitadas), y esas mismas tablas pasadas a gráficos.

multas_hecho=multas.HECHO_BOL.value_counts()
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=multas_hecho.head(10).plot.barh()
ax.locator_params(axis='y',nbins=10)
ax.locator_params(axis='x',nbins=20)
ax.set_xlabel('Número de multas',size=20)
ax.grid(axis='x')
ax.invert_yaxis()
ax.set_yticklabels(['{:>80}'.format(x.strip()[:80]) for x in multas_hecho.index],size=10)
ax.set_title('Hechos denunciados más frecuentes Jul-17')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('hechos_fecuentes_Jul17',bbox_inches = 'tight')

pngoutput_52_0

multas_hecho_importe=multas.groupby('HECHO_BOL')
multas_hecho_importe['IMP_BOL','PUNTOS'].sum().sort_values('PUNTOS',ascending=False).reset_index().head()
HECHO_BOL IMP_BOL PUNTOS
0 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 2,014,600.0 8786
1 REBASAR UN SEMÁFORO EN FASE ROJA. … 426,800.0 8332
2 UTILIZAR MANUALMENTE PANTALLAS, NAVEGADORES, D… 157,600.0 2331
3 CONDUCIR CON UNA TASA DE ALCOHOLEMIA NO PERMIT… 289,000.0 1692
4 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 4,243,700.0 1532
multas_hecho_importe['IMP_BOL','PUNTOS'].sum().sort_values('IMP_BOL',ascending=False).reset_index().head()

HECHO_BOL IMP_BOL PUNTOS
0 NO RESPETAR LAS SEŃALES EN UNA VÍA DE CIRCULAC… 5,168,520.0 0
1 ESTACIONAR, SIN LA CORRESPONDIENTE AUTORIZACIÓ… 4,272,390.0 0
2 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 4,243,700.0 1532
3 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 2,014,600.0 8786
4 ESTACIONAR EN LUGAR PROHIBIDO DEBIDAMENTE SEŃA… 1,230,210.0 0
pd_aux=multas_hecho_importe['PUNTOS'].aggregate('sum').sort_values(ascending=False).head(7)
N=5
width=.5
ind = np.arange(len(pd_aux))
fig, ax = plt.subplots()
rects = ax.barh(ind, pd_aux.values, width, align='center',color='b', label='Puntos')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de multa',size=20)
ax.set_xlabel('Acumulado euros de multas',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>80}'.format(x.strip()[:80]) for x in pd_aux.index],
size=12,rotation=0,color='b')
ax.invert_yaxis()

ax.grid(axis='x')
ax.set_title('Puntos por tipo multa Jul-17')
ax.legend()
fig.suptitle(fuente,size=10,x=1,y=-0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/puntos_totales), size=16, ha='left', va='center')

plt.show()
fig.savefig('puntos_hecho_multa_Jul17',bbox_inches = 'tight')

output_57_0

Redondeando podemos ver que el 32% de los puntos retirados se pierden por correr en zonas de tráfico limitado a 50km/h..

pd_aux=multas_hecho_importe['IMP_BOL'].aggregate('sum').sort_values(ascending=False).head(10)
N=5
width=.5
ind = np.arange(len(pd_aux))
fig = plt.figure(1, (10,7))
ax = fig.add_subplot(1,1,1)

rects = ax.barh(ind, pd_aux.values, width, align='center',color='b', label='Euros')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de multa',size=20)
ax.set_xlabel('Acumulado euros de multas',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>120}'.format(x.strip()[:120]) for x in pd_aux.index],
size=16,rotation=0,color='b')
ax.invert_yaxis()

ax.grid(axis='x')
ax.set_title('Euros por tipo multa')
ax.legend()
fig.suptitle(fuente,size=16,x=1,y=-0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/euros_totales), size=20, ha='left', va='center')

plt.show()
fig.savefig('euros_hecho_multa_Jul17',bbox_inches = 'tight')

output_60_0

..y el 22% de los euros recaudados vienen de multas por circular en zonas restriguidas.

Y finalmente analizaremos las multas de velocidad.
Empezamos haciendo un filtro :

multas_filtrada_velocidad=multas[multas.velocidad_circulacion!=0]

Calculando el tamaño del nuevo pandas vemos que hay 53.243 multas de velocidad.

multas_de_velocidad=len(multas_filtrada_velocidad)
multas_de_velocidad

53243

Saquemos unos cálculos interesantes

multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].describe()
count mean std min 25% 50% 75% max
velocidad_limite
40 206.0 63.8 6.9 54.0 59.0 62.0 68.0 88.0
50 12,073.0 67.9 6.9 53.0 63.0 66.0 71.0 100.0
60 364.0 84.3 8.8 70.0 77.0 83.0 90.0 114.0
70 34,826.0 79.6 6.5 74.0 75.0 77.0 82.0 136.0
80 20.0 98.8 6.2 93.0 94.8 97.5 101.0 117.0
90 5,754.0 100.4 6.1 95.0 96.0 99.0 102.0 152.0

Aquí podemos ver algunas características de las multas de velocidad ordenadas por la velocidad límite. Con algunas curiosidades a primera vista : la mayor parte de las multas se consiguen en zonas con 70 km/h de velocidad límite..Se ha multado a 53km/h en zonas de 50km/hr, sin embargo no se ha multado hasta los 93km/h en zona de 80km/h..aunque cierto es que el número de multas a 80km/h es despreciable : 20 de las 44.659…

Veamoslo en gráficos : número de multas

multas_filtrada_velocidad['velocidad_limite'].sort_values().unique()

array([40, 50, 60, 70, 80, 90])

multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].count()

velocidad_limite
40 206
50 12073
60 364
70 34826
80 20
90 5754
Name: velocidad_circulacion, dtype: int64

fig = plt.figure(1, (12,7))
width=.8
ind = np.arange(len(multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].aggregate('count')))

ax = fig.add_subplot(111)

rects = ax.bar(ind, multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].aggregate('count'),
width, align='center',color='b', label='nº multas')

ax.set_xticks(ind)
ax.set_xticklabels([x for x in multas_filtrada_velocidad['velocidad_limite'].sort_values().unique()],
size=12,rotation=0,color='b')
ax.set_xlabel('Velocidad límite en km/h')
ax.set_ylabel('Número de multas',size=16)
ax.grid(axis='y')
ax.set_title('Número de multas de velocidad por velocidad límite Jul-17')
ax.legend()
fig.suptitle(fuente,size=10,x=1,y=-0.01)

for rect in rects:
height = rect.get_height()
plt.text(rect.get_x()+rect.get_width()/2,height ,'%1.1f%%' % (100*height/multas_de_velocidad), size=10, ha='center', va='bottom')

plt.show()
fig.savefig('multas_velocidad_limite_Jul17',bbox_inches = 'tight')

output_72_0

..y un par de gráficas adicionales con la distribución de velocidad que llevaban los multados frente ordenados por la velocidad límite :

fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax.plot(
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==50]['velocidad_circulacion'].sort_values().unique(),
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==50].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
label='50 km/h',
)

ax.plot(
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==70]['velocidad_circulacion'].sort_values().unique(),
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==70].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
label='70 km/h',
)
ax.plot(
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==90]['velocidad_circulacion'].sort_values().unique(),
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==90].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
label='90 km/h',
)

ax.locator_params(axis='x',nbins=20)
ax.set_xlabel('Velocidad en la infracción km/h')
ax.set_xlim([50,160])
ax.set_ylim([0,7000])
ax.set_ylabel('Número de multas',size=16)
ax.grid(axis='y')
ax.set_title('Distribución de velocidad de infracción por velocidad límite Jul-17')
ax.legend()

fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('distribucion_velocidad_507090_Jul17',bbox_inches = 'tight')

output_74_0

fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax.plot(
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==40]['velocidad_circulacion'].sort_values().unique(),
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==40].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
label='40 km/h',
)

ax.plot(
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==60]['velocidad_circulacion'].sort_values().unique(),
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==60].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
label='60 km/h',
)
ax.plot(
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==80]['velocidad_circulacion'].sort_values().unique(),
multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==80].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
label='80 km/h',
)

ax.locator_params(axis='x',nbins=20)
ax.set_xlabel('Velocidad en la infracción km/h')
ax.set_xlim([40,130])
ax.set_ylim([0,70])
ax.set_ylabel('Número de multas',size=16)
ax.grid(axis='y')
ax.set_title('Distribución de velocidad de infracción por velocidad límite, Jul-17')
ax.legend()

fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('dsitribibucion_velocidad_406080_Jul17',bbox_inches = 'tight')

output_75_0

Con el afan de ver las multas «extremas», no en absoluto si no con el ratio velocidad_circulacion/velocidad/limite, introducimos una nueva columna…

multas_filtrada_velocidad['ratio']=multas_filtrada_velocidad['velocidad_circulacion']/multas_filtrada_velocidad['velocidad_limite']

/Users/waly/anaconda/envs/OpenData/lib/python3.6/site-packages/ipykernel_launcher.py:1: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
«»»Entry point for launching an IPython kernel.

y vemos los casos extremos (head y tail)..
En la parte alta : multa catalogada como GRAVE a 88 km/hr en zona de 40km/h en la Avenida Séneca con velocida máxima 40km/h, a las 17:05…otros 500€ del ala!
..y en la parte baja : multa a 95km/h en el km de la M-30, km 27,  en zona de 90km/h a las 18:23..100€ por esos 5 km/h

multas_filtrada_velocidad.sort_values('ratio',ascending=False).head(2)
CALIFICACION LUGAR MES ANIO HORA IMP_BOL DESCUENTO PUNTOS DENUNCIANTE HECHO_BOL VEL_LIMITE VEL_CIRCULA COORDENADA_X COORDENADA_Y hora_entera velocidad_limite velocidad_circulacion ratio
231825 GRAVE S/N AV SENECA 7 2017 1900-01-01 17:05:00 500.0 SI 6 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 40 88 17:00 40 88 2.2
115695 GRAVE F081 PO ERMITA DEL SANTO 7 2017 1900-01-01 12:25:00 500.0 SI 6 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 40 88 12:00 40 88 2.2
multas_filtrada_velocidad.sort_values('ratio',ascending=False).tail(2)
CALIFICACION LUGAR MES ANIO HORA IMP_BOL DESCUENTO PUNTOS DENUNCIANTE HECHO_BOL VEL_LIMITE VEL_CIRCULA COORDENADA_X COORDENADA_Y hora_entera velocidad_limite velocidad_circulacion ratio
231079 GRAVE M-30 CALZADA 2 KM 27.000 7 2017 1900-01-01 18:23:00 100.0 SI 0 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 90 95 18:00 90 95 1.1
168620 GRAVE M-30 C-2 KM 7,800 CR-CRA 7 2017 1900-01-01 02:28:00 100.0 SI 0 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 90 95 02:00 90 95 1.1
<br />

Cuanto se gastó el Ayuntamiento de Madrid en los acto protocolarios con el Real Madrid y el Atlético Madrid el pasado Mayo?

Partiendo del pandas de la entrada anterior añado un dato para todos los seguidores del Real Madrid y Atlético :


for x in gastos.index[:-1]:
if str(gastos.loc[[x]]['MOTIVO'].values[0]).find('Real Madrid')!=-1:
print ('{} ----> {} euros\n'.format(gastos.loc[[x]][['MOTIVO']].values[0][0],
gastos.loc[[x]]['gasto_total'].values[0]))

Atención institucional y protocolaria, con motivo de las recepciones los días 22 y 30 de mayo de 2017, al Real Madrid y al Atlético de Madrid, como campeones de las ligas masculina y femenina de fútbol la temporada 2016-2017 —-> 220.0 euros

Atención institucional y protocolaria, con motivo de la recepción en el Palacio de Cibeles al Real Madrid, como campeón de la Champios League 2017 —-> 110.0 euros

330€ en total…me imagino que ambos equipos regalarían al menos una camiseta a la Alcaldesa!

Fuente :  Open Data Ayuntamiento Madrid

Analizando el fichero de Gastos de Protocolo del Ayuntamiento de Madrid con Python, Pandas y MatplotLib

Análisis de los Gastos de Representación del Ayuntamiento de Madrid, sacados de su portal de #OpenData del primer semestre 2017.

Fuente en : http://datos.madrid.es/sites/v/index.jsp?vgnextoid=9acf953e27827510VgnVCM2000001f4a900aRCRD&vgnextchannel=374512b9ace9f310VgnVCM100000171f5a0aRCRD

Como siempre comenzamos con la imporación de las librerías necesarias.

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import datetime
import matplotlib.dates as mdates
%matplotlib inline
import matplotlib.ticker as mtick
from matplotlib.ticker import FuncFormatter

String para referenciar la informacion al site de OpenData del Ayuntamiento de Madrid

fuente='Fuente : Ayuntamiento de Madrid, http://datos.madrid.es'

URL del fichero fuente :

path_web='http://datos.madrid.es/egobfiles/MANUAL/300058/Protocolarios%202017.csv'

Importamos los datos…

gastos=pd.read_csv(path_web,sep=";",encoding='windows-1250',index_col=False)

Veamos el formato que tienen :

gastos.columns

Index([‘APELLIDO 1’, ‘APELLIDO 2’, ‘NOMBRE’, ‘NUMTRANS’, ‘PUESTO’,
‘NOMBRE UNIDAD ADSCRIPCIÓN’, ‘NOMBRE AREA/JM/OOAA/EP’, ‘FECHA’,
‘MOTIVO’, ‘COMIDAS \nINSTITUCIONALES’, ‘PRODUCTOS \nALIMENTICIOS’,
‘FLORES’, ‘CATERING ‘, ‘TROFEOS DISTINCIONES’, ‘CONCEPTO’, ‘IMPORTE’,
‘Unnamed: 16′],
dtype=’object’)

gastos.head(2)
APELLIDO 1 APELLIDO 2 NOMBRE NUMTRANS PUESTO NOMBRE UNIDAD ADSCRIPCIÓN NOMBRE AREA/JM/OOAA/EP FECHA MOTIVO COMIDAS
INSTITUCIONALES
PRODUCTOS
ALIMENTICIOS
FLORES CATERING TROFEOS DISTINCIONES CONCEPTO IMPORTE Unnamed: 16
0 ARCE LEGUA ROMMY 516203.0 CONCEJAL/A PRESIDENTE/A DE DISTRITO CONCEJAL PRESIDENTE JMD ARGANZUELA JUNTA MUNICIPAL DEL DISTRITO DE ARGANZUELA NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 ARCE LEGUA ROMMY 516203.0 CONCEJAL/A PRESIDENTE/A DE DISTRITO CONCEJAL PRESIDENTE JMD USERA JUNTA MUNICIPAL DEL DISTRITO DE USERA 19/04/2017 Inauguración Semana del Libro en el Distrito d… NaN NaN NaN 715,00 NaN NaN NaN NaN

Llenamos los campos N/A con ceros

gastos.fillna(0,inplace=True)

Eliminamos el separador de miles (‘.’) y cambiamos el decimal por (‘.’), ya que son los utilizados en Float de Python3

gastos['IMPORTE']=gastos['IMPORTE'].apply(lambda x: float(
str(x).replace(',', '%temp%').replace('.', '').replace('%temp%', '.')))

Vemos una anomalía en el formato. Entendemos que cada fila es un gasto, del que tenemos los datos del empleado público que lo realizó fecha, motivo etc..Adicionalmente tenemos una serie de columnas en las que se describe el gasto : CATERING, FLORES; TROFEOS etc…Pero además nos encontramos con una columna ‘CONCEPTO’ y otra importe..

gastos[gastos['IMPORTE']!=0][['CONCEPTO','IMPORTE']]
CONCEPTO IMPORTE
14 Obsequios protocolarios 19.75
18 Obsequios protocolarios 399.30
22 Obsequios protocolarios 526.35
31 Recital de guitarra 605.00
33 Servicio de Guías del Museo Reina Sofía 332.75
47 Obsequios protocolarios 220.00
49 Obsequios protocolarios 110.00
53 Obsequios protocolarios 64.58
71 Pago desplazamiento de Tania Balló para asisti… 187.85
72 Organización, coordinación y difusión rueda pr… 211.75
93 Velas 40.00

Para tener datos con formato homogéneo movemos el contenido de la columna ‘CONCEPTO’ a cabececeras de columnas cuyo valor será la celda de ‘IMPORTE’ :

for x in gastos.index:
if gastos.iloc[[x]]['CONCEPTO'].values[0]!=0:
gastos.loc[x,gastos.iloc[[x]]['CONCEPTO'].values[0]]=gastos.iloc[[x]]['IMPORTE'].values[0]
gastos.columns

Index([‘APELLIDO 1’, ‘APELLIDO 2’, ‘NOMBRE’, ‘NUMTRANS’, ‘PUESTO’,
‘NOMBRE UNIDAD ADSCRIPCIÓN’, ‘NOMBRE AREA/JM/OOAA/EP’, ‘FECHA’,
‘MOTIVO’, ‘COMIDAS \nINSTITUCIONALES’, ‘PRODUCTOS \nALIMENTICIOS’,
‘FLORES’, ‘CATERING ‘, ‘TROFEOS DISTINCIONES’, ‘CONCEPTO’, ‘IMPORTE’,
‘Unnamed: 16’, ‘Obsequios protocolarios’, ‘Recital de guitarra’,
‘Servicio de Guías del Museo Reina Sofía’,
‘Pago desplazamiento de Tania Balló para asistir al acto de presentación a los medios de comunicación , dentro del Plan Memoria de Madrid, de un homenaje a las mujeres que formaron parte de la Generación del 27, en la Fundación José Ortega y Gasset-Gregori’,
‘Organización, coordinación y difusión rueda prensa música Semana Santa’,
‘Velas’],
dtype=’object’)

gastos.columns=['APELLIDO 1', 'APELLIDO 2', 'NOMBRE', 'NUMTRANS', 'PUESTO',
'NOMBRE UNIDAD', 'NOMBRE AREA', 'FECHA',
'MOTIVO', 'COMIDAS INSTITUCIONALES', 'PRODUCTOS ALIMENTICIOS',
'FLORES', 'CATERING', 'TROFEOS DISTINCIONES', 'CONCEPTO', 'IMPORTE',
'Unnamed: 16', 'Obsequios protocolarios', 'Recital de guitarra',
'Servicio de Guías del Museo Reina Sofía',
'Desp de Tania Balló',
'Rueda prensa música Semana Santa',
'Velas']

Para facilitar el trabajo definiremos una lista con las columnas relacionadas con Conceptos de gasto. Son columnas que tendrán un contenido numérico.

columnas_gastos=gastos.columns[[9,10,11,12,13,17,18,19,20,21,22]]
columnas_gastos

Index([‘COMIDAS INSTITUCIONALES’, ‘PRODUCTOS ALIMENTICIOS’, ‘FLORES’,
‘CATERING’, ‘TROFEOS DISTINCIONES’, ‘Obsequios protocolarios’,
‘Recital de guitarra’, ‘Servicio de Guías del Museo Reina Sofía’,
‘Desp de Tania Balló’, ‘Rueda prensa música Semana Santa’, ‘Velas’],
dtype=’object’)

De igual manera a como antes hicimos cambiaremos el formato del separador de miles y decimal.

for columna in columnas_gastos[0:5]:
gastos[columna]=gastos[columna].apply(lambda x: float(
str(x).replace(',', '%temp%').replace('.', '').replace('%temp%', '.')))

Creamos un par de columnas auxilares, y formateamos correctamente la columna de Fechas :
– Una de ella con la concatenacion de Nombre (‘NOMBRE’) y primer Apellido (‘APELLIDO 1’)
– La otra con la suma de todas las columnas de gastos de la fila

gastos['NOM_APELL'] = gastos.apply(lambda x: ''.join([str(x['NOMBRE']),' ',str(x['APELLIDO 1'])]), axis=1)

En la web se aclara que : «Para facilitar el análisis comparativo de los gastos, figuran todos los titulares en activo en algún momento del periodo al que se refiere el fichero, sin que conste cantidad alguna cuando no se hubiese producido algún gasto.». En nuestro caso vamos a borar esas lineas con gastos a cero.

gastos['gasto_total']=gastos[columnas_gastos].sum(axis=1)
gastos = gastos.drop(gastos[gastos.gasto_total==0].index)
<br />
gastos['FECHA']=pd.to_datetime(gastos['FECHA'],format='%d/%m/%Y')

Aseguramos que no hay celdas con N/A y eliminamos las columnas que no necesitamos

gastos.fillna(0,inplace=True)
del gastos['IMPORTE']
del gastos['CONCEPTO']
del gastos['NOMBRE']
del gastos['APELLIDO 1']
del gastos['APELLIDO 2']

Empezamos a calcular y a sacar los primeros resultados. ¿Cuánto es el total de gastos? y ¿Cuantas entradas tiene el fichero?

len(gastos)

92

total_gastos=gastos[columnas_gastos].sum().sum()
print ("{0:,.2f} euros".format(total_gastos))

31,146.54 euros

Veamos el nombre, cargo y área de las personas nominadas en la lista :

tabla_personas=gastos[['NOM_APELL','PUESTO','NOMBRE AREA']].pivot_table(index=['NOM_APELL','PUESTO','NOMBRE AREA'],aggfunc='count')
tabla_personas
NOM_APELL PUESTO NOMBRE AREA
CELIA MAYER CONCEJAL/A DE GOBIERNO AREA DE GOBIERNO DE CULTURA Y DEPORTES
ESTHER GOMEZ CONCEJAL/A PRESIDENTE/A DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO DE CARABANCHEL
FRANCISCO PEREZ CONCEJAL/A PRESIDENTE/A DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO DE VILLA DE VALLECAS
FRANCISCO PÉREZ CONCEJAL PRESIDENTE DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO PUENTE DE VALLECAS
INES SABANES CONCEJAL/A DE GOBIERNO AREA DE GOBIERNO DE MEDIO AMBIENTE Y MOVILIDAD
JOSE JAVIER BARBERO CONCEJAL/A DE GOBIERNO AREA DE GOBIERNO DE SALUD, SEGURIDAD Y EMERGENCIAS
JOSE MANUEL CALVO CONCEJAL/A DE GOBIERNO AREA DE GOBIERNO DE DESARROLLO URBANO SOSTENIBLE
MANUELA CARMENA ALCALDE/SA ALCALDIA
CONCEJAL/A DE GOBIERNO AREA DE GOBIERNO DE CULTURA Y DEPORTES
MARTA GOMEZ CONCEJAL/A PRESIDENTE/A DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO DE BARAJAS
JUNTA MUNICIPAL DEL DISTRITO DE SAN BLAS CANILLEJAS
MAURICIO VALIENTE CONCEJAL PRESIDENTE DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO DE CHAMARTIN
TERCER/A TENIENTE DE ALCALDIA PLENO
PABLO CESAR CARMONA CONCEJAL/A PRESIDENTE/A DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO DE MORATALAZ
ROMMY ARCE CONCEJAL/A PRESIDENTE/A DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO DE USERA
YOLANDA RODRIGUEZ CONCEJAL/A PRESIDENTE/A DE DISTRITO JUNTA MUNICIPAL DEL DISTRITO DE CIUDAD LINEAL

Vemos como por ejemplo nuestra Alcaldesa reporta los gastos desde sus dos funciones :
– Alcaldesa
– Concejala de deportes

Calculemos los gastos presentados por cada una de las personas :

gastos_nombre=gastos.groupby(['NOM_APELL'])['gasto_total']
gastos_nombre.sum().sort_values(ascending=False)

NOM_APELL
MANUELA CARMENA 21831.73
MAURICIO VALIENTE 3789.25
CELIA MAYER 1021.60
PABLO CESAR CARMONA 893.75
ESTHER GOMEZ 796.00
ROMMY ARCE 715.00
JOSE JAVIER BARBERO 464.86
MARTA GOMEZ 423.06
FRANCISCO PÉREZ 319.00
YOLANDA RODRIGUEZ 250.24
JOSE MANUEL CALVO 233.05
FRANCISCO PEREZ 209.00
INES SABANES 200.00
Name: gasto_total, dtype: float64

Manuela Carmena, lógicamente asociado a  la máxima posición en la jerarquia, tiene el volumen más alto de gasto de representación y protocolo. Bajo mi punto de vista es una señal de buena práctica corporativa. Veámoslo a continuación en una tabla, con referencia adicional al porcentaje sobre el gasto total :

pd_aux=gastos_nombre.sum().sort_values(ascending=False)
width=.5
ind = np.arange(len(pd_aux))

fig = plt.figure(1, (10,7))
ax = fig.add_subplot(1,1,1)

rects = ax.barh(ind,pd_aux.values, width, align='center',color='b', label='Euros')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de gasto',size=20)
ax.set_xlabel('Euros',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>20}'.format(x.strip()[:20]) for x in pd_aux.index],
size=12,rotation=0,color='b')
ax.invert_yaxis()
ax.get_xaxis().set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x,P: format(int(x), ',')))

ax.set_xticks([x for x in range(0,25000,2000)])

ax.grid(axis='x')
ax.set_title('Gasto por concepto')
ax.legend(loc=5)
fig.suptitle(fuente,size=20,x=1,y=0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/total_gastos), size=16, ha='left', va='center')

plt.show()
fig.savefig('gastos_persona',bbox_inches = 'tight')

output_53_0

Calculemos ahora los gastos por posición y no por nombre :

gastos_depto=gastos.groupby(['NOMBRE UNIDAD'])['gasto_total']
gastos_depto.sum().sort_values(ascending=False)

NOMBRE UNIDAD
ALCALDE 21710.04
CONCEJAL PRESIDENTE JMD 2239.80
PLENO 1549.45
DELEGACION AREA DE GOBIERNO DE CULTURA Y DEPORTES 1143.29
CONCEJAL PRESIDENTE JMD MORATALAZ 893.75
CONCEJAL PRESIDENTE JMD CARABANCHEL 796.00
CONCEJAL PRESIDENTE JMD USERA 715.00
DELEGACION AREA DE GOBIERNO DE SALUD, SEGURIDAD,Y EMERGENCIAS 464.86
CONCEJAL PRESIDENTE JMD SAN BLAS CANILLEJAS 384.56
CONCEJAL PRESIDENTE JMD PUENTE DE VALLECAS 319.00
CONCEJAL PRESIDENTE JMD CIUDAD LINEAL 250.24
DELEGADO AREA DE GOBIERNO DE DESARROLLO URBANO SOSTENIBLE 233.05
CONCEJAL PRESIDENTE JMD VILLA DE VALLECAS 209.00
DELEGACION AREA DE GOBIERNO DE MEDIO AMBIENTE Y MOVILIDAD 200.00
CONCEJAL PRESIDENTE JMD BARAJAS 38.50
Name: gasto_total, dtype: float64

Vemos que como Alcaldesa Manuela Carmena tiene 21.710 €, muy poco menos que el total a su nombre. Veamos esos resultados en un gráfico, con porcentajes también para ver los números relativos

pd_aux=gastos_depto.sum().sort_values(ascending=False)
N=5
width=.5
ind = np.arange(len(pd_aux))

fig = plt.figure(1, (10,7))
ax = fig.add_subplot(1,1,1)

rects = ax.barh(ind,pd_aux.values, width, align='center',color='b', label='Miles de Euros')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de gasto',size=20)
ax.set_xlabel('Acumulado miles de euros',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>50}'.format(x.strip()[:50]) for x in pd_aux.index],
size=12,rotation=0,color='b')
ax.invert_yaxis()
ax.get_xaxis().set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x,P: format(int(x/1000), ',')))

ax.set_xticks([x for x in range(0,24000,2000)])
ax.grid(axis='x')
ax.set_title('Gasto por concepto')
ax.legend(loc=5)
fig.suptitle(fuente,size=20,x=1,y=0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/total_gastos), size=16, ha='left', va='center')

plt.show()
fig.savefig('gastos_persona',bbox_inches = 'tight')

output_57_0

Otra tabla : distribución de gasto por concepto (los relacionados en columnas_gastos)

pd_aux=gastos[columnas_gastos].sum().sort_values(ascending=True)
N=5
width=.5
ind = np.arange(len(pd_aux))
fig, ax = plt.subplots()
rects = ax.barh(ind, pd_aux.values, width, align='center',color='b', label='Euros')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de gasto',size=20)
ax.set_xlabel('Acumulado euros',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>60}'.format(x.strip()[:60]) for x in pd_aux.index],
size=12,rotation=0,color='b')
#ax.invert_yaxis()

ax.grid(axis='x')
ax.set_title('Gasto por concepto')
ax.legend(loc=4)
fig.suptitle(fuente,size=10,x=1,y=-0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/total_gastos), size=14, ha='left', va='center')

plt.show()
fig.savefig('gastos_euros_concepto',bbox_inches = 'tight')

output_59_0

A continuación veamos la distribución por eventos, es decir filtrando por los motivos incluidos en cada uno de los gastos. Hemos limitado, con el objetivo de que quede una gráfica legible, a 20 eventos. He incluido dos gráficas : la primera con los 20 eventos con mayor gasto y otra equivalente con los 20 con menor gasto.

gastos_motivo=gastos.groupby(['MOTIVO'])['gasto_total']
gastos_motivo.sum().sort_values(ascending=False)

MOTIVO
Adquisición de Llaves de Oro de la Villa, para su entrega al Presidente de la República Argentina D. Mauricio Macri y otra para stock para futuras visitas de Estado 3243.36
Atención protocolaria con motivo del Acto Jubilados del Ayuntamiento de Madrid 2016, celebrado en los Jardines de Cecilio Rodríguez 2970.00
Atención protocolaria con motivo de la entrega de medallas de Oro de la Ciudad 2805.00
Atención protocolaria con motivo de la Presentación Madrid en FIL 2017 (Feria Internacional del Libro de Guadalajara, México) bajo el lema «Ganarás la Luz» 1958.00
Atención protocolaria con motivo del acto de inauguración de la exposición de pintura municipal «La mirada de nuestro tiempo» 1169.74
Cóctel inauguración asignación nombre «Manuel Alonso Zapata» a sala exposiciones Centro Cultural Nicolás Salmerón. Asociaciones de Vecinos y Grupos Municipales del distrito. 1000.00
Cóctel inauguración Centro Socio Comunitario Colonias Históricas de Chamartín. Asociaciones de Vecinos y Grupos Municipales del distrito. 952.00
Atención protocolaria con motivo de la visita institucional de los participantes en el Digital Entreprise Show, Presidentes de las empresas Partner, Ceos, Country Manager y embajadores de Israel y Suecia, entre otros 949.41
Adquisición de obsequios para su entrega como atención institucional y protocolaria 925.65
Placa entregada con ocasión aniversario liberación campo de Mauthausen, en recuerdo y homenaje a los madrileńos que sufrieron los horrores de los campos de concentración nazi 907.50
Rosquillas para los centros municipales de mayores con motivo de las Fiestas de San Isidro 796.00
Inauguración Semana del Libro en el Distrito de Usera 715.00
Atención institucional y protocolaria con motivo de la celebración del Pleno de Adolescentes 686.40
Atención protocolaria celebrada con motivo de la visita institucional del Presidente de Ecuador, Excmo. Sr. Don Rafael Correa 660.00
Atención protocolaria de carácter institucional con motivo de la presencia en Madrid de alcaldes y autoridades de varios países, para asistir al Foro Mundial sobre las Violencias Urbanas y Educación para la Convivencia y la Paz 654.50
Recital de guitarra como atención protocolaria en el acto de inauguración del Jardín de los Combatientes de la Nueve, al que asistieron entre otros la Alcaldesa de París, Anne Hidalgo 605.00
Organización del acto de presentación de la programación musical de la edición 2017 de la Semana Santa, en la capilla del Museo de Historia 563.75
Adquisición de medallas de Oro de la Ciudad para su entrega en la Fiesta de San Isidro 540.53
Servicio de catering para acto de presentación a los medios de comunicación, dentro del Plan Memoria de Madrid, de un homenaje a las mujeres que formaron parte de la Generación del 27, en la Fundación José Ortega y Gasset-Gregorio Marańón 457.85
Adquisición de flores para el acto protocolario de inauguración del Jardín de los Combatientes de la Nueve, al que asistieron entre otros la Alcaldesa de París, Anne Hidalgo 432.00
Adquisición de medallas de oro, plata y bronce, para su entrega en el Concurso de Rosas Nuevas 333.96
Atención protocolaria a las autoridades invitadas asistentes al Foro Mundial sobre las Violencias Urbanas y Educación para la Convivencia y la Paz, celebrado el 21 de abril de 2017, consistente en un servicio de Guía en castellano y otro Guía en inglés pa 332.75
Desayuno sesión Foro Local de Moratalaz 330.00
Sesión Foro Local de Moratalaz 330.00
Atención protocolaria con motivo de la visita institucional de los integrantes de la Alegre Cofradía del Entierro de la Sardina, durante la Fiesta del Carnaval 2017 323.40
Atención protocolaria. Centros de flores, con motivo de la visita institucional del Presidente de Ecuador, Excmo. Sr. Don Rafael Correa 320.00
Ofrenda floral Estación del Pozo. Conmemoración 319.00
Comida ofrecida a los miembros del Comisionado de la Memoria Histórica 277.50
Atención protocolaria con motivo del pregón de la Fiesta de San Isidro 272.49
Atención protocolaria con motivo de la celebración el día 9 de mayo de 2017 en el Palacio de Cibeles, del I Encuentro de Municipios Madrileńos Comprometidos con el Refugio, en materia de acogida personas migrantes y refugiadas 250.02

Atención institucional y protocolaria, con motivo de la recepción en el Palacio de Cibeles al Real Madrid, como campeón de la Champios League 2017 110.00
Atención protocolaria con motivo de la visita institucional de la Asociación de Cámaras de Comercio Europeas, en la Sala de Madera del Palacio de Cibeles 106.26
Atención protocolaria con motivo de la recepción a un grupo de profesores y alumnos de la Fundación International Studies Miami, en la Sala de Madera del Palacio de Cibeles 94.78
Reunión con Director de Gabinete, Gerente Organismo Autonomo Madrid Salud, Subdirector General de Prevención y Promoción de la Salud y la Jefa de Servicio de Prevención y Promoción de Salud, para tratar el tema sobre reorientación del organismo Autónomo M 81.20
Gasto suplido por adquisición de flores para el acto protocolario de inauguración del Jardín de los Combatientes de la Nueve, al que asistieron entre otros la Alcaldesa de París, Anne Hidalgo (Grujeval SL) 75.00
Atención protocolaria con motivo de la visita institucional del Gobernador de San Petesburgo y su Delegación 73.49
Flores entregadas en Mauthausen, en recuerdo y homenaje a los madrileńos que sufrieron los horrores de los campos de concentración nazi 68.40
Reunión con Coordinador General de Seguridad y Emergencias, Director de Gabinete, Asesor, Sargento de Policia, para tratar el tema sobre la unidad de gestión por la diversidad 67.20
Atención protocolaria con motivo de la celebración en el Palacio de Cibeles, de un Encuentro de Mediación Escolar con Centros Educativos de Primaria y Secundaria 64.58
Comida ofrecida a especialistas en Derechos Humanos pertenecientes a la Universidad Autónoma y Complutense de Madrid 63.25
Atención protocolaria con motivo de la visita de miembros de la Asociación de Familiares de Víctimas del accidente del Yack42 54.54
Desayuno ofrecido a los miembros del Comisionado de la Memoria Histórica 49.15
Encuentro equipo de trabajo preparatorio Foro Local de Chamartín. Total 3 comensales 47.80
Atención protocolaria con motivo de la visita institucional del Canciller de México, a quien acompańaban la Embajadora de México en Espańa y una delegación de ese país 45.75
Entrega de velas para el acto de encendido de velas conmemorativas en el Día Internacional de Conmemoración Anual en Memoria de las Victimas del Holocausto 40.00
Con motivo del Día Internacional de la Mujer, el 8 de marzo, se obsequia con un clavel morado a todas las mujeres que asistan al Pleno del Distrito, que se celebra el mismo día 38.50
Atención protocolaria con motivo de la visita de una delegación de Israel, encabezada por el Alcalde de Tel Aviv, Mr. Ron Huldai y por el Embajador de ese país en Espańa, Mr. Daniel Kutner 37.95
Reunión con Coordinador General de Seguridad y Emergencias, Asesor, Director General de Policia Municipal; Inspector Jefe, Subinspector, Oficial Subinspección territorial de Villa de Vallecas, subinspector Territorial de Puente de Vallecas, Oficial Subins 32.00
Reunión con el Secretario General de Caritas Espańola, para tratar temas relacionados con el acceso a la salud de la población vulnerable 30.80
Reunión con estudiante para tratar tema sobre participación y problemas de botellón 25.00
Atención protocolaria con motivo de la visita de los familiares de D. Leopoldo López 24.50
Atención protocolaria con motivo de la visita institucional del Presidente de la Generalitat D. Carles Puigdemont y del Vicepresidente D. Oriol Junqueras 24.50
Reunión con experta en adicciones para tratar el tema sobre la problemática de las adicciones 23.60
Reunión con Director General de Seguridad de Madrid destino para tratar temas relativos a seguridad 22.30
Reunion con Concejal del Grupo Municipal PSOE para la realización de análisis político 20.30
Atención protocolaria a la Alcaldesa de Roma Dońa Virginia Raggi, al Palazzo Senatorio en Roma 19.75
Reunión con representante de Forum de Ciudadanos para la Paz para tratar temas relativos a la seguridad 19.00
Pegamento para la placa entregada con ocasión aniversario liberación campo de Mauthausen, en recuerdo y homenaje a los madrileńos que sufrieron los horrores de los campos de concentración nazi 15.80
Atención protocolaria con motivo de la cuestación contra el cáncer, organizada por la Asociación Espańola contra el Cáncer, ofreciéndose un desayuno a los integrantes de la mesa que se instaló en la Plaza de Cibeles 11.10
Café ofrecido a los participantes en el 1ş Encuentro en la Casa de Cisneros «Homenaje a Herman Heller» 8.85
Name: gasto_total, Length: 82, dtype: float64

pd_aux=gastos_motivo.sum().sort_values(ascending=False).head(20)
N=5
width=.7
ind = np.arange(len(pd_aux))

fig = plt.figure(1, (30,30))
ax = fig.add_subplot(1,1,1)

rects = ax.barh(ind,pd_aux.values, width, align='center',color='b', label='Euros')

ax.set_ylabel('Concepto de gasto',size=30)
ax.set_xlabel('Acumulado en euros',size=30)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>100}'.format(x.strip()[:100]) for x in pd_aux.index],
size=40,rotation=0,color='b')
ax.invert_yaxis()
ax.get_xaxis().set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x,P: format(int(x), ',')))

ax.set_xticks([x for x in range(0,4000,500)])
for item in (ax.get_xticklabels()):
item.set_fontsize(35)

ax.grid(axis='x')
ax.set_title('Eventos',size=40)
ax.legend(loc=5,fontsize=40)
fig.suptitle(fuente,size=40,x=1,y=0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/total_gastos), size=35, ha='left', va='center')

plt.show()
fig.savefig('gastos_evento',bbox_inches = 'tight')

output_62_0

pd_aux=gastos_motivo.sum().sort_values(ascending=False).tail(20)
N=5
width=.7
ind = np.arange(len(pd_aux))

fig = plt.figure(1, (30,30))
ax = fig.add_subplot(1,1,1)

rects = ax.barh(ind,pd_aux.values, width, align='center',color='b', label='Euros')

ax.set_ylabel('Concepto de gasto',size=40)
ax.set_xlabel('Acumulado en euros',size=40)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>100}'.format(x.strip()[:100]) for x in pd_aux.index],
size=40,rotation=0,color='b')
ax.invert_yaxis()
ax.get_xaxis().set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x,P: format(int(x), ',')))

ax.set_xticks([x for x in range(0,200,50)])
for item in (ax.get_xticklabels()):
item.set_fontsize(40)

ax.grid(axis='x')
ax.set_title('Eventos',size=30)
ax.legend(loc=5,fontsize=50)
fig.suptitle(fuente,size=40,x=1,y=0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/total_gastos), size=40, ha='left', va='center')

plt.show()
fig.savefig('gastos_evento',dpi=200,bbox_inches = 'tight')

output_63_0

Y veamos que se pueden limitar los gráficos a una persona en particular. Veamos aqui la misma serie anterior, pero filtrada para presentar sólo los datos de la Alcaldesa Carmena.

pd_aux=gastos[gastos['NOM_APELL']=='MANUELA CARMENA'][columnas_gastos].sum().sort_values(ascending=True)
gastos_carmena=pd_aux.sum()
N=5
width=.5
ind = np.arange(len(pd_aux))
fig, ax = plt.subplots()
rects = ax.barh(ind, pd_aux.values, width, align='center',color='b', label='Euros')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de gasto',size=20)
ax.set_xlabel('Acumulado euros',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>60}'.format(x.strip()[:60]) for x in pd_aux.index],
size=12,rotation=0,color='b')
#ax.invert_yaxis()

ax.grid(axis='x')
ax.set_title('Gasto por concepto')
ax.legend(loc=4)
fig.suptitle(fuente,size=10,x=1,y=-0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/gastos_carmena), size=14, ha='left', va='center')

plt.show()
fig.savefig('gastos_euros_concepto',bbox_inches = 'tight')

output_65_0

gastos_motivo=gastos[gastos['NOM_APELL']=='MANUELA CARMENA'].groupby(['MOTIVO'])['gasto_total']
pd_aux=gastos_motivo.sum().sort_values(ascending=False).head(30)
width=.5
ind = np.arange(len(pd_aux))

fig = plt.figure(1, (30,30))
ax = fig.add_subplot(1,1,1)

rects = ax.barh(ind,pd_aux.values, width, align='center',color='b', label='Euros')

ax.set_ylabel('Concepto de gasto',size=40)
ax.set_xlabel('Acumulado en euros',size=40)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>100}'.format(x.strip()[:100]) for x in pd_aux.index],
size=50,rotation=0,color='b')
ax.invert_yaxis()
ax.get_xaxis().set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x,P: format(int(x), ',')))

ax.set_xticks([x for x in range(0,4000,500)])
for item in (ax.get_xticklabels()):
item.set_fontsize(40)

ax.grid(axis='x')
ax.set_title('Eventos',size=40)
ax.legend(loc=5,fontsize=50)
fig.suptitle(fuente,size=40,x=1,y=0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/gastos_carmena), size=40, ha='left', va='center')

plt.show()
fig.savefig('gastos_evento',bbox_inches = 'tight')

output_66_0

gastos_motivo=gastos[gastos['NOM_APELL']=='MANUELA CARMENA'].groupby(['MOTIVO'])['gasto_total']
pd_aux=gastos_motivo.sum().sort_values(ascending=False).tail(30)
N=5
width=.5
ind = np.arange(len(pd_aux))

fig = plt.figure(1, (30,30))
ax = fig.add_subplot(1,1,1)

rects = ax.barh(ind,pd_aux.values, width, align='center',color='b', label='Euros')

ax.set_ylabel('Concepto de gasto',size=40)
ax.set_xlabel('Acumulado en euros',size=40)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>100}'.format(x.strip()[:100]) for x in pd_aux.index],
size=50,rotation=0,color='b')
ax.invert_yaxis()
ax.get_xaxis().set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x,P: format(int(x), ',')))

ax.set_xticks([x for x in range(0,500,50)])

for item in (ax.get_xticklabels()):
item.set_fontsize(50)

ax.grid(axis='x')
ax.set_title('Eventos',size=40)
ax.legend(loc=5,fontsize=40)
fig.suptitle(fuente,size=40,x=1,y=0.01)

for rect in rects:
width = rect.get_width()
plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/gastos_carmena), size=40, ha='left', va='center')

plt.show()
fig.savefig('gastos_evento',bbox_inches = 'tight')

output_67_0

Se me ocurre preparar una gráfica como la anterior, pero categorizando los gastos según su concepto (de nuevo bajo columnas_gastos)

lista_ordenada=gastos.sort_values('gasto_total',ascending=False).head(20)
ind = np.arange(len(lista_ordenada))
fig = plt.figure(1, (30,30))
ax = fig.add_subplot(1,1,1)
width=.5
suelo=0

for i in columnas_gastos:
ax.barh(ind,lista_ordenada[i], width, align='center',
label=i[0:30],left=suelo)
suelo=suelo+lista_ordenada[i]

#ax.set_ylabel('Concepto de gasto',size=50)
ax.set_xlabel('Acumulado en euros',size=50)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>100}'.format(x.strip()[:100]) for x in lista_ordenada['MOTIVO']],
size=50,rotation=0,color='b')
ax.invert_yaxis()
ax.get_xaxis().set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x,P: format(int(x), ',')))

ax.set_xticks([x for x in range(0,4000,500)])
for item in (ax.get_xticklabels()):
item.set_fontsize(50)

ax.grid(axis='x')
ax.set_title('Eventos',size=50)
ax.legend(loc=4,fontsize=50)
fig.suptitle(fuente,size=50,x=1,y=0.01)

plt.show()
fig.savefig('gastos_evento',bbox_inches = 'tight')

output_70_0

Para finalizar juguemos un poco con las fechas..

raw = gastos.sort_values('FECHA')['FECHA']
vals= gastos.sort_values('FECHA')['gasto_total']
fig, ax = plt.subplots()
ax.plot_date(raw, vals)
date_fmt = '%m-%y'
date_formatter = mdates.DateFormatter(date_fmt)
ax.xaxis.set_major_formatter(date_formatter)
fig.autofmt_xdate()
plt.show()

output_72_0

Vemos que hay un gasto del año pasado. Bien por la transparencia al haberse incluido, pero nos deja el gráfico descolocado!. Identifiquemosló para eliminarlo :

gastos[gastos['FECHA']<datetime.datetime.strptime('01/01/2017', "%d/%m/%Y")][['NOM_APELL','PUESTO','FECHA','MOTIVO','gasto_total']]
NOM_APELL PUESTO FECHA MOTIVO gasto_total
67 ESTHER GOMEZ CONCEJAL/A PRESIDENTE/A DE DISTRITO 2016-05-13 Rosquillas para los centros municipales de may… 796.0
print (gastos.loc[[67]]['MOTIVO'].values[0])

Rosquillas para los centros municipales de mayores con motivo de las Fiestas de San Isidro

Generemos de nuevo la gráfica eliminando esa entrada :

raw = gastos[gastos['FECHA']>datetime.datetime.strptime('01/01/2017','%d/%m/%Y')].sort_values('FECHA')['FECHA']
vals= gastos[gastos['FECHA']>datetime.datetime.strptime('01/01/2017','%d/%m/%Y')].sort_values('FECHA')['gasto_total']
#fig, ax = plt.subplots()
fig = plt.figure(1, (12,7))
ax = fig.add_subplot(1,1,1)
ax.plot_date(raw, vals)
ax.tick_params(axis='x',length=10)
ax.xaxis.set_major_locator(mdates.DayLocator(bymonthday=(1,10,20)) )
date_fmt ='%d-%m'
ax.xaxis.set_major_formatter(mdates.DateFormatter(date_fmt))
fig.autofmt_xdate()
plt.show()

output_77_0

Ahora queda una gráfica mucho más clara. Sólo por jugar un poco más con ella veamos la acumulación de gastos alrededor del 20 de Abril. Para ello filtramos los datos a los días entre el 18 y el 22 de April.

inicio=datetime.datetime.strptime('18/04/2017','%d/%m/%Y')
final=datetime.datetime.strptime('22/04/2017','%d/%m/%Y')
raw = gastos[(gastos['FECHA']>inicio) & (gastos['FECHA']<final)].sort_values('FECHA')['FECHA'] vals= gastos[(gastos['FECHA']>inicio) & (gastos['FECHA']<final)].sort_values('FECHA')['gasto_total']
#fig, ax = plt.subplots()
fig = plt.figure(1, (10,7))
ax = fig.add_subplot(1,1,1)
ax.plot_date(raw, vals)
ax.tick_params(axis='x',length=10)
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1) )
date_fmt ='%d-%m'
ax.xaxis.set_major_formatter(mdates.DateFormatter(date_fmt))
fig.autofmt_xdate()
plt.show()

output_79_0

Efectivamente vemos que el día 20 se reportaron varios gastos. Identifiquémoslos en una tabla :

gastos[gastos['FECHA']==datetime.datetime.strptime('20/04/2017', "%d/%m/%Y")][['NOM_APELL','PUESTO','FECHA','MOTIVO','gasto_total']]

NOM_APELL PUESTO FECHA MOTIVO gasto_total
28 MANUELA CARMENA ALCALDE/SA 2017-04-20 Adquisición de flores para el acto protocolari… 432.00
29 MANUELA CARMENA ALCALDE/SA 2017-04-20 Gasto suplido por adquisición de flores para e… 75.00
30 MANUELA CARMENA ALCALDE/SA 2017-04-20 Atención protocolaria con motivo de la visita … 45.75
31 MANUELA CARMENA ALCALDE/SA 2017-04-20 Recital de guitarra como atención protocolaria… 605.00
32 MANUELA CARMENA ALCALDE/SA 2017-04-20 Atención protocolaria de carácter instituciona… 654.50
80 YOLANDA RODRIGUEZ CONCEJAL/A PRESIDENTE/A DE DISTRITO 2017-04-20 Acto Inauguración Jardín Combatientes de La Nu… 120.00
87 MAURICIO VALIENTE CONCEJAL PRESIDENTE DE DISTRITO 2017-04-20 Cóctel inauguración Centro Socio Comunitario C… 952.00

Hemos terminado…hemos visto como usar Python3, Pandas y MathPlotLib para importar una tabla de Datos Abiertos, ver el formato, realizar unos arreglos para preparala y empezar a sacar resultados presentándolos en modo tabla y gráficos. Reitero el agradecimiento al Ayuntamiento de Madrid por este ejercicio de transparencia.

He repasado los resultados varias veces. Si aun así detectan algún error ruego que me lo comuniquen (mharias@me.com) y lo subsanaré lo más urgente que pueda. Pido de antemano disculpas si encuentran algo incorrecto.

Cómo analizar multas con Python y Pandas

Análisis con Python3 de multas de circulación impuestas en Madrid durante Junio 2017

Vamos a analizar el fichero de multas del Ayuntamiento de Madrid, con información sacada del portal de OPenData : http://datos.madrid.es

Primero importamos las librerias necesarias : pandas, numpy y matplotlib

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import datetime
import matplotlib.dates as mdates
%matplotlib inline
import matplotlib.ticker as mtick
from matplotlib.ticker import FuncFormatter
pd.options.display.float_format = '{:,.1f}'.format

Preparamos una texto para incluirlo en cada gráfico como fuente…

fuente='Fuente : Ayuntamiento de Madrid, http://datos.madrid.es'

Preparando la URL de la fuente de datos

path_web='http://datos.madrid.es/egob/catalogo/210104-158-multas-circulacion-detalle.csv'
path_local='/Users/waly/Downloads/201704_detalle_v1.csv'

cabecera de las columnas

nombre_columnas=['CALIFICACION','LUGAR','MES','ANIO','HORA','IMP_BOL','DESCUENTO','PUNTOS','DENUNCIANTE','HECHO_BOL','VEL_LIMITE','VEL_CIRCULA','COORDENADA_X','COORDENADA_Y']

Leemos los datos desde su localizacion en ‘path_web’, en este fichero tenemos los datos de Junio de 2017. Al respecto de la la identificacion de la multa en el tiempo tendremos la hora pero no el día del mes, es decir : tendremos las multas puestas a las 13:10 a lo largo de todo el mes, pero no podremos partirlas por día. No encuentro otra razón que no sea evitar cualquier vía de identificación del conductor.

multas=pd.read_csv(path_web,sep=";",encoding='windows-1250',index_col=False,header=None,names=nombre_columnas,skiprows=1)

confirmamos que ha bajado correctamente

multas.columns
Index(['CALIFICACION', 'LUGAR', 'MES', 'ANIO', 'HORA', 'IMP_BOL', 'DESCUENTO',
       'PUNTOS', 'DENUNCIANTE', 'HECHO_BOL', 'VEL_LIMITE', 'VEL_CIRCULA',
       'COORDENADA_X', 'COORDENADA_Y'],
      dtype='object')

Convertimos la columna ‘HORA’ con horas tal que 21.30 en datetime

multas['HORA']=pd.to_datetime(multas['HORA'],format='%H.%M')

Añadimos una columna hora_entera, tal que la hora (desde 00 hasta 23) para facilidad de cálculo de algunos gráficos..

for n in range(0,multas.shape[0]):
    multas.set_value(n,'hora_entera',multas.loc[n,'HORA'].strftime('%H')+':00');

Hay que tratar un poco los dos campos relacionados con velocidad (limite y velocidad multada:
a) tanto los vaklores de aquellas multas no relacionadas con velocidad en las que el valor es un string de 4 caracteres BS : ‘ ‘
b) Aquellos relacionados con velocidad en los que hay que convertir el string con la velocidad a un integer.
He generado un par de columnas adicionales para contener estos datos ya tratados..

velocidad=lambda x : 0 if x=='   ' else int(x.strip())
multas['velocidad_limite']=multas['VEL_LIMITE'].apply(velocidad)
multas['velocidad_circulacion']=multas['VEL_CIRCULA'].apply(velocidad)
multas.columns
Index(['CALIFICACION', 'LUGAR', 'MES', 'ANIO', 'HORA', 'IMP_BOL', 'DESCUENTO',
       'PUNTOS', 'DENUNCIANTE', 'HECHO_BOL', 'VEL_LIMITE', 'VEL_CIRCULA',
       'COORDENADA_X', 'COORDENADA_Y', 'hora_entera', 'velocidad_limite',
       'velocidad_circulacion'],
      dtype='object')

Empezamos a sacar algunos resultados :

Número total de multas : 246.991 multas en Junio 2017

len(multas)
246991

Cuántos puntos se han perdido en esas multas? : 28.617

puntos_totales=multas.PUNTOS.sum()
puntos_totales
28617

Cual es la suma del los importes de esas multas, antes de descuento? : 24.498.900€

euros_totales=multas.IMP_BOL.sum()
euros_totales
24498900.0

Cual es el importe de la multa de más importe?

multas.IMP_BOL.max()
1000.0

Veamos la distribución de multas por hora : (recordamos de nuevo que en esta gráfica se representa el acumulado en esa hora de todos los dias del mes)

multas_hist=multas['hora_entera'].value_counts().sort_index(axis=0)

fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=multas_hist.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('Número de  multas',size=16)
ax.grid(axis='y')
ax.set_title('Multas acumuladas por hora')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('multas_hora',bbox_inches = 'tight')

output_43_0

Con porcentajes en vez de números absolutos :

multas_hist_porcentaje=multas['hora_entera'].value_counts().sort_index(axis=0)/len(multas)*100
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=multas_hist_porcentaje.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('% multas',size=16)
ax.grid(axis='y')
ax.set_title('Porcentajes de multas en cada hora')
fmt = '%3.1f%%'
yticks = mtick.FormatStrFormatter(fmt)
ax.yaxis.set_major_formatter(yticks)
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('multas_hora_porcentaje',bbox_inches = 'tight')

output_45_0

Seguimos con la distribución de euros y puntos perdidos por hora :

multas_euros=multas.sort_values('HORA').groupby("hora_entera",sort=False).IMP_BOL
fig1 = plt.figure()
ax1 = fig1.add_subplot(1,1,1)
ax1 = multas_euros.sum().plot.bar()
ax1.locator_params(axis='y',nbins=10)
ax1.set_xlabel('Hora del día',size=16)
ax1.set_ylabel('Euros',size=16)
ax1.tick_params(axis='x',direction='out', length=6, width=2, colors='black')
#ax1.set_xticklabels(multas_euros['hora_entera'])
ax1.grid(axis='y')
ax1.set_title('Distribucion Euros de multa por hora')
fig1.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('euros_hora',bbox_inches = 'tight')

output_47_0

multas_puntos=multas.sort_values('HORA').groupby("hora_entera",sort=False).PUNTOS
fig1 = plt.figure(1,(7,4))
ax1 = fig1.add_subplot(1,1,1)
ax1 = multas_puntos.sum().plot.bar()
ax1.locator_params(axis='y',nbins=10)
ax1.set_xlabel('Hora del día',size=16)
ax1.set_ylabel('Puntos',size=16)
ax1.tick_params(axis='x',direction='out', length=6, width=2, colors='black')
#ax1.set_xticklabels(multas_euros['hora_entera'])
ax1.grid(axis='y')
ax1.set_title('Distribucion Puntos por hora')
fig1.suptitle(fuente,size=10,x=1,y=-0.01)
fig1.savefig('puntos_hora',bbox_inches = 'tight')

output_48_0

He calculado un par de ratios de interés, euros por multa y puntos por multa a lo largo del día, viendo que la media por la noche es significativamente superior a la media del día. Aquí vemos exclusivamente que las multas de la noche traen más euros y quitan as puntos que las multas de día, cosa que parece razonable, en ambos casos vemos que la hora caliente es de 04:00 a 05:00 de la madrugada, con más de 160€ y casi 0.6 puntos por multa.

ratio_euros_multas=multas_euros.sum()/multas_hist
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=ratio_euros_multas.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('€/multa',size=16)
ax.grid(axis='y')
ax.set_title('Media de € por multa')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('media_euros_multa',bbox_inches = 'tight')

output_50_0

ratio_puntos_multas=multas_puntos.sum()/multas_hist
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=ratio_puntos_multas.plot.bar()
ax.locator_params(axis='y',nbins=10)
ax.set_xlabel('Hora del día')
ax.set_ylabel('Puntos/multa',size=16)
ax.grid(axis='y')
ax.set_title('Media de puntos por multas en cada hora')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('media_puntos_multa',bbox_inches = 'tight')

output_51_0

Toca ahora analizar las multas según su tipo, siguiendo con la terminología del fichero : con el HECHO_BOL, el hecho descrito en el boletín de multa. Empezamos por las multas más frecuentes según tipo, podemos ver que más de 55000 multas vienen de saltarse las restricciones de trafico en zonas con circulación limitado, como ejemplo el centro de MAdrid. Analizaremos en profundidad este hecho en otro post. Continuaremos con un par de tablas con los puntos y euros de los hechos de multas que más puntos retiran (slatarse un semaforo en rojo) y euros recaudan (circular por zonas limitadas), y esas mismas tablas pasadas a gráficos.

multas_hecho=multas.HECHO_BOL.value_counts()
fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax=multas_hecho.head(10).plot.barh()
ax.locator_params(axis='y',nbins=10)
ax.locator_params(axis='x',nbins=20)
ax.set_xlabel('Número de multas',size=20)
ax.grid(axis='x')
ax.invert_yaxis()
ax.set_yticklabels(['{:>80}'.format(x.strip()[:80]) for x in multas_hecho.index],size=10)
ax.set_title('Hechos denunciados más frecuentes')
fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('hechos_fecuentes',bbox_inches = 'tight')

output_53_0

multas_hecho_importe=multas.groupby('HECHO_BOL')
multas_hecho_importe['IMP_BOL','PUNTOS'].sum().sort_values('PUNTOS',ascending=False).reset_index().head()

.dataframe thead tr:only-child th {
text-align: right;
}

.dataframe thead th {
text-align: left;
}

.dataframe tbody tr th {
vertical-align: top;
}

HECHO_BOL IMP_BOL PUNTOS
0 REBASAR UN SEMÁFORO EN FASE ROJA. … 438,400.0 8532
1 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 1,852,400.0 8372
2 UTILIZAR MANUALMENTE PANTALLAS, NAVEGADORES, D… 223,000.0 3303
3 CONDUCIR CON UNA TASA DE ALCOHOLEMIA NO PERMIT… 310,000.0 1830
4 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 3,504,000.0 1438
multas_hecho_importe['IMP_BOL','PUNTOS'].sum().sort_values('IMP_BOL',ascending=False).reset_index().head()


.dataframe thead tr:only-child th {
text-align: right;
}

.dataframe thead th {
text-align: left;
}

.dataframe tbody tr th {
vertical-align: top;
}

HECHO_BOL IMP_BOL PUNTOS
0 NO RESPETAR LAS SEŃALES EN UNA VÍA DE CIRCULAC… 5,100,750.0 0
1 ESTACIONAR, SIN LA CORRESPONDIENTE AUTORIZACIÓ… 4,564,800.0 0
2 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 3,504,000.0 1438
3 SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 1,852,400.0 8372
4 ESTACIONAR EN LUGAR PROHIBIDO DEBIDAMENTE SEŃA… 1,512,090.0 0
pd_aux=multas_hecho_importe['PUNTOS'].aggregate('sum').sort_values(ascending=False).head(7)
N=5
width=.5
ind = np.arange(len(pd_aux))
fig, ax = plt.subplots()
rects = ax.barh(ind, pd_aux.values, width, align='center',color='b', label='Puntos')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de multa',size=20)
ax.set_xlabel('Acumulado euros de multas',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>80}'.format(x.strip()[:80]) for x in pd_aux.index],
                   size=12,rotation=0,color='b')
ax.invert_yaxis()


ax.grid(axis='x')
ax.set_title('Puntos por tipo multa')
ax.legend()
fig.suptitle(fuente,size=10,x=1,y=-0.01)

for rect in rects:
    width = rect.get_width()
    plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/puntos_totales), size=16, ha='left', va='center')

plt.show()
fig.savefig('puntos_hecho_multa',bbox_inches = 'tight')

output_58_0

Redondeando podemos ver que el 30% de los puntos retirados se pierden saltándose un semáforo en rojo..

pd_aux=multas_hecho_importe['IMP_BOL'].aggregate('sum').sort_values(ascending=False).head(10)
N=5
width=.5
ind = np.arange(len(pd_aux))
fig, ax = plt.subplots()
rects = ax.barh(ind, pd_aux.values, width, align='center',color='b', label='Euros')
ax.set_title('Simple plot')
ax.set_ylabel('Concepto de multa',size=20)
ax.set_xlabel('Acumulado euros de multas',size=16)
ax.set_yticks(ind)
ax.set_yticklabels(['{:>80}'.format(x.strip()[:80]) for x in pd_aux.index],
                   size=12,rotation=0,color='b')
ax.invert_yaxis()


ax.grid(axis='x')
ax.set_title('Euros por tipo multa')
ax.legend()
fig.suptitle(fuente,size=10,x=1,y=-0.01)

for rect in rects:
    width = rect.get_width()
    plt.text(width,rect.get_y()+rect.get_height()/2 ,'%1.1f%%' % (100*width/euros_totales), size=16, ha='left', va='center')

plt.show()
fig.savefig('euros_hecho_multa',bbox_inches = 'tight')

output_61_0

..y el 21% de los euros recaudados vienen de multas por circular en zonas restriguidas.

Y finalmente analizaremos las multas de velocidad.
Empezamos haciendo un filtro :

multas_filtrada_velocidad=multas[multas.velocidad_circulacion!=0]

Calculando el tamaño del nuevo pandas vemos que hay 44.659 multas de velocidad.

multas_de_velocidad=len(multas_filtrada_velocidad)
multas_de_velocidad
44659

Saquemos unos cálculos interesantes

multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].describe()

.dataframe thead tr:only-child th {
text-align: right;
}

.dataframe thead th {
text-align: left;
}

.dataframe tbody tr th {
vertical-align: top;
}

count mean std min 25% 50% 75% max
velocidad_limite
40 698.0 61.6 7.1 47.0 57.0 60.0 65.0 90.0
50 10,300.0 68.2 6.9 52.0 63.0 66.0 71.0 99.0
60 440.0 87.7 9.2 67.0 81.0 87.0 93.0 126.0
70 26,933.0 79.5 6.5 74.0 75.0 77.0 82.0 136.0
80 92.0 104.2 7.7 95.0 97.0 104.0 109.2 126.0
90 6,196.0 100.2 5.9 95.0 96.0 98.0 102.0 152.0

Aquí podemos ver algunas características de las multas de velocidad ordenadas por la velocidad límite. Con algunas curiosidades a primera vista : la mayor parte de las multas se consiguen en zonas con 70 km/h de velocidad límite..Se ha multado a 52km/h en zonas de 50km/hr, sin embargo no se ha multado hasta los 95km/h en zona de 80km/h..aunque cierto es que el número de multas a 80km/h es despreciable : 92 de las 44.659…

Veamoslo en gráficos : número de multas

multas_filtrada_velocidad['velocidad_limite'].sort_values().unique()
array([40, 50, 60, 70, 80, 90])
multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].count()
velocidad_limite
40      698
50    10300
60      440
70    26933
80       92
90     6196
Name: velocidad_circulacion, dtype: int64
fig = plt.figure(1, (12,7))
width=.8
ind = np.arange(len(multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].aggregate('count')))


ax = fig.add_subplot(111)

rects = ax.bar(ind, multas_filtrada_velocidad.groupby('velocidad_limite')['velocidad_circulacion'].aggregate('count'), 
                width, align='center',color='b', label='nº multas')

ax.set_xticks(ind)
ax.set_xticklabels([x for x in multas_filtrada_velocidad['velocidad_limite'].sort_values().unique()],
                   size=12,rotation=0,color='b')
ax.set_xlabel('Velocidad límite en km/h')
ax.set_ylabel('Número de multas',size=16)
ax.grid(axis='y')
ax.set_title('Número de multas de velocidad por velocidad límite')
ax.legend()
fig.suptitle(fuente,size=10,x=1,y=-0.01)

for rect in rects:
    height = rect.get_height()
    plt.text(rect.get_x()+rect.get_width()/2,height ,'%1.1f%%' % (100*height/multas_de_velocidad), size=10,  ha='center', va='bottom')


plt.show()
fig.savefig('multas_velocidad_limite',bbox_inches = 'tight')

output_73_0

..y un par de gráficas adicionales con la distribución de velocidad que llevaban los multados frente ordenados por la velocidad límite :

fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax.plot(
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==50]['velocidad_circulacion'].sort_values().unique(),
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==50].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
        label='50 km/h',
        )

ax.plot(
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==70]['velocidad_circulacion'].sort_values().unique(),
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==70].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
        label='70 km/h',
        )
ax.plot(
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==90]['velocidad_circulacion'].sort_values().unique(),
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==90].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
        label='90 km/h',
        )


ax.locator_params(axis='x',nbins=20)
ax.set_xlabel('Velocidad en la infracción km/h')
ax.set_xlim([50,160])
ax.set_ylim([0,5000])
ax.set_ylabel('Número de multas',size=16)
ax.grid(axis='y')
ax.set_title('Distribución de velocidad de infracción por velocidad límite')
ax.legend()

fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('distribucion_velocidad_507090',bbox_inches = 'tight')

output_75_0

fig = plt.figure(1, (7,4))
ax = fig.add_subplot(1,1,1)
ax.plot(
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==40]['velocidad_circulacion'].sort_values().unique(),
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==40].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
        label='40 km/h',
        )

ax.plot(
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==60]['velocidad_circulacion'].sort_values().unique(),
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==60].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
        label='60 km/h',
        )
ax.plot(
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==80]['velocidad_circulacion'].sort_values().unique(),
        multas_filtrada_velocidad[multas_filtrada_velocidad['velocidad_limite']==80].groupby('velocidad_circulacion')['velocidad_circulacion'].count(),
        label='80 km/h',
        )


ax.locator_params(axis='x',nbins=20)
ax.set_xlabel('Velocidad en la infracción km/h')
ax.set_xlim([40,130])
ax.set_ylim([0,70])
ax.set_ylabel('Número de multas',size=16)
ax.grid(axis='y')
ax.set_title('Distribución de velocidad de infracción por velocidad límite')
ax.legend()

fig.suptitle(fuente,size=10,x=1,y=-0.01)
fig.savefig('dsitribibucion_velocidad_406080',bbox_inches = 'tight')

output_76_0

Con el afan de ver las multas «extremas», no en absoluto si no con el ratio velocidad_circulacion/velocidad/limite, introducimos una nueva columna…

multas_filtrada_velocidad['ratio']=multas_filtrada_velocidad['velocidad_circulacion']/multas_filtrada_velocidad['velocidad_limite']
/Users/waly/anaconda/envs/OpenData/lib/python3.6/site-packages/ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  """Entry point for launching an IPython kernel.

y vemos los casos extremos (head y tail)..
En la parte alta : multa a 90 km/hr en el Paseo Ermita del Santo con velocida máxima 40km/h, a las 09:08…500€ del ala!
..y en la parte baja : multa a 52km/h en la Av Santo Domingo de la Calzada en zona de 50km/h a las 11:57..100€ por esos 2 km/h

multas_filtrada_velocidad.sort_values('ratio',ascending=False).head(2)
CALIFICACION LUGAR MES ANIO HORA IMP_BOL DESCUENTO PUNTOS DENUNCIANTE HECHO_BOL VEL_LIMITE VEL_CIRCULA COORDENADA_X COORDENADA_Y hora_entera velocidad_limite velocidad_circulacion ratio
78630 GRAVE F074 PO ERMITA DEL SANTO 6 2017 1900-01-01 09:08:00 500.0 SI 6 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 40 90 09:00 40 90 2.2
246435 GRAVE F059 AV TRECE ROSAS 6 2017 1900-01-01 10:25:00 500.0 SI 6 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 40 89 10:00 40 89 2.2
multas_filtrada_velocidad.sort_values('ratio',ascending=False).tail(2)
CALIFICACION LUGAR MES ANIO HORA IMP_BOL DESCUENTO PUNTOS DENUNCIANTE HECHO_BOL VEL_LIMITE VEL_CIRCULA COORDENADA_X COORDENADA_Y hora_entera velocidad_limite velocidad_circulacion ratio
210191 GRAVE M-30 CALZADA 2 KM 27.000 6 2017 1900-01-01 14:21:00 100.0 SI 0 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 90 95 14:00 90 95 1.1
205273 GRAVE F028 AV SANTO DOMINGO CAL 6 2017 1900-01-01 11:57:00 100.0 SI 0 POLICIA MUNICIPAL SOBREPASAR LA VELOCIDADMÁXIMA EN VÍAS LIMITADA… 50 52 11:00 50 52 1.0

Algunos gráficos de la EPA

Vemos aquí algunos gráficos de la última EPA publicada, Julio 2017   , comparados con series desde el año 2002, recuerdo aquí que la EPA tiene carácter trimestral, luego nos encontramos por tanto con 4 series por año..

No pretendo jugar a «dos Papas» con las escalas, tan solo las optimizo con el objetivo de que queden los movimientos lo más claro posible.

He preparado una serie de gáficos con los principales números desde 2002, incluyendo la mencionada última publicación de EPA.

Empezamos con la evolución de la población inactiva desde 2002:

Inactivos2
A continuacién vemos la evolución de población activa desde 2002 :
Activos-3
El turno de Ocupados, desde 2002 al segundo trimestre de 2017.
Ocupados-2
Para finalmente presentar los datos de población en situación de desempleo :
Parados
El pico en el número de parados se encuentra en el primer trimestre de 13, con 6.278.000 millones de parados  y el trimestre con menos paro  lo encontramos en el segundo trimestre de 2006 con 1.766.000 parados, estos casi siete años incrementaron el número de parados en 4.500.000.
Preparando estos gráficos me preguntaba que porcentajes suponían estas cantidades en el total de la población. Pongamonos a ello..
Estos datos están bajados directamente del censo del INE,
Poblacion
Vemos el estancamiento de la población en el entorno de los 46.500.000 de habitantes
Extactamente 46.528.970 en los últimos datos publicados relativos al primer semestre de 2017.
Recordad que en esta entrada veíamos la distribución de la población tal que : población activa, inactiva y menores de 16 años. Saquemos pues esta última gráfica para poder hacer una comparación completa de los movimientos de población en estos años.
Aquí vemos la gráfica :
Poblacion < 16
Por su interés podemos preparar una gráfica con todas las series que hemos ido preparando :
Ocupados, Activos etc..
Y para finalizar añadimos un par de gráficas de columnas en horizontal con estos mismos números, que bajo mi punto de vista añaden mucha claridad a los números. He includo dos versiones, una de ellas con los valores en absoluto y la otra con porcentajes relacionados con el total de la población.
Incluyo un comentario importante aqui relacionado con la construccion de estas dos gráficas : la suma de inactivos, activos más menores de 16 años no coincide exactamente con el valor de población, sigo investigando para ver donde he podido cometer el error, pero en terminos generales la gráfica nos sigue dando una visión general de la composición de la población en España desde el punto de vista de empleo.
Poblacion por distribucion-3
poblacion distribucion empleo en porcentaje-2
Eso es todo desde esta entrada…
Manuel

Glosario de la EPA : conceptos y definiciones

He elegido hacer un análisis de la última EPA, del INE, como primer ejercicio real.

Creo que es bueno hacer un repaso de los principales conceptos, si ya los conoces salta a al siguiente post.

Hago un corta y pega de las definiciones desde página Glosario de Conceptos de la EPA:

  1. Empleo :Conjunto de tareas y cometidos desempeñados por una persona, o que se prevé que esta pueda desempeñar en su puesto de trabajo.
  2. Ocupados :Los ocupados son todas aquellas personas de 16 años o más que, según los criterios OIT, durante la semana de referencia tuvieron un empleo por cuenta ajena, asalariado, o ejercieron una actividad por cuenta propia, trabajadores por cuenta propia.
    En relación a este empleo o actividad, pudieron haberse encontrado en la semana de referencia:
    – trabajando al menos una hora en el citado período a cambio de una remuneración, salario, beneficio empresarial o ganancia familiar, etc., o
    – ausentes del mismo pero con un fuerte vínculo con dicho empleo.
  3. Parados : Parados son todas aquellas personas de 16 años o más que, según los criterios OIT, durante la semana de referencia, se encontraran en las tres situaciones siguientes simultáneamente:

    a) sin trabajo, es decir, no tenían un empleo asalariado o por cuenta propia,

    b) disponibles para trabajar, es decir, disponibles para un empleo como asalariado o un empleo por cuenta propia dentro de las dos semanas posteriores a la semana de referencia,

    c) buscando activamente un trabajo durante el mes precedente al domingo de la semana de referencia. Este último requisito no se precisa en el caso de haber encontrado un empleo al que se incorporará dentro de los tres meses posteriores a la semana de referencia.

  4. Población activa o activos : La población activa comprende las personas ocupadas y paradas durante la semana de referencia, según los criterios OIT.
  5. Población inactiva o inactivos : Abarca a todas las personas de 16 o más años, no clasificadas como ocupadas ni paradas ni población contada aparte durante la semana de referencia, según los criterios OIT.
  6. Tasa de actividad : La tasa de actividad se define como el cociente entre el total de activos y la población de 16 y más años.
  7. Tasa de empleo :Es el cociente entre el total de ocupados y la población de 16 y más años.
  8. Tasa de paro :Es el cociente entre el número de parados y el de activos. Se calcula para ambos sexos y para cada uno de ellos por separado.
  9. Tasa global de actividad : Es el cociente entre el número total de activos y la población total. Se calcula para ambos sexos y para cada uno de ellos por separado.
  10. Tasa global de empleo : Es el cociente entre el número total de ocupados y la población total. Se calcula para ambos sexos y para cada uno de ellos por separado.

 

En este documento tenéis la metodología usada al realizar la consulta.

Un punto importante relativo a la Población Contada Aparte : tal cual entendemos las definiciones la poblacion total es igual a :

  • la suma de Inactivos,
  • + Activos (que a su vez es suma de Parados + Empleados),
  • + Población menor de 16 años,
  • a ese número, y según la página 20, apartado 10.8, habría que añadir la Población Contada Aparte : que textualmente es :

10.6 Población contada aparte

Los varones que cumplían el servicio militar obligatorio (o servicio social susti- tutorio) se consideraban población contada aparte, esto es, no se les incluían entre los activos ni entre los inactivos, independientemente de que en la semana de referencia hubieran trabajado o no. El servicio militar obligatorio desapareció en diciembre de 2001.

 

Como quiera que ya no aplica ese concepto entiendo que finalmente queda :

Población Total = Inactivos + Activos + Población menor 16 años…

…pues no!!!

…en la siguiente entrada veremos que esa ecuación no se cumple y se me pierden alrededor de 400.000 españoles!. Sigo intentando ver donde está el error bien de cáculo (que lo dudo) bien de concepto.