<jupyter_start><jupyter_text>Series y Dataframes (PANDAS)En este notebook contiene las herramientas básicas que utilizaremos durante el curso para el manejo de datos de forma tabular. La biblioteca *Pandas* contiene dos estructuras para el manejo de datos llamadas *Series* y *DataFrames* las serán el caballo de batalla durante el curso. El uso de estas herramientas se elige por su fácil manejo en comparación con herramientas comunes para el manejo de datos de alto desempeño (como son bases de datos) y a la vez nos permiten un desempeño similar a las herramientas especializadas.  Para hacer uso de las herramietas dentro de la bibliotyeca *Pandas* ejecutamos el comando `import pandas` por simpleza se utilisa un un alias `pd`para poder usar dicho alias se declara de la siguiente forma<jupyter_code>import pandas as pd
import numpy as np<jupyter_output><empty_output><jupyter_text>SeriesLas series son similares a un vector o una matriz unidimensional la cual contiene una secuencia de valores u objetos junto con un arreglo de etiquetas que se les llama *índices*. Una *Serie* en pandas se declara de la siguiente forma:<jupyter_code>Serie_1 = pd.Series([10,9,8,7,6,5,4,3,2,1])
Serie_1<jupyter_output><empty_output><jupyter_text>Como se observa en el ejemplo la primera columna son los índices de la *Serie*, mientras que la segunda son los valores. Por defecto los índices son enteros que van de $0$ a $N-1$, donde $N$ es la cantidad de datos que se tienen. Para acceder a los datos de forma de arreglos de tipo *numpy* usamos el atributo `values`de la *Series* mientras que para los índices usamos `index`<jupyter_code>Serie_1.values
Serie_1.index<jupyter_output><empty_output><jupyter_text>Como se ve *pandas* utiliza una función similar a *range* para generar los índices, si de desea utilizar un conjunto especifico de elementos  para usar como índice esto se puede hacer al momento de crear la *serie*.<jupyter_code>Serie_2= pd.Series([10,9,8,7,6,5,4,3,2,1,], index =['e','h', 'i','a', 'b','d', 'c', 'g', 'f', 'j'])
Serie_2
Serie_2.index<jupyter_output><empty_output><jupyter_text>Para seleccionar algún valor dentro de la serie o un conjunto de valores utilizamos los `[]`<jupyter_code>Serie_1[0]
Serie_1[[0, 1,4]]
Serie_2['a']
Serie_2[['a', 'c', 'b']]<jupyter_output><empty_output><jupyter_text>Como se observa, al pasar una lista de índices es distinto que solicitar un sólo elemento, la diferencia radica que al pasar una lista de índices el objeto que nos regresa es una *serie* que sólo contiene a los elementos solicitados.Para modificar un elemento o un conjunto de elementos también utilizamos `[]`<jupyter_code>Serie_2['a'] = 25
Serie_2
Serie_1[[0,1,2,3]]= -1
Serie_1<jupyter_output><empty_output><jupyter_text>Si en lugar de usar índices usamos un vector booleano del mismo tamaño filtramos los elementos de la *serie*<jupyter_code>Serie_1[[True,True, True, False, True, True,False,False,False,False ]]<jupyter_output><empty_output><jupyter_text>Esto nos permite hacer filtros usando los condicionales booleanos de python, pues al utilizar los condicionales en la serie el resultado regresa una serie con verdarero (`True`) o falso (`False`) que podemos utilizar como un vector booleano para filtrar<jupyter_code>Serie_1>0
Serie_1[Serie_1 >0]<jupyter_output><empty_output><jupyter_text>Una forma de pensar las series que puede resultar conveniente, es como diccionarios grandes, donde las llaves son los índices. A diferencia de los diccionarios podemos aplicar multiplicaciones, funciones matemáticas o condicionales booleanos a los valores y los resultados preservan la relación índice valor.<jupyter_code>Serie_2*4
Serie_2^2
np.sqrt(Serie_2)<jupyter_output><empty_output><jupyter_text>Si el identificador de nuestros datos es lo que se utiliza como indice entonces podemos usar dicho identificador para determinar si un elemento se encuentra dentro de la *serie*<jupyter_code>'z' in Serie_2
'j' in Serie_2<jupyter_output><empty_output><jupyter_text>Si una *serie* se parece a un diccionario, debería ser natural construir una *serie* a partir de un diccionario. Con tal proposito usando un diccionario como argumento al constructor (función que crea el objeto), en vez de una lista, se construye una *serie* donde las llaves del diccionario son los índices de la *serie*.<jupyter_code>diccionario= {'Ana':8.5, 'karla':10, 'ruben': 8 , 'ismael': 9, 'ivan':10 }
Serie_3= pd.Series(diccionario)
Serie_3<jupyter_output><empty_output><jupyter_text>Si se desea un conjunto específico de índices en una *serie*, el conjunto se pasa como una lista como el parámetro `index`.<jupyter_code>Nombres = ['Ana', 'karla',  'lucrecia', 'irma', 'ruben' , 'ismael', 'ivan' ]
Serie_4 = pd.Series(diccionario, index = Nombres) 
Serie_4<jupyter_output><empty_output><jupyter_text>Al construir la *serie* ésta mantiene el orden de los índices, en el primer caso mantiene el orden de las llaves de `diccionario`mentras que en el segundo la de la lista `Nombres`.Si no existe un valor para algún índice, la acción por defecto en *pandas* es dar el valor `NaN` (*not a number*). No importa si la *serie* son números, strings u otro objeto de python `NaN` se utiliza para señalar que el valor no esta. Gracias a esta regla *pandas* tienen funciones para determinar que elementos son `NaN`y aquellos que no **isnull** y **notnull** son los funciones que se utilizan para tales motivos respectivamente.<jupyter_code>pd.isnull(Serie_4)
pd.notnull(Serie_4)<jupyter_output><empty_output><jupyter_text>Estan funciones también se encuentran implementadoas en la *series* como métodos<jupyter_code>Serie_4.isnull()
Serie_4.notnull()<jupyter_output><empty_output><jupyter_text>A partir de dos o más series es posible hacer operaciones respetando el vínculo índice valor,<jupyter_code>Serie_4

Serie_5 = pd.Series({'raul': 6, 'maria':10, 'emilio':8, 'alfredo': 9.5, 'ivan': 7.5, 'fabian':7.5, 'ismael': 7})
Serie_5
Serie_4 + Serie_5<jupyter_output><empty_output><jupyter_text>Al no poder hacer la operación en ciertos elementos el comportamiento por defecto en *pandas* es que determinar que no hay valor, lo cual hace  más relevante el uso de las funciónes `isnull` y `notnull`.A las *series* se les puede dar un nombre este se encuentra en la instancia `name` dentro de la *serie*, de forma similar y como buena practica se recomienda que los índices tengan nombre este se encuentra dentro de la instancia `index.name`.<jupyter_code>Serie_4.name= 'Calificaciones'
Serie_4.index.name='Nombre'
Serie_4<jupyter_output><empty_output><jupyter_text>Esto será de gran utilidad al combinar series para hacer manejos complicados en los datos.  De ser necesario es posible cambiar los índices, para tal motivo es necesario dar una lista con los nuevos índices que sea del mismo tamaño que la longitud de la *serie*,<jupyter_code>Serie_4.index= ['Ana', 'Karla', 'Lucrecia', 'Irma', 'Ruben', 'Ismael', '<NAME>']
Serie_4<jupyter_output><empty_output><jupyter_text>Una instancia dentro de las series que nos puede ser de utilidad es la instancia `shape` la cual nos regresa una tupla en donde la primera entrada es el tamaño de la *serie* y la segunda se encuentra vacia.<jupyter_code>Serie_4.shape
Serie_4.shape[0]<jupyter_output><empty_output><jupyter_text>Otra forma de acceder a los elementos dentro de una serie es utilizando<jupyter_code>Serie_4['Ruben']<jupyter_output><empty_output><jupyter_text>DataFramesLos *dataframes* de *pandas* se crearon usando como base los dataframes usados en *R*, el lenguage de programación [*R*](https://www.r-project.org/) fue creado con la intención de generar herramientas especializadas en estadística por *<NAME>* y *<NAME>* en la Universidad de Aucland, en caso de ser necesario se usará *R*. Los *Dataframes* son la herrmaienta utilizaremos con mayor frecuencia durante el curso. La selección de esta herramienta se debe a que permite un manejo de los datos en forma de tabla, aunque existen otras herramientas que nos permiten tener los datos en forma de tablas, como son las bases de datos, la facilidad de como se manejan de las columnas de los *dataframes* dentro de *pandas* convierte a los *Dataframes* en herramientas accesibles y poderosas. Una forma de pensar a los *Dataframes* es como un diccionario de diccionarios, o un diccionario de *Series*, estas dos formas serán convenientes dependiendo de las problemáticas a resolver y como se desee interpretar la información que se analiza.Para construir nuestro primer *dataframe* usemos la primera analogía.<jupyter_code>dic_dic={'Poblacion': {'Baja California':2844469, 'Ciudad de Mexico': 8720916, 'Hidalgo':2345514, 'Morelos':1612899,'Nayarit':949684 },
'Poblacion_60_a': {'Baja California':164888, 'Ciudad de Mexico':859438 , 'Hidalgo':204325, 'Morelos':143942,'Nayarit':9061},
'Poblacion_0_4': {'Baja California':267954, 'Ciudad de Mexico':664092, 'Hidalgo':237423, 'Morelos':150281, 'Nayarit':92384},
'Poblacion_5_9': {'Ciudad de Mexico': 671579, 'Hidalgo':243185, 'Morelos':157253, 'Nayarit':96834, 'Baja California':277635},
'Poblacion_10_14': {'Baja California':267938, 'Ciudad de Mexico':704950, 'Hidalgo':270650, 'Morelos':168541, 'Nayarit':103122}
}
 
df_1=pd.DataFrame(dic_dic)
df_1<jupyter_output><empty_output><jupyter_text>Como se observa el comportamiento por defecto de la función constructora usando un diccionario de diccionarios (diccionarios anidados) es tomar como columnas las llaves del diccionario exterior y como renglones las llaves de los diccionarios internos. Si quisieramos que fuese las columnas se convirtieran en los renglones existen varias formas de lograr este cometido la más simples es tomar la transpuesta del *dataframe*.   Recornando sus clases de matemáticas tomar la transpuesta es cambiar los renglones de una matriz y convertirlos en columnas respetando el orden dado, esta operación se puede hacer en los dataframes de forma muy fácil usando el método implementado en los *dataframes*<jupyter_code>df_1.T<jupyter_output><empty_output><jupyter_text>Muchas de las operaciones que se pueden hacer con matrices es posibles hacerlas con los *dataframes* y es de las cosas que se atenderan durante el curso, esta operación muestra la capacidad de los *dataframes* para ser operados por renglones o columnas. La mayoría de las operaciones que se están implementadas en *pandas* dejan al objeto en su forma original al menos que se indique lo contrario, es decir `df_1` sigue siendo el mismo y no se vio afectado por la operación `.T`, esto es importante tenerlo en cuenta pues es causa común de errores al hacer código.<jupyter_code>df_1<jupyter_output><empty_output><jupyter_text>Para entender un poco más de como se construyen los *datafremes* modifiquemos un poco el diccionario añadiendo elementos a los diccionarios internos para que ver el comportamiento con diccionarios de distinto tamaño.<jupyter_code>dic_dic['Poblacion']['Tamaulipas']=	3024238
df_2 =pd.DataFrame(dic_dic)
df_2<jupyter_output><empty_output><jupyter_text>La función constructura de los *dataframes* nos permite que los diccionarios tengan distintos tamaños pues existe un forma de determinar la relación índice-valor. Una diferencía con respecto a esto, es cuando se construye un *dataframe* a partir de un diccionario de listas, *pandas* asumirá que la relación índice-valor esta dada por el orden en las listas.<jupyter_code>dic_lis={'Poblacion': [2844469,  8720916, 2345514, 1612899,949684 ],
'Poblacion_60_a': [164888, 859438 , 204325, 143942,9061],
'Poblacion_0_4': [267954, 664092, 237423, 150281, 92384],
'Poblacion_5_9': [277635, 671579, 243185, 157253, 96834],
'Poblacion_10_14': [267938,704950, 270650, 168541,103122]}
pd.DataFrame(dic_lis)<jupyter_output><empty_output><jupyter_text>Al no tener etiquetas el índice en el *dataframe* se toma como un rango por defecto, igual que se hace con la construcción de las *series*. Sin embargo al utilizar listas es necesario que todas tengan el mismo tamaño y que las listas esten ordenadas, es decir que los datos en el indice *i* de una lista correspondan al dato *i* en todas las otras. Si se ponen atención en los ejemplos mostrados los datos en el diciionario anidado no se encuentran en orden, no es necesarios que los diccionarios internos tengan el mismo orden, pues se tiene las llaves de los diccionarios internos para asegurar que al generar el dataframe se mantenga la relación índice-valor. Ahora construimos un *dataframe* a partir de un diccionario de series.<jupyter_code>dic_ser= {'Poblacion': pd.Series({'Puebla':5383133,'Qu<NAME>oo':1135309, 'Sinaloa':2608442}),
          'Poblacion_0_4':pd.Series({'Puebla':579804,'Qu<NAME>oo':107380, 'Sinaloa':244907}),
          'Poblacion_5_9':pd.Series({'Puebla':586108,'Quint<NAME>oo':106802, 'Sinaloa':262539}),
          'Poblacion_10_14':pd.Series({'Puebla':624075,'Quintana Roo':106745, 'Sinaloa':275179}),
          'Poblacion_60_a':pd.Series({'Puebla':440546,'Qu<NAME>':44163, 'Sinaloa':220998})
         }

df_2=pd.DataFrame(dic_ser)
df_2<jupyter_output><empty_output><jupyter_text>Es posible definir el orden en el cual se encuentran las columnas de los dataframes y tambíen que columnas se encuentren dentro, esto lo podemos hacer desde la función constructor del dataframe<jupyter_code>df_3=pd.DataFrame(dic_ser, columns= ['Poblacion_0_4','Poblacion_5_9', 'Poblacion_60_a', 'Poblacion' ])
df_3<jupyter_output><empty_output><jupyter_text>Otra forma en la cual nos seria útil construir los *dataframes* es a partir de un diccionario de diccionarios<jupyter_code>dic_cic_2= { 'Poblacion':{'Puebla':5383133,'Qu<NAME>':1135309, 'Sinaloa':2608442},
'Poblacion_0_4':{'Puebla':579804,'Qu<NAME>':107380, 'Sinaloa':244907},
'Poblacion_5_9':{'Puebla':586108,'Quintana Roo':106802, 'Sinaloa':262539},
'Poblacion_10_14':{'Puebla':624075,'Qu<NAME>':106745, 'Sinaloa':275179},
'Poblacion_60_a':{'Puebla':440546,'Quintana Roo':44163, 'Sinaloa':220998}}
df_4= pd.DataFrame(dic_cic_2)
df_4<jupyter_output><empty_output><jupyter_text>Los valores en el diccionario se toman como columnas, con el nombre de las llaves en el diccionario. Recordemos que los índices y las columnas pueden ser cosas distintas no sólo *strings*, pueden ser tuplas, enteros, flotantes, etc.A continuación mostramos una tabla con los posibles estructuras de datos que pueden utilizarse para generar un *dataframe* en *pandas*. | Estructura datos | Descripción ||:-------|:-----------:|| Array 2D|  Una matriz de *numpy* .|| Diccionaro de *array*, lista, o tuplas| Cada valor en el diccionario se convierte en  una columna en el *dataframe* en donde cada *array*, lista o tupla tienen que ser de la misma longitud.| | Diccionario de *Series*| Cada valor del diccionario se convierte en una columna cuya etiqueta se toma de las llaves (`key`); los índices son la unión del conjunto de índices de  todas las *series*, si no se pasa un índice explicitamente.|| Dicccionario de diccionarios | Cada diccionario internos se convierte en una columna; los índices se forman a partir de la unión de las  llaves(`key`) de los diccionarios internos.|| Lista de diccionarios o *Series*| Los elementos de las listas o diccionarios se convierten en renglones en el *dataframe*, la unión de las llaves del diccionario o de los índices de las *Series*, se vuelven las etiquetas de los renglones.|| Listas de listas o tuplas | Se toma como el caso de *Array 2D*.| Otro *dataframe*| Se utilizan los íondices del dataframe si no se pasa otro distinto.|| Numpy Array enmascarada| Como *Array 2D* salvo aquellos que tengan  `False` en la matriz mascara, se tomaran como *NA*.|   Índices Como hemos visto los índices son una parte muy importante para el manejo de los datos tanto en las *series* como en los *dataframes*. Son tan importantes que los índices de éstas estructuras tienen su propio objeto dedicado en *pandas* el cual esta encargado de almacenar las etiquetas<jupyter_code>Serie_1=pd.Series(range(10), index=['b','e','f','g','h','i','k','n','z' ,'w'])
Serie_1
indice= Serie_1.index
indice<jupyter_output><empty_output><jupyter_text>Al construir la *serie* o un *dataframe* el conjunto que se define como índice, ya sea un array o una secuencia, se convierte de forma interna a un objeto de este tipo.<jupyter_code>indice[1:]
indice[:5]<jupyter_output><empty_output><jupyter_text>Los índices son del tipo inmutable, es decir que no pueden ser modificados por el usuario, si para ser modificado es necesario redefinirlos a través de funciones hechas para tal proposito. ReindexarUn método dentro de las *series* y los *dataframes* que es útil es el de reindexar el cual nos permite crear un objeto nuevo a partir de los datos de otro formado por un nuevo índice.<jupyter_code>Serie_1=pd.Series(range(20,30), index=[0,5,6,9,3,15,12,4,7,13])
Serie_1

Ses=Serie_1.reindex(range(10))#[0,1,2,3,4,5,6,7,8,9]) 
Ses<jupyter_output><empty_output><jupyter_text>La función nos permite hacer generar un objeto nuevo el cual tiene el orden y los datos deseados, a los datos que no se encuentren en el objeto original se les asigna `NaN`. Si los datos tienen cierto orden, es posible hacer una interpolación y determijnar un valor para los datos faltantes.<jupyter_code>Ses=pd.Series(['rojo','rojo','rojo','rojo','rojo','rojo','azul','azul','azul','azul','azul',
           'verde', 'verde', 'verde', 'verde', 'verde', 'verde', 'verde', 'verde', 'verde'], 
          index =[2,3,5,6,8,9,11,12,14,15,17,18,20,21,23,24,26,27,29,30])
Ses_2=Ses.reindex(range(5,25), method='ffill')
Ses_2<jupyter_output><empty_output><jupyter_text>Como los *dataframes* utilizan los objetos de tipo *index* tanto en el índice de los renglones como en las columnas, por lo que se puede utilizar la función en ambos casos.<jupyter_code>df_1
df_1.reindex(['Baja California', 'Ciudad de Mexico', 'Yucatan', 'Hidalgo','Nayarit'])
df_1.reindex(columns= ['Poblacion_0_4', 'Poblacion_5_9', 'Poblacion_10_14', 'Poblacion_15_19'])<jupyter_output><empty_output><jupyter_text>Aunque se tiene el método `reindex` la forma que más comunmente se utiliza es a través del método `loc`<jupyter_code>df_1.loc[['Baja California', 'Ciudad de Mexico', 'Hidalgo','Nayarit'], ['Poblacion_0_4', 'Poblacion_5_9', 'Poblacion_10_14']]<jupyter_output><empty_output><jupyter_text>El método `loc` nos permite tener acceso a los datos asociados a los índices que se deseen (renglones o columnas) usadno `[]` para tal propósito, esta forma es de acuerdo al método para acceder a los elementos en una matiz de *Numpy*. Al utilizar listas de índices el método regresa un *dataframe*, si lo que se desea es tener acceso a un valor específico dentro del *dataframe* se utiliza la pareja índice-columna dentro de los `[]`.<jupyter_code>df_1.loc['Nayarit','Poblacion']<jupyter_output><empty_output><jupyter_text>Otro método para seleccionar elementos de forma especifica es a través de `iloc`, el cual utiliza los índices enteros en lugar de los índices definidos por el usuario, esta forma de acceso nos permite usar las notaciones clásicas de Python para tomar intervalos especificos. Por defecto sí se pasa sólo un entero toma como al entero dentro de los índices de los renglones<jupyter_code>df_1.iloc[2:4,2:5]

df_1.iloc[4]<jupyter_output><empty_output><jupyter_text>EliminarPara eliminar renglones se puede hacer utilizando el método `drop`, recordemos que la mayoría de los metodos no modifica el objeto original como es el caso de este método, si se desea modificar el objeto original es necesario pasar como argumento `inplace=True`. Por defecto `drop` elimina los índices en los renglones del *dataframe* para eliminar elementos en las columnas es necesario pasar como argumento `axis=1` el valor que toma por defecto es 0.<jupyter_code>df_1.drop(['Nayarit', 'Ciudad de Mexico'])
df_1.drop('Nayarit')
df_1.drop('Poblacion', axis=1)<jupyter_output><empty_output><jupyter_text>Inspeccionar, Seleccionar y Filtrar.Dos métodos que nos serán de gran utilidad para inspeccionar tanto las *series* como los *dataframes* son `head` y `tail`. `head` nos muestran los primeros cinco elementos de la *serie* o los cinco primeros renglones del *dataframe*, mientras que `tail` los últimos cinco elementos o renglones según sea el caso. Si se desea ver más elementos se puede pasar un entero $n$ positivo para mostrar los primeros o últimos elementos segun sea el caso.<jupyter_code>import numpy as np
np.random.seed(15)
df_5= pd.DataFrame(np.random.rand(15,15))
df_5.head()<jupyter_output><empty_output><jupyter_text>Nos interesa hacer un manejo de los datos utilizando las *series* y los *dataframes*, una posibilidad es usando el método `reindex` visto anteriormente, sin embargo para nuestra facilidad los creadores de *Pandas* nos permiten seleccionar y filtrar los datos utilizando los `[]` para seleccionar los índices. Por defecto los `[]` toman como valor índices sobre las columnas, a estos podemos pasar una o una lista de columnas. En el primer caso el objeto que se regresa es una serie, mientras que si pasamos una lista entonces lo que se regresa es un *dataframe*.<jupyter_code>df_1['Poblacion_10_14']
type(df_1['Poblacion_10_14'])
df_1[['Poblacion_10_14']]
type(df_1[['Poblacion_10_14']])<jupyter_output><empty_output><jupyter_text>Es posible definir intervalos en los índices en los renglones<jupyter_code>df_1['Ciudad de Mexico':'Morelos']<jupyter_output><empty_output><jupyter_text>En el caso de las columnas es necesario tomar una lista y pasar como argumento<jupyter_code>df_5[[4,6,8,10,12]]<jupyter_output><empty_output><jupyter_text>Se pueden declarar columnas nuevas pasando el nombre de la columna y dando una *serie*, si se desea que toda la columna tenga un valor se le designa con `=`. Usando la misma notación es posible cambiar todos los valores de una columna.<jupyter_code>df_5['entero']=1
df_5.head()
df_5[0]=2
df_5.head(10)<jupyter_output><empty_output><jupyter_text>Una de las herramientas más poderosas de *pandas* es poder hacer un filtrado de los datos a partir de comparaciones lógicas, las cuales nos regresan vectores booleanos.<jupyter_code>df_5[df_5>.5]<jupyter_output><empty_output><jupyter_text>Si nos interesa una columna en particular y sólo tomar elementos que cumplan con una condición dada se hace de forma similar.<jupyter_code>df_5[df_5[1]>0.5]<jupyter_output><empty_output><jupyter_text>O podemos hacer condiciones sobre las propias columnas del *dataframe*<jupyter_code>df_1[df_1['Poblacion_10_14']< df_1['Poblacion_5_9']]<jupyter_output><empty_output><jupyter_text>Otra forma de uso de para tener acceso a las columnas en los *dataframes* es usar la notación de una instancia dentro de un objeto, esto se hace a traves de `.`<jupyter_code>df_1.Poblacion
df_1[df_1.Poblacion > 1000000]<jupyter_output><empty_output><jupyter_text>Estas formas de seleccionar elementos dentro del *dataframe* se basan en la forma de seleccionar elementos de las matrices de *Numpy*, los desarrolladores de *pandas* implementaron los métodos `loc` y `iloc` para poder seleccionar los elementos dentro de los *dataframes* generando así múltiples formas para la selección de elementos. Más adelante en el curso se verán otras formas de selección para los caso con múltiples índices jerárquicos.En la siguiente tabla resumimos algunas formas para la seleccion de los datos|Tipo de llamado | Descripción||----------------|--------------||`df.loc[val]`| Se selecciona un renglón o un subconjunto de renglones usando el índice ||`df.loc[:,val]`| Selecciona una sola columna o un conjunto de columnas||`df[val]`| Se selecciona una columna del *dataframe*; si *val* es un arreglo boleano de 1D filtra renglones y si es *val*  es un arreglo 2D se puede usar para designar valores||`df.iloc[val]`| Selecciona la el renglón o un subconjunto de renglones usando la posición con enteros||`df.iloc[:,val]`| Selección de una columna o múltiples columnas usando la posición por enteros||`df.iloc[val1,val2]`| Selección de renglón y columna usando enteros||`df.at[val1,val2]`| Seleccionar un sólo valor usando las etiquedas del índice tanto en renglones como en columnas||`df.get(val)`|Regresa la columna *val* esta en los índices de las columnas|  Operaciones aritméticas en *Dataframes*Como ya vimos es posible hacer operacións aritméticas en las *series* (sumar, restar, multiplicar por escalares multiplicar elemento a elemento )<jupyter_code>Serie_5
Serie_2
Serie_5+ Serie_2
Serie_5*Serie_2
Serie_5/Serie_2<jupyter_output><empty_output><jupyter_text>Pero como sabemos cuando los índices no son los mismos entonces por defecto se coloca `NaN` esto lleva a múltiples problemas si se hacen mas de una operación operaciones. En el caso de los *dataframes* los índices deben de coincidir tanto en renglones, como en columnas para que las operaciones se lleven acabo.<jupyter_code>df_6= pd.DataFrame(np.random.randint(20,size=(7,5)), columns= list('efghi') , index= ['Ale', 'Ivan', 'Mariana', 'Diana', 'Susana', 'Liliana', 'Juan'])
df_7= pd.DataFrame(np.random.randint(20,size=(7,5)), columns= list('bcdef'),  index= ['Ale', 'Ivan',  'Susana', 'Juan', 'Tania', 'Jose', 'Rosa'])
df_6 + df_7<jupyter_output><empty_output><jupyter_text>Es claro que si se continua haciendo operaciones el número de `NaN` aumente en muchos esenarios esto no es un comportamiento se dese, por tal motivo *pandas*  ofrece una salida ante esta problemática usando los métodos pre establecidos como`add` (para la suma) para que en caso de que los índices no se encuentren en en segundo *dataframe* entonces asignar un valor determiando como $0.0$.<jupyter_code>df_6.add( df_7, fill_value = 0.0)<jupyter_output><empty_output><jupyter_text>Es claro que no se elimina la problemática en su totalidad, pero nos permite tener un mejor manejo de como hacer las operaciones entre *dataframes*, en particular el método suma a los valores que se encuentran en los dos *dataframes*, si sólo se encuentran en un *dataframe* hace la operación con el *fill_value*. Como se observa se mantiene el hecho que si el valor no se encuentra dentro de cierta columna, entonces en la suma el valos se mantiene sin un valor específico, esto es importante pues se mantiene la consistencia de los datos. A continuación mostramos una tabla con los métodos para hacer operaciones aritméticas.|Método| Operación||--------|---------||`add`, `radd`| Suma (+)||`sub`, `rsub`| Resta (-)||`div`, `rdiv`| División (/)||`floordv`,`rfloordiv`|División entera (//)||`mul`, `rmul`| Multiplicación (*)||`pow`, `rpow`| Exponente(**)|   Operaciones entre *Dataframes* y *Series**Pandas* nos permite hacer operaciones entre *dataframes* y *series*, es común hacer operaciones como restar un elemento (renglón) a un *dataframe* generando un *dataframe*, por tal motivo este tipo de operaciónes estan implementadas<jupyter_code>df_7-df_7.iloc[0]<jupyter_output><empty_output><jupyter_text>cuando los índices de una *serie* se encuentrán dentro de las columnas del *dataframe* la operación se hace, en caso de los índices no se encuentren el resultado de la operación es `NaN`. El *dataframe* resultante tiene como columnas a la unión de los índices de la *serie* y las columnas del *dataframe* original.<jupyter_code>df_7-df_6.iloc[0]<jupyter_output><empty_output><jupyter_text>Si se desea hacer una operación con las columnas es necesario usar los métodos implementados dentro de *pandas* para tal motivo, por defecto la operación se hace sobre las columnas por lo que para que nos permita hacer la operación es necesario pasar como argumento `axis='index'` o `index=0`.<jupyter_code>df_6.add(df_6['f'] , axis='index')<jupyter_output><empty_output><jupyter_text>Aplicación de funciones y mapeos Como hemos visto en *Pandas* se han implementado operaciones básica para hacer uso dentro de los *dataframes*, las cuales normalmente son elemento a elemento. Es común hacer operaciones como ordenar los datos o aplicar funciones estadísticas básicas dentro del manejo de datos, por lo cual los desarrolladodes de *Pandas* han tiene implementado muchas de estas funciones.<jupyter_code>df_8 = pd.DataFrame(np.random.randn(7, 5), columns=list('jmlkn'),
                    index= ['Ale', 'Ivan',  'Susana', 'Juan', 'Tania', 'Jose', 'Rosa'])
df_8
df_8.abs()<jupyter_output><empty_output><jupyter_text>El método `abs` toma la función *valor absoluto* $| |:\mathbb{R} \rightarrow \mathbb{R}^+$ elemento a elemento. Otro método es el de `sum` el cual regresa una serie donde los elementos son la suma ya sea de renglones o columnas, por defecto el método toma las columnas para tomar los renglones se le pasa como parámetro `axis= 1`.<jupyter_code>df_8.sum()
df_8.sum(axis=1)<jupyter_output><empty_output><jupyter_text>Para ordenar los datos a partir de su índice se utiliza el método `sort_index` el cual ordena los datos utilizando el índice en los renglones y si se desea ordenar los índices para las columnas se le pasa el argumento `axis=1` para las columnas.<jupyter_code>df_8.sort_index()
df_8.sort_index(axis=1)<jupyter_output><empty_output><jupyter_text>Si se desea ordenar los datos usando alguna columna en especifico se utiliza `sort_values`, y se utiliza el argumento `ascending= False` para tomarlo de forma descendiente.<jupyter_code>df_8.sort_values('j', ascending=False)<jupyter_output><empty_output><jupyter_text>Para obtener la suma acumulada se utiliza el método `cumsum`  el comportamiento por defecto es tomar la suma tomando una columna fija e ir sumando para cada índice, tomar fijo el índice se pasa como argumento `axis=1`.<jupyter_code>df_8.cumsum(axis=1)<jupyter_output><empty_output><jupyter_text>Otros métodos que se consideran de utilidad son `max` y `min` que son la función de máximo y mínimo respectivamente<jupyter_code>df_8.min()
df_8.min(axis=1)<jupyter_output><empty_output><jupyter_text>Al regresar una *Serie*, se puede aplicar los métodos que se tengan implementados para las *Series* en una misma execución<jupyter_code>df_8.min().max()<jupyter_output><empty_output><jupyter_text>Aunque saber el valor de estas funciones nos puede resultar de utilidad, en muchos casos lo que se desea saber es "quien" es el que tiene tal mínimo (o máximo). Para tal motivo están implementados los métodos `idmax` y `idmin` que regresan una *serie* con los indices mínimos ( o máximos ) para cada columna (o renglón con `axis=1` como parámetro)<jupyter_code>df_8.idxmin()<jupyter_output><empty_output><jupyter_text>Si lo que se desea es saber el lugar que ocupa cada índice (reglón) con respecto a como se ordenan los datos en los diferentes tipos de datos (columnas o renglones) se puede utilizar el método `rank`<jupyter_code>df_8.rank()
df_8.rank(axis=1)<jupyter_output><empty_output><jupyter_text>Los métodos descritos con anterioridad pueden ser usados para hacer operacions más complejas, las operaciones como promedio (`mean`), desviación estandar `std` o covarianza `cov` también se encuentran implementadas. Para tener una descripción estadística básica del conjunto de datos podemos utilizar el método `describe`.<jupyter_code>df_8.describe()<jupyter_output><empty_output><jupyter_text>Si lo que se encuentra dentro de oun dataframe no son números si no strings `describe`regresa las estadísticas básicas relacionadas a las cadenas.<jupyter_code>df_9 = pd.DataFrame([pd.Series(['a', 'a', 'b', 'c'] * 4),
        pd.Series(['hg', 'jg', 'lk', 'jg'] * 4),
        pd.Series(['qhg', 'rjg', 'tlk', 'ljh'] * 4), 
        pd.Series(['ashg', 'tylk', 'tylk', 'tylk'] * 4)]
        , index = ['una', 'dos', 'tres', 'cuatro']).T
df_9.describe()<jupyter_output><empty_output><jupyter_text>La siguiente es una lista con los métos estadisticos que se encuentran disponibles junto con su descripción|Método|Descripción||------|-----------||`count`             | Número de de valores distintos a `NaN`.||`describe`          | Calcula un conjunto de estadisticas para laas *Series* o los *Dataframes*. ||`min max`           | Calcula el mínimo y el máximo. ||`idxmin`, `idxgmax` | Calcula el índice en el cual se encuentran el mínimo o el máximo. ||`quantile`          | Calcula el cuantil que se solicite sin espesificar toma .5 . ||`sum`               | La suma de los valores. ||`mean`              | El promedio. ||`median`            | La mediana (50% quantile) de los valores.||`mad`               | La desviacion promedio del valor promedio. ||`prod`              | El producto de todos los valores. ||`var`               | La varianza. ||`std`               | La desviación estandar. ||`cumsum`            | La suma acumulada de los valores.| |`cummin`, `cummax`  | El valor acumulado mínimo o máximo respectivamente.| |`cumprod`           | El producto acumulado. ||`diff`              | La primera diferencia. ||`pct_change`        | Calcula el cambio en los porcentajes. | Durante el curso se verán más funciones estadísticas que ya se en cuentran implementadas dentro de los *dataframes* en *Pandas* como el cálculo de la covarianza y la correlación que se encuentran implementadas usando los métodos `cov` y `corr`.   Aplicacción de funciones Muchas veces dentro del ánalisis de datos es necesario aplicar funciones que no se encuentran implementadas o funciones definidas por el usuario a valores de los datos. El método `apply`  que se encuentra implementado dentro de los *dataframes* y las *series*, nos permite aplicar funciones definidas por el usuario o bibliotecas especializadas a los elementos dentro de los *dataframes* o las *series*, las cual nos pueden servir para distintos usos como seleccionar elementos, aplicación de modelos matemáticos, etc. El método `apply` nos permite utilizar el cálculo $\lambda$ (*lambda*) para aplicar funciones, las cuales pueden se simpĺes o complejas, que dependan sólo de un elemento, varios elementos o de todos los elementos, etc.  Dependiendo de lo que se requiera se puede controlar el modo en el cual se aplica una función definida por el usuario a los elementos del dataframe. Si se desea aplicar la misma función a todos los elementos del *dataframe* y la función no depende de otro paramétros se utiliza el método directamente.<jupyter_code>df_8.apply(lambda x: .5*x +4 )<jupyter_output><empty_output><jupyter_text>Si se desea aplicar haciendo una iteración sobre un eje en particular, se le pasa el parámetro `axis`. Si se utiliza más de un parámetro dentro de la iteración, se puede acceder a este usando la notación de diccionario de python (`x[]`) o bien como si fuera una instancia dentro de un objeto (`x.`)<jupyter_code>df_8.apply(lambda x: .5*x['j'] +4 - x['m'] , axis = 1)
df_8.apply(lambda x: .5*x.j +4 - x.m , axis = 1)<jupyter_output><empty_output><jupyter_text>Más adelante se ampliará sobre el uso del método `apply` para los distintos usos especificos, la intención es que se conozca sus implementaciones básicas para empezar a familiarizarse con su uso. Observaciones GeneralesLas *Series* y los *Dataframes* son herramientas muy poderosas que se encuentran a nuestra disposición dentro de la biblioteca *Pandas*, estas estructuras nos permiten hacer un manejo de diferentes tipos de datos y tener una relación valor-índice. A partir de esta relación es posible seleccionar, cortar, filtrar, etc los datos que se encuentran dentro de los *dataframes* o las *series*.<jupyter_code><jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08974504894149783, 'fraction_numerical': 0.026633280218529478, 'mean_word_length': 5.891176470588236, 'pattern_counts': {'":': 0, '<': 272, '<?xml version=': 0, '>': 276, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '65', 'dir': 'jupyter-structured-clean-dedup', 'id': '532131', 'license': 'Apache-2.0', 'n_tokens_mistral': 12687, 'n_tokens_neox': 12292, 'n_words': 4346, 'path': 'Series_Dataframes.ipynb', 'repo_name': 'msalazarcgeo/Curso_intro_Prog_Ana_Dat', 'size': '35182'}