Pandas Libreria que provee simples pero poderosas estructuras y herramientas para el analisis de datos
Pues cómo:
...y uno que otro extra, si da tiempo...
Un archivo de la Nómina Mensual de la DGII en Febrero y Enero 2016 que yo tome del repositorio de datos abiertos del gobierno de Republica Dominicana (http://datos.gob.do/dataset/nomina-de-empleados-dgii-2016) hace ya más de un año....
In [1]:
import pandas as pd
El archivo es separado por ';'.
Asi como read_csv, existen funciones para leer:
In [2]:
# DataFrame cariñosamente df
df = pd.read_csv('cs_DGII_Nomina_2016.csv',sep=';', encoding="ISO-8859-1") # encoding???
In [3]:
df.head()
Out[3]:
In [4]:
df.tail(10)
Out[4]:
In [5]:
df.shape
Out[5]:
In [6]:
df.info()
In [7]:
df.isnull().any()
Out[7]:
In [8]:
df.columns
Out[8]:
La columna Salario parece tener un espacio de mas. Esto nos importa porque por comodidad queremos acceder a los valores de cada columna haciendo algo como:
In [9]:
df.Mes.head()
Out[9]:
Arreglemos eso de una forma que se asegure que todos los nombres de columnas esten "limpios". Para eso:
In [10]:
df.columns = [x.strip().lower() for x in df.columns.values]
df.columns
Out[10]:
Ahora todas las columnas tienen nombres sencillos, faciles de utilizar.
In [11]:
df.salario.head()
Out[11]:
La columna salario tiene un tipo de dato object. Esto normalmente se refiere a texto libre. Para nuestro analisis necesitamos interpretar los valores como números.
Con la función apply le aplicamos una operación a cada valor de la columna. En este caso usamos un lambda (o funcion anonima) para:
In [12]:
limpiar_salario = lambda x: pd.to_numeric(x.replace(',','').strip(), errors='coerce')
df.salario = df.salario.apply(limpiar_salario)
Veamos como quedo:
In [13]:
df.salario.head()
Out[13]:
In [14]:
df.salario.describe()
Out[14]:
In [15]:
df.puesto.describe()
Out[15]:
271 puestos diferentes? Vamos a asegurarnos que no se abulte esta cantidad debido a que un mismo puesto este escrito con espacios de más como paso con la columna Salario (o sin acentos).
Para esto necesitaremos uno que otro truco cuya explicación escapa al alcance de la presentación (pero que puede ser replicada con un poco de stackoverflow)
Brevemente:
In [16]:
from unidecode import unidecode
import re
def limpiar_puesto(pstr):
pstr = unidecode(pstr).strip().lower()
pstr = re.sub('\(.*?\)','', pstr)
pstr = pstr.translate({ord('.'): None, ord(','): None})
return pstr
In [17]:
# tambien podemos acceder a las columnas con esta notación df['columna']
df['puesto_clean'] = df.puesto.apply(limpiar_puesto)
df.puesto_clean.describe()
Out[17]:
Oh sorpresa! Ahora son 269 puestos en lugar de 271.
Se deja de ejercicio al interesado averiguar cuales valores fueron eliminados
In [18]:
df.genero.value_counts()
Out[18]:
In [19]:
df.genero.describe()
Out[19]:
In [20]:
df.mes.value_counts()
Out[20]:
In [21]:
df.mes.describe()
Out[21]:
In [22]:
df.to_csv('dgii_clean.csv')
In [23]:
ene = df[df.mes == 'ene-16']
feb = df[df.mes == 'feb-16']
print(ene.shape)
print(feb.shape)
In [24]:
ene[ene.salario > 250000][['puesto_clean','salario','genero']]
Out[24]:
Si se seleccionamos las columnas y despues filtramos, tambien funciona?
In [25]:
ene[['puesto_clean','salario','genero']][ene.salario > 250000]
Out[25]:
Por que?
In [26]:
(ene.salario > 250000).head()
Out[26]:
Resulta que la condicion devuelve una serie de booleanos indicando cuales registros cumplen con la condicion.
Agrupar o separar los datos según los valores de una columna es el primer paso, conocido como split, de patrón split-apply-combine que veremos de manera natural aquí.
In [27]:
por_mes = df.groupby(by='mes')
por_mes
Out[27]:
groupby nos da un colección que relaciona el nombre de cada grupo con la parte de la data que pertenece a dicho grupo.
In [28]:
for mes, data_mes in por_mes:
print(mes, data_mes.shape)
Pero tambien nos permite hacerle preguntas a los distintos grupos a la vez. En efecto llevando a cabo los pasos:
Todo esto, en un simple línea de código:
In [29]:
por_mes.salario.describe()
Out[29]:
Podemos agrupar por cualquier numero de columnas que querramos, formando una jerarquia de profundidad arbitraria.
In [30]:
df.groupby(by=['mes','genero']).salario.describe()
Out[30]:
In [31]:
feb_por_gen = feb.groupby(by='genero')
total_empleados = feb.salario.count()
feb_por_gen.salario.count().apply(lambda x: x/total_empleados)
Out[31]:
In [32]:
total_nomina = feb.salario.sum()
feb_por_gen.salario.sum().apply(lambda x: x/total_nomina)
Out[32]:
Se fijaron bien?
Las mujeres son el 58.97% de la empleomanía y el 60.41% de la nómina. Mientras que los hombres son el 41.02% de la empleomanía y el 39.59% de la nómina.
Hmmm...
In [33]:
# esto es para que genere los graficos aqui dentro
%matplotlib inline
feb_por_gen.salario.sum().apply(lambda x: x/total_nomina).plot(kind='bar')
Out[33]:
La libreria de visualización Seaborn es:
In [34]:
import seaborn as sns
sns.countplot(x='genero',data=feb)
Out[34]:
In [35]:
feb_por_gen.salario.sum().apply(lambda x: x/total_nomina).plot(kind='bar')
Out[35]:
In [36]:
sns.distplot(feb.salario, kde=False)
Out[36]:
In [37]:
sns.FacetGrid(data=feb, col='genero', size=8).map(sns.distplot,'salario', kde=False)
Out[37]:
In [38]:
sns.factorplot(data=feb, x='genero',y='salario',hue='genero', kind='box',size=6)
Out[38]:
In [39]:
import numpy as np
sns.distplot(np.random.normal(feb.salario.mean(),feb.salario.std(),feb.shape[0]))
Out[39]:
In [40]:
feb_por_gen.salario.mean()
Out[40]:
In [41]:
salario_promedio_f, salario_promedio_m = feb_por_gen.salario.mean()[['F','M']]
salario_promedio_f - salario_promedio_m
Out[41]:
Parece ser que el salario promedio de las mujeres supero en 2601.05 DOP al de los hombres...
In [42]:
salario_promedio_f/salario_promedio_m
Out[42]:
In [43]:
(salario_promedio_f/salario_promedio_m - 1)*100
Out[43]:
Sin controlar por ningun otro factor: en febrero de 2016 en la DGII, las mujeres en promedio ganaron aprox. 2601.05 DOP más que los hombres. El equivalente a un 6.16% más aprox.
Esto va en la misma linea que lo sugerido por las estadisticas publicas mencionadas aqui por Deloitte.
In [44]:
bottom25 = feb[feb.salario <= feb.salario.quantile(.25)]
bottom25_by_gender = bottom25.groupby(by='genero')
bottom25_by_gender.salario.describe()
Out[44]:
In [45]:
bottom25_by_gender.salario.mean()['F'] - bottom25_by_gender.salario.mean()['M']
Out[45]:
In [46]:
(bottom25_by_gender.salario.mean()['F']/bottom25_by_gender.salario.mean()['M'] - 1) * 100
Out[46]:
En el primer cuartil de salarios la mujer promedio gano 4.30% (783.97 DOP) menos que el hombre promedio.
In [47]:
sns.factorplot(data=bottom25, x='genero',y='salario',hue='genero', kind='box',size=8)
Out[47]:
In [48]:
top25 = feb[feb.salario >= feb.salario.quantile(.75)]
top25_by_gender = top25.groupby(by='genero')
top25_by_gender.salario.describe()
Out[48]:
In [49]:
top25_by_gender.salario.mean()['F'] - top25_by_gender.salario.mean()['M']
Out[49]:
In [50]:
(top25_by_gender.salario.mean()['F']/top25_by_gender.salario.mean()['M'] - 1) * 100
Out[50]:
En el ultimo cuartil la mujer promedio gano 9% (7294.20 DOP) menos que el hombre promedio. Pero, notan algo extraño?
In [51]:
sns.factorplot(data=top25, x='genero',y='salario',hue='genero', kind='box',size=6)
Out[51]:
In [52]:
trunc_top25 = feb[(feb.salario >= feb.salario.quantile(.75)) & (feb.salario < feb.salario.max())]
trunc_top25_by_gender = trunc_top25.groupby(by='genero')
trunc_top25_by_gender.salario.describe()
Out[52]:
In [53]:
trunc_top25_by_gender.salario.mean()['F'] - trunc_top25_by_gender.salario.mean()['M']
Out[53]:
In [54]:
(trunc_top25_by_gender.salario.mean()['F']/trunc_top25_by_gender.salario.mean()['M'] - 1) * 100
Out[54]:
Ahora la diferencia se redujo a 7.10% (5,632.26 DOP) menos en salario promedio para las mujeres que los hombres.
En este caso, a manera ilustrativa, consideramos que el salario más alto dista demasiado de los demás y su influencia distorsiona lo que queremos evaluar.
In [55]:
sns.factorplot(data=trunc_top25, x='genero',y='salario',hue='genero', kind='box',size=6)
Out[55]:
In [56]:
pd.set_option('mode.chained_assignment',None) # incantacion arcana que podemos ignorar por ahora
feb['salario_bin'] = pd.qcut(feb.salario,10,precision=0)
# guardemos esto para graficarlo mas abajo
epg_mean = pd.pivot_table(data=feb,index=['salario_bin'],columns=['genero'],values='salario',aggfunc=np.mean)
epg_mean
Out[56]:
In [57]:
from matplotlib import pyplot as plt
# Pandas nos genera el grafico inicial
ax = epg_mean.plot(grid=True,figsize=(10,8))
# Pero con matplotlib modificamos los nombres, las unidades y marcas de los ejes
ax.set_xlabel('Deciles de Salario')
ax.set_ylabel('Salario Promedio')
ticks = ax.set_xticklabels(ax.get_xticklabels(),rotation=45)
# Finalmente agregemos el promedio general como linea punteada para referencia
plt.axhline(feb.salario.mean(),linestyle='dashed')
Out[57]:
Soy yo a las líneas están superpuestas en todos los deciles excepto el primero y el último?
Hasta ahora hemos estado mirando la nómina total, por género y por decil pero sin importar la posición.
Realmente es justo comparar los salarios entre diferentes puestos?
Miremos solamente los puestos que tienen 20+ empleados y por lo menos un hombre y una mujer (otro split-apply-combine)
In [58]:
spg = feb.groupby(by=['puesto_clean']).filter(\
lambda x: \
len(x) > 19 and \
len(x[x.genero == 'F']) > 0 and \
len(x[x.genero == 'M']) > 0)
print(spg.shape[0])
In [59]:
spg.puesto_clean.nunique()
Out[59]:
In [60]:
spg.puesto_clean.value_counts()
Out[60]:
In [61]:
spg.groupby(by='genero').salario.describe()
Out[61]:
In [62]:
sns.factorplot(data=spg, x='genero',y='salario',hue='genero', kind='box',size=6)
Out[62]:
In [63]:
spg_mean_pivot = pd.pivot_table(data=spg,values='salario',index=['puesto_clean'],columns=['genero'],aggfunc=np.mean)
spg_mean_pivot.sort_values('F',inplace=True)
spg_mean_pivot.head(10)
Out[63]:
In [64]:
spg_mean_pivot.plot(kind='barh',figsize=(10,15))
Out[64]:
In [69]:
spg_diff_pivot = spg_mean_pivot.apply(lambda x: x - x['M'], axis = 1)
spg_diff_pivot.sort_values('F',inplace=True)
spg_diff_pivot['F'].plot(kind='barh',figsize=(10,15))
spg_diff_pivot
Out[69]:
In [66]:
spg_rel_pivot = spg_mean_pivot.apply(lambda x: (x/x['M'] - 1) * 100, axis=1)
spg_rel_pivot['mas'] = spg_diff_pivot.F.map(lambda x: x > 0)
spg_rel_pivot.sort_values('F',inplace=True)
f = plt.figure(figsize=(12,12))
ax = sns.barplot(data=spg_rel_pivot,y=spg_rel_pivot.index,x='F',hue='mas',orient='h')
ax.set_xlabel('% diferencia promedio')
Out[66]:
In [68]:
spg_rel_pivot
Out[68]:
In [67]:
print("En {:d} de {:d} puestos el salario promedio de las mujeres es mayor que el de los hombres".format(spg_rel_pivot.mas.sum(), spg_rel_pivot.shape[0]))
Comparar grupos y distribuciones es complicado. Aquí no exploramos puntos como:
El resumen en letras se basa en la mediana de sucesivas división de los datos ordenados y resulta bastante útil para resaltar los sesgos (skew) que pueden tener los datos en una dirección u otra.
Todo muy chulo y bonito, pero podemos inferir algo?
Bueno, si y no.
No podemos inferir nada concreto en términos generales sobre la "brecha salarial de género" ya que los datos que tenemos son solo de dos meses en una institución. Ni con todos los trucos, artimañas y librerías del mundo podemos escapar el simple hecho de que no tenemos datos sobre la población en general.
Si podemos hablar bastante sobre los salarios y la "brecha" en esta institución en particular hasta el momento donde tenemos datos (Ene-Feb 2016), ya lo hemos hecho.