Logo Studenta

Clase 05 - Guión de Clase(1)

¡Estudia con miles de materiales!

Vista previa del material en texto

Clase 05 - Introducción a la manipulación de datos con Pandas (Parte I) 
Data Wrangling, o convertir caos en información
Data Wrangling
Un famoso dicho surgido en los inicios de la computación moderna es “Garbage In, Garbage Out”, también conocido por sus iniciales GIGO. Esto significa que si los datos de entrada no están correctamente preparados, cualquier procesamiento posterior dará resultados incorrectos. Los datos son habitualmente caóticos, con lo que si los introducimos en nuestros algoritmos y modelos sin más, obtendremos todo tipo de errores y resultados inválidos. Por esto, la limpieza de datos o Data Wrangling es un aspecto fundamental en todo proyecto de Data Science. 
Cuando hablamos de Data Wrangling en Python, estamos hablando de Pandas. El Data Wrangling, también conocido como Data Munging o preprocesamiento y manipulación de datos, es un conjunto de operaciones en el proceso de Data Science que tienen que ver con tomar los datos “crudos” asociados a los problemas del mundo real, organizarlos, “limpiarlos” y convertirlos en un input valioso y útil para los algoritmos que se van a usar a continuación en dicho proceso.
Es importante recordar que cuando hacemos práctica de Data Science, es común que trabajemos con datos ordenados, pero en el mundo real esto no siempre es así. Los datos pueden estar mal organizados, u organizados para otro propósito; pueden estar incompletos; pueden existir valores anómalos que pueden a su vez ser de interés o no. Todas estas situaciones deben ser resueltas con Data Wrangling antes de aplicar cualquier análisis o algoritmo. De lo contrario, podríamos obtener resultados erróneos, o directamente sería imposible obtenerlos. 
Pandas al rescate
El proceso de Data Wrangling será cubierto en profundidad más adelante. En esta clase, introducimos la parte práctica de este proceso a través del uso de Pandas.
Pandas es una librería de uso libre que está construida sobre NumPy. Esto quiere decir que toma las estructuras básicas de NumPy y las extiende y mejora añadiendo métodos muy útiles, haciendo de esta forma más fácil la manipulación de datos que son más complejos, de forma totalmente compatible con NumPy. 
En particular, Pandas define dos tipos de datos, denominados Series y DataFrame, que proveen una funcionalidad muy eficiente y práctica para manipular datos complejos y de gran volumen. Veamos estas estructuras de datos en profundidad a continuación.
Las estructuras de datos en Pandas
Objeto Series
Un objeto Series es, básicamente, un array con índices. Los índices juegan un papel fundamental en la manipulación de datos, como veremos más adelante. Si bien los arrays ya tienen índices, teniendo en cuenta que cualquiera de sus elementos puede ser accedido referenciándolo con el índice entre corchetes, de la forma objeto[índice], los objetos Series explotan al máximo esta funcionalidad agregándole vitaminas.
Comencemos definiendo un objeto Series y veamos cómo funciona.
	Numeros = range(50,70,2) # Números pares entre 50 y 70
Numeros_serie = pd.Series(Numeros) # Creamos un objeto Series
print(Numeros_serie) # El objeto Series
print(Numeros_serie.values) # Valores del objeto
print(Numeros_serie.index) # Índices del objeto
print(Numeros_serie[2]) # Acceso de forma tradicional, como en NumPy
Entonces, ¿no es esto un objeto NumPy?. Casi. La diferencia está en los índices. Al definirse de manera explícita incorporan más flexibilidad que termina siendo muy útil a la hora de llevar a cabo operaciones muy comunes con los datos.
Para empezar, los índices son de un solo tipo y no pueden cambiarse, pero sí pueden definirse como queramos. Supongamos que queremos utilizar texto en vez de números:
	Numeros_en_texto = ['primero','segundo','tercero','cuarto','quinto','sexto','séptimo','octavo','noveno','décimo'] # Array de textos personalizados
Numeros_serie_2 = pd.Series(Numeros,index=Numeros_en_texto) # Los mismos datos pero con los índices de la línea anterior
print(Numeros_serie_2)
Recordemos que tanto los valores como los índices son de un sólo tipo de dato, cada uno por su lado, que queda fijo en la creación del objeto. Esto mantiene la eficiencia de los objetos en Python: al ser las estructuras de un solo tipo, las operaciones para cada elemento son las mismas, y en consecuencia el tiempo de procesamiento es menor.
Objeto DataFrame
Un DataFrame es una extensión del objeto Series para trabajar en dos dimensiones. Puede pensarse como una sucesión de objetos Series del mismo tamaño, y que comparten los mismos índices. Un esquema de un DataFrame sería el siguiente:
Veamos un ejemplo:
	modelos = ['A4 3.0 Quattro 4dr manual',
 'A4 3.0 Quattro 4dr auto',
 'A6 3.0 4dr',
 'A6 3.0 Quattro 4dr',
 'A4 3.0 convertible 2dr']
peso = [3583, 3627, 3561, 3880, 3814]
precios = ['$33,430', '$34,480', '$36,640', '$39,640', '$42,490']
Autos_peso = pd.Series(peso,index=modelos) # Construimos un objeto Series con los modelos y sus pesos
print(Autos_peso)
Autos_precio = pd.Series(precios,index=modelos) # Construimos un objeto Series con los modelos y sus precios
print(Autos_precio)
print(Autos_peso.index, Autos_precio.index) # Los índices son los mismos
Autos = pd.DataFrame({'Peso':Autos_peso,'Precio':Autos_precio}) # Formamos un DataFrame uniendo los dos objetos Series como columnas
Autos
Veamos el ejemplo del Ajedrez con un DataFrame:
	Ajedrez_64 = np.arange(1,65) # Una array de números de 1 a 64 
print(Ajedrez_64)
Ajedrez_64 = np.arange(1,65).reshape(8,8) # Organizarlo en 8 x 8
Ajedrez_df = pd.DataFrame(Ajedrez_64,columns=range(1,9),index=['A','B','C','D','E','F','G','H']) # Tablero con letras como filas y números como columnas
Ajedrez_df
Selección de elementos
Una vez creados estos objetos, veamos la mejor forma de trabajar con ellos. Una primera toma de contacto es el acceso a los elementos. Veamos cómo hacer esto con los ejemplos anteriores. A continuación veremos ejemplos con el tipo Series
	print(Numeros_serie_2)
print(Numeros_serie_2['quinto']) # Accedo al elemento referenciando su índice
print(Numeros_serie_2.loc['quinto']) # Estilo pandas, accediendo al quinto elemento. Equivalente al anterior, por claridad recomendamos usar este cuando trabajamos con Pandas
print(Numeros_serie_2.iloc[5]) # Estilo de acceso tradicional, selecciona el sexto elemento. Si los índices son numéricos, iloc y loc ayudan a entender el tipo de referencia 
print('quinto' in Numeros_serie_2) # ¿Hay un objeto con el índice 'quinto'?
Numeros_serie_2['primero'] = 11 # Modificando un elemento referenciándolo por su índice
print(Numeros_serie_2)
print(Numeros_serie_2[Numeros_serie_2 >= 60]) # Seleccionando elementos con criterio
Con DataFrame, extendemos estas formas de acceso al considerar files (índices) y columnas
	print(Autos.index) # Índices: se usan para filas
print(Autos.columns) # Columnas del DataFrame
print(Autos.values) # Todos los datos
print(Autos['Peso']) # Selecciono la columna "Peso"
print(Autos.values[1]) # Selecciono la segunda fila
print(Autos.loc['A4 3.0 Quattro 4dr auto',]) # Equivalente al anterior, pero menos "crudo"
print(Autos.loc[Autos.Peso >= 3600,'Precio']) # Selección más compleja, con criterios
print(Autos.T) # Datos transpuestos
Operaciones con datos. Datos ausentes
Transposición
La transposición consiste en intercambiar las filas de un Data Frame por sus columnas. Esta operación no realiza ninguna modificación sobre los datos, sino que es meramente un cambio visual. La transposición es útil ya que en ocasiones puede resultar más cómodo trabajar con el Data Frame transpuesto en lugar del original.
	print(Autos.T)
Funciones vectorizadas
Las funciones vectorizadas o ufuncs que vimos anteriormente también pueden aplicarse a los objetos de Pandas, ya que, recordemos, Pandas está construido sobre NumPy. En el siguiente bloque de código se calcula un nuevo Data Frame, el cual es resultado de realizar un cálculo sobre una determinada fila
	largo = [179, 179, 192, 192, 180]
Autos_2 = pd.DataFrame({'Peso':peso,'Largo':largo},index=modelos) # Crear dataframe a partirde peso y largo de autos
print(Autos_2)
print(Autos_2 / Autos_2.iloc[0] * 100) # Cuánto mide y cuánto pesa cada auto en porcentaje con respecto al primero
Tengamos en cuenta que los resultados de las operaciones conservan los índices y las columnas, y los índices quedan alineados entre los objetos que participan de la operación. Veremos esto con ejemplos
	Numeros_3 = range(51,70,2) # Números impares entre 50 y 70
Numeros_serie_3 = pd.Series(Numeros_3,index=Numeros_en_texto) # Creo un objeto Series con el mismo índice que el anterior, que contenía los números pares
print(Numeros_serie_3)
print(Numeros_serie_2 + Numeros_serie_3) # Sumo ambos objetos, elemento a elemento. Los índices se conservan
print(Numeros_serie_2 * 1.5) # Otro ejemplo de conservación de índices
	Numeros_serie_2_porcion = Numeros_serie_2[4:7] # Una porcihón de la serie de números pares
Numeros_serie_3_porcion = Numeros_serie_3[5:8] # Una porción de la serie de números impares
print(Numeros_serie_2_porcion, Numeros_serie_3_porcion)
print(Numeros_serie_2_porcion + Numeros_serie_3_porcion) # Suma de las dos series
Observemos lo que sucede en la última línea. ¿Cómo son los dos objetos que se suman?
	Numeros_serie_2_porcion
quinto 58
sexto 60
séptimo 62
	Numeros_serie_3_porcion
sexto 61
séptimo 63
octavo 65
Notemos que coinciden en dos índices, “sexto” y “séptimo”. Estos índices se unen en la suma final. Los índices que no coinciden (“quinto”, presente sólo en el primer objeto, y “octavo”, presente sólo en el segundo) son rellenados con elementos vacíos, representados por el símbolo NaN (Not a Number)
	Numeros_serie_2_porcion + Numeros_serie_3_porcion
octavo NaN
quinto NaN
sexto 121.0
séptimo 125.0
Asimismo, los índices se ordenan automáticamente. En este caso, como es texto, se ordenan alfabéticamente. Si fueran números, se ordenan de menor a mayor.
Veamos el siguiente código:
	print(Numeros_serie_2_porcion + Numeros_serie_3_porcion)
print(Numeros_serie_2_porcion.add(Numeros_serie_3_porcion)) # Operación equivalente a la anterior
print(65 + np.nan) # np.nan es un valor NaN. Cualquier valor sumado a NaN da como resultado NaN
print(Numeros_serie_2_porcion.add(Numeros_serie_3_porcion,fill_value=0)) # Rellena NaN con ceros. 
Podemos esquematizar la operación anterior de la siguiente forma:
	Suma común
	
	
	
	Numeros_serie_2_porcion
	Numeros_serie_3_porcion
	Resultado
	quinto
	58
	quinto
	NaN
	NaN
	sexto
	60
	sexto
	61
	121
	séptimo
	62
	séptimo
	63
	125
	octavo
	NaN
	octavo
	65
	NaN
	
	
	
	
	
	Suma con relleno "0"
	
	
	
	Numeros_serie_2_porcion
	Numeros_serie_3_porcion
	Resultado
	quinto
	58
	quinto
	0
	58
	sexto
	60
	sexto
	61
	121
	séptimo
	62
	séptimo
	63
	125
	octavo
	0
	octavo
	65
	65
De esta forma, comenzamos a ver cómo Pandas maneja los valores ausentes. Veremos estas operaciones en detalle más adelante.
Datos ausentes
Tal como vimos anteriormente, no sólo de forma teórica sino muchas veces en el mundo real, es muy posible que se presenten situaciones donde existan datos ausentes. Causas posibles para esto pueden ser fallas en algún paso de la carga de datos, omisión directa de la carga de datos, o bien reticencia de parte de un encuestado a dar una respuesta determinada. Los datos ausentes son mucho más comunes de lo que podría pensarse, y son un denominador común en muchos problemas de Data Science. Por lo tanto, es necesario conocer cómo trabajar con ellos.
Como veíamos en la sección anterior, NumPy define un dato especial denominado NaN (Not a Number) que en sí es un dato de tipo punto flotante (en memoria se ve como un número con coma). No obstante, NaN tiene propiedades especiales. En primer lugar, cualquier elemento operado con NaN da como resultado NaN. Esto puede tener consecuencias si no se maneja convenientemente, ya que no todos los algoritmos de Data Science aceptan este tipo de datos. Por lo tanto, puede ser necesario considerar reemplazarlos o manipularlos de algún modo. Una manera básica de hacer esto es operar ignorando los NaN, con los operadores nan de NumPy:
	valor_nan = np.nan # Defino un valor que contiene un NaN
print(type(valor_nan)) # Un valor NaN es de tipo punto flotante (float)
print(2 * valor_nan) # Cualquier valor operado con NaN da como resultado NaN
np.nanprod([2,valor_nan]) # NumPy maneja esta situación ignorando el NaN con el operador nanprod
Ahora bien, veamos cómo detectar y filtrar estos valores:
	Numeros_nan = Numeros_serie_2_porcion + Numeros_serie_3_porcion
print(Numeros_nan) # Objeto Series con dos valores NaN
print(Numeros_nan.isnull()) # Marca con True las posiciones que son NaN y con False las que no lo son
print(Numeros_nan.dropna()) # Devuelve sólo las filas sin NaN
print(Numeros_nan.fillna(0)) # Rellena (cambia) los valores NaN con ceros

Más contenidos de este tema