Análisis y visualización de datos usando Python: Notas de Instructor

Soluciones a desafíos

Instalar los paquetes requeridos para el taller

Por favor sigue las instrucciones del documento Setup para instalar los programas necesarios para esta lección. Si encuentras problemas, por favor crea un issue con la etiqueta High-priority.

Revisando instalaciones

En el directorio _includes/scripts encontrarás un script llamado check_env.py. Éste revisa la funcionalidad de la versión instalada de Anaconda.

Por defecto, Data Carpentry no requiere que la gente descargue el repositorio completo con todos los scripts y apéndices. Por lo tanto, como instructor, debes decidir cómo te gustaría proveer este script a las estudiantes, si es que decides hacerlo. Para usarlo, las estudiantes pueden navegar en su terminal hasta _includes/scripts y ejecutar lo siguiente:

python check_env.py

Si las estudiantes reciben un AssertionError, éste te informará cómo ayudar a corregir la instalación. De lo contrario, ¡te dirá que el sistema está listo para Data Carpentry!

01-short-introduction-to-Python

Desafíos sobre tuplas

Desafíos sobre diccionarios

Asegúrate también de aclarar que acceder al “segundo valor” se trata del nombre de la clave. Agrega por ejemplo rev[10] = "ten" para aclarar que no se trata de la posición.

rev
{1: 'one', 2: 'two', 3: 'three'}
rev[2] = "apple-sauce"
{1: 'one', 2: 'apple-sauce', 3: 'three'}

02-starting-with-data

Nota sobre bugs

Pandas < .18.1 tiene un error bug por el cual surveys_df['weight'].describe() puede devolver un error en tiempo de ejecución.

Desafíos sobre DataFrames

Desafíos sobre calcular estadísticas de los datos

Ambos resultan en el mismo output, sirviendo como formas alternativas de obtener los valores únicos. nunique combina el conteo con la extracción de valores únicos.

Desafíos sobre agrupamientos

surveys_df.groupby(['plot_id','sex']).agg({"year": 'min',
                                           "hindfoot_length": 'median',
                                           "weight": 'mean'})`
surveys_df.groupby(['plot_id'])['weight'].describe()

Desafíos sobre gráficos

surveys_df.groupby('plot_id').mean()["weight"].plot(kind='bar')

promedio de peso de las especies por sitio

surveys_df.groupby('sex').count()["record_id"].plot(kind='bar')

total de machos contra el total de hembras para todo el dataset

03-index-slice-subset

Sugerencia: usa el método .head() a lo largo de esta lección con el fin de mantener tu pantalla limpia. Anima a las estudiantes a probar comandos con y sin .head() para reforzar la utilidad de esta herramienta y luego emplearla, o no, según su preferencia. Por ejemplo, si una estudiante muestra preocupación por mantener el ritmo de tipeo, infórmale que puede evitar .head(), pero que tú lo usarás para mantener visibles más líneas anteriores de código.

Desafíos sobre indexación

Desafíos sobre rangos

Desafíos avanzados sobre queries

surveys_df[~surveys_df["sex"].isin(['M', 'F'])]

Desafíos sobre máscaras

new = surveys_df[~surveys_df['sex'].isin(['M', 'F'])].copy()
new['sex']='x'
print(len(new))

Puedes verificar el número de valores Nan con sum(surveys_df['sex'].isnull()), que resulta igual al número de registros que no son female ni male.

# selecciona los datos con isin:
stack_selection = surveys_df[(surveys_df['sex'].isin(['M', 'F'])) &
							surveys_df["weight"] > 0.][["sex", "weight", "plot_id"]]
# calcula el promedio de weight para cada combinación de plot_id y sex:
stack_selection = stack_selection.groupby(["plot_id", "sex"]).mean().unstack()
# podemos representarlo como un gráfico de barras apiladas:
stack_selection.plot(kind='bar', stacked=True)

Sugerencia: Como sabemos que todos los otros valores son Nan, podemos seleccionar también todos los valores que no sean null (esto es sólo un vistazo previo, habrá más al respecto en la siguiente lección):

stack_selection = surveys_df[(surveys_df['sex'].notnull()) &
					surveys_df["weight"] > 0.][["sex", "weight", "plot_id"]]

promedio de weight para cada plot por sex

Sin embargo, debido al comando unstack, el encabezado de la leyenda contiene dos niveles. Para removerlo, los nombres de columnas deben ser simplificados:

stack_selection.columns = stack_selection.columns.droplevel()

promedio de `weight` para cada `plot_id` por `sex`

04-data-types-and-format

Desafíos sobre cambiar tipos

Pandas no puede convertir datos de tipo float a int si la columna contiene valores NaN.

Desafíos sobre cuentas

surveys_df.isnull()

Si las estudiantes tienen problemas para generar el output, u ocurre algo con éste, existe un archivo llamado “sample output” con los datos que ellas deben generar.

05-merging-data

# lee los archivos:
survey2001 = pd.read_csv("data/survey2001.csv")
survey2002 = pd.read_csv("data/survey2002.csv")
# concatena los archivos:
survey_all = pd.concat([survey2001, survey2002], axis=0)
# recupera el peso de cada año, agrupado por sexo:
weight_year = survey_all.groupby(['year', 'sex']).mean()["wgt"].unstack()
# genera el gráfico:
weight_year.plot(kind="bar")
plt.tight_layout()  # tip(!)

peso promedio por año, agrupado por sexo

# escribe a un archivo:
weight_year.to_csv("weight_for_year.csv")
# lee los datos de nuevo:
pd.read_csv("weight_for_year.csv", index_col=0)
merged_left = pd.merge(left=surveys_df,right=species_df, how='left', on="species_id")

Luego calcula y crea un gráfico de la distribución de:

1. taxa por parcela (número de especies de cada taxa por parcela):

La distribución de especies (número de taxa por cada parcela) puede calcularse de la siguiente forma:

merged_left.groupby(["plot_id"])["taxa"].nunique().plot(kind='bar')

taxa por parcela

Sugerencia: También es posible graficar el número de invididuos de cada taxa en cada parcela (gráfico de barras apiladas):

merged_left.groupby(["plot_id", "taxa"]).count()["record_id"].unstack().plot(kind='bar', stacked=True)
plt.legend(loc='upper center', ncol=3, bbox_to_anchor=(0.5, 1.05))

(de otro modo, la leyenda se superpone con el gráfico de barras)

taxa por parcela

2. taxa por sexo por parcela: Otorguemos los valores ‘M|F’ a los valores Nan (también podrían cambiarse a ‘x’):

merged_left.loc[merged_left["sex"].isnull(), "sex"] = 'M|F'

Número de taxa por cada combinación de parcela/sexo:

ntaxa_sex_site= merged_left.groupby(["plot_id", "sex"])["taxa"].nunique().reset_index(level=1)
ntaxa_sex_site = ntaxa_sex_site.pivot_table(values="taxa", columns="sex", index=ntaxa_sex_site.index)
ntaxa_sex_site.plot(kind="bar", legend=False)
plt.legend(loc='upper center', ncol=3, bbox_to_anchor=(0.5, 1.08),
           fontsize='small', frameon=False)

taxa por parcela por sexo

Sugerencia (sólo para discutir)):

También puede calcularse el número de individuos de cada taxa en cada parcela y por sexo:

sex_taxa_site  = merged_left.groupby(["plot_id", "taxa", "sex"]).count()['record_id']
sex_taxa_site.unstack(level=[1, 2]).plot(kind='bar', logy=True)
plt.legend(loc='upper center', ncol=3, bbox_to_anchor=(0.5, 1.15),
           fontsize='small', frameon=False)

taxa por parcela por sexo

En verdad, éste gráfico no es el mejor que podría elegirse, pues no es legible… Una primera alternativa para mejorarlo es utilizar facets. Sin embargo, pandas/matplotlib no los provee por defecto. Un ejemplo en matplotlib puro (usando M|F para registros sin sexo definido):

fig, axs = plt.subplots(3, 1)
for sex, ax in zip(["M", "F", "M|F"], axs):
    sex_taxa_site[sex_taxa_site["sex"] == sex].plot(kind='bar', ax=ax, legend=False)
    ax.set_ylabel(sex)
    if not ax.is_last_row():
        ax.set_xticks([])
        ax.set_xlabel("")
axs[0].legend(loc='upper center', ncol=5, bbox_to_anchor=(0.5, 1.3),
              fontsize='small', frameon=False)

taxa por parcela por sexo

Sin embargo, sería mejor indicar Seaborn y Altair por sus tipos de visualizaciones multivariadas.

plot_info = pd.read_csv("data/plots.csv")
plot_info.groupby("plot_type").count()
merged_site_type = pd.merge(merged_left, plot_info, on='plot_id')
# para cada parcela, recupera el número de especies por parcela
nspecies_site = merged_site_type.groupby(["plot_id"])["species"].nunique().rename("nspecies")
# para cada parcela, recupera el número de individuos
nindividuals_site = merged_site_type.groupby(["plot_id"]).count()['record_id'].rename("nindiv")
# combina las dos series
diversity_index = pd.concat([nspecies_site, nindividuals_site], axis=1)
# calcula el índice de diversidad
diversity_index['diversity'] = diversity_index['nspecies']/diversity_index['nindiv']

Generando un gráfico de barras:

diversity_index['diversity'].plot(kind="barh")
plt.xlabel("Diversity index")

taxa por parcela por sexo

06-loops-and-functions

Desafíos sobre bucles básicos

for creature in animals:
    print(creature+',', end='')

Este bucle también agrega una coma luego del último animal. Una solución mejor, sin bucles, sería: ','.join(animals)

Desafíos sobre modificación de bucles

surveys_year = surveys_df[surveys_df.year == year].dropna()

Aunque podrías hacer la lista manualmente, ¿por qué no recuperas el primer y el último año a partir del código?

n_year = 5  # mejor visión general si defines una variable
first_year = surveys_df['year'].min()
last_year = surveys_df['year'].max()

for year in range(first_year, last_year, n_year):
    print(year)

    # Selecciona los datos para los años correctos
    surveys_year = surveys_df[surveys_df.year == year].dropna()

De forma similar al ejemplo anterior, pero usando la columna species_id: surveys_df['species_id'].unique(). Sin embargo, usar los nombres de especies mejoraría la interpretación de los nombres de archivos. Un join con las especies: merged_left = pd.merge(left=surveys,right=species, how='left', on="species_id") y usando la columna species.

Desafíos sobre funciones

Para mayor claridad, ¡demuéstralo en un entorno de debugging!

Desafíos adicionales sobre funciones

def one_year_csv_writer(this_year, all_data, folder_to_save, root_name):
    """
    Escribe un archivo csv para los datos de un año dado.

    Parámetros
    ---------
    this_year : int
        año para el cual se extraen datos
    all_data: pd.DataFrame
        DataFrame con datos de múltiples años
    folder_to_save : str
        carpeta para guardar los archivos de datos
    root_name: str
        raíz de los nombres de archivo donde se guardan los datos
    """

    # Selecciona datos para el año
    surveys_year = all_data[all_data.year == this_year]

    # Escribe el nuevo DataFrame a un archivo csv
    filename = os.path.join(folder_to_save, ''.join([root_name, str(this_year), '.csv']))
    surveys_year.to_csv(filename)

También adapta la función yearly_data_csv_writer con los inputs adicionales.

Adapta los argumentos de entrada, por ejemplo: 1978, 1979.

Desafíos sobre manejo de output

Implementación dentro de la función:

filenames = []
for year in range(start_year, end_year+1):
    filenames.append(one_year_csv_writer(year, all_data, folder_to_save, root_name))
return filenames
NoneType
yearly_data_arg_test(surveys_df, end_year=2001)

Desafíos sobre modificación de funciones

def one_year_csv_writer(this_year, all_data, folder_to_save='./', root_name='survey'):
    """
    Escribe un archivo csv para los datos de un año dado.

    Parámetros
    ---------
    this_year : int
        año para el cual se extraen datos
    all_data: pd.DataFrame
        DataFrame con datos de múltiples años
    folder_to_save : str
        carpeta para guardar los archivos de datos
    root_name: str
        raíz de los nombres de archivo donde se guardan los datos
    """

    # Selecciona datos para el año
    surveys_year = all_data[all_data.year == this_year]

    # Escribe el nuevo DataFrame a un archivo csv
    filename = os.path.join(folder_to_save, ''.join([root_name, str(this_year), '.csv']))
    surveys_year.to_csv(filename)
    # Escribe el nuevo DataFrame a un archivo csv
    if len(surveys_year) > 0:
        filename = os.path.join(folder_to_save, ''.join([root_name, str(this_year), '.csv']))
        surveys_year.to_csv(filename)
    else:
        print("No data for year " + str(this_year))
def yearly_data_csv_writer(all_data, yearcolumn="year",
                           folder_to_save='./', root_name='survey'):
    """
    Escribe archivos csv separados para los datos de cada año.

    all_data --- DataFrame con datos de múltiples años
    yearcolumn --- nombre de columna con el año de los datos
    folder_to_save --- carpeta para guardar los archivos de datos
    root_name --- inicio de los nombres de archivo almacenados
    """
    years = all_data["year"].unique()

    # "end_year" es el último año de los datos que queremos extraer, así que hacemos un bucle hasta end_year+1
    filenames = []
    for year in years:
        filenames.append(one_year_csv_writer(year, all_data, folder_to_save, root_name))
    return filenames

07-visualization-ggplot-python

Si las estudiantes tienen problemas para generar el output, u ocurre algo con éste, existe un archivo llamado “sample output” con los datos que ellas debían generar en la lección 03.

Las notebooks de iPython para graficar pueden verse en la carpeta _extras

08-putting-it-all-together

Las científicas suelen operar sobre ecuaciones matemáticas. La capacidad para usar estas ecuaciones en sus gráficas otorga mucho valor agregado. Por fortuna, Matplotlib provee herramientas poderosas para el control de texto. Una de ellas es la capacidad para usar la notación matemática de LaTeX al usar texto (puedes aprender más sobre la notación matemática de LaTeX aquí: https://en.wikibooks.org/wiki/LaTeX/Mathematics). Para usar notación matemática, enciera tu texto con el signo $. LaTeX usa mucho el caracter de barra invertida o backlash (“\”). Como este caracter tiene un significado especial en las secuencias de caracteres de Python, deberían reemplazarse todas las barras invertidas relacionadas con LaTeX por dos barras invertidas.

plt.plot(t, t, 'r--', label='$y=x$')
plt.plot(t, t**2 , 'bs-', label='$y=x^2$')
plt.plot(t, (t - 5)**2 + 5 * t - 0.5, 'g^:', label='$y=(x - 5)^2 + 5  x - \\frac{1}{2}$') # nota la barra invertida doble

plt.legend(loc='upper left', shadow=True, fontsize='x-large')

# Nota las dobles barras invertidas en la línea de abajo
plt.xlabel('Éste es el eje x. También puede contener matemática, como $\\bar{x}=\\frac{\\sum_{i=1}^{n} {x}} {N}$')
plt.ylabel('Éste es el eje y')
plt.title('Éste es el título de la figura')

plt.show()

09-working-with-sql

FIXME

Ésta página contiene más información.