import pandas as pd

"""
Explanation: Introducción a la programación estadística <br/> con Python y Pandas (...)

Herramientas que usaremos


Python


Pandas
    Libreria que provee simples pero poderosas estructuras y herramientas para el analisis de datos

Jupyter Notebook
    Ambiente web para la creacion de documentos interactivos con elementos de programacion y datos (Python es solo uno de los multiples lenguajes que soporta)
NumPy
    Libreria base que provee estructuras de datos optimizadas (fast! fast! fast!) para el analisis (arreglo multidimensional: Ndarray)
matplotlib
    Libreria base de visualización
Seaborn
    Libreria para visualizacion estadistica, construida sobre matplotlib para ser mucho mas facil de usar y amigable
todo lo anterior se consigue con solo instalar Anaconda (la distribución de Python más popular)
dos o tres cositas mas...

Cosas que no veremos

la historia de Python
ni las razones para usarlo
tampoco veremos el millon de cosas que se pueden hacer con Python y su ecosistema
detalles tecnicos como instalacion/configuracion, 
conceptos de programacion/implementación "avanzados" (les pasaremos por arribita si es necesario)

Que si veremos?
Pues cómo:

importar datos de un archivo (csv)
ver la estructura básica de los datos
realizar un minímo de limpieza y orden:
Transformar campos
Agregar nuevos campos


acceder al contenido de:
columnas especificas
registros especificos


calcular estadisticas descriptivas
Rango, media, desviación estandar
Cuantiles
resumen de 7 números


explorar subdivisiones de los datos
group by
split-apply-combine


generar algunos graficos
Histograma
Diagrama de caja



...y uno que otro extra, si da tiempo...
Pero con cuáles datos?
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....
Y que al parecer ya no está disponible en el portal datos.gob.do ni en la página de la DGII
Importemos la libreria pandas con el alias pd (por convencion)
End of explanation
"""


# DataFrame cariñosamente df
df = pd.read_csv('cs_DGII_Nomina_2016.csv',sep=';', encoding="ISO-8859-1") # encoding???

"""
Explanation: Cargamos el archivo a un DataFrame en memoria
El archivo es separado por ';'.
Asi como read_csv, existen funciones para leer:
* excel
* SAS
* STATA
* archivos de longitud fija
* JSON, entre otros...
End of explanation
"""


df.head()

"""
Explanation: Demos un vistazo a los primeros 5 registros
End of explanation
"""


df.tail(10)

"""
Explanation: y a los ultimos 10
End of explanation
"""


df.shape

"""
Explanation: Que tamaño tiene nuestro DataFrame?
End of explanation
"""


df.info()

"""
Explanation: En español?
5,416 registros y 5 columnas
Nuestro DataFrame parece una tabla o una hoja de cálculo...
Cada registro tiene un indice (hasta ahora numerico y autoasignado), cada columna tiene nombre y un tipo de datos...
...parecen no necesitar mucha explicación. Aún así veamos que nos dice Pandas:
End of explanation
"""


df.isnull().any()

"""
Explanation: Limpieza de data
Valores faltantes o nulos
El conteo de cada columna y el tipo nos sugiere que no hay valores faltantes. Pero, vamos a confirmarlo:
End of explanation
"""


df.columns

"""
Explanation: Normalizar o limpiar los nombres de las columnas
El DataFrame tiene una propiedad columns que es (como) una lista de los nombres de las columnas.
End of explanation
"""


df.Mes.head()

"""
Explanation: 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:
End of explanation
"""


df.columns = [x.strip().lower() for x in df.columns.values]
df.columns

"""
Explanation: Arreglemos eso de una forma que se asegure que todos los nombres de columnas esten "limpios". Para eso:

iteramos por cada columna e invocamos funciones propias del tipo de dato string para limpiarlos (gracias Python)
usamos strip para quitar espacios delante y detras
y finalmente lower para convertir todo a minusculas
End of explanation
"""


df.salario.head()

"""
Explanation: Ahora todas las columnas tienen nombres sencillos, faciles de utilizar.
Salarios limpios
End of explanation
"""


limpiar_salario = lambda x: pd.to_numeric(x.replace(',','').strip(), errors='coerce')
df.salario = df.salario.apply(limpiar_salario)

"""
Explanation: 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:
* quitar las comas y los espacios en blanco (replace y strip)
* convertir a número (to_numeric)
End of explanation
"""


df.salario.head()

"""
Explanation: Veamos como quedo:
End of explanation
"""


df.salario.describe()

"""
Explanation: Resumen de 7 números
Habiendo realizado la conversión podemos ver estadísticas descriptivas básicas (por fin!) de los salarios.
End of explanation
"""


df.puesto.describe()

"""
Explanation: Limpie su puesto
End of explanation
"""


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

# tambien podemos acceder a las columnas con esta notación df['columna']
df['puesto_clean'] = df.puesto.apply(limpiar_puesto)
df.puesto_clean.describe()

"""
Explanation: 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:
* definimos una funcion limpiar_puesto donde
    * usamos unidecode para que nos ayude a quitar los acentos
    * usamos re para con una expresión regular quitar los fragmentos entre parentesis de los puestos
    * usamos translate para quitar puntos y comas no deseados
* le aplicamos esta funcion a los datos de la columna puesto para crear una nueva columna puesto_clean
End of explanation
"""


df.genero.value_counts()

df.genero.describe()

"""
Explanation: Oh sorpresa! Ahora son 269 puestos en lugar de 271.
Se deja de ejercicio al interesado averiguar cuales valores fueron eliminados
Todos los generos son limpios
End of explanation
"""


df.mes.value_counts()

df.mes.describe()

df.to_csv('dgii_clean.csv')

"""
Explanation: Y los meses tambien
End of explanation
"""


ene = df[df.mes == 'ene-16']
feb = df[df.mes == 'feb-16']
print(ene.shape)
print(feb.shape)

"""
Explanation: Empezar a explorar un poco
Filtrar y seleccionar data
Separemos la nomina de cada mes disponible.
End of explanation
"""


ene[ene.salario > 250000][['puesto_clean','salario','genero']]

"""
Explanation: High-rollers
Veamos las posiciones de grandes salarios. Filtramos con una condicion y seleccionamos las columnas que nos interesan:
End of explanation
"""


ene[['puesto_clean','salario','genero']][ene.salario > 250000]

"""
Explanation: Si se seleccionamos las columnas y despues filtramos, tambien funciona?
End of explanation
"""


(ene.salario > 250000).head()

"""
Explanation: Por que?
End of explanation
"""


por_mes = df.groupby(by='mes')
por_mes

"""
Explanation: Resulta que la condicion devuelve una serie de booleanos indicando cuales registros cumplen con la condicion.
Agrupar data (Group By)
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í.
End of explanation
"""


for mes, data_mes in por_mes:
    print(mes, data_mes.shape)

"""
Explanation: groupby nos da un colección que relaciona el nombre de cada grupo con la parte de la data que pertenece a dicho grupo.
End of explanation
"""


por_mes.salario.describe()

"""
Explanation: Pero tambien nos permite hacerle preguntas a los distintos grupos a la vez. En efecto llevando a cabo los pasos:

split : separar los datos por el mes al que pertenecen
apply : a cada grupo formado aplicarle la operación describe
combine : tomar los resultados y combinarlos en una nueva tabla resultado

Todo esto, en un simple línea de código:
End of explanation
"""


df.groupby(by=['mes','genero']).salario.describe()

"""
Explanation: Podemos agrupar por cualquier numero de columnas que querramos, formando una jerarquia de profundidad arbitraria.
End of explanation
"""


feb_por_gen = feb.groupby(by='genero')
total_empleados = feb.salario.count()
feb_por_gen.salario.count().apply(lambda x: x/total_empleados)

"""
Explanation: Proporción de la empleomanía por genero
Podemos realizar varios pasos tipo apply en hilo, inclusive con operaciones arbitrarias (funciones anonimas o lambdas)
End of explanation
"""


total_nomina = feb.salario.sum()
feb_por_gen.salario.sum().apply(lambda x: x/total_nomina)

"""
Explanation: Proporción de la nomina por genero
End of explanation
"""


# 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')

"""
Explanation: 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...
A graficar un poco
Resulta que con Pandas, podemos continuar el hilo de apply y convertir la tabla en un grafico de barra muy facilmente. En este caso, la proporción de la nómina asignada a hombres y mujeres.
End of explanation
"""


import seaborn as sns
sns.countplot(x='genero',data=feb)

"""
Explanation: Mejoremos el look un poco con Seaborn

La libreria de visualización Seaborn es:
* muy popular
* facil de usar
* mas inteligente
* se ve mas bonita por defecto que matplotlib (tan solo importarla cambia el estilo y colores de los graficos generados)
Diagrama de frecuencia de empleados mujeres y hombres
End of explanation
"""


feb_por_gen.salario.sum().apply(lambda x: x/total_nomina).plot(kind='bar')

"""
Explanation: Proporción de la nómina asignada a mujeres y hombres (reloaded)
Aquí vemos como el estilo del gráfico cambio solo por importar Seaborn. A mi me parece más bonito.
End of explanation
"""


sns.distplot(feb.salario, kde=False)

"""
Explanation: Histogramas del Salario
End of explanation
"""


sns.FacetGrid(data=feb, col='genero', size=8).map(sns.distplot,'salario', kde=False)

"""
Explanation: Histograma por genero (lado a lado)
End of explanation
"""


sns.factorplot(data=feb, x='genero',y='salario',hue='genero', kind='box',size=6)

"""
Explanation: Diagrama de cajas (box & whiskers)
End of explanation
"""


import numpy as np
sns.distplot(np.random.normal(feb.salario.mean(),feb.salario.std(),feb.shape[0]))

"""
Explanation: Una tangente (a)normal
Cómo se vería una distribución normal (gaussiana) si tuviera la media y desviación que vemos empíricamente en los datos?
Seaborn y Numpy al rescate!
End of explanation
"""


feb_por_gen.salario.mean()

salario_promedio_f, salario_promedio_m = feb_por_gen.salario.mean()[['F','M']]
salario_promedio_f - salario_promedio_m

"""
Explanation: Empecemos a enfocar nuestra exploración
Diference entre salario promedio por genero
End of explanation
"""


salario_promedio_f/salario_promedio_m

(salario_promedio_f/salario_promedio_m - 1)*100

"""
Explanation: Parece ser que el salario promedio de las mujeres supero en 2601.05 DOP al de los hombres...
Relacion salario mujeres y hombres
End of explanation
"""


bottom25 = feb[feb.salario <= feb.salario.quantile(.25)]
bottom25_by_gender = bottom25.groupby(by='genero')
bottom25_by_gender.salario.describe()

bottom25_by_gender.salario.mean()['F'] - bottom25_by_gender.salario.mean()['M']

(bottom25_by_gender.salario.mean()['F']/bottom25_by_gender.salario.mean()['M'] - 1) * 100

"""
Explanation: 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.

Que pasa cuando miramos los grupos que menos ganan?
End of explanation
"""


sns.factorplot(data=bottom25, x='genero',y='salario',hue='genero', kind='box',size=8)

"""
Explanation: En el primer cuartil de salarios la mujer promedio gano 4.30% (783.97 DOP) menos que el hombre promedio.
End of explanation
"""


top25 = feb[feb.salario >= feb.salario.quantile(.75)]
top25_by_gender = top25.groupby(by='genero')
top25_by_gender.salario.describe()

top25_by_gender.salario.mean()['F'] - top25_by_gender.salario.mean()['M']

(top25_by_gender.salario.mean()['F']/top25_by_gender.salario.mean()['M'] - 1) * 100

"""
Explanation: Que pasa cuando miramos los grupos que mas ganan?
End of explanation
"""


sns.factorplot(data=top25, x='genero',y='salario',hue='genero', kind='box',size=6)

"""
Explanation: En el ultimo cuartil la mujer promedio gano 9% (7294.20 DOP) menos que el hombre promedio. Pero, notan algo extraño?
End of explanation
"""


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()

trunc_top25_by_gender.salario.mean()['F'] - trunc_top25_by_gender.salario.mean()['M']

(trunc_top25_by_gender.salario.mean()['F']/trunc_top25_by_gender.salario.mean()['M'] - 1) * 100

"""
Explanation: Cual es el impacto de la persona que mas gana en la diferencia?
Outliers o valores extremos
End of explanation
"""


sns.factorplot(data=trunc_top25, x='genero',y='salario',hue='genero', kind='box',size=6)

"""
Explanation: 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.
End of explanation
"""


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

"""
Explanation: Profundicemos de cuartiles a deciles
Veamos el comportamiento del salario promedio por genero para cada decil. Para eso necesitaremos:

importar a Numpy, solo para usar una funcion de promedio
crear una columna nueva que asigne cada record a un decil
Pivot Table! (igualito que en Excel)
End of explanation
"""


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')

"""
Explanation: Graficamente es mas bonito
Seaborn es esteticamente más agradable y suele ahorrar mucho trabajo, los gráficos de Pandas se consiguen de la manera más facil; pero a veces es necesario modificar detalles que requieren usar matplotlib directamente.
End of explanation
"""


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])

"""
Explanation: Soy yo a las líneas están superpuestas en todos los deciles excepto el primero y el último?
Mismo trabajo = mismo sueldo?
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)
End of explanation
"""


spg.puesto_clean.nunique()

spg.puesto_clean.value_counts()

"""
Explanation: Cuales puestos quedaron?
Y cuantos empleados hay en cada uno?
End of explanation
"""


spg.groupby(by='genero').salario.describe()

sns.factorplot(data=spg, x='genero',y='salario',hue='genero', kind='box',size=6)

"""
Explanation: Cómo se ve el salario en general por género de esta subpoblación?
End of explanation
"""


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)

spg_mean_pivot.plot(kind='barh',figsize=(10,15))

"""
Explanation: Y el salario para cada puesto por género?
Veamos los puestos ordenado de menor a mayor por el salario promedio de las mujeres que los ocupan.
End of explanation
"""


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

"""
Explanation: Calculemos la diferencia entre los promedios de cada puesto
Restandole al salario promedio de las mujeres el promedio de los hombres.
End of explanation
"""


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')

spg_rel_pivot

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]))

"""
Explanation: En terminos relativos (%)?
End of explanation
"""


